Skip to content

Commit 9c3caa8

Browse files
committed
Merge branch 'cassandra-6.0' into trunk
* cassandra-6.0: A test emitting both a failure and error element in the same JUnit XML testcase crashes CI summary generation
2 parents 7456dbc + ae44744 commit 9c3caa8

2 files changed

Lines changed: 186 additions & 1 deletion

File tree

test/unit/org/apache/cassandra/CassandraXMLJUnitResultFormatter.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,14 +338,35 @@ private void formatError(final String type, final Test test, final Throwable t)
338338
failedTests.put(test, test);
339339
}
340340

341-
final Element nested = doc.createElement(type);
342341
Element currentTest;
343342
if (test != null) {
344343
currentTest = testElements.get(createDescription(test));
345344
} else {
346345
currentTest = rootElement;
347346
}
348347

348+
// A test can trigger both addFailure and addError (e.g. assertion failure during the test
349+
// body followed by an error during teardown). The JUnit XML spec allows only one status
350+
// child per <testcase>, so if one already exists we append the new message to it rather
351+
// than adding a second child element (which would break XML parsers that enforce this rule).
352+
Element existing = (Element) currentTest.getElementsByTagName(FAILURE).item(0);
353+
if (existing == null)
354+
existing = (Element) currentTest.getElementsByTagName(ERROR).item(0);
355+
356+
if (existing != null)
357+
{
358+
final String message = t.getMessage();
359+
if (message != null && message.length() > 0)
360+
{
361+
String prev = existing.getAttribute(ATTR_MESSAGE);
362+
existing.setAttribute(ATTR_MESSAGE, prev.isEmpty() ? message : prev + " | " + message);
363+
}
364+
final String strace = JUnitTestRunner.getFilteredTrace(t);
365+
existing.appendChild(doc.createTextNode("\n--- additional error ---\n" + strace));
366+
return;
367+
}
368+
369+
final Element nested = doc.createElement(type);
349370
currentTest.appendChild(nested);
350371

