dd-trace 3.47.0 → 3.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-3rdparty.csv +1 -0
- package/README.md +1 -32
- package/ci/init.js +1 -4
- package/index.d.ts +21 -0
- package/package.json +7 -6
- package/packages/datadog-instrumentations/src/amqplib.js +1 -1
- package/packages/datadog-instrumentations/src/child_process.js +150 -0
- package/packages/datadog-instrumentations/src/cucumber.js +12 -12
- package/packages/datadog-instrumentations/src/express.js +20 -0
- package/packages/datadog-instrumentations/src/grpc/client.js +56 -36
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -2
- package/packages/datadog-instrumentations/src/jest.js +149 -11
- package/packages/datadog-instrumentations/src/mocha.js +142 -16
- package/packages/datadog-instrumentations/src/mongoose.js +23 -10
- package/packages/datadog-instrumentations/src/next.js +17 -3
- package/packages/datadog-instrumentations/src/playwright.js +41 -9
- package/packages/datadog-plugin-amqplib/src/consumer.js +10 -1
- package/packages/datadog-plugin-amqplib/src/producer.js +14 -1
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +107 -1
- package/packages/datadog-plugin-child_process/src/index.js +91 -0
- package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +125 -0
- package/packages/datadog-plugin-cucumber/src/index.js +16 -11
- package/packages/datadog-plugin-cypress/src/plugin.js +52 -23
- package/packages/datadog-plugin-grpc/src/client.js +16 -2
- package/packages/datadog-plugin-http/src/client.js +1 -1
- package/packages/datadog-plugin-jest/src/index.js +43 -6
- package/packages/datadog-plugin-kafkajs/src/consumer.js +16 -0
- package/packages/datadog-plugin-mocha/src/index.js +47 -17
- package/packages/datadog-plugin-playwright/src/index.js +19 -5
- package/packages/datadog-plugin-rhea/src/consumer.js +11 -1
- package/packages/datadog-plugin-rhea/src/producer.js +11 -0
- package/packages/dd-trace/src/appsec/addresses.js +2 -0
- package/packages/dd-trace/src/appsec/api_security_sampler.js +16 -3
- package/packages/dd-trace/src/appsec/channels.js +2 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/command-injection-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +7 -28
- package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +10 -6
- package/packages/dd-trace/src/appsec/iast/context/context-plugin.js +90 -0
- package/packages/dd-trace/src/appsec/iast/context/kafka-ctx-plugin.js +14 -0
- package/packages/dd-trace/src/appsec/iast/iast-plugin.js +12 -1
- package/packages/dd-trace/src/appsec/iast/index.js +4 -4
- package/packages/dd-trace/src/appsec/iast/overhead-controller.js +1 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/csi-methods.js +1 -0
- package/packages/dd-trace/src/appsec/iast/taint-tracking/index.js +10 -0
- package/packages/dd-trace/src/appsec/iast/taint-tracking/operations-taint-object.js +53 -0
- package/packages/dd-trace/src/appsec/iast/taint-tracking/operations.js +10 -46
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +13 -9
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugins/kafka.js +47 -0
- package/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js +3 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +29 -2
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
- package/packages/dd-trace/src/appsec/index.js +17 -2
- package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -1
- package/packages/dd-trace/src/appsec/remote_config/index.js +1 -0
- package/packages/dd-trace/src/appsec/rule_manager.js +2 -2
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +83 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +25 -6
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +2 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +83 -41
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +30 -8
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -1
- package/packages/dd-trace/src/ci-visibility/{intelligent-test-runner/get-itr-configuration.js → requests/get-library-configuration.js} +18 -6
- package/packages/dd-trace/src/config.js +22 -9
- package/packages/dd-trace/src/datastreams/processor.js +6 -0
- package/packages/dd-trace/src/datastreams/writer.js +2 -5
- package/packages/dd-trace/src/dogstatsd.js +3 -5
- package/packages/dd-trace/src/encode/agentless-ci-visibility.js +5 -3
- package/packages/dd-trace/src/exporters/common/request.js +21 -3
- package/packages/dd-trace/src/format.js +25 -1
- package/packages/dd-trace/src/noop/span.js +1 -0
- package/packages/dd-trace/src/opentelemetry/span.js +9 -2
- package/packages/dd-trace/src/opentracing/span.js +38 -0
- package/packages/dd-trace/src/opentracing/span_context.js +12 -6
- package/packages/dd-trace/src/opentracing/tracer.js +2 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +25 -9
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/dd-trace/src/plugins/util/git.js +6 -0
- package/packages/dd-trace/src/plugins/util/test.js +53 -8
- package/packages/dd-trace/src/profiling/config.js +22 -22
- package/packages/dd-trace/src/proxy.js +31 -23
- package/packages/dd-trace/src/span_processor.js +5 -1
- package/packages/dd-trace/src/telemetry/index.js +6 -0
- package/packages/dd-trace/src/telemetry/logs/index.js +2 -2
- package/packages/dd-trace/src/telemetry/send-data.js +0 -3
- package/packages/datadog-instrumentations/src/child-process.js +0 -29
- package/packages/dd-trace/src/plugins/util/exec.js +0 -34
|
@@ -9,7 +9,9 @@ const {
|
|
|
9
9
|
JEST_WORKER_COVERAGE_PAYLOAD_CODE,
|
|
10
10
|
getTestLineStart,
|
|
11
11
|
getTestSuitePath,
|
|
12
|
-
getTestParametersString
|
|
12
|
+
getTestParametersString,
|
|
13
|
+
EFD_STRING,
|
|
14
|
+
removeEfdStringFromTestName
|
|
13
15
|
} = require('../../dd-trace/src/plugins/util/test')
|
|
14
16
|
const {
|
|
15
17
|
getFormattedJestTestParameters,
|
|
@@ -37,11 +39,16 @@ const testRunFinishCh = channel('ci:jest:test:finish')
|
|
|
37
39
|
const testErrCh = channel('ci:jest:test:err')
|
|
38
40
|
|
|
39
41
|
const skippableSuitesCh = channel('ci:jest:test-suite:skippable')
|
|
40
|
-
const
|
|
42
|
+
const libraryConfigurationCh = channel('ci:jest:library-configuration')
|
|
43
|
+
const knownTestsCh = channel('ci:jest:known-tests')
|
|
41
44
|
|
|
42
45
|
const itrSkippedSuitesCh = channel('ci:jest:itr:skipped-suites')
|
|
43
46
|
|
|
47
|
+
// Maximum time we'll wait for the tracer to flush
|
|
48
|
+
const FLUSH_TIMEOUT = 10000
|
|
49
|
+
|
|
44
50
|
let skippableSuites = []
|
|
51
|
+
let knownTests = []
|
|
45
52
|
let isCodeCoverageEnabled = false
|
|
46
53
|
let isSuitesSkippingEnabled = false
|
|
47
54
|
let isUserCodeCoverageEnabled = false
|
|
@@ -49,6 +56,8 @@ let isSuitesSkipped = false
|
|
|
49
56
|
let numSkippedSuites = 0
|
|
50
57
|
let hasUnskippableSuites = false
|
|
51
58
|
let hasForcedToRunSuites = false
|
|
59
|
+
let isEarlyFlakeDetectionEnabled = false
|
|
60
|
+
let earlyFlakeDetectionNumRetries = 0
|
|
52
61
|
|
|
53
62
|
const sessionAsyncResource = new AsyncResource('bound-anonymous-fn')
|
|
54
63
|
|
|
@@ -62,6 +71,7 @@ const specStatusToTestStatus = {
|
|
|
62
71
|
|
|
63
72
|
const asyncResources = new WeakMap()
|
|
64
73
|
const originalTestFns = new WeakMap()
|
|
74
|
+
const retriedTestsToNumAttempts = new Map()
|
|
65
75
|
|
|
66
76
|
// based on https://github.com/facebook/jest/blob/main/packages/jest-circus/src/formatNodeAssertErrors.ts#L41
|
|
67
77
|
function formatJestError (errors) {
|
|
@@ -90,6 +100,10 @@ function getTestEnvironmentOptions (config) {
|
|
|
90
100
|
return {}
|
|
91
101
|
}
|
|
92
102
|
|
|
103
|
+
function getEfdTestName (testName, numAttempt) {
|
|
104
|
+
return `${EFD_STRING} (#${numAttempt}): ${testName}`
|
|
105
|
+
}
|
|
106
|
+
|
|
93
107
|
function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
94
108
|
return class DatadogEnvironment extends BaseEnvironment {
|
|
95
109
|
constructor (config, context) {
|
|
@@ -101,6 +115,44 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
101
115
|
this.global._ddtrace = global._ddtrace
|
|
102
116
|
|
|
103
117
|
this.testEnvironmentOptions = getTestEnvironmentOptions(config)
|
|
118
|
+
|
|
119
|
+
const repositoryRoot = this.testEnvironmentOptions._ddRepositoryRoot
|
|
120
|
+
|
|
121
|
+
if (repositoryRoot) {
|
|
122
|
+
this.testSourceFile = getTestSuitePath(context.testPath, repositoryRoot)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.isEarlyFlakeDetectionEnabled = this.testEnvironmentOptions._ddIsEarlyFlakeDetectionEnabled
|
|
126
|
+
|
|
127
|
+
if (this.isEarlyFlakeDetectionEnabled) {
|
|
128
|
+
earlyFlakeDetectionNumRetries = this.testEnvironmentOptions._ddEarlyFlakeDetectionNumRetries
|
|
129
|
+
try {
|
|
130
|
+
this.knownTestsForThisSuite = this.getKnownTestsForSuite(this.testEnvironmentOptions._ddKnownTests)
|
|
131
|
+
} catch (e) {
|
|
132
|
+
// If there has been an error parsing the tests, we'll disable Early Flake Deteciton
|
|
133
|
+
this.isEarlyFlakeDetectionEnabled = false
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Function that receives a list of known tests for a test service and
|
|
139
|
+
// returns the ones that belong to the current suite
|
|
140
|
+
getKnownTestsForSuite (knownTests) {
|
|
141
|
+
let knownTestsForSuite = knownTests
|
|
142
|
+
// If jest runs in band, the known tests are not serialized, so they're an array.
|
|
143
|
+
if (!Array.isArray(knownTests)) {
|
|
144
|
+
knownTestsForSuite = JSON.parse(knownTestsForSuite)
|
|
145
|
+
}
|
|
146
|
+
return knownTestsForSuite
|
|
147
|
+
.filter(test => test.includes(this.testSuite))
|
|
148
|
+
.map(test => test.replace(`jest.${this.testSuite}.`, '').trim())
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Add the `add_test` event we don't have the test object yet, so
|
|
152
|
+
// we use its describe block to get the full name
|
|
153
|
+
getTestNameFromAddTestEvent (event, state) {
|
|
154
|
+
const describeSuffix = getJestTestName(state.currentDescribeBlock)
|
|
155
|
+
return removeEfdStringFromTestName(`${describeSuffix} ${event.testName}`).trim()
|
|
104
156
|
}
|
|
105
157
|
|
|
106
158
|
async handleTestEvent (event, state) {
|
|
@@ -124,23 +176,56 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
124
176
|
}
|
|
125
177
|
}
|
|
126
178
|
if (event.name === 'test_start') {
|
|
179
|
+
let isNewTest = false
|
|
180
|
+
let numEfdRetry = null
|
|
127
181
|
const testParameters = getTestParametersString(this.nameToParams, event.test.name)
|
|
128
182
|
// Async resource for this test is created here
|
|
129
183
|
// It is used later on by the test_done handler
|
|
130
184
|
const asyncResource = new AsyncResource('bound-anonymous-fn')
|
|
131
185
|
asyncResources.set(event.test, asyncResource)
|
|
186
|
+
const testName = getJestTestName(event.test)
|
|
187
|
+
|
|
188
|
+
if (this.isEarlyFlakeDetectionEnabled) {
|
|
189
|
+
const originalTestName = removeEfdStringFromTestName(testName)
|
|
190
|
+
isNewTest = retriedTestsToNumAttempts.has(originalTestName)
|
|
191
|
+
if (isNewTest) {
|
|
192
|
+
numEfdRetry = retriedTestsToNumAttempts.get(originalTestName)
|
|
193
|
+
retriedTestsToNumAttempts.set(originalTestName, numEfdRetry + 1)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
132
197
|
asyncResource.runInAsyncScope(() => {
|
|
133
198
|
testStartCh.publish({
|
|
134
|
-
name:
|
|
199
|
+
name: removeEfdStringFromTestName(testName),
|
|
135
200
|
suite: this.testSuite,
|
|
201
|
+
testSourceFile: this.testSourceFile,
|
|
136
202
|
runner: 'jest-circus',
|
|
137
203
|
testParameters,
|
|
138
|
-
frameworkVersion: jestVersion
|
|
204
|
+
frameworkVersion: jestVersion,
|
|
205
|
+
isNew: isNewTest,
|
|
206
|
+
isEfdRetry: numEfdRetry > 0
|
|
139
207
|
})
|
|
140
208
|
originalTestFns.set(event.test, event.test.fn)
|
|
141
209
|
event.test.fn = asyncResource.bind(event.test.fn)
|
|
142
210
|
})
|
|
143
211
|
}
|
|
212
|
+
if (event.name === 'add_test') {
|
|
213
|
+
if (this.isEarlyFlakeDetectionEnabled) {
|
|
214
|
+
const testName = this.getTestNameFromAddTestEvent(event, state)
|
|
215
|
+
const isNew = !this.knownTestsForThisSuite?.includes(testName)
|
|
216
|
+
const isSkipped = event.mode === 'todo' || event.mode === 'skip'
|
|
217
|
+
if (isNew && !isSkipped && !retriedTestsToNumAttempts.has(testName)) {
|
|
218
|
+
retriedTestsToNumAttempts.set(testName, 0)
|
|
219
|
+
for (let retryIndex = 0; retryIndex < earlyFlakeDetectionNumRetries; retryIndex++) {
|
|
220
|
+
if (this.global.test) {
|
|
221
|
+
this.global.test(getEfdTestName(event.testName, retryIndex), event.fn, event.timeout)
|
|
222
|
+
} else {
|
|
223
|
+
log.error('Early flake detection could not retry test because global.test is undefined')
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
144
229
|
if (event.name === 'test_done') {
|
|
145
230
|
const asyncResource = asyncResources.get(event.test)
|
|
146
231
|
asyncResource.runInAsyncScope(() => {
|
|
@@ -164,6 +249,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
164
249
|
testSkippedCh.publish({
|
|
165
250
|
name: getJestTestName(event.test),
|
|
166
251
|
suite: this.testSuite,
|
|
252
|
+
testSourceFile: this.testSourceFile,
|
|
167
253
|
runner: 'jest-circus',
|
|
168
254
|
frameworkVersion: jestVersion,
|
|
169
255
|
testStartLine: getTestLineStart(event.test.asyncError, this.testSuite)
|
|
@@ -206,7 +292,7 @@ addHook({
|
|
|
206
292
|
}
|
|
207
293
|
// TODO: could we get the rootDir from each test?
|
|
208
294
|
const [test] = shardedTests
|
|
209
|
-
const rootDir = test
|
|
295
|
+
const rootDir = test?.context?.config?.rootDir
|
|
210
296
|
|
|
211
297
|
const jestSuitesToRun = getJestSuitesToRun(skippableSuites, shardedTests, rootDir || process.cwd())
|
|
212
298
|
|
|
@@ -234,24 +320,45 @@ function cliWrapper (cli, jestVersion) {
|
|
|
234
320
|
const configurationPromise = new Promise((resolve) => {
|
|
235
321
|
onDone = resolve
|
|
236
322
|
})
|
|
237
|
-
if (!
|
|
323
|
+
if (!libraryConfigurationCh.hasSubscribers) {
|
|
238
324
|
return runCLI.apply(this, arguments)
|
|
239
325
|
}
|
|
240
326
|
|
|
241
327
|
sessionAsyncResource.runInAsyncScope(() => {
|
|
242
|
-
|
|
328
|
+
libraryConfigurationCh.publish({ onDone })
|
|
243
329
|
})
|
|
244
330
|
|
|
245
331
|
try {
|
|
246
|
-
const { err,
|
|
332
|
+
const { err, libraryConfig } = await configurationPromise
|
|
247
333
|
if (!err) {
|
|
248
|
-
isCodeCoverageEnabled =
|
|
249
|
-
isSuitesSkippingEnabled =
|
|
334
|
+
isCodeCoverageEnabled = libraryConfig.isCodeCoverageEnabled
|
|
335
|
+
isSuitesSkippingEnabled = libraryConfig.isSuitesSkippingEnabled
|
|
336
|
+
isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
|
|
337
|
+
earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
|
|
250
338
|
}
|
|
251
339
|
} catch (err) {
|
|
252
340
|
log.error(err)
|
|
253
341
|
}
|
|
254
342
|
|
|
343
|
+
if (isEarlyFlakeDetectionEnabled) {
|
|
344
|
+
const knownTestsPromise = new Promise((resolve) => {
|
|
345
|
+
onDone = resolve
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
sessionAsyncResource.runInAsyncScope(() => {
|
|
349
|
+
knownTestsCh.publish({ onDone })
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
const { err, knownTests: receivedKnownTests } = await knownTestsPromise
|
|
354
|
+
if (!err) {
|
|
355
|
+
knownTests = receivedKnownTests
|
|
356
|
+
}
|
|
357
|
+
} catch (err) {
|
|
358
|
+
log.error(err)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
255
362
|
if (isSuitesSkippingEnabled) {
|
|
256
363
|
const skippableSuitesPromise = new Promise((resolve) => {
|
|
257
364
|
onDone = resolve
|
|
@@ -311,6 +418,21 @@ function cliWrapper (cli, jestVersion) {
|
|
|
311
418
|
status = 'fail'
|
|
312
419
|
error = new Error(`Failed test suites: ${numFailedTestSuites}. Failed tests: ${numFailedTests}`)
|
|
313
420
|
}
|
|
421
|
+
let timeoutId
|
|
422
|
+
|
|
423
|
+
// Pass the resolve callback to defer it to DC listener
|
|
424
|
+
const flushPromise = new Promise((resolve) => {
|
|
425
|
+
onDone = () => {
|
|
426
|
+
clearTimeout(timeoutId)
|
|
427
|
+
resolve()
|
|
428
|
+
}
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
432
|
+
timeoutId = setTimeout(() => {
|
|
433
|
+
resolve('timeout')
|
|
434
|
+
}, FLUSH_TIMEOUT).unref()
|
|
435
|
+
})
|
|
314
436
|
|
|
315
437
|
sessionAsyncResource.runInAsyncScope(() => {
|
|
316
438
|
testSessionFinishCh.publish({
|
|
@@ -322,9 +444,16 @@ function cliWrapper (cli, jestVersion) {
|
|
|
322
444
|
numSkippedSuites,
|
|
323
445
|
hasUnskippableSuites,
|
|
324
446
|
hasForcedToRunSuites,
|
|
325
|
-
error
|
|
447
|
+
error,
|
|
448
|
+
isEarlyFlakeDetectionEnabled,
|
|
449
|
+
onDone
|
|
326
450
|
})
|
|
327
451
|
})
|
|
452
|
+
const waitingResult = await Promise.race([flushPromise, timeoutPromise])
|
|
453
|
+
|
|
454
|
+
if (waitingResult === 'timeout') {
|
|
455
|
+
log.error('Timeout waiting for the tracer to flush')
|
|
456
|
+
}
|
|
328
457
|
|
|
329
458
|
numSkippedSuites = 0
|
|
330
459
|
|
|
@@ -438,10 +567,15 @@ function configureTestEnvironment (readConfigsResult) {
|
|
|
438
567
|
// because `jestAdapterWrapper` runs in a different process. We have to go through `testEnvironmentOptions`
|
|
439
568
|
configs.forEach(config => {
|
|
440
569
|
config.testEnvironmentOptions._ddTestCodeCoverageEnabled = isCodeCoverageEnabled
|
|
570
|
+
config.testEnvironmentOptions._ddKnownTests = knownTests
|
|
441
571
|
})
|
|
442
572
|
|
|
443
573
|
isUserCodeCoverageEnabled = !!readConfigsResult.globalConfig.collectCoverage
|
|
444
574
|
|
|
575
|
+
if (readConfigsResult.globalConfig.forceExit) {
|
|
576
|
+
log.warn("Jest's '--forceExit' flag has been passed. This may cause loss of data.")
|
|
577
|
+
}
|
|
578
|
+
|
|
445
579
|
if (isCodeCoverageEnabled) {
|
|
446
580
|
const globalConfig = {
|
|
447
581
|
...readConfigsResult.globalConfig,
|
|
@@ -498,6 +632,10 @@ addHook({
|
|
|
498
632
|
_ddForcedToRun,
|
|
499
633
|
_ddUnskippable,
|
|
500
634
|
_ddItrCorrelationId,
|
|
635
|
+
_ddKnownTests,
|
|
636
|
+
_ddIsEarlyFlakeDetectionEnabled,
|
|
637
|
+
_ddEarlyFlakeDetectionNumRetries,
|
|
638
|
+
_ddRepositoryRoot,
|
|
501
639
|
...restOfTestEnvironmentOptions
|
|
502
640
|
} = testEnvironmentOptions
|
|
503
641
|
|
|
@@ -11,7 +11,9 @@ const {
|
|
|
11
11
|
mergeCoverage,
|
|
12
12
|
getTestSuitePath,
|
|
13
13
|
fromCoverageMapToCoverage,
|
|
14
|
-
getCallSites
|
|
14
|
+
getCallSites,
|
|
15
|
+
addEfdStringToTestName,
|
|
16
|
+
removeEfdStringFromTestName
|
|
15
17
|
} = require('../../dd-trace/src/plugins/util/test')
|
|
16
18
|
|
|
17
19
|
const testStartCh = channel('ci:mocha:test:start')
|
|
@@ -20,7 +22,8 @@ const skipCh = channel('ci:mocha:test:skip')
|
|
|
20
22
|
const testFinishCh = channel('ci:mocha:test:finish')
|
|
21
23
|
const parameterizedTestCh = channel('ci:mocha:test:parameterize')
|
|
22
24
|
|
|
23
|
-
const
|
|
25
|
+
const libraryConfigurationCh = channel('ci:mocha:library-configuration')
|
|
26
|
+
const knownTestsCh = channel('ci:mocha:known-tests')
|
|
24
27
|
const skippableSuitesCh = channel('ci:mocha:test-suite:skippable')
|
|
25
28
|
|
|
26
29
|
const testSessionStartCh = channel('ci:mocha:session:start')
|
|
@@ -40,6 +43,7 @@ const testToAr = new WeakMap()
|
|
|
40
43
|
const originalFns = new WeakMap()
|
|
41
44
|
const testFileToSuiteAr = new Map()
|
|
42
45
|
const testToStartLine = new WeakMap()
|
|
46
|
+
const newTests = {}
|
|
43
47
|
|
|
44
48
|
// `isWorker` is true if it's a Mocha worker
|
|
45
49
|
let isWorker = false
|
|
@@ -54,6 +58,10 @@ let skippedSuites = []
|
|
|
54
58
|
const unskippableSuites = []
|
|
55
59
|
let isForcedToRun = false
|
|
56
60
|
let itrCorrelationId = ''
|
|
61
|
+
let isEarlyFlakeDetectionEnabled = false
|
|
62
|
+
let earlyFlakeDetectionNumRetries = 0
|
|
63
|
+
let isSuitesSkippingEnabled = false
|
|
64
|
+
let knownTests = []
|
|
57
65
|
|
|
58
66
|
function getSuitesByTestFile (root) {
|
|
59
67
|
const suitesByTestFile = {}
|
|
@@ -93,6 +101,26 @@ function isRetry (test) {
|
|
|
93
101
|
return test._currentRetry !== undefined && test._currentRetry !== 0
|
|
94
102
|
}
|
|
95
103
|
|
|
104
|
+
function getTestFullName (test) {
|
|
105
|
+
return `mocha.${getTestSuitePath(test.file, process.cwd())}.${removeEfdStringFromTestName(test.fullTitle())}`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isNewTest (test) {
|
|
109
|
+
return !knownTests.includes(getTestFullName(test))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function retryTest (test) {
|
|
113
|
+
const originalTestName = test.title
|
|
114
|
+
const suite = test.parent
|
|
115
|
+
for (let retryIndex = 0; retryIndex < earlyFlakeDetectionNumRetries; retryIndex++) {
|
|
116
|
+
const clonedTest = test.clone()
|
|
117
|
+
clonedTest.title = addEfdStringToTestName(originalTestName, retryIndex + 1)
|
|
118
|
+
suite.addTest(clonedTest)
|
|
119
|
+
clonedTest._ddIsNew = true
|
|
120
|
+
clonedTest._ddIsEfdRetry = true
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
96
124
|
function getTestAsyncResource (test) {
|
|
97
125
|
if (!test.fn) {
|
|
98
126
|
return testToAr.get(test)
|
|
@@ -123,6 +151,19 @@ function mochaHook (Runner) {
|
|
|
123
151
|
|
|
124
152
|
patched.add(Runner)
|
|
125
153
|
|
|
154
|
+
shimmer.wrap(Runner.prototype, 'runTests', runTests => function (suite, fn) {
|
|
155
|
+
if (isEarlyFlakeDetectionEnabled) {
|
|
156
|
+
// by the time we reach `this.on('test')`, it is too late. We need to add retries here
|
|
157
|
+
suite.tests.forEach(test => {
|
|
158
|
+
if (!test.isPending() && isNewTest(test)) {
|
|
159
|
+
test._ddIsNew = true
|
|
160
|
+
retryTest(test)
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
return runTests.apply(this, arguments)
|
|
165
|
+
})
|
|
166
|
+
|
|
126
167
|
shimmer.wrap(Runner.prototype, 'run', run => function () {
|
|
127
168
|
if (!testStartCh.hasSubscribers || isWorker) {
|
|
128
169
|
return run.apply(this, arguments)
|
|
@@ -144,6 +185,24 @@ function mochaHook (Runner) {
|
|
|
144
185
|
status = 'fail'
|
|
145
186
|
}
|
|
146
187
|
|
|
188
|
+
if (isEarlyFlakeDetectionEnabled) {
|
|
189
|
+
/**
|
|
190
|
+
* If Early Flake Detection (EFD) is enabled the logic is as follows:
|
|
191
|
+
* - If all attempts for a test are failing, the test has failed and we will let the test process fail.
|
|
192
|
+
* - If just a single attempt passes, we will prevent the test process from failing.
|
|
193
|
+
* The rationale behind is the following: you may still be able to block your CI pipeline by gating
|
|
194
|
+
* on flakiness (the test will be considered flaky), but you may choose to unblock the pipeline too.
|
|
195
|
+
*/
|
|
196
|
+
for (const tests of Object.values(newTests)) {
|
|
197
|
+
const failingNewTests = tests.filter(test => test.isFailed())
|
|
198
|
+
const areAllNewTestsFailing = failingNewTests.length === tests.length
|
|
199
|
+
if (failingNewTests.length && !areAllNewTestsFailing) {
|
|
200
|
+
this.stats.failures -= failingNewTests.length
|
|
201
|
+
this.failures -= failingNewTests.length
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
147
206
|
if (status === 'fail') {
|
|
148
207
|
error = new Error(`Failed tests: ${this.failures}.`)
|
|
149
208
|
}
|
|
@@ -168,7 +227,8 @@ function mochaHook (Runner) {
|
|
|
168
227
|
numSkippedSuites: skippedSuites.length,
|
|
169
228
|
hasForcedToRunSuites: isForcedToRun,
|
|
170
229
|
hasUnskippableSuites: !!unskippableSuites.length,
|
|
171
|
-
error
|
|
230
|
+
error,
|
|
231
|
+
isEarlyFlakeDetectionEnabled
|
|
172
232
|
})
|
|
173
233
|
}))
|
|
174
234
|
|
|
@@ -253,8 +313,35 @@ function mochaHook (Runner) {
|
|
|
253
313
|
const testStartLine = testToStartLine.get(test)
|
|
254
314
|
const asyncResource = new AsyncResource('bound-anonymous-fn')
|
|
255
315
|
testToAr.set(test.fn, asyncResource)
|
|
316
|
+
|
|
317
|
+
const {
|
|
318
|
+
file: testSuiteAbsolutePath,
|
|
319
|
+
title,
|
|
320
|
+
_ddIsNew: isNew,
|
|
321
|
+
_ddIsEfdRetry: isEfdRetry
|
|
322
|
+
} = test
|
|
323
|
+
|
|
324
|
+
const testInfo = {
|
|
325
|
+
testName: test.fullTitle(),
|
|
326
|
+
testSuiteAbsolutePath,
|
|
327
|
+
title,
|
|
328
|
+
isNew,
|
|
329
|
+
isEfdRetry,
|
|
330
|
+
testStartLine
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// We want to store the result of the new tests
|
|
334
|
+
if (isNew) {
|
|
335
|
+
const testFullName = getTestFullName(test)
|
|
336
|
+
if (newTests[testFullName]) {
|
|
337
|
+
newTests[testFullName].push(test)
|
|
338
|
+
} else {
|
|
339
|
+
newTests[testFullName] = [test]
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
256
343
|
asyncResource.runInAsyncScope(() => {
|
|
257
|
-
testStartCh.publish(
|
|
344
|
+
testStartCh.publish(testInfo)
|
|
258
345
|
})
|
|
259
346
|
})
|
|
260
347
|
|
|
@@ -323,10 +410,23 @@ function mochaHook (Runner) {
|
|
|
323
410
|
})
|
|
324
411
|
|
|
325
412
|
this.on('pending', (test) => {
|
|
413
|
+
const testStartLine = testToStartLine.get(test)
|
|
414
|
+
const {
|
|
415
|
+
file: testSuiteAbsolutePath,
|
|
416
|
+
title
|
|
417
|
+
} = test
|
|
418
|
+
|
|
419
|
+
const testInfo = {
|
|
420
|
+
testName: test.fullTitle(),
|
|
421
|
+
testSuiteAbsolutePath,
|
|
422
|
+
title,
|
|
423
|
+
testStartLine
|
|
424
|
+
}
|
|
425
|
+
|
|
326
426
|
const asyncResource = getTestAsyncResource(test)
|
|
327
427
|
if (asyncResource) {
|
|
328
428
|
asyncResource.runInAsyncScope(() => {
|
|
329
|
-
skipCh.publish(
|
|
429
|
+
skipCh.publish(testInfo)
|
|
330
430
|
})
|
|
331
431
|
} else {
|
|
332
432
|
// if there is no async resource, the test has been skipped through `test.skip`
|
|
@@ -338,7 +438,7 @@ function mochaHook (Runner) {
|
|
|
338
438
|
testToAr.set(test, skippedTestAsyncResource)
|
|
339
439
|
}
|
|
340
440
|
skippedTestAsyncResource.runInAsyncScope(() => {
|
|
341
|
-
skipCh.publish(
|
|
441
|
+
skipCh.publish(testInfo)
|
|
342
442
|
})
|
|
343
443
|
}
|
|
344
444
|
})
|
|
@@ -358,8 +458,8 @@ function mochaEachHook (mochaEach) {
|
|
|
358
458
|
const [params] = arguments
|
|
359
459
|
const { it, ...rest } = mochaEach.apply(this, arguments)
|
|
360
460
|
return {
|
|
361
|
-
it: function (
|
|
362
|
-
parameterizedTestCh.publish({
|
|
461
|
+
it: function (title) {
|
|
462
|
+
parameterizedTestCh.publish({ title, params })
|
|
363
463
|
it.apply(this, arguments)
|
|
364
464
|
},
|
|
365
465
|
...rest
|
|
@@ -384,7 +484,7 @@ addHook({
|
|
|
384
484
|
return run.apply(this, arguments)
|
|
385
485
|
}
|
|
386
486
|
|
|
387
|
-
if (!
|
|
487
|
+
if (!libraryConfigurationCh.hasSubscribers || this.isWorker) {
|
|
388
488
|
if (this.isWorker) {
|
|
389
489
|
isWorker = true
|
|
390
490
|
}
|
|
@@ -425,21 +525,47 @@ addHook({
|
|
|
425
525
|
global.run()
|
|
426
526
|
}
|
|
427
527
|
|
|
428
|
-
const
|
|
528
|
+
const onReceivedKnownTests = ({ err, knownTests: receivedKnownTests }) => {
|
|
429
529
|
if (err) {
|
|
430
|
-
|
|
530
|
+
knownTests = []
|
|
531
|
+
isEarlyFlakeDetectionEnabled = false
|
|
532
|
+
} else {
|
|
533
|
+
knownTests = receivedKnownTests
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (isSuitesSkippingEnabled) {
|
|
537
|
+
skippableSuitesCh.publish({
|
|
538
|
+
onDone: mochaRunAsyncResource.bind(onReceivedSkippableSuites)
|
|
539
|
+
})
|
|
540
|
+
} else {
|
|
541
|
+
global.run()
|
|
431
542
|
}
|
|
432
|
-
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const onReceivedConfiguration = ({ err, libraryConfig }) => {
|
|
546
|
+
if (err || !skippableSuitesCh.hasSubscribers || !knownTestsCh.hasSubscribers) {
|
|
433
547
|
return global.run()
|
|
434
548
|
}
|
|
435
549
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
550
|
+
isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
|
|
551
|
+
isSuitesSkippingEnabled = libraryConfig.isSuitesSkippingEnabled
|
|
552
|
+
earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
|
|
553
|
+
|
|
554
|
+
if (isEarlyFlakeDetectionEnabled) {
|
|
555
|
+
knownTestsCh.publish({
|
|
556
|
+
onDone: mochaRunAsyncResource.bind(onReceivedKnownTests)
|
|
557
|
+
})
|
|
558
|
+
} else if (isSuitesSkippingEnabled) {
|
|
559
|
+
skippableSuitesCh.publish({
|
|
560
|
+
onDone: mochaRunAsyncResource.bind(onReceivedSkippableSuites)
|
|
561
|
+
})
|
|
562
|
+
} else {
|
|
563
|
+
global.run()
|
|
564
|
+
}
|
|
439
565
|
}
|
|
440
566
|
|
|
441
567
|
mochaRunAsyncResource.runInAsyncScope(() => {
|
|
442
|
-
|
|
568
|
+
libraryConfigurationCh.publish({
|
|
443
569
|
onDone: mochaRunAsyncResource.bind(onReceivedConfiguration)
|
|
444
570
|
})
|
|
445
571
|
})
|
|
@@ -79,21 +79,26 @@ addHook({
|
|
|
79
79
|
})
|
|
80
80
|
|
|
81
81
|
let callbackWrapped = false
|
|
82
|
-
const lastArgumentIndex = arguments.length - 1
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
shimmer.wrap(arguments, lastArgumentIndex, originalCb => {
|
|
87
|
-
return function () {
|
|
88
|
-
finish()
|
|
83
|
+
const wrapCallbackIfExist = (args) => {
|
|
84
|
+
const lastArgumentIndex = args.length - 1
|
|
89
85
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
if (typeof args[lastArgumentIndex] === 'function') {
|
|
87
|
+
// is a callback, wrap it to execute finish()
|
|
88
|
+
shimmer.wrap(args, lastArgumentIndex, originalCb => {
|
|
89
|
+
return function () {
|
|
90
|
+
finish()
|
|
91
|
+
|
|
92
|
+
return originalCb.apply(this, arguments)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
93
95
|
|
|
94
|
-
|
|
96
|
+
callbackWrapped = true
|
|
97
|
+
}
|
|
95
98
|
}
|
|
96
99
|
|
|
100
|
+
wrapCallbackIfExist(arguments)
|
|
101
|
+
|
|
97
102
|
return asyncResource.runInAsyncScope(() => {
|
|
98
103
|
startCh.publish({
|
|
99
104
|
filters,
|
|
@@ -106,8 +111,16 @@ addHook({
|
|
|
106
111
|
if (!callbackWrapped) {
|
|
107
112
|
shimmer.wrap(res, 'exec', originalExec => {
|
|
108
113
|
return function wrappedExec () {
|
|
114
|
+
if (!callbackWrapped) {
|
|
115
|
+
wrapCallbackIfExist(arguments)
|
|
116
|
+
}
|
|
117
|
+
|
|
109
118
|
const execResult = originalExec.apply(this, arguments)
|
|
110
119
|
|
|
120
|
+
if (callbackWrapped || typeof execResult?.then !== 'function') {
|
|
121
|
+
return execResult
|
|
122
|
+
}
|
|
123
|
+
|
|
111
124
|
// wrap them method, wrap resolve and reject methods
|
|
112
125
|
shimmer.wrap(execResult, 'then', originalThen => {
|
|
113
126
|
return function wrappedThen () {
|
|
@@ -290,9 +290,23 @@ addHook({
|
|
|
290
290
|
shimmer.massWrap(request.NextRequest.prototype, ['text', 'json'], function (originalMethod) {
|
|
291
291
|
return async function wrappedJson () {
|
|
292
292
|
const body = await originalMethod.apply(this, arguments)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
293
|
+
|
|
294
|
+
bodyParsedChannel.publish({ body })
|
|
295
|
+
|
|
296
|
+
return body
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
shimmer.wrap(request.NextRequest.prototype, 'formData', function (originalFormData) {
|
|
301
|
+
return async function wrappedFormData () {
|
|
302
|
+
const body = await originalFormData.apply(this, arguments)
|
|
303
|
+
|
|
304
|
+
let normalizedBody = body
|
|
305
|
+
if (typeof body.entries === 'function') {
|
|
306
|
+
normalizedBody = Object.fromEntries(body.entries())
|
|
307
|
+
}
|
|
308
|
+
bodyParsedChannel.publish({ body: normalizedBody })
|
|
309
|
+
|
|
296
310
|
return body
|
|
297
311
|
}
|
|
298
312
|
})
|