userver: cmake/UserverTestsuite.cmake
⚠️ This is the documentation for an old userver version. Click here to switch to the latest version.
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages Concepts
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()