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

Introduce MSTest Support #44

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
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"editor.tabSize": 2
}
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Parse test results from JUnit, TestNG, xUnit, Mocha(json), Cucumber(json) and ma
| TestNG | ✅ |
| JUnit | ✅ |
| NUnit (v2 & v3) | ✅ |
| MSTest | ✅ |
| xUnit | ✅ |
| Mocha (json & mochawesome) | ✅ |
| Cucumber | ✅ |
| Cucumber | ✅ |
6 changes: 5 additions & 1 deletion src/helpers/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ 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",
"TestRun.Results.UnitTestResult",
"TestRun.TestDefinitions.UnitTest",
"TestRun.TestDefinitions.UnitTest.TestCategory.TestCategoryItem",
"TestRun.TestDefinitions.UnitTest.Properties.Property"
];

const configured_parser = new XMLParser({
Expand Down
3 changes: 3 additions & 0 deletions src/parsers/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const testng = require('./testng');
const junit = require('./junit');
const nunit = require('./nunit');
const mstest = require('./mstest');
const xunit = require('./xunit');
const mocha = require('./mocha');
const cucumber = require('./cucumber');
Expand Down Expand Up @@ -40,6 +41,8 @@ function getParser(type) {
return xunit;
case 'nunit':
return nunit;
case 'mstest':
return mstest;
case 'mocha':
return mocha;
case 'cucumber':
Expand Down
190 changes: 190 additions & 0 deletions src/parsers/mstest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
const { getJsonFromXMLFile } = require('../helpers/helper');

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

const RESULT_MAP = {
Passed: "PASS",
Failed: "FAIL",
NotExecuted: "SKIP",
}

function populateMetaData(rawElement, map) {
if (rawElement.TestCategory && rawElement.TestCategory.TestCategoryItem) {
let rawCategories = rawElement.TestCategory.TestCategoryItem;
for (let i = 0; i < rawCategories.length; i++) {
let categoryName = rawCategories[i]["@_TestCategory"];
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);
}
}
}

// as per https://github.com/microsoft/vstest/issues/2480:
// - properties are supported by the XSD but are not included in TRX Visual Studio output
// - including support for properties because third-party extensions might generate this data
if (rawElement.Properties) {
let rawProperties = rawElement.Properties.Property;
for (let i = 0; i < rawProperties.length; i++) {
let key = rawProperties[i].Key ?? "not-set";
let val = rawProperties[i].Value ?? "";
map.set(key, val);
}
}
}

function getTestResultDuration(rawTestResult) {
// durations are represented in a timeformat with 7 digit microsecond precision
// TODO: introduce d3-time-format after https://github.com/test-results-reporter/parser/issues/42 is fixed.
return 0;
}

function getTestCaseName(rawDefinition) {
if (rawDefinition.TestMethod) {
let className = rawDefinition.TestMethod["@_className"];
let name = rawDefinition.TestMethod["@_name"];

// attempt to produce fully-qualified name
if (className) {
className = className.split(",")[0]; // handle strong-name scenario (typeName, assembly, culture, version)
return className.concat(".", name);
} else {
return name;
}
} else {
throw new Error("Unrecognized TestDefinition");
}
}

function getTestSuiteName(testCase) {
// assume testCase.name is full-qualified namespace.classname.methodname
let index = testCase.name.lastIndexOf(".");
return testCase.name.substring(0, index);
}

function getTestCase(rawTestResult, definitionMap) {
let id = rawTestResult["@_testId"];

if (definitionMap.has(id)) {
var rawDefinition = definitionMap.get(id);

var testCase = new TestCase();
testCase.id = id;
testCase.name = getTestCaseName(rawDefinition);
testCase.status = RESULT_MAP[rawTestResult["@_outcome"]];
testCase.duration = getTestResultDuration(rawTestResult);

// collect error messages
if (rawTestResult.Output && rawTestResult.Output.ErrorInfo) {
testCase.setFailure(rawTestResult.Output.ErrorInfo.Message);
testCase.stack_trace = rawTestResult.Output.ErrorInfo.StackTrace ?? '';
}
// populate meta
populateMetaData(rawDefinition, testCase.meta_data);

return testCase;
} else {
throw new Error(`Unrecognized testId ${id ?? ''}`);
}
}

function getTestDefinitionsMap(rawTestDefinitions) {
let map = new Map();

// assume all definitions are 'UnitTest' elements
if (rawTestDefinitions.UnitTest) {
let rawUnitTests = rawTestDefinitions.UnitTest;
for (let i = 0; i < rawUnitTests.length; i++) {
let rawUnitTest = rawUnitTests[i];
let id = rawUnitTest["@_id"];
if (id) {
map.set(id, rawUnitTest);
}
}
}

return map;
}

function getTestResults(rawTestResults) {
let results = [];

// assume all results are UnitTestResult elements
if (rawTestResults.UnitTestResult) {
let unitTests = rawTestResults.UnitTestResult;
for (let i = 0; i < unitTests.length; i++) {
results.push(unitTests[i]);
}
}
return results;
}

function getTestSuites(rawTestRun) {
// outcomes + durations are stored in /TestRun/TestResults/*
const testResults = getTestResults(rawTestRun.Results);
// test names and details are stored in /TestRun/TestDefinitions/*
const testDefinitions = getTestDefinitionsMap(rawTestRun.TestDefinitions);

// trx does not include suites, so we'll reverse engineer them by
// grouping results from the same className
let suiteMap = new Map();

for (let i = 0; i < testResults.length; i++) {
let rawTestResult = testResults[i];
let testCase = getTestCase(rawTestResult, testDefinitions);
let suiteName = getTestSuiteName(testCase);

if (!suiteMap.has(suiteName)) {
let suite = new TestSuite();
suite.name = suiteName;
suiteMap.set(suiteName, suite);
}
suiteMap.get(suiteName).cases.push(testCase);
}

var result = [];
for (let suite of suiteMap.values()) {
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.skipped = suite.cases.filter(i => i.status == "SKIP").length;
suite.errors = suite.cases.filter(i => i.status == "ERROR").length;
suite.duration = suite.cases.reduce((total, test) => { return total + test.duration }, 0);
result.push(suite);
}

return result;
}

function getTestResult(json) {
const rawTestRun = json.TestRun;

let result = new TestResult();
result.id = rawTestRun["@_id"];
result.suites.push(...getTestSuites(rawTestRun));

// calculate totals
result.total = result.suites.reduce((total, suite) => { return total + suite.total }, 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);
result.duration = result.suites.reduce((total, suite) => { return total + suite.duration }, 0);

return result;
}

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

module.exports = {
parse
}
Loading
Loading