UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

544 lines (474 loc) 17 kB
'use strict' const CiPlugin = require('../../dd-trace/src/plugins/ci_plugin') const { storage } = require('../../datadog-core') const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper') const { TEST_STATUS, TEST_PARAMETERS, finishAllTraceSpans, getTestSuitePath, getTestParametersString, getTestSuiteCommonTags, addIntelligentTestRunnerSpanTags, TEST_SOURCE_START, TEST_ITR_UNSKIPPABLE, TEST_ITR_FORCED_RUN, TEST_CODE_OWNERS, ITR_CORRELATION_ID, TEST_SOURCE_FILE, TEST_IS_NEW, TEST_IS_RETRY, TEST_EARLY_FLAKE_ENABLED, TEST_EARLY_FLAKE_ABORT_REASON, TEST_SESSION_ID, TEST_MODULE_ID, TEST_MODULE, TEST_SUITE_ID, TEST_COMMAND, TEST_SUITE, MOCHA_IS_PARALLEL, TEST_IS_RUM_ACTIVE, TEST_BROWSER_DRIVER, TEST_RETRY_REASON, TEST_MANAGEMENT_ENABLED, TEST_MANAGEMENT_IS_QUARANTINED, TEST_MANAGEMENT_IS_DISABLED, TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX, TEST_HAS_FAILED_ALL_RETRIES, TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED, TEST_RETRY_REASON_TYPES, TEST_IS_MODIFIED, isModifiedTest } = require('../../dd-trace/src/plugins/util/test') const { COMPONENT } = require('../../dd-trace/src/constants') const { TELEMETRY_EVENT_CREATED, TELEMETRY_EVENT_FINISHED, TELEMETRY_CODE_COVERAGE_STARTED, TELEMETRY_CODE_COVERAGE_FINISHED, TELEMETRY_ITR_FORCED_TO_RUN, TELEMETRY_CODE_COVERAGE_EMPTY, TELEMETRY_ITR_UNSKIPPABLE, TELEMETRY_CODE_COVERAGE_NUM_FILES, TELEMETRY_TEST_SESSION } = require('../../dd-trace/src/ci-visibility/telemetry') const id = require('../../dd-trace/src/id') const log = require('../../dd-trace/src/log') const BREAKPOINT_SET_GRACE_PERIOD_MS = 200 function getTestSuiteLevelVisibilityTags (testSuiteSpan) { const testSuiteSpanContext = testSuiteSpan.context() const suiteTags = { [TEST_SUITE_ID]: testSuiteSpanContext.toSpanId(), [TEST_SESSION_ID]: testSuiteSpanContext.toTraceId(), [TEST_COMMAND]: testSuiteSpanContext._tags[TEST_COMMAND], [TEST_MODULE]: 'mocha' } if (testSuiteSpanContext._parentId) { suiteTags[TEST_MODULE_ID] = testSuiteSpanContext._parentId.toString(10) } return suiteTags } class MochaPlugin extends CiPlugin { static id = 'mocha' constructor (...args) { super(...args) this._testSuites = new Map() this._testTitleToParams = {} this.sourceRoot = process.cwd() this.addSub('ci:mocha:test-suite:code-coverage', ({ coverageFiles, suiteFile }) => { if (!this.libraryConfig?.isCodeCoverageEnabled) { return } const testSuite = getTestSuitePath(suiteFile, this.sourceRoot) const testSuiteSpan = this._testSuites.get(testSuite) if (!coverageFiles.length) { this.telemetry.count(TELEMETRY_CODE_COVERAGE_EMPTY) } const relativeCoverageFiles = [...coverageFiles, suiteFile] .map(filename => getTestSuitePath(filename, this.repositoryRoot || this.sourceRoot)) const { _traceId, _spanId } = testSuiteSpan.context() const formattedCoverage = { sessionId: _traceId, suiteId: _spanId, files: relativeCoverageFiles } this.tracer._exporter.exportCoverage(formattedCoverage) this.telemetry.ciVisEvent(TELEMETRY_CODE_COVERAGE_FINISHED, 'suite', { library: 'istanbul' }) this.telemetry.distribution(TELEMETRY_CODE_COVERAGE_NUM_FILES, {}, relativeCoverageFiles.length) }) this.addBind('ci:mocha:test-suite:start', (ctx) => { const { testSuiteAbsolutePath, isUnskippable, isForcedToRun, itrCorrelationId } = ctx // If the test module span is undefined, the plugin has not been initialized correctly and we bail out if (!this.testModuleSpan) { return } const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.sourceRoot) const testSuiteMetadata = getTestSuiteCommonTags( this.command, this.frameworkVersion, testSuite, 'mocha' ) if (isUnskippable) { testSuiteMetadata[TEST_ITR_UNSKIPPABLE] = 'true' this.telemetry.count(TELEMETRY_ITR_UNSKIPPABLE, { testLevel: 'suite' }) } if (isForcedToRun) { testSuiteMetadata[TEST_ITR_FORCED_RUN] = 'true' this.telemetry.count(TELEMETRY_ITR_FORCED_TO_RUN, { testLevel: 'suite' }) } testSuiteMetadata[TEST_SOURCE_FILE] = this.repositoryRoot !== this.sourceRoot && !!this.repositoryRoot ? getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot) : testSuite if (testSuiteMetadata[TEST_SOURCE_FILE]) { testSuiteMetadata[TEST_SOURCE_START] = 1 } const codeOwners = this.getCodeOwners(testSuiteMetadata) if (codeOwners) { testSuiteMetadata[TEST_CODE_OWNERS] = codeOwners } const testSuiteSpan = this.tracer.startSpan('mocha.test_suite', { childOf: this.testModuleSpan, tags: { [COMPONENT]: this.constructor.id, ...this.testEnvironmentMetadata, ...testSuiteMetadata }, integrationName: this.constructor.id }) this.telemetry.ciVisEvent(TELEMETRY_EVENT_CREATED, 'suite') if (this.libraryConfig?.isCodeCoverageEnabled) { this.telemetry.ciVisEvent(TELEMETRY_CODE_COVERAGE_STARTED, 'suite', { library: 'istanbul' }) } if (itrCorrelationId) { testSuiteSpan.setTag(ITR_CORRELATION_ID, itrCorrelationId) } const store = storage('legacy').getStore() ctx.parentStore = store ctx.currentStore = { ...store, testSuiteSpan } this._testSuites.set(testSuite, testSuiteSpan) }) this.addSub('ci:mocha:test-suite:finish', ({ testSuiteSpan, status }) => { if (testSuiteSpan) { // the test status of the suite may have been set in ci:mocha:test-suite:error already if (!testSuiteSpan.context()._tags[TEST_STATUS]) { testSuiteSpan.setTag(TEST_STATUS, status) } testSuiteSpan.finish() this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite') } }) this.addBind('ci:mocha:test-suite:error', (ctx) => { const { error } = ctx const testSuiteSpan = ctx.currentStore?.testSuiteSpan if (testSuiteSpan) { testSuiteSpan.setTag('error', error) testSuiteSpan.setTag(TEST_STATUS, 'fail') ctx.parentStore = ctx.currentStore ctx.currentStore = { ...ctx.currentStore, testSuiteSpan } } return ctx.currentStore }) this.addSub('ci:mocha:test:is-modified', ({ modifiedTests, file, onDone }) => { const testPath = getTestSuitePath(file, this.repositoryRoot) const isModified = isModifiedTest( testPath, null, null, modifiedTests, this.constructor.id ) onDone(isModified) }) this.addBind('ci:mocha:test:fn', (ctx) => { return ctx.currentStore }) this.addBind('ci:mocha:test:start', (ctx) => { const store = storage('legacy').getStore() const span = this.startTestSpan(ctx) ctx.parentStore = store ctx.currentStore = { ...store, span } this.activeTestSpan = span return ctx.currentStore }) this.addSub('ci:mocha:worker:finish', () => { this.tracer._exporter.flush() }) this.addSub('ci:mocha:test:finish', ({ span, status, hasBeenRetried, isLastRetry, hasFailedAllRetries, attemptToFixPassed, attemptToFixFailed, isAttemptToFixRetry, isAtrRetry }) => { if (span) { span.setTag(TEST_STATUS, status) if (hasBeenRetried) { span.setTag(TEST_IS_RETRY, 'true') if (isAtrRetry) { span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atr) } else { span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.ext) } } if (hasFailedAllRetries) { span.setTag(TEST_HAS_FAILED_ALL_RETRIES, 'true') } if (attemptToFixPassed) { span.setTag(TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED, 'true') } else if (attemptToFixFailed) { span.setTag(TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED, 'false') } if (isAttemptToFixRetry) { span.setTag(TEST_IS_RETRY, 'true') span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atf) } const spanTags = span.context()._tags this.telemetry.ciVisEvent( TELEMETRY_EVENT_FINISHED, 'test', { hasCodeOwners: !!spanTags[TEST_CODE_OWNERS], isNew: spanTags[TEST_IS_NEW] === 'true', isRum: spanTags[TEST_IS_RUM_ACTIVE] === 'true', browserDriver: spanTags[TEST_BROWSER_DRIVER] } ) span.finish() finishAllTraceSpans(span) this.activeTestSpan = null if (this.di && this.libraryConfig?.isDiEnabled && this.runningTestProbe && isLastRetry) { this.removeDiProbe(this.runningTestProbe) this.runningTestProbe = null } } }) this.addBind('ci:mocha:test:skip', (ctx) => { const store = storage('legacy').getStore() // skipped through it.skip, so the span is not created yet // for this test if (!store) { const span = this.startTestSpan(ctx) ctx.parentStore = store ctx.currentStore = { ...store, span } this.activeTestSpan = span } return ctx.currentStore }) this.addBind('ci:mocha:test:error', (ctx) => { const { err } = ctx const span = ctx.currentStore?.span if (err && span) { if (err.constructor.name === 'Pending' && !this.forbidPending) { span.setTag(TEST_STATUS, 'skip') } else { span.setTag(TEST_STATUS, 'fail') span.setTag('error', err) } ctx.parentStore = ctx.currentStore ctx.currentStore = { ...ctx.currentStore, span } this.activeTestSpan = span } return ctx.currentStore }) this.addSub('ci:mocha:test:retry', ({ span, isFirstAttempt, willBeRetried, err, test, isAtrRetry }) => { if (span) { span.setTag(TEST_STATUS, 'fail') if (!isFirstAttempt) { span.setTag(TEST_IS_RETRY, 'true') if (isAtrRetry) { span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atr) } else { span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.ext) } } if (err) { span.setTag('error', err) } const spanTags = span.context()._tags this.telemetry.ciVisEvent( TELEMETRY_EVENT_FINISHED, 'test', { hasCodeOwners: !!spanTags[TEST_CODE_OWNERS], isNew: spanTags[TEST_IS_NEW] === 'true', isRum: spanTags[TEST_IS_RUM_ACTIVE] === 'true', browserDriver: spanTags[TEST_BROWSER_DRIVER] } ) if (isFirstAttempt && willBeRetried && this.di && this.libraryConfig?.isDiEnabled) { const probeInformation = this.addDiProbe(err) if (probeInformation) { const { file, line, stackIndex } = probeInformation this.runningTestProbe = { file, line } this.testErrorStackIndex = stackIndex test._ddShouldWaitForHitProbe = true const waitUntil = Date.now() + BREAKPOINT_SET_GRACE_PERIOD_MS while (Date.now() < waitUntil) { // TODO: To avoid a race condition, we should wait until `probeInformation.setProbePromise` has resolved. // However, Mocha doesn't have a mechanism for waiting asyncrounously here, so for now, we'll have to // fall back to a fixed syncronous delay. } } } span.finish() finishAllTraceSpans(span) } }) this.addSub('ci:mocha:test:parameterize', ({ title, params }) => { this._testTitleToParams[title] = params }) this.addSub('ci:mocha:session:finish', ({ status, isSuitesSkipped, testCodeCoverageLinesTotal, numSkippedSuites, hasForcedToRunSuites, hasUnskippableSuites, error, isEarlyFlakeDetectionEnabled, isEarlyFlakeDetectionFaulty, isTestManagementEnabled, isParallel }) => { if (this.testSessionSpan) { const { isSuitesSkippingEnabled, isCodeCoverageEnabled } = this.libraryConfig || {} this.testSessionSpan.setTag(TEST_STATUS, status) this.testModuleSpan.setTag(TEST_STATUS, status) if (error) { this.testSessionSpan.setTag('error', error) this.testModuleSpan.setTag('error', error) } if (isParallel) { this.testSessionSpan.setTag(MOCHA_IS_PARALLEL, 'true') } if (isTestManagementEnabled) { this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true') } addIntelligentTestRunnerSpanTags( this.testSessionSpan, this.testModuleSpan, { isSuitesSkipped, isSuitesSkippingEnabled, isCodeCoverageEnabled, testCodeCoverageLinesTotal, skippingCount: numSkippedSuites, skippingType: 'suite', hasForcedToRunSuites, hasUnskippableSuites } ) if (isEarlyFlakeDetectionEnabled) { this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ENABLED, 'true') } if (isEarlyFlakeDetectionFaulty) { this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'faulty') } this.testModuleSpan.finish() this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module') this.testSessionSpan.finish() this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session') finishAllTraceSpans(this.testSessionSpan) this.telemetry.count(TELEMETRY_TEST_SESSION, { provider: this.ciProviderName, autoInjected: !!getEnvironmentVariable('DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER') }) } this.libraryConfig = null this.tracer._exporter.flush() }) this.addSub('ci:mocha:worker-report:trace', (traces) => { const formattedTraces = JSON.parse(traces).map(trace => trace.map(span => { const formattedSpan = { ...span, span_id: id(span.span_id), trace_id: id(span.trace_id), parent_id: id(span.parent_id) } if (formattedSpan.name === 'mocha.test') { const testSuite = span.meta[TEST_SUITE] const testSuiteSpan = this._testSuites.get(testSuite) if (!testSuiteSpan) { log.warn('Test suite span not found for test span with test suite', testSuite) return formattedSpan } const suiteTags = getTestSuiteLevelVisibilityTags(testSuiteSpan) formattedSpan.meta = { ...formattedSpan.meta, ...suiteTags } } return formattedSpan }) ) formattedTraces.forEach(trace => { this.tracer._exporter.export(trace) }) }) this.addBind('ci:mocha:global:run', (ctx) => { return ctx.currentStore }) } startTestSpan (testInfo) { const { testName, testSuiteAbsolutePath, title, isNew, isEfdRetry, testStartLine, isParallel, isAttemptToFix, isDisabled, isQuarantined, isModified } = testInfo const extraTags = {} const testParametersString = getTestParametersString(this._testTitleToParams, title) if (testParametersString) { extraTags[TEST_PARAMETERS] = testParametersString } if (testStartLine) { extraTags[TEST_SOURCE_START] = testStartLine } if (isParallel) { extraTags[MOCHA_IS_PARALLEL] = 'true' } if (isAttemptToFix) { extraTags[TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX] = 'true' } if (isDisabled) { extraTags[TEST_MANAGEMENT_IS_DISABLED] = 'true' } if (isQuarantined) { extraTags[TEST_MANAGEMENT_IS_QUARANTINED] = 'true' } if (isModified) { extraTags[TEST_IS_MODIFIED] = 'true' if (isEfdRetry) { extraTags[TEST_IS_RETRY] = 'true' extraTags[TEST_RETRY_REASON] = TEST_RETRY_REASON_TYPES.efd } } const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.sourceRoot) const testSuiteSpan = this._testSuites.get(testSuite) extraTags[TEST_SOURCE_FILE] = this.repositoryRoot !== this.sourceRoot && !!this.repositoryRoot ? getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot) : testSuite if (isNew) { extraTags[TEST_IS_NEW] = 'true' if (isEfdRetry) { extraTags[TEST_IS_RETRY] = 'true' extraTags[TEST_RETRY_REASON] = TEST_RETRY_REASON_TYPES.efd } } return super.startTestSpan(testName, testSuite, testSuiteSpan, extraTags) } } module.exports = MochaPlugin