351372
final String message = t.getMessage();
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.cassandra;
20+
21+
import java.io.ByteArrayOutputStream;
22+
23+
import javax.xml.parsers.DocumentBuilderFactory;
24+
25+
import org.apache.tools.ant.taskdefs.optional.junit.JUnitTest;
26+
import org.junit.Before;
27+
import org.junit.Test;
28+
import org.w3c.dom.Document;
29+
import org.w3c.dom.Element;
30+
import org.w3c.dom.NodeList;
31+
32+
import junit.framework.AssertionFailedError; // checkstyle: permit this import
33+
34+
import static org.junit.Assert.assertEquals;
35+
import static org.junit.Assert.assertTrue;
36+
37+
public class CassandraXMLJUnitResultFormatterTest
38+
{
39+
private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
40+
41+
// Minimal junit.framework.Test implementation used to drive the formatter.
42+
private static class FakeTest implements junit.framework.Test
43+
{
44+
private final String className;
45+
private final String methodName;
46+
47+
FakeTest(String className, String methodName)
48+
{
49+
this.className = className;
50+
this.methodName = methodName;
51+
}
52+
53+
@Override
54+
public int countTestCases() { return 1; }
55+
56+
@Override
57+
public void run(junit.framework.TestResult result) {}
58+
59+
// Providing toString() in JUnit3 name-extraction format: "methodName(className)"
60+
@Override
61+
public String toString() { return methodName + "(" + className + ")"; }
62+
}
63+
64+
private CassandraXMLJUnitResultFormatter formatter;
65+
private ByteArrayOutputStream out;
66+
67+
@Before
68+
public void setUp()
69+
{
70+
formatter = new CassandraXMLJUnitResultFormatter();
71+
out = new ByteArrayOutputStream();
72+
formatter.setOutput(out);
73+
JUnitTest suite = new JUnitTest("org.apache.cassandra.SomeTest");
74+
formatter.startTestSuite(suite);
75+
}
76+
77+
/**
78+
* Regression test for CASSANDRA-21396: when both addFailure and addError are called for the
79+
* same test (e.g. assertion failure in test body + error during teardown), the formatter must
80+
* emit exactly one status child element under testcase section rather than two.
81+
* Having two status children violates the JUnit XML spec and breaks ci_summary.html generation.
82+
* Both messages must be preserved in the surviving element's message attribute.
83+
*/
84+
@Test
85+
public void testFailureThenErrorProducesOneStatusChild() throws Exception
86+
{
87+
FakeTest test = new FakeTest("org.apache.cassandra.SomeTest", "someTestMethod");
88+
89+
formatter.startTest(test);
90+
formatter.addFailure(test, new AssertionFailedError("first failure message"));
91+
formatter.addError(test, new RuntimeException("second error message"));
92+
formatter.endTest(test);
93+
94+
JUnitTest suite = new JUnitTest("org.apache.cassandra.SomeTest");
95+
suite.setCounts(1, 1, 1, 0);
96+
formatter.endTestSuite(suite);
97+
98+
Document doc = parseOutput();
99+
NodeList testcases = doc.getElementsByTagName("testcase");
100+
assertEquals("Expected exactly one testcase element", 1, testcases.getLength());
101+
102+
Element testcase = (Element) testcases.item(0);
103+
NodeList failures = testcase.getElementsByTagName("failure");
104+
NodeList errors = testcase.getElementsByTagName("error");
105+
int totalStatusChildren = failures.getLength() + errors.getLength();
106+
assertEquals("Expected exactly one status child element (failure or error) per testcase", 1, totalStatusChildren);
107+
108+
Element statusElem = (Element) (failures.getLength() > 0 ? failures.item(0) : errors.item(0));
109+
String message = statusElem.getAttribute("message");
110+
assertTrue("message should contain first failure text", message.contains("first failure message"));
111+
assertTrue("message should contain second error text", message.contains("second error message"));
112+
}
113+
114+
@Test
115+
public void testSingleFailureIsUnchanged() throws Exception
116+
{
117+
FakeTest test = new FakeTest("org.apache.cassandra.SomeTest", "someTestMethod");
118+
119+
formatter.startTest(test);
120+
formatter.addFailure(test, new AssertionFailedError("only failure"));
121+
formatter.endTest(test);
122+
123+
JUnitTest suite = new JUnitTest("org.apache.cassandra.SomeTest");
124+
suite.setCounts(1, 1, 0, 0);
125+
formatter.endTestSuite(suite);
126+
127+
Document doc = parseOutput();
128+
Element testcase = (Element) doc.getElementsByTagName("testcase").item(0);
129+
NodeList failures = testcase.getElementsByTagName("failure");
130+
NodeList errors = testcase.getElementsByTagName("error");
131+
132+
assertEquals("Expected one failure element", 1, failures.getLength());
133+
assertEquals("Expected no error element", 0, errors.getLength());
134+
assertEquals("only failure", ((Element) failures.item(0)).getAttribute("message"));
135+
}
136+
137+
@Test
138+
public void testPassedTestHasNoStatusChild() throws Exception
139+
{
140+
FakeTest test = new FakeTest("org.apache.cassandra.SomeTest", "someTestMethod");
141+
142+
formatter.startTest(test);
143+
formatter.endTest(test);
144+
145+
JUnitTest suite = new JUnitTest("org.apache.cassandra.SomeTest");
146+
suite.setCounts(1, 0, 0, 0);
147+
formatter.endTestSuite(suite);
148+
149+
Document doc = parseOutput();
150+
Element testcase = (Element) doc.getElementsByTagName("testcase").item(0);
151+
NodeList failures = testcase.getElementsByTagName("failure");
152+
NodeList errors = testcase.getElementsByTagName("error");
153+
NodeList skipped = testcase.getElementsByTagName("skipped");
154+
155+
assertEquals(0, failures.getLength());
156+
assertEquals(0, errors.getLength());
157+
assertEquals(0, skipped.getLength());
158+
}
159+
160+
private Document parseOutput() throws Exception
161+
{
162+
return DOCUMENT_BUILDER_FACTORY.newDocumentBuilder().parse(new java.io.ByteArrayInputStream(out.toByteArray()));
163+
}
164+
}

0 commit comments

Comments
 (0)