Skip to content

Commit

Permalink
Add metadata to TestCase (#35)
Browse files Browse the repository at this point in the history
* add meta-data to testcase #34

* xUnit implementation + tests

* mocha-test-explorer extension config support

* junit implementation

* cucumber implementation

* mocha implementation

* testng implementation

* strip tag indicator character from mocha + cucumber.

add support for key/value tags
  • Loading branch information
bryanbcook authored Nov 11, 2023
1 parent 8a8863e commit e3c18be
Show file tree
Hide file tree
Showing 21 changed files with 446 additions and 15 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,8 @@
"devDependencies": {
"c8": "^7.12.0",
"mocha": "^10.0.0"
},
"mocha": {
"spec": "tests/**/*.spec.js"
}
}
4 changes: 4 additions & 0 deletions src/helpers/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ const FORCED_ARRAY_KEYS = [
"testsuites.testsuite",
"testsuites.testsuite.testcase",
"testsuites.testsuite.testcase.failure",
"testsuites.testsuite.testcase.properties.property",
"assemblies",
"assemblies.assembly",
"assemblies.assembly.collection",
"assemblies.assembly.collection.test",
"assemblies.assembly.collection.test.failure",
"assemblies.assembly.collection.test.traits.trait",
"testng-results",
"testng-results.suite",
"testng-results.suite.groups.group",
"testng-results.suite.groups.group.method",
"testng-results.suite.test",
"testng-results.suite.test.class",
"testng-results.suite.test.class.test-method",
Expand Down
1 change: 1 addition & 0 deletions src/models/TestCase.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ declare class TestCase {
failure: string;
stack_trace: string;
steps: TestStep[];
meta_data: Map<string,string>;
}

declare namespace TestCase { }
Expand Down
1 change: 1 addition & 0 deletions src/models/TestCase.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class TestCase {
this.failure = '';
this.stack_trace = '';
this.steps = [];
this.meta_data = new Map();
}

}
Expand Down
15 changes: 15 additions & 0 deletions src/parsers/cucumber.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ function getTestCase(rawCase) {
const test_case = new TestCase();
test_case.name = rawCase["name"];
test_case.duration = rawCase["duration"];
if (rawCase.tags && rawCase.tags.length > 0) {
const tagsArray = rawCase.tags;
let tags = [];
let rawTags = [];
for (let i = 0; i < tagsArray.length; i++) {
let rawTagName = tagsArray[i]["name"];
let tag = rawTagName.substring(1).split("=");
let tagName = tag[0];
test_case.meta_data.set(tagName, tag[1] ?? "")
tags.push(tagName);
rawTags.push(rawTagName);
}
test_case.meta_data.set("tags", tags.join(","));
test_case.meta_data.set("tagsRaw", rawTags.join(","));
}
if (rawCase.state && rawCase.state === "failed") {
test_case.status = 'FAIL';
test_case.failure = rawCase.errorStack;
Expand Down
20 changes: 18 additions & 2 deletions src/parsers/junit.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@ const TestResult = require('../models/TestResult');
const TestSuite = require('../models/TestSuite');
const TestCase = require('../models/TestCase');

function getTestCase(rawCase) {
function getTestCase(rawCase, suiteProperties) {
const test_case = new TestCase();
test_case.name = rawCase["@_name"];
test_case.duration = rawCase["@_time"] * 1000;
for (let [key,value] of suiteProperties) {
test_case.meta_data.set(key, value);
}
if (rawCase.properties && rawCase.properties.property.length > 0) {
const raw_properties = rawCase.properties.property;
for (let i = 0; i < raw_properties.length; i++) {
test_case.meta_data.set(raw_properties[i]["@_name"], raw_properties[i]["@_value"]);
}
}
if (rawCase.failure && rawCase.failure.length > 0) {
test_case.status = 'FAIL';
test_case.failure = rawCase.failure[0]["@_message"];
Expand All @@ -34,10 +43,17 @@ function getTestSuite(rawSuite) {
suite.passed = suite.total - suite.failed - suite.errors;
suite.duration = rawSuite["@_time"] * 1000;
suite.status = suite.total === suite.passed ? 'PASS' : 'FAIL';
const properties = new Map();
if (rawSuite.properties && rawSuite.properties.property.length > 0) {
const raw_properties = rawSuite.properties.property;
for (let i = 0; i < raw_properties.length; i++) {
properties.set(raw_properties[i]["@_name"], raw_properties[i]["@_value"])
}
}
const raw_test_cases = rawSuite.testcase;
if (raw_test_cases) {
for (let i = 0; i < raw_test_cases.length; i++) {
suite.cases.push(getTestCase(raw_test_cases[i]));
suite.cases.push(getTestCase(raw_test_cases[i], properties));
}
}
return suite;
Expand Down
16 changes: 16 additions & 0 deletions src/parsers/mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ function getTestCase(rawCase) {
const test_case = new TestCase();
test_case.name = rawCase["title"];
test_case.duration = rawCase["duration"];
const regexp = /([\@\#][^\s]*)/gm; // match @tag or #tag
let matches = [...test_case.name.matchAll(regexp)];
if (matches.length > 0) {
let tags = [];
let rawTags = [];
for (let match of matches) {
let rawTag = match[0];
let tag = rawTag.substring(1).split("=");
let tagName = tag[0];
test_case.meta_data.set(tagName, tag[1] ?? "");
tags.push(tagName);
rawTags.push(rawTag);
}
test_case.meta_data.set("tags", tags.join(","));
test_case.meta_data.set("tagsRaw", rawTags.join(","));
}
if (rawCase["state"] == "pending") {
test_case.status = 'SKIP';
}
Expand Down
59 changes: 52 additions & 7 deletions src/parsers/testng.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,46 @@ const TestResult = require('../models/TestResult');
const TestSuite = require('../models/TestSuite');
const TestCase = require('../models/TestCase');

function getTestCase(rawCase) {
// assemble a fully qualified test name (class.name)
function getFullTestName(raw) {
return "".concat(raw["@_class"], ".", raw["@_name"]);
}

// create a mapping between fully qualified test name and and group
function getSuiteGroups(rawSuite) {
let testCaseToGroupMap = new Map();

if (rawSuite.groups && rawSuite.groups.group.length > 0) {
let raw_groups = rawSuite.groups.group;
for (let i = 0; i < raw_groups.length; i++) {
let group_methods = raw_groups[i].method;
let groupName = raw_groups[i]["@_name"];
for (let j = 0; j < group_methods.length; j++) {
let method = group_methods[j];
let key = getFullTestName(method);
if (!testCaseToGroupMap.has(key)) {
testCaseToGroupMap.set(key, []);
}
testCaseToGroupMap.get(key).push(groupName);
}
}
}
return testCaseToGroupMap;
}

function getTestCase(rawCase, testCaseToGroupMap) {
const test_case = new TestCase();
test_case.name = rawCase["@_name"];
test_case.duration = rawCase["@_duration-ms"];
test_case.status = rawCase["@_status"];
const key = getFullTestName(rawCase);
if (testCaseToGroupMap.has(key)) {
let groups = testCaseToGroupMap.get(key);
test_case.meta_data.set("groups", groups.join(","));
groups.forEach(group => {
test_case.meta_data.set(group, "");
})
}
if (rawCase.exception) {
test_case.failure = rawCase.exception[0].message;
}
Expand All @@ -18,14 +53,18 @@ function getTestCase(rawCase) {
return test_case;
}

function getTestSuiteFromTest(rawTest) {
function getTestSuiteFromTest(rawTest, testCaseToGroupMap) {
const suite = new TestSuite();
suite.name = rawTest['@_name'];
suite.duration = rawTest['@_duration-ms'];
const rawTestMethods = [];
const rawClasses = rawTest.class;
for (let i = 0; i < rawClasses.length; i++) {
rawTestMethods.push(...rawClasses[i]['test-method'].filter(raw => !raw['@_is-config']));
let testMethods = rawClasses[i]['test-method'].filter(raw => !raw['@_is-config']);
testMethods.forEach(testMethod => {
testMethod["@_class"] = rawClasses[i]["@_name"]; // push className onto test-method
});
rawTestMethods.push(...testMethods);
}
suite.total = rawTestMethods.length;
suite.passed = rawTestMethods.filter(test => test['@_status'] === 'PASS').length;
Expand All @@ -38,7 +77,7 @@ function getTestSuiteFromTest(rawTest) {
}
suite.status = suite.total === suite.passed ? 'PASS' : 'FAIL';
for (let i = 0; i < rawTestMethods.length; i++) {
suite.cases.push(getTestCase(rawTestMethods[i]));
suite.cases.push(getTestCase(rawTestMethods[i], testCaseToGroupMap));
}
return suite;
}
Expand All @@ -49,11 +88,16 @@ function getTestSuite(rawSuite) {
suite.duration = rawSuite['@_duration-ms'];
const rawTests = rawSuite.test;
const rawTestMethods = [];
const testCaseToGroupMap = getSuiteGroups(rawSuite);
for (let i = 0; i < rawTests.length; i++) {
const rawTest = rawTests[i];
const rawClasses = rawTest.class;
for (let j = 0; j < rawClasses.length; j++) {
rawTestMethods.push(...rawClasses[j]['test-method'].filter(raw => !raw['@_is-config']));
let testMethods = rawClasses[i]['test-method'].filter(raw => !raw['@_is-config']);
testMethods.forEach(testMethod => {
testMethod["@_class"] = rawClasses[i]["@_name"]; // push className onto test-method
});
rawTestMethods.push(...testMethods);
}
}
suite.total = rawTestMethods.length;
Expand All @@ -67,7 +111,7 @@ function getTestSuite(rawSuite) {
}
suite.status = suite.total === suite.passed ? 'PASS' : 'FAIL';
for (let i = 0; i < rawTestMethods.length; i++) {
suite.cases.push(getTestCase(rawTestMethods[i]));
suite.cases.push(getTestCase(rawTestMethods[i], testCaseToGroupMap));
}
return suite;
}
Expand Down Expand Up @@ -107,12 +151,13 @@ function parse(file) {
}
} else if (suitesWithTests.length === 1) {
const suite = suitesWithTests[0];
const testCaseToGroupMap = getSuiteGroups(suite);
result.name = suite['@_name'];
result.duration = suite['@_duration-ms'];
const rawTests = suite.test;
const rawTestsWithClasses = rawTests.filter(_rawTest => _rawTest.class);
for (let i = 0; i < rawTestsWithClasses.length; i++) {
result.suites.push(getTestSuiteFromTest(rawTestsWithClasses[i]));
result.suites.push(getTestSuiteFromTest(rawTestsWithClasses[i], testCaseToGroupMap));
}
} else if (suitesWithTests.length === 0){
const suite = suites[0];
Expand Down
7 changes: 7 additions & 0 deletions src/parsers/xunit.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ function getTestCase(rawCase) {
else {
test_case.status = 'PASS';
}
if(rawCase.traits && rawCase.traits.trait && rawCase.traits.trait.length > 0) {
const traits = rawCase.traits.trait;
for(let i = 0; i < traits.length; i++) {
test_case.meta_data.set( traits[i]["@_name"], traits[i]["@_value"]);
}
}

return test_case;
}

Expand Down
8 changes: 8 additions & 0 deletions tests/data/cucumber/single-suite-single-test.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@
{
"name": "@green",
"line": 4
},
{
"name": "@fast",
"line": 4
},
{
"name": "@testCase=1234",
"line": 4
}
],
"type": "scenario"
Expand Down
24 changes: 24 additions & 0 deletions tests/data/junit/multiple-suites-properties.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<testsuites id="id" name="result name" tests="2" failures="1" errors="" time="20">
<testsuite id="codereview.cobol.analysisProvider" name="suite name 1" tests="1" failures="1" time="10">
<properties>
<property name="key1" value="value1" />
<property name="key2" value="value2" />
</properties>
<testcase id="codereview.cobol.rules.ProgramIdRule" name="Use a program name that matches the source file name" time="10">
<properties>
<property name="key1" value="override-value1" />
</properties>
<failure message="PROGRAM.cbl:2 Use a program name that matches the source file name" type="WARNING">
Some Text
</failure>
</testcase>
</testsuite>
<testsuite id="codereview.cobol.analysisProvider" name="suite name 2" tests="1" failures="0" time="10">
<testcase id="codereview.cobol.rules.ProgramIdRule" name="Use a program name that matches the source file name" time="10">
<failure message="PROGRAM.cbl:2 Use a program name that matches the source file name" type="WARNING">
Some Text
</failure>
</testcase>
</testsuite>
</testsuites>
85 changes: 85 additions & 0 deletions tests/data/mocha/json/multiple-suites-multiple-tests-tags.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{
"stats": {
"suites": 2,
"tests": 3,
"passes": 2,
"pending": 0,
"failures": 1,
"start": "2022-06-11T05:28:11.204Z",
"end": "2022-06-11T05:28:11.227Z",
"duration": 7
},
"tests": [
{
"title": "sample test case @fast #1255",
"fullTitle": "Example Suite 1 sample test case @fast #1255",
"file": "",
"duration": 3,
"currentRetry": 0,
"speed": "fast",
"err": {}
},
{
"title": "sample test case 2",
"fullTitle": "Example Suite 1 sample test case 2",
"file": "",
"duration": 1,
"currentRetry": 0,
"speed": "fast",
"err": {}
},
{
"title": "sample test case #1234",
"fullTitle": "Example Suite 2 sample test case #1234",
"file": "",
"duration": 1,
"currentRetry": 0,
"err": {
"stack": "AssertionError [ERR_ASSERTION]: Dummy reason\n at Context.<anonymous> (tests\\parser.mocha.json.spec.js:7:12)\n at processImmediate (node:internal/timers:466:21)",
"message": "Dummy reason",
"generatedMessage": false,
"name": "AssertionError",
"code": "ERR_ASSERTION",
"operator": "fail"
}
}
],
"pending": [],
"failures": [
{
"title": "sample test case #1234",
"fullTitle": "Example Suite 2 sample test case #1234",
"file": "",
"duration": 1,
"currentRetry": 0,
"err": {
"stack": "AssertionError [ERR_ASSERTION]: Dummy reason\n at Context.<anonymous> (tests\\parser.mocha.json.spec.js:7:12)\n at processImmediate (node:internal/timers:466:21)",
"message": "Dummy reason",
"generatedMessage": false,
"name": "AssertionError",
"code": "ERR_ASSERTION",
"operator": "fail"
}
}
],
"passes": [
{
"title": "sample test case @fast #1255",
"fullTitle": "Example Suite 1 sample test case @fast #1255",
"file": "",
"duration": 3,
"currentRetry": 0,
"speed": "fast",
"err": {}
},
{
"title": "sample test case 2",
"fullTitle": "Example Suite 1 sample test case 2",
"file": "",
"duration": 1,
"currentRetry": 0,
"speed": "fast",
"err": {}
}
]
}
Loading

0 comments on commit e3c18be

Please sign in to comment.