From dba9197ec71ed64c6f08c7ce73f1af5bc3331f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ho=C5=99e=C5=88ovsk=C3=BD?= Date: Fri, 12 May 2023 17:13:21 +0200 Subject: [PATCH 01/10] Add new config option: STATIC_ANALYSIS_SUPPORT --- BUILD.bazel | 2 ++ CMake/CatchConfigOptions.cmake | 1 + docs/configuration.md | 26 ++++++++++++++ src/CMakeLists.txt | 1 + src/catch2/catch_all.hpp | 1 + src/catch2/catch_user_config.hpp.in | 9 +++++ .../catch_config_static_analysis_support.hpp | 34 +++++++++++++++++++ src/catch2/meson.build | 1 + 8 files changed, 75 insertions(+) create mode 100644 src/catch2/internal/catch_config_static_analysis_support.hpp diff --git a/BUILD.bazel b/BUILD.bazel index 3125e7c5..02ec9226 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -43,12 +43,14 @@ expand_template( "#cmakedefine CATCH_CONFIG_NO_GLOBAL_NEXTAFTER": "", "#cmakedefine CATCH_CONFIG_NO_POSIX_SIGNALS": "", "#cmakedefine CATCH_CONFIG_NO_USE_ASYNC": "", + "#cmakedefine CATCH_CONFIG_NO_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT": "", "#cmakedefine CATCH_CONFIG_NO_WCHAR": "", "#cmakedefine CATCH_CONFIG_NO_WINDOWS_SEH": "", "#cmakedefine CATCH_CONFIG_NOSTDOUT": "", "#cmakedefine CATCH_CONFIG_POSIX_SIGNALS": "", "#cmakedefine CATCH_CONFIG_PREFIX_ALL": "", "#cmakedefine CATCH_CONFIG_SHARED_LIBRARY": "", + "#cmakedefine CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT": "", "#cmakedefine CATCH_CONFIG_USE_ASYNC": "", "#cmakedefine CATCH_CONFIG_WCHAR": "", "#cmakedefine CATCH_CONFIG_WINDOWS_CRTDBG": "", diff --git a/CMake/CatchConfigOptions.cmake b/CMake/CatchConfigOptions.cmake index e59f3a1e..067739dc 100644 --- a/CMake/CatchConfigOptions.cmake +++ b/CMake/CatchConfigOptions.cmake @@ -41,6 +41,7 @@ set(_OverridableOptions "WCHAR" "WINDOWS_SEH" "GETENV" + "EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT" ) foreach(OptionName ${_OverridableOptions}) diff --git a/docs/configuration.md b/docs/configuration.md index d4421f3c..d6e159e5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -15,6 +15,7 @@ [Enabling stringification](#enabling-stringification)
[Disabling exceptions](#disabling-exceptions)
[Overriding Catch's debug break (`-b`)](#overriding-catchs-debug-break--b)
+[Static analysis support](#static-analysis-support)
Catch2 is designed to "just work" as much as possible, and most of the configuration options below are changed automatically during compilation, @@ -264,6 +265,31 @@ The macro will be used as is, that is, `CATCH_BREAK_INTO_DEBUGGER();` must compile and must break into debugger. +## Static analysis support + +> Introduced in Catch2 X.Y.Z. + +Some parts of Catch2, e.g. `SECTION`s, can be hard for static analysis +tools to reason about. Catch2 can change its internals to help static +analysis tools reason about the tests. + +Catch2 automatically detects some static analysis tools (initial +implementation checks for clang-tidy and Coverity), but you can override +its detection (in either direction) via + +``` +CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT // force enables static analysis help +CATCH_CONFIG_NO_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT // force disables static analysis help +``` + +_As the name suggests, this is currently experimental, and thus we provide +no backwards compatibility guarantees._ + +**DO NOT ENABLE THIS FOR BUILDS YOU INTEND TO RUN.** The changed internals +are not meant to be runnable, only "scannable". + + + --- [Home](Readme.md#top) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1e0a09f1..0fdf931e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -73,6 +73,7 @@ set(IMPL_HEADERS ${SOURCES_DIR}/internal/catch_compiler_capabilities.hpp ${SOURCES_DIR}/internal/catch_config_android_logwrite.hpp ${SOURCES_DIR}/internal/catch_config_counter.hpp + ${SOURCES_DIR}/internal/catch_config_static_analysis_support.hpp ${SOURCES_DIR}/internal/catch_config_uncaught_exceptions.hpp ${SOURCES_DIR}/internal/catch_config_wchar.hpp ${SOURCES_DIR}/internal/catch_console_colour.hpp diff --git a/src/catch2/catch_all.hpp b/src/catch2/catch_all.hpp index 72de8de9..70ec402d 100644 --- a/src/catch2/catch_all.hpp +++ b/src/catch2/catch_all.hpp @@ -54,6 +54,7 @@ #include #include #include +#include #include #include #include diff --git a/src/catch2/catch_user_config.hpp.in b/src/catch2/catch_user_config.hpp.in index f7973af1..11ab5a6d 100644 --- a/src/catch2/catch_user_config.hpp.in +++ b/src/catch2/catch_user_config.hpp.in @@ -169,6 +169,15 @@ #endif +#cmakedefine CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT +#cmakedefine CATCH_CONFIG_NO_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT + +#if defined( CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT ) && \ + defined( CATCH_CONFIG_NO_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT ) +# error Cannot force STATIC_ANALYSIS_SUPPORT to both ON and OFF +#endif + + // ------ // Simple toggle defines // their value is never used and they cannot be overridden diff --git a/src/catch2/internal/catch_config_static_analysis_support.hpp b/src/catch2/internal/catch_config_static_analysis_support.hpp new file mode 100644 index 00000000..81bdf39f --- /dev/null +++ b/src/catch2/internal/catch_config_static_analysis_support.hpp @@ -0,0 +1,34 @@ + +// 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 + +/** \file + * Wrapper for the STATIC_ANALYSIS_SUPPORT configuration option + * + * Some of Catch2's macros can be defined differently to work better with + * static analysis tools, like clang-tidy or coverity. + * Currently the main use case is to show that `SECTION`s are executed + * exclusively, and not all in one run of a `TEST_CASE`. + */ + +#ifndef CATCH_CONFIG_STATIC_ANALYSIS_SUPPORT_HPP_INCLUDED +#define CATCH_CONFIG_STATIC_ANALYSIS_SUPPORT_HPP_INCLUDED + +#include + +#if defined(__clang_analyzer__) || defined(__COVERITY__) + #define CATCH_INTERNAL_CONFIG_STATIC_ANALYSIS_SUPPORT +#endif + +#if defined( CATCH_INTERNAL_CONFIG_STATIC_ANALYSIS_SUPPORT ) && \ + !defined( CATCH_CONFIG_NO_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT ) && \ + !defined( CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT ) +# define CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT +#endif + + +#endif // CATCH_CONFIG_STATIC_ANALYSIS_SUPPORT_HPP_INCLUDED diff --git a/src/catch2/meson.build b/src/catch2/meson.build index 89e96e23..2e9469d8 100644 --- a/src/catch2/meson.build +++ b/src/catch2/meson.build @@ -78,6 +78,7 @@ internal_headers = [ 'internal/catch_compiler_capabilities.hpp', 'internal/catch_config_android_logwrite.hpp', 'internal/catch_config_counter.hpp', + 'internal/catch_config_static_analysis_support.hpp', 'internal/catch_config_uncaught_exceptions.hpp', 'internal/catch_config_wchar.hpp', 'internal/catch_console_colour.hpp', From bf5aa7b383b143ba9939fad289d1f7a41f07f8cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ho=C5=99e=C5=88ovsk=C3=BD?= Date: Mon, 15 May 2023 14:33:24 +0200 Subject: [PATCH 02/10] Experimental static analysis support in TEST_CASE and SECTION Closes #2681 --- src/catch2/internal/catch_section.hpp | 67 ++++++++++++++++++--- src/catch2/internal/catch_test_registry.hpp | 67 +++++++++++++++++---- 2 files changed, 111 insertions(+), 23 deletions(-) diff --git a/src/catch2/internal/catch_section.hpp b/src/catch2/internal/catch_section.hpp index 8c1a882b..bd92bdf4 100644 --- a/src/catch2/internal/catch_section.hpp +++ b/src/catch2/internal/catch_section.hpp @@ -9,6 +9,7 @@ #define CATCH_SECTION_HPP_INCLUDED #include +#include #include #include #include @@ -38,16 +39,62 @@ namespace Catch { } // end namespace Catch -#define INTERNAL_CATCH_SECTION( ... ) \ - CATCH_INTERNAL_START_WARNINGS_SUPPRESSION \ - CATCH_INTERNAL_SUPPRESS_UNUSED_VARIABLE_WARNINGS \ - if( Catch::Section const& INTERNAL_CATCH_UNIQUE_NAME( catch_internal_Section ) = Catch::Section( CATCH_INTERNAL_LINEINFO, __VA_ARGS__ ) ) \ - CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION +#if !defined(CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT) +# define INTERNAL_CATCH_SECTION( ... ) \ + CATCH_INTERNAL_START_WARNINGS_SUPPRESSION \ + CATCH_INTERNAL_SUPPRESS_UNUSED_VARIABLE_WARNINGS \ + if ( Catch::Section const& INTERNAL_CATCH_UNIQUE_NAME( \ + catch_internal_Section ) = \ + Catch::Section( CATCH_INTERNAL_LINEINFO, __VA_ARGS__ ) ) \ + CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION + +# define INTERNAL_CATCH_DYNAMIC_SECTION( ... ) \ + CATCH_INTERNAL_START_WARNINGS_SUPPRESSION \ + CATCH_INTERNAL_SUPPRESS_UNUSED_VARIABLE_WARNINGS \ + if ( Catch::Section const& INTERNAL_CATCH_UNIQUE_NAME( \ + catch_internal_Section ) = \ + Catch::SectionInfo( \ + CATCH_INTERNAL_LINEINFO, \ + ( Catch::ReusableStringStream() << __VA_ARGS__ ) \ + .str() ) ) \ + CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION + +#else + +// These section definitions imply that at most one section at one level +// will be intered (because only one section's __LINE__ can be equal to +// the dummy `catchInternalSectionHint` variable from `TEST_CASE`). + +namespace Catch { + namespace Detail { + // Intentionally without linkage, as it should only be used as a dummy + // symbol for static analysis. + int GetNewSectionHint(); + } // namespace Detail +} // namespace Catch + + +# define INTERNAL_CATCH_SECTION( ... ) \ + CATCH_INTERNAL_START_WARNINGS_SUPPRESSION \ + CATCH_INTERNAL_SUPPRESS_UNUSED_VARIABLE_WARNINGS \ + CATCH_INTERNAL_SUPPRESS_SHADOW_WARNINGS \ + if ( [[maybe_unused]] int catchInternalPreviousSectionHint = \ + catchInternalSectionHint, \ + catchInternalSectionHint = Catch::Detail::GetNewSectionHint(); \ + catchInternalPreviousSectionHint == __LINE__ ) \ + CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION + +# define INTERNAL_CATCH_DYNAMIC_SECTION( ... ) \ + CATCH_INTERNAL_START_WARNINGS_SUPPRESSION \ + CATCH_INTERNAL_SUPPRESS_UNUSED_VARIABLE_WARNINGS \ + CATCH_INTERNAL_SUPPRESS_SHADOW_WARNINGS \ + if ( [[maybe_unused]] int catchInternalPreviousSectionHint = \ + catchInternalSectionHint, \ + catchInternalSectionHint = Catch::Detail::GetNewSectionHint(); \ + catchInternalPreviousSectionHint == __LINE__ ) \ + CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION + +#endif -#define INTERNAL_CATCH_DYNAMIC_SECTION( ... ) \ - CATCH_INTERNAL_START_WARNINGS_SUPPRESSION \ - CATCH_INTERNAL_SUPPRESS_UNUSED_VARIABLE_WARNINGS \ - if( Catch::Section const& INTERNAL_CATCH_UNIQUE_NAME( catch_internal_Section ) = Catch::SectionInfo( CATCH_INTERNAL_LINEINFO, (Catch::ReusableStringStream() << __VA_ARGS__).str() ) ) \ - CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION #endif // CATCH_SECTION_HPP_INCLUDED diff --git a/src/catch2/internal/catch_test_registry.hpp b/src/catch2/internal/catch_test_registry.hpp index 5cb73067..d248d3cf 100644 --- a/src/catch2/internal/catch_test_registry.hpp +++ b/src/catch2/internal/catch_test_registry.hpp @@ -8,6 +8,7 @@ #ifndef CATCH_TEST_REGISTRY_HPP_INCLUDED #define CATCH_TEST_REGISTRY_HPP_INCLUDED +#include #include #include #include @@ -72,6 +73,9 @@ struct AutoReg : Detail::NonCopyable { void TestName::test() #endif + +#if !defined(CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT) + /////////////////////////////////////////////////////////////////////////////// #define INTERNAL_CATCH_TESTCASE2( TestName, ... ) \ static void TestName(); \ @@ -84,19 +88,40 @@ struct AutoReg : Detail::NonCopyable { #define INTERNAL_CATCH_TESTCASE( ... ) \ INTERNAL_CATCH_TESTCASE2( INTERNAL_CATCH_UNIQUE_NAME( CATCH2_INTERNAL_TEST_ ), __VA_ARGS__ ) - /////////////////////////////////////////////////////////////////////////////// - #define INTERNAL_CATCH_METHOD_AS_TEST_CASE( QualifiedMethod, ... ) \ - CATCH_INTERNAL_START_WARNINGS_SUPPRESSION \ - CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS \ - CATCH_INTERNAL_SUPPRESS_UNUSED_VARIABLE_WARNINGS \ - namespace { \ - const Catch::AutoReg INTERNAL_CATCH_UNIQUE_NAME( autoRegistrar )( \ - Catch::makeTestInvoker( &QualifiedMethod ), \ - CATCH_INTERNAL_LINEINFO, \ - "&" #QualifiedMethod##_catch_sr, \ - Catch::NameAndTags{ __VA_ARGS__ } ); \ - } /* NOLINT */ \ - CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION +#else // ^^ !CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT | vv CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT + + +// Dummy registrator for the dumy test case macros +namespace Catch { + namespace Detail { + struct DummyUse { + DummyUse( void ( * )( int ) ); + }; + } // namespace Detail +} // namespace Catch + +// Note that both the presence of the argument and its exact name are +// necessary for the section support. + +// We provide a shadowed variable so that a `SECTION` inside non-`TEST_CASE` +// tests can compile. The redefined `TEST_CASE` shadows this with param. +static int catchInternalSectionHint = 0; + +# define INTERNAL_CATCH_TESTCASE2( fname ) \ + static void fname( int ); \ + CATCH_INTERNAL_START_WARNINGS_SUPPRESSION \ + CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS \ + CATCH_INTERNAL_SUPPRESS_UNUSED_VARIABLE_WARNINGS \ + static const Catch::Detail::DummyUse INTERNAL_CATCH_UNIQUE_NAME( \ + dummyUser )( &fname ); \ + CATCH_INTERNAL_SUPPRESS_SHADOW_WARNINGS \ + static void fname( [[maybe_unused]] int catchInternalSectionHint ) \ + CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION +# define INTERNAL_CATCH_TESTCASE( ... ) \ + INTERNAL_CATCH_TESTCASE2( INTERNAL_CATCH_UNIQUE_NAME( dummyFunction ) ) + + +#endif // CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT /////////////////////////////////////////////////////////////////////////////// #define INTERNAL_CATCH_TEST_CASE_METHOD2( TestName, ClassName, ... )\ @@ -118,6 +143,22 @@ struct AutoReg : Detail::NonCopyable { #define INTERNAL_CATCH_TEST_CASE_METHOD( ClassName, ... ) \ INTERNAL_CATCH_TEST_CASE_METHOD2( INTERNAL_CATCH_UNIQUE_NAME( CATCH2_INTERNAL_TEST_ ), ClassName, __VA_ARGS__ ) + + /////////////////////////////////////////////////////////////////////////////// + #define INTERNAL_CATCH_METHOD_AS_TEST_CASE( QualifiedMethod, ... ) \ + CATCH_INTERNAL_START_WARNINGS_SUPPRESSION \ + CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS \ + CATCH_INTERNAL_SUPPRESS_UNUSED_VARIABLE_WARNINGS \ + namespace { \ + const Catch::AutoReg INTERNAL_CATCH_UNIQUE_NAME( autoRegistrar )( \ + Catch::makeTestInvoker( &QualifiedMethod ), \ + CATCH_INTERNAL_LINEINFO, \ + "&" #QualifiedMethod##_catch_sr, \ + Catch::NameAndTags{ __VA_ARGS__ } ); \ + } /* NOLINT */ \ + CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION + + /////////////////////////////////////////////////////////////////////////////// #define INTERNAL_CATCH_REGISTER_TESTCASE( Function, ... ) \ do { \ From dff7513b289f823fec91010f8898b8a01f7dcd12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ho=C5=99e=C5=88ovsk=C3=BD?= Date: Mon, 29 May 2023 21:41:51 +0200 Subject: [PATCH 03/10] Static analysis cleanup in tests --- tests/SelfTest/IntrospectiveTests/Details.tests.cpp | 4 ++-- tests/SelfTest/IntrospectiveTests/Reporters.tests.cpp | 4 ++-- tests/SelfTest/UsageTests/Matchers.tests.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/SelfTest/IntrospectiveTests/Details.tests.cpp b/tests/SelfTest/IntrospectiveTests/Details.tests.cpp index 80eb647b..d7175756 100644 --- a/tests/SelfTest/IntrospectiveTests/Details.tests.cpp +++ b/tests/SelfTest/IntrospectiveTests/Details.tests.cpp @@ -95,8 +95,8 @@ namespace { MoveChecker() = default; MoveChecker( MoveChecker const& rhs ) = default; MoveChecker& operator=( MoveChecker const& rhs ) = default; - MoveChecker( MoveChecker&& rhs ) { rhs.has_moved = true; } - MoveChecker& operator=( MoveChecker&& rhs ) { + MoveChecker( MoveChecker&& rhs ) noexcept { rhs.has_moved = true; } + MoveChecker& operator=( MoveChecker&& rhs ) noexcept { rhs.has_moved = true; return *this; } diff --git a/tests/SelfTest/IntrospectiveTests/Reporters.tests.cpp b/tests/SelfTest/IntrospectiveTests/Reporters.tests.cpp index b74580c6..1568c951 100644 --- a/tests/SelfTest/IntrospectiveTests/Reporters.tests.cpp +++ b/tests/SelfTest/IntrospectiveTests/Reporters.tests.cpp @@ -163,7 +163,7 @@ namespace { std::vector& recorder, Catch::IConfig const* config ): EventListenerBase( config ), - m_witness( witness ), + m_witness( CATCH_MOVE(witness) ), m_recorder( recorder ) {} @@ -181,7 +181,7 @@ namespace { std::vector& recorder, Catch::ReporterConfig&& config ): StreamingReporterBase( CATCH_MOVE(config) ), - m_witness( witness ), + m_witness( CATCH_MOVE(witness) ), m_recorder( recorder ) {} diff --git a/tests/SelfTest/UsageTests/Matchers.tests.cpp b/tests/SelfTest/UsageTests/Matchers.tests.cpp index a40908d7..74bedf5e 100644 --- a/tests/SelfTest/UsageTests/Matchers.tests.cpp +++ b/tests/SelfTest/UsageTests/Matchers.tests.cpp @@ -890,7 +890,7 @@ struct MatcherA : Catch::Matchers::MatcherGenericBase { return "equals: (int) 1 or (string) \"1\""; } bool match( int i ) const { return i == 1; } - bool match( std::string s ) const { return s == "1"; } + bool match( std::string const& s ) const { return s == "1"; } }; struct MatcherB : Catch::Matchers::MatcherGenericBase { From 0631b607ee2bbc07c7c238f0b15b23ef21926960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ho=C5=99e=C5=88ovsk=C3=BD?= Date: Wed, 31 May 2023 15:12:23 +0200 Subject: [PATCH 04/10] Test & document SKIP in generator constructor Closes #1593 --- docs/generators.md | 18 +++++++++++++ docs/skipping-passing-failing.md | 6 +++++ .../Baselines/automake.sw.approved.txt | 1 + .../Baselines/automake.sw.multi.approved.txt | 1 + .../Baselines/compact.sw.approved.txt | 3 ++- .../Baselines/compact.sw.multi.approved.txt | 3 ++- .../Baselines/console.std.approved.txt | 12 ++++++++- .../Baselines/console.sw.approved.txt | 12 ++++++++- .../Baselines/console.sw.multi.approved.txt | 12 ++++++++- .../SelfTest/Baselines/junit.sw.approved.txt | 9 ++++++- .../Baselines/junit.sw.multi.approved.txt | 9 ++++++- .../Baselines/sonarqube.sw.approved.txt | 7 +++++ .../Baselines/sonarqube.sw.multi.approved.txt | 7 +++++ tests/SelfTest/Baselines/tap.sw.approved.txt | 4 ++- .../Baselines/tap.sw.multi.approved.txt | 4 ++- .../Baselines/teamcity.sw.approved.txt | 3 +++ .../Baselines/teamcity.sw.multi.approved.txt | 3 +++ tests/SelfTest/Baselines/xml.sw.approved.txt | 10 +++++-- .../Baselines/xml.sw.multi.approved.txt | 10 +++++-- .../SelfTest/UsageTests/Generators.tests.cpp | 10 +++---- tests/SelfTest/UsageTests/Skip.tests.cpp | 27 +++++++++++++++++++ 21 files changed, 153 insertions(+), 18 deletions(-) diff --git a/docs/generators.md b/docs/generators.md index 37d7424a..09799752 100644 --- a/docs/generators.md +++ b/docs/generators.md @@ -221,3 +221,21 @@ For full example of implementing your own generator, look into Catch2's examples, specifically [Generators: Create your own generator](../examples/300-Gen-OwnGenerator.cpp). + +### Handling empty generators + +The generator interface assumes that a generator always has at least one +element. This is not always true, e.g. if the generator depends on an external +datafile, the file might be missing. + +There are two ways to handle this, depending on whether you want this +to be an error or not. + + * If empty generator **is** an error, throw an exception in constructor. + * If empty generator **is not** an error, use the [`SKIP`](skipping-passing-failing.md#skipping-test-cases-at-runtime) in constructor. + + + +--- + +[Home](Readme.md#top) diff --git a/docs/skipping-passing-failing.md b/docs/skipping-passing-failing.md index 4300d9d3..d866b418 100644 --- a/docs/skipping-passing-failing.md +++ b/docs/skipping-passing-failing.md @@ -84,6 +84,12 @@ exit code, same as it does if no test cases have run. This behaviour can be overridden using the [--allow-running-no-tests](command-line.md#no-tests-override) flag. +### `SKIP` inside generators + +You can also use the `SKIP` macro inside generator's constructor to handle +cases where the generator is empty, but you do not want to fail the test +case. + ## Passing and failing test cases diff --git a/tests/SelfTest/Baselines/automake.sw.approved.txt b/tests/SelfTest/Baselines/automake.sw.approved.txt index 7c2836e8..6b5938a6 100644 --- a/tests/SelfTest/Baselines/automake.sw.approved.txt +++ b/tests/SelfTest/Baselines/automake.sw.approved.txt @@ -130,6 +130,7 @@ Nor would this :test-result: FAIL Custom std-exceptions can be custom translated :test-result: PASS Default scale is invisible to comparison :test-result: PASS Directly creating an EnumInfo +:test-result: SKIP Empty generators can SKIP in constructor :test-result: PASS Empty stream name opens cout stream :test-result: FAIL EndsWith string matcher :test-result: PASS Enums can quickly have stringification enabled using REGISTER_ENUM diff --git a/tests/SelfTest/Baselines/automake.sw.multi.approved.txt b/tests/SelfTest/Baselines/automake.sw.multi.approved.txt index 75569816..cd56e648 100644 --- a/tests/SelfTest/Baselines/automake.sw.multi.approved.txt +++ b/tests/SelfTest/Baselines/automake.sw.multi.approved.txt @@ -128,6 +128,7 @@ :test-result: FAIL Custom std-exceptions can be custom translated :test-result: PASS Default scale is invisible to comparison :test-result: PASS Directly creating an EnumInfo +:test-result: SKIP Empty generators can SKIP in constructor :test-result: PASS Empty stream name opens cout stream :test-result: FAIL EndsWith string matcher :test-result: PASS Enums can quickly have stringification enabled using REGISTER_ENUM diff --git a/tests/SelfTest/Baselines/compact.sw.approved.txt b/tests/SelfTest/Baselines/compact.sw.approved.txt index c72f140d..be7a4120 100644 --- a/tests/SelfTest/Baselines/compact.sw.approved.txt +++ b/tests/SelfTest/Baselines/compact.sw.approved.txt @@ -520,6 +520,7 @@ ToString.tests.cpp:: passed: enumInfo->lookup(1) == "Value2" for: V ToString.tests.cpp:: passed: enumInfo->lookup(3) == "{** unexpected enum value **}" for: {** unexpected enum value **} == "{** unexpected enum value **}" +Skip.tests.cpp:: skipped: 'This generator is empty' Stream.tests.cpp:: passed: Catch::makeStream( "" )->isConsole() for: true Matchers.tests.cpp:: failed: testStringForMatching(), EndsWith( "Substring" ) for: "this string contains 'abc' as a substring" ends with: "Substring" Matchers.tests.cpp:: failed: testStringForMatching(), EndsWith( "this", Catch::CaseSensitive::No ) for: "this string contains 'abc' as a substring" ends with: "this" (case insensitive) @@ -2537,7 +2538,7 @@ InternalBenchmark.tests.cpp:: passed: med == 18. for: 18.0 == 18.0 InternalBenchmark.tests.cpp:: passed: q3 == 23. for: 23.0 == 23.0 Misc.tests.cpp:: passed: Misc.tests.cpp:: passed: -test cases: 408 | 308 passed | 84 failed | 5 skipped | 11 failed as expected +test cases: 409 | 308 passed | 84 failed | 6 skipped | 11 failed as expected assertions: 2225 | 2048 passed | 145 failed | 32 failed as expected diff --git a/tests/SelfTest/Baselines/compact.sw.multi.approved.txt b/tests/SelfTest/Baselines/compact.sw.multi.approved.txt index 7970ca44..6c48ab91 100644 --- a/tests/SelfTest/Baselines/compact.sw.multi.approved.txt +++ b/tests/SelfTest/Baselines/compact.sw.multi.approved.txt @@ -518,6 +518,7 @@ ToString.tests.cpp:: passed: enumInfo->lookup(1) == "Value2" for: V ToString.tests.cpp:: passed: enumInfo->lookup(3) == "{** unexpected enum value **}" for: {** unexpected enum value **} == "{** unexpected enum value **}" +Skip.tests.cpp:: skipped: 'This generator is empty' Stream.tests.cpp:: passed: Catch::makeStream( "" )->isConsole() for: true Matchers.tests.cpp:: failed: testStringForMatching(), EndsWith( "Substring" ) for: "this string contains 'abc' as a substring" ends with: "Substring" Matchers.tests.cpp:: failed: testStringForMatching(), EndsWith( "this", Catch::CaseSensitive::No ) for: "this string contains 'abc' as a substring" ends with: "this" (case insensitive) @@ -2526,7 +2527,7 @@ InternalBenchmark.tests.cpp:: passed: med == 18. for: 18.0 == 18.0 InternalBenchmark.tests.cpp:: passed: q3 == 23. for: 23.0 == 23.0 Misc.tests.cpp:: passed: Misc.tests.cpp:: passed: -test cases: 408 | 308 passed | 84 failed | 5 skipped | 11 failed as expected +test cases: 409 | 308 passed | 84 failed | 6 skipped | 11 failed as expected assertions: 2225 | 2048 passed | 145 failed | 32 failed as expected diff --git a/tests/SelfTest/Baselines/console.std.approved.txt b/tests/SelfTest/Baselines/console.std.approved.txt index 31ca04f0..0945f0df 100644 --- a/tests/SelfTest/Baselines/console.std.approved.txt +++ b/tests/SelfTest/Baselines/console.std.approved.txt @@ -383,6 +383,16 @@ Exception.tests.cpp:: FAILED: due to unexpected exception with message: custom std exception +------------------------------------------------------------------------------- +Empty generators can SKIP in constructor +------------------------------------------------------------------------------- +Skip.tests.cpp: +............................................................................... + +Skip.tests.cpp:: SKIPPED: +explicitly with message: + This generator is empty + ------------------------------------------------------------------------------- EndsWith string matcher ------------------------------------------------------------------------------- @@ -1533,6 +1543,6 @@ due to unexpected exception with message: Why would you throw a std::string? =============================================================================== -test cases: 408 | 322 passed | 69 failed | 6 skipped | 11 failed as expected +test cases: 409 | 322 passed | 69 failed | 7 skipped | 11 failed as expected assertions: 2208 | 2048 passed | 128 failed | 32 failed as expected diff --git a/tests/SelfTest/Baselines/console.sw.approved.txt b/tests/SelfTest/Baselines/console.sw.approved.txt index 68bda6cd..150980e8 100644 --- a/tests/SelfTest/Baselines/console.sw.approved.txt +++ b/tests/SelfTest/Baselines/console.sw.approved.txt @@ -3956,6 +3956,16 @@ with expansion: == "{** unexpected enum value **}" +------------------------------------------------------------------------------- +Empty generators can SKIP in constructor +------------------------------------------------------------------------------- +Skip.tests.cpp: +............................................................................... + +Skip.tests.cpp:: SKIPPED: +explicitly with message: + This generator is empty + ------------------------------------------------------------------------------- Empty stream name opens cout stream ------------------------------------------------------------------------------- @@ -18222,6 +18232,6 @@ Misc.tests.cpp: Misc.tests.cpp:: PASSED: =============================================================================== -test cases: 408 | 308 passed | 84 failed | 5 skipped | 11 failed as expected +test cases: 409 | 308 passed | 84 failed | 6 skipped | 11 failed as expected assertions: 2225 | 2048 passed | 145 failed | 32 failed as expected diff --git a/tests/SelfTest/Baselines/console.sw.multi.approved.txt b/tests/SelfTest/Baselines/console.sw.multi.approved.txt index 3f5e91d1..4cc942dd 100644 --- a/tests/SelfTest/Baselines/console.sw.multi.approved.txt +++ b/tests/SelfTest/Baselines/console.sw.multi.approved.txt @@ -3954,6 +3954,16 @@ with expansion: == "{** unexpected enum value **}" +------------------------------------------------------------------------------- +Empty generators can SKIP in constructor +------------------------------------------------------------------------------- +Skip.tests.cpp: +............................................................................... + +Skip.tests.cpp:: SKIPPED: +explicitly with message: + This generator is empty + ------------------------------------------------------------------------------- Empty stream name opens cout stream ------------------------------------------------------------------------------- @@ -18211,6 +18221,6 @@ Misc.tests.cpp: Misc.tests.cpp:: PASSED: =============================================================================== -test cases: 408 | 308 passed | 84 failed | 5 skipped | 11 failed as expected +test cases: 409 | 308 passed | 84 failed | 6 skipped | 11 failed as expected assertions: 2225 | 2048 passed | 145 failed | 32 failed as expected diff --git a/tests/SelfTest/Baselines/junit.sw.approved.txt b/tests/SelfTest/Baselines/junit.sw.approved.txt index 74e08986..c992154c 100644 --- a/tests/SelfTest/Baselines/junit.sw.approved.txt +++ b/tests/SelfTest/Baselines/junit.sw.approved.txt @@ -1,7 +1,7 @@ - + @@ -462,6 +462,13 @@ at Exception.tests.cpp: + + +SKIPPED +This generator is empty +at Skip.tests.cpp: + + diff --git a/tests/SelfTest/Baselines/junit.sw.multi.approved.txt b/tests/SelfTest/Baselines/junit.sw.multi.approved.txt index 73f37422..79c32365 100644 --- a/tests/SelfTest/Baselines/junit.sw.multi.approved.txt +++ b/tests/SelfTest/Baselines/junit.sw.multi.approved.txt @@ -1,6 +1,6 @@ - + @@ -461,6 +461,13 @@ at Exception.tests.cpp: + + +SKIPPED +This generator is empty +at Skip.tests.cpp: + + diff --git a/tests/SelfTest/Baselines/sonarqube.sw.approved.txt b/tests/SelfTest/Baselines/sonarqube.sw.approved.txt index eeb8d17b..592887f9 100644 --- a/tests/SelfTest/Baselines/sonarqube.sw.approved.txt +++ b/tests/SelfTest/Baselines/sonarqube.sw.approved.txt @@ -1870,6 +1870,13 @@ at Misc.tests.cpp: + + +SKIPPED +This generator is empty +at Skip.tests.cpp: + + SKIPPED diff --git a/tests/SelfTest/Baselines/sonarqube.sw.multi.approved.txt b/tests/SelfTest/Baselines/sonarqube.sw.multi.approved.txt index a804cb6b..3509287f 100644 --- a/tests/SelfTest/Baselines/sonarqube.sw.multi.approved.txt +++ b/tests/SelfTest/Baselines/sonarqube.sw.multi.approved.txt @@ -1869,6 +1869,13 @@ at Misc.tests.cpp: + + +SKIPPED +This generator is empty +at Skip.tests.cpp: + + SKIPPED diff --git a/tests/SelfTest/Baselines/tap.sw.approved.txt b/tests/SelfTest/Baselines/tap.sw.approved.txt index ba7d2dfa..acd0a1c1 100644 --- a/tests/SelfTest/Baselines/tap.sw.approved.txt +++ b/tests/SelfTest/Baselines/tap.sw.approved.txt @@ -984,6 +984,8 @@ ok {test-number} - enumInfo->lookup(0) == "Value1" for: Value1 == "Value1" ok {test-number} - enumInfo->lookup(1) == "Value2" for: Value2 == "Value2" # Directly creating an EnumInfo ok {test-number} - enumInfo->lookup(3) == "{** unexpected enum value **}" for: {** unexpected enum value **} == "{** unexpected enum value **}" +# Empty generators can SKIP in constructor +ok {test-number} - # SKIP 'This generator is empty' # Empty stream name opens cout stream ok {test-number} - Catch::makeStream( "" )->isConsole() for: true # EndsWith string matcher @@ -4475,5 +4477,5 @@ ok {test-number} - q3 == 23. for: 23.0 == 23.0 ok {test-number} - # xmlentitycheck ok {test-number} - -1..2236 +1..2237 diff --git a/tests/SelfTest/Baselines/tap.sw.multi.approved.txt b/tests/SelfTest/Baselines/tap.sw.multi.approved.txt index 07014c2e..03329049 100644 --- a/tests/SelfTest/Baselines/tap.sw.multi.approved.txt +++ b/tests/SelfTest/Baselines/tap.sw.multi.approved.txt @@ -982,6 +982,8 @@ ok {test-number} - enumInfo->lookup(0) == "Value1" for: Value1 == "Value1" ok {test-number} - enumInfo->lookup(1) == "Value2" for: Value2 == "Value2" # Directly creating an EnumInfo ok {test-number} - enumInfo->lookup(3) == "{** unexpected enum value **}" for: {** unexpected enum value **} == "{** unexpected enum value **}" +# Empty generators can SKIP in constructor +ok {test-number} - # SKIP 'This generator is empty' # Empty stream name opens cout stream ok {test-number} - Catch::makeStream( "" )->isConsole() for: true # EndsWith string matcher @@ -4464,5 +4466,5 @@ ok {test-number} - q3 == 23. for: 23.0 == 23.0 ok {test-number} - # xmlentitycheck ok {test-number} - -1..2236 +1..2237 diff --git a/tests/SelfTest/Baselines/teamcity.sw.approved.txt b/tests/SelfTest/Baselines/teamcity.sw.approved.txt index aff95cf6..a298633a 100644 --- a/tests/SelfTest/Baselines/teamcity.sw.approved.txt +++ b/tests/SelfTest/Baselines/teamcity.sw.approved.txt @@ -299,6 +299,9 @@ ##teamcity[testFinished name='Default scale is invisible to comparison' duration="{duration}"] ##teamcity[testStarted name='Directly creating an EnumInfo'] ##teamcity[testFinished name='Directly creating an EnumInfo' duration="{duration}"] +##teamcity[testStarted name='Empty generators can SKIP in constructor'] +##teamcity[testIgnored name='Empty generators can SKIP in constructor' message='Skip.tests.cpp:|n...............................................................................|n|nSkip.tests.cpp:|nexplicit skip with message:|n "This generator is empty"'] +##teamcity[testFinished name='Empty generators can SKIP in constructor' duration="{duration}"] ##teamcity[testStarted name='Empty stream name opens cout stream'] ##teamcity[testFinished name='Empty stream name opens cout stream' duration="{duration}"] ##teamcity[testStarted name='EndsWith string matcher'] diff --git a/tests/SelfTest/Baselines/teamcity.sw.multi.approved.txt b/tests/SelfTest/Baselines/teamcity.sw.multi.approved.txt index f16bc135..861d6471 100644 --- a/tests/SelfTest/Baselines/teamcity.sw.multi.approved.txt +++ b/tests/SelfTest/Baselines/teamcity.sw.multi.approved.txt @@ -299,6 +299,9 @@ ##teamcity[testFinished name='Default scale is invisible to comparison' duration="{duration}"] ##teamcity[testStarted name='Directly creating an EnumInfo'] ##teamcity[testFinished name='Directly creating an EnumInfo' duration="{duration}"] +##teamcity[testStarted name='Empty generators can SKIP in constructor'] +##teamcity[testIgnored name='Empty generators can SKIP in constructor' message='Skip.tests.cpp:|n...............................................................................|n|nSkip.tests.cpp:|nexplicit skip with message:|n "This generator is empty"'] +##teamcity[testFinished name='Empty generators can SKIP in constructor' duration="{duration}"] ##teamcity[testStarted name='Empty stream name opens cout stream'] ##teamcity[testFinished name='Empty stream name opens cout stream' duration="{duration}"] ##teamcity[testStarted name='EndsWith string matcher'] diff --git a/tests/SelfTest/Baselines/xml.sw.approved.txt b/tests/SelfTest/Baselines/xml.sw.approved.txt index 8509e370..bf9cf205 100644 --- a/tests/SelfTest/Baselines/xml.sw.approved.txt +++ b/tests/SelfTest/Baselines/xml.sw.approved.txt @@ -4364,6 +4364,12 @@ C + + + This generator is empty + + + @@ -21192,6 +21198,6 @@ b1! - - + + diff --git a/tests/SelfTest/Baselines/xml.sw.multi.approved.txt b/tests/SelfTest/Baselines/xml.sw.multi.approved.txt index adf83986..41dc8cb3 100644 --- a/tests/SelfTest/Baselines/xml.sw.multi.approved.txt +++ b/tests/SelfTest/Baselines/xml.sw.multi.approved.txt @@ -4364,6 +4364,12 @@ C + + + This generator is empty + + + @@ -21191,6 +21197,6 @@ b1! - - + + diff --git a/tests/SelfTest/UsageTests/Generators.tests.cpp b/tests/SelfTest/UsageTests/Generators.tests.cpp index 274c63b8..8e2c387a 100644 --- a/tests/SelfTest/UsageTests/Generators.tests.cpp +++ b/tests/SelfTest/UsageTests/Generators.tests.cpp @@ -261,6 +261,10 @@ TEST_CASE("Copy and then generate a range", "[generators]") { } } +#if defined( __clang__ ) +# pragma clang diagnostic pop +#endif + TEST_CASE("#1913 - GENERATE inside a for loop should not keep recreating the generator", "[regression][generators]") { static int counter = 0; for (int i = 0; i < 3; ++i) { @@ -305,9 +309,5 @@ TEST_CASE( "#2615 - Throwing in constructor generator fails test case but does n // this should fail the test case, but not abort the application auto sample = GENERATE( make_test_generator() ); // this assertion shouldn't trigger - REQUIRE( sample == 0U ); + REQUIRE( sample == 0 ); } - -#if defined( __clang__ ) -# pragma clang diagnostic pop -#endif diff --git a/tests/SelfTest/UsageTests/Skip.tests.cpp b/tests/SelfTest/UsageTests/Skip.tests.cpp index 6bd4189b..661795e1 100644 --- a/tests/SelfTest/UsageTests/Skip.tests.cpp +++ b/tests/SelfTest/UsageTests/Skip.tests.cpp @@ -71,3 +71,30 @@ TEST_CASE( "failing for some generator values causes entire test case to fail", FAIL(); } } + +namespace { + class test_skip_generator : public Catch::Generators::IGenerator { + public: + explicit test_skip_generator() { SKIP( "This generator is empty" ); } + + auto get() const -> int const& override { + static constexpr int value = 1; + return value; + } + + auto next() -> bool override { return false; } + }; + + static auto make_test_skip_generator() + -> Catch::Generators::GeneratorWrapper { + return { new test_skip_generator() }; + } + +} // namespace + +TEST_CASE( "Empty generators can SKIP in constructor", "[skipping]" ) { + // The generator signals emptiness with `SKIP` + auto sample = GENERATE( make_test_skip_generator() ); + // This assertion would fail, but shouldn't trigger + REQUIRE( sample == 0 ); +} From 91317366304dc6b29a6ebc9670c703192e9f4f42 Mon Sep 17 00:00:00 2001 From: Vertexwahn Date: Thu, 1 Jun 2023 21:00:56 +0200 Subject: [PATCH 05/10] Bazel support: Update skylib --- WORKSPACE.bazel | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index d962a995..a5c6182d 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -4,10 +4,10 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") http_archive( name = "bazel_skylib", - sha256 = "b8a1527901774180afc798aeb28c4634bdccf19c4d98e7bdd1ce79d1fe9aaad7", + sha256 = "66ffd9315665bfaafc96b52278f57c7e2dd09f5ede279ea6d39b2be471e7e3aa", urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.4.1/bazel-skylib-1.4.1.tar.gz", - "https://github.com/bazelbuild/bazel-skylib/releases/download/1.4.1/bazel-skylib-1.4.1.tar.gz", + "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.4.2/bazel-skylib-1.4.2.tar.gz", + "https://github.com/bazelbuild/bazel-skylib/releases/download/1.4.2/bazel-skylib-1.4.2.tar.gz", ], ) From 7a52dfa77b67b0041f7ad32b4f290b32abe48627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ho=C5=99e=C5=88ovsk=C3=BD?= Date: Sun, 11 Jun 2023 19:36:20 +0200 Subject: [PATCH 06/10] Fix typo in cross-docs links --- docs/reporters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reporters.md b/docs/reporters.md index 496c61a9..e2abfe34 100644 --- a/docs/reporters.md +++ b/docs/reporters.md @@ -52,7 +52,7 @@ its machine-readable XML output to file `result-junit.xml`, and the uses ANSI colour codes for colouring the output. Using multiple reporters (or one reporter and one-or-more [event -listeners](event-listener.md#top)) can have surprisingly complex semantics +listeners](event-listeners.md#top)) can have surprisingly complex semantics when using customization points provided to reporters by Catch2, namely capturing stdout/stderr from test cases. From c8363143e7caad34520042158468a69aeae59a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ho=C5=99e=C5=88ovsk=C3=BD?= Date: Wed, 14 Jun 2023 16:30:56 +0200 Subject: [PATCH 07/10] Add test scaffolding for catch_discover_tests --- .github/workflows/linux-other-builds.yml | 8 +- CMakeLists.txt | 1 + CMakePresets.json | 3 +- tests/CMakeLists.txt | 12 ++ .../TestScripts/DiscoverTests/CMakeLists.txt | 16 +++ .../DiscoverTests/VerifyRegistration.py | 117 ++++++++++++++++++ .../DiscoverTests/register-tests.cpp | 11 ++ 7 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 tests/TestScripts/DiscoverTests/CMakeLists.txt create mode 100644 tests/TestScripts/DiscoverTests/VerifyRegistration.py create mode 100644 tests/TestScripts/DiscoverTests/register-tests.cpp diff --git a/.github/workflows/linux-other-builds.yml b/.github/workflows/linux-other-builds.yml index cf4e2c06..da24104d 100644 --- a/.github/workflows/linux-other-builds.yml +++ b/.github/workflows/linux-other-builds.yml @@ -29,13 +29,13 @@ jobs: build_type: Debug std: 14 other_pkgs: g++-7 - cmake_configurations: -DCATCH_BUILD_EXTRA_TESTS=ON -DCATCH_BUILD_EXAMPLES=ON + cmake_configurations: -DCATCH_BUILD_EXTRA_TESTS=ON -DCATCH_BUILD_EXAMPLES=ON -DCATCH_ENABLE_CMAKE_HELPER_TESTS=ON - cxx: g++-7 build_description: Extras + Examples build_type: Release std: 14 other_pkgs: g++-7 - cmake_configurations: -DCATCH_BUILD_EXTRA_TESTS=ON -DCATCH_BUILD_EXAMPLES=ON + cmake_configurations: -DCATCH_BUILD_EXTRA_TESTS=ON -DCATCH_BUILD_EXAMPLES=ON -DCATCH_ENABLE_CMAKE_HELPER_TESTS=ON # Extras and examples with Clang-10 - cxx: clang++-10 @@ -43,13 +43,13 @@ jobs: build_type: Debug std: 17 other_pkgs: clang-10 - cmake_configurations: -DCATCH_BUILD_EXTRA_TESTS=ON -DCATCH_BUILD_EXAMPLES=ON + cmake_configurations: -DCATCH_BUILD_EXTRA_TESTS=ON -DCATCH_BUILD_EXAMPLES=ON -DCATCH_ENABLE_CMAKE_HELPER_TESTS=ON - cxx: clang++-10 build_description: Extras + Examples build_type: Release std: 17 other_pkgs: clang-10 - cmake_configurations: -DCATCH_BUILD_EXTRA_TESTS=ON -DCATCH_BUILD_EXAMPLES=ON + cmake_configurations: -DCATCH_BUILD_EXTRA_TESTS=ON -DCATCH_BUILD_EXAMPLES=ON -DCATCH_ENABLE_CMAKE_HELPER_TESTS=ON # Configure tests with Clang-10 - cxx: clang++-10 diff --git a/CMakeLists.txt b/CMakeLists.txt index b3e81153..5ef06de7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,7 @@ cmake_dependent_option(CATCH_ENABLE_COVERAGE "Generate coverage for codecov.io" cmake_dependent_option(CATCH_ENABLE_WERROR "Enables Werror during build" ON "CATCH_DEVELOPMENT_BUILD" OFF) cmake_dependent_option(CATCH_BUILD_SURROGATES "Enable generating and building surrogate TUs for the main headers" OFF "CATCH_DEVELOPMENT_BUILD" OFF) cmake_dependent_option(CATCH_ENABLE_CONFIGURE_TESTS "Enable CMake configuration tests. WARNING: VERY EXPENSIVE" OFF "CATCH_DEVELOPMENT_BUILD" OFF) +cmake_dependent_option(CATCH_ENABLE_CMAKE_HELPER_TESTS "Enable CMake helper tests. WARNING: VERY EXPENSIVE" OFF "CATCH_DEVELOPMENT_BUILD" OFF) # Catch2's build breaks if done in-tree. You probably should not build diff --git a/CMakePresets.json b/CMakePresets.json index 00f3a6d3..88541285 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -18,7 +18,8 @@ "CATCH_BUILD_EXAMPLES": "ON", "CATCH_BUILD_EXTRA_TESTS": "ON", "CATCH_BUILD_SURROGATES": "ON", - "CATCH_ENABLE_CONFIGURE_TESTS": "ON" + "CATCH_ENABLE_CONFIGURE_TESTS": "ON", + "CATCH_ENABLE_CMAKE_HELPER_TESTS": "ON" } } ] diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7be57abe..14a68e34 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -622,6 +622,18 @@ if (CATCH_ENABLE_CONFIGURE_TESTS) endforeach() endif() +if (CATCH_ENABLE_CMAKE_HELPER_TESTS) + add_test(NAME "CMakeHelper::DiscoverTests" + COMMAND + "${PYTHON_EXECUTABLE}" "${CMAKE_CURRENT_LIST_DIR}/TestScripts/DiscoverTests/VerifyRegistration.py" "${CATCH_DIR}" "${CMAKE_CURRENT_BINARY_DIR}" + ) + set_tests_properties("CMakeHelper::DiscoverTests" + PROPERTIES + COST 240 + LABELS "uses-python" + ) +endif() + foreach (reporterName # "Automake" - the simple .trs format does not support any kind of comments/metadata "compact" "console" diff --git a/tests/TestScripts/DiscoverTests/CMakeLists.txt b/tests/TestScripts/DiscoverTests/CMakeLists.txt new file mode 100644 index 00000000..d19f2f88 --- /dev/null +++ b/tests/TestScripts/DiscoverTests/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.10) + +project(discover-tests-test + LANGUAGES CXX +) + +add_executable(tests + register-tests.cpp +) + +add_subdirectory(${CATCH2_PATH} catch2-build) +target_link_libraries(tests PRIVATE Catch2::Catch2WithMain) + +include(CTest) +include(Catch) +catch_discover_tests(tests) diff --git a/tests/TestScripts/DiscoverTests/VerifyRegistration.py b/tests/TestScripts/DiscoverTests/VerifyRegistration.py new file mode 100644 index 00000000..058499e1 --- /dev/null +++ b/tests/TestScripts/DiscoverTests/VerifyRegistration.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +# 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 + +import os +import subprocess +import sys + + +def build_project(sources_dir, output_base_path, catch2_path): + build_dir = os.path.join(output_base_path, 'ctest-registration-test') + config_cmd = ['cmake', + '-B', build_dir, + '-S', sources_dir, + f'-DCATCH2_PATH={catch2_path}', + '-DCMAKE_BUILD_TYPE=Debug'] + + build_cmd = ['cmake', + '--build', build_dir, + '--config', 'Debug'] + + try: + subprocess.run(config_cmd, + capture_output = True, + check = True, + text = True) + subprocess.run(build_cmd, + capture_output = True, + check = True, + text = True) + except subprocess.CalledProcessError as err: + print('Error when building the test project') + print(f'cmd: {err.cmd}') + print(f'stderr: {err.stderr}') + print(f'stdout: {err.stdout}') + exit(3) + + return build_dir + + + +def get_test_names(build_path): + # For now we assume that Windows builds are done using MSBuild under + # Debug configuration. This means that we need to add "Debug" folder + # to the path when constructing it. On Linux, we don't add anything. + config_path = "Debug" if os.name == 'nt' else "" + full_path = os.path.join(build_path, config_path, 'tests') + + + cmd = [full_path, '--reporter', 'xml', '--list-tests'] + result = subprocess.run(cmd, + capture_output = True, + check = True, + text = True) + + import xml.etree.ElementTree as ET + root = ET.fromstring(result.stdout) + return [tc.text for tc in root.findall('TestCase/Name')] + + +def list_ctest_tests(build_path): + old_path = os.getcwd() + os.chdir(build_path) + + cmd = ['ctest', '-C', 'debug', '--show-only=json-v1'] + result = subprocess.run(cmd, + capture_output = True, + check = True, + text = True) + os.chdir(old_path) + + import json + + ctest_response = json.loads(result.stdout) + tests = ctest_response['tests'] + test_names = [] + for test in tests: + test_command = test['command'] + # First part of the command is the binary, second is the filter. + # If there are less, registration has failed. If there are more, + # registration has changed and the script needs updating. + assert len(test_command) == 2 + test_names.append(test_command[1]) + test_name = test_command[1] + + return test_names + + +if __name__ == '__main__': + if len(sys.argv) != 3: + print(f'Usage: {sys.argv[0]} path-to-catch2-cml output-path') + exit(2) + catch2_path = sys.argv[1] + output_base_path = sys.argv[2] + sources_dir = os.path.dirname(os.path.abspath(sys.argv[0])) + + build_path = build_project(sources_dir, output_base_path, catch2_path) + + catch_test_names = get_test_names(build_path) + ctest_test_names = list_ctest_tests(build_path) + + if len(catch_test_names) != len(ctest_test_names): + print("Mismatch between catch test names and ctest test names!") + for catch_test in catch_test_names: + if catch_test not in ctest_test_names: + print(f"Catch2 test '{catch_test}' not found in CTest") + for ctest_test in ctest_test_names: + if ctest_test not in catch_test_names: + print(f"CTest test '{ctest_test}' not found in Catch2") + + exit(1) + diff --git a/tests/TestScripts/DiscoverTests/register-tests.cpp b/tests/TestScripts/DiscoverTests/register-tests.cpp new file mode 100644 index 00000000..91382eaa --- /dev/null +++ b/tests/TestScripts/DiscoverTests/register-tests.cpp @@ -0,0 +1,11 @@ + +// 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 + +#include + +TEST_CASE("Some test") {} From a0c6a28460872bfbdbc2ffc4c7ad19f352343586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ho=C5=99e=C5=88ovsk=C3=BD?= Date: Wed, 14 Jun 2023 23:31:41 +0200 Subject: [PATCH 08/10] Fix possible FP in catch_discover_tests tests --- .../DiscoverTests/VerifyRegistration.py | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/tests/TestScripts/DiscoverTests/VerifyRegistration.py b/tests/TestScripts/DiscoverTests/VerifyRegistration.py index 058499e1..aea0f012 100644 --- a/tests/TestScripts/DiscoverTests/VerifyRegistration.py +++ b/tests/TestScripts/DiscoverTests/VerifyRegistration.py @@ -19,11 +19,11 @@ def build_project(sources_dir, output_base_path, catch2_path): '-S', sources_dir, f'-DCATCH2_PATH={catch2_path}', '-DCMAKE_BUILD_TYPE=Debug'] - + build_cmd = ['cmake', '--build', build_dir, '--config', 'Debug'] - + try: subprocess.run(config_cmd, capture_output = True, @@ -39,9 +39,9 @@ def build_project(sources_dir, output_base_path, catch2_path): print(f'stderr: {err.stderr}') print(f'stdout: {err.stdout}') exit(3) - + return build_dir - + def get_test_names(build_path): @@ -104,14 +104,16 @@ if __name__ == '__main__': catch_test_names = get_test_names(build_path) ctest_test_names = list_ctest_tests(build_path) - if len(catch_test_names) != len(ctest_test_names): - print("Mismatch between catch test names and ctest test names!") - for catch_test in catch_test_names: - if catch_test not in ctest_test_names: - print(f"Catch2 test '{catch_test}' not found in CTest") - for ctest_test in ctest_test_names: - if ctest_test not in catch_test_names: - print(f"CTest test '{ctest_test}' not found in Catch2") + mismatched = 0 + for catch_test in catch_test_names: + if catch_test not in ctest_test_names: + print(f"Catch2 test '{catch_test}' not found in CTest") + mismatched += 1 + for ctest_test in ctest_test_names: + if ctest_test not in catch_test_names: + print(f"CTest test '{ctest_test}' not found in Catch2") + mismatched += 1 + if mismatched: + print(f"Found {mismatched} mismatched tests catch test names and ctest test commands!") exit(1) - From 42ee66b5e625b7ed43727e3617af58ca8fad4a12 Mon Sep 17 00:00:00 2001 From: Robin Christ Date: Wed, 14 Jun 2023 23:40:10 +0200 Subject: [PATCH 09/10] Fix handling of semicolon and backslash characters in CMake test discovery (#2676) This PR fixes the handling of semicolon and backslash characters in test names in the CMake test discovery Closes #2674 --- extras/CatchAddTests.cmake | 15 ++++++++++----- .../TestScripts/DiscoverTests/register-tests.cpp | 1 + 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/extras/CatchAddTests.cmake b/extras/CatchAddTests.cmake index 91f79f3c..46d7a2a9 100644 --- a/extras/CatchAddTests.cmake +++ b/extras/CatchAddTests.cmake @@ -74,6 +74,10 @@ function(catch_discover_tests_impl) ) endif() + # Make sure to escape ; (semicolons) in test names first, because + # that'd break the foreach loop for "Parse output" later and create + # wrongly splitted and thus failing test cases (false positives) + string(REPLACE ";" "\;" output "${output}") string(REPLACE "\n" ";" output "${output}") # Prepare reporter @@ -119,15 +123,16 @@ function(catch_discover_tests_impl) # Parse output foreach(line ${output}) - set(test ${line}) + set(test "${line}") # Escape characters in test case names that would be parsed by Catch2 - set(test_name ${test}) - foreach(char , [ ]) - string(REPLACE ${char} "\\${char}" test_name ${test_name}) + # Note that the \ escaping must happen FIRST! Do not change the order. + set(test_name "${test}") + foreach(char \\ , [ ]) + string(REPLACE ${char} "\\${char}" test_name "${test_name}") endforeach(char) # ...add output dir if(output_dir) - string(REGEX REPLACE "[^A-Za-z0-9_]" "_" test_name_clean ${test_name}) + string(REGEX REPLACE "[^A-Za-z0-9_]" "_" test_name_clean "${test_name}") set(output_dir_arg "--out ${output_dir}/${output_prefix}${test_name_clean}${output_suffix}") endif() diff --git a/tests/TestScripts/DiscoverTests/register-tests.cpp b/tests/TestScripts/DiscoverTests/register-tests.cpp index 91382eaa..21238d59 100644 --- a/tests/TestScripts/DiscoverTests/register-tests.cpp +++ b/tests/TestScripts/DiscoverTests/register-tests.cpp @@ -8,4 +8,5 @@ #include +TEST_CASE("@Script[C:\\EPM1A]=x;\"SCALA_ZERO:\"", "[script regressions]"){} TEST_CASE("Some test") {} From e4b16053a6763cd6f8b89aeaf59303a461ccf755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ho=C5=99e=C5=88ovsk=C3=BD?= Date: Thu, 15 Jun 2023 14:19:39 +0200 Subject: [PATCH 10/10] Escape Catch2 test names in catch_discover_tests tests --- tests/TestScripts/DiscoverTests/VerifyRegistration.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/TestScripts/DiscoverTests/VerifyRegistration.py b/tests/TestScripts/DiscoverTests/VerifyRegistration.py index aea0f012..9ec42f24 100644 --- a/tests/TestScripts/DiscoverTests/VerifyRegistration.py +++ b/tests/TestScripts/DiscoverTests/VerifyRegistration.py @@ -90,6 +90,10 @@ def list_ctest_tests(build_path): return test_names +def escape_catch2_test_name(name): + for char in ('\\', ',', '[', ']'): + name = name.replace(char, f"\\{char}") + return name if __name__ == '__main__': if len(sys.argv) != 3: @@ -101,7 +105,7 @@ if __name__ == '__main__': build_path = build_project(sources_dir, output_base_path, catch2_path) - catch_test_names = get_test_names(build_path) + catch_test_names = [escape_catch2_test_name(name) for name in get_test_names(build_path)] ctest_test_names = list_ctest_tests(build_path) mismatched = 0