UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

1,044 lines (900 loc) 36.7 kB
'use strict' const { createCoverageMap } = require('../../../vendor/dist/istanbul-lib-coverage') const shimmer = require('../../datadog-shimmer') const log = require('../../dd-trace/src/log') const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper') const { getCoveredFilenamesFromCoverage, resetCoverage, mergeCoverage, fromCoverageMapToCoverage, getTestSuitePath, CUCUMBER_WORKER_TRACE_PAYLOAD_CODE, getIsFaultyEarlyFlakeDetection } = require('../../dd-trace/src/plugins/util/test') const satisfies = require('../../../vendor/dist/semifies') const { addHook, channel } = require('./helpers/instrument') const testStartCh = channel('ci:cucumber:test:start') const testRetryCh = channel('ci:cucumber:test:retry') const testFinishCh = channel('ci:cucumber:test:finish') // used for test steps too const testFnCh = channel('ci:cucumber:test:fn') const testStepStartCh = channel('ci:cucumber:test-step:start') const errorCh = channel('ci:cucumber:error') const testSuiteStartCh = channel('ci:cucumber:test-suite:start') const testSuiteFinishCh = channel('ci:cucumber:test-suite:finish') const testSuiteCodeCoverageCh = channel('ci:cucumber:test-suite:code-coverage') const libraryConfigurationCh = channel('ci:cucumber:library-configuration') const knownTestsCh = channel('ci:cucumber:known-tests') const skippableSuitesCh = channel('ci:cucumber:test-suite:skippable') const sessionStartCh = channel('ci:cucumber:session:start') const sessionFinishCh = channel('ci:cucumber:session:finish') const testManagementTestsCh = channel('ci:cucumber:test-management-tests') const modifiedFilesCh = channel('ci:cucumber:modified-files') const isModifiedCh = channel('ci:cucumber:is-modified-test') const workerReportTraceCh = channel('ci:cucumber:worker-report:trace') const itrSkippedSuitesCh = channel('ci:cucumber:itr:skipped-suites') const getCodeCoverageCh = channel('ci:nyc:get-coverage') const isMarkedAsUnskippable = (pickle) => { return pickle.tags.some(tag => tag.name === '@datadog:unskippable') } // We'll preserve the original coverage here const originalCoverageMap = createCoverageMap() // TODO: remove in a later major version const patched = new WeakSet() const lastStatusByPickleId = new Map() const numRetriesByPickleId = new Map() const numAttemptToCtx = new Map() const newTestsByTestFullname = new Map() const modifiedTestsByPickleId = new Map() let eventDataCollector = null let pickleByFile = {} const pickleResultByFile = {} let skippableSuites = [] let itrCorrelationId = '' let isForcedToRun = false let isUnskippable = false let isSuitesSkippingEnabled = false let isEarlyFlakeDetectionEnabled = false let earlyFlakeDetectionNumRetries = 0 let earlyFlakeDetectionFaultyThreshold = 0 let isEarlyFlakeDetectionFaulty = false let isFlakyTestRetriesEnabled = false let isKnownTestsEnabled = false let isTestManagementTestsEnabled = false let isImpactedTestsEnabled = false let testManagementAttemptToFixRetries = 0 let testManagementTests = {} let modifiedFiles = {} let numTestRetries = 0 let knownTests = {} let skippedSuites = [] let isSuitesSkipped = false function isValidKnownTests (receivedKnownTests) { return !!receivedKnownTests.cucumber } function getSuiteStatusFromTestStatuses (testStatuses) { if (testStatuses.includes('fail')) { return 'fail' } if (testStatuses.every(status => status === 'skip')) { return 'skip' } return 'pass' } function getStatusFromResult (result) { if (result.status === 1) { return { status: 'pass' } } if (result.status === 2) { return { status: 'skip' } } if (result.status === 4) { return { status: 'skip', skipReason: 'not implemented' } } return { status: 'fail', errorMessage: result.message } } function getStatusFromResultLatest (result) { if (result.status === 'PASSED') { return { status: 'pass' } } if (result.status === 'SKIPPED' || result.status === 'PENDING') { return { status: 'skip' } } if (result.status === 'UNDEFINED') { return { status: 'skip', skipReason: 'not implemented' } } return { status: 'fail', errorMessage: result.message } } function isNewTest (testSuite, testName) { if (!isValidKnownTests(knownTests)) { return false } const testsForSuite = knownTests.cucumber[testSuite] || [] return !testsForSuite.includes(testName) } function getTestProperties (testSuite, testName) { const { attempt_to_fix: attemptToFix, disabled, quarantined } = testManagementTests?.cucumber?.suites?.[testSuite]?.tests?.[testName]?.properties || {} return { attemptToFix, disabled, quarantined } } function getTestStatusFromRetries (testStatuses) { if (testStatuses.every(status => status === 'fail')) { return 'fail' } if (testStatuses.includes('pass')) { return 'pass' } return 'pass' } function getErrorFromCucumberResult (cucumberResult) { if (!cucumberResult.message) { return } const [message] = cucumberResult.message.split('\n') const error = new Error(message) if (cucumberResult.exception) { error.type = cucumberResult.exception.type } error.stack = cucumberResult.message return error } function getChannelPromise (channelToPublishTo, isParallel = false, frameworkVersion = null) { return new Promise(resolve => { channelToPublishTo.publish({ onDone: resolve, isParallel, frameworkVersion }) }) } function getShouldBeSkippedSuite (pickle, suitesToSkip) { const testSuitePath = getTestSuitePath(pickle.uri, process.cwd()) const isUnskippable = isMarkedAsUnskippable(pickle) const isSkipped = suitesToSkip.includes(testSuitePath) return [isSkipped && !isUnskippable, testSuitePath] } // From cucumber@>=11 function getFilteredPicklesNew (coordinator, suitesToSkip) { return coordinator.sourcedPickles.reduce((acc, sourcedPickle) => { const { pickle } = sourcedPickle const [shouldBeSkipped, testSuitePath] = getShouldBeSkippedSuite(pickle, suitesToSkip) if (shouldBeSkipped) { acc.skippedSuites.add(testSuitePath) } else { acc.picklesToRun.push(sourcedPickle) } return acc }, { skippedSuites: new Set(), picklesToRun: [] }) } function getFilteredPickles (runtime, suitesToSkip) { return runtime.pickleIds.reduce((acc, pickleId) => { const pickle = runtime.eventDataCollector.getPickle(pickleId) const [shouldBeSkipped, testSuitePath] = getShouldBeSkippedSuite(pickle, suitesToSkip) if (shouldBeSkipped) { acc.skippedSuites.add(testSuitePath) } else { acc.picklesToRun.push(pickleId) } return acc }, { skippedSuites: new Set(), picklesToRun: [] }) } // From cucumber@>=11 function getPickleByFileNew (coordinator) { return coordinator.sourcedPickles.reduce((acc, { pickle }) => { if (acc[pickle.uri]) { acc[pickle.uri].push(pickle) } else { acc[pickle.uri] = [pickle] } return acc }, {}) } function getPickleByFile (runtimeOrCoodinator) { return runtimeOrCoodinator.pickleIds.reduce((acc, pickleId) => { const test = runtimeOrCoodinator.eventDataCollector.getPickle(pickleId) if (acc[test.uri]) { acc[test.uri].push(test) } else { acc[test.uri] = [test] } return acc }, {}) } function wrapRun (pl, isLatestVersion, version) { if (patched.has(pl)) return patched.add(pl) shimmer.wrap(pl.prototype, 'run', run => function () { if (!testFinishCh.hasSubscribers) { return run.apply(this, arguments) } let numAttempt = 0 const testFileAbsolutePath = this.pickle.uri const testSourceLine = this.gherkinDocument?.feature?.location?.line const testStartPayload = { testName: this.pickle.name, testFileAbsolutePath, testSourceLine, isParallel: !!getEnvironmentVariable('CUCUMBER_WORKER_ID') } const ctx = testStartPayload numAttemptToCtx.set(numAttempt, ctx) testStartCh.runStores(ctx, () => {}) const promises = {} try { this.eventBroadcaster.on('envelope', async (testCase) => { // Only supported from >=8.0.0 if (testCase?.testCaseFinished) { const { testCaseFinished: { willBeRetried } } = testCase if (willBeRetried) { // test case failed and will be retried let error try { const cucumberResult = this.getWorstStepResult() error = getErrorFromCucumberResult(cucumberResult) } catch { // ignore error } const failedAttemptCtx = numAttemptToCtx.get(numAttempt) const isFirstAttempt = numAttempt++ === 0 const isAtrRetry = !isFirstAttempt && isFlakyTestRetriesEnabled if (promises.hitBreakpointPromise) { await promises.hitBreakpointPromise } // the current span will be finished and a new one will be created testRetryCh.publish({ isFirstAttempt, error, isAtrRetry, ...failedAttemptCtx.currentStore }) const newCtx = { ...testStartPayload, promises } numAttemptToCtx.set(numAttempt, newCtx) testStartCh.runStores(newCtx, () => {}) } } }) let promise testFnCh.runStores(ctx, () => { promise = run.apply(this, arguments) }) promise.finally(async () => { const result = this.getWorstStepResult() const { status, skipReason } = isLatestVersion ? getStatusFromResultLatest(result) : getStatusFromResult(result) if (lastStatusByPickleId.has(this.pickle.id)) { lastStatusByPickleId.get(this.pickle.id).push(status) } else { lastStatusByPickleId.set(this.pickle.id, [status]) } let isNew = false let isEfdRetry = false let isAttemptToFix = false let isAttemptToFixRetry = false let hasFailedAllRetries = false let hasPassedAllRetries = false let hasFailedAttemptToFix = false let isDisabled = false let isQuarantined = false let isModified = false if (isTestManagementTestsEnabled) { const testSuitePath = getTestSuitePath(testFileAbsolutePath, process.cwd()) const testProperties = getTestProperties(testSuitePath, this.pickle.name) const numRetries = numRetriesByPickleId.get(this.pickle.id) isAttemptToFix = testProperties.attemptToFix isAttemptToFixRetry = isAttemptToFix && numRetries > 0 isDisabled = testProperties.disabled isQuarantined = testProperties.quarantined if (isAttemptToFixRetry) { const statuses = lastStatusByPickleId.get(this.pickle.id) if (statuses.length === testManagementAttemptToFixRetries + 1) { const { pass, fail } = statuses.reduce((acc, status) => { acc[status]++ return acc }, { pass: 0, fail: 0 }) hasFailedAllRetries = fail === testManagementAttemptToFixRetries + 1 hasPassedAllRetries = pass === testManagementAttemptToFixRetries + 1 hasFailedAttemptToFix = fail > 0 } } } const numRetries = numRetriesByPickleId.get(this.pickle.id) if (isImpactedTestsEnabled) { isModified = modifiedTestsByPickleId.get(this.pickle.id) } if (isKnownTestsEnabled && status !== 'skip') { isNew = numRetries !== undefined } if (isNew || isModified) { isEfdRetry = numRetries > 0 } const attemptCtx = numAttemptToCtx.get(numAttempt) const error = getErrorFromCucumberResult(result) if (promises.hitBreakpointPromise) { await promises.hitBreakpointPromise } testFinishCh.publish({ status, skipReason, error, isNew, isEfdRetry, isFlakyRetry: numAttempt > 0, isAttemptToFix, isAttemptToFixRetry, hasFailedAllRetries, hasPassedAllRetries, hasFailedAttemptToFix, isDisabled, isQuarantined, isModified, ...attemptCtx.currentStore }) }) return promise } catch (err) { ctx.err = err errorCh.runStores(ctx, () => { throw err }) } }) shimmer.wrap(pl.prototype, 'runStep', runStep => function () { if (!testFinishCh.hasSubscribers) { return runStep.apply(this, arguments) } const testStep = arguments[0] let resource if (isLatestVersion) { resource = testStep.text } else { resource = testStep.isHook ? 'hook' : testStep.pickleStep.text } const ctx = { resource } return testStepStartCh.runStores(ctx, () => { try { const promise = runStep.apply(this, arguments) promise.then((result) => { const finalResult = satisfies(version, '>=12.0.0') ? result.result : result const getStatus = satisfies(version, '>=7.3.0') ? getStatusFromResultLatest : getStatusFromResult const { status, skipReason, errorMessage } = getStatus(finalResult) testFinishCh.publish({ isStep: true, status, skipReason, errorMessage, ...ctx.currentStore }) }) return promise } catch (err) { ctx.err = err errorCh.runStores(ctx, () => { throw err }) } }) }) } function pickleHook (PickleRunner, version) { const pl = PickleRunner.default wrapRun(pl, false, version) return PickleRunner } function testCaseHook (TestCaseRunner, version) { const pl = TestCaseRunner.default wrapRun(pl, true, version) return TestCaseRunner } // Valid for old and new cucumber versions function getCucumberOptions (adapterOrCoordinator) { if (adapterOrCoordinator.adapter) { return adapterOrCoordinator.adapter.worker?.options || adapterOrCoordinator.adapter.options } return adapterOrCoordinator.options } function getWrappedStart (start, frameworkVersion, isParallel = false, isCoordinator = false) { return async function () { if (!libraryConfigurationCh.hasSubscribers) { return start.apply(this, arguments) } const options = getCucumberOptions(this) if (!isParallel && this.adapter?.options) { isParallel = options.parallel > 0 } let errorSkippableRequest const configurationResponse = await getChannelPromise(libraryConfigurationCh, isParallel, frameworkVersion) isEarlyFlakeDetectionEnabled = configurationResponse.libraryConfig?.isEarlyFlakeDetectionEnabled earlyFlakeDetectionNumRetries = configurationResponse.libraryConfig?.earlyFlakeDetectionNumRetries earlyFlakeDetectionFaultyThreshold = configurationResponse.libraryConfig?.earlyFlakeDetectionFaultyThreshold isSuitesSkippingEnabled = configurationResponse.libraryConfig?.isSuitesSkippingEnabled isFlakyTestRetriesEnabled = configurationResponse.libraryConfig?.isFlakyTestRetriesEnabled numTestRetries = configurationResponse.libraryConfig?.flakyTestRetriesCount isKnownTestsEnabled = configurationResponse.libraryConfig?.isKnownTestsEnabled isTestManagementTestsEnabled = configurationResponse.libraryConfig?.isTestManagementEnabled testManagementAttemptToFixRetries = configurationResponse.libraryConfig?.testManagementAttemptToFixRetries isImpactedTestsEnabled = configurationResponse.libraryConfig?.isImpactedTestsEnabled if (isKnownTestsEnabled) { const knownTestsResponse = await getChannelPromise(knownTestsCh) if (knownTestsResponse.err) { isEarlyFlakeDetectionEnabled = false isKnownTestsEnabled = false } else { knownTests = knownTestsResponse.knownTests } } if (isSuitesSkippingEnabled) { const skippableResponse = await getChannelPromise(skippableSuitesCh) errorSkippableRequest = skippableResponse.err skippableSuites = skippableResponse.skippableSuites if (!errorSkippableRequest) { const filteredPickles = isCoordinator ? getFilteredPicklesNew(this, skippableSuites) : getFilteredPickles(this, skippableSuites) const { picklesToRun } = filteredPickles const oldPickles = isCoordinator ? this.sourcedPickles : this.pickleIds isSuitesSkipped = picklesToRun.length !== oldPickles.length log.debug('%s out of %s suites are going to run.', picklesToRun.length, oldPickles.length) if (isCoordinator) { this.sourcedPickles = picklesToRun } else { this.pickleIds = picklesToRun } skippedSuites = [...filteredPickles.skippedSuites] itrCorrelationId = skippableResponse.itrCorrelationId } } pickleByFile = isCoordinator ? getPickleByFileNew(this) : getPickleByFile(this) if (isKnownTestsEnabled) { const isFaulty = !isValidKnownTests(knownTests) || getIsFaultyEarlyFlakeDetection( Object.keys(pickleByFile), knownTests.cucumber, earlyFlakeDetectionFaultyThreshold ) if (isFaulty) { isEarlyFlakeDetectionEnabled = false isKnownTestsEnabled = false isEarlyFlakeDetectionFaulty = true } } if (isTestManagementTestsEnabled) { const testManagementTestsResponse = await getChannelPromise(testManagementTestsCh) if (testManagementTestsResponse.err) { isTestManagementTestsEnabled = false } else { testManagementTests = testManagementTestsResponse.testManagementTests } } if (isImpactedTestsEnabled) { const impactedTestsResponse = await getChannelPromise(modifiedFilesCh) if (!impactedTestsResponse.err) { modifiedFiles = impactedTestsResponse.modifiedFiles } } const processArgv = process.argv.slice(2).join(' ') const command = getEnvironmentVariable('npm_lifecycle_script') || `cucumber-js ${processArgv}` if (isFlakyTestRetriesEnabled && !options.retry && numTestRetries > 0) { options.retry = numTestRetries } sessionStartCh.publish({ command, frameworkVersion }) if (!errorSkippableRequest && skippedSuites.length) { itrSkippedSuitesCh.publish({ skippedSuites, frameworkVersion }) } const success = await start.apply(this, arguments) let untestedCoverage if (getCodeCoverageCh.hasSubscribers) { untestedCoverage = await getChannelPromise(getCodeCoverageCh) } let testCodeCoverageLinesTotal if (global.__coverage__) { try { if (untestedCoverage) { originalCoverageMap.merge(fromCoverageMapToCoverage(untestedCoverage)) } testCodeCoverageLinesTotal = originalCoverageMap.getCoverageSummary().lines.pct } catch { // ignore errors } // restore the original coverage global.__coverage__ = fromCoverageMapToCoverage(originalCoverageMap) } sessionFinishCh.publish({ status: success ? 'pass' : 'fail', isSuitesSkipped, testCodeCoverageLinesTotal, numSkippedSuites: skippedSuites.length, hasUnskippableSuites: isUnskippable, hasForcedToRunSuites: isForcedToRun, isEarlyFlakeDetectionEnabled, isEarlyFlakeDetectionFaulty, isTestManagementTestsEnabled, isParallel }) eventDataCollector = null return success } } // Generates suite start and finish events in the main process. // Handles EFD in both the main process and the worker process. function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = false, isWorker = false) { return async function () { if (!testSuiteFinishCh.hasSubscribers) { return runTestCaseFunction.apply(this, arguments) } const pickle = isNewerCucumberVersion ? arguments[0].pickle : this.eventDataCollector.getPickle(arguments[0]) const testCase = isNewerCucumberVersion ? arguments[0].testCase : arguments[1] const gherkinDocument = isNewerCucumberVersion ? arguments[0].gherkinDocument : this.eventDataCollector.getGherkinDocument(pickle.uri) const testFileAbsolutePath = pickle.uri const testSuitePath = getTestSuitePath(testFileAbsolutePath, process.cwd()) // If it's a worker, suite events are handled in `getWrappedParseWorkerMessage` if (!isWorker && !pickleResultByFile[testFileAbsolutePath]) { // first test in suite isUnskippable = isMarkedAsUnskippable(pickle) isForcedToRun = isUnskippable && skippableSuites.includes(testSuitePath) testSuiteStartCh.publish({ testFileAbsolutePath, isUnskippable, isForcedToRun, itrCorrelationId }) } let isNew = false let isAttemptToFix = false let isDisabled = false let isQuarantined = false let isModified = false if (isTestManagementTestsEnabled) { const testProperties = getTestProperties(testSuitePath, pickle.name) isAttemptToFix = testProperties.attemptToFix isDisabled = testProperties.disabled isQuarantined = testProperties.quarantined // If attempt to fix is enabled, we run even if the test is disabled if (!isAttemptToFix && isDisabled) { this.options.dryRun = true } } if (isImpactedTestsEnabled) { const setIsModified = (receivedIsModified) => { isModified = receivedIsModified } const scenarios = gherkinDocument.feature?.children?.filter( children => pickle.astNodeIds.includes(children.scenario.id) ).map(scenario => scenario.scenario) const stepIds = testCase?.testSteps?.flatMap(testStep => testStep.stepDefinitionIds) isModifiedCh.publish({ scenarios, testFileAbsolutePath: gherkinDocument.uri, modifiedFiles, stepIds, stepDefinitions: this.supportCodeLibrary.stepDefinitions, setIsModified }) modifiedTestsByPickleId.set(pickle.id, isModified) } if (isKnownTestsEnabled && !isAttemptToFix) { isNew = isNewTest(testSuitePath, pickle.name) if (isNew) { numRetriesByPickleId.set(pickle.id, 0) } } // TODO: for >=11 we could use `runTestCaseResult` instead of accumulating results in `lastStatusByPickleId` let runTestCaseResult = await runTestCaseFunction.apply(this, arguments) const testStatuses = lastStatusByPickleId.get(pickle.id) const lastTestStatus = testStatuses.at(-1) // New tests should not be marked as attempt to fix, so EFD + Attempt to fix should not be enabled at the same time if (isAttemptToFix && lastTestStatus !== 'skip') { for (let retryIndex = 0; retryIndex < testManagementAttemptToFixRetries; retryIndex++) { numRetriesByPickleId.set(pickle.id, retryIndex + 1) // eslint-disable-next-line no-await-in-loop runTestCaseResult = await runTestCaseFunction.apply(this, arguments) } } // If it's a new test and it hasn't been skipped, we run it again if (isEarlyFlakeDetectionEnabled && lastTestStatus !== 'skip' && (isNew || isModified)) { for (let retryIndex = 0; retryIndex < earlyFlakeDetectionNumRetries; retryIndex++) { numRetriesByPickleId.set(pickle.id, retryIndex + 1) // eslint-disable-next-line no-await-in-loop runTestCaseResult = await runTestCaseFunction.apply(this, arguments) } } let testStatus = lastTestStatus let shouldBePassedByEFD = false let shouldBePassedByTestManagement = false if ((isNew || isModified) && isEarlyFlakeDetectionEnabled) { /** * If Early Flake Detection (EFD) is enabled the logic is as follows: * - If all attempts for a test are failing, the test has failed and we will let the test process fail. * - If just a single attempt passes, we will prevent the test process from failing. * The rationale behind is the following: you may still be able to block your CI pipeline by gating * on flakiness (the test will be considered flaky), but you may choose to unblock the pipeline too. */ testStatus = getTestStatusFromRetries(testStatuses) if (testStatus === 'pass') { // for cucumber@>=11, setting `this.success` does not work, so we have to change the returned value shouldBePassedByEFD = true this.success = true } } if (isTestManagementTestsEnabled && (isDisabled || isQuarantined)) { this.success = true shouldBePassedByTestManagement = true } if (pickleResultByFile[testFileAbsolutePath]) { pickleResultByFile[testFileAbsolutePath].push(testStatus) } else { pickleResultByFile[testFileAbsolutePath] = [testStatus] } // If it's a worker, suite events are handled in `getWrappedParseWorkerMessage` if (!isWorker && pickleResultByFile[testFileAbsolutePath].length === pickleByFile[testFileAbsolutePath].length) { // last test in suite const testSuiteStatus = getSuiteStatusFromTestStatuses(pickleResultByFile[testFileAbsolutePath]) if (global.__coverage__) { const coverageFiles = getCoveredFilenamesFromCoverage(global.__coverage__) testSuiteCodeCoverageCh.publish({ coverageFiles, suiteFile: testFileAbsolutePath, testSuitePath }) // We need to reset coverage to get a code coverage per suite // Before that, we preserve the original coverage mergeCoverage(global.__coverage__, originalCoverageMap) resetCoverage(global.__coverage__) } testSuiteFinishCh.publish({ status: testSuiteStatus, testSuitePath }) } if (isNewerCucumberVersion && isEarlyFlakeDetectionEnabled && (isNew || isModified)) { return shouldBePassedByEFD } if (isNewerCucumberVersion && isTestManagementTestsEnabled && (isQuarantined || isDisabled)) { return shouldBePassedByTestManagement } return runTestCaseResult } } function getWrappedParseWorkerMessage (parseWorkerMessageFunction, isNewVersion) { return function (worker, message) { if (!testSuiteFinishCh.hasSubscribers) { return parseWorkerMessageFunction.apply(this, arguments) } // If the message is an array, it's a dd-trace message, so we need to stop cucumber processing, // or cucumber will throw an error // TODO: identify the message better if (Array.isArray(message)) { const [messageCode, payload] = message if (messageCode === CUCUMBER_WORKER_TRACE_PAYLOAD_CODE) { workerReportTraceCh.publish(payload) return } } const envelope = isNewVersion ? message.envelope : message.jsonEnvelope if (!envelope) { return parseWorkerMessageFunction.apply(this, arguments) } let parsed = envelope if (typeof parsed === 'string') { try { parsed = JSON.parse(envelope) } catch { // ignore errors and continue return parseWorkerMessageFunction.apply(this, arguments) } } let pickle if (parsed.testCaseStarted) { if (isNewVersion) { pickle = this.inProgress[worker.id].pickle } else { const { pickleId } = this.eventDataCollector.testCaseMap[parsed.testCaseStarted.testCaseId] pickle = this.eventDataCollector.getPickle(pickleId) } // THIS FAILS IN PARALLEL MODE const testFileAbsolutePath = pickle.uri // First test in suite if (!pickleResultByFile[testFileAbsolutePath]) { pickleResultByFile[testFileAbsolutePath] = [] testSuiteStartCh.publish({ testFileAbsolutePath }) } } const parseWorkerResponse = parseWorkerMessageFunction.apply(this, arguments) // after calling `parseWorkerMessageFunction`, the test status can already be read if (parsed.testCaseFinished) { let worstTestStepResult if (isNewVersion && eventDataCollector) { pickle = this.inProgress[worker.id].pickle worstTestStepResult = eventDataCollector.getTestCaseAttempt(parsed.testCaseFinished.testCaseStartedId).worstTestStepResult } else { const testCase = this.eventDataCollector.getTestCaseAttempt(parsed.testCaseFinished.testCaseStartedId) worstTestStepResult = testCase.worstTestStepResult pickle = testCase.pickle } const { status } = getStatusFromResultLatest(worstTestStepResult) let isNew = false if (isKnownTestsEnabled) { isNew = isNewTest(pickle.uri, pickle.name) } const testFileAbsolutePath = pickle.uri const finished = pickleResultByFile[testFileAbsolutePath] if (isEarlyFlakeDetectionEnabled && isNew) { const testFullname = `${pickle.uri}:${pickle.name}` let testStatuses = newTestsByTestFullname.get(testFullname) if (testStatuses) { testStatuses.push(status) } else { testStatuses = [status] newTestsByTestFullname.set(testFullname, testStatuses) } // We have finished all retries if (testStatuses.length === earlyFlakeDetectionNumRetries + 1) { const newTestFinalStatus = getTestStatusFromRetries(testStatuses) // we only push to `finished` if the retries have finished finished.push(newTestFinalStatus) } } else { // TODO: can we get error message? const finished = pickleResultByFile[testFileAbsolutePath] finished.push(status) } if (finished.length === pickleByFile[testFileAbsolutePath].length) { testSuiteFinishCh.publish({ status: getSuiteStatusFromTestStatuses(finished), testSuitePath: getTestSuitePath(testFileAbsolutePath, process.cwd()) }) } } return parseWorkerResponse } } // Test start / finish for older versions. The only hook executed in workers when in parallel mode addHook({ name: '@cucumber/cucumber', versions: ['7.0.0 - 7.2.1'], file: 'lib/runtime/pickle_runner.js' }, pickleHook) // Test start / finish for newer versions. The only hook executed in workers when in parallel mode addHook({ name: '@cucumber/cucumber', versions: ['>=7.3.0'], file: 'lib/runtime/test_case_runner.js' }, testCaseHook) // From 7.3.0 onwards, runPickle becomes runTestCase. Not executed in parallel mode. // `getWrappedStart` generates session start and finish events // `getWrappedRunTestCase` generates suite start and finish events and handles EFD. // TODO (fix): there is a lib/runtime/index in >=11.0.0, but we don't instrument it because it's not useful for us // This causes a info log saying "Found incompatible integration version". addHook({ name: '@cucumber/cucumber', versions: ['>=7.3.0 <11.0.0'], file: 'lib/runtime/index.js' }, (runtimePackage, frameworkVersion) => { shimmer.wrap(runtimePackage.default.prototype, 'runTestCase', runTestCase => getWrappedRunTestCase(runTestCase)) shimmer.wrap(runtimePackage.default.prototype, 'start', start => getWrappedStart(start, frameworkVersion)) return runtimePackage }) // Not executed in parallel mode. // `getWrappedStart` generates session start and finish events // `getWrappedRunTestCase` generates suite start and finish events and handles EFD. addHook({ name: '@cucumber/cucumber', versions: ['>=7.0.0 <7.3.0'], file: 'lib/runtime/index.js' }, (runtimePackage, frameworkVersion) => { shimmer.wrap(runtimePackage.default.prototype, 'runPickle', runPickle => getWrappedRunTestCase(runPickle)) shimmer.wrap(runtimePackage.default.prototype, 'start', start => getWrappedStart(start, frameworkVersion)) return runtimePackage }) // Only executed in parallel mode. // `getWrappedStart` generates session start and finish events // `getWrappedParseWorkerMessage` generates suite start and finish events addHook({ name: '@cucumber/cucumber', versions: ['>=8.0.0 <11.0.0'], file: 'lib/runtime/parallel/coordinator.js' }, (coordinatorPackage, frameworkVersion) => { shimmer.wrap(coordinatorPackage.default.prototype, 'start', start => getWrappedStart(start, frameworkVersion, true)) shimmer.wrap( coordinatorPackage.default.prototype, 'parseWorkerMessage', parseWorkerMessage => getWrappedParseWorkerMessage(parseWorkerMessage) ) return coordinatorPackage }) // >=11.0.0 hooks // `getWrappedRunTestCase` does two things: // - generates suite start and finish events in the main process, // - handles EFD in both the main process and the worker process. addHook({ name: '@cucumber/cucumber', versions: ['>=11.0.0'], file: 'lib/runtime/worker.js' }, (workerPackage) => { shimmer.wrap( workerPackage.Worker.prototype, 'runTestCase', runTestCase => getWrappedRunTestCase(runTestCase, true, !!getEnvironmentVariable('CUCUMBER_WORKER_ID')) ) return workerPackage }) // `getWrappedStart` generates session start and finish events addHook({ name: '@cucumber/cucumber', versions: ['>=11.0.0'], file: 'lib/runtime/coordinator.js' }, (coordinatorPackage, frameworkVersion) => { shimmer.wrap( coordinatorPackage.Coordinator.prototype, 'run', run => getWrappedStart(run, frameworkVersion, false, true) ) return coordinatorPackage }) // Necessary because `eventDataCollector` is no longer available in the runtime instance addHook({ name: '@cucumber/cucumber', versions: ['>=11.0.0'], file: 'lib/formatter/helpers/event_data_collector.js' }, (eventDataCollectorPackage) => { shimmer.wrap(eventDataCollectorPackage.default.prototype, 'parseEnvelope', parseEnvelope => function () { eventDataCollector = this return parseEnvelope.apply(this, arguments) }) return eventDataCollectorPackage }) // Only executed in parallel mode for >=11, in the main process. // `getWrappedParseWorkerMessage` generates suite start and finish events // In `startWorker` we pass early flake detection info to the worker. addHook({ name: '@cucumber/cucumber', versions: ['>=11.0.0'], file: 'lib/runtime/parallel/adapter.js' }, (adapterPackage) => { shimmer.wrap( adapterPackage.ChildProcessAdapter.prototype, 'parseWorkerMessage', parseWorkerMessage => getWrappedParseWorkerMessage(parseWorkerMessage, true) ) // EFD in parallel mode only supported in >=11.0.0 shimmer.wrap(adapterPackage.ChildProcessAdapter.prototype, 'startWorker', startWorker => function () { if (isKnownTestsEnabled && isValidKnownTests(knownTests)) { this.options.worldParameters._ddIsKnownTestsEnabled = true this.options.worldParameters._ddIsEarlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled this.options.worldParameters._ddKnownTests = knownTests this.options.worldParameters._ddEarlyFlakeDetectionNumRetries = earlyFlakeDetectionNumRetries } else { isEarlyFlakeDetectionEnabled = false isKnownTestsEnabled = false this.options.worldParameters._ddIsEarlyFlakeDetectionEnabled = false this.options.worldParameters._ddIsKnownTestsEnabled = false this.options.worldParameters._ddEarlyFlakeDetectionNumRetries = 0 } if (isImpactedTestsEnabled) { this.options.worldParameters._ddImpactedTestsEnabled = isImpactedTestsEnabled this.options.worldParameters._ddModifiedFiles = modifiedFiles } return startWorker.apply(this, arguments) }) return adapterPackage }) // Hook executed in the worker process when in parallel mode. // In this hook we read the information passed in `worldParameters` and make it available for // `getWrappedRunTestCase`. addHook({ name: '@cucumber/cucumber', versions: ['>=11.0.0'], file: 'lib/runtime/parallel/worker.js' }, (workerPackage) => { shimmer.wrap( workerPackage.ChildProcessWorker.prototype, 'initialize', initialize => async function () { await initialize.apply(this, arguments) isKnownTestsEnabled = !!this.options.worldParameters._ddIsKnownTestsEnabled if (isKnownTestsEnabled) { knownTests = this.options.worldParameters._ddKnownTests // if for whatever reason the worker does not receive valid known tests, we disable EFD and known tests if (!isValidKnownTests(knownTests)) { isKnownTestsEnabled = false knownTests = {} } } isEarlyFlakeDetectionEnabled = !!this.options.worldParameters._ddIsEarlyFlakeDetectionEnabled if (isEarlyFlakeDetectionEnabled) { earlyFlakeDetectionNumRetries = this.options.worldParameters._ddEarlyFlakeDetectionNumRetries } isImpactedTestsEnabled = !!this.options.worldParameters._ddImpactedTestsEnabled if (isImpactedTestsEnabled) { modifiedFiles = this.options.worldParameters._ddModifiedFiles } } ) return workerPackage })