diff options
| author | Leitwolf11 | 2026-10-24 12:41:08 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-10-24 12:41:08 +0300 |
| commit | e18f19b7dd70c99fbcf2b3fe812ee18dabc92663 (patch) | |
| tree | 4cfc1a3c51ade9b8281925d1c122c12c87f8e381 /src | |
| parent | e104b694513964d5a84e77f751203cda970d741b (diff) | |
| download | catch3-e18f19b7dd70c99fbcf2b3fe812ee18dabc92663.tar.gz | |
End the output capture when a test exits, fail on missing reports
A test that called exit() crashed its worker with SIGSEGV on Linux and
macOS. The exit hook read Catch2's captured output through a scoped
deactivation, which switched the capture back on; libstdc++ then flushed
std::cout at exit into a capture buffer that static destruction had
already freed. RunContext::endCapture() now ends the capture for good and
returns what it held, so the standard streams are their own again.
A JUnit, JSON or trace report that could not be written was only
reported on stderr, and a run whose tests passed still exited with 0, so a
build server could miss it. Such a run now exits with 1; failed tests
still exit with 42.
Integration tests of the runner's output move their helpers into
RunnerFixtureClient.hpp and cover --version, --help, the listing modes, a
bad option, exit codes 1 to 4, -v, -q, --slowest, colours, --analytics
in text and JSON, and Bazel's test environment.
Diffstat (limited to 'src')
| -rw-r--r-- | src/Progmasoft/Runner/Execution.cpp | 2 | ||||
| -rw-r--r-- | src/Progmasoft/Runner/Runner.cpp | 257 | ||||
| -rw-r--r-- | src/catch2/internal/catch_run_context.cpp | 4 | ||||
| -rw-r--r-- | src/catch2/internal/catch_run_context.hpp | 9 |
4 files changed, 161 insertions, 111 deletions
diff --git a/src/Progmasoft/Runner/Execution.cpp b/src/Progmasoft/Runner/Execution.cpp index ab1e92c4..77a4771a 100644 --- a/src/Progmasoft/Runner/Execution.cpp +++ b/src/Progmasoft/Runner/Execution.cpp @@ -507,7 +507,7 @@ namespace Progmasoft::Catch3::Runner { return; } - const std::string output = state->context->capturedOutput(); + const std::string output = state->context->endCapture(); std::fflush(stdout); std::fflush(stderr); WriteStandardError(output); diff --git a/src/Progmasoft/Runner/Runner.cpp b/src/Progmasoft/Runner/Runner.cpp index f85af36b..c4a6feb9 100644 --- a/src/Progmasoft/Runner/Runner.cpp +++ b/src/Progmasoft/Runner/Runner.cpp @@ -1,24 +1,9 @@ // Copyright (c) 2026 Progmasoft. // SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 -#include "Analysis.hpp" -#include "Catalog.hpp" -#include "Console.hpp" -#include "Execution.hpp" -#include "Files.hpp" -#include "Git.hpp" -#include "Hash.hpp" -#include "History.hpp" -#include "Json.hpp" -#include "Options.hpp" -#include "Planner.hpp" -#include "Process.hpp" -#include "ResultsJson.hpp" -#include "Scheduler.hpp" - #include <Progmasoft/Catch3/Runner.hpp> #include <Progmasoft/Catch3/Version.hpp> #include <Progmasoft/Catch3/XmlWriter.hpp> - +#include <algorithm> #include <catch2/catch_config.hpp> #include <catch2/catch_session.hpp> #include <catch2/catch_user_config.hpp> @@ -28,14 +13,27 @@ #include <catch2/internal/catch_context.hpp> #include <catch2/internal/catch_startup_exception_registry.hpp> #include <catch2/internal/catch_test_case_registry_impl.hpp> - -#include <algorithm> #include <chrono> #include <cstdio> #include <exception> #include <map> #include <random> +#include "Analysis.hpp" +#include "Catalog.hpp" +#include "Console.hpp" +#include "Execution.hpp" +#include "Files.hpp" +#include "Git.hpp" +#include "Hash.hpp" +#include "History.hpp" +#include "Json.hpp" +#include "Options.hpp" +#include "Planner.hpp" +#include "Process.hpp" +#include "ResultsJson.hpp" +#include "Scheduler.hpp" + namespace Progmasoft::Catch3 { namespace @@ -103,19 +101,22 @@ namespace Progmasoft::Catch3 { options.junitPath = xml; } - const std::string filter = - Runner::ReadEnvironment("TESTBRIDGE_TEST_ONLY"); + const std::string filter + = Runner::ReadEnvironment("TESTBRIDGE_TEST_ONLY"); if (!filter.empty()) { - options.testSpecs = {filter}; + options.testSpecs = { filter }; } - const std::string index = Runner::ReadEnvironment("TEST_SHARD_INDEX"); - const std::string total = Runner::ReadEnvironment("TEST_TOTAL_SHARDS"); - const std::string status = - Runner::ReadEnvironment("TEST_SHARD_STATUS_FILE"); + const std::string index + = Runner::ReadEnvironment("TEST_SHARD_INDEX"); + const std::string total + = Runner::ReadEnvironment("TEST_TOTAL_SHARDS"); + const std::string status + = Runner::ReadEnvironment("TEST_SHARD_STATUS_FILE"); if (!index.empty() && !total.empty()) { - auto parsed = Runner::ParseOptions({"--shard=" + index + "/" + total}); + auto parsed = Runner::ParseOptions( + { "--shard=" + index + "/" + total }); if (parsed) { options.shard = parsed->shard; @@ -128,29 +129,32 @@ namespace Progmasoft::Catch3 } else { - notes.push_back("ignoring Bazel's shard " + index + "/" + - total + ": " + parsed.GetError().Message()); + notes.push_back("ignoring Bazel's shard " + index + "/" + + total + ": " + + parsed.GetError().Message()); } } - const std::string seed = Runner::ReadEnvironment("TEST_RANDOM_SEED"); + const std::string seed + = Runner::ReadEnvironment("TEST_RANDOM_SEED"); if (!seed.empty()) { - auto parsed = Runner::ParseOptions({"--seed=" + seed}); + auto parsed = Runner::ParseOptions({ "--seed=" + seed }); if (parsed) { options.seed = parsed->seed; options.seedGiven = true; } } - const std::string outputs = - Runner::ReadEnvironment("TEST_UNDECLARED_OUTPUTS_DIR"); + const std::string outputs + = Runner::ReadEnvironment("TEST_UNDECLARED_OUTPUTS_DIR"); if (!outputs.empty() && options.jsonPath.empty()) { - options.jsonPath = - (std::filesystem::path{outputs} / "catch3.json").string(); + options.jsonPath + = (std::filesystem::path{ outputs } / "catch3.json") + .string(); } - if (options.historyDirectory.empty() && - !Runner::HasEnvironment("CATCH3_HISTORY")) + if (options.historyDirectory.empty() + && !Runner::HasEnvironment("CATCH3_HISTORY")) { // A sandboxed test has nowhere lasting to keep history. options.historyEnabled = false; @@ -165,8 +169,8 @@ namespace Progmasoft::Catch3 { return options.historyDirectory; } - const std::string fromEnvironment = - Runner::ReadEnvironment("CATCH3_HISTORY"); + const std::string fromEnvironment + = Runner::ReadEnvironment("CATCH3_HISTORY"); if (!fromEnvironment.empty()) { return fromEnvironment; @@ -217,7 +221,8 @@ namespace Progmasoft::Catch3 if (entry.timeout) { test.Set("timeoutSeconds", - static_cast<double>(entry.timeout->count()) / 1000.0); + static_cast<double>(entry.timeout->count()) + / 1000.0); } if (entry.retries) { @@ -261,7 +266,8 @@ namespace Progmasoft::Catch3 case Runner::ListFormat::kNames: for (const std::size_t index : matched) { - std::printf("%s\n", catalog.Tests()[index].name.c_str()); + std::printf("%s\n", + catalog.Tests()[index].name.c_str()); } break; case Runner::ListFormat::kText: @@ -340,15 +346,15 @@ namespace Progmasoft::Catch3 const auto median = [](std::vector<std::int64_t> values) { if (values.empty()) { - return std::int64_t{0}; + return std::int64_t{ 0 }; } std::sort(values.begin(), values.end()); return values[values.size() / 2]; }; row.failureRate = row.runs == 0 ? 0.0 - : static_cast<double>(failures) / - static_cast<double>(row.runs); + : static_cast<double>(failures) + / static_cast<double>(row.runs); row.flakyEvents = test.FlakyEvents(20); row.median = test.DurationPercentile(0.5).value_or(0); row.recentMedian = median(recent); @@ -373,17 +379,26 @@ namespace Progmasoft::Catch3 ordered.resize(std::min(count, ordered.size())); return ordered; }; - const auto flaky = - top([](const Row &row) { return static_cast<double>(row.flakyEvents); }, 10); - const auto failing = - top([](const Row &row) { return row.failureRate; }, 10); - const auto slowest = - top([](const Row &row) { return static_cast<double>(row.median); }, 10); + const auto flaky = top( + [](const Row &row) { + return static_cast<double>(row.flakyEvents); + }, + 10); + const auto failing = top( + [](const Row &row) { + return row.failureRate; + }, + 10); + const auto slowest = top( + [](const Row &row) { + return static_cast<double>(row.median); + }, + 10); const auto slower = top( [](const Row &row) { return row.olderMedian > 0 - ? static_cast<double>(row.recentMedian) / - static_cast<double>(row.olderMedian) + ? static_cast<double>(row.recentMedian) + / static_cast<double>(row.olderMedian) : 0.0; }, 10); @@ -425,7 +440,9 @@ namespace Progmasoft::Catch3 { if (row->flakyEvents > 0) { - std::printf(" %3zu %s\n", row->flakyEvents, row->key.c_str()); + std::printf(" %3zu %s\n", + row->flakyEvents, + row->key.c_str()); } } std::printf("\nin quarantine:\n"); @@ -433,7 +450,9 @@ namespace Progmasoft::Catch3 { if (!row.quarantine.empty()) { - std::printf(" %s: %s\n", row.key.c_str(), row.quarantine.c_str()); + std::printf(" %s: %s\n", + row.key.c_str(), + row.quarantine.c_str()); } } std::printf("\nfailing most often:\n"); @@ -453,11 +472,12 @@ namespace Progmasoft::Catch3 FormatMicroseconds(row->median).c_str(), row->key.c_str()); } - std::printf("\ngetting slower (last 5 runs against the 15 before):\n"); + std::printf( + "\ngetting slower (last 5 runs against the 15 before):\n"); for (const Row *row : slower) { - if (row->olderMedian > 0 && - row->recentMedian > row->olderMedian * 3 / 2) + if (row->olderMedian > 0 + && row->recentMedian > row->olderMedian * 3 / 2) { std::printf(" %10s -> %-10s %s\n", FormatMicroseconds(row->olderMedian).c_str(), @@ -473,7 +493,7 @@ namespace Progmasoft::Catch3 { for (const auto &entry : catalog.Tests()) { - const std::filesystem::path file{entry.file}; + const std::filesystem::path file{ entry.file }; if (file.is_absolute()) { return file.parent_path(); @@ -496,8 +516,8 @@ namespace Progmasoft::Catch3 { const std::filesystem::path program = Runner::CurrentExecutable( arguments.empty() ? nullptr : arguments.front().c_str()); - const std::string programName = - program.empty() ? "tests" : program.stem().string(); + const std::string programName + = program.empty() ? "tests" : program.stem().string(); std::vector<std::string> rest; if (arguments.size() > 1) { @@ -554,7 +574,8 @@ namespace Progmasoft::Catch3 PrintError(catalog.GetError().Message()); return Catch::UnspecifiedErrorExitCode; } - const Catch::Config config{Runner::ToConfigData(options, programName)}; + const Catch::Config config{ Runner::ToConfigData(options, + programName) }; const auto &invalid = config.testSpec().getInvalidSpecs(); if (!invalid.empty()) { @@ -566,8 +587,8 @@ namespace Progmasoft::Catch3 } std::vector<std::string> unmatched; auto matched = catalog->Match(config, unmatched); - if (options.mode == Runner::Mode::kListTests || - options.mode == Runner::Mode::kListTags) + if (options.mode == Runner::Mode::kListTests + || options.mode == Runner::Mode::kListTags) { if (options.shard.count > 1) { @@ -594,10 +615,12 @@ namespace Progmasoft::Catch3 return List(options, *catalog, matched, programName); } - const std::filesystem::path historyFile = Runner::HistoryFile( - HistoryDirectory(options, program), programName); + const std::filesystem::path historyFile + = Runner::HistoryFile(HistoryDirectory(options, program), + programName); Runner::History history; - if (options.historyEnabled || options.mode == Runner::Mode::kAnalytics) + if (options.historyEnabled + || options.mode == Runner::Mode::kAnalytics) { auto loaded = Runner::History::Load(historyFile); if (loaded) @@ -606,8 +629,8 @@ namespace Progmasoft::Catch3 } else { - notes.push_back("the history could not be read: " + - loaded.GetError().Message()); + notes.push_back("the history could not be read: " + + loaded.GetError().Message()); } } if (options.mode == Runner::Mode::kAnalytics) @@ -634,14 +657,14 @@ namespace Progmasoft::Catch3 { changes = std::move(*read); } - else if (options.selection == Runner::Selection::kChanged || - options.selection == Runner::Selection::kSmart) + else if (options.selection == Runner::Selection::kChanged + || options.selection == Runner::Selection::kSmart) { notes.push_back(read.GetError().Message()); } }; - if (options.selection == Runner::Selection::kChanged || - options.selection == Runner::Selection::kSmart) + if (options.selection == Runner::Selection::kChanged + || options.selection == Runner::Selection::kSmart) { readChanges(); } @@ -661,17 +684,20 @@ namespace Progmasoft::Catch3 } notes.insert(notes.end(), plan->notes.begin(), plan->notes.end()); - Runner::ConsoleReport console{options, stdout}; - const std::size_t workers = - options.inProcess - ? 1 - : std::max<std::size_t>( - 1, - std::min<std::size_t>( - options.jobs == 0 ? Runner::HardwareThreads() - : options.jobs, - plan->tests.size())); - console.RunStarting(plan->tests.size(), workers, options.seed, notes); + Runner::ConsoleReport console{ options, stdout }; + const std::size_t workers + = options.inProcess + ? 1 + : std::max<std::size_t>( + 1, + std::min<std::size_t>( + options.jobs == 0 ? Runner::HardwareThreads() + : options.jobs, + plan->tests.size())); + console.RunStarting(plan->tests.size(), + workers, + options.seed, + notes); std::string guard = options.exitGuardPath; if (BazelTest() && guard.empty()) @@ -682,8 +708,8 @@ namespace Progmasoft::Catch3 { if (auto status = Runner::CreatePrivateFile(guard); !status) { - PrintError("the exit guard file: " + - status.GetError().Message()); + PrintError("the exit guard file: " + + status.GetError().Message()); return Catch::UnspecifiedErrorExitCode; } } @@ -701,14 +727,20 @@ namespace Progmasoft::Catch3 events.testFinished = [&](const TestResult &test) { console.TestFinished(test); }; - events.retrying = [&](const TestResult &test, const Attempt &attempt) { - console.Retrying(test, attempt); - }; - auto outcome = Runner::RunPlan( - options, *plan, *catalog, program.string(), programName, events); - run.wallMicroseconds = std::chrono::duration_cast<std::chrono::microseconds>( - std::chrono::steady_clock::now() - started) - .count(); + events.retrying + = [&](const TestResult &test, const Attempt &attempt) { + console.Retrying(test, attempt); + }; + auto outcome = Runner::RunPlan(options, + *plan, + *catalog, + program.string(), + programName, + events); + run.wallMicroseconds + = std::chrono::duration_cast<std::chrono::microseconds>( + std::chrono::steady_clock::now() - started) + .count(); run.workers = outcome.workers; run.tests = std::move(outcome.tests); for (auto &kept : plan->kept) @@ -717,10 +749,12 @@ namespace Progmasoft::Catch3 } run.notes = std::move(outcome.notes); - const bool anyFailure = std::any_of( - run.tests.begin(), run.tests.end(), [](const TestResult &test) { - return IsFailure(test.outcome) || test.flaky; - }); + const bool anyFailure + = std::any_of(run.tests.begin(), + run.tests.end(), + [](const TestResult &test) { + return IsFailure(test.outcome) || test.flaky; + }); if (anyFailure) { readChanges(); @@ -734,31 +768,40 @@ namespace Progmasoft::Catch3 Runner::Analyze(run, analysis); console.RunEnded(run); + // A report that was asked for and is missing would let a build + // server miss the run, so it fails the run. + bool reportMissing = false; if (!options.junitPath.empty()) { if (auto status = Runner::WriteFileAtomically( - options.junitPath, JUnitXmlWriter::ToString(run)); + options.junitPath, + JUnitXmlWriter::ToString(run)); !status) { PrintError(status.GetError().Message()); + reportMissing = true; } } if (!options.jsonPath.empty()) { if (auto status = Runner::WriteFileAtomically( - options.jsonPath, Runner::ToJson(run).ToPrettyString()); + options.jsonPath, + Runner::ToJson(run).ToPrettyString()); !status) { PrintError(status.GetError().Message()); + reportMissing = true; } } if (!options.tracePath.empty()) { - if (auto status = Runner::WriteFileAtomically( - options.tracePath, Runner::TraceJson(run)); + if (auto status + = Runner::WriteFileAtomically(options.tracePath, + Runner::TraceJson(run)); !status) { PrintError(status.GetError().Message()); + reportMissing = true; } } if (options.historyEnabled && !run.tests.empty()) @@ -800,8 +843,8 @@ namespace Progmasoft::Catch3 { return Catch::UnmatchedTestSpecExitCode; } - const bool quarantineCounts = - options.quarantine == Runner::QuarantineMode::kReport; + const bool quarantineCounts + = options.quarantine == Runner::QuarantineMode::kReport; bool failed = false; std::size_t ran = 0; std::size_t skipped = 0; @@ -812,14 +855,18 @@ namespace Progmasoft::Catch3 ++ran; } skipped += test.outcome == Outcome::kSkipped ? 1 : 0; - failed = failed || test.FailsRun() || - (quarantineCounts && test.quarantined && - IsFailure(test.outcome)); + failed = failed || test.FailsRun() + || (quarantineCounts && test.quarantined + && IsFailure(test.outcome)); } if (failed) { return Catch::TestFailureExitCode; } + if (reportMissing) + { + return Catch::UnspecifiedErrorExitCode; + } if (run.tests.empty() && !options.allowNoTests) { return Catch::NoTestsRunExitCode; diff --git a/src/catch2/internal/catch_run_context.cpp b/src/catch2/internal/catch_run_context.cpp index 5c57ef18..30fa3edd 100644 --- a/src/catch2/internal/catch_run_context.cpp +++ b/src/catch2/internal/catch_run_context.cpp @@ -748,11 +748,11 @@ namespace Catch { return m_atomicAssertionCount.failed >= m_abortAfterXFailedAssertions; } - std::string RunContext::capturedOutput() { + std::string RunContext::endCapture() { if ( !m_outputRedirect->isActive() ) { return {}; } - auto _ = scopedDeactivate( *m_outputRedirect ); + m_outputRedirect->deactivate(); return m_outputRedirect->getStdout() + m_outputRedirect->getStderr(); } diff --git a/src/catch2/internal/catch_run_context.hpp b/src/catch2/internal/catch_run_context.hpp index c7bee285..5feb8890 100644 --- a/src/catch2/internal/catch_run_context.hpp +++ b/src/catch2/internal/catch_run_context.hpp @@ -106,9 +106,12 @@ namespace Catch { // !TBD We need to do this another way! bool aborting() const; - //! What the running test printed so far and was captured; for a - //! process that ends inside a test, whose end is never reported. - std::string capturedOutput(); + //! Ends the capture of the running test's output and returns what + //! it captured; for a process that ends inside a test, whose end is + //! never reported. The standard streams are their own again, so + //! flushing them at exit no longer reaches the capture buffers, + //! which may be gone by then. + std::string endCapture(); private: void assertionPassedFastPath( SourceLineInfo lineInfo ); |