Skip to content

Commit 67f113f

Browse files
authored
Add support NUnit XML format (#38)
* implementation for nunit #4 * reorganized tests to support v2 + v3 * Refactored to support NUnit v2+v3 * updated readme
1 parent a44248f commit 67f113f

File tree

7 files changed

+813
-1
lines changed

7 files changed

+813
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Parse test results from JUnit, TestNG, xUnit, Mocha(json), Cucumber(json) and ma
88
|-------------------------------|---------|
99
| TestNG ||
1010
| JUnit ||
11+
| NUnit (v2 & v3) ||
1112
| xUnit ||
1213
| Mocha (json & mochawesome) ||
1314
| Cucumber ||

src/helpers/helper.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,26 @@ const FORCED_ARRAY_KEYS = [
3131
"testng-results.suite.test",
3232
"testng-results.suite.test.class",
3333
"testng-results.suite.test.class.test-method",
34-
"testng-results.suite.test.class.test-method.exception",
34+
"testng-results.suite.test.class.test-method.exception"
3535
];
3636

3737
const configured_parser = new XMLParser({
3838
isArray: (name, jpath, isLeafNode, isAttribute) => {
3939
if( FORCED_ARRAY_KEYS.indexOf(jpath) !== -1) {
4040
return true;
41+
}
42+
// handle nunit deep hierarchy
43+
else if (jpath.startsWith("test-results") || jpath.startsWith("test-run")) {
44+
let parts = jpath.split(".");
45+
switch(parts[parts.length - 1]) {
46+
case "category":
47+
case "property":
48+
case "test-suite":
49+
case "test-case":
50+
return true;
51+
default:
52+
return false;
53+
}
4154
}
4255
},
4356
ignoreAttributes: false,

src/parsers/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const testng = require('./testng');
22
const junit = require('./junit');
3+
const nunit = require('./nunit');
34
const xunit = require('./xunit');
45
const mocha = require('./mocha');
56
const cucumber = require('./cucumber');
@@ -37,6 +38,8 @@ function getParser(type) {
3738
return junit;
3839
case 'xunit':
3940
return xunit;
41+
case 'nunit':
42+
return nunit;
4043
case 'mocha':
4144
return mocha;
4245
case 'cucumber':

src/parsers/nunit.js

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
const { getJsonFromXMLFile } = require('../helpers/helper');
2+
3+
const TestResult = require('../models/TestResult');
4+
const TestSuite = require('../models/TestSuite');
5+
const TestCase = require('../models/TestCase');
6+
7+
const SUITE_TYPES_WITH_TESTCASES = [
8+
"TestFixture",
9+
"ParameterizedTest",
10+
"GenericFixture",
11+
"ParameterizedMethod" // v3
12+
]
13+
14+
const RESULTMAP = {
15+
Success: "PASS", // v2
16+
Failure: "FAIL", // v2
17+
Ignored: "SKIP", // v2
18+
NotRunnable: "SKIP", // v2
19+
Error: "ERROR", // v2
20+
Inconclusive: "FAIL", // v2
21+
22+
Passed: "PASS", // v3
23+
Failed: "FAIL", // v3
24+
Skipped: "SKIP", // v3
25+
}
26+
27+
function mergeMeta(map1, map2) {
28+
for(let kvp of map1) {
29+
map2.set(kvp[0], kvp[1]);
30+
}
31+
}
32+
33+
function populateMetaData(raw, map) {
34+
35+
// v2 supports categories
36+
if (raw.categories) {
37+
let categories = raw.categories.category;
38+
for (let i = 0; i < categories.length; i++) {
39+
let categoryName = categories[i]["@_name"];
40+
map.set(categoryName, "");
41+
42+
// create comma-delimited list of categories
43+
if (map.has("Categories")) {
44+
map.set("Categories", map.get("Categories").concat(",", categoryName));
45+
} else {
46+
map.set("Categories", categoryName);
47+
}
48+
}
49+
}
50+
51+
// v2/v3 support properties
52+
if (raw.properties) {
53+
let properties = raw.properties.property;
54+
for (let i = 0; i < properties.length; i++) {
55+
let property = properties[i];
56+
let propName = property["@_name"];
57+
let propValue = property["@_value"];
58+
59+
// v3 treats 'Categories' as property "Category"
60+
if (propName == "Category") {
61+
62+
if (map.has("Categories")) {
63+
map.set("Categories", map.get("Categories").concat(",", propValue));
64+
} else {
65+
map.set("Categories", propValue);
66+
}
67+
map.set(propValue, "");
68+
69+
} else {
70+
map.set(propName, propValue);
71+
}
72+
}
73+
}
74+
}
75+
76+
function getNestedTestCases(rawSuite) {
77+
if (rawSuite.results) {
78+
return rawSuite.results["test-case"];
79+
} else {
80+
return rawSuite["test-case"];
81+
}
82+
}
83+
84+
function hasNestedSuite(rawSuite) {
85+
return getNestedSuite(rawSuite) !== null;
86+
}
87+
88+
function getNestedSuite(rawSuite) {
89+
// nunit v2 nests test-suite inside 'results'
90+
if (rawSuite.results && rawSuite.results["test-suite"]) {
91+
return rawSuite.results["test-suite"];
92+
} else {
93+
// nunit v3 nests test-suites as immediate children
94+
if (rawSuite["test-suite"]) {
95+
return rawSuite["test-suite"];
96+
}
97+
else {
98+
// not nested
99+
return null;
100+
}
101+
}
102+
}
103+
104+
function getTestCases(rawSuite, parent_meta) {
105+
var cases = [];
106+
107+
let rawTestCases = getNestedTestCases(rawSuite);
108+
if (rawTestCases) {
109+
for (let i = 0; i < rawTestCases.length; i++) {
110+
let rawCase = rawTestCases[i];
111+
let testCase = new TestCase();
112+
let result = rawCase["@_result"]
113+
testCase.id = rawCase["@_id"] ?? "";
114+
testCase.name = rawCase["@_fullname"] ?? rawCase["@_name"];
115+
testCase.duration = rawCase["@_time"] * 1000; // in milliseconds
116+
testCase.status = RESULTMAP[result];
117+
118+
// v2 : non-executed should be tests should be Ignored
119+
if (rawCase["@_executed"] == "False") {
120+
testCase.status = "SKIP"; // exclude failures that weren't executed.
121+
}
122+
// v3 : failed tests with error label should be Error
123+
if (rawCase["@_label"] == "Error") {
124+
testCase.status = "ERROR";
125+
}
126+
let errorDetails = rawCase.reason ?? rawCase.failure;
127+
if (errorDetails !== undefined) {
128+
testCase.setFailure(errorDetails.message);
129+
if (errorDetails["stack-trace"]) {
130+
testCase.stack_trace = errorDetails["stack-trace"]
131+
}
132+
}
133+
// copy parent_meta data to test case
134+
mergeMeta(parent_meta, testCase.meta_data);
135+
populateMetaData(rawCase, testCase.meta_data);
136+
137+
cases.push( testCase );
138+
}
139+
}
140+
141+
return cases;
142+
}
143+
144+
function getTestSuites(rawSuites, assembly_meta) {
145+
var suites = [];
146+
147+
for(let i = 0; i < rawSuites.length; i++) {
148+
let rawSuite = rawSuites[i];
149+
150+
if (rawSuite["@_type"] == "Assembly") {
151+
assembly_meta = new Map();
152+
populateMetaData(rawSuite, assembly_meta);
153+
}
154+
155+
if (hasNestedSuite(rawSuite)) {
156+
// handle nested test-suites
157+
suites.push(...getTestSuites(getNestedSuite(rawSuite), assembly_meta));
158+
} else if (SUITE_TYPES_WITH_TESTCASES.indexOf(rawSuite["@_type"]) !== -1) {
159+
160+
let suite = new TestSuite();
161+
suite.id = rawSuite["@_id"] ?? '';
162+
suite.name = rawSuite["@_fullname"] ?? rawSuite["@_name"];
163+
suite.duration = rawSuite["@_time"] * 1000; // in milliseconds
164+
suite.status = RESULTMAP[rawSuite["@_result"]];
165+
166+
var meta_data = new Map();
167+
mergeMeta(assembly_meta, meta_data);
168+
populateMetaData(rawSuite, meta_data);
169+
suite.cases.push(...getTestCases(rawSuite, meta_data));
170+
171+
// calculate totals
172+
suite.total = suite.cases.length;
173+
suite.passed = suite.cases.filter(i => i.status == "PASS").length;
174+
suite.failed = suite.cases.filter(i => i.status == "FAIL").length;
175+
suite.errors = suite.cases.filter(i => i.status == "ERROR").length;
176+
suite.skipped = suite.cases.filter(i => i.status == "SKIP").length;
177+
178+
suites.push(suite);
179+
}
180+
}
181+
182+
return suites;
183+
}
184+
185+
186+
function getTestResult(json) {
187+
const nunitVersion = (json["test-results"] !== undefined) ? "v2" :
188+
(json["test-run"] !== undefined) ? "v3" : null;
189+
190+
if (nunitVersion == null) {
191+
throw new Error("Unrecognized xml format");
192+
}
193+
194+
const result = new TestResult();
195+
const rawResult = json["test-results"] ?? json["test-run"];
196+
const rawSuite = rawResult["test-suite"][0];
197+
198+
result.name = rawResult["@_fullname"] ?? rawResult["@_name"];
199+
result.duration = rawSuite["@_time"] * 1000; // in milliseconds
200+
201+
result.suites.push(...getTestSuites( [ rawSuite ], null));
202+
203+
result.total = result.suites.reduce( (total, suite) => { return total + suite.cases.length}, 0);
204+
result.passed = result.suites.reduce( (total, suite) => { return total + suite.passed}, 0);
205+
result.failed = result.suites.reduce( (total, suite) => { return total + suite.failed}, 0);
206+
result.skipped = result.suites.reduce( (total, suite) => { return total + suite.skipped}, 0);
207+
result.errors = result.suites.reduce( (total, suite) => { return total + suite.errors}, 0);
208+
209+
return result;
210+
}
211+
212+
function parse(file) {
213+
const json = getJsonFromXMLFile(file);
214+
return getTestResult(json);
215+
}
216+
217+
module.exports = {
218+
parse
219+
}

0 commit comments

Comments
 (0)