UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

267 lines (247 loc) 8.54 kB
const { promisify } = require('util') const { SAMPLING_RULE_DECISION } = require('../../dd-trace/src/constants') const { SAMPLING_PRIORITY, SPAN_TYPE, RESOURCE_NAME } = require('../../../ext/tags') const { AUTO_KEEP } = require('../../../ext/priority') const { TEST_TYPE, TEST_NAME, TEST_SUITE, TEST_STATUS, TEST_PARAMETERS, TEST_FRAMEWORK_VERSION, CI_APP_ORIGIN, getTestEnvironmentMetadata, getTestParametersString, finishAllTraceSpans, getTestParentSpan, getTestSuitePath } = require('../../dd-trace/src/plugins/util/test') function getTestSpanMetadata (tracer, test, sourceRoot) { const childOf = getTestParentSpan(tracer) const { file: testSuiteAbsolutePath } = test const fullTestName = test.fullTitle() const testSuite = getTestSuitePath(testSuiteAbsolutePath, sourceRoot) return { childOf, resource: `${testSuite}.${fullTestName}`, [TEST_TYPE]: 'test', [TEST_NAME]: fullTestName, [TEST_SUITE]: testSuite, [SAMPLING_RULE_DECISION]: 1, [SAMPLING_PRIORITY]: AUTO_KEEP, [TEST_FRAMEWORK_VERSION]: tracer._version } } function createWrapRunTest (tracer, testEnvironmentMetadata, sourceRoot) { return function wrapRunTest (runTest) { return async function runTestWithTrace () { // `runTest` is rerun when retries are configured through `this.retries` and the test fails. // This clause prevents rewrapping `this.test.fn` when it has already been wrapped. if (this.test._currentRetry !== undefined && this.test._currentRetry !== 0) { return runTest.apply(this, arguments) } let specFunction = this.test.fn if (specFunction.length) { specFunction = promisify(specFunction) // otherwise you have to explicitly call done() this.test.async = 0 this.test.sync = true } const { childOf, resource, ...testSpanMetadata } = getTestSpanMetadata(tracer, this.test, sourceRoot) const testParametersString = getTestParametersString(nameToParams, this.test.title) if (testParametersString) { testSpanMetadata[TEST_PARAMETERS] = testParametersString } this.test.fn = tracer.wrap( 'mocha.test', { type: 'test', childOf, resource, tags: { ...testSpanMetadata, ...testEnvironmentMetadata } }, async () => { const activeSpan = tracer.scope().active() activeSpan.context()._trace.origin = CI_APP_ORIGIN let result try { const context = this.test.ctx result = await specFunction.call(context) if (context.test.state !== 'failed' && !context.test.timedOut) { activeSpan.setTag(TEST_STATUS, 'pass') } else { activeSpan.setTag(TEST_STATUS, 'fail') } } catch (error) { // this.skip has been called if (error.constructor.name === 'Pending' && !this.forbidPending) { activeSpan.setTag(TEST_STATUS, 'skip') } else { activeSpan.setTag(TEST_STATUS, 'fail') activeSpan.setTag('error', error) } throw error } finally { finishAllTraceSpans(activeSpan) } return result } ) return runTest.apply(this, arguments) } } } function getAllTestsInSuite (root) { const tests = [] function getTests (suiteOrTest) { suiteOrTest.tests.forEach(test => { tests.push(test) }) suiteOrTest.suites.forEach(suite => { getTests(suite) }) } getTests(root) return tests } // Necessary to get the skipped tests, that do not go through runTest function createWrapRunTests (tracer, testEnvironmentMetadata, sourceRoot) { return function wrapRunTests (runTests) { return function runTestsWithTrace () { this.once('end', () => tracer._exporter._writer.flush()) runTests.apply(this, arguments) const suite = arguments[0] const tests = getAllTestsInSuite(suite) tests.forEach(test => { const { pending: isSkipped } = test // We call `getAllTestsInSuite` with the root suite so every skipped test // should already have an associated test span. // This function is called with every suite, so we need a way to mark // the test as already accounted for. We do this through `__datadog_skipped`. // If the test is already marked as skipped, we don't create an additional test span. if (!isSkipped || test.__datadog_skipped) { return } test.__datadog_skipped = true const { childOf, resource, ...testSpanMetadata } = getTestSpanMetadata(tracer, test, sourceRoot) const testSpan = tracer .startSpan('mocha.test', { childOf, tags: { [SPAN_TYPE]: 'test', [RESOURCE_NAME]: resource, ...testSpanMetadata, ...testEnvironmentMetadata, [TEST_STATUS]: 'skip' } }) testSpan.context()._trace.origin = CI_APP_ORIGIN testSpan.finish() }) } } } const nameToParams = {} function wrapMochaEach (mochaEach) { return function mochaEachWithTrace () { const [params] = arguments const { it, ...rest } = mochaEach.apply(this, arguments) return { it: function (name) { nameToParams[name] = params it.apply(this, arguments) }, ...rest } } } function createWrapFail (tracer, testEnvironmentMetadata, sourceRoot) { return function wrapFail (fail) { return function failWithTrace (hook, err) { if (hook.type !== 'hook') { /** * This clause is to cover errors that are uncaught, such as: * it('will fail', done => { * setTimeout(() => { * // will throw but will not be caught by `runTestWithTrace` * expect(true).to.equal(false) * done() * }, 100) * }) */ const testSpan = tracer.scope().active() if (!testSpan) { return fail.apply(this, arguments) } const { [TEST_NAME]: testName, [TEST_SUITE]: testSuite, [TEST_STATUS]: testStatus } = testSpan._spanContext._tags const isActiveSpanFailing = hook.fullTitle() === testName && hook.file.endsWith(testSuite) if (isActiveSpanFailing && !testStatus) { testSpan.setTag(TEST_STATUS, 'fail') testSpan.setTag('error', err) // need to manually finish, as it will not be caught in `runTestWithTrace` testSpan.finish() } return fail.apply(this, arguments) } if (err && hook.ctx && hook.ctx.currentTest) { err.message = `${hook.title}: ${err.message}` const { childOf, resource, ...testSpanMetadata } = getTestSpanMetadata(tracer, hook.ctx.currentTest, sourceRoot) const testSpan = tracer .startSpan('mocha.test', { childOf, tags: { [SPAN_TYPE]: 'test', [RESOURCE_NAME]: resource, ...testSpanMetadata, ...testEnvironmentMetadata, [TEST_STATUS]: 'fail' } }) testSpan.setTag('error', err) testSpan.context()._trace.origin = CI_APP_ORIGIN testSpan.finish() } return fail.apply(this, arguments) } } } module.exports = [ { name: 'mocha', versions: ['>=5.2.0'], file: 'lib/runner.js', patch (Runner, tracer, config) { const testEnvironmentMetadata = getTestEnvironmentMetadata('mocha', config) const sourceRoot = process.cwd() this.wrap(Runner.prototype, 'runTests', createWrapRunTests(tracer, testEnvironmentMetadata, sourceRoot)) this.wrap(Runner.prototype, 'runTest', createWrapRunTest(tracer, testEnvironmentMetadata, sourceRoot)) this.wrap(Runner.prototype, 'fail', createWrapFail(tracer, testEnvironmentMetadata, sourceRoot)) }, unpatch (Runner) { this.unwrap(Runner.prototype, 'runTests') this.unwrap(Runner.prototype, 'runTest') this.unwrap(Runner.prototype, 'fail') } }, { name: 'mocha-each', versions: ['>=2.0.1'], patch (mochaEach) { return this.wrapExport(mochaEach, wrapMochaEach(mochaEach)) }, unpatch (mochaEach) { this.unwrapExport(mochaEach) } } ]