Skip to content

Commit

Permalink
Merge pull request #72 from jonesbusy/feature/xunit-format
Browse files Browse the repository at this point in the history
Add support for XUnit .NET format
  • Loading branch information
uhafner authored Jan 30, 2024
2 parents 7a20b75 + 7e9013d commit 9f4cfcf
Show file tree
Hide file tree
Showing 10 changed files with 324 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ This library consists basically of two separate parts:
* [PIT](https://pitest.org/) Mutation coverage results
* [JUnit](https://junit.org/junit5/) test results
* [NUnit](https://nunit.org) test results
* [XUnit](https://xunit.net) test results

All source code is licensed under the MIT license. Contributions to this library are welcome!
115 changes: 115 additions & 0 deletions src/main/java/edu/hm/hafner/coverage/parser/XunitParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package edu.hm.hafner.coverage.parser;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

import edu.hm.hafner.coverage.ModuleNode;
import edu.hm.hafner.coverage.TestCase;
import edu.hm.hafner.coverage.TestCase.TestCaseBuilder;

/**
* Parses reports in the
* <a href="https://xunit.net/docs/format-xml-v2">XUnit format</a>
* into a Java object model.
*
* @author Valentin Delaye
*/
@SuppressWarnings("checkstyle:ClassDataAbstractionCoupling")
public class XunitParser extends AbstractTestParser {
private static final long serialVersionUID = -5468593789018138107L;

private static final QName COLLECTION = new QName("collection");
private static final QName TEST = new QName("test");
private static final QName RESULT = new QName("result");
private static final QName TYPE = new QName("type");
private static final String PASS = "Pass";
private static final String FAIL = "Fail";
private static final String SKIP = "Skip";

/**
* Creates a new instance of {@link XunitParser}.
*/
public XunitParser() {
this(ProcessingMode.FAIL_FAST);
}

Check warning on line 37 in src/main/java/edu/hm/hafner/coverage/parser/XunitParser.java

View workflow job for this annotation

GitHub Actions / Quality Monitor

Not covered lines

Lines 36-37 are not covered by tests

/**
* Creates a new instance of {@link XunitParser}.
*
* @param processingMode
* determines whether to ignore errors
*/
public XunitParser(final ProcessingMode processingMode) {
super(processingMode, COLLECTION, TEST);
}

@Override
TestCase readTestCase(final XMLEventReader reader, final StartElement testCaseElement,
final String suiteName, final ModuleNode root) throws XMLStreamException {
var builder = new TestCaseBuilder();

builder.withTestName(getOptionalValueOf(testCaseElement, NAME).orElse(createId()));

readStatus(testCaseElement, builder);

while (reader.hasNext()) {

Check warning on line 58 in src/main/java/edu/hm/hafner/coverage/parser/XunitParser.java

View workflow job for this annotation

GitHub Actions / Quality Monitor

Partially covered line

Line 58 is only partially covered, one branch is missing
XMLEvent event = reader.nextEvent();

if (event.isStartElement() && isFailure(event)) {
readFailure(reader, builder);
}
else if (event.isEndElement() && TEST.equals(event.asEndElement().getName())) {

Check warning on line 64 in src/main/java/edu/hm/hafner/coverage/parser/XunitParser.java

View workflow job for this annotation

GitHub Actions / Quality Monitor

Mutation survived

One mutation survived in line 64 (NegateConditionalsMutator)
Raw output
Survived mutations:
- negated conditional (org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator)
var className = getOptionalValueOf(testCaseElement, TYPE).orElse(suiteName);
builder.withClassName(className);
var packageNode = root.findOrCreatePackageNode(EMPTY);
var classNode = packageNode.findOrCreateClassNode(className);
classNode.addTestCase(builder.build());
return builder.build();

Check warning on line 70 in src/main/java/edu/hm/hafner/coverage/parser/XunitParser.java

View workflow job for this annotation

GitHub Actions / Quality Monitor

Mutation survived

One mutation survived in line 70 (NullReturnValsMutator)
Raw output
Survived mutations:
- replaced return value with null for edu/hm/hafner/coverage/parser/XunitParser::readTestCase (org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator)
}
}
throw createEofException();

Check warning on line 73 in src/main/java/edu/hm/hafner/coverage/parser/XunitParser.java

View workflow job for this annotation

GitHub Actions / Quality Monitor

Not covered line

Line 73 is not covered by tests
}

private void readStatus(final StartElement testCaseElement, final TestCaseBuilder builder) {
var status = getValueOf(testCaseElement, RESULT);
switch (status) {
case PASS:
builder.withStatus(TestCase.TestResult.PASSED);
break;
case FAIL:
builder.withStatus(TestCase.TestResult.FAILED);
break;
case SKIP:
default:
builder.withStatus(TestCase.TestResult.SKIPPED);
break;
}
}

private boolean isFailure(final XMLEvent event) {
return FAILURE.equals(getElementName(event));
}

private void readFailure(final XMLEventReader reader, final TestCaseBuilder builder)
throws XMLStreamException {
builder.withFailure();

var aggregatedContent = new StringBuilder();
while (true) {
XMLEvent event = reader.nextEvent();
if (event.isCharacters()) {
aggregatedContent.append(event.asCharacters().getData());
}
else if (event.isEndElement() && isFailure(event)) {
return;
}
else if (event.isEndElement() && event.asEndElement().getName().equals(MESSAGE)) {

Check warning on line 109 in src/main/java/edu/hm/hafner/coverage/parser/XunitParser.java

View workflow job for this annotation

GitHub Actions / Quality Monitor

Partially covered line

Line 109 is only partially covered, one branch is missing

Check warning on line 109 in src/main/java/edu/hm/hafner/coverage/parser/XunitParser.java

View workflow job for this annotation

GitHub Actions / Quality Monitor

Mutation survived

One mutation survived in line 109 (NegateConditionalsMutator)
Raw output
Survived mutations:
- negated conditional (org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator)
builder.withDescription(aggregatedContent.toString());
return;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import edu.hm.hafner.coverage.parser.NunitParser;
import edu.hm.hafner.coverage.parser.OpenCoverParser;
import edu.hm.hafner.coverage.parser.PitestParser;
import edu.hm.hafner.coverage.parser.XunitParser;

/**
* Provides a registry for all available {@link CoverageParserType parsers}.
Expand All @@ -24,7 +25,8 @@ public enum CoverageParserType {
OPENCOVER,
JACOCO,
PIT,
JUNIT
JUNIT,
XUNIT
}

/**
Expand Down Expand Up @@ -70,6 +72,8 @@ public CoverageParser get(final CoverageParserType parser, final ProcessingMode
return new PitestParser(processingMode);
case JUNIT:
return new JunitParser(processingMode);
case XUNIT:
return new XunitParser(processingMode);
}
throw new IllegalArgumentException("Unknown parser type: " + parser);

Check warning on line 78 in src/main/java/edu/hm/hafner/coverage/registry/ParserRegistry.java

View workflow job for this annotation

GitHub Actions / Quality Monitor

Not covered line

Line 78 is not covered by tests
}
Expand Down
103 changes: 103 additions & 0 deletions src/test/java/edu/hm/hafner/coverage/parser/XunitParserTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package edu.hm.hafner.coverage.parser;

import java.util.Collection;
import java.util.NoSuchElementException;

import org.junit.jupiter.api.Test;

import edu.hm.hafner.coverage.ClassNode;
import edu.hm.hafner.coverage.CoverageParser;
import edu.hm.hafner.coverage.CoverageParser.ProcessingMode;
import edu.hm.hafner.coverage.Metric;
import edu.hm.hafner.coverage.ModuleNode;
import edu.hm.hafner.coverage.Node;
import edu.hm.hafner.coverage.PackageNode;
import edu.hm.hafner.coverage.TestCase;
import edu.hm.hafner.coverage.TestCase.TestResult;
import edu.hm.hafner.coverage.TestCount;

import static edu.hm.hafner.coverage.assertions.Assertions.*;

class XunitParserTest extends AbstractParserTest {
private static final String EMPTY = "-";

@Override
CoverageParser createParser(final ProcessingMode processingMode) {
return new XunitParser(processingMode);
}

@Override
protected String getFolder() {
return "xunit";
}

@Test
void shouldReadReport() {
ModuleNode tree = readReport("xunit.xml");

assertThat(tree).hasName(EMPTY);
assertThat(getPackage(tree)).hasName("-");
assertThat(getFirstClass(tree)).hasName("test.Tests2");
assertThat(getFirstTest(tree).getDescription()).contains("Assert.Equal() Failure");

assertThat(tree.aggregateValues()).contains(new TestCount(3));
}

@Test
void shouldReadReportWithoutFailure() {
ModuleNode tree = readReport("xunit-no-failure-block.xml");

assertThat(tree).hasName(EMPTY);
assertThat(getPackage(tree)).hasName("-");
assertThat(getFirstClass(tree)).hasName("test.Tests2");
assertThat(getFirstTest(tree).getDescription()).contains("");
assertThat(tree.aggregateValues()).contains(new TestCount(3));
}

@Test
void shouldReadReportWithInvalidStatus() {
ModuleNode tree = readReport("xunit-invalid-status.xml");

assertThat(tree).hasName(EMPTY);
assertThat(getPackage(tree)).hasName("-");
assertThat(getFirstClass(tree)).hasName("test.Tests2");
assertThat(tree.aggregateValues()).contains(new TestCount(3));
}

@Test
void shouldReadReportWithoutErrorMessage() {
ModuleNode tree = readReport("xunit-no-message.xml");

assertThat(tree).hasName(EMPTY);
assertThat(getPackage(tree)).hasName("-");
assertThat(getFirstClass(tree)).hasName("test.Tests2");
assertThat(getFirstTest(tree).getDescription()).contains("");
assertThat(tree.aggregateValues()).contains(new TestCount(3));
}

private PackageNode getPackage(final Node node) {
var children = node.getChildren();
assertThat(children).hasSize(1).first().isInstanceOf(PackageNode.class);

return (PackageNode) children.get(0);
}

private ClassNode getFirstClass(final Node node) {
var packageNode = getPackage(node);

var children = packageNode.getChildren();
assertThat(children).isNotEmpty().first().isInstanceOf(ClassNode.class);

return (ClassNode) children.get(0);
}

private TestCase getFirstTest(final Node node) {
return node.getAll(Metric.CLASS).stream()
.map(ClassNode.class::cast)
.map(ClassNode::getTestCases)
.flatMap(Collection::stream)
.filter(test -> test.getResult() == TestResult.FAILED)
.findFirst()
.orElseThrow(() -> new NoSuchElementException("No failed test found"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import edu.hm.hafner.coverage.parser.JunitParser;
import edu.hm.hafner.coverage.parser.NunitParser;
import edu.hm.hafner.coverage.parser.PitestParser;
import edu.hm.hafner.coverage.parser.XunitParser;
import edu.hm.hafner.coverage.parser.OpenCoverParser;
import edu.hm.hafner.coverage.registry.ParserRegistry.CoverageParserType;

Expand All @@ -28,6 +29,7 @@ void shouldCreateSomeParsers() {
.isInstanceOf(JunitParser.class);
assertThat(registry.get(CoverageParserType.OPENCOVER, ProcessingMode.IGNORE_ERRORS)).isInstanceOf(OpenCoverParser.class);
assertThat(registry.get(CoverageParserType.NUNIT, ProcessingMode.IGNORE_ERRORS)).isInstanceOf(NunitParser.class);
assertThat(registry.get(CoverageParserType.XUNIT, ProcessingMode.IGNORE_ERRORS)).isInstanceOf(XunitParser.class);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<assembly name="test.dll" run-date="2024-01-23" run-time="12:33:37" total="0" passed="0" failed="0" skipped="0" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<assemblies timestamp="01/26/2024 13:49:51">
<assembly name="/home/jenkins/test2/bin/Debug/net8.0/test2.dll" run-date="2024-01-26" run-time="13:49:51" total="3" passed="1" failed="1" skipped="1" time="1.100" errors="0">
<errors />
<collection total="3" passed="1" failed="1" skipped="1" name="Test collection for test.Tests2" time="0.005">
<test name="test.Tests2.ShouldCreateItem" type="test.Tests2" method="ShouldCreateItem" time="0.0026206" result="invalid">
<traits />
</test>
<test name="test.Tests2.ShouldCreateItem3" type="test.Tests2" method="ShouldCreateItem3" time="0.0010000" result="invalid">
<reason><![CDATA[specific reason]]></reason>
<output>specific reason
</output>
<traits />
</test>
<test name="test.Tests2.ShouldCreateItem2" type="test.Tests2" method="ShouldCreateItem2" time="0.0010159" result="invalid">
<failure>
<message>Assert.Equal() Failure
↓ (pos 4)
Expected: Test3
Actual: Test
↑ (pos 4)</message>
<stack-trace> at test.Tests2.ShouldCreateItem2() in /home/jenkins/test2/Tests2.cs:line 36
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)</stack-trace>
</failure>
<traits />
</test>
</collection>
</assembly>
</assemblies>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<assemblies timestamp="01/26/2024 13:49:51">
<assembly name="/home/jenkins/test2/bin/Debug/net8.0/test2.dll" run-date="2024-01-26" run-time="13:49:51" total="3" passed="1" failed="1" skipped="1" time="1.100" errors="0">
<errors />
<collection total="3" passed="1" failed="1" skipped="1" name="Test collection for test.Tests2" time="0.005">
<test name="test.Tests2.ShouldCreateItem" type="test.Tests2" method="ShouldCreateItem" time="0.0026206" result="Pass">
<traits />
</test>
<test name="test.Tests2.ShouldCreateItem3" type="test.Tests2" method="ShouldCreateItem3" time="0.0010000" result="Skip">
<reason><![CDATA[specific reason]]></reason>
<output>specific reason
</output>
<traits />
</test>
<test name="test.Tests2.ShouldCreateItem2" type="test.Tests2" method="ShouldCreateItem2" time="0.0010159" result="Fail">
<traits />
</test>
</collection>
</assembly>
</assemblies>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<assemblies timestamp="01/26/2024 13:49:51">
<assembly name="/home/jenkins/test2/bin/Debug/net8.0/test2.dll" run-date="2024-01-26" run-time="13:49:51" total="3" passed="1" failed="1" skipped="1" time="1.100" errors="0">
<errors />
<collection total="3" passed="1" failed="1" skipped="1" name="Test collection for test.Tests2" time="0.005">
<test name="test.Tests2.ShouldCreateItem" type="test.Tests2" method="ShouldCreateItem" time="0.0026206" result="Pass">
<traits />
</test>
<test name="test.Tests2.ShouldCreateItem3" type="test.Tests2" method="ShouldCreateItem3" time="0.0010000" result="Skip">
<reason><![CDATA[specific reason]]></reason>
<output>specific reason
</output>
<traits />
</test>
<test name="test.Tests2.ShouldCreateItem2" type="test.Tests2" method="ShouldCreateItem2" time="0.0010159" result="Fail">
<failure />
<traits />
</test>
</collection>
</assembly>
</assemblies>
29 changes: 29 additions & 0 deletions src/test/resources/edu/hm/hafner/coverage/parser/xunit/xunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<assemblies timestamp="01/26/2024 13:49:51">
<assembly name="/home/jenkins/test2/bin/Debug/net8.0/test2.dll" run-date="2024-01-26" run-time="13:49:51" total="3" passed="1" failed="1" skipped="1" time="1.100" errors="0">
<errors />
<collection total="3" passed="1" failed="1" skipped="1" name="Test collection for test.Tests2" time="0.005">
<test name="test.Tests2.ShouldCreateItem" type="test.Tests2" method="ShouldCreateItem" time="0.0026206" result="Pass">
<traits />
</test>
<test name="test.Tests2.ShouldCreateItem3" type="test.Tests2" method="ShouldCreateItem3" time="0.0010000" result="Skip">
<reason><![CDATA[specific reason]]></reason>
<output>specific reason
</output>
<traits />
</test>
<test name="test.Tests2.ShouldCreateItem2" type="test.Tests2" method="ShouldCreateItem2" time="0.0010159" result="Fail">
<failure>
<message>Assert.Equal() Failure
↓ (pos 4)
Expected: Test3
Actual: Test
↑ (pos 4)</message>
<stack-trace> at test.Tests2.ShouldCreateItem2() in /home/jenkins/test2/Tests2.cs:line 36
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)</stack-trace>
</failure>
<traits />
</test>
</collection>
</assembly>
</assemblies>

0 comments on commit 9f4cfcf

Please sign in to comment.