Commit Graph
2 Commits
Author SHA1 Message Date
Ralf W. Grosse-Kunstleve 524d72b36d fix: strdup "self" arg in def_property_static, partially revert #6010 (gh-5976) (#6015)
* fix: strdup args added after initialize_generic in def_property_static (gh-5976)

`def_property_static` calls `process_attributes::init` on already-initialized
function records (after `initialize_generic`'s strdup loop has run).
Args added at this stage (e.g. "self" via `append_self_arg_if_needed`) remain
as string literals, so `destruct()` would call `free()` on them.

Fix by strdup'ing name/descr of any args appended by the late
`process_attributes::init` call. Root cause introduced by gh-5486.

Made-with: Cursor

* Partially revert gh-6010: remove py_is_finalizing() workarounds

Now that the root cause (free of string literals in def_property_static,
gh-5976) is fixed in the previous commit, the py_is_finalizing() guards
introduced in gh-6010 are no longer needed:

- tp_dealloc_impl: remove early return during finalization (was leaking
  all function records instead of properly destroying them)
- destruct(): remove guard around arg.value.dec_ref()
- common.h: remove py_is_finalizing() helper (no remaining callers)

The genuine fix from gh-6010 (PyObject_Free + Py_DECREF ordering in
tp_dealloc_impl) is retained.

Made-with: Cursor

* test: add embedding test for py::enum_ across interpreter restart (gh-5976)

py::enum_ is the primary trigger for gh-5976 because its constructor
creates properties via def_property_static / def_property_readonly_static,
which call process_attributes::init on already-initialized function records.
Yet none of the existing embedding tests used py::enum_ at all.

Add an PYBIND11_EMBEDDED_MODULE with py::enum_ and a test case that imports
it, finalize/reinitializes the interpreter, and re-imports it. This exercises
the def_property_static code path that was fixed in the preceding commit.

Note: on Python 3.14.2 (and likely 3.12+), tp_dealloc_impl is not called
during Py_FinalizeEx for function record PyObjects — they simply leak because
types are effectively immortalized. As a result, this test cannot trigger the
original free()-on-string-literal crash on this Python version. However, it
remains valuable as a regression guard: on Python builds where finalization
does clean up function records (or if CPython changes this behavior), the
test would catch the crash. It also verifies that py::enum_ survives
interpreter restart correctly, which was previously untested.

Made-with: Cursor

* test: skip enum restart test on Python 3.12 (pre-existing crash)

Made-with: Cursor

* Add test_standalone_enum_module.py, standalone_enum_module.cpp

* Make standalone_enum_module.cpp more similar to #5976 reproducer. Also fix clang-tidy error.

* This crashes when testing locally:

( cd /wrk/forked/pybind11/tests && PYTHONPATH=/wrk/bld/pybind11_gcc_v3.14.2_df793163d58_default/lib /wrk/bld/pybind11_gcc_v3.14.2_df793163d58_default/TestVenv/bin/python3 -m pytest test_standalone_enum_module.py )

============================= test session starts ==============================
platform linux -- Python 3.14.2, pytest-9.0.2, pluggy-1.6.0
installed packages of interest: build==1.4.2 numpy==2.4.3 scipy==1.17.1
C++ Info: 13.3.0 C++20 __pybind11_internals_v12_system_libstdcpp_gxx_abi_1xxx_use_cxx11_abi_1__ PYBIND11_SIMPLE_GIL_MANAGEMENT=False
rootdir: /wrk/forked/pybind11/tests
configfile: pytest.ini
plugins: timeout-2.4.0, xdist-3.8.0
collected 1 item

test_standalone_enum_module.py F                                         [100%]

=================================== FAILURES ===================================
________________________ test_enum_import_exit_no_crash ________________________

    def test_enum_import_exit_no_crash():
        # Modeled after reproducer under issue #5976
>       env.check_script_success_in_subprocess(
            f"""
            import sys
            sys.path.insert(0, {os.path.dirname(env.__file__)!r})
            import standalone_enum_module as m
            assert m.SomeEnum.__class__.__name__ == "pybind11_type"
            """,
            rerun=1,
        )

test_standalone_enum_module.py:10:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

code = 'import sys\nsys.path.insert(0, \'/wrk/forked/pybind11/tests\')\nimport standalone_enum_module as m\nassert m.SomeEnum.__class__.__name__ == "pybind11_type"'

    def check_script_success_in_subprocess(code: str, *, rerun: int = 8) -> None:
        """Runs the given code in a subprocess."""
        import os
        import subprocess
        import sys
        import textwrap

        if ANDROID or IOS or sys.platform.startswith("emscripten"):
            pytest.skip("Requires subprocess support")

        code = textwrap.dedent(code).strip()
        try:
            for _ in range(rerun):  # run flakily failing test multiple times
                subprocess.check_output(
                    [sys.executable, "-c", code],
                    cwd=os.getcwd(),
                    stderr=subprocess.STDOUT,
                    text=True,
                )
        except subprocess.CalledProcessError as ex:
>           raise RuntimeError(
                f"Subprocess failed with exit code {ex.returncode}.\n\n"
                f"Code:\n"
                f"```python\n"
                f"{code}\n"
                f"```\n\n"
                f"Output:\n"
                f"{ex.output}"
            ) from None
E           RuntimeError: Subprocess failed with exit code -6.
E
E           Code:
E           ```python
E           import sys
E           sys.path.insert(0, '/wrk/forked/pybind11/tests')
E           import standalone_enum_module as m
E           assert m.SomeEnum.__class__.__name__ == "pybind11_type"
E           ```
E
E           Output:
E           munmap_chunk(): invalid pointer

_          = 0
code       = 'import sys\nsys.path.insert(0, \'/wrk/forked/pybind11/tests\')\nimport standalone_enum_module as m\nassert m.SomeEnum.__class__.__name__ == "pybind11_type"'
os         = <module 'os' (frozen)>
rerun      = 1
subprocess = <module 'subprocess' from '/wrk/cpython_installs/v3.14.2_df793163d58_default/lib/python3.14/subprocess.py'>
sys        = <module 'sys' (built-in)>
textwrap   = <module 'textwrap' from '/wrk/cpython_installs/v3.14.2_df793163d58_default/lib/python3.14/textwrap.py'>

env.py:68: RuntimeError
=========================== short test summary info ============================
FAILED test_standalone_enum_module.py::test_enum_import_exit_no_crash - Runti...
============================== 1 failed in 0.23s ===============================

ERROR: completed_process.returncode=1

* Add "Added in PR #6015" comments, for easy reference back to this PR

* test: use PYBIND11_CATCH2_SKIP_IF for Python 3.12 enum restart skip

Replace #if/#else/#endif preprocessor guard with runtime
PYBIND11_CATCH2_SKIP_IF so the test is always compiled and
shows [ SKIPPED ] in output on Python 3.12.

Made-with: Cursor

* fix: suppress MSVC C4127 in PYBIND11_CATCH2_SKIP_IF macro

The constant condition in PYBIND11_CATCH2_SKIP_IF triggers MSVC
warning C4127 (conditional expression is constant), which becomes
a build error under /WX.

Made-with: Cursor
2026-03-29 20:17:22 -07:00
Ralf W. Grosse-Kunstleve 78381e5e28 Improve C++ test infrastructure: progress reporter, timeouts, and skip hanging Move Subinterpreter test (#5942)
* 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 #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 #5933.

* Add test summary to progress reporter

Print the total number of test cases and assertions at the end of the
test run, making it easy to spot if tests are disabled or added.

Example output:
  [  PASSED  ] 20 test cases, 1589 assertions.

* Add PYBIND11_CATCH2_SKIP_IF macro to skip tests at runtime

Catch2 v2 doesn't have native skip support (v3 does with SKIP()).
This macro allows tests to be skipped with a visible message while
still appearing in the test list.

Use this for the Move Subinterpreter test on free-threaded Python 3.14+
so it shows as skipped rather than being conditionally compiled out.

Example output:
  [ RUN      ] Move Subinterpreter
  [ SKIPPED ] Skipped on free-threaded Python 3.14+ (see PR #5940)
  [       OK ] Move Subinterpreter

* Fix clang-tidy bugprone-macro-parentheses warning in PYBIND11_CATCH2_SKIP_IF
2025-12-21 21:25:06 -08:00