Skip to content

Commit bb12f2d

Browse files
committed
fix #177
1 parent 3c28cf9 commit bb12f2d

4 files changed

Lines changed: 102 additions & 14 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## [5.0.3] - 2026-08-31
4+
### Fixed
5+
- Issue 177: Fix 177 different behavior of + and +=.
6+
37
## [5.0.2] - 2026-08-31
48
### Fixed
59
- Issue 180: Fix `add_testsuite` regression caused by 5.0.1.

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "uv_build"
44

55
[project]
66
name = "junitparser"
7-
version = "5.0.2"
7+
version = "5.0.3"
88
authors = [
99
{ name="Weiwei Wang", email="gastlygem@gmail.com" },
1010
]

‎src/junitparser/junitparser.py‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -722,22 +722,21 @@ def __len__(self):
722722

723723
def __add__(self, other):
724724
result = type(self)()
725-
for suite in self:
726-
result.add_testsuite(deepcopy(suite))
727-
for suite in other:
728-
result.add_testsuite(deepcopy(suite))
725+
result += self
726+
result += other
729727
return result
730728

731729
def __iadd__(self, other):
732-
if other._elem.tag == "testsuites":
733-
for suite in other:
734-
self.add_testsuite(deepcopy(suite))
735-
elif other._elem.tag == "testsuite":
730+
if isinstance(other, TestSuite):
736731
suite = self.testsuite(name=other.name)
737732
for case in other:
738733
suite._add_testcase_no_update_stats(deepcopy(case))
739734
self.add_testsuite(suite)
740-
self.update_statistics()
735+
else:
736+
# A testsuites container, or any iterable of testsuites.
737+
for suite in other:
738+
self.add_testsuite(deepcopy(suite))
739+
self.update_statistics()
741740

742741
return self
743742

‎tests/test_general.py‎

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,8 @@ def test_construct_xml(self):
250250
assert case[0].attrib["name"] == "case1"
251251

252252
def test_add_testsuite_keeps_live_reference(self):
253-
"""Regression test for #180."""
253+
# issue 180: add_testsuite used to deepcopy, so cases added afterwards
254+
# went nowhere
254255
report = JUnitXml()
255256
suite = TestSuite("suite")
256257
report.add_testsuite(suite)
@@ -271,15 +272,15 @@ def test_add_testsuite_keeps_live_reference(self):
271272
assert report.failures == 2
272273

273274
def test_add_testsuite_merge_keeps_live_reference(self):
274-
"""Regression test for #180, merge branch of ``add_testsuite``."""
275+
# issue 180, this time down the merge branch
275276
report = JUnitXml()
276277
suite = TestSuite("suite")
277278
report.add_testsuite(suite)
278-
# An equal suite is merged into the existing one instead of appended.
279+
# equal suites get merged, not appended
279280
other = TestSuite("suite")
280281
other.add_testcase(TestCase("case 1"))
281282
report.add_testsuite(other)
282-
# The caller's original suite object still points into the report.
283+
# suite is still the one in the report
283284
suite.add_testcase(TestCase("case 2"))
284285
report.update_statistics()
285286

@@ -402,6 +403,90 @@ def test_iadd_two_different_suites(self):
402403
assert isinstance(suite1, JUnitXml)
403404
assert len(list(iter(suite1))) == 2
404405

406+
def _xml_with_stats(self, name, passes, failures):
407+
# one suite, counts already written out
408+
xml = JUnitXml()
409+
suite = TestSuite(name)
410+
for i in range(passes):
411+
suite.add_testcase(TestCase(f"{name}_pass{i}"))
412+
for i in range(failures):
413+
case = TestCase(f"{name}_fail{i}")
414+
case.result = [Failure("boom")]
415+
suite.add_testcase(case)
416+
xml.add_testsuite(suite)
417+
xml.update_statistics()
418+
return xml
419+
420+
def test_iadd_refreshes_stale_statistics(self):
421+
# issue 177: IntAttr only counts when the attr is missing, so += has to
422+
# refresh it by hand
423+
a = self._xml_with_stats("A", 5, 2)
424+
b = self._xml_with_stats("B", 3, 1)
425+
assert (a.tests, a.failures) == (7, 2)
426+
427+
a += b
428+
429+
assert a.tests == 11
430+
assert a.failures == 3
431+
assert sum(1 for suite in a for _ in suite) == 11
432+
433+
def test_iadd_self_merge_refreshes_statistics(self):
434+
# issue 177, x += x
435+
xml = self._xml_with_stats("A", 4, 0)
436+
xml += xml
437+
assert sum(1 for suite in xml for _ in suite) == 8
438+
assert xml.tests == 8
439+
440+
def test_add_and_iadd_agree(self):
441+
# issue 177: + and += must give the same thing
442+
added = self._xml_with_stats("A", 5, 2) + self._xml_with_stats("B", 3, 1)
443+
inplace = self._xml_with_stats("A", 5, 2)
444+
inplace += self._xml_with_stats("B", 3, 1)
445+
446+
assert (added.tests, added.failures) == (inplace.tests, inplace.failures)
447+
assert len(added) == len(inplace)
448+
assert [suite.name for suite in added] == [suite.name for suite in inplace]
449+
450+
def test_add_bare_testsuite_matches_iadd(self):
451+
# += takes a bare testsuite, + used to iterate it as if it were a
452+
# testsuites and drop the cases under the root
453+
suite = TestSuite("B")
454+
suite.add_testcase(TestCase("b0"))
455+
suite.add_testcase(TestCase("b1"))
456+
457+
added = self._xml_with_stats("A", 1, 0) + suite
458+
inplace = self._xml_with_stats("A", 1, 0)
459+
inplace += deepcopy(suite)
460+
461+
assert len(added) == 2
462+
assert added.tests == 3
463+
assert (added.tests, len(added)) == (inplace.tests, len(inplace))
464+
# no testcase directly under testsuites
465+
assert added._elem.find("testcase") is None
466+
467+
def test_add_iterable_of_suites(self):
468+
# + never looked at the operand's element, so a plain list works.
469+
# += takes one now too.
470+
suites = [TestSuite("B"), TestSuite("C")]
471+
for i, suite in enumerate(suites):
472+
suite.add_testcase(TestCase(f"case{i}"))
473+
474+
added = self._xml_with_stats("A", 2, 0) + suites
475+
inplace = self._xml_with_stats("A", 2, 0)
476+
inplace += deepcopy(suites)
477+
478+
assert len(added) == 3
479+
assert added.tests == 4
480+
assert (len(added), added.tests) == (len(inplace), inplace.tests)
481+
482+
def test_add_generator_of_suites(self):
483+
# NOTE: single pass, don't iterate the operand twice
484+
suite = TestSuite("B")
485+
suite.add_testcase(TestCase("b0"))
486+
result = self._xml_with_stats("A", 2, 0) + (s for s in [suite])
487+
assert len(result) == 2
488+
assert result.tests == 3
489+
405490
def test_xml_statistics(self):
406491
result1 = JUnitXml()
407492
suite1 = TestSuite()

0 commit comments

Comments
 (0)