Skip to content

Commit 23bfe9a

Browse files
committed
Improve C++ test infrastructure and disable hanging test
This commit improves the C++ test infrastructure to ensure test output is visible in CI logs, and disables a test that hangs on free-threaded Python 3.14+. Changes: ## CI/test infrastructure improvements - .github/workflows: Added `timeout-minutes: 3` to all C++ test steps to prevent indefinite hangs. - tests/**/CMakeLists.txt: Added `USES_TERMINAL` to C++ test targets (cpptest, test_cross_module_rtti, test_pure_cpp) to ensure output is shown immediately rather than buffered and possibly lost on crash/timeout. - tests/test_with_catch/catch.cpp: Added a custom Catch2 progress reporter with timestamps, Python version info, and a SIGTERM handler to make test execution and failures clearly visible in CI logs. ## Disabled hanging test - The "Move Subinterpreter" test is disabled on free-threaded Python 3.14+ due to a hang in Py_EndInterpreter() when the subinterpreter is destroyed from a different thread than it was created on. Work on fixing the underlying issue will continue under PR pybind#5940. Context: We were in the dark for months (since we started testing with Python 3.14t) because CI logs gave no clue about the root cause of hangs. This led to ignoring intermittent hangs (mostly on macOS). Our hand was forced only with the Python 3.14.1 release, when hangs became predictable on all platforms. For the full development history of these changes, see PR pybind#5933.
1 parent d4f9cfb commit 23bfe9a

File tree

8 files changed

+146
-4
lines changed

8 files changed

+146
-4
lines changed

.github/workflows/ci.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ jobs:
229229
run: cmake --build . --target pytest
230230

231231
- name: Compiled tests
232+
timeout-minutes: 3
232233
run: cmake --build . --target cpptest
233234

234235
- name: Interface test
@@ -334,6 +335,7 @@ jobs:
334335
run: cmake --build --preset default --target pytest
335336

336337
- name: C++ tests
338+
timeout-minutes: 3
337339
run: cmake --build --preset default --target cpptest
338340

339341
- name: Visibility test
@@ -393,6 +395,7 @@ jobs:
393395
run: cmake --build build --target pytest
394396

395397
- name: C++ tests
398+
timeout-minutes: 3
396399
run: cmake --build build --target cpptest
397400

398401
- name: Interface test
@@ -516,6 +519,7 @@ jobs:
516519
run: cmake --build build --target pytest
517520

518521
- name: C++ tests
522+
timeout-minutes: 3
519523
run: cmake --build build --target cpptest
520524

521525
- name: Interface test
@@ -570,6 +574,7 @@ jobs:
570574
run: cmake --build build --target pytest
571575

572576
- name: C++ tests
577+
timeout-minutes: 3
573578
run: cmake --build build --target cpptest
574579

575580
- name: Interface test
@@ -652,6 +657,7 @@ jobs:
652657
cmake --build build-11 --target check
653658
654659
- name: C++ tests C++11
660+
timeout-minutes: 3
655661
run: |
656662
set +e; source /opt/intel/oneapi/setvars.sh; set -e
657663
cmake --build build-11 --target cpptest
@@ -689,6 +695,7 @@ jobs:
689695
cmake --build build-17 --target check
690696
691697
- name: C++ tests C++17
698+
timeout-minutes: 3
692699
run: |
693700
set +e; source /opt/intel/oneapi/setvars.sh; set -e
694701
cmake --build build-17 --target cpptest
@@ -760,6 +767,7 @@ jobs:
760767
run: cmake --build build --target pytest
761768

762769
- name: C++ tests
770+
timeout-minutes: 3
763771
run: cmake --build build --target cpptest
764772

765773
- name: Interface test
@@ -1000,6 +1008,7 @@ jobs:
10001008
run: cmake --build build --target pytest
10011009

10021010
- name: C++20 tests
1011+
timeout-minutes: 3
10031012
run: cmake --build build --target cpptest -j 2
10041013

10051014
- name: Interface test C++20
@@ -1076,6 +1085,7 @@ jobs:
10761085
run: cmake --build build --target pytest -j 2
10771086

10781087
- name: C++11 tests
1088+
timeout-minutes: 3
10791089
run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build --target cpptest -j 2
10801090

10811091
- name: Interface test C++11
@@ -1100,6 +1110,7 @@ jobs:
11001110
run: cmake --build build2 --target pytest -j 2
11011111

11021112
- name: C++14 tests
1113+
timeout-minutes: 3
11031114
run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build2 --target cpptest -j 2
11041115

11051116
- name: Interface test C++14
@@ -1124,6 +1135,7 @@ jobs:
11241135
run: cmake --build build3 --target pytest -j 2
11251136

11261137
- name: C++17 tests
1138+
timeout-minutes: 3
11271139
run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build3 --target cpptest -j 2
11281140

11291141
- name: Interface test C++17
@@ -1195,6 +1207,7 @@ jobs:
11951207
run: cmake --build . --target pytest -j 2
11961208

