From af9fb392f88b02f3257e00438b0244437449b683 Mon Sep 17 00:00:00 2001 From: Anudeep Date: Sun, 28 Jul 2024 08:48:11 +0530 Subject: [PATCH] chore: cucumber refactor and capture steps (#77) * chore: cucumber refactor and capture steps * fix: duration --- package-lock.json | 4 +- package.json | 2 +- src/parsers/base.parser.js | 48 +++++++ src/parsers/cucumber.js | 234 ++++++++++++++++++---------------- tests/parser.cucumber.spec.js | 194 ++++++++++++++++++++-------- 5 files changed, 319 insertions(+), 163 deletions(-) create mode 100644 src/parsers/base.parser.js diff --git a/package-lock.json b/package-lock.json index 50d0757..274afb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "test-results-parser", - "version": "0.1.19", + "version": "0.1.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "test-results-parser", - "version": "0.1.19", + "version": "0.1.20", "license": "MIT", "dependencies": { "fast-xml-parser": "^4.3.2", diff --git a/package.json b/package.json index e6692a4..9f46daa 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/parsers/base.parser.js b/src/parsers/base.parser.js new file mode 100644 index 0000000..37901e6 --- /dev/null +++ b/src/parsers/base.parser.js @@ -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 } \ No newline at end of file diff --git a/src/parsers/cucumber.js b/src/parsers/cucumber.js index 878a1b2..8709eca 100644 --- a/src/parsers/cucumber.js +++ b/src/parsers/cucumber.js @@ -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 -} +} \ No newline at end of file diff --git a/tests/parser.cucumber.spec.js b/tests/parser.cucumber.spec.js index 50767a6..2ac383c 100644 --- a/tests/parser.cucumber.spec.js +++ b/tests/parser.cucumber.spec.js @@ -43,14 +43,39 @@ describe('Parser - Cucumber Json', () => { failure: "", id: "", name: "Addition of two numbers", - passed: 0, + passed: 3, skipped: 0, stack_trace: "", status: "PASS", - tags: ["@green", "@fast"], - metadata: { testCase: "1234" }, - steps: [], - total: 0 + tags: ["@green", "@fast", "@blue", "@slow"], + metadata: { "suite": "1234", testCase: "1234" }, + steps: [ + { + "id": "", + "name": "I have number 6 in calculator", + "duration": 1.21, + "status": "PASS", + "failure": "", + "stack_trace": "" + }, + { + "id": "", + "name": "I entered number 7", + "duration": 0.14, + "status": "PASS", + "failure": "", + "stack_trace": "" + }, + { + "id": "", + "name": "I should see result 13", + "duration": 0.24, + "status": "PASS", + "failure": "", + "stack_trace": "" + } + ], + total: 3 } ] } @@ -80,97 +105,172 @@ describe('Parser - Cucumber Json', () => { it('multiple suites', () => { const result = parse({ type: 'cucumber', files: [`${testDataPath}/multiple-suites-multiple-tests.json`] }); assert.deepEqual(result, { - id: '', - name: '', + id: "", + name: "", total: 3, passed: 2, failed: 1, errors: 0, skipped: 0, retried: 0, - duration: 3.36, - status: 'FAIL', + duration: 3.37, + status: "FAIL", tags: [], metadata: {}, suites: [ { - id: '', - name: 'Addition', + id: "", + name: "Addition", total: 2, passed: 1, failed: 1, errors: 0, skipped: 0, - duration: 2.84, - status: 'FAIL', + duration: 2.85, + status: "FAIL", tags: [], metadata: {}, cases: [ { - attachments: [], - duration: 2.56, - errors: 0, - failed: 0, - failure: "AssertionError [ERR_ASSERTION]: 13 == 14\n + expected - actual\n\n -13\n +14\n\n", id: "", name: "Addition of two numbers", - passed: 0, + total: 3, + passed: 2, + failed: 1, + errors: 0, skipped: 0, - stack_trace: " at CustomWorld. (D:\\workspace\\nodejs\\cc-tests\\features\\support\\steps.js:18:12)", + duration: 2.56, status: "FAIL", + failure: "AssertionError [ERR_ASSERTION]: 13 == 14\n + expected - actual\n\n -13\n +14\n\n", + stack_trace: " at CustomWorld. (D:\\workspace\\nodejs\\cc-tests\\features\\support\\steps.js:18:12)", tags: [], metadata: {}, - steps: [], - total: 0 + steps: [ + { + id: "", + name: "I have number 6 in calculator", + duration: 1.1, + status: "PASS", + failure: "", + stack_trace: "" + }, + { + id: "", + name: "I add number 7", + duration: 0.13, + status: "PASS", + failure: "", + stack_trace: "" + }, + { + id: "", + name: "I should see result 14", + duration: 1.33, + status: "FAIL", + failure: "AssertionError [ERR_ASSERTION]: 13 == 14\n + expected - actual\n\n -13\n +14\n\n", + stack_trace: " at CustomWorld. (D:\\workspace\\nodejs\\cc-tests\\features\\support\\steps.js:18:12)" + } + ], + attachments: [] }, { - attachments: [], - duration: 0.28, - errors: 0, - failed: 0, - failure: "", id: "", name: "Addition of two numbers v2", - passed: 0, + total: 3, + passed: 3, + failed: 0, + errors: 0, skipped: 0, - stack_trace: "", + duration: 0.29, status: "PASS", + failure: "", + stack_trace: "", tags: [], metadata: {}, - steps: [], - total: 0 + steps: [ + { + id: "", + name: "I have number 6 in calculator", + duration: 0.11, + status: "PASS", + failure: "", + stack_trace: "" + }, + { + id: "", + name: "I add number 7", + duration: 0.1, + status: "PASS", + failure: "", + stack_trace: "" + }, + { + id: "", + name: "I should see result 13", + duration: 0.08, + status: "PASS", + failure: "", + stack_trace: "" + } + ], + attachments: [] } ] }, { - id: '', - name: 'Subtraction', + id: "", + name: "Subtraction", total: 1, passed: 1, failed: 0, errors: 0, skipped: 0, duration: 0.52, - status: 'PASS', + status: "PASS", tags: [], metadata: {}, cases: [ { - attachments: [], - duration: 0.52, - errors: 0, - failed: 0, - failure: "", id: "", name: "Subtraction of two numbers", - passed: 0, + total: 3, + passed: 3, + failed: 0, + errors: 0, skipped: 0, - stack_trace: "", + duration: 0.52, status: "PASS", + failure: "", + stack_trace: "", tags: [], metadata: {}, - steps: [], - total: 0 + steps: [ + { + id: "", + name: "I have number 10 in calculator", + duration: 0.08, + status: "PASS", + failure: "", + stack_trace: "" + }, + { + id: "", + name: "I subtract number 7", + duration: 0.13, + status: "PASS", + failure: "", + stack_trace: "" + }, + { + id: "", + name: "I should see result 3", + duration: 0.31, + status: "PASS", + failure: "", + stack_trace: "" + } + ], + attachments: [] } ] } @@ -187,12 +287,4 @@ describe('Parser - Cucumber Json', () => { assert.notEqual(null, result2); }); - function newMap(obj) { - let map = new Map(); - for (const property in obj) { - map.set(property, obj[property]); - } - return map; - } }); -