Skip to content

Commit

Permalink
chore: cucumber refactor and capture steps (#77)
Browse files Browse the repository at this point in the history
* chore: cucumber refactor and capture steps

* fix: duration
  • Loading branch information
ASaiAnudeep authored Jul 28, 2024
1 parent 5f5d98c commit af9fb39
Show file tree
Hide file tree
Showing 5 changed files with 319 additions and 163 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "test-results-parser",
"version": "0.1.19",
"version": "0.1.20",
"description": "Parse test results from JUnit, TestNG, xUnit, cucumber and many more",
"main": "src/index.js",
"types": "./src/index.d.ts",
Expand Down
48 changes: 48 additions & 0 deletions src/parsers/base.parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const { unescape } = require('html-escaper');


class BaseParser {

/**
* @param {string} value
* @returns
*/
parseText(value) {
return value ? unescape(value) : value;
}

/**
*
* @param {string[]} parent_tags
* @param {string[]} child_tags
*/
mergeTags(parent_tags, child_tags) {
if (!parent_tags) {
parent_tags = [];
}
if (!child_tags) {
child_tags = [];
}
for (const tag of parent_tags) {
if (child_tags.indexOf(tag) === -1) {
child_tags.push(tag);
}
}
}

mergeMetadata(parent_metadata, child_metadata) {
if (!parent_metadata) {
parent_metadata = {};
}
if (!child_metadata) {
child_metadata = {};
}
for (const [key, value] of Object.entries(parent_metadata)) {
if (!child_metadata[key]) {
child_metadata[key] = value;
}
}
}
}

module.exports = { BaseParser }
234 changes: 125 additions & 109 deletions src/parsers/cucumber.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,139 +3,155 @@ const { resolveFilePath } = require('../helpers/helper');
const TestResult = require('../models/TestResult');
const TestSuite = require('../models/TestSuite');
const TestCase = require('../models/TestCase');
const TestStep = require('../models/TestStep');
const { BaseParser } = require('./base.parser');

function getTestCase(rawCase) {
const test_case = new TestCase();
test_case.name = rawCase["name"];
test_case.duration = rawCase["duration"];
setMetaData(rawCase, test_case);
if (rawCase.state && rawCase.state === "failed") {
test_case.status = 'FAIL';
setErrorAndStackTrace(test_case, rawCase.errorStack);
class CucumberParser extends BaseParser {

constructor(file) {
super();
this.result = new TestResult();
this.raw_result = this.#getCucumberResult(file);
}
else {
test_case.status = 'PASS';

/**
* @returns {import('./cucumber.result').CucumberJsonResult}
*/
#getCucumberResult(file) {
return require(resolveFilePath(file));
}
return test_case;
}

/**
* @param {TestCase} test_case
* @param {string?} message
*/
function setErrorAndStackTrace(test_case, message) {
if (message) {
const stack_trace_start_index = message.indexOf(' at ');
if (stack_trace_start_index) {
const failure = message.slice(0, stack_trace_start_index);
const stack_trace = message.slice(stack_trace_start_index);
test_case.setFailure(failure);
test_case.stack_trace = stack_trace;
} else {
test_case.setFailure(message);
}
parse() {
this.#setTestResults();
return this.result;
}
}

/**
*
* @param {import('./cucumber.result').CucumberElement} element
* @param {TestCase | TestSuite} test_element
*/
function setMetaData(element, test_element) {
const tags = element.tags;
if (tags && tags.length > 0) {
for (const tag of tags) {
if (tag["name"].includes("=")) {
const [name, value] = tag["name"].substring(1).split("=");
test_element.metadata[name] = value;
} else {
test_element.tags.push(tag["name"]);
#setTestResults() {
this.result.name = '';
this.#setTestSuites();
this.result.status = this.result.suites.every(suite => suite.status === "PASS") ? "PASS" : "FAIL";
this.result.total = this.result.suites.reduce((total, suite) => total + suite.total, 0);
this.result.passed = this.result.suites.reduce((total, suite) => total + suite.passed, 0);
this.result.failed = this.result.suites.reduce((total, suite) => total + suite.failed, 0);
this.result.duration = this.result.suites.reduce((total, suite) => total + suite.duration, 0);
this.result.duration = parseFloat(this.result.duration.toFixed(2));
}

#setTestSuites() {
for (const feature of this.raw_result) {
const test_suite = new TestSuite();
test_suite.name = feature.name;
test_suite.total = feature.elements.length;
for (const scenario of feature.elements) {
test_suite.cases.push(this.#getTestCase(scenario));
}
test_suite.total = test_suite.cases.length;
test_suite.passed = test_suite.cases.filter(_ => _.status === "PASS").length;
test_suite.failed = test_suite.cases.filter(_ => _.status === "FAIL").length;
test_suite.duration = test_suite.cases.reduce((total, _) => total + _.duration, 0);
test_suite.duration = parseFloat(test_suite.duration.toFixed(2));
test_suite.status = test_suite.total === test_suite.passed ? 'PASS' : 'FAIL';
const { tags, metadata } = this.#getTagsAndMetadata(feature.tags);
test_suite.tags = tags;
test_suite.metadata = metadata;
for (const test_case of test_suite.cases) {
this.mergeTags(test_suite.tags, test_case.tags);
this.mergeMetadata(test_suite.metadata, test_case.metadata);
}

this.result.suites.push(test_suite);
}
}
}

function getTestSuite(rawSuite) {
const suite = new TestSuite();
suite.name = rawSuite["name"];
suite.total = rawSuite["tests"];
suite.passed = rawSuite["passes"];
suite.failed = rawSuite["failures"];
suite.duration = rawSuite["duration"];
suite.status = suite.total === suite.passed ? 'PASS' : 'FAIL';
setMetaData(rawSuite, suite);
const raw_test_cases = rawSuite.elements;
if (raw_test_cases) {
for (let i = 0; i < raw_test_cases.length; i++) {
suite.cases.push(getTestCase(raw_test_cases[i]));
/**
*
* @param {import('./cucumber.result').CucumberElement} scenario
*/
#getTestCase(scenario) {
const test_case = new TestCase();
test_case.name = scenario.name;
for (const step of scenario.steps) {
test_case.steps.push(this.#getTestStep(step));
}
test_case.total = test_case.steps.length;
test_case.passed = test_case.steps.filter(step => step.status === "PASS").length;
test_case.failed = test_case.steps.filter(step => step.status === "FAIL").length;
test_case.duration = test_case.steps.reduce((total, _) => total + _.duration, 0);
test_case.duration = parseFloat((test_case.duration).toFixed(2));
test_case.status = test_case.total === test_case.passed ? 'PASS' : 'FAIL';
if (test_case.status === "FAIL") {
const failed_step = test_case.steps.find(step => step.status === "FAIL");
test_case.failure = failed_step.failure;
test_case.stack_trace = failed_step.stack_trace
}
const { tags, metadata } = this.#getTagsAndMetadata(scenario.tags);
test_case.tags = tags;
test_case.metadata = metadata;
return test_case;
}
return suite;
}

/**
* @param {import("./cucumber.result").CucumberJsonResult} json
*/
function getTestResult(json) {
const result = new TestResult();
const { stats, suites } = preprocess(json);
result.name = suites["name"] || "";
result.total = stats["tests"];
result.passed = stats["passes"];
result.failed = stats["failures"];
const errors = stats["errors"];
if (errors) {
result.errors = errors;
/**
*
* @param {import('./cucumber.result').CucumberStep} step
*/
#getTestStep(step) {
const test_step = new TestStep();
test_step.name = step.name;
test_step.status = step.result.status === "passed" ? "PASS" : "FAIL";
test_step.duration = step.result.duration ? parseFloat((step.result.duration / 1000000).toFixed(2)) : 0;
if (test_step.status === "FAIL") {
const { failure, stack_trace } = this.#getFailureAndStackTrace(step.result.error_message);
test_step.failure = failure;
test_step.stack_trace = stack_trace;
}
return test_step;
}
result.duration = stats["duration"] || 0;

if (suites.length > 0) {
for (let i = 0; i < suites.length; i++) {
result.suites.push(getTestSuite(suites[i]));
/**
*
* @param {string} message
*/
#getFailureAndStackTrace(message) {
if (message) {
const stack_trace_start_index = message.indexOf(' at ');
if (stack_trace_start_index) {
const failure = this.parseText(message.slice(0, stack_trace_start_index));
const stack_trace = message.slice(stack_trace_start_index);
return { failure, stack_trace };
} else {
return { failure: message, stack_trace: '' };
}
}
return { failure: '', stack_trace: '' };
}
result.status = result.total === result.passed ? 'PASS' : 'FAIL';
return result;
}

/**
* Function to format the raw json report
* @param {import("./cucumber.result").CucumberJsonResult} json
* @returns formatted json object
*/
function preprocess(json) {
const formattedResult = { stats: {}, suites: [] };

json.forEach(testSuite => {
testSuite.elements.forEach(testCase => {
testCase.state = testCase.steps.every(step => step.result.status === "passed") ? "passed" : "failed";
testCase.duration = testCase.steps.map(step => step.result.duration).reduce((total, currVal) => total + currVal, 0) / 1000000;
testCase.duration = parseFloat(testCase.duration.toFixed(2));
testCase.errorStack = testCase.steps.filter(step => step.result.status === "failed").map(step => step.result.error_message)[0] || "";
})
testSuite.tests = testSuite.elements.length;

if (testSuite.tests) {
testSuite.failures = testSuite.elements.filter(testCase => testCase.state === "failed").length;
testSuite.passes = testSuite.elements.filter(testCase => testCase.state === "passed").length;
testSuite.duration = testSuite.elements.map(testCase => testCase.duration).reduce((total, currVal) => total + currVal, 0);
/**
*
* @param {import('./cucumber.result').CucumberTag[]} cucumber_tags
*/
#getTagsAndMetadata(cucumber_tags) {
const metadata = {};
const tags = [];
if (cucumber_tags) {
for (const tag of cucumber_tags) {
if (tag["name"].includes("=")) {
const [name, value] = tag["name"].substring(1).split("=");
metadata[name] = value;
} else {
tags.push(tag["name"]);
}
}
}
formattedResult.suites.push(testSuite);
});

formattedResult.stats.suites = formattedResult.suites.length;
for (const statsType of ["tests", "passes", "failures", "errors", "duration"]) {
formattedResult.stats[statsType] = formattedResult.suites.map(testSuite => testSuite[statsType]).reduce((total, currVal) => total + currVal, 0) || 0;
return { tags, metadata };
}
return formattedResult;

}

function parse(file) {
const json = require(resolveFilePath(file));
return getTestResult(json);
const parser = new CucumberParser(file);
return parser.parse();
}

module.exports = {
parse
}
}
Loading

0 comments on commit af9fb39

Please sign in to comment.