11971209
- name: C++ tests
1210+
timeout-minutes: 3
11981211
run: cmake --build . --target cpptest -j 2
11991212

12001213
- name: Interface test
@@ -1257,6 +1270,7 @@ jobs:
12571270
run: cmake --build . --target pytest -j 2
12581271

12591272
- name: C++ tests
1273+
timeout-minutes: 3
12601274
run: cmake --build . --target cpptest -j 2
12611275

12621276
- name: Interface test
@@ -1329,6 +1343,7 @@ jobs:
13291343
run: cmake --build build --target pytest -j 2
13301344

13311345
- name: C++ tests
1346+
timeout-minutes: 3
13321347
run: PYTHONHOME=/clangarm64 PYTHONPATH=/clangarm64 cmake --build build --target cpptest -j 2
13331348

13341349
- name: Interface test

.github/workflows/reusable-standard.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ jobs:
8383
run: cmake --build build --target pytest
8484

8585
- name: C++ tests
86+
timeout-minutes: 3
8687
run: cmake --build build --target cpptest
8788

8889
- name: Interface test

.github/workflows/upstream.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ jobs:
6666
run: cmake --build build11 --target pytest -j 2
6767

6868
- name: C++11 tests
69+
timeout-minutes: 3
6970
run: cmake --build build11 --target cpptest -j 2
7071

7172
- name: Interface test C++11
@@ -87,6 +88,7 @@ jobs:
8788
run: cmake --build build17 --target pytest
8889

8990
- name: C++17 tests
91+
timeout-minutes: 3
9092
run: cmake --build build17 --target cpptest
9193

9294
# Third build - C++17 mode with unstable ABI

tests/pure_cpp/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ target_link_libraries(smart_holder_poc_test PRIVATE pybind11::headers Catch2::Ca
1515
add_custom_target(
1616
test_pure_cpp
1717
COMMAND "$<TARGET_FILE:smart_holder_poc_test>"
18-
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")
18+
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
19+
USES_TERMINAL # Ensures output is shown immediately (not buffered and possibly lost on crash)
20+
)
1921

2022
add_dependencies(check test_pure_cpp)

tests/test_cross_module_rtti/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ add_custom_target(
6060
test_cross_module_rtti
6161
COMMAND "$<TARGET_FILE:test_cross_module_rtti_main>"
6262
DEPENDS test_cross_module_rtti_main
63-
WORKING_DIRECTORY "$<TARGET_FILE_DIR:test_cross_module_rtti_main>")
63+
WORKING_DIRECTORY "$<TARGET_FILE_DIR:test_cross_module_rtti_main>"
64+
USES_TERMINAL # Ensures output is shown immediately (not buffered and possibly lost on crash)
65+
)
6466

6567
set_target_properties(test_cross_module_rtti_bindings PROPERTIES LIBRARY_OUTPUT_DIRECTORY
6668
"${CMAKE_CURRENT_BINARY_DIR}")

tests/test_with_catch/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ add_custom_target(
4747
cpptest
4848
COMMAND "$<TARGET_FILE:test_with_catch>"
4949
DEPENDS test_with_catch
50-
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")
50+
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
51+
USES_TERMINAL # Ensures output is shown immediately (not buffered and possibly lost on crash)
52+
)
5153

