From 8af114383af639be9af1916a7723ab09869a0823 Mon Sep 17 00:00:00 2001 From: Oleksandr Shevtsov Date: Thu, 8 Jun 2023 22:15:40 +0300 Subject: [PATCH] feat: provide option to overwrite historyId hash with own function, address #209 --- README.md | 29 ++++++++++ reporter/allure-cypress/AllureInterface.js | 7 +++ reporter/allure-cypress/AllureReporter.js | 3 +- reporter/index.d.ts | 5 ++ reporter/index.js | 3 +- reporter/stubbedAllure.js | 1 + writer.js | 6 ++- writer/clearEmptyHookSteps.js | 2 +- writer/customTestName.js | 8 ++- writer/handleMultiDomain.js | 1 + writer/results.js | 63 +++++++++++++++++----- 11 files changed, 109 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 36aa85b..83459e0 100644 --- a/README.md +++ b/README.md @@ -370,6 +370,35 @@ It will be used for: In lower versions some other heuristics would be used, but they are not as reliable as `after:spec`. +## Test name duplicates + +By default Allure calculates hash from test title to identify test and show its' proper previous results. +This may lead to tests having the same name being counted by allure as retries of the same test. +There are several ways to avoid this situation: + +- the best way to avoid it is basically using unique test names + +- update specific test name + ```js + cy.allure().testName('new_test_name') + ``` + +- specify your own function for all tests to not only take test.title, but also concatenate it with some other information in `cypress/support/index` or `cypress/support/e2e.js` file, for example: + - use relative spec file path like "cypress/e2e/results2/test.cy.js" and test title: + ```js + Cypress.Allure.reporter.getInterface().defineHistoryId((title) => { + return `${Cypress.spec.relative}${title}`; + }); + ``` + - use browser name and test title: + ```js + Cypress.Allure.reporter.getInterface().defineHistoryId((title) => { + return `${Cypress.browser.name}${title}`; + }); + ``` + + The rule is that this function should return any string (folder name, project name, platform, browser name, Cypress.spec content, etc.), and if those strings will be different - their test historyId hashes will be different - tests will be recognized as different by allure. + ## Suite structuring Allure support 3 levels of suite structure: diff --git a/reporter/allure-cypress/AllureInterface.js b/reporter/allure-cypress/AllureInterface.js index c0c9d8e..7200891 100644 --- a/reporter/allure-cypress/AllureInterface.js +++ b/reporter/allure-cypress/AllureInterface.js @@ -124,6 +124,13 @@ Allure.prototype.defineSuiteLabels = function (defineSuiteLabelsFn) { this.reporter.defineSuiteLabelsFn = defineSuiteLabelsFn; }; +Allure.prototype.defineHistoryId = function (defineHistoryId) { + if (!defineHistoryId) { + return; + } + this.reporter.defineHistoryId = defineHistoryId; +}; + Allure.prototype.label = function (name, value) { if (this.reporter.currentTest && !this.reporter.currentHook) { const labelIndex = (name) => diff --git a/reporter/allure-cypress/AllureReporter.js b/reporter/allure-cypress/AllureReporter.js index 69abc82..5df0ad3 100644 --- a/reporter/allure-cypress/AllureReporter.js +++ b/reporter/allure-cypress/AllureReporter.js @@ -27,6 +27,7 @@ module.exports = class AllureReporter { this.gherkin = new CucumberHandler(this); this.config = options; this.defineSuiteLabelsFn = (titles) => titles; + this.defineHistoryId = (testTitle) => testTitle; } /** @@ -221,7 +222,7 @@ module.exports = class AllureReporter { } this.currentTest.info.historyId = crypto - .MD5(test.title) + .MD5(this.defineHistoryId(test.title)) .toString(crypto.enc.Hex); this.currentTest.info.stage = Stage.RUNNING; this.addPackageLabel(); diff --git a/reporter/index.d.ts b/reporter/index.d.ts index b00b5b1..9ef084f 100644 --- a/reporter/index.d.ts +++ b/reporter/index.d.ts @@ -258,6 +258,11 @@ declare global { fileInfo: SuiteLabelFunctionFileInfo ) => string[] ): void; + + /** + * Specify string which will be used to calculate historyId for test + */ + defineHistoryId(fn: (testTitle: string) => string): void; } } } diff --git a/reporter/index.js b/reporter/index.js index a6f8974..f43700a 100644 --- a/reporter/index.js +++ b/reporter/index.js @@ -81,7 +81,8 @@ const invokeResultsWriter = (allure, isGlobal) => { files: allure.reporter.files || [], mapping: allure.reporter.mochaIdToAllure, clearSkipped: config.clearSkipped(), - isGlobal + isGlobal, + defineHistoryId: allure.reporter.defineHistoryId }, { log: false } ).catch((e) => diff --git a/reporter/stubbedAllure.js b/reporter/stubbedAllure.js index 5cba2ae..1d8d46f 100644 --- a/reporter/stubbedAllure.js +++ b/reporter/stubbedAllure.js @@ -43,5 +43,6 @@ const stubbedAllure = { testParameter: () => {}, testName: () => {}, defineSuiteLabels: () => {}, + defineHistoryId: () => {}, logCommandSteps: () => {} }; diff --git a/writer.js b/writer.js index 97a42a0..c8bd32e 100644 --- a/writer.js +++ b/writer.js @@ -59,7 +59,8 @@ function allureWriter(on, config) { files, mapping, clearSkipped, - isGlobal + isGlobal, + defineHistoryId }) => { const { resultsDir: relativeResultsDir, writer } = results; @@ -76,7 +77,8 @@ function allureWriter(on, config) { clearSkipped, writer, allureMapping, - isGlobal + isGlobal, + defineHistoryId }); return null; diff --git a/writer/clearEmptyHookSteps.js b/writer/clearEmptyHookSteps.js index accda1c..855d5eb 100644 --- a/writer/clearEmptyHookSteps.js +++ b/writer/clearEmptyHookSteps.js @@ -1,7 +1,7 @@ const logger = require('../reporter/debug'); const clearEmptyHookSteps = (test) => { - if (!test.steps.length) { + if (!test || !test.steps || !test.steps.length) { return test; } diff --git a/writer/customTestName.js b/writer/customTestName.js index dacfa53..02bd8d1 100644 --- a/writer/customTestName.js +++ b/writer/customTestName.js @@ -1,7 +1,11 @@ const crypto = require('crypto-js'); const logger = require('../reporter/debug'); -const overwriteTestNameMaybe = (test) => { +const defaultHistoryId = (title) => title; + +const overwriteTestNameMaybe = (test, defineHistoryId) => { + const historyIdFn = defineHistoryId || defaultHistoryId; + const overrideIndex = test.parameters.findIndex( (p) => p.name === 'OverwriteTestName' ); @@ -10,7 +14,7 @@ const overwriteTestNameMaybe = (test) => { logger.writer('overwriting test "%s" name to "%s"', test.name, name); test.name = name; test.fullName = name; - test.historyId = crypto.MD5(name).toString(crypto.enc.Hex); + test.historyId = crypto.MD5(historyIdFn(name)).toString(crypto.enc.Hex); test.parameters.splice(overrideIndex, 1); } return test; diff --git a/writer/handleMultiDomain.js b/writer/handleMultiDomain.js index ffdad30..608a525 100644 --- a/writer/handleMultiDomain.js +++ b/writer/handleMultiDomain.js @@ -71,6 +71,7 @@ const sanitizeSuites = (folder, files = [], isGlobal = false) => { (file) => file.historyId === child.historyId && file.uuid !== child.uuid && + file.steps && file.steps.length ); diff --git a/writer/results.js b/writer/results.js index 87f9cb0..3d096e9 100644 --- a/writer/results.js +++ b/writer/results.js @@ -119,7 +119,13 @@ const writeSuites = ({ groups, resultsDir, tests, clearSkipped }) => { }); }; -const writeTests = ({ tests, resultsDir, clearSkipped, allureMapping }) => { +const writeTests = ({ + tests, + resultsDir, + clearSkipped, + allureMapping, + defineHistoryId +}) => { if (!tests || !tests.length) { return; } @@ -140,7 +146,7 @@ const writeTests = ({ tests, resultsDir, clearSkipped, allureMapping }) => { const fileName = `${test.uuid}-result.json`; logger.writer('write test "%s" to file "%s"', test.name, fileName); const testResultPath = path.join(resultsDir, fileName); - const updatedTest = overwriteTestNameMaybe(test); + const updatedTest = overwriteTestNameMaybe(test, defineHistoryId); const testResult = clearEmptyHookSteps(updatedTest); fs.writeFileSync(testResultPath, JSON.stringify(testResult)); }); @@ -172,7 +178,10 @@ const catchError = (fn, ...args) => { try { fn(...args); } catch (e) { - process.stdout.write(`error while writing allure results: ${e}`); + const entity = args[args.length - 1]; + process.stdout.write( + `error while writing allure results for ${entity}: ${e}` + ); logger.writer('failed to write allure results: %O', e); } }; @@ -183,7 +192,8 @@ const writeResultFiles = ({ clearSkipped, writer, allureMapping, - isGlobal + isGlobal, + defineHistoryId }) => { !fs.existsSync(resultsDir) && fs.mkdirSync(resultsDir, { recursive: true }); @@ -192,20 +202,49 @@ const writeResultFiles = ({ const { groups, tests, attachments, envInfo, categories, executorInfo } = writer; - catchError(writeAttachmentFiles, { files, resultsDir, tests }); - catchError(writeSuites, { groups, resultsDir, tests, clearSkipped }); - catchError(writeTests, { tests, resultsDir, clearSkipped, allureMapping }); - catchError(writeAttachments, { attachments, resultsDir }); - catchError(handleAfterTestWrites, { resultsDir, isGlobal }); + catchError(writeAttachmentFiles, { files, resultsDir, tests }, 'files'); + catchError( + writeSuites, + { groups, resultsDir, tests, clearSkipped }, + 'suites' + ); + catchError( + writeTests, + { + tests, + resultsDir, + clearSkipped, + allureMapping, + defineHistoryId + }, + 'tests' + ); + catchError(writeAttachments, { attachments, resultsDir }, 'attachments'); + catchError( + handleAfterTestWrites, + { resultsDir, isGlobal }, + 'after test writes' + ); const allureResultsPath = (file) => path.join(resultsDir, file); - catchError(writeInfoFile, allureResultsPath('categories.json'), categories); - catchError(writeInfoFile, allureResultsPath('executor.json'), executorInfo); + catchError( + writeInfoFile, + allureResultsPath('categories.json'), + categories, + 'cetegories file' + ); + catchError( + writeInfoFile, + allureResultsPath('executor.json'), + executorInfo, + 'executor file' + ); catchError( writeEnvProperties, allureResultsPath('environment.properties'), - envInfo + envInfo, + 'env file' ); logger.writer('finished writing allure results'); };