Skip to content

Commit

Permalink
Merge pull request #69 from jonesbusy/feature/nunit-parser
Browse files Browse the repository at this point in the history
Add support for NUnit format
  • Loading branch information
uhafner authored Jan 24, 2024
2 parents a8ed3c1 + 601dcc1 commit 61e5134
Show file tree
Hide file tree
Showing 10 changed files with 570 additions and 0 deletions.
165 changes: 165 additions & 0 deletions src/main/java/edu/hm/hafner/coverage/parser/NunitParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package edu.hm.hafner.coverage.parser;

import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

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.CoverageParser;
import edu.hm.hafner.coverage.ModuleNode;
import edu.hm.hafner.coverage.TestCase;
import edu.hm.hafner.coverage.TestCase.TestCaseBuilder;
import edu.hm.hafner.util.FilteredLog;
import edu.hm.hafner.util.SecureXmlParserFactory;
import edu.hm.hafner.util.SecureXmlParserFactory.ParsingException;

/**
* Parses reports in the <a href="https://docs.nunit.org/articles/nunit/technical-notes/usage/Test-Result-XML-Format.html">NUnit format</a> into a Java object model.
*
* @author Valentin Delaye
*/
@SuppressWarnings("checkstyle:ClassDataAbstractionCoupling")
public class NunitParser extends CoverageParser {
private static final long serialVersionUID = -5468593789018138107L;

private static final QName TEST_SUITE = new QName("test-suite");
private static final QName TEST_CASE = new QName("test-case");
private static final QName NAME = new QName("name");
private static final QName CLASS_NAME = new QName("classname");
private static final QName FAILURE = new QName("failure");
private static final QName MESSAGE = new QName("message");
private static final QName RESULT = new QName("result");
private static final String PASSED = "Passed";
private static final String FAILED = "Failed";
private static final String SKIPPED = "Skipped";

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

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

@Override
protected ModuleNode parseReport(final Reader reader, final FilteredLog log) {
try {
var factory = new SecureXmlParserFactory();
var eventReader = factory.createXmlEventReader(reader);

var root = new ModuleNode(EMPTY);
var tests = readTestCases(eventReader, root);
handleEmptyResults(log, tests.isEmpty());
return root;
}
catch (XMLStreamException exception) {
throw new ParsingException(exception);
}
}

private List<Object> readTestCases(final XMLEventReader eventReader,
final ModuleNode root) throws XMLStreamException {
String suiteName = EMPTY;
var tests = new ArrayList<>();
while (eventReader.hasNext()) {
XMLEvent event = eventReader.nextEvent();

if (event.isStartElement() && TEST_SUITE.equals(event.asStartElement().getName())) {
suiteName = getOptionalValueOf(event.asStartElement(), NAME).orElse(EMPTY);
}
else if (event.isStartElement() && TEST_CASE.equals(event.asStartElement().getName())) {
tests.add(readTestCase(eventReader, event.asStartElement(), suiteName, root));
}
}
return tests;
}

private 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()));

var status = getValueOf(testCaseElement, RESULT);
switch (status) {
case PASSED:
builder.withStatus(TestCase.TestResult.PASSED);
break;
case FAILED:
builder.withStatus(TestCase.TestResult.FAILED);
break;
case SKIPPED:
default:
builder.withStatus(TestCase.TestResult.SKIPPED);
break;
}

while (reader.hasNext()) {

Check warning on line 113 in src/main/java/edu/hm/hafner/coverage/parser/NunitParser.java

View workflow job for this annotation

GitHub Actions / Quality Monitor

Partially covered line

Line 113 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_CASE.equals(event.asEndElement().getName())) {
var className = getOptionalValueOf(testCaseElement, CLASS_NAME).orElse(suiteName);
builder.withClassName(className);
var packageNode = root.findOrCreatePackageNode(EMPTY);
var classNode = packageNode.findOrCreateClassNode(className);
classNode.addTestCase(builder.build());
break;
}
}
return builder.build();

Check warning on line 128 in src/main/java/edu/hm/hafner/coverage/parser/NunitParser.java

View workflow job for this annotation

GitHub Actions / Quality Monitor

Mutation survived

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

