From e75cf935acd097ba9ff0f947edfacf38bb4006a1 Mon Sep 17 00:00:00 2001 From: Miguel Cid Flor Date: Tue, 12 May 2026 17:13:34 +0100 Subject: [PATCH 1/3] test(mutation): Added a test to see if the args of __add__ mutate after operation --- tests/test_general.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_general.py b/tests/test_general.py index d41f865..8d8c1d9 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -249,6 +249,31 @@ def test_construct_xml(self): assert len(case) == 1 assert case[0].attrib["name"] == "case1" + def test_add_does_not_mutate_operands(self): + text = """ + + + + test_x.py:11: unconditional skip + + @pytest.fixture(scope="module") def compb(): yield > raise PermissionError E + PermissionError test_x.py:6: PermissionError + + + + """ + a = JUnitXml.fromstring(text) + b = JUnitXml.fromstring(text) + + result = a + b + assert a.tests == 2, f"Operand 'a' was mutated! tests={a.tests}" + assert b.tests == 2, f"Operand 'b' was mutated! tests={b.tests}" + assert result.tests == 4, f"Result wrong: tests={result.tests}" + _ = a + b + assert a.tests == 2, "Operand 'a' emptied after uncaptured +" + assert b.tests == 2, "Operand 'b' emptied after uncaptured +" + def test_add(self): result1 = JUnitXml() suite1 = TestSuite("suite1") From 4fa96220ccca347dcdbf3dcf0ac5c616e8ee69c1 Mon Sep 17 00:00:00 2001 From: Miguel Cid Flor Date: Tue, 12 May 2026 17:44:09 +0100 Subject: [PATCH 2/3] fix(JUnitXml): After an add the values that were added mutated --- src/junitparser/junitparser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/junitparser/junitparser.py b/src/junitparser/junitparser.py index e4cd9cf..ab34e7a 100644 --- a/src/junitparser/junitparser.py +++ b/src/junitparser/junitparser.py @@ -728,9 +728,9 @@ def __len__(self): def __add__(self, other): result = type(self)() for suite in self: - result.add_testsuite(suite) + result.add_testsuite(deepcopy(suite)) for suite in other: - result.add_testsuite(suite) + result.add_testsuite(deepcopy(suite)) return result def __iadd__(self, other): From 88cc9f9758bbafedd6b1c4447a7eba0ccc77e2f7 Mon Sep 17 00:00:00 2001 From: Miguel Cid Flor Date: Tue, 12 May 2026 18:25:10 +0100 Subject: [PATCH 3/3] refactor(JUnitXml): The same problem may be happening in other places it makes more sense to put the deepcopy inside the function --- src/junitparser/junitparser.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/junitparser/junitparser.py b/src/junitparser/junitparser.py index ab34e7a..0784786 100644 --- a/src/junitparser/junitparser.py +++ b/src/junitparser/junitparser.py @@ -728,9 +728,9 @@ def __len__(self): def __add__(self, other): result = type(self)() for suite in self: - result.add_testsuite(deepcopy(suite)) + result.add_testsuite(suite) for suite in other: - result.add_testsuite(deepcopy(suite)) + result.add_testsuite(suite) return result def __iadd__(self, other): @@ -747,6 +747,7 @@ def __iadd__(self, other): return self def add_testsuite(self, suite: TestSuite): + suite = deepcopy(suite) """Add a testsuite.""" for existing_suite in self: if existing_suite == suite: