diff --git a/test/unit/org/apache/cassandra/CassandraXMLJUnitResultFormatter.java b/test/unit/org/apache/cassandra/CassandraXMLJUnitResultFormatter.java index bd6093d1c71..e4ffb82083a 100644 --- a/test/unit/org/apache/cassandra/CassandraXMLJUnitResultFormatter.java +++ b/test/unit/org/apache/cassandra/CassandraXMLJUnitResultFormatter.java @@ -338,7 +338,6 @@ private void formatError(final String type, final Test test, final Throwable t) failedTests.put(test, test); } - final Element nested = doc.createElement(type); Element currentTest; if (test != null) { currentTest = testElements.get(createDescription(test)); @@ -346,6 +345,28 @@ private void formatError(final String type, final Test test, final Throwable t) currentTest = rootElement; } + // A test can trigger both addFailure and addError (e.g. assertion failure during the test + // body followed by an error during teardown). The JUnit XML spec allows only one status + // child per , so if one already exists we append the new message to it rather + // than adding a second child element (which would break XML parsers that enforce this rule). + Element existing = (Element) currentTest.getElementsByTagName(FAILURE).item(0); + if (existing == null) + existing = (Element) currentTest.getElementsByTagName(ERROR).item(0); + + if (existing != null) + { + final String message = t.getMessage(); + if (message != null && message.length() > 0) + { + String prev = existing.getAttribute(ATTR_MESSAGE); + existing.setAttribute(ATTR_MESSAGE, prev.isEmpty() ? message : prev + " | " + message); + } + final String strace = JUnitTestRunner.getFilteredTrace(t); + existing.appendChild(doc.createTextNode("\n--- additional error ---\n" + strace)); + return; + } + + final Element nested = doc.createElement(type); currentTest.appendChild(nested); final String message = t.getMessage(); diff --git a/test/unit/org/apache/cassandra/CassandraXMLJUnitResultFormatterTest.java b/test/unit/org/apache/cassandra/CassandraXMLJUnitResultFormatterTest.java new file mode 100644 index 00000000000..6a0618bc28d --- /dev/null +++ b/test/unit/org/apache/cassandra/CassandraXMLJUnitResultFormatterTest.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra; + +import java.io.ByteArrayOutputStream; + +import javax.xml.parsers.DocumentBuilderFactory; + +import org.apache.tools.ant.taskdefs.optional.junit.JUnitTest; +import org.junit.Before; +import org.junit.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import junit.framework.AssertionFailedError; // checkstyle: permit this import + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class CassandraXMLJUnitResultFormatterTest +{ + private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); + + // Minimal junit.framework.Test implementation used to drive the formatter. + private static class FakeTest implements junit.framework.Test + { + private final String className; + private final String methodName; + + FakeTest(String className, String methodName) + { + this.className = className; + this.methodName = methodName; + } + + @Override + public int countTestCases() { return 1; } + + @Override + public void run(junit.framework.TestResult result) {} + + // Providing toString() in JUnit3 name-extraction format: "methodName(className)" + @Override + public String toString() { return methodName + "(" + className + ")"; } + } + + private CassandraXMLJUnitResultFormatter formatter; + private ByteArrayOutputStream out; + + @Before + public void setUp() + { + formatter = new CassandraXMLJUnitResultFormatter(); + out = new ByteArrayOutputStream(); + formatter.setOutput(out); + JUnitTest suite = new JUnitTest("org.apache.cassandra.SomeTest"); + formatter.startTestSuite(suite); + } + + /** + * Regression test for CASSANDRA-21396: when both addFailure and addError are called for the + * same test (e.g. assertion failure in test body + error during teardown), the formatter must + * emit exactly one status child element under testcase section rather than two. + * Having two status children violates the JUnit XML spec and breaks ci_summary.html generation. + * Both messages must be preserved in the surviving element's message attribute. + */ + @Test + public void testFailureThenErrorProducesOneStatusChild() throws Exception + { + FakeTest test = new FakeTest("org.apache.cassandra.SomeTest", "someTestMethod"); + + formatter.startTest(test); + formatter.addFailure(test, new AssertionFailedError("first failure message")); + formatter.addError(test, new RuntimeException("second error message")); + formatter.endTest(test); + + JUnitTest suite = new JUnitTest("org.apache.cassandra.SomeTest"); + suite.setCounts(1, 1, 1, 0); + formatter.endTestSuite(suite); + + Document doc = parseOutput(); + NodeList testcases = doc.getElementsByTagName("testcase"); + assertEquals("Expected exactly one testcase element", 1, testcases.getLength()); + + Element testcase = (Element) testcases.item(0); + NodeList failures = testcase.getElementsByTagName("failure"); + NodeList errors = testcase.getElementsByTagName("error"); + int totalStatusChildren = failures.getLength() + errors.getLength(); + assertEquals("Expected exactly one status child element (failure or error) per testcase", 1, totalStatusChildren); + + Element statusElem = (Element) (failures.getLength() > 0 ? failures.item(0) : errors.item(0)); + String message = statusElem.getAttribute("message"); + assertTrue("message should contain first failure text", message.contains("first failure message")); + assertTrue("message should contain second error text", message.contains("second error message")); + } + + @Test + public void testSingleFailureIsUnchanged() throws Exception + { + FakeTest test = new FakeTest("org.apache.cassandra.SomeTest", "someTestMethod"); + + formatter.startTest(test); + formatter.addFailure(test, new AssertionFailedError("only failure")); + formatter.endTest(test); + + JUnitTest suite = new JUnitTest("org.apache.cassandra.SomeTest"); + suite.setCounts(1, 1, 0, 0); + formatter.endTestSuite(suite); + + Document doc = parseOutput(); + Element testcase = (Element) doc.getElementsByTagName("testcase").item(0); + NodeList failures = testcase.getElementsByTagName("failure"); + NodeList errors = testcase.getElementsByTagName("error"); + + assertEquals("Expected one failure element", 1, failures.getLength()); + assertEquals("Expected no error element", 0, errors.getLength()); + assertEquals("only failure", ((Element) failures.item(0)).getAttribute("message")); + } + + @Test + public void testPassedTestHasNoStatusChild() throws Exception + { + FakeTest test = new FakeTest("org.apache.cassandra.SomeTest", "someTestMethod"); + + formatter.startTest(test); + formatter.endTest(test); + + JUnitTest suite = new JUnitTest("org.apache.cassandra.SomeTest"); + suite.setCounts(1, 0, 0, 0); + formatter.endTestSuite(suite); + + Document doc = parseOutput(); + Element testcase = (Element) doc.getElementsByTagName("testcase").item(0); + NodeList failures = testcase.getElementsByTagName("failure"); + NodeList errors = testcase.getElementsByTagName("error"); + NodeList skipped = testcase.getElementsByTagName("skipped"); + + assertEquals(0, failures.getLength()); + assertEquals(0, errors.getLength()); + assertEquals(0, skipped.getLength()); + } + + private Document parseOutput() throws Exception + { + return DOCUMENT_BUILDER_FACTORY.newDocumentBuilder().parse(new java.io.ByteArrayInputStream(out.toByteArray())); + } +}