Support Bazel's TEST_PREMATURE_EXIT_FILE and add it to CLI as well

This tells Catch2 to create an empty file at specified path before
the tests start, and delete it after the tests finish. This allows
callers to catch cases where the test binary silently exits before
finishing (e.g. via call to `exit(0)` inside the code under test),
by looking whether the file still exists.

Closes #3020
This commit is contained in:
Martin Hořeňovský
2025-09-21 11:19:18 +02:00
parent dc3a4ea41a
commit 8ed8c431c6
9 changed files with 250 additions and 1 deletions

View File

@@ -429,6 +429,50 @@ set_tests_properties(Reporters::CrashInJunitReporter
LABELS "uses-signals"
)
add_executable(QuickExitInTest ${TESTS_DIR}/X40-QuickExit.cpp)
target_link_libraries(QuickExitInTest PRIVATE Catch2::Catch2WithMain)
add_test(
NAME BazelEnv::TEST_PREMATURE_EXIT_FILE
COMMAND
Python3::Interpreter
"${CATCH_DIR}/tests/TestScripts/testBazelExitGuardFile.py"
$<TARGET_FILE:QuickExitInTest>
"${CMAKE_CURRENT_BINARY_DIR}"
"bazel"
)
set_tests_properties(BazelEnv::TEST_PREMATURE_EXIT_FILE
PROPERTIES
LABELS "uses-python"
)
add_test(
NAME PrematureExitGuardFileCanBeUsedFromCLI::CheckAfterCrash
COMMAND
Python3::Interpreter
"${CATCH_DIR}/tests/TestScripts/testBazelExitGuardFile.py"
$<TARGET_FILE:QuickExitInTest>
"${CMAKE_CURRENT_BINARY_DIR}"
"cli"
)
set_tests_properties(PrematureExitGuardFileCanBeUsedFromCLI::CheckAfterCrash
PROPERTIES
LABELS "uses-python"
)
add_test(
NAME PrematureExitGuardFileCanBeUsedFromCLI::CheckWithoutCrash
COMMAND
Python3::Interpreter
"${CATCH_DIR}/tests/TestScripts/testBazelExitGuardFile.py"
$<TARGET_FILE:QuickExitInTest>
"${CMAKE_CURRENT_BINARY_DIR}"
"no-crash"
)
set_tests_properties(PrematureExitGuardFileCanBeUsedFromCLI::CheckWithoutCrash
PROPERTIES
LABELS "uses-python"
)
add_executable(AssertionStartingEventGoesBeforeAssertionIsEvaluated
X20-AssertionStartingEventGoesBeforeAssertionIsEvaluated.cpp
)

View File

@@ -0,0 +1,28 @@
// 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
* Call ~~quick_exit~~ inside a test.
*
* This is used to test whether Catch2 properly creates the crash guard
* file based on provided arguments.
*/
#include <catch2/catch_test_macros.hpp>
#include <cstdlib>
TEST_CASE("quick_exit", "[quick_exit]") {
// Return 0 as fake "successful" exit, while there should be a guard
// file created and kept.
std::exit(0);
// We cannot use quick_exit because libstdc++ on older MacOS versions didn't support it yet.
// std::quick_exit(0);
}
TEST_CASE("pass") {}

View File

@@ -0,0 +1,88 @@
#!/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 sys
import subprocess
def generate_path_suffix() -> str:
return os.urandom(16).hex()[:16]
def run_common(cmd, env = None):
cmd_env = env if env is not None else os.environ.copy()
print('Running:', cmd)
try:
ret = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
universal_newlines=True,
env=cmd_env
)
except subprocess.SubprocessError as ex:
print('Could not run "{}"'.format(cmd))
print("Return code: {}".format(ex.returncode))
print("stdout: {}".format(ex.stdout))
print("stderr: {}".format(ex.stderr))
raise
def test_bazel_env_vars(bin_path, guard_path):
env = os.environ.copy()
env["TEST_PREMATURE_EXIT_FILE"] = guard_path
env["BAZEL_TEST"] = '1'
run_common([bin_path], env)
def test_cli_parameter(bin_path, guard_path):
cmd = [
bin_path,
'--premature-exit-guard-file',
guard_path
]
run_common(cmd)
def test_no_crash(bin_path, guard_path):
cmd = [
bin_path,
'--premature-exit-guard-file',
guard_path,
# Disable the quick-exit test
'~[quick_exit]'
]
run_common(cmd)
checks = {
'bazel': (test_bazel_env_vars, True),
'cli': (test_cli_parameter, True),
'no-crash': (test_no_crash, False),
}
bin_path = os.path.abspath(sys.argv[1])
output_dir = os.path.abspath(sys.argv[2])
test_kind = sys.argv[3]
guard_file_path = os.path.join(output_dir, f"guard_file.{generate_path_suffix()}")
print(f'Guard file path: "{guard_file_path}"')
check_func, file_should_exist = checks[test_kind]
check_func(bin_path, guard_file_path)
assert os.path.exists(guard_file_path) == file_should_exist
# With randomly generated file suffix, we should not run into name conflicts.
# However, we try to cleanup anyway, to avoid having infinity files in
# long living build directories.
if file_should_exist:
try:
os.remove(guard_file_path)
except Exception as ex:
print(f'Could not remove file {guard_file_path} because: {ex}')