userver: cmake/UserverTestsuite.cmake
Loading...
Searching...
No Matches
cmake/UserverTestsuite.cmake
# Functions for setting up Python virtual environments and running
# testsuite tests.
#
# Provides:
# - USERVER_PYTHON_PATH option
# - USERVER_FEATURE_TESTSUITE option
# - USERVER_PIP_USE_SYSTEM_PACKAGES option
# - USERVER_PIP_OPTIONS option
# - userver_venv_setup function that sets up a Python virtual environment
# with the given requirements
# - userver_testsuite_requirements function that returns a list of requirements
# files needed to run userver testsuite
# - userver_testsuite_add function that registers a directory with testsuite
# tests in ctest. Note that userver testsuite requires some arguments, they
# should be passed manually using PYTEST_ARGS
# - userver_testsuite_add_simple that automatically detects and fills in some
# PYTEST_ARGS
#
# Implementation note: public functions here should be usable even without
# a direct include of this script, so the functions should not rely
# on non-cache variables being present.
include_guard(GLOBAL)
function(_userver_prepare_testsuite)
set(USERVER_PYTHON_PATH "python3" CACHE FILEPATH "Path to python3 executable to use")
message(STATUS "Python: ${USERVER_PYTHON_PATH}")
option(USERVER_FEATURE_TESTSUITE "Enable functional tests via testsuite" ON)
option(
USERVER_PIP_USE_SYSTEM_PACKAGES
"Use system python packages inside venv"
OFF
)
set(USERVER_PIP_OPTIONS "" CACHE STRING "Options for all pip calls")
if(USERVER_FEATURE_TESTSUITE AND NOT USERVER_PYTHON_DEV_CHECKED)
# find package python3-dev required by venv
execute_process(
COMMAND sh "-c" "command -v python3-config"
OUTPUT_VARIABLE PYTHONCONFIG_FOUND
)
if(NOT PYTHONCONFIG_FOUND)
message(FATAL_ERROR "Python dev is not found")
endif()
set(USERVER_PYTHON_DEV_CHECKED TRUE CACHE INTERNAL "")
endif()
if(NOT USERVER_TESTSUITE_DIR)
get_filename_component(
USERVER_TESTSUITE_DIR "${CMAKE_CURRENT_LIST_DIR}/../testsuite" ABSOLUTE)
endif()
set_property(GLOBAL PROPERTY userver_testsuite_dir "${USERVER_TESTSUITE_DIR}")
endfunction()
_userver_prepare_testsuite()
function(userver_venv_setup)
set(options UNIQUE)
set(oneValueArgs NAME PYTHON_OUTPUT_VAR)
set(multiValueArgs REQUIREMENTS PIP_ARGS)
cmake_parse_arguments(
ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}")
if(NOT ARG_REQUIREMENTS)
message(FATAL_ERROR "No REQUIREMENTS given for venv")
return()
endif()
if(NOT ARG_NAME)
set(venv_name "venv")
set(python_output_var "TESTSUITE_VENV_PYTHON")
else()
set(venv_name "venv-${ARG_NAME}")
string(TOUPPER "TESTSUITE_VENV_${ARG_NAME}_PYTHON" python_output_var)
endif()
if(ARG_PYTHON_OUTPUT_VAR)
set(python_output_var "${ARG_PYTHON_OUTPUT_VAR}")
endif()
if(ARG_UNIQUE)
set(parent_directory "${CMAKE_BINARY_DIR}")
else()
set(parent_directory "${CMAKE_CURRENT_BINARY_DIR}")
endif()
set(venv_additional_args)
if(USERVER_PIP_USE_SYSTEM_PACKAGES)
list(APPEND venv_additional_args "--system-site-packages")
endif()
list(APPEND ARG_PIP_ARGS ${USERVER_PIP_OPTIONS})
set(venv_dir "${parent_directory}/${venv_name}")
set(venv_bin_dir "${venv_dir}/bin")
set("${python_output_var}" "${venv_bin_dir}/python" PARENT_SCOPE)
# A unique venv is set up once for the whole build.
# For example, a userver gRPC cmake script may be included multiple times
# during the Configure, but only 1 venv should be created.
# Global properties are used to check that all userver_venv_setup
# for a given venv are invoked with the same params.
if(ARG_UNIQUE)
set(venv_unique_params
venv ${ARG_REQUIREMENTS} ${venv_additional_args} ${ARG_PIP_ARGS})
get_property(cached_venv_unique_params
GLOBAL PROPERTY "userver-venv-${ARG_NAME}-params")
if(cached_venv_unique_params)
if(NOT cached_venv_unique_params STREQUAL venv_unique_params)
message(FATAL_ERROR
"Unique venv '${ARG_NAME}' is created multiple times with "
"different params, "
"before='${cached_venv_unique_params}' "
"after='${venv_unique_params}'")
endif()
return()
endif()
endif()
message(STATUS "Setting up the venv at ${venv_dir}")
if(NOT EXISTS "${venv_dir}")
execute_process(
COMMAND
"${USERVER_PYTHON_PATH}"
-m venv
"${venv_dir}"
${venv_additional_args}
RESULT_VARIABLE status
)
if(status)
file(REMOVE_RECURSE "${venv_dir}")
message(FATAL_ERROR
"Failed to create Python virtual environment. "
"On Debian-based systems, venv is installed separately:\n"
"sudo apt install python3-venv"
)
endif()
endif()
# If pip has already installed packages using the same requirements,
# then don't run it again. This optimization dramatically reduces
# re-Configure times.
set(venv_params "")
set(format_version 2)
string(APPEND venv_params "format-version=${format_version}\n")
string(APPEND venv_params "pip-args=${ARG_PIP_ARGS}\n")
foreach(requirement IN LISTS ARG_REQUIREMENTS)
file(READ "${requirement}" requirement_contents)
if(NOT requirement_contents MATCHES "\n$")
message(FATAL_ERROR "venv requirements file must end with a newline")
endif()
string(APPEND venv_params "${requirement_contents}")
endforeach()
set(should_run_pip TRUE)
set(venv_params_file "${venv_dir}/venv-params.txt")
if(EXISTS "${venv_params_file}")
file(READ "${venv_params_file}" venv_params_old)
if(venv_params_old STREQUAL venv_params)
set(should_run_pip FALSE)
endif()
endif()
if(should_run_pip)
message(STATUS "Installing requirements:")
foreach(requirement IN LISTS ARG_REQUIREMENTS)
message(STATUS " ${requirement}")
endforeach()
list(
TRANSFORM ARG_REQUIREMENTS
PREPEND "--requirement="
OUTPUT_VARIABLE pip_requirements
)
execute_process(
COMMAND
"${venv_bin_dir}/python3" -m pip install
--disable-pip-version-check
-U ${pip_requirements}
${ARG_PIP_ARGS}
RESULT_VARIABLE status
)
if(status)
message(FATAL_ERROR "Failed to install venv requirements")
endif()
file(WRITE "${venv_params_file}" "${venv_params}")
endif()
if(ARG_UNIQUE)
set_property(GLOBAL PROPERTY "userver-venv-${ARG_NAME}-params"
${venv_unique_params})
endif()
endfunction()
function(userver_testsuite_requirements)
set(options)
set(oneValueArgs REQUIREMENT_FILES_VAR)
set(multiValueArgs)
cmake_parse_arguments(
ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}")
get_property(USERVER_TESTSUITE_DIR GLOBAL PROPERTY userver_testsuite_dir)
list(APPEND requirements_files
"${USERVER_TESTSUITE_DIR}/requirements.txt")
if(USERVER_FEATURE_GRPC OR TARGET userver::grpc)
get_property(protobuf_category
GLOBAL PROPERTY userver_protobuf_version_category)
if(NOT protobuf_category)
include(SetupProtobuf)
get_property(protobuf_category
GLOBAL PROPERTY userver_protobuf_version_category)
endif()
list(APPEND requirements_files
"${USERVER_TESTSUITE_DIR}/requirements-grpc-${protobuf_category}.txt")
endif()
if(USERVER_FEATURE_MONGODB OR TARGET userver::mongo)
list(APPEND requirements_files
"${USERVER_TESTSUITE_DIR}/requirements-mongo.txt")
list(APPEND testsuite_modules mongodb)
endif()
if(USERVER_FEATURE_POSTGRESQL OR TARGET userver::postgresql)
list(APPEND requirements_files
"${USERVER_TESTSUITE_DIR}/requirements-postgres.txt")
if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
list(APPEND testsuite_modules postgresql-binary)
else()
list(APPEND testsuite_modules postgresql)
endif()
endif()
if(USERVER_FEATURE_YDB OR TARGET userver::ydb)
list(APPEND requirements_files
"${USERVER_TESTSUITE_DIR}/requirements-ydb.txt")
endif()
if(USERVER_FEATURE_REDIS OR TARGET userver::redis)
list(APPEND requirements_files
"${USERVER_TESTSUITE_DIR}/requirements-redis.txt")
list(APPEND testsuite_modules redis)
endif()
if(USERVER_FEATURE_CLICKHOUSE OR TARGET userver::clickhouse)
list(APPEND testsuite_modules clickhouse)
endif()
if(USERVER_FEATURE_RABBITMQ OR TARGET userver::rabbitmq)
list(APPEND testsuite_modules rabbitmq)
endif()
if(USERVER_FEATURE_MYSQL OR TARGET userver::mysql)
list(APPEND testsuite_modules mysql)
endif()
# This function returns "public" dependencies for userver-based services.
# For private dependencies that only userver's own tests need, see
# SetupUserverTestsuiteEnv.cmake
file(READ "${USERVER_TESTSUITE_DIR}/requirements-testsuite.txt"
requirements_testsuite_text)
if(testsuite_modules)
list(JOIN testsuite_modules "," testsuite_modules_str)
string(
REPLACE
"yandex-taxi-testsuite[]"
"yandex-taxi-testsuite[${testsuite_modules_str}]"
requirements_testsuite_text
"${requirements_testsuite_text}"
)
endif()
set(requirements_testsuite_file
"${CMAKE_BINARY_DIR}/requirements-userver-testsuite.txt")
file(WRITE "${requirements_testsuite_file}" "${requirements_testsuite_text}")
list(APPEND requirements_files "${requirements_testsuite_file}")
set("${ARG_REQUIREMENT_FILES_VAR}" ${requirements_files} PARENT_SCOPE)
endfunction()
function(userver_testsuite_add)
set(oneValueArgs
SERVICE_TARGET
WORKING_DIRECTORY
PYTHON_BINARY
PRETTY_LOGS
)
set(multiValueArgs
PYTEST_ARGS
REQUIREMENTS
PYTHONPATH
)
cmake_parse_arguments(
ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
include(CTest)
get_property(USERVER_TESTSUITE_DIR GLOBAL PROPERTY userver_testsuite_dir)
if (NOT ARG_SERVICE_TARGET)
message(FATAL_ERROR "No SERVICE_TARGET given for testsuite")
return()
endif()
if (NOT ARG_WORKING_DIRECTORY)
set(ARG_WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
endif()
if (NOT DEFINED ARG_PRETTY_LOGS)
set(ARG_PRETTY_LOGS ON)
endif()
set(TESTSUITE_TARGET "testsuite-${ARG_SERVICE_TARGET}")
if (NOT USERVER_FEATURE_TESTSUITE)
message(STATUS "Testsuite target ${TESTSUITE_TARGET} is disabled")
return()
endif()
if(ARG_PYTHON_BINARY)
if(ARG_REQUIREMENTS)
message(FATAL_ERROR
"PYTHON_BINARY and REQUIREMENTS options are incompatible")
endif()
set(python_binary "${ARG_PYTHON_BINARY}")
else()
userver_testsuite_requirements(REQUIREMENT_FILES_VAR requirement_files)
list(APPEND requirement_files ${ARG_REQUIREMENTS})
userver_venv_setup(
NAME "${TESTSUITE_TARGET}"
REQUIREMENTS ${requirement_files}
PYTHON_OUTPUT_VAR python_binary
)
endif()
if(NOT python_binary)
message(FATAL_ERROR "No python binary given.")
endif()
set(TESTSUITE_RUNNER "${CMAKE_CURRENT_BINARY_DIR}/runtests-${TESTSUITE_TARGET}")
list(APPEND ARG_PYTHONPATH ${USERVER_TESTSUITE_DIR}/pytest_plugins)
execute_process(
COMMAND
"${python_binary}" ${USERVER_TESTSUITE_DIR}/create_runner.py
-o ${TESTSUITE_RUNNER}
--python=${python_binary}
"--python-path=${ARG_PYTHONPATH}"
--
--build-dir=${CMAKE_BINARY_DIR}
${ARG_PYTEST_ARGS}
RESULT_VARIABLE STATUS
)
if (STATUS)
message(FATAL_ERROR "Failed to create testsuite runner")
endif()
set(PRETTY_LOGS_MODE "")
if (ARG_PRETTY_LOGS)
set(PRETTY_LOGS_MODE "--service-logs-pretty")
endif()
# Without WORKING_DIRECTORY the `add_test` prints better diagnostic info
add_test(
NAME "${TESTSUITE_TARGET}"
COMMAND
"${TESTSUITE_RUNNER}"
${PRETTY_LOGS_MODE}
-vv
"${ARG_WORKING_DIRECTORY}"
)
add_custom_target(
"start-${ARG_SERVICE_TARGET}"
COMMAND
"${TESTSUITE_RUNNER}"
${PRETTY_LOGS_MODE}
--service-runner-mode
-vvs
"${ARG_WORKING_DIRECTORY}"
DEPENDS
"${TESTSUITE_RUNNER}"
"${ARG_SERVICE_TARGET}"
USES_TERMINAL
)
endfunction()
# Tries to search service files in some standard places.
# Should be invoked from the service's CMakeLists.txt
# Supports the following file structure (and a few others):
# - configs/config.yaml
# - configs/config_vars.[testsuite|tests].yaml [optional]
# - configs/dynamic_config_fallback.json [optional]
# - configs/[secdist|secure_data].json [optional]
# - [testsuite|tests]/conftest.py
function(userver_testsuite_add_simple)
set(oneValueArgs
SERVICE_TARGET
WORKING_DIRECTORY
PYTHON_BINARY
PRETTY_LOGS
CONFIG_PATH
CONFIG_VARS_PATH
DYNAMIC_CONFIG_FALLBACK_PATH
SECDIST_PATH
TEST_ENV
)
set(multiValueArgs
PYTEST_ARGS
REQUIREMENTS
PYTHONPATH
)
cmake_parse_arguments(
ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
set(pytest_additional_args)
if(ARG_WORKING_DIRECTORY)
if(IS_ABSOLUTE "${ARG_WORKING_DIRECTORY}")
file(RELATIVE_PATH tests_relative_path
"${CMAKE_CURRENT_SOURCE_DIR}" "${ARG_WORKING_DIRECTORY}")
else()
set(tests_relative_path "${ARG_WORKING_DIRECTORY}")
get_filename_component(ARG_WORKING_DIRECTORY "${ARG_WORKING_DIRECTORY}"
REALPATH BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
endif()
else()
foreach(probable_tests_path IN ITEMS
"testsuite"
"tests"
"."
)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${probable_tests_path}/conftest.py")
set(ARG_WORKING_DIRECTORY
"${CMAKE_CURRENT_SOURCE_DIR}/${probable_tests_path}")
set(tests_relative_path "${probable_tests_path}")
break()
endif()
endforeach()
endif()
if(NOT ARG_SERVICE_TARGET)
if(tests_relative_path STREQUAL "." OR tests_relative_path STREQUAL "tests")
set(ARG_SERVICE_TARGET "${PROJECT_NAME}")
else()
set(ARG_SERVICE_TARGET "${PROJECT_NAME}-${tests_relative_path}")
endif()
endif()
if(ARG_CONFIG_PATH)
get_filename_component(config_path "${ARG_CONFIG_PATH}"
REALPATH BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
else()
foreach(probable_config_path IN ITEMS
"${CMAKE_CURRENT_SOURCE_DIR}/configs/static_config.yaml"
"${CMAKE_CURRENT_SOURCE_DIR}/configs/config.yaml"
"${CMAKE_CURRENT_SOURCE_DIR}/static_config.yaml"
"${CMAKE_CURRENT_SOURCE_DIR}/config.yaml"
)
if(EXISTS "${probable_config_path}")
set(config_path "${probable_config_path}")
break()
endif()
endforeach()
if(NOT config_path)
message(FATAL_ERROR
"Failed to find service static config for testsuite. "
"Please pass it to ${CMAKE_CURRENT_FUNCTION} as CONFIG_PATH arg.")
endif()
endif()
if(ARG_CONFIG_VARS_PATH)
get_filename_component(config_vars_path "${ARG_CONFIG_VARS_PATH}"
REALPATH BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
else()
foreach(probable_config_vars_path IN ITEMS
"${CMAKE_CURRENT_SOURCE_DIR}/configs/config_vars.testsuite.yaml"
"${CMAKE_CURRENT_SOURCE_DIR}/configs/config_vars.testing.yaml"
"${CMAKE_CURRENT_SOURCE_DIR}/configs/config_vars.yaml"
"${CMAKE_CURRENT_SOURCE_DIR}/config_vars.testsuite.yaml"
"${CMAKE_CURRENT_SOURCE_DIR}/config_vars.testing.yaml"
"${CMAKE_CURRENT_SOURCE_DIR}/config_vars.yaml"
)
if(EXISTS "${probable_config_vars_path}")
set(config_vars_path "${probable_config_vars_path}")
break()
endif()
endforeach()
endif()
if(config_vars_path)
list(APPEND pytest_additional_args
"--service-config-vars=${config_vars_path}")
endif()
if(ARG_DYNAMIC_CONFIG_FALLBACK_PATH)
get_filename_component(dynamic_config_fallback_path
"${ARG_DYNAMIC_CONFIG_FALLBACK_PATH}"
REALPATH BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
else()
foreach(probable_dynamic_config_fallback_path IN ITEMS
"${CMAKE_CURRENT_SOURCE_DIR}/configs/dynamic_config_fallback.json"
"${CMAKE_CURRENT_SOURCE_DIR}/dynamic_config_fallback.json"
)
if(EXISTS "${probable_dynamic_config_fallback_path}")
set(dynamic_config_fallback_path
"${probable_dynamic_config_fallback_path}")
break()
endif()
endforeach()
endif()
if(dynamic_config_fallback_path)
list(APPEND pytest_additional_args
"--config-fallback=${dynamic_config_fallback_path}")
endif()
if(ARG_SECDIST_PATH)
get_filename_component(secdist_path "${ARG_CONFIG_VARS_PATH}"
REALPATH BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
else()
foreach(probable_secdist_path IN ITEMS
"${CMAKE_CURRENT_SOURCE_DIR}/configs/secdist.json"
"${CMAKE_CURRENT_SOURCE_DIR}/configs/secure_data.json"
"${CMAKE_CURRENT_SOURCE_DIR}/secdist.json"
"${CMAKE_CURRENT_SOURCE_DIR}/secure_data.json"
)
if(EXISTS "${probable_secdist_path}")
set(secdist_path "${probable_secdist_path}")
break()
endif()
endforeach()
endif()
if(secdist_path)
list(APPEND pytest_additional_args
"--service-secdist=${secdist_path}")
endif()
if(EXISTS "${CMAKE_CURRENT_BINARY_DIR}/proto")
list(APPEND ARG_PYTHONPATH "${CMAKE_CURRENT_BINARY_DIR}/proto")
endif()
userver_testsuite_add(
SERVICE_TARGET "${ARG_SERVICE_TARGET}"
WORKING_DIRECTORY "${ARG_WORKING_DIRECTORY}"
PYTHON_BINARY "${ARG_PYTHON_BINARY}"
PRETTY_LOGS "${ARG_PRETTY_LOGS}"
PYTEST_ARGS
"--service-config=${config_path}"
"--service-source-dir=${CMAKE_CURRENT_SOURCE_DIR}"
"--service-binary=${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}"
${pytest_additional_args}
${ARG_PYTEST_ARGS}
REQUIREMENTS ${ARG_REQUIREMENTS}
PYTHONPATH ${ARG_PYTHONPATH}
)
if(ARG_TEST_ENV)
set_tests_properties("testsuite-${ARG_SERVICE_TARGET}"
PROPERTIES ENVIRONMENT ${ARG_TEST_ENV}
)
endif()
endfunction()