diff --git a/cmake/QtBaseGlobalTargets.cmake b/cmake/QtBaseGlobalTargets.cmake index dae78ed63ba..fa16f4a70cc 100644 --- a/cmake/QtBaseGlobalTargets.cmake +++ b/cmake/QtBaseGlobalTargets.cmake @@ -286,6 +286,7 @@ qt_copy_or_install(FILES cmake/QtWriteArgsFile.cmake cmake/modulecppexports.h.in cmake/modulecppexports_p.h.in + cmake/qbatchedtestrunner.in.cpp DESTINATION "${__GlobalConfig_install_dir}" ) diff --git a/cmake/QtBuildInternalsExtra.cmake.in b/cmake/QtBuildInternalsExtra.cmake.in index 3297d85c52c..d259194ec28 100644 --- a/cmake/QtBuildInternalsExtra.cmake.in +++ b/cmake/QtBuildInternalsExtra.cmake.in @@ -87,6 +87,9 @@ set(QT_BUILD_MINIMAL_STATIC_TESTS @QT_BUILD_MINIMAL_STATIC_TESTS@ CACHE BOOL set(QT_BUILD_MINIMAL_ANDROID_MULTI_ABI_TESTS @QT_BUILD_MINIMAL_ANDROID_MULTI_ABI_TESTS@ CACHE BOOL "Build minimal subset of tests for Android multi-ABI Qt builds") +set(QT_BUILD_TESTS_BATCHED @QT_BUILD_TESTS_BATCHED@ CACHE BOOL + "Should all tests be batched into a single binary.") + set(QT_BUILD_TESTS_BY_DEFAULT @QT_BUILD_TESTS_BY_DEFAULT@ CACHE BOOL "Should tests be built as part of the default 'all' target.") set(QT_BUILD_EXAMPLES_BY_DEFAULT @QT_BUILD_EXAMPLES_BY_DEFAULT@ CACHE BOOL diff --git a/cmake/QtResourceHelpers.cmake b/cmake/QtResourceHelpers.cmake index 5d605d90a02..8a3ea8babda 100644 --- a/cmake/QtResourceHelpers.cmake +++ b/cmake/QtResourceHelpers.cmake @@ -2,6 +2,14 @@ # SPDX-License-Identifier: BSD-3-Clause function(qt_internal_add_resource target resourceName) + if(NOT TARGET "${target}") + qt_internal_is_in_test_batch(in_batch ${target}) + if(NOT in_batch) + message(FATAL_ERROR "Trying to add resource to a non-existing target \"${target}\".") + endif() + qt_internal_test_batch_target_name(target) + endif() + # Don't try to add resources when cross compiling, and the target is actually a host target # (like a tool). qt_is_imported_target("${target}" is_imported) diff --git a/cmake/QtSetup.cmake b/cmake/QtSetup.cmake index b37f765e16c..eec10bfa427 100644 --- a/cmake/QtSetup.cmake +++ b/cmake/QtSetup.cmake @@ -225,6 +225,18 @@ if(QT_BUILD_STANDALONE_TESTS) endif() set(BUILD_TESTING ${QT_BUILD_TESTS} CACHE INTERNAL "") +set(_qt_batch_tests OFF) +if(INPUT_batch_tests) + set(_qt_batch_tests ON) +endif() +option(QT_BUILD_TESTS_BATCHED "Link all tests into a single binary." ${_qt_batch_tests}) + +if(QT_BUILD_TESTS AND QT_BUILD_TESTS_BATCHED AND CMAKE_VERSION VERSION_LESS "3.18") + message(FATAL_ERROR + "Test batching requires at least CMake 3.18, due to requiring per-source " + "TARGET_DIRECTORY assignments.") +endif() + # QT_BUILD_TOOLS_WHEN_CROSSCOMPILING -> QT_FORCE_BUILD_TOOLS # pre-6.4 compatibility flag (remove sometime in the future) if(CMAKE_CROSSCOMPILING AND QT_BUILD_TOOLS_WHEN_CROSSCOMPILING) diff --git a/cmake/QtTargetHelpers.cmake b/cmake/QtTargetHelpers.cmake index cfeb969ca97..4a51849c786 100644 --- a/cmake/QtTargetHelpers.cmake +++ b/cmake/QtTargetHelpers.cmake @@ -4,15 +4,20 @@ # This function can be used to add sources/libraries/etc. to the specified CMake target # if the provided CONDITION evaluates to true. function(qt_internal_extend_target target) + if(NOT TARGET "${target}") + qt_internal_is_in_test_batch(in_batch ${target}) + if(NOT in_batch) + message(FATAL_ERROR "Trying to extend a non-existing target \"${target}\".") + endif() + qt_internal_test_batch_target_name(target) + endif() + # Don't try to extend_target when cross compiling an imported host target (like a tool). qt_is_imported_target("${target}" is_imported) if(is_imported) return() endif() - if (NOT TARGET "${target}") - message(FATAL_ERROR "Trying to extend non-existing target \"${target}\".") - endif() qt_parse_all_arguments(arg "qt_extend_target" "" "PRECOMPILED_HEADER" "CONDITION;${__default_public_args};${__default_private_args};${__default_private_module_args};COMPILE_FLAGS;NO_PCH_SOURCES" ${ARGN}) if ("x${arg_CONDITION}" STREQUAL x) @@ -884,7 +889,11 @@ endfunction() # qt_internal_add_global_definition function for a specific 'target'. function(qt_internal_undefine_global_definition target) if(NOT TARGET ${target}) - message(FATAL_ERROR "${target} is not a target.") + qt_internal_is_in_test_batch(in_batch ${target}) + if(NOT ${in_batch}) + message(FATAL_ERROR "${target} is not a target.") + endif() + qt_internal_test_batch_target_name(target) endif() if("${ARGN}" STREQUAL "") diff --git a/cmake/QtTestHelpers.cmake b/cmake/QtTestHelpers.cmake index d4f38eda690..6b90ea55554 100644 --- a/cmake/QtTestHelpers.cmake +++ b/cmake/QtTestHelpers.cmake @@ -5,6 +5,9 @@ # the binary is built under ${CMAKE_CURRENT_BINARY_DIR} and never installed. # See qt_internal_add_executable() for more details. function(qt_internal_add_benchmark target) + if(QT_BUILD_TESTS_BATCHED) + message(WARNING "Benchmarks won't be batched - unsupported (yet)") + endif() qt_parse_all_arguments(arg "qt_add_benchmark" "${__qt_internal_add_executable_optional_args}" @@ -72,6 +75,17 @@ function(qt_internal_add_benchmark target) qt_internal_add_test_finalizers("${target}") endfunction() +function(qt_internal_add_test_dependencies target) + if(QT_BUILD_TESTS_BATCHED) + qt_internal_test_batch_target_name(target) + endif() + add_dependencies(${target} ${ARGN}) +endfunction() + +function(qt_internal_test_batch_target_name out) + set(${out} "test_batch" PARENT_SCOPE) +endfunction() + # Simple wrapper around qt_internal_add_executable for manual tests which insure that # the binary is built under ${CMAKE_CURRENT_BINARY_DIR} and never installed. # See qt_internal_add_executable() for more details. @@ -192,6 +206,169 @@ function(qt_internal_setup_docker_test_fixture name) endfunction() +function(qt_internal_get_test_batch out) + get_property(batched_list GLOBAL PROPERTY _qt_batched_test_list_property) + set(${out} ${batched_list} PARENT_SCOPE) +endfunction() + +function(qt_internal_prepare_test_target_flags version_arg exceptions_text gui_text) + cmake_parse_arguments(arg "EXCEPTIONS;NO_EXCEPTIONS;GUI" "VERSION" "" ${ARGN}) + + if (arg_VERSION) + set(${version_arg} VERSION "${arg_VERSION}" PARENT_SCOPE) + endif() + + # Qt modules get compiled without exceptions enabled by default. + # However, testcases should be still built with exceptions. + set(${exceptions_text} "EXCEPTIONS" PARENT_SCOPE) + if (${arg_NO_EXCEPTIONS}) + set(${exceptions_text} "" PARENT_SCOPE) + endif() + + if (${arg_GUI}) + set(${gui_text} "GUI" PARENT_SCOPE) + endif() +endfunction() + +function(qt_internal_get_test_arg_definitions optional_args single_value_args multi_value_args) + set(${optional_args} + RUN_SERIAL + EXCEPTIONS + NO_EXCEPTIONS + GUI + QMLTEST + CATCH + LOWDPI + NO_WRAPPER + BUILTIN_TESTDATA + PARENT_SCOPE + ) + set(${single_value_args} + OUTPUT_DIRECTORY + WORKING_DIRECTORY + TIMEOUT + VERSION + PARENT_SCOPE + ) + set(${multi_value_args} + QML_IMPORTPATH + TESTDATA + QT_TEST_SERVER_LIST + ${__default_private_args} + ${__default_public_args} + PARENT_SCOPE + ) +endfunction() + +function(qt_internal_add_test_to_batch batch_name name) + qt_internal_get_test_arg_definitions(optional_args single_value_args multi_value_args) + + cmake_parse_arguments( + arg "${optional_args}" "${single_value_args}" "${multi_value_args}" ${ARGN}) + qt_internal_prepare_test_target_flags(version_arg exceptions_text gui_text ${ARGN}) + + qt_internal_test_batch_target_name(target) + + # Lazy-init the test batch + if(NOT TARGET ${target}) + qt_internal_add_executable(${target} + ${exceptions_text} + ${gui_text} + ${version_arg} + NO_INSTALL + OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/build_dir" + SOURCES "${QT_CMAKE_DIR}/qbatchedtestrunner.in.cpp" + DEFINES QTEST_BATCH_TESTS + INCLUDE_DIRECTORIES ${private_includes} + LIBRARIES ${QT_CMAKE_EXPORT_NAMESPACE}::Core + ${QT_CMAKE_EXPORT_NAMESPACE}::Test + ${QT_CMAKE_EXPORT_NAMESPACE}::TestPrivate + ) + + set_property(TARGET ${target} PROPERTY _qt_has_exceptions ${arg_EXCEPTIONS}) + set_property(TARGET ${target} PROPERTY _qt_has_gui ${arg_GUI}) + set_property(TARGET ${target} PROPERTY _qt_has_lowdpi ${arg_LOWDPI}) + set_property(TARGET ${target} PROPERTY _qt_version ${version_arg}) + else() + # Check whether the args match with the batch. Some differences between + # flags cannot be reconciled - one should not combine these tests into + # a single binary. + qt_internal_get_target_property( + batch_has_exceptions ${target} _qt_has_exceptions) + if(NOT ${batch_has_exceptions} STREQUAL ${arg_EXCEPTIONS}) + qt_internal_get_test_batch(test_batch_contents) + message(FATAL_ERROR "Conflicting exceptions declaration between test \ + batch (${test_batch_contents}) and ${name}") + endif() + qt_internal_get_target_property(batch_has_gui ${target} _qt_has_gui) + if(NOT ${batch_has_gui} STREQUAL ${arg_GUI}) + qt_internal_get_test_batch(test_batch_contents) + message(FATAL_ERROR "Conflicting gui declaration between test batch \ + (${test_batch_contents}) and ${name}") + endif() + qt_internal_get_target_property( + batch_has_lowdpi ${target} _qt_has_lowdpi) + if(NOT ${batch_has_lowdpi} STREQUAL ${arg_LOWDPI}) + qt_internal_get_test_batch(test_batch_contents) + message(FATAL_ERROR "Conflicting lowdpi declaration between test batch \ + (${test_batch_contents}) and ${name}") + endif() + qt_internal_get_target_property(batch_version ${target} _qt_version) + if(NOT "${batch_version} " STREQUAL " " AND + NOT "${version_arg} " STREQUAL " " AND + NOT "${batch_version} " STREQUAL "${version_arg} ") + qt_internal_get_test_batch(test_batch_contents) + message(FATAL_ERROR "Conflicting version declaration between test \ + batch ${test_batch_contents} (${batch_version}) and ${name} (${version_arg})") + endif() + endif() + + get_property(batched_test_list GLOBAL PROPERTY _qt_batched_test_list_property) + if(NOT batched_test_list) + set_property(GLOBAL PROPERTY _qt_batched_test_list_property "") + set(batched_test_list "") + endif() + list(PREPEND batched_test_list ${name}) + set_property(GLOBAL PROPERTY _qt_batched_test_list_property ${batched_test_list}) + + # Merge the current test with the rest of the batch + qt_internal_extend_target(${target} + INCLUDE_DIRECTORIES ${arg_INCLUDE_DIRECTORIES} + PUBLIC_LIBRARIES ${arg_PUBLIC_LIBRARIES} + LIBRARIES ${arg_LIBRARIES} + SOURCES ${arg_SOURCES} + DEFINES ${arg_DEFINES} + COMPILE_OPTIONS ${arg_COMPILE_OPTIONS} + COMPILE_FLAGS ${arg_COMPILE_FLAGS} + LINK_OPTIONS ${arg_LINK_OPTIONS} + MOC_OPTIONS ${arg_MOC_OPTIONS} + ENABLE_AUTOGEN_TOOLS ${arg_ENABLE_AUTOGEN_TOOLS} + DISABLE_AUTOGEN_TOOLS ${arg_DISABLE_AUTOGEN_TOOLS}) + + foreach(source ${arg_SOURCES}) + # We define the test name which is later used to launch this test using + # commandline parameters. Target directory is that of the target test_batch, + # otherwise the batch won't honor our choices of compile definitions. + set_source_files_properties(${source} + TARGET_DIRECTORY ${target} + PROPERTIES COMPILE_DEFINITIONS + "BATCHED_TEST_NAME=\"${name}\"") + endforeach() + set(${batch_name} ${target} PARENT_SCOPE) +endfunction() + +# Checks whether the test 'name' is present in the test batch. See QT_BUILD_TESTS_BATCHED. +# The result of the check is placed in the 'out' variable. +function(qt_internal_is_in_test_batch out name) + set(${out} FALSE PARENT_SCOPE) + if(QT_BUILD_TESTS_BATCHED) + get_property(batched_test_list GLOBAL PROPERTY _qt_batched_test_list_property) + if("${name}" IN_LIST batched_test_list) + set(${out} TRUE PARENT_SCOPE) + endif() + endif() +endfunction() + # This function creates a CMake test target with the specified name for use with CTest. # # All tests are wrapped with cmake script that supports TESTARGS and TESTRUNNER environment @@ -204,31 +381,8 @@ endfunction() # Arguments: # BUILTIN_TESTDATA the option forces adding the provided TESTDATA to resources. function(qt_internal_add_test name) - # EXCEPTIONS is a noop as they are enabled by default. - set(optional_args - RUN_SERIAL - EXCEPTIONS - NO_EXCEPTIONS - GUI - QMLTEST - CATCH - LOWDPI - NO_WRAPPER - BUILTIN_TESTDATA - ) - set(single_value_args - OUTPUT_DIRECTORY - WORKING_DIRECTORY - TIMEOUT - VERSION - ) - set(multi_value_args - QML_IMPORTPATH - TESTDATA - QT_TEST_SERVER_LIST - ${__default_private_args} - ${__default_public_args} - ) + qt_internal_get_test_arg_definitions(optional_args single_value_args multi_value_args) + qt_parse_all_arguments(arg "qt_add_test" "${optional_args}" "${single_value_args}" @@ -240,20 +394,13 @@ function(qt_internal_add_test name) set(arg_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") endif() - # Qt modules get compiled without exceptions enabled by default. - # However, testcases should be still built with exceptions. - set(exceptions_text "EXCEPTIONS") - if (${arg_NO_EXCEPTIONS}) - set(exceptions_text "") - endif() + set(private_includes + "${CMAKE_CURRENT_SOURCE_DIR}" + "${CMAKE_CURRENT_BINARY_DIR}" + "$" + ) - if (${arg_GUI}) - set(gui_text "GUI") - endif() - - if (arg_VERSION) - set(version_arg VERSION "${arg_VERSION}") - endif() + set(testname "${name}") if(arg_PUBLIC_LIBRARIES) message(WARNING @@ -261,15 +408,16 @@ function(qt_internal_add_test name) "removed in a future Qt version. Use the LIBRARIES option instead.") endif() - # Handle cases where we have a qml test without source files - if (arg_SOURCES) - set(private_includes - "${CMAKE_CURRENT_SOURCE_DIR}" - "${CMAKE_CURRENT_BINARY_DIR}" - "$" - ${arg_INCLUDE_DIRECTORIES} - ) + if(QT_BUILD_TESTS_BATCHED AND NOT arg_QMLTEST) + qt_internal_add_test_to_batch(name ${name} ${ARGN}) + elseif(arg_SOURCES) + if(QT_BUILD_TESTS_BATCHED AND arg_QMLTEST) + message(WARNING "QML tests won't be batched - unsupported (yet)") + endif() + # Handle cases where we have a qml test without source files + list(APPEND private_includes ${arg_INCLUDE_DIRECTORIES}) + qt_internal_prepare_test_target_flags(version_arg exceptions_text gui_text ${ARGN}) qt_internal_add_executable("${name}" ${exceptions_text} ${gui_text} @@ -391,16 +539,20 @@ function(qt_internal_add_test name) qt_internal_collect_command_environment(test_env_path test_env_plugin_path) if(arg_NO_WRAPPER OR QT_NO_TEST_WRAPPERS) - add_test(NAME "${name}" COMMAND ${test_executable} ${extra_test_args} + if(QT_BUILD_TESTS_BATCHED) + message(FATAL_ERROR "Wrapperless tests are unspupported with test batching") + endif() + + add_test(NAME "${testname}" COMMAND ${test_executable} ${extra_test_args} WORKING_DIRECTORY "${test_working_dir}") - set_property(TEST "${name}" APPEND PROPERTY + set_property(TEST "${testname}" APPEND PROPERTY ENVIRONMENT "PATH=${test_env_path}" "QT_TEST_RUNNING_IN_CTEST=1" "QT_PLUGIN_PATH=${test_env_plugin_path}" ) else() - set(test_wrapper_file "${CMAKE_CURRENT_BINARY_DIR}/${name}Wrapper$.cmake") - qt_internal_create_test_script(NAME "${name}" + set(test_wrapper_file "${CMAKE_CURRENT_BINARY_DIR}/${testname}Wrapper$.cmake") + qt_internal_create_test_script(NAME "${testname}" COMMAND "${test_executable}" ARGS "${extra_test_args}" WORKING_DIRECTORY "${test_working_dir}" @@ -412,12 +564,12 @@ function(qt_internal_add_test name) endif() if(arg_QT_TEST_SERVER_LIST AND NOT ANDROID) - qt_internal_setup_docker_test_fixture(${name} ${arg_QT_TEST_SERVER_LIST}) + qt_internal_setup_docker_test_fixture(${testname} ${arg_QT_TEST_SERVER_LIST}) endif() - set_tests_properties("${name}" PROPERTIES RUN_SERIAL "${arg_RUN_SERIAL}" LABELS "${label}") - if (arg_TIMEOUT) - set_tests_properties(${name} PROPERTIES TIMEOUT ${arg_TIMEOUT}) + set_tests_properties("${testname}" PROPERTIES RUN_SERIAL "${arg_RUN_SERIAL}" LABELS "${label}") + if(arg_TIMEOUT) + set_tests_properties(${testname} PROPERTIES TIMEOUT ${arg_TIMEOUT}) endif() # Add a ${target}/check makefile target, to more easily test one test. @@ -427,15 +579,15 @@ function(qt_internal_add_test name) if(is_multi_config) set(test_config_options -C $) endif() - add_custom_target("${name}_check" + add_custom_target("${testname}_check" VERBATIM COMMENT "Running ${CMAKE_CTEST_COMMAND} -V -R \"^${name}$\" ${test_config_options}" COMMAND "${CMAKE_CTEST_COMMAND}" -V -R "^${name}$" ${test_config_options} ) if(TARGET "${name}") - add_dependencies("${name}_check" "${name}") + add_dependencies("${testname}_check" "${name}") if(ANDROID) - add_dependencies("${name}_check" "${name}_make_apk") + add_dependencies("${testname}_check" "${name}_make_apk") endif() endif() @@ -466,8 +618,8 @@ function(qt_internal_add_test name) ) endforeach() - if (builtin_files) - qt_internal_add_resource(${name} "${name}_testdata_builtin" + if(builtin_files) + qt_internal_add_resource(${name} "${testname}_testdata_builtin" PREFIX "/" FILES ${builtin_files} BASE ${CMAKE_CURRENT_SOURCE_DIR}) @@ -551,6 +703,10 @@ for this function. Will be ignored") set(executable_file "${arg_COMMAND}") endif() + set(executable_name ${arg_NAME}) + if(QT_BUILD_TESTS_BATCHED) + qt_internal_test_batch_target_name(executable_name) + endif() add_test(NAME "${arg_NAME}" COMMAND "${CMAKE_COMMAND}" "-P" "${arg_OUTPUT_FILE}" WORKING_DIRECTORY "${arg_WORKING_DIRECTORY}") @@ -559,8 +715,8 @@ for this function. Will be ignored") # CROSSCOMPILING_EMULATOR don't check if actual cross compilation is configured, # emulator is prepended independently. set(crosscompiling_emulator "") - if(CMAKE_CROSSCOMPILING AND TARGET ${arg_NAME}) - get_target_property(crosscompiling_emulator ${arg_NAME} CROSSCOMPILING_EMULATOR) + if(CMAKE_CROSSCOMPILING AND TARGET ${executable_name}) + get_target_property(crosscompiling_emulator ${executable_name} CROSSCOMPILING_EMULATOR) if(NOT crosscompiling_emulator) set(crosscompiling_emulator "") else() diff --git a/cmake/qbatchedtestrunner.in.cpp b/cmake/qbatchedtestrunner.in.cpp new file mode 100644 index 00000000000..fb49cbbb988 --- /dev/null +++ b/cmake/qbatchedtestrunner.in.cpp @@ -0,0 +1,18 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include +#include +#include + +int main(int argc, char **argv) +{ + if (argc == 1) { + printf("%s\n", QTest::qGetTestCaseNames().join( + QStringLiteral(" ")).toStdString().c_str()); + return 0; + } + + const auto entryFunction = QTest::qGetTestCaseEntryFunction(QString::fromUtf8(argv[1])); + return entryFunction ? entryFunction(argc - 1, argv + 1) : -1; +} diff --git a/src/testlib/CMakeLists.txt b/src/testlib/CMakeLists.txt index 32119615419..1d5dc71460a 100644 --- a/src/testlib/CMakeLists.txt +++ b/src/testlib/CMakeLists.txt @@ -41,7 +41,7 @@ qt_internal_add_module(Test qtestaccessible.h qtestassert.h qtestblacklist.cpp qtestblacklist_p.h - qtestcase.cpp qtestcase.h + qtestcase.cpp qtestcase.h qtestcase_p.h qtestcoreelement_p.h qtestdata.cpp qtestdata.h qtestelement.cpp qtestelement_p.h @@ -74,6 +74,7 @@ qt_internal_add_module(Test PRIVATE_MODULE_INTERFACE Qt::CorePrivate GENERATE_CPP_EXPORTS + GENERATE_PRIVATE_CPP_EXPORTS ) #### Keys ignored in scope 1:.:.:testlib.pro:: @@ -90,6 +91,11 @@ qt_internal_extend_target(Test CONDITION QT_FEATURE_itemmodeltester qabstractitemmodeltester.cpp qabstractitemmodeltester.h ) +qt_internal_extend_target(Test CONDITION QT_FEATURE_batch_test_support + SOURCES + qtestregistry.cpp qtestregistry_p.h +) + qt_internal_extend_target(Test CONDITION QT_FEATURE_valgrind SOURCES 3rdparty/callgrind_p.h diff --git a/src/testlib/configure.cmake b/src/testlib/configure.cmake index d8ede189f4e..3490c648748 100644 --- a/src/testlib/configure.cmake +++ b/src/testlib/configure.cmake @@ -32,6 +32,13 @@ qt_feature("valgrind" PUBLIC PURPOSE "Profiling support with callgrind." CONDITION ( LINUX OR APPLE ) AND QT_FEATURE_process AND QT_FEATURE_regularexpression ) +qt_feature("batch_test_support" PUBLIC + LABEL "Batch tests" + PURPOSE "Allows merging of all tests into a single executable on demand" + AUTODETECT QT_BUILD_TESTS_BATCHED + ENABLE INPUT_batch_tests STREQUAL 'yes' +) qt_configure_add_summary_section(NAME "Qt Testlib") qt_configure_add_summary_entry(ARGS "itemmodeltester") +qt_configure_add_summary_entry(ARGS "batch_test_support") qt_configure_end_summary_section() # end of "Qt Testlib" section diff --git a/src/testlib/doc/qttestlib.qdocconf b/src/testlib/doc/qttestlib.qdocconf index fe80362ccab..4330fe197a8 100644 --- a/src/testlib/doc/qttestlib.qdocconf +++ b/src/testlib/doc/qttestlib.qdocconf @@ -48,6 +48,8 @@ excludedirs += ../../../examples/widgets/doc imagedirs += images +defines += QT_FEATURE_batch_test_support + # Add a thumbnail for examples that do not have images manifestmeta.thumbnail.names = "QtTestLib/Chapter *" diff --git a/src/testlib/qt_cmdline.cmake b/src/testlib/qt_cmdline.cmake index e69de29bb2d..d3601c1e396 100644 --- a/src/testlib/qt_cmdline.cmake +++ b/src/testlib/qt_cmdline.cmake @@ -0,0 +1 @@ +qt_commandline_option(batch-tests TYPE boolean NAME batch_tests) diff --git a/src/testlib/qtest.h b/src/testlib/qtest.h index f4f0ff5dfd3..841751fac13 100644 --- a/src/testlib/qtest.h +++ b/src/testlib/qtest.h @@ -582,6 +582,7 @@ struct QtCoverageScanner #define TESTLIB_SELFCOVERAGE_START(name) #endif +#if !defined(QTEST_BATCH_TESTS) // Internal (but used by some testlib selftests to hack argc and argv). // Tests should normally implement initMain() if they have set-up to do before // instantiating the test class. @@ -595,6 +596,30 @@ int main(int argc, char *argv[]) \ QTEST_SET_MAIN_SOURCE_PATH \ return QTest::qExec(&tc, argc, argv); \ } +#else +// BATCHED_TEST_NAME is defined for each test in a batch in cmake. Some odd +// targets, like snippets, don't define it though. Play safe by providing a +// default value. +#if !defined(BATCHED_TEST_NAME) +#define BATCHED_TEST_NAME "other" +#endif +#define QTEST_MAIN_WRAPPER(TestObject, ...) \ +\ +void qRegister##TestObject() \ +{ \ + auto runTest = [](int argc, char** argv) -> int { \ + TESTLIB_SELFCOVERAGE_START(TestObject) \ + QT_PREPEND_NAMESPACE(QTest::Internal::callInitMain)(); \ + __VA_ARGS__ \ + TestObject tc; \ + QTEST_SET_MAIN_SOURCE_PATH \ + return QTest::qExec(&tc, argc, argv); \ + }; \ + QTest::qRegisterTestCase(BATCHED_TEST_NAME, runTest); \ +} \ +\ +Q_CONSTRUCTOR_FUNCTION(qRegister##TestObject) +#endif // For when you don't even want a QApplication: #define QTEST_APPLESS_MAIN(TestObject) QTEST_MAIN_WRAPPER(TestObject) diff --git a/src/testlib/qtestcase.cpp b/src/testlib/qtestcase.cpp index 89530f823fe..99be5afaefa 100644 --- a/src/testlib/qtestcase.cpp +++ b/src/testlib/qtestcase.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include +#include #include #include @@ -33,6 +34,9 @@ #include #include #include +#if QT_CONFIG(batch_test_support) +#include +#endif // QT_CONFIG(batch_test_support) #include #include #if defined(HAVE_XCTEST) @@ -2388,6 +2392,32 @@ void QTest::qCleanup() #endif } +#if QT_CONFIG(batch_test_support) || defined(Q_QDOC) +/*! + Registers the test \a name, with entry function \a entryFunction, in a + central test case registry for the current binary. + + The \a name will be listed when running the batch test binary with no + parameters. Running the test binary with the argv[1] of \a name will result + in \a entryFunction being called. +*/ +void QTest::qRegisterTestCase(const QString &name, TestEntryFunction entryFunction) +{ + QTest::TestRegistry::instance()->registerTest(name, entryFunction); +} + +QList QTest::qGetTestCaseNames() +{ + return QTest::TestRegistry::instance()->getAllTestNames(); +} + +QTest::TestEntryFunction QTest::qGetTestCaseEntryFunction(const QString& name) +{ + return QTest::TestRegistry::instance()->getTestEntryFunction(name); +} + +#endif // QT_CONFIG(batch_test_support) + /*! \overload \since 4.4 diff --git a/src/testlib/qtestcase.h b/src/testlib/qtestcase.h index a0df8dd3056..cd713ceebf3 100644 --- a/src/testlib/qtestcase.h +++ b/src/testlib/qtestcase.h @@ -374,6 +374,11 @@ namespace QTest Q_TESTLIB_EXPORT int qExec(QObject *testObject, int argc = 0, char **argv = nullptr); Q_TESTLIB_EXPORT int qExec(QObject *testObject, const QStringList &arguments); +#if QT_CONFIG(batch_test_support) || defined(Q_QDOC) + using TestEntryFunction = int (*)(int, char **); + Q_TESTLIB_EXPORT void qRegisterTestCase(const QString &name, TestEntryFunction entryFunction); +#endif // QT_CONFIG(batch_test_support) + Q_TESTLIB_EXPORT void setMainSourcePath(const char *file, const char *builddir = nullptr); Q_TESTLIB_EXPORT bool qVerify(bool statement, const char *statementStr, const char *description, diff --git a/src/testlib/qtestcase_p.h b/src/testlib/qtestcase_p.h new file mode 100644 index 00000000000..91a5314f978 --- /dev/null +++ b/src/testlib/qtestcase_p.h @@ -0,0 +1,36 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QTESTCASE_P_H +#define QTESTCASE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include +#include + +#include +#include + +QT_BEGIN_NAMESPACE + +namespace QTest { +#if QT_CONFIG(batch_test_support) + Q_TESTLIB_PRIVATE_EXPORT QList qGetTestCaseNames(); + Q_TESTLIB_PRIVATE_EXPORT TestEntryFunction qGetTestCaseEntryFunction(const QString &name); +#endif // QT_CONFIG(batch_test_support) +} // namespace QTest + +QT_END_NAMESPACE + +#endif // QTESTCASE_P_H diff --git a/src/testlib/qtestregistry.cpp b/src/testlib/qtestregistry.cpp new file mode 100644 index 00000000000..ff1f8a57e63 --- /dev/null +++ b/src/testlib/qtestregistry.cpp @@ -0,0 +1,36 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include + +QT_REQUIRE_CONFIG(batch_test_support); + +QT_BEGIN_NAMESPACE + +namespace QTest { +Q_GLOBAL_STATIC(TestRegistry, g_registry); + +TestRegistry *TestRegistry::instance() +{ + return g_registry; +} + +void TestRegistry::registerTest(const QString& name, TestEntryFunction entry) +{ + m_tests.emplace(name, std::move(entry)); +} + +TestRegistry::TestEntryFunction +TestRegistry::getTestEntryFunction(const QString& name) const +{ + const auto it = m_tests.find(name); + return it != m_tests.end() ? it.value() : nullptr; +} + +QStringList TestRegistry::getAllTestNames() const +{ + return m_tests.keys(); +} +} + +QT_END_NAMESPACE diff --git a/src/testlib/qtestregistry_p.h b/src/testlib/qtestregistry_p.h new file mode 100644 index 00000000000..eab6d4356c0 --- /dev/null +++ b/src/testlib/qtestregistry_p.h @@ -0,0 +1,36 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QTESTREGISTRY_P_H +#define QTESTREGISTRY_P_H + +#include +#include +#include + +QT_REQUIRE_CONFIG(batch_test_support); + +QT_BEGIN_NAMESPACE + +namespace QTest { +class TestRegistry { +public: + using TestEntryFunction = int(*)(int argv, char** argc); + + static TestRegistry* instance(); + + void registerTest(const QString& name, TestEntryFunction data); + size_t total() const { + return m_tests.size(); + } + TestEntryFunction getTestEntryFunction(const QString& name) const; + QStringList getAllTestNames() const; + +private: + QHash m_tests; +}; +} // namespace QTest + +QT_END_NAMESPACE + +#endif // QTESTREGISTRY_P_H