diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c4e3e9..78a6174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +### Fixed +- Reporting of feature (suite) for parallel execution [#142](https://github.com/reportportal/agent-js-cucumber/issues/142). ## [5.3.1] - 2024-04-30 ### Security diff --git a/modules/cucumber-reportportal-formatter.js b/modules/cucumber-reportportal-formatter.js index 42a0610..8918e0e 100644 --- a/modules/cucumber-reportportal-formatter.js +++ b/modules/cucumber-reportportal-formatter.js @@ -86,7 +86,7 @@ const createRPFormatterClass = (config) => } onGherkinDocumentEvent(data) { - this.storage.setDocument(data); + this.storage.setFeature(data.uri, data.feature); this.storage.setAstNodesData(data, utils.findAstNodesData(data.feature.children)); } @@ -146,6 +146,33 @@ const createRPFormatterClass = (config) => this.storage.setSteps(testCaseId, stepsMap); } + startFeature({ pickleFeatureUri, feature }) { + if (this.storage.getFeatureTempId(pickleFeatureUri)) return; + + const launchTempId = this.storage.getLaunchTempId(); + const suiteData = { + name: `${feature.keyword}: ${feature.name}`, + startTime: this.reportportal.helpers.now(), + type: this.isScenarioBasedStatistics ? TEST_ITEM_TYPES.TEST : TEST_ITEM_TYPES.SUITE, + description: (feature.description || '').trim(), + attributes: utils.createAttributes(feature.tags), + codeRef: utils.formatCodeRef(pickleFeatureUri, feature.name), + }; + + const { tempId } = this.reportportal.startTestItem(suiteData, launchTempId); + this.storage.updateFeature(pickleFeatureUri, { tempId }); + } + + finishFeature(pickleFeatureUri) { + const { tempId, endTime } = this.storage.getFeature(pickleFeatureUri); + + this.reportportal.finishTestItem(tempId, { + endTime: endTime || this.reportportal.helpers.now(), + }); + + this.storage.deleteFeature(pickleFeatureUri); + } + onTestCaseStartedEvent(data) { const { id, testCaseId, attempt } = data; this.storage.setTestCaseStartedId(id, testCaseId); @@ -155,36 +182,10 @@ const createRPFormatterClass = (config) => uri: pickleFeatureUri, astNodeIds: [scenarioId, parametersId], } = this.storage.getPickle(pickleId); - const currentFeatureUri = this.storage.getCurrentFeatureUri(); const feature = this.storage.getFeature(pickleFeatureUri); const launchTempId = this.storage.getLaunchTempId(); - const isNeedToStartFeature = currentFeatureUri !== pickleFeatureUri; - - // start FEATURE if no currentFeatureUri or new feature - // else finish old one const featureCodeRef = utils.formatCodeRef(pickleFeatureUri, feature.name); - if (isNeedToStartFeature) { - const isFirstFeatureInLaunch = currentFeatureUri === null; - const suiteData = { - name: `${feature.keyword}: ${feature.name}`, - startTime: this.reportportal.helpers.now(), - type: this.isScenarioBasedStatistics ? TEST_ITEM_TYPES.TEST : TEST_ITEM_TYPES.SUITE, - description: (feature.description || '').trim(), - attributes: utils.createAttributes(feature.tags), - codeRef: featureCodeRef, - }; - - if (!isFirstFeatureInLaunch) { - const previousFeatureTempId = this.storage.getFeatureTempId(); - this.reportportal.finishTestItem(previousFeatureTempId, { - endTime: this.reportportal.helpers.now(), - }); - } - - this.storage.setCurrentFeatureUri(pickleFeatureUri); - const { tempId } = this.reportportal.startTestItem(suiteData, launchTempId, ''); - this.storage.setFeatureTempId(tempId); - } + this.startFeature({ pickleFeatureUri, feature }); // current feature node rule(this entity is for grouping several // scenarios in one logical block) || scenario @@ -209,7 +210,7 @@ const createRPFormatterClass = (config) => attributes: utils.createAttributes(tags), codeRef: currentNodeCodeRef, }; - const parentId = this.storage.getFeatureTempId(); + const parentId = this.storage.getFeatureTempId(pickleFeatureUri); const { tempId } = this.reportportal.startTestItem(testData, launchTempId, parentId); ruleTempId = tempId; @@ -273,7 +274,7 @@ const createRPFormatterClass = (config) => }); testData.parameters = this.storage.getParameters(parametersId); } - const parentId = ruleTempId || this.storage.getFeatureTempId(); + const parentId = ruleTempId || this.storage.getFeatureTempId(pickleFeatureUri); const { tempId } = this.reportportal.startTestItem(testData, launchTempId, parentId); this.storage.setScenarioTempId(testCaseId, tempId); this.storage.updateTestCase(testCaseId, { @@ -289,7 +290,7 @@ const createRPFormatterClass = (config) => // start step if (step) { - const currentFeatureUri = this.storage.getCurrentFeatureUri(); + const currentFeatureUri = (this.storage.getPickle(testCase.pickleId) || {}).uri; const astNodesData = this.storage.getAstNodesData(currentFeatureUri); const { text: stepName, type, astNodeIds } = step; @@ -423,6 +424,7 @@ const createRPFormatterClass = (config) => onTestStepFinishedEvent(data) { const { testCaseStartedId, testStepId, testStepResult } = data; const testCaseId = this.storage.getTestCaseId(testCaseStartedId); + const testCase = this.storage.getTestCase(testCaseId); const step = this.storage.getStep(testCaseId, testStepId); const tempStepId = this.storage.getStepTempId(testStepId); let status; @@ -476,7 +478,7 @@ const createRPFormatterClass = (config) => this.config.takeScreenshot && this.config.takeScreenshot === 'onFailure'; if (isBrowserAvailable && isTakeScreenshotOptionProvidedInRPConfig) { - const currentFeatureUri = this.storage.getCurrentFeatureUri(); + const currentFeatureUri = (this.storage.getPickle(testCase.pickleId) || {}).uri; const astNodesData = this.storage.getAstNodesData(currentFeatureUri); const screenshotName = utils.getScreenshotName(astNodesData, step.astNodeIds); @@ -575,12 +577,15 @@ const createRPFormatterClass = (config) => this.storage.removeTestCase(testCaseId); this.storage.removeScenarioTempId(testCaseStartedId); } + + const { uri: pickleFeatureUri } = this.storage.getPickle(testCase.pickleId); + this.storage.updateFeature(pickleFeatureUri, { endTime: this.reportportal.helpers.now() }); } onTestRunFinishedEvent() { - const featureTempId = this.storage.getFeatureTempId(); - this.reportportal.finishTestItem(featureTempId, { - endTime: this.reportportal.helpers.now(), + const featureUris = this.storage.getActiveFeatureUris(); + featureUris.forEach((featureUri) => { + this.finishFeature(featureUri); }); const launchId = this.storage.getLaunchTempId(); @@ -589,8 +594,6 @@ const createRPFormatterClass = (config) => ...(this.customLaunchStatus && { status: this.customLaunchStatus }), }); this.storage.setLaunchTempId(null); - this.storage.setCurrentFeatureUri(null); - this.storage.setFeatureTempId(null); this.customLaunchStatus = null; }); } diff --git a/modules/storage.js b/modules/storage.js index a0217e1..5c96d94 100644 --- a/modules/storage.js +++ b/modules/storage.js @@ -17,9 +17,7 @@ module.exports = class Storage { constructor() { this.launchTempId = null; - this.currentFeatureUri = null; - this.featureTempId = null; - this.documents = new Map(); + this.features = new Map(); this.pickles = new Map(); this.hooks = new Map(); this.testCases = new Map(); @@ -43,28 +41,6 @@ module.exports = class Storage { return this.launchTempId; } - getCurrentFeatureUri() { - return this.currentFeatureUri; - } - - setCurrentFeatureUri(value) { - this.currentFeatureUri = value; - } - - setDocument(gherkinDocument) { - this.documents.set(gherkinDocument.uri, gherkinDocument); - } - - getDocument(uri) { - return this.documents.get(uri); - } - - getFeature(uri) { - const document = this.getDocument(uri); - - return document && document.feature; - } - setPickle(pickle) { this.pickles.set(pickle.id, pickle); } @@ -139,12 +115,30 @@ module.exports = class Storage { return this.parameters.get(id); } - setFeatureTempId(value) { - this.featureTempId = value; + updateFeature(id, newData) { + const feature = this.features.get(id) || {}; + this.features.set(id, { ...feature, ...newData }); + } + + getFeature(id) { + return this.features.get(id); + } + + setFeature(id, feature) { + this.features.set(id, feature); + } + + deleteFeature(id) { + this.features.delete(id); + } + + getFeatureTempId(id) { + const feature = this.features.get(id); + return feature && feature.tempId; } - getFeatureTempId() { - return this.featureTempId; + getActiveFeatureUris() { + return Array.from(this.features.keys()); } setScenarioTempId(testCaseStartedId, scenarioTempId) { diff --git a/tests/cucumber-reportportal-formatter.spec.js b/tests/cucumber-reportportal-formatter.spec.js index 90648cf..1274af8 100644 --- a/tests/cucumber-reportportal-formatter.spec.js +++ b/tests/cucumber-reportportal-formatter.spec.js @@ -23,8 +23,6 @@ const { pickle, pickleWithParameters, testCaseWithParameters, - ruleId, - ruleTempId, hook, testCase, testCaseStarted, @@ -38,6 +36,8 @@ const { feature, scenario, step, + launchTempId, + featureTempId, } = require('./data'); const { STATUSES, @@ -179,8 +179,8 @@ describe('cucumber-reportportal-formatter', () => { beforeEach(() => { formatter.onGherkinDocumentEvent(gherkinDocument); }); - it('should set document to storage', () => { - expect(formatter.storage.getDocument(uri)).toBe(gherkinDocument); + it('should set feature to storage', () => { + expect(formatter.storage.getFeature(uri)).toBe(gherkinDocument.feature); }); it('should set document feature.children', () => { @@ -272,46 +272,6 @@ describe('cucumber-reportportal-formatter', () => { expect(formatter.storage.getTestCaseId(testCaseStarted.id)).toBe(testCase.id); }); - it('should start FEATURE if no currentFeatureUri or new feature', () => { - formatter.onTestCaseStartedEvent(testCaseStarted); - - expect(formatter.storage.getFeatureTempId()).toBe('testItemId'); - }); - - it('should finish previous FEATURE if currentFeatureUri is different from pickleFeatureUri', () => { - const finishTestItem = jest.spyOn(formatter.reportportal, 'finishTestItem'); - const tempFeatureId = 'tempFeatureId'; - formatter.storage.setFeatureTempId(tempFeatureId); - formatter.storage.setCurrentFeatureUri('currentFeatureUri'); - - formatter.onTestCaseStartedEvent(testCaseStarted); - - expect(finishTestItem).toHaveBeenCalledWith(tempFeatureId, { - endTime: mockedDate, - }); - }); - - it('should not finish FEATURE if pickleFeatureUri the same as currentFeatureUrl', () => { - const finishTestItem = jest.spyOn(formatter.reportportal, 'finishTestItem'); - const tempFeatureId = 'tempFeatureId'; - formatter.storage.setFeatureTempId(tempFeatureId); - formatter.storage.setCurrentFeatureUri(pickle.uri); - - formatter.onTestCaseStartedEvent(testCaseStarted); - - expect(finishTestItem).not.toHaveBeenCalled(); - }); - - it('should start new FEATURE if it is first feature in this launch', () => { - const finishTestItem = jest.spyOn(formatter.reportportal, 'finishTestItem'); - const tempFeatureId = 'tempFeatureId'; - formatter.storage.setFeatureTempId(tempFeatureId); - - formatter.onTestCaseStartedEvent(testCaseStarted); - - expect(finishTestItem).not.toHaveBeenCalled(); - }); - it('start scenario flow', () => { formatter.onTestCaseStartedEvent(testCaseStarted); @@ -832,6 +792,36 @@ describe('cucumber-reportportal-formatter', () => { }); }); + describe('startFeature', () => { + it('startTestItem should be called', () => { + const spyStartTestItem = jest.spyOn(formatter.reportportal, 'startTestItem'); + formatter.storage.setLaunchTempId(launchTempId); + + formatter.startFeature({ pickleFeatureUri: uri, feature }); + + expect(spyStartTestItem).lastCalledWith( + { + name: `Feature: ${feature.name}`, + startTime: mockedDate, + type: 'SUITE', + codeRef: `${uri}/${feature.name}`, + attributes: [], + description: '', + }, + launchTempId, + ); + }); + + it('should be skipped if suite is already running', () => { + const spyStartTestItem = jest.spyOn(formatter.reportportal, 'startTestItem'); + formatter.storage.setFeature(uri, { tempId: featureTempId }); + + formatter.startFeature({ pickleFeatureUri: uri, feature }); + + expect(spyStartTestItem).not.toHaveBeenCalled(); + }); + }); + describe('onTestRunFinishedEvent', () => { beforeEach(() => { formatter.onGherkinDocumentEvent(gherkinDocument); @@ -859,8 +849,8 @@ describe('cucumber-reportportal-formatter', () => { expect(spyGetPromiseFinishAllItems).toBeCalledWith('tempLaunchId'); expect(formatter.storage.getLaunchTempId()).toBeNull(); - expect(formatter.storage.getCurrentFeatureUri()).toBeNull(); - expect(formatter.storage.getFeatureTempId()).toBeNull(); + expect(formatter.storage.getActiveFeatureUris().length).toBe(0); + expect(formatter.storage.getFeatureTempId(uri)).toBeUndefined(); }); }); }); diff --git a/tests/storage.spec.js b/tests/storage.spec.js index 3095f03..67a7833 100644 --- a/tests/storage.spec.js +++ b/tests/storage.spec.js @@ -50,22 +50,17 @@ describe('test Storage', () => { expect(storage.getLaunchTempId()).toBe(launchTempId); }); - it('set/getCurrentFeatureUri', () => { - storage.setCurrentFeatureUri(uri); - - expect(storage.getCurrentFeatureUri()).toBe(uri); - }); - - it('set/getDocument', () => { - storage.setDocument(gherkinDocument); + it('set/getFeature', () => { + storage.setFeature(uri, feature); - expect(storage.getDocument(uri)).toEqual(gherkinDocument); + expect(storage.getFeature(uri)).toEqual(feature); }); - it('set/getFeature', () => { - storage.setDocument(gherkinDocument); + it('set/deleteFeature', () => { + storage.setFeature(uri, feature); + storage.deleteFeature(uri); - expect(storage.getFeature(uri)).toEqual(feature); + expect(storage.getFeature(uri)).toBeUndefined(); }); it('set/getPickle', () => { @@ -141,9 +136,15 @@ describe('test Storage', () => { }); it('set/getFeatureTempId', () => { - storage.setFeatureTempId(featureTempId); + storage.setFeature(uri, { tempId: featureTempId }); + + expect(storage.getFeatureTempId(uri)).toBe(featureTempId); + }); + + it('set/getActiveFeatureUris', () => { + storage.setFeature(uri, feature); - expect(storage.getFeatureTempId()).toBe(featureTempId); + expect(storage.getActiveFeatureUris()).toEqual([uri]); }); it('set/getScenarioTempId', () => {