5254
pybind11_add_module(external_module THIN_LTO external_module.cpp)
5355
set_target_properties(external_module PROPERTIES LIBRARY_OUTPUT_DIRECTORY

tests/test_with_catch/catch.cpp

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@
33

44
#include <pybind11/embed.h>
55

6+
#include <chrono>
7+
#include <csignal>
8+
#include <cstring>
9+
#include <ctime>
10+
#include <iomanip>
11+
#include <sstream>
12+
13+
#ifndef _WIN32
14+
# include <unistd.h>
15+
#endif
16+
617
// Silence MSVC C++17 deprecation warning from Catch regarding std::uncaught_exceptions (up to
718
// catch 2.0.1; this should be fixed in the next catch release after 2.0.1).
819
PYBIND11_WARNING_DISABLE_MSVC(4996)
@@ -13,11 +24,109 @@ PYBIND11_WARNING_DISABLE_MSVC(4996)
1324
#endif
1425

1526
#define CATCH_CONFIG_RUNNER
27+
#define CATCH_CONFIG_DEFAULT_REPORTER "progress"
1628
#include <catch.hpp>
1729

1830
namespace py = pybind11;
1931

32+
// Simple progress reporter that prints a line per test case.
33+
namespace {
34+
35+
class ProgressReporter : public Catch::StreamingReporterBase<ProgressReporter> {
36+
public:
37+
using StreamingReporterBase<ProgressReporter>::StreamingReporterBase;
38+
39+
static std::string getDescription() { return "Simple progress reporter (one line per test)"; }
40+
41+
void testCaseStarting(Catch::TestCaseInfo const &testInfo) override {
42+
print_python_version_once();
43+
auto &os = Catch::cout();
44+
os << "[ RUN ] " << testInfo.name << '\n';
45+
os.flush();
46+
}
47+
48+
void testCaseEnded(Catch::TestCaseStats const &stats) override {
49+
bool failed = stats.totals.assertions.failed > 0;
50+
auto &os = Catch::cout();
51+
os << (failed ? "[ FAILED ] " : "[ OK ] ") << stats.testInfo.name << '\n';
52+
os.flush();
53+
}
54+
55+
void noMatchingTestCases(std::string const &spec) override {
56+
auto &os = Catch::cout();
57+
os << "[ NO TEST ] no matching test cases for spec: " << spec << '\n';
58+
os.flush();
59+
}
60+
61+
void reportInvalidArguments(std::string const &arg) override {
62+
auto &os = Catch::cout();
63+
os << "[ ERROR ] invalid Catch2 arguments: " << arg << '\n';
64+
os.flush();
65+
}
66+
67+
void assertionStarting(Catch::AssertionInfo const &) override {}
68+
69+
bool assertionEnded(Catch::AssertionStats const &) override { return false; }
70+
71+
private:
72+
void print_python_version_once() {
73+
if (printed_) {
74+
return;
75+
}
76+
printed_ = true;
77+
auto &os = Catch::cout();
78+
os << "[ PYTHON ] " << Py_GetVersion() << '\n';
79+
os.flush();
80+
}
81+
82+
bool printed_ = false;
83+
};
84+
85+
} // namespace
86+
87+
CATCH_REGISTER_REPORTER("progress", ProgressReporter)
88+
89+
namespace {
90+
91+
std::string get_utc_timestamp() {
92+
auto now = std::chrono::system_clock::now();
93+
auto time_t_now = std::chrono::system_clock::to_time_t(now);
94+
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
95+
96+
std::tm utc_tm{};
97+
#if defined(_WIN32)
98+
gmtime_s(&utc_tm, &time_t_now);
99+
#else
100+
gmtime_r(&time_t_now, &utc_tm);
101+
#endif
102+
103+
std::ostringstream oss;
104+
oss << std::put_time(&utc_tm, "%Y-%m-%d %H:%M:%S") << '.' << std::setfill('0') << std::setw(3)
105+
<< ms.count() << 'Z';
106+
return oss.str();
107+
}
108+
109+
#ifndef _WIN32
110+
// Signal handler to print a message when the process is terminated.
111+
// Uses only async-signal-safe functions.
112+
void termination_signal_handler(int sig) {
113+
const char *msg = "[ SIGNAL ] Process received SIGTERM\n";
114+
// write() is async-signal-safe, unlike std::cout
115+
ssize_t written = write(STDOUT_FILENO, msg, strlen(msg));
116+
(void) written; // suppress "unused variable" warnings
117+
// Re-raise with default handler to get proper exit status
118+
std::signal(sig, SIG_DFL);
119+
std::raise(sig);
120+
}
121+
#endif
122+
123+
} // namespace
124+
20125
int main(int argc, char *argv[]) {
126+
#ifndef _WIN32
127+
std::signal(SIGTERM, termination_signal_handler);
128+
#endif
129+
21130
// Setup for TEST_CASE in test_interpreter.cpp, tagging on a large random number:
22131
std::string updated_pythonpath("pybind11_test_with_catch_PYTHONPATH_2099743835476552");
23132
const char *preexisting_pythonpath = getenv("PYTHONPATH");
@@ -35,9 +144,15 @@ int main(int argc, char *argv[]) {
35144
setenv("PYTHONPATH", updated_pythonpath.c_str(), /*replace=*/1);
36145
#endif
37146

147+
std::cout << "[ STARTING ] " << get_utc_timestamp() << '\n';
148+
std::cout.flush();
149+
38150
py::scoped_interpreter guard{};
39151

40152
auto result = Catch::Session().run(argc, argv);
41153

154+
std::cout << "[ DONE ] " << get_utc_timestamp() << " (result " << result << ")\n";
155+
std::cout.flush();
156+
42157
return result < 0xff ? result : 0xff;
43158
}

tests/test_with_catch/test_subinterpreter.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ TEST_CASE("Single Subinterpreter") {
9090
unsafe_reset_internals_for_single_interpreter();
9191
}
9292

93-
# if PY_VERSION_HEX >= 0x030D0000
93+
// "Move Subinterpreter" test is disabled on free-threaded Python 3.14+ due to a hang
94+
// in Py_EndInterpreter() when the subinterpreter is destroyed from a different thread
95+
// than it was created on. See: https://github.com/pybind/pybind11/pull/5940
96+
# if PY_VERSION_HEX >= 0x030D0000 && !(PY_VERSION_HEX >= 0x030E0000 && defined(Py_GIL_DISABLED))
9497
TEST_CASE("Move Subinterpreter") {
9598
std::unique_ptr<py::subinterpreter> sub(new py::subinterpreter(py::subinterpreter::create()));
9699

0 commit comments

Comments
 (0)