From 7991d1c0cd53d6c03155b513392a1ab1cd2c4a98 Mon Sep 17 00:00:00 2001 From: ShivaPriyanShanmuga Date: Sat, 20 Jun 2026 18:36:42 -0400 Subject: [PATCH] Ignore non-JSON stdout preamble in catch_discover_tests (#3162) catch_discover_tests runs the test executable with --list-tests --reporter json and parses the whole stdout as JSON. If a third-party library writes to stdout from a static initializer, that text precedes Catch2's JSON output and breaks the parser. Strip everything before the first '{' (where Catch2's JSON starts) before parsing, so such startup output is ignored. The registration test now prints from a static initializer to cover this, and the Python verifier skips the preamble the same way. --- extras/CatchAddTests.cmake | 13 +++++++++++++ .../DiscoverTests/VerifyRegistration.py | 7 ++++++- tests/TestScripts/DiscoverTests/register-tests.cpp | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/extras/CatchAddTests.cmake b/extras/CatchAddTests.cmake index 4c27f479ec..285a9295d3 100644 --- a/extras/CatchAddTests.cmake +++ b/extras/CatchAddTests.cmake @@ -134,6 +134,19 @@ function(catch_discover_tests_impl) endforeach() endif() + # The test executable might print to stdout before Catch2 emits its JSON, + # e.g. a third-party library logging from a static initializer. Such text + # would break the JSON parser, so we strip everything before the first '{', + # which is where Catch2's JSON output starts. + string(FIND "${listing_output}" "{" json_start) + if(json_start EQUAL -1) + message(FATAL_ERROR + "Could not find the start of JSON output when listing tests from executable '${_TEST_EXECUTABLE}':\n" + " Output: ${listing_output}\n" + ) + endif() + string(SUBSTRING "${listing_output}" ${json_start} -1 listing_output) + # Parse JSON output for list of tests/class names/tags string(JSON version GET "${listing_output}" "version") if(NOT version STREQUAL "1") diff --git a/tests/TestScripts/DiscoverTests/VerifyRegistration.py b/tests/TestScripts/DiscoverTests/VerifyRegistration.py index 7d9862cfb9..48eb584c3a 100644 --- a/tests/TestScripts/DiscoverTests/VerifyRegistration.py +++ b/tests/TestScripts/DiscoverTests/VerifyRegistration.py @@ -79,7 +79,12 @@ def get_test_names(build_path: str) -> List[TestInfo]: check = True, text = True) - test_listing = json.loads(result.stdout) + # The executable might print to stdout before Catch2's JSON output, e.g. + # from a third-party library's static initializer, so we skip everything + # before the first '{', just like CatchAddTests.cmake does. + json_start = result.stdout.find('{') + assert json_start != -1, f"Could not find JSON output in:\n{result.stdout}" + test_listing = json.loads(result.stdout[json_start:]) assert test_listing['version'] == 1 diff --git a/tests/TestScripts/DiscoverTests/register-tests.cpp b/tests/TestScripts/DiscoverTests/register-tests.cpp index be533ab69b..5575e33381 100644 --- a/tests/TestScripts/DiscoverTests/register-tests.cpp +++ b/tests/TestScripts/DiscoverTests/register-tests.cpp @@ -8,6 +8,20 @@ #include +#include + +namespace { + // Emulate a third-party library that writes to stdout from a static + // initializer. This text precedes Catch2's JSON output during test + // discovery, and `catch_discover_tests` has to ignore it. See #3162. + struct PrintsDuringStaticInit { + PrintsDuringStaticInit() { + std::cout << "Some third-party library started up successfully\n"; + } + }; + const PrintsDuringStaticInit printsDuringStaticInit{}; +} + TEST_CASE("@Script[C:\\EPM1A]=x;\"SCALA_ZERO:\"", "[script regressions]"){} TEST_CASE("Some test") {} TEST_CASE( "Let's have a test case with a long name. Longer. No, even longer. "