From 1b6515ebd477c0517bf2afa244a310320bd2683c Mon Sep 17 00:00:00 2001 From: Jason Turner Date: Fri, 24 Apr 2026 11:09:25 -0600 Subject: [PATCH] Fix warnings as errors for GCC ASAN+UBSAN+Release --- CMakeLists.txt | 3 + cmake/Catch.cmake | 233 +++++++++++--- cmake/CatchAddTests.cmake | 298 ++++++++++++++---- cmake/CatchShardTests.cmake | 72 +++++ cmake/CatchShardTestsImpl.cmake | 52 +++ cmake/ParseAndAddCatchTests.cmake | 275 ++++++++++------ include/chaiscript/chaiscript_defines.hpp | 4 - .../chaiscript/language/chaiscript_engine.hpp | 2 +- .../chaiscript/language/chaiscript_parser.hpp | 73 +---- include/chaiscript/utility/json.hpp | 19 +- unittests/emscripten_eval_test.cpp | 12 +- unittests/emscripten_exception_test.cpp | 5 +- 12 files changed, 753 insertions(+), 295 deletions(-) create mode 100644 cmake/CatchShardTests.cmake create mode 100644 cmake/CatchShardTestsImpl.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index aa4d9b25..c84853ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -391,6 +391,9 @@ if(BUILD_TESTING) if(NOT UNIT_TEST_LIGHT) add_library(catch2 STATIC unittests/catch_amalgamated.cpp) + if(NOT MSVC) + target_compile_options(catch2 PRIVATE -Wno-conversion -Wno-noexcept -Wno-maybe-uninitialized) + endif() add_executable(compiled_tests unittests/compiled_tests.cpp) target_link_libraries(compiled_tests catch2 ${LIBS} ${CHAISCRIPT_LIBS}) diff --git a/cmake/Catch.cmake b/cmake/Catch.cmake index 486e3233..fcdebd60 100644 --- a/cmake/Catch.cmake +++ b/cmake/Catch.cmake @@ -33,6 +33,13 @@ same as the Catch name; see also ``TEST_PREFIX`` and ``TEST_SUFFIX``. [TEST_SUFFIX suffix] [PROPERTIES name1 value1...] [TEST_LIST var] + [REPORTER reporter] + [OUTPUT_DIR dir] + [OUTPUT_PREFIX prefix] + [OUTPUT_SUFFIX suffix] + [DISCOVERY_MODE ] + [SKIP_IS_FAILURE] + [ADD_TAGS_AS_LABELS] ) ``catch_discover_tests`` sets up a post-build command on the test executable @@ -90,86 +97,222 @@ same as the Catch name; see also ``TEST_PREFIX`` and ``TEST_SUFFIX``. executable is being used in multiple calls to ``catch_discover_tests()``. Note that this variable is only available in CTest. + ``REPORTER reporter`` + Use the specified reporter when running the test case. The reporter will + be passed to the Catch executable as ``--reporter reporter``. + + ``OUTPUT_DIR dir`` + If specified, the parameter is passed along as + ``--out dir/`` to Catch executable. The actual file name is the + same as the test name. This should be used instead of + ``EXTRA_ARGS --out foo`` to avoid race conditions writing the result output + when using parallel test execution. + + ``OUTPUT_PREFIX prefix`` + May be used in conjunction with ``OUTPUT_DIR``. + If specified, ``prefix`` is added to each output file name, like so + ``--out dir/prefix``. + + ``OUTPUT_SUFFIX suffix`` + May be used in conjunction with ``OUTPUT_DIR``. + If specified, ``suffix`` is added to each output file name, like so + ``--out dir/suffix``. This can be used to add a file extension to + the output e.g. ".xml". + + ``DL_PATHS path...`` + Specifies paths that need to be set for the dynamic linker to find shared + libraries/DLLs when running the test executable (PATH/LD_LIBRARY_PATH respectively). + These paths will both be set when retrieving the list of test cases from the + test executable and when the tests are executed themselves. This requires + cmake/ctest >= 3.22. + + ``DL_FRAMEWORK_PATHS path...`` + Specifies paths that need to be set for the dynamic linker to find libraries + packaged as frameworks on Apple platforms when running the test executable + (DYLD_FRAMEWORK_PATH). These paths will both be set when retrieving the list + of test cases from the test executable and when the tests are executed themselves. + This requires cmake/ctest >= 3.22. + + ``DISCOVERY_MODE mode`` + Provides control over when ``catch_discover_tests`` performs test discovery. + By default, ``POST_BUILD`` sets up a post-build command to perform test discovery + at build time. In certain scenarios, like cross-compiling, this ``POST_BUILD`` + behavior is not desirable. By contrast, ``PRE_TEST`` delays test discovery until + just prior to test execution. This way test discovery occurs in the target environment + where the test has a better chance at finding appropriate runtime dependencies. + + ``DISCOVERY_MODE`` defaults to the value of the + ``CMAKE_CATCH_DISCOVER_TESTS_DISCOVERY_MODE`` variable if it is not passed when + calling ``catch_discover_tests``. This provides a mechanism for globally selecting + a preferred test discovery behavior without having to modify each call site. + + ``SKIP_IS_FAILURE`` + Disables skipped test detection. + + ``ADD_TAGS_AS_LABELS`` + Adds all test tags as CTest labels. + #]=======================================================================] #------------------------------------------------------------------------------ function(catch_discover_tests TARGET) + cmake_parse_arguments( "" - "" - "TEST_PREFIX;TEST_SUFFIX;WORKING_DIRECTORY;TEST_LIST" - "TEST_SPEC;EXTRA_ARGS;PROPERTIES" + "SKIP_IS_FAILURE;ADD_TAGS_AS_LABELS" + "TEST_PREFIX;TEST_SUFFIX;WORKING_DIRECTORY;TEST_LIST;REPORTER;OUTPUT_DIR;OUTPUT_PREFIX;OUTPUT_SUFFIX;DISCOVERY_MODE" + "TEST_SPEC;EXTRA_ARGS;PROPERTIES;DL_PATHS;DL_FRAMEWORK_PATHS" ${ARGN} ) + if(${CMAKE_VERSION} VERSION_LESS "3.19") + message(FATAL_ERROR "This script requires JSON support from CMake version 3.19 or greater.") + endif() + if(NOT _WORKING_DIRECTORY) set(_WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") endif() if(NOT _TEST_LIST) set(_TEST_LIST ${TARGET}_TESTS) endif() + if(_DL_PATHS AND ${CMAKE_VERSION} VERSION_LESS "3.22.0") + message(FATAL_ERROR "The DL_PATHS option requires at least cmake 3.22") + endif() + if(_DL_FRAMEWORK_PATHS AND ${CMAKE_VERSION} VERSION_LESS "3.22.0") + message(FATAL_ERROR "The DL_FRAMEWORK_PATHS option requires at least cmake 3.22") + endif() + if(NOT _DISCOVERY_MODE) + if(NOT CMAKE_CATCH_DISCOVER_TESTS_DISCOVERY_MODE) + set(CMAKE_CATCH_DISCOVER_TESTS_DISCOVERY_MODE "POST_BUILD") + endif() + set(_DISCOVERY_MODE ${CMAKE_CATCH_DISCOVER_TESTS_DISCOVERY_MODE}) + endif() + if(NOT _DISCOVERY_MODE MATCHES "^(POST_BUILD|PRE_TEST)$") + message(FATAL_ERROR "Unknown DISCOVERY_MODE: ${_DISCOVERY_MODE}") + endif() ## Generate a unique name based on the extra arguments - string(SHA1 args_hash "${_TEST_SPEC} ${_EXTRA_ARGS}") + string(SHA1 args_hash "${_TEST_SPEC} ${_EXTRA_ARGS} ${_REPORTER} ${_OUTPUT_DIR} ${_OUTPUT_PREFIX} ${_OUTPUT_SUFFIX}") string(SUBSTRING ${args_hash} 0 7 args_hash) # Define rule to generate test list for aforementioned test executable - set(ctest_include_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_include-${args_hash}.cmake") - set(ctest_tests_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_tests-${args_hash}.cmake") + set(ctest_file_base "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}-${args_hash}") + set(ctest_include_file "${ctest_file_base}_include.cmake") + set(ctest_tests_file "${ctest_file_base}_tests.cmake") + get_property(crosscompiling_emulator TARGET ${TARGET} PROPERTY CROSSCOMPILING_EMULATOR ) - add_custom_command( - TARGET ${TARGET} POST_BUILD - BYPRODUCTS "${ctest_tests_file}" - COMMAND "${CMAKE_COMMAND}" - -D "TEST_TARGET=${TARGET}" - -D "TEST_EXECUTABLE=$" - -D "TEST_EXECUTOR=${crosscompiling_emulator}" - -D "TEST_WORKING_DIR=${_WORKING_DIRECTORY}" - -D "TEST_SPEC=${_TEST_SPEC}" - -D "TEST_EXTRA_ARGS=${_EXTRA_ARGS}" - -D "TEST_PROPERTIES=${_PROPERTIES}" - -D "TEST_PREFIX=${_TEST_PREFIX}" - -D "TEST_SUFFIX=${_TEST_SUFFIX}" - -D "TEST_LIST=${_TEST_LIST}" - -D "CTEST_FILE=${ctest_tests_file}" - -P "${_CATCH_DISCOVER_TESTS_SCRIPT}" - VERBATIM - ) + if(NOT _SKIP_IS_FAILURE) + set(_PROPERTIES ${_PROPERTIES} SKIP_RETURN_CODE 4) + endif() - file(WRITE "${ctest_include_file}" - "if(EXISTS \"${ctest_tests_file}\")\n" - " include(\"${ctest_tests_file}\")\n" - "else()\n" - " add_test(${TARGET}_NOT_BUILT-${args_hash} ${TARGET}_NOT_BUILT-${args_hash})\n" - "endif()\n" - ) - - if(NOT ${CMAKE_VERSION} VERSION_LESS "3.10.0") - # Add discovered tests to directory TEST_INCLUDE_FILES - set_property(DIRECTORY - APPEND PROPERTY TEST_INCLUDE_FILES "${ctest_include_file}" + if(_DISCOVERY_MODE STREQUAL "POST_BUILD") + add_custom_command( + TARGET ${TARGET} POST_BUILD + BYPRODUCTS "${ctest_tests_file}" + COMMAND "${CMAKE_COMMAND}" + -D "TEST_TARGET=${TARGET}" + -D "TEST_EXECUTABLE=$" + -D "TEST_EXECUTOR=${crosscompiling_emulator}" + -D "TEST_WORKING_DIR=${_WORKING_DIRECTORY}" + -D "TEST_SPEC=${_TEST_SPEC}" + -D "TEST_EXTRA_ARGS=${_EXTRA_ARGS}" + -D "TEST_PROPERTIES=${_PROPERTIES}" + -D "TEST_PREFIX=${_TEST_PREFIX}" + -D "TEST_SUFFIX=${_TEST_SUFFIX}" + -D "TEST_LIST=${_TEST_LIST}" + -D "TEST_REPORTER=${_REPORTER}" + -D "TEST_OUTPUT_DIR=${_OUTPUT_DIR}" + -D "TEST_OUTPUT_PREFIX=${_OUTPUT_PREFIX}" + -D "TEST_OUTPUT_SUFFIX=${_OUTPUT_SUFFIX}" + -D "TEST_DL_PATHS=${_DL_PATHS}" + -D "TEST_DL_FRAMEWORK_PATHS=${_DL_FRAMEWORK_PATHS}" + -D "CTEST_FILE=${ctest_tests_file}" + -D "ADD_TAGS_AS_LABELS=${_ADD_TAGS_AS_LABELS}" + -P "${_CATCH_DISCOVER_TESTS_SCRIPT}" + VERBATIM ) - else() - # Add discovered tests as directory TEST_INCLUDE_FILE if possible - get_property(test_include_file_set DIRECTORY PROPERTY TEST_INCLUDE_FILE SET) - if (NOT ${test_include_file_set}) - set_property(DIRECTORY - PROPERTY TEST_INCLUDE_FILE "${ctest_include_file}" + + file(WRITE "${ctest_include_file}" + "if(EXISTS \"${ctest_tests_file}\")\n" + " include(\"${ctest_tests_file}\")\n" + "else()\n" + " add_test(${TARGET}_NOT_BUILT-${args_hash} ${TARGET}_NOT_BUILT-${args_hash})\n" + "endif()\n" + ) + + elseif(_DISCOVERY_MODE STREQUAL "PRE_TEST") + + get_property(GENERATOR_IS_MULTI_CONFIG GLOBAL + PROPERTY GENERATOR_IS_MULTI_CONFIG + ) + + if(GENERATOR_IS_MULTI_CONFIG) + set(ctest_tests_file "${ctest_file_base}_tests-$.cmake") + endif() + + string(CONCAT ctest_include_content + "if(EXISTS \"$\")" "\n" + " if(NOT EXISTS \"${ctest_tests_file}\" OR" "\n" + " NOT \"${ctest_tests_file}\" IS_NEWER_THAN \"$\" OR\n" + " NOT \"${ctest_tests_file}\" IS_NEWER_THAN \"\${CMAKE_CURRENT_LIST_FILE}\")\n" + " include(\"${_CATCH_DISCOVER_TESTS_SCRIPT}\")" "\n" + " catch_discover_tests_impl(" "\n" + " TEST_EXECUTABLE" " [==[" "$" "]==]" "\n" + " TEST_EXECUTOR" " [==[" "${crosscompiling_emulator}" "]==]" "\n" + " TEST_WORKING_DIR" " [==[" "${_WORKING_DIRECTORY}" "]==]" "\n" + " TEST_SPEC" " [==[" "${_TEST_SPEC}" "]==]" "\n" + " TEST_EXTRA_ARGS" " [==[" "${_EXTRA_ARGS}" "]==]" "\n" + " TEST_PROPERTIES" " [==[" "${_PROPERTIES}" "]==]" "\n" + " TEST_PREFIX" " [==[" "${_TEST_PREFIX}" "]==]" "\n" + " TEST_SUFFIX" " [==[" "${_TEST_SUFFIX}" "]==]" "\n" + " TEST_LIST" " [==[" "${_TEST_LIST}" "]==]" "\n" + " TEST_REPORTER" " [==[" "${_REPORTER}" "]==]" "\n" + " TEST_OUTPUT_DIR" " [==[" "${_OUTPUT_DIR}" "]==]" "\n" + " TEST_OUTPUT_PREFIX" " [==[" "${_OUTPUT_PREFIX}" "]==]" "\n" + " TEST_OUTPUT_SUFFIX" " [==[" "${_OUTPUT_SUFFIX}" "]==]" "\n" + " CTEST_FILE" " [==[" "${ctest_tests_file}" "]==]" "\n" + " TEST_DL_PATHS" " [==[" "${_DL_PATHS}" "]==]" "\n" + " TEST_DL_FRAMEWORK_PATHS" " [==[" "${_DL_FRAMEWORK_PATHS}" "]==]" "\n" + " ADD_TAGS_AS_LABELS" " [==[" "${_ADD_TAGS_AS_LABELS}" "]==]" "\n" + " )" "\n" + " endif()" "\n" + " include(\"${ctest_tests_file}\")" "\n" + "else()" "\n" + " add_test(${TARGET}_NOT_BUILT ${TARGET}_NOT_BUILT)" "\n" + "endif()" "\n" + ) + + if(GENERATOR_IS_MULTI_CONFIG) + foreach(_config ${CMAKE_CONFIGURATION_TYPES}) + file(GENERATE OUTPUT "${ctest_file_base}_include-${_config}.cmake" CONTENT "${ctest_include_content}" CONDITION $) + endforeach() + string(CONCAT ctest_include_multi_content + "if(NOT CTEST_CONFIGURATION_TYPE)" "\n" + " message(\"No configuration for testing specified, use '-C '.\")" "\n" + "else()" "\n" + " include(\"${ctest_file_base}_include-\${CTEST_CONFIGURATION_TYPE}.cmake\")" "\n" + "endif()" "\n" ) + file(GENERATE OUTPUT "${ctest_include_file}" CONTENT "${ctest_include_multi_content}") else() - message(FATAL_ERROR - "Cannot set more than one TEST_INCLUDE_FILE" - ) + file(GENERATE OUTPUT "${ctest_file_base}_include.cmake" CONTENT "${ctest_include_content}") + file(WRITE "${ctest_include_file}" "include(\"${ctest_file_base}_include.cmake\")") endif() endif() + # Add discovered tests to directory TEST_INCLUDE_FILES + set_property(DIRECTORY + APPEND PROPERTY TEST_INCLUDE_FILES "${ctest_include_file}" + ) + endfunction() ############################################################################### set(_CATCH_DISCOVER_TESTS_SCRIPT ${CMAKE_CURRENT_LIST_DIR}/CatchAddTests.cmake + CACHE INTERNAL "Catch2 full path to CatchAddTests.cmake helper file" ) diff --git a/cmake/CatchAddTests.cmake b/cmake/CatchAddTests.cmake index 3575a35c..4c27f479 100644 --- a/cmake/CatchAddTests.cmake +++ b/cmake/CatchAddTests.cmake @@ -1,18 +1,12 @@ # Distributed under the OSI-approved BSD 3-Clause License. See accompanying # file Copyright.txt or https://cmake.org/licensing for details. -set(prefix "${TEST_PREFIX}") -set(suffix "${TEST_SUFFIX}") -set(spec ${TEST_SPEC}) -set(extra_args ${TEST_EXTRA_ARGS}) -set(properties ${TEST_PROPERTIES}) -set(script) -set(suite) -set(tests) - function(add_command NAME) set(_args "") - foreach(_arg ${ARGN}) + # use ARGV* instead of ARGN, because ARGN splits arrays into multiple arguments + math(EXPR _last_arg ${ARGC}-1) + foreach(_n RANGE 1 ${_last_arg}) + set(_arg "${ARGV${_n}}") if(_arg MATCHES "[^-./:a-zA-Z0-9_]") set(_args "${_args} [==[${_arg}]==]") # form a bracket_argument else() @@ -22,55 +16,239 @@ function(add_command NAME) set(script "${script}${NAME}(${_args})\n" PARENT_SCOPE) endfunction() -# Run test executable to get list of available tests -if(NOT EXISTS "${TEST_EXECUTABLE}") - message(FATAL_ERROR - "Specified test executable '${TEST_EXECUTABLE}' does not exist" +function(catch_discover_tests_impl) + + cmake_parse_arguments( + "" + "" + "TEST_EXECUTABLE;TEST_WORKING_DIR;TEST_OUTPUT_DIR;TEST_OUTPUT_PREFIX;TEST_OUTPUT_SUFFIX;TEST_PREFIX;TEST_REPORTER;TEST_SPEC;TEST_SUFFIX;TEST_LIST;CTEST_FILE" + "TEST_EXTRA_ARGS;TEST_PROPERTIES;TEST_EXECUTOR;TEST_DL_PATHS;TEST_DL_FRAMEWORK_PATHS;ADD_TAGS_AS_LABELS" + ${ARGN} + ) + + set(add_tags "${_ADD_TAGS_AS_LABELS}") + set(prefix "${_TEST_PREFIX}") + set(suffix "${_TEST_SUFFIX}") + set(spec ${_TEST_SPEC}) + set(extra_args ${_TEST_EXTRA_ARGS}) + set(properties ${_TEST_PROPERTIES}) + set(reporter ${_TEST_REPORTER}) + set(output_dir ${_TEST_OUTPUT_DIR}) + set(output_prefix ${_TEST_OUTPUT_PREFIX}) + set(output_suffix ${_TEST_OUTPUT_SUFFIX}) + set(dl_paths ${_TEST_DL_PATHS}) + set(dl_framework_paths ${_TEST_DL_FRAMEWORK_PATHS}) + set(environment_modifications "") + set(script) + set(suite) + set(tests) + + if(WIN32) + set(dl_paths_variable_name PATH) + elseif(APPLE) + set(dl_paths_variable_name DYLD_LIBRARY_PATH) + else() + set(dl_paths_variable_name LD_LIBRARY_PATH) + endif() + + # Run test executable to get list of available tests + if(NOT EXISTS "${_TEST_EXECUTABLE}") + message(FATAL_ERROR + "Specified test executable '${_TEST_EXECUTABLE}' does not exist" + ) + endif() + + if(dl_paths) + cmake_path(CONVERT "$ENV{${dl_paths_variable_name}}" TO_NATIVE_PATH_LIST env_dl_paths) + list(PREPEND env_dl_paths "${dl_paths}") + cmake_path(CONVERT "${env_dl_paths}" TO_NATIVE_PATH_LIST paths) + set(ENV{${dl_paths_variable_name}} "${paths}") + endif() + + if(APPLE AND dl_framework_paths) + cmake_path(CONVERT "$ENV{DYLD_FRAMEWORK_PATH}" TO_NATIVE_PATH_LIST env_dl_framework_paths) + list(PREPEND env_dl_framework_paths "${dl_framework_paths}") + cmake_path(CONVERT "${env_dl_framework_paths}" TO_NATIVE_PATH_LIST paths) + set(ENV{DYLD_FRAMEWORK_PATH} "${paths}") + endif() + + execute_process( + COMMAND ${_TEST_EXECUTOR} "${_TEST_EXECUTABLE}" ${spec} --list-tests --reporter json + OUTPUT_VARIABLE listing_output + RESULT_VARIABLE result + WORKING_DIRECTORY "${_TEST_WORKING_DIR}" + ) + if(NOT ${result} EQUAL 0) + message(FATAL_ERROR + "Error listing tests from executable '${_TEST_EXECUTABLE}':\n" + " Result: ${result}\n" + " Output: ${listing_output}\n" + ) + endif() + + # Prepare reporter + if(reporter) + set(reporter_arg "--reporter ${reporter}") + + # Run test executable to check whether reporter is available + # note that the use of --list-reporters is not the important part, + # we only want to check whether the execution succeeds with ${reporter_arg} + execute_process( + COMMAND ${_TEST_EXECUTOR} "${_TEST_EXECUTABLE}" ${spec} ${reporter_arg} --list-reporters + OUTPUT_VARIABLE reporter_check_output + RESULT_VARIABLE reporter_check_result + WORKING_DIRECTORY "${_TEST_WORKING_DIR}" + ) + if(${reporter_check_result} EQUAL 255) + message(FATAL_ERROR + "\"${reporter}\" is not a valid reporter!\n" + ) + elseif(NOT ${reporter_check_result} EQUAL 0) + message(FATAL_ERROR + "Error checking for reporter in test executable '${_TEST_EXECUTABLE}':\n" + " Result: ${reporter_check_result}\n" + " Output: ${reporter_check_output}\n" + ) + endif() + endif() + + # Prepare output dir + if(output_dir AND NOT IS_ABSOLUTE ${output_dir}) + set(output_dir "${_TEST_WORKING_DIR}/${output_dir}") + if(NOT EXISTS ${output_dir}) + file(MAKE_DIRECTORY ${output_dir}) + endif() + endif() + + if(dl_paths) + foreach(path ${dl_paths}) + cmake_path(NATIVE_PATH path native_path) + list(PREPEND environment_modifications "${dl_paths_variable_name}=path_list_prepend:${native_path}") + endforeach() + endif() + + if(APPLE AND dl_framework_paths) + foreach(path ${dl_framework_paths}) + cmake_path(NATIVE_PATH path native_path) + list(PREPEND environment_modifications "DYLD_FRAMEWORK_PATH=path_list_prepend:${native_path}") + endforeach() + endif() + + # Parse JSON output for list of tests/class names/tags + string(JSON version GET "${listing_output}" "version") + if(NOT version STREQUAL "1") + message(FATAL_ERROR "Unsupported catch output version: '${version}'") + endif() + + # Speed-up reparsing by cutting away unneeded parts of JSON. + string(JSON test_listing GET "${listing_output}" "listings" "tests") + string(JSON num_tests LENGTH "${test_listing}") + + # Exit early if no tests are detected + if(num_tests STREQUAL "0") + file(WRITE "${_CTEST_FILE}" "") + return() + endif() + + # CMake's foreach-RANGE is inclusive, so we have to subtract 1 + math(EXPR num_tests "${num_tests} - 1") + + foreach(idx RANGE ${num_tests}) + string(JSON single_test GET ${test_listing} ${idx}) + string(JSON test_tags GET "${single_test}" "tags") + string(JSON plain_name GET "${single_test}" "name") + + # Escape characters in test case names that would be parsed by Catch2 + # Note that the \ escaping must happen FIRST! Do not change the order. + set(escaped_name "${plain_name}") + foreach(char \\ , [ ] ;) + string(REPLACE ${char} "\\${char}" escaped_name "${escaped_name}") + endforeach(char) + # ...add output dir + if(output_dir) + string(REGEX REPLACE "[^A-Za-z0-9_]" "_" escaped_name_clean "${escaped_name}") + set(output_dir_arg "--out ${output_dir}/${output_prefix}${escaped_name_clean}${output_suffix}") + endif() + + # ...and add to script + add_command(add_test + "${prefix}${plain_name}${suffix}" + ${_TEST_EXECUTOR} + "${_TEST_EXECUTABLE}" + "${escaped_name}" + ${extra_args} + "${reporter_arg}" + "${output_dir_arg}" + ) + add_command(set_tests_properties + "${prefix}${plain_name}${suffix}" + PROPERTIES + WORKING_DIRECTORY "${_TEST_WORKING_DIR}" + ${properties} + ) + + if(add_tags) + string(JSON num_tags LENGTH "${test_tags}") + math(EXPR num_tags "${num_tags} - 1") + set(tag_list "") + if(num_tags GREATER_EQUAL "0") + foreach(tag_idx RANGE ${num_tags}) + string(JSON a_tag GET "${test_tags}" "${tag_idx}") + # Catch2's tags can contain semicolons, which are list element separators + # in CMake, so we have to escape them. Ideally we could use the [=[...]=] + # syntax for this, but CTest currently keeps the square quotes in the label + # name. So we add 2 backslashes to escape it instead. + # **IMPORTANT**: The number of backslashes depends on how many layers + # of CMake the tag goes. If this script is changed, the + # number of backslashes to escape may change as well. + string(REPLACE ";" "\\;" a_tag "${a_tag}") + list(APPEND tag_list "${a_tag}") + endforeach() + + add_command(set_tests_properties + "${prefix}${plain_name}${suffix}" + PROPERTIES + LABELS "${tag_list}" + ) + endif() + endif(add_tags) + + if(environment_modifications) + add_command(set_tests_properties + "${prefix}${plain_name}${suffix}" + PROPERTIES + ENVIRONMENT_MODIFICATION "${environment_modifications}") + endif() + + list(APPEND tests "${prefix}${plain_name}${suffix}") + endforeach() + + # Create a list of all discovered tests, which users may use to e.g. set + # properties on the tests + add_command(set ${_TEST_LIST} ${tests}) + + # Write CTest script + file(WRITE "${_CTEST_FILE}" "${script}") +endfunction() + +if(CMAKE_SCRIPT_MODE_FILE) + catch_discover_tests_impl( + TEST_EXECUTABLE ${TEST_EXECUTABLE} + TEST_EXECUTOR ${TEST_EXECUTOR} + TEST_WORKING_DIR ${TEST_WORKING_DIR} + TEST_SPEC ${TEST_SPEC} + TEST_EXTRA_ARGS ${TEST_EXTRA_ARGS} + TEST_PROPERTIES ${TEST_PROPERTIES} + TEST_PREFIX ${TEST_PREFIX} + TEST_SUFFIX ${TEST_SUFFIX} + TEST_LIST ${TEST_LIST} + TEST_REPORTER ${TEST_REPORTER} + TEST_OUTPUT_DIR ${TEST_OUTPUT_DIR} + TEST_OUTPUT_PREFIX ${TEST_OUTPUT_PREFIX} + TEST_OUTPUT_SUFFIX ${TEST_OUTPUT_SUFFIX} + TEST_DL_PATHS ${TEST_DL_PATHS} + TEST_DL_FRAMEWORK_PATHS ${TEST_DL_FRAMEWORK_PATHS} + CTEST_FILE ${CTEST_FILE} + ADD_TAGS_AS_LABELS ${ADD_TAGS_AS_LABELS} ) endif() -execute_process( - COMMAND ${TEST_EXECUTOR} "${TEST_EXECUTABLE}" ${spec} --list-test-names-only - OUTPUT_VARIABLE output - RESULT_VARIABLE result -) -# Catch --list-test-names-only reports the number of tests, so 0 is... surprising -if(${result} EQUAL 0) - message(WARNING - "Test executable '${TEST_EXECUTABLE}' contains no tests!\n" - ) -elseif(${result} LESS 0) - message(FATAL_ERROR - "Error running test executable '${TEST_EXECUTABLE}':\n" - " Result: ${result}\n" - " Output: ${output}\n" - ) -endif() - -string(REPLACE "\n" ";" output "${output}") - -# Parse output -foreach(line ${output}) - set(test ${line}) - # ...and add to script - add_command(add_test - "${prefix}${test}${suffix}" - ${TEST_EXECUTOR} - "${TEST_EXECUTABLE}" - ${test} - ${extra_args} - ) - add_command(set_tests_properties - "${prefix}${test}${suffix}" - PROPERTIES - WORKING_DIRECTORY "${TEST_WORKING_DIR}" - ${properties} - ) - list(APPEND tests "${prefix}${test}${suffix}") -endforeach() - -# Create a list of all discovered tests, which users may use to e.g. set -# properties on the tests -add_command(set ${TEST_LIST} ${tests}) - -# Write CTest script -file(WRITE "${CTEST_FILE}" "${script}") diff --git a/cmake/CatchShardTests.cmake b/cmake/CatchShardTests.cmake new file mode 100644 index 00000000..89666d66 --- /dev/null +++ b/cmake/CatchShardTests.cmake @@ -0,0 +1,72 @@ + +# Copyright Catch2 Authors +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE.txt or copy at +# https://www.boost.org/LICENSE_1_0.txt) + +# SPDX-License-Identifier: BSL-1.0 + +# Supported optional args: +# * SHARD_COUNT - number of shards to split target's tests into +# * REPORTER - reporter spec to use for tests +# * TEST_SPEC - test spec used for filtering tests +function(catch_add_sharded_tests TARGET) + if(${CMAKE_VERSION} VERSION_LESS "3.10.0") + message(FATAL_ERROR "add_sharded_catch_tests only supports CMake versions 3.10.0 and up") + endif() + + cmake_parse_arguments( + "" + "" + "SHARD_COUNT;REPORTER;TEST_SPEC" + "" + ${ARGN} + ) + + if(NOT DEFINED _SHARD_COUNT) + set(_SHARD_COUNT 2) + endif() + + # Generate a unique name based on the extra arguments + string(SHA1 args_hash "${_TEST_SPEC} ${_EXTRA_ARGS} ${_REPORTER} ${_OUTPUT_DIR} ${_OUTPUT_PREFIX} ${_OUTPUT_SUFFIX} ${_SHARD_COUNT}") + string(SUBSTRING ${args_hash} 0 7 args_hash) + + set(ctest_include_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}-sharded-tests-include-${args_hash}.cmake") + set(ctest_tests_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}-sharded-tests-impl-${args_hash}.cmake") + + file(WRITE "${ctest_include_file}" + "if(EXISTS \"${ctest_tests_file}\")\n" + " include(\"${ctest_tests_file}\")\n" + "else()\n" + " add_test(${TARGET}_NOT_BUILT-${args_hash} ${TARGET}_NOT_BUILT-${args_hash})\n" + "endif()\n" + ) + + set_property(DIRECTORY + APPEND PROPERTY TEST_INCLUDE_FILES "${ctest_include_file}" + ) + + set(shard_impl_script_file "${_CATCH_DISCOVER_SHARD_TESTS_IMPL_SCRIPT}") + + add_custom_command( + TARGET ${TARGET} POST_BUILD + BYPRODUCTS "${ctest_tests_file}" + COMMAND "${CMAKE_COMMAND}" + -D "TARGET_NAME=${TARGET}" + -D "TEST_BINARY=$" + -D "CTEST_FILE=${ctest_tests_file}" + -D "SHARD_COUNT=${_SHARD_COUNT}" + -D "REPORTER_SPEC=${_REPORTER}" + -D "TEST_SPEC=${_TEST_SPEC}" + -P "${shard_impl_script_file}" + VERBATIM + ) +endfunction() + + +############################################################################### + +set(_CATCH_DISCOVER_SHARD_TESTS_IMPL_SCRIPT + ${CMAKE_CURRENT_LIST_DIR}/CatchShardTestsImpl.cmake + CACHE INTERNAL "Catch2 full path to CatchShardTestsImpl.cmake helper file" +) diff --git a/cmake/CatchShardTestsImpl.cmake b/cmake/CatchShardTestsImpl.cmake new file mode 100644 index 00000000..83fac688 --- /dev/null +++ b/cmake/CatchShardTestsImpl.cmake @@ -0,0 +1,52 @@ + +# Copyright Catch2 Authors +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE.txt or copy at +# https://www.boost.org/LICENSE_1_0.txt) + +# SPDX-License-Identifier: BSL-1.0 + +# Indirection for CatchShardTests that allows us to delay the script +# file generation until build time. + +# Expected args: +# * TEST_BINARY - full path to the test binary to run sharded +# * CTEST_FILE - full path to ctest script file to write to +# * TARGET_NAME - name of the target to shard (used for test names) +# * SHARD_COUNT - number of shards to split the binary into +# Optional args: +# * REPORTER_SPEC - reporter specs to be passed down to the binary +# * TEST_SPEC - test spec to pass down to the test binary + +if(NOT EXISTS "${TEST_BINARY}") + message(FATAL_ERROR + "Specified test binary '${TEST_BINARY}' does not exist" + ) +endif() + +set(other_args "") +if(TEST_SPEC) + set(other_args "${other_args} ${TEST_SPEC}") +endif() +if(REPORTER_SPEC) + set(other_args "${other_args} --reporter ${REPORTER_SPEC}") +endif() + +# foreach RANGE in cmake is inclusive of the end, so we have to adjust it +math(EXPR adjusted_shard_count "${SHARD_COUNT} - 1") + +file(WRITE "${CTEST_FILE}" + "string(RANDOM LENGTH 8 ALPHABET \"0123456789abcdef\" rng_seed)\n" + "\n" + "foreach(shard_idx RANGE ${adjusted_shard_count})\n" + " add_test(${TARGET_NAME}-shard-" [[${shard_idx}]] "/${adjusted_shard_count}\n" + " ${TEST_BINARY}" + " --shard-index " [[${shard_idx}]] + " --shard-count ${SHARD_COUNT}" + " --rng-seed " [[0x${rng_seed}]] + " --order rand" + "${other_args}" + "\n" + " )\n" + "endforeach()\n" +) diff --git a/cmake/ParseAndAddCatchTests.cmake b/cmake/ParseAndAddCatchTests.cmake index cb2846d0..d7c17ace 100644 --- a/cmake/ParseAndAddCatchTests.cmake +++ b/cmake/ParseAndAddCatchTests.cmake @@ -1,9 +1,11 @@ #==================================================================================================# # supported macros # # - TEST_CASE, # +# - TEMPLATE_TEST_CASE # # - SCENARIO, # # - TEST_CASE_METHOD, # # - CATCH_TEST_CASE, # +# - CATCH_TEMPLATE_TEST_CASE # # - CATCH_SCENARIO, # # - CATCH_TEST_CASE_METHOD. # # # @@ -39,9 +41,24 @@ # PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS (Default OFF) # # -- causes CMake to rerun when file with tests changes so that new tests will be discovered # # # +# One can also set (locally) the optional variable OptionalCatchTestLauncher to precise the way # +# a test should be run. For instance to use test MPI, one can write # +# set(OptionalCatchTestLauncher ${MPIEXEC} ${MPIEXEC_NUMPROC_FLAG} ${NUMPROC}) # +# just before calling this ParseAndAddCatchTests function # +# # +# The AdditionalCatchParameters optional variable can be used to pass extra argument to the test # +# command. For example, to include successful tests in the output, one can write # +# set(AdditionalCatchParameters --success) # +# # +# After the script, the ParseAndAddCatchTests_TESTS property for the target, and for each source # +# file in the target is set, and contains the list of the tests extracted from that target, or # +# from that file. This is useful, for example to add further labels or properties to the tests. # +# # #==================================================================================================# -cmake_minimum_required(VERSION 2.8.8) +if(CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 2.8.8) + message(FATAL_ERROR "ParseAndAddCatchTests requires CMake 2.8.8 or newer") +endif() option(PARSE_CATCH_TESTS_VERBOSE "Print Catch to CTest parser debug messages" OFF) option(PARSE_CATCH_TESTS_NO_HIDDEN_TESTS "Exclude tests with [!hide], [.] or [.foo] tags" OFF) @@ -49,10 +66,10 @@ option(PARSE_CATCH_TESTS_ADD_FIXTURE_IN_TEST_NAME "Add fixture class name to the option(PARSE_CATCH_TESTS_ADD_TARGET_IN_TEST_NAME "Add target name to the test name" ON) option(PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS "Add test file to CMAKE_CONFIGURE_DEPENDS property" OFF) -function(PrintDebugMessage) - if(PARSE_CATCH_TESTS_VERBOSE) - message(STATUS "ParseAndAddCatchTests: ${ARGV}") - endif() +function(ParseAndAddCatchTests_PrintDebugMessage) + if(PARSE_CATCH_TESTS_VERBOSE) + message(STATUS "ParseAndAddCatchTests: ${ARGV}") + endif() endfunction() # This removes the contents between @@ -60,7 +77,7 @@ endfunction() # - full line comments (i.e. // ... ) # contents have been read into '${CppCode}'. # !keep partial line comments -function(RemoveComments CppCode) +function(ParseAndAddCatchTests_RemoveComments CppCode) string(ASCII 2 CMakeBeginBlockComment) string(ASCII 3 CMakeEndBlockComment) string(REGEX REPLACE "/\\*" "${CMakeBeginBlockComment}" ${CppCode} "${${CppCode}}") @@ -72,114 +89,162 @@ function(RemoveComments CppCode) endfunction() # Worker function -function(ParseFile SourceFile TestTarget) - # According to CMake docs EXISTS behavior is well-defined only for full paths. - get_filename_component(SourceFile ${SourceFile} ABSOLUTE) - if(NOT EXISTS ${SourceFile}) - message(WARNING "Cannot find source file: ${SourceFile}") - return() +function(ParseAndAddCatchTests_ParseFile SourceFile TestTarget) + # If SourceFile is an object library, do not scan it (as it is not a file). Exit without giving a warning about a missing file. + if(SourceFile MATCHES "\\\$") + ParseAndAddCatchTests_PrintDebugMessage("Detected OBJECT library: ${SourceFile} this will not be scanned for tests.") + return() + endif() + # According to CMake docs EXISTS behavior is well-defined only for full paths. + get_filename_component(SourceFile ${SourceFile} ABSOLUTE) + if(NOT EXISTS ${SourceFile}) + message(WARNING "Cannot find source file: ${SourceFile}") + return() + endif() + ParseAndAddCatchTests_PrintDebugMessage("parsing ${SourceFile}") + file(STRINGS ${SourceFile} Contents NEWLINE_CONSUME) + + # Remove block and fullline comments + ParseAndAddCatchTests_RemoveComments(Contents) + + # Find definition of test names + # https://regex101.com/r/JygOND/1 + string(REGEX MATCHALL "[ \t]*(CATCH_)?(TEMPLATE_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)[ \t]*\\([ \t\n]*\"[^\"]*\"[ \t\n]*(,[ \t\n]*\"[^\"]*\")?(,[ \t\n]*[^\,\)]*)*\\)[ \t\n]*\{+[ \t]*(//[^\n]*[Tt][Ii][Mm][Ee][Oo][Uu][Tt][ \t]*[0-9]+)*" Tests "${Contents}") + + if(PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS AND Tests) + ParseAndAddCatchTests_PrintDebugMessage("Adding ${SourceFile} to CMAKE_CONFIGURE_DEPENDS property") + set_property( + DIRECTORY + APPEND + PROPERTY CMAKE_CONFIGURE_DEPENDS ${SourceFile} + ) + endif() + + # check CMP0110 policy for new add_test() behavior + if(POLICY CMP0110) + cmake_policy(GET CMP0110 _cmp0110_value) # new add_test() behavior + else() + # just to be thorough explicitly set the variable + set(_cmp0110_value) + endif() + + foreach(TestName ${Tests}) + # Strip newlines + string(REGEX REPLACE "\\\\\n|\n" "" TestName "${TestName}") + + # Get test type and fixture if applicable + string(REGEX MATCH "(CATCH_)?(TEMPLATE_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)[ \t]*\\([^,^\"]*" TestTypeAndFixture "${TestName}") + string(REGEX MATCH "(CATCH_)?(TEMPLATE_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)" TestType "${TestTypeAndFixture}") + string(REGEX REPLACE "${TestType}\\([ \t]*" "" TestFixture "${TestTypeAndFixture}") + + # Get string parts of test definition + string(REGEX MATCHALL "\"+([^\\^\"]|\\\\\")+\"+" TestStrings "${TestName}") + + # Strip wrapping quotation marks + string(REGEX REPLACE "^\"(.*)\"$" "\\1" TestStrings "${TestStrings}") + string(REPLACE "\";\"" ";" TestStrings "${TestStrings}") + + # Validate that a test name and tags have been provided + list(LENGTH TestStrings TestStringsLength) + if(TestStringsLength GREATER 2 OR TestStringsLength LESS 1) + message(FATAL_ERROR "You must provide a valid test name and tags for all tests in ${SourceFile}") endif() - PrintDebugMessage("parsing ${SourceFile}") - file(STRINGS ${SourceFile} Contents NEWLINE_CONSUME) - # Remove block and fullline comments - RemoveComments(Contents) + # Assign name and tags + list(GET TestStrings 0 Name) + if("${TestType}" STREQUAL "SCENARIO") + set(Name "Scenario: ${Name}") + endif() + if(PARSE_CATCH_TESTS_ADD_FIXTURE_IN_TEST_NAME AND "${TestType}" MATCHES "(CATCH_)?TEST_CASE_METHOD" AND TestFixture) + set(CTestName "${TestFixture}:${Name}") + else() + set(CTestName "${Name}") + endif() + if(PARSE_CATCH_TESTS_ADD_TARGET_IN_TEST_NAME) + set(CTestName "${TestTarget}:${CTestName}") + endif() + # add target to labels to enable running all tests added from this target + set(Labels ${TestTarget}) + if(TestStringsLength EQUAL 2) + list(GET TestStrings 1 Tags) + string(TOLOWER "${Tags}" Tags) + # remove target from labels if the test is hidden + if("${Tags}" MATCHES ".*\\[!?(hide|\\.)\\].*") + list(REMOVE_ITEM Labels ${TestTarget}) + endif() + string(REPLACE "]" ";" Tags "${Tags}") + string(REPLACE "[" "" Tags "${Tags}") + else() + # unset tags variable from previous loop + unset(Tags) + endif() - # Find definition of test names - string(REGEX MATCHALL "[ \t]*(CATCH_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)[ \t]*\\([^\)]+\\)+[ \t\n]*{+[ \t]*(//[^\n]*[Tt][Ii][Mm][Ee][Oo][Uu][Tt][ \t]*[0-9]+)*" Tests "${Contents}") + list(APPEND Labels ${Tags}) - if(PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS AND Tests) - PrintDebugMessage("Adding ${SourceFile} to CMAKE_CONFIGURE_DEPENDS property") + set(HiddenTagFound OFF) + foreach(label ${Labels}) + string(REGEX MATCH "^!hide|^\\." result ${label}) + if(result) + set(HiddenTagFound ON) + break() + endif() + endforeach(label) + if(PARSE_CATCH_TESTS_NO_HIDDEN_TESTS AND ${HiddenTagFound} AND ${CMAKE_VERSION} VERSION_LESS "3.9") + ParseAndAddCatchTests_PrintDebugMessage("Skipping test \"${CTestName}\" as it has [!hide], [.] or [.foo] label") + else() + ParseAndAddCatchTests_PrintDebugMessage("Adding test \"${CTestName}\"") + if(Labels) + ParseAndAddCatchTests_PrintDebugMessage("Setting labels to ${Labels}") + endif() + + # Escape commas in the test spec + string(REPLACE "," "\\," Name ${Name}) + + # Work around CMake 3.18.0 change in `add_test()`, before the escaped quotes were necessary, + # only with CMake 3.18.0 the escaped double quotes confuse the call. This change is reverted in 3.18.1 + # And properly introduced in 3.19 with the CMP0110 policy + if(_cmp0110_value STREQUAL "NEW" OR ${CMAKE_VERSION} VERSION_EQUAL "3.18") + ParseAndAddCatchTests_PrintDebugMessage("CMP0110 set to NEW, no need for add_test(\"\") workaround") + else() + ParseAndAddCatchTests_PrintDebugMessage("CMP0110 set to OLD adding \"\" for add_test() workaround") + set(CTestName "\"${CTestName}\"") + endif() + + # Handle template test cases + if("${TestTypeAndFixture}" MATCHES ".*TEMPLATE_.*") + set(Name "${Name} - *") + endif() + + # Add the test and set its properties + add_test(NAME "${CTestName}" COMMAND ${OptionalCatchTestLauncher} $ ${Name} ${AdditionalCatchParameters}) + # Old CMake versions do not document VERSION_GREATER_EQUAL, so we use VERSION_GREATER with 3.8 instead + if(PARSE_CATCH_TESTS_NO_HIDDEN_TESTS AND ${HiddenTagFound} AND ${CMAKE_VERSION} VERSION_GREATER "3.8") + ParseAndAddCatchTests_PrintDebugMessage("Setting DISABLED test property") + set_tests_properties("${CTestName}" PROPERTIES DISABLED ON) + else() + set_tests_properties("${CTestName}" PROPERTIES FAIL_REGULAR_EXPRESSION "No tests ran" + LABELS "${Labels}") + endif() set_property( - DIRECTORY + TARGET ${TestTarget} APPEND - PROPERTY CMAKE_CONFIGURE_DEPENDS ${SourceFile} - ) + PROPERTY ParseAndAddCatchTests_TESTS "${CTestName}") + set_property( + SOURCE ${SourceFile} + APPEND + PROPERTY ParseAndAddCatchTests_TESTS "${CTestName}") endif() - - foreach(TestName ${Tests}) - # Strip newlines - string(REGEX REPLACE "\\\\\n|\n" "" TestName "${TestName}") - - # Get test type and fixture if applicable - string(REGEX MATCH "(CATCH_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)[ \t]*\\([^,^\"]*" TestTypeAndFixture "${TestName}") - string(REGEX MATCH "(CATCH_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)" TestType "${TestTypeAndFixture}") - string(REPLACE "${TestType}(" "" TestFixture "${TestTypeAndFixture}") - - # Get string parts of test definition - string(REGEX MATCHALL "\"+([^\\^\"]|\\\\\")+\"+" TestStrings "${TestName}") - - # Strip wrapping quotation marks - string(REGEX REPLACE "^\"(.*)\"$" "\\1" TestStrings "${TestStrings}") - string(REPLACE "\";\"" ";" TestStrings "${TestStrings}") - - # Validate that a test name and tags have been provided - list(LENGTH TestStrings TestStringsLength) - if(TestStringsLength GREATER 2 OR TestStringsLength LESS 1) - message(FATAL_ERROR "You must provide a valid test name and tags for all tests in ${SourceFile}") - endif() - - # Assign name and tags - list(GET TestStrings 0 Name) - if("${TestType}" STREQUAL "SCENARIO") - set(Name "Scenario: ${Name}") - endif() - if(PARSE_CATCH_TESTS_ADD_FIXTURE_IN_TEST_NAME AND TestFixture) - set(CTestName "${TestFixture}:${Name}") - else() - set(CTestName "${Name}") - endif() - if(PARSE_CATCH_TESTS_ADD_TARGET_IN_TEST_NAME) - set(CTestName "${TestTarget}:${CTestName}") - endif() - # add target to labels to enable running all tests added from this target - set(Labels ${TestTarget}) - if(TestStringsLength EQUAL 2) - list(GET TestStrings 1 Tags) - string(TOLOWER "${Tags}" Tags) - # remove target from labels if the test is hidden - if("${Tags}" MATCHES ".*\\[!?(hide|\\.)\\].*") - list(REMOVE_ITEM Labels ${TestTarget}) - endif() - string(REPLACE "]" ";" Tags "${Tags}") - string(REPLACE "[" "" Tags "${Tags}") - endif() - - list(APPEND Labels ${Tags}) - - list(FIND Labels "!hide" IndexOfHideLabel) - set(HiddenTagFound OFF) - foreach(label ${Labels}) - string(REGEX MATCH "^!hide|^\\." result ${label}) - if(result) - set(HiddenTagFound ON) - break() - endif(result) - endforeach(label) - if(PARSE_CATCH_TESTS_NO_HIDDEN_TESTS AND ${HiddenTagFound}) - PrintDebugMessage("Skipping test \"${CTestName}\" as it has [!hide], [.] or [.foo] label") - else() - PrintDebugMessage("Adding test \"${CTestName}\"") - if(Labels) - PrintDebugMessage("Setting labels to ${Labels}") - endif() - - # Add the test and set its properties - add_test(NAME "\"${CTestName}\"" COMMAND ${TestTarget} ${Name} ${AdditionalCatchParameters}) - set_tests_properties("\"${CTestName}\"" PROPERTIES FAIL_REGULAR_EXPRESSION "No tests ran" - LABELS "${Labels}") - endif() - - endforeach() + endforeach() endfunction() # entry point function(ParseAndAddCatchTests TestTarget) - PrintDebugMessage("Started parsing ${TestTarget}") - get_target_property(SourceFiles ${TestTarget} SOURCES) - PrintDebugMessage("Found the following sources: ${SourceFiles}") - foreach(SourceFile ${SourceFiles}) - ParseFile(${SourceFile} ${TestTarget}) - endforeach() - PrintDebugMessage("Finished parsing ${TestTarget}") + message(DEPRECATION "ParseAndAddCatchTest: function deprecated because of possibility of missed test cases. Consider using 'catch_discover_tests' from 'Catch.cmake'") + ParseAndAddCatchTests_PrintDebugMessage("Started parsing ${TestTarget}") + get_target_property(SourceFiles ${TestTarget} SOURCES) + ParseAndAddCatchTests_PrintDebugMessage("Found the following sources: ${SourceFiles}") + foreach(SourceFile ${SourceFiles}) + ParseAndAddCatchTests_ParseFile(${SourceFile} ${TestTarget}) + endforeach() + ParseAndAddCatchTests_PrintDebugMessage("Finished parsing ${TestTarget}") endfunction() diff --git a/include/chaiscript/chaiscript_defines.hpp b/include/chaiscript/chaiscript_defines.hpp index f50510f0..5dc9ff7a 100644 --- a/include/chaiscript/chaiscript_defines.hpp +++ b/include/chaiscript/chaiscript_defines.hpp @@ -59,10 +59,6 @@ static_assert(_MSC_FULL_VER >= 190024210, "Visual C++ 2015 Update 3 or later req #define CHAISCRIPT_MODULE_EXPORT extern "C" #endif -#if defined(CHAISCRIPT_MSVC) || (defined(__GNUC__) && __GNUC__ >= 5) || defined(CHAISCRIPT_CLANG) -#define CHAISCRIPT_UTF16_UTF32 -#endif - #ifdef _DEBUG #define CHAISCRIPT_DEBUG true #else diff --git a/include/chaiscript/language/chaiscript_engine.hpp b/include/chaiscript/language/chaiscript_engine.hpp index 6a027405..fa46a460 100644 --- a/include/chaiscript/language/chaiscript_engine.hpp +++ b/include/chaiscript/language/chaiscript_engine.hpp @@ -611,7 +611,7 @@ namespace chaiscript { /// (the symbol mentioned above), an exception is thrown. /// /// \throw chaiscript::exception::load_module_error In the event that no matching module can be found. - std::string load_module(const std::string &t_module_name) { + std::string load_module([[maybe_unused]] const std::string &t_module_name) { #ifdef CHAISCRIPT_NO_DYNLOAD throw chaiscript::exception::load_module_error("Loadable module support was disabled (CHAISCRIPT_NO_DYNLOAD)"); #else diff --git a/include/chaiscript/language/chaiscript_parser.hpp b/include/chaiscript/language/chaiscript_parser.hpp index 8e537d8d..cb023a2d 100644 --- a/include/chaiscript/language/chaiscript_parser.hpp +++ b/include/chaiscript/language/chaiscript_parser.hpp @@ -22,15 +22,11 @@ #include "../dispatchkit/boxed_value.hpp" #include "../utility/hash.hpp" #include "../utility/static_string.hpp" +#include "../utility/unicode.hpp" #include "chaiscript_common.hpp" #include "chaiscript_optimizer.hpp" #include "chaiscript_tracer.hpp" -#if defined(CHAISCRIPT_UTF16_UTF32) -#include -#include -#endif - #if defined(CHAISCRIPT_MSVC) && defined(max) && defined(min) #define CHAISCRIPT_PUSHED_MIN_MAX #pragma push_macro("max") // Why Microsoft? why? This is worse than bad @@ -64,41 +60,26 @@ namespace chaiscript { // Generic for u16, u32 and wchar template struct Char_Parser_Helper { - // common for all implementations - static std::string u8str_from_ll(long long val) { - using char_type = std::string::value_type; - - char_type c[2]; - c[1] = char_type(val); - c[0] = char_type(val >> 8); - - if (c[0] == 0) { - return std::string(1, c[1]); // size, character - } - - return std::string(c, 2); // char buffer, size - } - static string_type str_from_ll(long long val) { - using target_char_type = string_type::value_type; -#if defined(CHAISCRIPT_UTF16_UTF32) - // prepare converter - std::wstring_convert, target_char_type> converter; - // convert - return converter.from_bytes(u8str_from_ll(val)); -#else - // no conversion available, just put value as character - return string_type(1, target_char_type(val)); // size, character -#endif + string_type out; + utility::unicode::append_codepoint(out, static_cast(val)); + return out; } }; - // Specialization for char AKA UTF-8 + // Specialization for char AKA UTF-8: preserve raw two-byte packing + // of multi-character literals. template<> struct Char_Parser_Helper { static std::string str_from_ll(long long val) { - // little SFINAE trick to avoid base class - return Char_Parser_Helper::u8str_from_ll(val); + using char_type = std::string::value_type; + char_type c[2]; + c[1] = char_type(val); + c[0] = char_type(val >> 8); + if (c[0] == 0) { + return std::string(1, c[1]); + } + return std::string(c, 2); } }; } // namespace detail @@ -1108,40 +1089,20 @@ namespace chaiscript { } void process_unicode() { - const auto ch = static_cast(std::stoi(hex_matches, nullptr, 16)); + const auto ch = static_cast(std::stoi(hex_matches, nullptr, 16)); const auto match_size = hex_matches.size(); hex_matches.clear(); is_escaped = false; const auto u_size = unicode_size; unicode_size = 0; - char buf[4]; if (u_size != match_size) { throw exception::eval_error("Incomplete unicode escape sequence"); } - if (u_size == 4 && ch >= 0xD800 && ch <= 0xDFFF) { + if (u_size == 4 && utility::unicode::is_surrogate(ch)) { throw exception::eval_error("Invalid 16 bit universal character"); } - - if (ch < 0x80) { - match += static_cast(ch); - } else if (ch < 0x800) { - buf[0] = static_cast(0xC0 | (ch >> 6)); - buf[1] = static_cast(0x80 | (ch & 0x3F)); - match.append(buf, 2); - } else if (ch < 0x10000) { - buf[0] = static_cast(0xE0 | (ch >> 12)); - buf[1] = static_cast(0x80 | ((ch >> 6) & 0x3F)); - buf[2] = static_cast(0x80 | (ch & 0x3F)); - match.append(buf, 3); - } else if (ch < 0x200000) { - buf[0] = static_cast(0xF0 | (ch >> 18)); - buf[1] = static_cast(0x80 | ((ch >> 12) & 0x3F)); - buf[2] = static_cast(0x80 | ((ch >> 6) & 0x3F)); - buf[3] = static_cast(0x80 | (ch & 0x3F)); - match.append(buf, 4); - } else { - // this must be an invalid escape sequence? + if (utility::unicode::append_utf8(match, ch) == 0) { throw exception::eval_error("Invalid 32 bit universal character"); } } diff --git a/include/chaiscript/utility/json.hpp b/include/chaiscript/utility/json.hpp index 5160b727..5cab8cbf 100644 --- a/include/chaiscript/utility/json.hpp +++ b/include/chaiscript/utility/json.hpp @@ -7,6 +7,7 @@ #include "../chaiscript_defines.hpp" #include "quick_flat_map.hpp" +#include "unicode.hpp" #include #include #include @@ -467,22 +468,8 @@ namespace chaiscript::json { } } offset += 4; - const auto ch = static_cast(std::stoi(hex_matches, nullptr, 16)); - if (ch < 0x80) { - val += static_cast(ch); - } else if (ch < 0x800) { - val += static_cast(0xC0 | (ch >> 6)); - val += static_cast(0x80 | (ch & 0x3F)); - } else if (ch < 0x10000) { - val += static_cast(0xE0 | (ch >> 12)); - val += static_cast(0x80 | ((ch >> 6) & 0x3F)); - val += static_cast(0x80 | (ch & 0x3F)); - } else if (ch < 0x200000) { - val += static_cast(0xF0 | (ch >> 18)); - val += static_cast(0x80 | ((ch >> 12) & 0x3F)); - val += static_cast(0x80 | ((ch >> 6) & 0x3F)); - val += static_cast(0x80 | (ch & 0x3F)); - } else { + const auto ch = static_cast(std::stoi(hex_matches, nullptr, 16)); + if (chaiscript::utility::unicode::append_utf8(val, ch) == 0) { throw std::runtime_error(std::string("JSON ERROR: String: Invalid 32 bit universal character")); } } break; diff --git a/unittests/emscripten_eval_test.cpp b/unittests/emscripten_eval_test.cpp index a146fcb8..b758619a 100644 --- a/unittests/emscripten_eval_test.cpp +++ b/unittests/emscripten_eval_test.cpp @@ -20,31 +20,31 @@ int main() { chaiscript_eval("var x = 42"); // Test evalString - same as Emscripten evalString() - std::string s = chaiscript_eval_string("to_string(x)"); + [[maybe_unused]] std::string s = chaiscript_eval_string("to_string(x)"); assert(s == "42"); // Test evalInt - same as Emscripten evalInt() - int i = chaiscript_eval_int("1 + 2"); + [[maybe_unused]] int i = chaiscript_eval_int("1 + 2"); assert(i == 3); // Test evalBool - same as Emscripten evalBool() - bool b = chaiscript_eval_bool("true"); + [[maybe_unused]] bool b = chaiscript_eval_bool("true"); assert(b == true); b = chaiscript_eval_bool("false"); assert(b == false); // Test evalFloat - same as Emscripten evalFloat() - float f = chaiscript_eval_float("1.5f"); + [[maybe_unused]] float f = chaiscript_eval_float("1.5f"); assert(std::abs(f - 1.5f) < 0.001f); // Test evalDouble - same as Emscripten evalDouble() - double d = chaiscript_eval_double("3.14"); + [[maybe_unused]] double d = chaiscript_eval_double("3.14"); assert(std::abs(d - 3.14) < 0.001); // Test a more complex expression chaiscript_eval("def square(n) { return n * n; }"); - int sq = chaiscript_eval_int("square(7)"); + [[maybe_unused]] int sq = chaiscript_eval_int("square(7)"); assert(sq == 49); return 0; diff --git a/unittests/emscripten_exception_test.cpp b/unittests/emscripten_exception_test.cpp index 432fd52b..3b570a1e 100644 --- a/unittests/emscripten_exception_test.cpp +++ b/unittests/emscripten_exception_test.cpp @@ -22,7 +22,7 @@ int main() { // through the eval wrapper functions. In WASM builds without exception // support, these would abort instead of throwing. - bool caught = false; + [[maybe_unused]] bool caught = false; // Test 1: eval with undefined variable should throw caught = false; @@ -64,7 +64,8 @@ int main() { // Test 5: Verify normal operation still works after caught exceptions chaiscript_eval("var post_exception_test = 100"); - const int result = chaiscript_eval_int("post_exception_test"); + + [[maybe_unused]] const int result = chaiscript_eval_int("post_exception_test"); assert(result == 100 && "normal eval must work after caught exceptions"); std::cout << "All emscripten exception tests passed.\n";