Implement the batch_tests feature

An approach of test batching (joining multiple tests into a single
binary) has been taken, due to long linking times/binary size on certain
platforms, including WASM. This change adds a new feature
'batch_test_support' in Qt testlib. Based on the value of the feature,
test batching may become enabled with the -batch-tests switch.

Batching works for every target added via qt_internal_add_test. When
first such target is being processed, a new combined target for all of
the future test sources is created under the name of 'test_batch'.
CMake attempts to merge the parameters of each of the tests, and some
basic checks are run for parameter differences that are impossible to
reconcile.

On the C++ level, convenience macros instantiating the tests are
redefined when batch_tests is on. The new, changed behavior triggered
by the changes in the macros registers the tests in a central test
registry, where they are available for execution based solely on their
test name. The test name is interoperable with the names CMake is aware
of, so CTest is able to run the tests one by one in the combined binary.

Task-number: QTBUG-105273
Change-Id: I2b6071d58be16979bd967eab2d405249f5a4e658
Reviewed-by: Topi Reiniö <topi.reinio@qt.io>
This commit is contained in:
Mikolaj Boc 2022-07-12 18:15:24 +02:00
parent 8446655f24
commit 8d728a0ed9
17 changed files with 457 additions and 66 deletions

View File

@ -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}"
)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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,8 +889,12 @@ endfunction()
# qt_internal_add_global_definition function for a specific 'target'.
function(qt_internal_undefine_global_definition target)
if(NOT TARGET ${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 "")
message(FATAL_ERROR "The function expects at least one definition as an argument.")

View File

@ -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}"
"$<BUILD_INTERFACE:${QT_BUILD_DIR}/include>"
)
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()
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
if (arg_SOURCES)
set(private_includes
"${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_CURRENT_BINARY_DIR}"
"$<BUILD_INTERFACE:${QT_BUILD_DIR}/include>"
${arg_INCLUDE_DIRECTORIES}
)
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$<CONFIG>.cmake")
qt_internal_create_test_script(NAME "${name}"
set(test_wrapper_file "${CMAKE_CURRENT_BINARY_DIR}/${testname}Wrapper$<CONFIG>.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 $<CONFIG>)
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()

View File

@ -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 <QList>
#include <QString>
#include <QtTest/private/qtestcase_p.h>
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;
}

View File

@ -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:<TRUE>:
@ -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

View File

@ -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

View File

@ -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 *"

View File

@ -0,0 +1 @@
qt_commandline_option(batch-tests TYPE boolean NAME batch_tests)

View File

@ -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)<TestObject>(); \
__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)

View File

@ -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 <QtTest/qtestcase.h>
#include <QtTest/private/qtestcase_p.h>
#include <QtTest/qtestassert.h>
#include <QtCore/qbytearray.h>
@ -33,6 +34,9 @@
#include <QtTest/private/qtestresult_p.h>
#include <QtTest/private/qsignaldumper_p.h>
#include <QtTest/private/qbenchmark_p.h>
#if QT_CONFIG(batch_test_support)
#include <QtTest/private/qtestregistry_p.h>
#endif // QT_CONFIG(batch_test_support)
#include <QtTest/private/cycle_p.h>
#include <QtTest/private/qtestblacklist_p.h>
#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<QString> 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

View File

@ -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,

36
src/testlib/qtestcase_p.h Normal file
View File

@ -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 <QtTest/qtestcase.h>
#include <QtTest/private/qttestexports_p.h>
#include <QtTest/qttestglobal.h>
#include <QtCore/qstring.h>
#include <QtCore/qnamespace.h>
QT_BEGIN_NAMESPACE
namespace QTest {
#if QT_CONFIG(batch_test_support)
Q_TESTLIB_PRIVATE_EXPORT QList<QString> 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

View File

@ -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 <QtTest/private/qtestregistry_p.h>
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

View File

@ -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 <QString>
#include <QtCore/qhash.h>
#include <QtTest/qttestglobal.h>
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<QString, TestEntryFunction> m_tests;
};
} // namespace QTest
QT_END_NAMESPACE
#endif // QTESTREGISTRY_P_H