private boolean isFailure(final XMLEvent event) {
QName name;
if (event.isStartElement()) {
name = event.asStartElement().getName();
}
else {
name = event.asEndElement().getName();
}

return FAILURE.equals(name);
}

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 155 in src/main/java/edu/hm/hafner/coverage/parser/NunitParser.java

View workflow job for this annotation

GitHub Actions / Quality Monitor

Partially covered line

Line 155 is only partially covered, one branch is missing

Check warning on line 155 in src/main/java/edu/hm/hafner/coverage/parser/NunitParser.java

View workflow job for this annotation

GitHub Actions / Quality Monitor

Mutation survived

One mutation survived in line 155 (NegateConditionalsMutator)
Raw output
Survived mutations:
- negated conditional (org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator)
builder.withDescription(aggregatedContent.toString());
return;
}
}
}

private String createId() {
return UUID.randomUUID().toString();

Check warning on line 163 in src/main/java/edu/hm/hafner/coverage/parser/NunitParser.java

View workflow job for this annotation

GitHub Actions / Quality Monitor

Mutation survived

One mutation survived in line 163 (EmptyObjectReturnValsMutator)
Raw output
Survived mutations:
- replaced return value with "" for edu/hm/hafner/coverage/parser/NunitParser::createId (org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import edu.hm.hafner.coverage.parser.CoberturaParser;
import edu.hm.hafner.coverage.parser.JacocoParser;
import edu.hm.hafner.coverage.parser.JunitParser;
import edu.hm.hafner.coverage.parser.NunitParser;
import edu.hm.hafner.coverage.parser.OpenCoverParser;
import edu.hm.hafner.coverage.parser.PitestParser;

Expand All @@ -19,6 +20,7 @@ public class ParserRegistry {
/** Supported parsers. */
public enum CoverageParserType {
COBERTURA,
NUNIT,
OPENCOVER,
JACOCO,
PIT,
Expand Down Expand Up @@ -60,6 +62,8 @@ public CoverageParser get(final CoverageParserType parser, final ProcessingMode
return new CoberturaParser(processingMode);
case OPENCOVER:
return new OpenCoverParser();
case NUNIT:
return new NunitParser();
case JACOCO:
return new JacocoParser();
case PIT:
Expand Down
115 changes: 115 additions & 0 deletions src/test/java/edu/hm/hafner/coverage/parser/NunitParserTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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 NunitParserTest extends AbstractParserTest {
private static final String EMPTY = "-";

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

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

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

assertThat(tree).hasName(EMPTY);
assertThat(getPackage(tree)).hasName("-");
assertThat(getFirstClass(tree)).hasName("Tests");
assertThat(getFirstTest(tree).getDescription()).contains("Expected string length 4 but was 5. Strings differ at index 4");

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

@Test
void shouldReadReportInV2Format() {
ModuleNode tree = readReport("nunit2-format.xml");

assertThat(tree).hasName(EMPTY);
assertThat(getPackage(tree)).hasName("-");
assertThat(getFirstClass(tree)).hasName("MockTestFixture");
assertThat(getFirstTest(tree).getDescription()).contains("Intentional failure");

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

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

assertThat(tree).hasName(EMPTY);
assertThat(getPackage(tree)).hasName("-");
assertThat(getFirstClass(tree)).hasName("Tests");
assertThat(getFirstTest(tree).getDescription()).contains("");
assertThat(tree.aggregateValues()).contains(new TestCount(4));
}

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

assertThat(tree).hasName(EMPTY);
assertThat(getPackage(tree)).hasName("-");
assertThat(getFirstClass(tree)).hasName("Tests");
assertThat(getFirstTest(tree).getDescription()).contains("");
assertThat(tree.aggregateValues()).contains(new TestCount(4));
}

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

assertThat(tree).hasName(EMPTY);
assertThat(getPackage(tree)).hasName("-");
assertThat(getFirstClass(tree)).hasName("Tests");
assertThat(tree.aggregateValues()).contains(new TestCount(4));
}

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 @@ -6,6 +6,7 @@
import edu.hm.hafner.coverage.parser.CoberturaParser;
import edu.hm.hafner.coverage.parser.JacocoParser;
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.OpenCoverParser;
import edu.hm.hafner.coverage.registry.ParserRegistry.CoverageParserType;
Expand All @@ -26,6 +27,7 @@ void shouldCreateSomeParsers() {
assertThat(registry.get(CoverageParserType.JUNIT, ProcessingMode.IGNORE_ERRORS))
.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);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<test-run id="1" duration="0.0" testcasecount="0" total="0" passed="0" failed="0" inconclusive="0" skipped="0" result="Passed" start-time="2024-01-23T 12:33:37Z" end-time="2024-01-23T 12:33:37Z">
</test-run>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<test-run id="2" duration="0.46225700000000003" testcasecount="4" total="4" passed="2" failed="1" inconclusive="0" skipped="1" result="Failed" start-time="2024-01-23T 12:33:37Z" end-time="2024-01-23T 12:33:47Z">
<test-suite type="Assembly" name="test.dll" fullname="/home/jenkins/bin/Debug/net8.0/test.dll" total="4" passed="2" failed="1" inconclusive="0" skipped="1" result="Failed" start-time="2024-01-23T 12:33:46Z" end-time="2024-01-23T 12:33:47Z" duration="0.462257">
<test-suite type="TestSuite" name="test" fullname="test" total="4" passed="2" failed="1" inconclusive="0" skipped="1" result="Failed" start-time="2024-01-23T 12:33:46Z" end-time="2024-01-23T 12:33:47Z" duration="0.462257">
<test-suite type="TestFixture" name="Tests" fullname="test.Tests" classname="test.Tests" total="4" passed="2" failed="1" inconclusive="0" skipped="1" result="Failed" start-time="2024-01-23T 12:33:46Z" end-time="2024-01-23T 12:33:47Z" duration="0.462257">
<test-case name="FailedTEst" fullname="test.Tests.FailedTEst" methodname="FailedTEst" classname="Tests" result="Failed" start-time="2024-01-23T 12:33:46Z" end-time="2024-01-23T 12:33:46Z" duration="0.057283" asserts="0" seed="1556857297">
<failure>
<message> Expected string length 4 but was 5. Strings differ at index 4.
Expected: "Test"
But was: "Test1"
---------------^
</message>
<stack-trace> at test.Tests.FailedTEst() in /home/jenkins/Tests.cs:line 50

1) at test.Tests.FailedTEst() in /home/jenkins/Tests.cs:line 50

</stack-trace>
</failure>
</test-case>
<test-case name="IgnoredTest" fullname="test.Tests.IgnoredTest" methodname="IgnoredTest" classname="Tests" result="Invalid" start-time="2024-01-23T 12:33:46Z" end-time="2024-01-23T 12:33:46Z" duration="0.000378" asserts="0" seed="1438305193">
<output><![CDATA[Skipping this test
]]></output>
</test-case>
<test-case name="ShouldConnectToDatabase" fullname="test.Tests.ShouldConnectToDatabase" methodname="ShouldConnectToDatabase" classname="Tests" result="Invalid" start-time="2024-01-23T 12:33:46Z" end-time="2024-01-23T 12:33:47Z" duration="0.404245" asserts="0" seed="802014458" />
<test-case name="ShouldCreateItem" fullname="test.Tests.ShouldCreateItem" methodname="ShouldCreateItem" classname="Tests" result="Passed" start-time="2024-01-23T 12:33:47Z" end-time="2024-01-23T 12:33:47Z" duration="0.000351" asserts="0" seed="606875445" />
</test-suite>
</test-suite>
<errors />
</test-suite>
</test-run>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<test-run id="2" duration="0.46225700000000003" testcasecount="4" total="4" passed="2" failed="1" inconclusive="0" skipped="1" result="Failed" start-time="2024-01-23T 12:33:37Z" end-time="2024-01-23T 12:33:47Z">
<test-suite type="Assembly" name="test.dll" fullname="/home/jenkins/bin/Debug/net8.0/test.dll" total="4" passed="2" failed="1" inconclusive="0" skipped="1" result="Failed" start-time="2024-01-23T 12:33:46Z" end-time="2024-01-23T 12:33:47Z" duration="0.462257">
<test-suite type="TestSuite" name="test" fullname="test" total="4" passed="2" failed="1" inconclusive="0" skipped="1" result="Failed" start-time="2024-01-23T 12:33:46Z" end-time="2024-01-23T 12:33:47Z" duration="0.462257">
<test-suite type="TestFixture" name="Tests" fullname="test.Tests" classname="test.Tests" total="4" passed="2" failed="1" inconclusive="0" skipped="1" result="Failed" start-time="2024-01-23T 12:33:46Z" end-time="2024-01-23T 12:33:47Z" duration="0.462257">
<test-case name="FailedTEst" fullname="test.Tests.FailedTEst" methodname="FailedTEst" classname="Tests" result="Failed" start-time="2024-01-23T 12:33:46Z" end-time="2024-01-23T 12:33:46Z" duration="0.057283" asserts="0" seed="1556857297" />
<test-case name="IgnoredTest" fullname="test.Tests.IgnoredTest" methodname="IgnoredTest" classname="Tests" result="Skipped" start-time="2024-01-23T 12:33:46Z" end-time="2024-01-23T 12:33:46Z" duration="0.000378" asserts="0" seed="1438305193">
<output><![CDATA[Skipping this test
]]></output>
</test-case>
<test-case name="ShouldConnectToDatabase" fullname="test.Tests.ShouldConnectToDatabase" methodname="ShouldConnectToDatabase" classname="Tests" result="Passed" start-time="2024-01-23T 12:33:46Z" end-time="2024-01-23T 12:33:47Z" duration="0.404245" asserts="0" seed="802014458" />
<test-case name="ShouldCreateItem" fullname="test.Tests.ShouldCreateItem" methodname="ShouldCreateItem" classname="Tests" result="Passed" start-time="2024-01-23T 12:33:47Z" end-time="2024-01-23T 12:33:47Z" duration="0.000351" asserts="0" seed="606875445" />
</test-suite>
</test-suite>
<errors />
</test-suite>
</test-run>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<test-run id="2" duration="0.46225700000000003" testcasecount="4" total="4" passed="2" failed="1" inconclusive="0" skipped="1" result="Failed" start-time="2024-01-23T 12:33:37Z" end-time="2024-01-23T 12:33:47Z">
<test-suite type="Assembly" name="test.dll" fullname="/home/jenkins/bin/Debug/net8.0/test.dll" total="4" passed="2" failed="1" inconclusive="0" skipped="1" result="Failed" start-time="2024-01-23T 12:33:46Z" end-time="2024-01-23T 12:33:47Z" duration="0.462257">
<test-suite type="TestSuite" name="test" fullname="test" total="4" passed="2" failed="1" inconclusive="0" skipped="1" result="Failed" start-time="2024-01-23T 12:33:46Z" end-time="2024-01-23T 12:33:47Z" duration="0.462257">
<test-suite type="TestFixture" name="Tests" fullname="test.Tests" classname="test.Tests" total="4" passed="2" failed="1" inconclusive="0" skipped="1" result="Failed" start-time="2024-01-23T 12:33:46Z" end-time="2024-01-23T 12:33:47Z" duration="0.462257">
<test-case name="FailedTEst" fullname="test.Tests.FailedTEst" methodname="FailedTEst" classname="Tests" result="Failed" start-time="2024-01-23T 12:33:46Z" end-time="2024-01-23T 12:33:46Z" duration="0.057283" asserts="0" seed="1556857297">
<failure />
</test-case>
<test-case name="IgnoredTest" fullname="test.Tests.IgnoredTest" methodname="IgnoredTest" classname="Tests" result="Skipped" start-time="2024-01-23T 12:33:46Z" end-time="2024-01-23T 12:33:46Z" duration="0.000378" asserts="0" seed="1438305193">
<output><![CDATA[Skipping this test
]]></output>
</test-case>
<test-case name="ShouldConnectToDatabase" fullname="test.Tests.ShouldConnectToDatabase" methodname="ShouldConnectToDatabase" classname="Tests" result="Passed" start-time="2024-01-23T 12:33:46Z" end-time="2024-01-23T 12:33:47Z" duration="0.404245" asserts="0" seed="802014458" />
<test-case name="ShouldCreateItem" fullname="test.Tests.ShouldCreateItem" methodname="ShouldCreateItem" classname="Tests" result="Passed" start-time="2024-01-23T 12:33:47Z" end-time="2024-01-23T 12:33:47Z" duration="0.000351" asserts="0" seed="606875445" />
</test-suite>
</test-suite>
<errors />
</test-suite>
</test-run>
Loading

0 comments on commit 61e5134

Please sign in to comment.