Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bindings/pyroot/pythonizations/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ ROOT_ADD_PYUNITTEST(regression_pickle_no_cppyy regression_pickle_no_cppyy.py)

ROOT_ADD_PYUNITTEST(package_metadata package_metadata.py)

ROOT_ADD_PYUNITTEST(regression_23728 regression_23728.py)

if(NOT MSVC OR CMAKE_SIZEOF_VOID_P EQUAL 8 OR win_broken_tests)
# Test the conversion and low level views of interpreter-defined C++ arrays to NumPy
# We do not run this test on Windows x86 due to an interpreter issue with arrays:
Expand Down
81 changes: 81 additions & 0 deletions bindings/pyroot/pythonizations/test/regression_23728.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import os
import subprocess
import sys
import tempfile
import textwrap
import unittest


class Regression23728(unittest.TestCase):
"""
Deleting an object of a class without compiled dictionary during
TROOT::EndOfProcessCleanups() must not crash, even if the interpreter
already JIT-compiled a destructor earlier in the process. The cache of
compiled destructors used to be a function-local static, destroyed before
the end-of-process cleanup ran because its first use came after ROOT
registered its atexit handlers.

See https://github.com/root-project/root/issues/23728
"""

def test_exit_cleanup_after_interpreted_destructor(self):
import ROOT

with tempfile.TemporaryDirectory() as tmpdir:
fname = os.path.join(tmpdir, "vv.root")

# vector<vector<int>> has no compiled dictionary, so its destructor
# has to be JIT-compiled when the branch object is deleted.
with ROOT.TFile(fname, "RECREATE") as f:
t = ROOT.TTree("t", "t")
v = ROOT.std.vector[ROOT.std.vector[int]]()
t.Branch("vv", v)
v.push_back(ROOT.std.vector[int]([1, 2]))
t.Fill()
f.Write()

script = textwrap.dedent(
f"""
import ROOT

f = ROOT.TFile({fname!r})
# Let TROOT::EndOfProcessCleanups() delete the file, and with it
Comment thread
vepadulano marked this conversation as resolved.
# the tree and its branch object, from the C atexit handler.
ROOT.SetOwnership(f, False)
t = f.Get("t")
t.GetEntry(0)

# That only happens if the file is registered with gROOT and owns
# the tree. Check it, so that the test fails loudly instead of
# silently losing its purpose if these defaults change.
if not ROOT.gROOT.GetListOfFiles().FindObject(f):
raise RuntimeError("the file is not in gROOT->GetListOfFiles()")
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]()

print("end of script")
"""
)

# Poison freed memory, so that a use of destroyed objects (like the
# dead destructor cache of the issue) crashes deterministically,
# whatever the heap layout and the interpreter internals.
env = dict(os.environ, MALLOC_PERTURB_="85")

res = subprocess.run(
[sys.executable, "-c", script],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
env=env,
)

self.assertEqual(res.returncode, 0, msg=res.stdout)
self.assertIn("end of script", res.stdout)


if __name__ == "__main__":
unittest.main()
Loading