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
include_guard()
include(CTest)
include(FindPython)
option(USERVER_FEATURE_TESTSUITE "Enable functional tests via testsuite" ON)
if(USERVER_FEATURE_TESTSUITE)
get_property(userver_python_dev_checked
GLOBAL PROPERTY userver_python_dev_checked)
if(NOT userver_python_dev_checked)
# find package python3-dev required by virtualenv
execute_process(
COMMAND bash "-c" "command -v python3-config"
OUTPUT_VARIABLE PYTHONCONFIG_FOUND
)
if(NOT PYTHONCONFIG_FOUND)
message(FATAL_ERROR "Python dev is not found")
endif()
set_property(GLOBAL PROPERTY userver_python_dev_checked "TRUE")
endif()
endif()
get_filename_component(
USERVER_TESTSUITE_DIR "${CMAKE_CURRENT_LIST_DIR}/../testsuite" ABSOLUTE)
function(userver_venv_setup)
set(options UNIQUE)
set(oneValueArgs NAME PYTHON_OUTPUT_VAR)
set(multiValueArgs REQUIREMENTS VIRTUALENV_ARGS 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_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} ${ARG_VIRTUALENV_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()
find_program(TESTSUITE_VIRTUALENV virtualenv)
if(NOT TESTSUITE_VIRTUALENV)
message(FATAL_ERROR
"No virtualenv binary found, try to install:\n"
"Debian: sudo apt install virtualenv\n"
"MacOS: brew install virtualenv\n"
"ArchLinux: sudo pacman -S python-virtualenv")
endif()
message(STATUS "Setting up the virtualenv at ${venv_dir}")
if(NOT EXISTS "${venv_dir}")
execute_process(
COMMAND
"${TESTSUITE_VIRTUALENV}"
"--python=${PYTHON}"
"${venv_dir}"
${ARG_VIRTUALENV_ARGS}
RESULT_VARIABLE status
)
if(status)
file(REMOVE_RECURSE "${venv_dir}")
message(FATAL_ERROR "Failed to create Python virtual environment")
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 1)
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}/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_add)
set(options)
set(oneValueArgs
SERVICE_TARGET
WORKING_DIRECTORY
PYTHON_BINARY
PRETTY_LOGS
)
set(multiValueArgs
PYTEST_ARGS
REQUIREMENTS
PYTHONPATH
VIRTUALENV_ARGS
)
cmake_parse_arguments(
ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
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_REQUIREMENTS)
userver_venv_setup(
NAME ${TESTSUITE_TARGET}
REQUIREMENTS ${ARG_REQUIREMENTS}
PYTHON_OUTPUT_VAR PYTHON_BINARY
VIRTUALENV_ARGS ${ARG_VIRTUALENV_ARGS}
)
elseif (ARG_PYTHON_BINARY)
set(PYTHON_BINARY "${ARG_PYTHON_BINARY}")
else()
set(PYTHON_BINARY "${TESTSUITE_VENV_PYTHON}")
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} --service-runner-mode ${PRETTY_LOGS_MODE} -vvs ${ARG_WORKING_DIRECTORY}
DEPENDS ${TESTSUITE_RUNNER} ${ARG_SERVICE_TARGET}
)
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
# - configs/[secdist|secure_data].json [optional]
# - [testsuite|tests]/conftest.py
function(userver_testsuite_add_simple)
set(options)
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
VIRTUALENV_ARGS
)
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(NOT dynamic_config_fallback_path)
message(FATAL_ERROR
"Failed to find dynamic_config_fallback.json for testsuite. "
"Please pass it to ${CMAKE_CURRENT_FUNCTION} "
"as DYNAMIC_CONFIG_FALLBACK_PATH arg.")
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}"
"--config-fallback=${dynamic_config_fallback_path}"
${pytest_additional_args}
${ARG_PYTEST_ARGS}
REQUIREMENTS ${ARG_REQUIREMENTS}
PYTHONPATH ${ARG_PYTHONPATH}
VIRTUALENV_ARGS ${ARG_VIRTUALENV_ARGS}
)
if(ARG_TEST_ENV)
set_tests_properties("testsuite-${ARG_SERVICE_TARGET}"
PROPERTIES ENVIRONMENT ${ARG_TEST_ENV}
)
endif()
endfunction()