Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support NUnit XML format #38

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading