Skip to content

Commit

Permalink
Add support NUnit XML format (#38)
Browse files Browse the repository at this point in the history
* implementation for nunit #4

* reorganized tests to support v2 + v3

* Refactored to support NUnit v2+v3

* updated readme
  • Loading branch information
bryanbcook committed Nov 12, 2023
1 parent a44248f commit 67f113f
Show file tree
Hide file tree
Showing 7 changed files with 813 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Parse test results from JUnit, TestNG, xUnit, Mocha(json), Cucumber(json) and ma
|-------------------------------|---------|
| TestNG ||
| JUnit ||
| NUnit (v2 & v3) ||
| xUnit ||
| Mocha (json & mochawesome) ||
| Cucumber ||
15 changes: 14 additions & 1 deletion src/helpers/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,26 @@ const FORCED_ARRAY_KEYS = [
"testng-results.suite.test",
"testng-results.suite.test.class",
"testng-results.suite.test.class.test-method",
"testng-results.suite.test.class.test-method.exception",
"testng-results.suite.test.class.test-method.exception"
];

const configured_parser = new XMLParser({
isArray: (name, jpath, isLeafNode, isAttribute) => {
if( FORCED_ARRAY_KEYS.indexOf(jpath) !== -1) {
return true;
}
// handle nunit deep hierarchy
else if (jpath.startsWith("test-results") || jpath.startsWith("test-run")) {
let parts = jpath.split(".");
switch(parts[parts.length - 1]) {
case "category":
case "property":
case "test-suite":
case "test-case":
return true;
default:
return false;
}
}
},
ignoreAttributes: false,
Expand Down
3 changes: 3 additions & 0 deletions src/parsers/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const testng = require('./testng');
const junit = require('./junit');
const nunit = require('./nunit');
const xunit = require('./xunit');
const mocha = require('./mocha');
const cucumber = require('./cucumber');
Expand Down Expand Up @@ -37,6 +38,8 @@ function getParser(type) {
return junit;
case 'xunit':
return xunit;
case 'nunit':
return nunit;
case 'mocha':
return mocha;
case 'cucumber':
Expand Down
219 changes: 219 additions & 0 deletions src/parsers/nunit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
const { getJsonFromXMLFile } = require('../helpers/helper');

const TestResult = require('../models/TestResult');
const TestSuite = require('../models/TestSuite');
const TestCase = require('../models/TestCase');

const SUITE_TYPES_WITH_TESTCASES = [
"TestFixture",
"ParameterizedTest",
"GenericFixture",
"ParameterizedMethod" // v3
]

const RESULTMAP = {
Success: "PASS", // v2
Failure: "FAIL", // v2
Ignored: "SKIP", // v2
NotRunnable: "SKIP", // v2
Error: "ERROR", // v2
Inconclusive: "FAIL", // v2

Passed: "PASS", // v3
Failed: "FAIL", // v3
Skipped: "SKIP", // v3
}

function mergeMeta(map1, map2) {
for(let kvp of map1) {
map2.set(kvp[0], kvp[1]);
}
}

function populateMetaData(raw, map) {

// v2 supports categories
if (raw.categories) {
let categories = raw.categories.category;
for (let i = 0; i < categories.length; i++) {
let categoryName = categories[i]["@_name"];
map.set(categoryName, "");

// create comma-delimited list of categories
if (map.has("Categories")) {
map.set("Categories", map.get("Categories").concat(",", categoryName));
} else {
map.set("Categories", categoryName);
}
}
}

// v2/v3 support properties
if (raw.properties) {
let properties = raw.properties.property;
for (let i = 0; i < properties.length; i++) {
let property = properties[i];
let propName = property["@_name"];
let propValue = property["@_value"];

// v3 treats 'Categories' as property "Category"
if (propName == "Category") {

if (map.has("Categories")) {
map.set("Categories", map.get("Categories").concat(",", propValue));
} else {
map.set("Categories", propValue);
}
map.set(propValue, "");

} else {
map.set(propName, propValue);
}
}
}
}

function getNestedTestCases(rawSuite) {
if (rawSuite.results) {
return rawSuite.results["test-case"];
} else {
return rawSuite["test-case"];
}
}

function hasNestedSuite(rawSuite) {
return getNestedSuite(rawSuite) !== null;
}

function getNestedSuite(rawSuite) {
// nunit v2 nests test-suite inside 'results'
if (rawSuite.results && rawSuite.results["test-suite"]) {
return rawSuite.results["test-suite"];
} else {
// nunit v3 nests test-suites as immediate children
if (rawSuite["test-suite"]) {
return rawSuite["test-suite"];
}
else {
// not nested
return null;
}
}
}

function getTestCases(rawSuite, parent_meta) {
var cases = [];

let rawTestCases = getNestedTestCases(rawSuite);
if (rawTestCases) {
for (let i = 0; i < rawTestCases.length; i++) {
let rawCase = rawTestCases[i];
let testCase = new TestCase();
let result = rawCase["@_result"]
testCase.id = rawCase["@_id"] ?? "";
testCase.name = rawCase["@_fullname"] ?? rawCase["@_name"];
testCase.duration = rawCase["@_time"] * 1000; // in milliseconds
testCase.status = RESULTMAP[result];

// v2 : non-executed should be tests should be Ignored
if (rawCase["@_executed"] == "False") {
testCase.status = "SKIP"; // exclude failures that weren't executed.
}
// v3 : failed tests with error label should be Error
if (rawCase["@_label"] == "Error") {
testCase.status = "ERROR";
}
let errorDetails = rawCase.reason ?? rawCase.failure;
if (errorDetails !== undefined) {
testCase.setFailure(errorDetails.message);
if (errorDetails["stack-trace"]) {
testCase.stack_trace = errorDetails["stack-trace"]
}
}
// copy parent_meta data to test case
mergeMeta(parent_meta, testCase.meta_data);
populateMetaData(rawCase, testCase.meta_data);

cases.push( testCase );
}
}

return cases;
}

function getTestSuites(rawSuites, assembly_meta) {
var suites = [];

for(let i = 0; i < rawSuites.length; i++) {
let rawSuite = rawSuites[i];

if (rawSuite["@_type"] == "Assembly") {
assembly_meta = new Map();
populateMetaData(rawSuite, assembly_meta);
}

if (hasNestedSuite(rawSuite)) {
// handle nested test-suites
suites.push(...getTestSuites(getNestedSuite(rawSuite), assembly_meta));
} else if (SUITE_TYPES_WITH_TESTCASES.indexOf(rawSuite["@_type"]) !== -1) {

let suite = new TestSuite();
suite.id = rawSuite["@_id"] ?? '';
suite.name = rawSuite["@_fullname"] ?? rawSuite["@_name"];
suite.duration = rawSuite["@_time"] * 1000; // in milliseconds
suite.status = RESULTMAP[rawSuite["@_result"]];

var meta_data = new Map();
mergeMeta(assembly_meta, meta_data);
populateMetaData(rawSuite, meta_data);
suite.cases.push(...getTestCases(rawSuite, meta_data));

// calculate totals
suite.total = suite.cases.length;
suite.passed = suite.cases.filter(i => i.status == "PASS").length;
suite.failed = suite.cases.filter(i => i.status == "FAIL").length;
suite.errors = suite.cases.filter(i => i.status == "ERROR").length;
suite.skipped = suite.cases.filter(i => i.status == "SKIP").length;

suites.push(suite);
}
}

return suites;
}


function getTestResult(json) {
const nunitVersion = (json["test-results"] !== undefined) ? "v2" :
(json["test-run"] !== undefined) ? "v3" : null;

if (nunitVersion == null) {
throw new Error("Unrecognized xml format");
}

const result = new TestResult();
const rawResult = json["test-results"] ?? json["test-run"];
const rawSuite = rawResult["test-suite"][0];

result.name = rawResult["@_fullname"] ?? rawResult["@_name"];
result.duration = rawSuite["@_time"] * 1000; // in milliseconds

result.suites.push(...getTestSuites( [ rawSuite ], null));

result.total = result.suites.reduce( (total, suite) => { return total + suite.cases.length}, 0);
result.passed = result.suites.reduce( (total, suite) => { return total + suite.passed}, 0);
result.failed = result.suites.reduce( (total, suite) => { return total + suite.failed}, 0);
result.skipped = result.suites.reduce( (total, suite) => { return total + suite.skipped}, 0);
result.errors = result.suites.reduce( (total, suite) => { return total + suite.errors}, 0);

return result;
}

function parse(file) {
const json = getJsonFromXMLFile(file);
return getTestResult(json);
}

module.exports = {
parse
}
Loading

0 comments on commit 67f113f

Please sign in to comment.