From 752c04e37cf4f31f43d9ec20bd23fe858612e6b1 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Wed, 28 Oct 2026 19:42:22 +0000 Subject: [PATCH] [Python] Make the regression test for #23728 work effectively The test left the destructor of the branch class uncompiled until exit, so that cling had to JIT-compile it from TROOT::EndOfProcessCleanups(). On Windows, that cleanup runs from libCore's DLL_PROCESS_DETACH, after libCling's statics have already been torn down, so compiling anything there crashes with or without the fix (the no-fix run in #23850 fails identically). Pre-compile the exact destructor instead: the exit-time call is then a pure cache lookup, which is the regular exit path on Windows. The bug was in that lookup on the destroyed function-local static, so the test still fails without the fix. Write the branch through the overload taking the class name and the address. The templated Branch() that the pythonization on master calls refuses collections without a compiled CollectionProxy, which silently left the tree without the branch and the test without its purpose. Also give the child process a timeout: without the fix, ROOT's crash handler can hang instead of terminating the process. Assisted-by: claude-code:claude-fable-5-1 --- .../pythonizations/test/regression_23728.py | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/bindings/pyroot/pythonizations/test/regression_23728.py b/bindings/pyroot/pythonizations/test/regression_23728.py index 0371a664c691f..243473c6b4f79 100644 --- a/bindings/pyroot/pythonizations/test/regression_23728.py +++ b/bindings/pyroot/pythonizations/test/regression_23728.py @@ -29,7 +29,12 @@ def test_exit_cleanup_after_interpreted_destructor(self): with ROOT.TFile(fname, "RECREATE") as f: t = ROOT.TTree("t", "t") v = ROOT.std.vector[ROOT.std.vector[int]]() - t.Branch("vv", v) + # Use the overload taking the class name and the address: the + # templated Branch() refuses collections without a compiled + # CollectionProxy, which would silently leave the tree without + # the branch and the test without its purpose. + branch = t.Branch("vv", "vector >", ROOT.AddressOf(v)) + self.assertTrue(branch, "Branch() refused to create the branch") v.push_back(ROOT.std.vector[int]([1, 2])) t.Fill() f.Write() @@ -53,8 +58,22 @@ def test_exit_cleanup_after_interpreted_destructor(self): if t.GetDirectory() != f: raise RuntimeError("the tree is not owned by the file") - # Make the interpreter JIT-compile a destructor before exit. - ROOT.std.vector[ROOT.TNamed]() + # Sanity checks that the branch exists and that bytes are + # actually read, which is required for the reproducer. + if not t.GetBranch("vv"): + raise RuntimeError("the tree has no 'vv' branch") + if t.GetEntry(0) <= 0: + raise RuntimeError("no bytes read for entry 0: no branch object was created") + + # Make the interpreter JIT-compile the destructor of the branch + # class before exit. This creates the destructor cache before the + # end-of-process cleanup needs it (and, before the fix, destroyed + # it again before that). Using the very same class keeps the + # exit-time destructor call a pure cache lookup: compiling code + # at that point is not possible on Windows, where the atexit + # handler of libCore runs only when libCling's statics have + # already been torn down by its DLL_PROCESS_DETACH. + ROOT.std.vector[ROOT.std.vector[int]]() print("end of script") """ @@ -65,12 +84,16 @@ def test_exit_cleanup_after_interpreted_destructor(self): # whatever the heap layout and the interpreter internals. env = dict(os.environ, MALLOC_PERTURB_="85") + # Without the fix, ROOT's crash handler can hang on the way out + # of the segfault instead of terminating the process: fail + # instead of waiting for the ctest timeout. res = subprocess.run( [sys.executable, "-c", script], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env, + timeout=300, ) self.assertEqual(res.returncode, 0, msg=res.stdout)