dd-trace 6.8.0 → 6.10.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.
Files changed (209) hide show
  1. package/ci/diagnose.js +27 -25
  2. package/ci/init.js +6 -6
  3. package/ci/runbook.md +114 -20
  4. package/ci/test-optimization-validation/approval-artifacts.js +36 -3
  5. package/ci/test-optimization-validation/approval.js +78 -22
  6. package/ci/test-optimization-validation/blocker-category.js +24 -0
  7. package/ci/test-optimization-validation/ci-discovery.js +23 -17
  8. package/ci/test-optimization-validation/ci-package-scripts.js +34 -6
  9. package/ci/test-optimization-validation/ci-remediation.js +1 -1
  10. package/ci/test-optimization-validation/cli.js +280 -52
  11. package/ci/test-optimization-validation/command-blocker.js +159 -20
  12. package/ci/test-optimization-validation/command-output-policy.js +9 -3
  13. package/ci/test-optimization-validation/command-runner.js +9 -7
  14. package/ci/test-optimization-validation/environment.js +4 -2
  15. package/ci/test-optimization-validation/executable.js +15 -13
  16. package/ci/test-optimization-validation/execution-lock.js +70 -0
  17. package/ci/test-optimization-validation/framework-adapters/cucumber-validation.json +3 -0
  18. package/ci/test-optimization-validation/framework-adapters/cucumber.js +657 -5
  19. package/ci/test-optimization-validation/framework-adapters/vitest.js +517 -0
  20. package/ci/test-optimization-validation/generated-files.js +38 -28
  21. package/ci/test-optimization-validation/generated-test-contract.js +8 -4
  22. package/ci/test-optimization-validation/generated-verifier.js +24 -17
  23. package/ci/test-optimization-validation/literal-glob.js +52 -0
  24. package/ci/test-optimization-validation/manifest-loader.js +3 -0
  25. package/ci/test-optimization-validation/manifest-scaffold.js +300 -50
  26. package/ci/test-optimization-validation/manifest-schema.js +80 -17
  27. package/ci/test-optimization-validation/offline-fixtures.js +13 -4
  28. package/ci/test-optimization-validation/package-check.js +94 -0
  29. package/ci/test-optimization-validation/plan-writer.js +183 -10
  30. package/ci/test-optimization-validation/preflight-runner.js +110 -31
  31. package/ci/test-optimization-validation/project-build-artifact.js +31 -0
  32. package/ci/test-optimization-validation/redaction.js +18 -27
  33. package/ci/test-optimization-validation/report-writer.js +182 -48
  34. package/ci/test-optimization-validation/result-semantics.js +20 -3
  35. package/ci/test-optimization-validation/runner-command.js +40 -20
  36. package/ci/test-optimization-validation/runner-contract.js +79 -43
  37. package/ci/test-optimization-validation/scenarios/basic-reporting.js +59 -5
  38. package/ci/test-optimization-validation/scenarios/ci-wiring.js +141 -35
  39. package/ci/test-optimization-validation/scenarios/helpers.js +30 -6
  40. package/ci/test-optimization-validation/static-diagnosis.js +10 -3
  41. package/ci/test-optimization-validation/test-output.js +2 -1
  42. package/ci/test-optimization-validation/validation-blocker.js +21 -0
  43. package/ci/vitest-no-worker-init-setup.mjs +140 -42
  44. package/index.d.ts +140 -1
  45. package/init.js +1 -17
  46. package/initialize.mjs +11 -0
  47. package/loader-hook.mjs +6 -4
  48. package/package.json +8 -5
  49. package/packages/datadog-core/src/utils/src/parse-tags.js +0 -1
  50. package/packages/datadog-esbuild/src/utils.js +16 -1
  51. package/packages/datadog-instrumentations/src/ai.js +61 -34
  52. package/packages/datadog-instrumentations/src/bluebird.js +6 -6
  53. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +81 -5
  54. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
  55. package/packages/datadog-instrumentations/src/cucumber.js +124 -47
  56. package/packages/datadog-instrumentations/src/dns.js +21 -2
  57. package/packages/datadog-instrumentations/src/electron.js +1 -0
  58. package/packages/datadog-instrumentations/src/fastify.js +3 -1
  59. package/packages/datadog-instrumentations/src/grpc/client.js +4 -2
  60. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +3 -1
  61. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  62. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +629 -0
  63. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +34 -9
  64. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +199 -0
  65. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +20 -2
  66. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +32 -0
  67. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +62 -0
  68. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
  69. package/packages/datadog-instrumentations/src/helpers/router-helper.js +37 -6
  70. package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +1 -1
  71. package/packages/datadog-instrumentations/src/jest.js +980 -234
  72. package/packages/datadog-instrumentations/src/mocha/main.js +108 -10
  73. package/packages/datadog-instrumentations/src/mocha/utils.js +66 -50
  74. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +25 -0
  75. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -18
  76. package/packages/datadog-instrumentations/src/mysql.js +46 -16
  77. package/packages/datadog-instrumentations/src/mysql2.js +119 -18
  78. package/packages/datadog-instrumentations/src/next.js +155 -23
  79. package/packages/datadog-instrumentations/src/openai.js +14 -0
  80. package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
  81. package/packages/datadog-instrumentations/src/pg.js +209 -11
  82. package/packages/datadog-instrumentations/src/playwright.js +173 -79
  83. package/packages/datadog-instrumentations/src/promise-js.js +4 -4
  84. package/packages/datadog-instrumentations/src/promise.js +3 -3
  85. package/packages/datadog-instrumentations/src/router.js +195 -19
  86. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +85 -13
  87. package/packages/datadog-instrumentations/src/vitest-main.js +528 -72
  88. package/packages/datadog-instrumentations/src/vitest-util.js +106 -12
  89. package/packages/datadog-instrumentations/src/vitest-worker.js +382 -40
  90. package/packages/datadog-instrumentations/src/webdriverio.js +212 -22
  91. package/packages/datadog-instrumentations/src/when.js +3 -3
  92. package/packages/datadog-instrumentations/src/ws.js +5 -1
  93. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
  94. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +59 -48
  95. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +2 -5
  96. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
  97. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +1 -3
  98. package/packages/datadog-plugin-cucumber/src/index.js +5 -1
  99. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +51 -47
  100. package/packages/datadog-plugin-cypress/src/support.js +7 -4
  101. package/packages/datadog-plugin-electron/src/net.js +10 -4
  102. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
  103. package/packages/datadog-plugin-http/src/client.js +1 -1
  104. package/packages/datadog-plugin-http2/src/client.js +1 -1
  105. package/packages/datadog-plugin-jest/src/index.js +6 -22
  106. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
  107. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
  108. package/packages/datadog-plugin-kafkajs/src/producer.js +5 -3
  109. package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
  110. package/packages/datadog-plugin-mocha/src/index.js +832 -4
  111. package/packages/datadog-plugin-mysql/src/index.js +37 -0
  112. package/packages/datadog-plugin-next/src/index.js +51 -21
  113. package/packages/datadog-plugin-openai/src/tracing.js +4 -0
  114. package/packages/datadog-plugin-openai-agents/src/integration.js +278 -69
  115. package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
  116. package/packages/datadog-plugin-pg/src/index.js +66 -1
  117. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  118. package/packages/datadog-plugin-vitest/src/index.js +103 -27
  119. package/packages/datadog-plugin-ws/src/util.js +2 -1
  120. package/packages/datadog-shimmer/src/shimmer.js +6 -2
  121. package/packages/dd-trace/src/aiguard/sdk.js +3 -0
  122. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
  123. package/packages/dd-trace/src/appsec/{blocking.js → blocking/index.js} +26 -6
  124. package/packages/dd-trace/src/appsec/handlers/auth.js +62 -0
  125. package/packages/dd-trace/src/appsec/handlers/http-request.js +228 -0
  126. package/packages/dd-trace/src/appsec/handlers/http-response.js +78 -0
  127. package/packages/dd-trace/src/appsec/handlers/http-shared.js +24 -0
  128. package/packages/dd-trace/src/appsec/handlers/payments.js +104 -0
  129. package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +1 -1
  130. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
  131. package/packages/dd-trace/src/appsec/index.js +48 -476
  132. package/packages/dd-trace/src/appsec/sdk/track_event.js +9 -0
  133. package/packages/dd-trace/src/carrier.js +356 -0
  134. package/packages/dd-trace/src/ci-visibility/coverage-report-discovery.js +46 -14
  135. package/packages/dd-trace/src/ci-visibility/efd-retry-policy.js +89 -0
  136. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +27 -13
  137. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +17 -0
  138. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +21 -1
  139. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +22 -7
  140. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -14
  141. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +21 -5
  142. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +1 -1
  143. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +5 -1
  144. package/packages/dd-trace/src/config/generated-config-types.d.ts +5 -0
  145. package/packages/dd-trace/src/config/helper.js +2 -0
  146. package/packages/dd-trace/src/config/index.js +16 -5
  147. package/packages/dd-trace/src/config/remote_config.js +1 -1
  148. package/packages/dd-trace/src/config/supported-configurations.json +18 -0
  149. package/packages/dd-trace/src/datastreams/context.js +6 -1
  150. package/packages/dd-trace/src/datastreams/manager.js +5 -1
  151. package/packages/dd-trace/src/datastreams/pathway.js +23 -22
  152. package/packages/dd-trace/src/datastreams/processor.js +3 -2
  153. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  154. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +0 -1
  155. package/packages/dd-trace/src/guardrails/apply-pm2-env.js +16 -0
  156. package/packages/dd-trace/src/guardrails/log.js +1 -10
  157. package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
  158. package/packages/dd-trace/src/llmobs/eval-metric.js +249 -0
  159. package/packages/dd-trace/src/llmobs/experiments/index.js +33 -25
  160. package/packages/dd-trace/src/llmobs/experiments/util.js +4 -2
  161. package/packages/dd-trace/src/llmobs/index.js +34 -2
  162. package/packages/dd-trace/src/llmobs/noop.js +2 -0
  163. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +1 -1
  164. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -1
  165. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +5 -5
  166. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +1 -1
  167. package/packages/dd-trace/src/llmobs/sdk.js +149 -68
  168. package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
  169. package/packages/dd-trace/src/llmobs/tagger.js +96 -0
  170. package/packages/dd-trace/src/llmobs/telemetry.js +18 -0
  171. package/packages/dd-trace/src/llmobs/util.js +78 -0
  172. package/packages/dd-trace/src/llmobs/writers/base.js +3 -0
  173. package/packages/dd-trace/src/llmobs/writers/util.js +2 -0
  174. package/packages/dd-trace/src/log/channels.js +1 -9
  175. package/packages/dd-trace/src/log/levels.js +12 -0
  176. package/packages/dd-trace/src/openfeature/writers/exposures.js +1 -1
  177. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  178. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +1 -1
  179. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +8 -4
  180. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -1
  181. package/packages/dd-trace/src/opentelemetry/span.js +4 -0
  182. package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
  183. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
  184. package/packages/dd-trace/src/plugin_manager.js +2 -0
  185. package/packages/dd-trace/src/plugins/ci_plugin.js +21 -5
  186. package/packages/dd-trace/src/plugins/index.js +1 -0
  187. package/packages/dd-trace/src/plugins/util/ci.js +6 -0
  188. package/packages/dd-trace/src/plugins/util/git.js +6 -4
  189. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +2 -2
  190. package/packages/dd-trace/src/plugins/util/ip_extractor.js +1 -1
  191. package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +3 -62
  192. package/packages/dd-trace/src/plugins/util/stacktrace.js +3 -0
  193. package/packages/dd-trace/src/plugins/util/test.js +397 -74
  194. package/packages/dd-trace/src/plugins/util/url.js +1 -1
  195. package/packages/dd-trace/src/plugins/util/web.js +76 -3
  196. package/packages/dd-trace/src/profiler.js +1 -1
  197. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +1 -1
  198. package/packages/dd-trace/src/profiling/profilers/events.js +2 -2
  199. package/packages/dd-trace/src/proxy.js +1 -0
  200. package/packages/dd-trace/src/ritm.js +5 -0
  201. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +4 -2
  202. package/packages/dd-trace/src/sampling_rule.js +4 -2
  203. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +1 -1
  204. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +1 -1
  205. package/packages/dd-trace/src/telemetry/logs/index.js +3 -0
  206. package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
  207. package/vendor/dist/pprof-format/index.js +1 -1
  208. package/version.js +10 -8
  209. /package/packages/dd-trace/src/appsec/{blocked_templates.js → blocking/templates.js} +0 -0
@@ -1,7 +1,21 @@
1
1
  'use strict'
2
2
 
3
+ const { performance } = require('node:perf_hooks')
4
+ const { fileURLToPath } = require('node:url')
5
+
6
+ const { channel } = require('dc-polyfill')
7
+
3
8
  const CiPlugin = require('../../dd-trace/src/plugins/ci_plugin')
4
9
  const { storage } = require('../../datadog-core')
10
+ const {
11
+ getEfdRetryCountForDuration,
12
+ hasEfdRetries,
13
+ } = require('../../dd-trace/src/ci-visibility/efd-retry-policy')
14
+ const log = require('../../dd-trace/src/log')
15
+ const {
16
+ sendWebdriverioWorkerMessage,
17
+ SUITE_FINISH,
18
+ } = require('../../datadog-instrumentations/src/mocha/webdriverio-protocol')
5
19
 
6
20
  const {
7
21
  TEST_STATUS,
@@ -35,6 +49,8 @@ const {
35
49
  TEST_FINAL_STATUS,
36
50
  TEST_HAS_DYNAMIC_NAME,
37
51
  TEST_FRAMEWORK_ADAPTER,
52
+ DYNAMIC_NAME_RE,
53
+ getFailedTestReplayPromise,
38
54
  getTestSuiteExecutionKey,
39
55
  isModifiedTest,
40
56
  } = require('../../dd-trace/src/plugins/util/test')
@@ -51,6 +67,104 @@ const {
51
67
  TELEMETRY_TEST_SESSION,
52
68
  } = require('../../dd-trace/src/ci-visibility/telemetry')
53
69
 
70
+ const jasmineAdapterRunAsyncEndCh = 'tracing:orchestrion:@wdio/jasmine-framework:JasmineAdapter_run:asyncEnd'
71
+ const jasmineDoneCh = 'ci:webdriverio:jasmine:done'
72
+ const jasmineExecuteAsyncEndCh = 'tracing:orchestrion:@wdio/utils:executeAsync:asyncEnd'
73
+ const jasmineExecuteAsyncErrorCh = 'tracing:orchestrion:@wdio/utils:executeAsync:error'
74
+ const jasmineExecuteAsyncStartCh = 'tracing:orchestrion:@wdio/utils:executeAsync:start'
75
+ const jasmineReporterSpecDoneEndCh = 'tracing:orchestrion:@wdio/jasmine-framework:JasmineReporter_specDone:end'
76
+ const jasmineReporterSpecDoneStartCh = 'tracing:orchestrion:@wdio/jasmine-framework:JasmineReporter_specDone:start'
77
+ const jasmineReporterSpecStartedEndCh = 'tracing:orchestrion:@wdio/jasmine-framework:JasmineReporter_specStarted:end'
78
+ const jasmineReporterSuiteDoneEndCh = 'tracing:orchestrion:@wdio/jasmine-framework:JasmineReporter_suiteDone:end'
79
+ const jasmineReporterSuiteStartedEndCh = 'tracing:orchestrion:@wdio/jasmine-framework:JasmineReporter_suiteStarted:end'
80
+ const jasmineSpecAttemptDoneEndCh = 'tracing:orchestrion:jasmine-core:Spec_attemptDone:end'
81
+ const jasmineSpecExecuteStartCh = 'tracing:orchestrion:jasmine-core:Spec_execute:start'
82
+ const jasmineTestFunctionStartCh = 'tracing:orchestrion:@wdio/utils:testFrameworkFnWrapper:start'
83
+ const testFinishCh = channel('ci:mocha:test:finish')
84
+ const testRetryCh = channel('ci:mocha:test:retry')
85
+ const WEBDRIVERIO_JASMINE_ADAPTER = 'jasmine'
86
+ const workerFinishCh = channel('ci:mocha:worker:finish')
87
+ const WEBDRIVERIO_JASMINE_FAILED_EXPECTATION_COUNT = Symbol('webdriverioJasmineFailedExpectationCount')
88
+ const WEBDRIVERIO_JASMINE_FUNCTION_TYPE = Symbol('webdriverioJasmineFunctionType')
89
+ const WEBDRIVERIO_JASMINE_TEST = Symbol('webdriverioJasmineTest')
90
+
91
+ /**
92
+ * @typedef {object} WebdriverioJasmineResult
93
+ * @property {string|undefined} id
94
+ * @property {string|undefined} description
95
+ * @property {Array<{message?: string, stack?: string}>|undefined} failedExpectations
96
+ * @property {string|undefined} file
97
+ * @property {string|undefined} filename
98
+ * @property {string|undefined} fullName
99
+ * @property {string|undefined} parentSuiteId
100
+ * @property {string|undefined} status
101
+ */
102
+
103
+ /**
104
+ * Normalizes a WebdriverIO Jasmine spec identifier to a filesystem path.
105
+ *
106
+ * @param {string|undefined} file
107
+ * @returns {string|undefined}
108
+ */
109
+ function normalizeJasmineFile (file) {
110
+ return file?.startsWith('file://') ? fileURLToPath(file) : file
111
+ }
112
+
113
+ /**
114
+ * Maps a Jasmine result status to the Test Optimization status vocabulary.
115
+ *
116
+ * @param {string|undefined} status
117
+ * @returns {'pass'|'fail'|'skip'}
118
+ */
119
+ function getJasmineStatus (status) {
120
+ if (status === 'passed') return 'pass'
121
+ if (status === 'failed') return 'fail'
122
+ return 'skip'
123
+ }
124
+
125
+ /**
126
+ * Converts Jasmine's failed-expectation shape to an Error.
127
+ *
128
+ * @param {WebdriverioJasmineResult} result
129
+ * @returns {Error|undefined}
130
+ */
131
+ function getJasmineError (result) {
132
+ const failedExpectation = result.failedExpectations?.[0]
133
+ if (!failedExpectation) return
134
+
135
+ const error = new Error(failedExpectation.message)
136
+ if (failedExpectation.stack) {
137
+ error.stack = failedExpectation.stack
138
+ }
139
+ return error
140
+ }
141
+
142
+ /**
143
+ * Resolves the spec file responsible for a run-level Jasmine failure.
144
+ *
145
+ * @param {WebdriverioJasmineResult|undefined} result
146
+ * @param {string[]} specs
147
+ * @returns {string|undefined}
148
+ */
149
+ function getJasmineFailureFile (result, specs) {
150
+ const resultFile = normalizeJasmineFile(result?.file || result?.filename)
151
+ if (resultFile) {
152
+ return resultFile
153
+ }
154
+
155
+ const stack = result?.failedExpectations?.[0]?.stack
156
+ if (stack) {
157
+ for (const spec of specs) {
158
+ const file = normalizeJasmineFile(spec)
159
+ if (file && stack.includes(file)) {
160
+ return file
161
+ }
162
+ }
163
+ }
164
+
165
+ return specs.length === 1 ? normalizeJasmineFile(specs[0]) : undefined
166
+ }
167
+
54
168
  class MochaPlugin extends CiPlugin {
55
169
  static id = 'mocha'
56
170
 
@@ -60,10 +174,195 @@ class MochaPlugin extends CiPlugin {
60
174
  this._testTitleToParams = {}
61
175
  this.sourceRoot = process.cwd()
62
176
 
63
- this.addSub('ci:mocha:worker:configuration', ({ libraryConfig, repositoryRoot, testFramework }) => {
177
+ this.addSub('ci:mocha:worker:configuration', ({
178
+ libraryConfig,
179
+ repositoryRoot,
180
+ specs,
181
+ testFramework,
182
+ testFrameworkAdapter,
183
+ }) => {
64
184
  this.libraryConfig = libraryConfig
65
185
  this.testFramework = testFramework
186
+ this.testFrameworkAdapter = testFrameworkAdapter
66
187
  this._setRepositoryRoot(repositoryRoot)
188
+ if (testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER) {
189
+ this._webdriverioJasmineState = {
190
+ completedTestStatuses: new Map(),
191
+ currentResult: undefined,
192
+ pendingTestFinishCallbacks: [],
193
+ pendingTestFinishes: 0,
194
+ specs: specs || [],
195
+ suiteErrors: new Map(),
196
+ suiteFiles: new Map(),
197
+ suiteStatuses: new Map(),
198
+ tests: new Map(),
199
+ }
200
+ }
201
+ })
202
+
203
+ this.addBind(jasmineTestFunctionStartCh, (ctx) => {
204
+ if (this.testFrameworkAdapter !== WEBDRIVERIO_JASMINE_ADAPTER) {
205
+ return storage('legacy').getStore()
206
+ }
207
+
208
+ const result = this._webdriverioJasmineState?.currentResult
209
+ const type = ctx.arguments?.[1]
210
+ if (type === 'Test' || (type === 'Hook' && result)) {
211
+ return this.#configureWebdriverioJasmineFunction(ctx, result, type)
212
+ }
213
+ return storage('legacy').getStore()
214
+ })
215
+
216
+ this.addBind(jasmineExecuteAsyncStartCh, (ctx) => {
217
+ const currentStore = storage('legacy').getStore()
218
+ const test = currentStore?.[WEBDRIVERIO_JASMINE_TEST]
219
+ if (this.testFrameworkAdapter !== WEBDRIVERIO_JASMINE_ADAPTER || !test) {
220
+ return currentStore
221
+ }
222
+
223
+ const functionType = currentStore[WEBDRIVERIO_JASMINE_FUNCTION_TYPE]
224
+ if (functionType === 'Test') {
225
+ ctx.retryCallback = error => this.#retryWebdriverioJasmineTest(test, error)
226
+ }
227
+ const nextStore = {
228
+ ...test.currentStore,
229
+ [WEBDRIVERIO_JASMINE_FUNCTION_TYPE]: functionType,
230
+ }
231
+ if (functionType === 'Hook') {
232
+ nextStore[WEBDRIVERIO_JASMINE_FAILED_EXPECTATION_COUNT] =
233
+ this._webdriverioJasmineState.currentResult?.failedExpectations?.length || 0
234
+ }
235
+ return nextStore
236
+ })
237
+
238
+ this.addBind(jasmineSpecExecuteStartCh, (ctx) => {
239
+ if (this.testFrameworkAdapter !== WEBDRIVERIO_JASMINE_ADAPTER) {
240
+ return storage('legacy').getStore()
241
+ }
242
+
243
+ this.#configureWebdriverioJasmineLifecycle(ctx)
244
+ return storage('legacy').getStore()
245
+ })
246
+
247
+ this.addSub(jasmineSpecAttemptDoneEndCh, (ctx) => {
248
+ if (this.testFrameworkAdapter !== WEBDRIVERIO_JASMINE_ADAPTER) {
249
+ return
250
+ }
251
+
252
+ const status = typeof ctx.result === 'string' ? ctx.result : ctx.self?.result?.status
253
+ const runnerStatus = this.#prepareWebdriverioJasmineAttempt(ctx.self, status)
254
+ if (typeof ctx.result === 'string') {
255
+ ctx.result = runnerStatus
256
+ } else if (ctx.self?.result) {
257
+ ctx.self.result.status = runnerStatus
258
+ }
259
+ })
260
+
261
+ this.addSub(jasmineExecuteAsyncErrorCh, (ctx) => {
262
+ const currentStore = ctx.currentStore || storage('legacy').getStore()
263
+ const test = currentStore?.[WEBDRIVERIO_JASMINE_TEST]
264
+ if (
265
+ this.testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER &&
266
+ test &&
267
+ currentStore[WEBDRIVERIO_JASMINE_FUNCTION_TYPE] === 'Hook'
268
+ ) {
269
+ test.hasFinalHookFailure = true
270
+ }
271
+ })
272
+
273
+ this.addSub(jasmineExecuteAsyncEndCh, (ctx) => {
274
+ const currentStore = ctx.currentStore || storage('legacy').getStore()
275
+ const test = currentStore?.[WEBDRIVERIO_JASMINE_TEST]
276
+ const failedExpectationCount = currentStore?.[WEBDRIVERIO_JASMINE_FAILED_EXPECTATION_COUNT]
277
+ if (
278
+ this.testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER &&
279
+ test &&
280
+ currentStore[WEBDRIVERIO_JASMINE_FUNCTION_TYPE] === 'Hook' &&
281
+ this._webdriverioJasmineState.currentResult?.failedExpectations?.length > failedExpectationCount
282
+ ) {
283
+ test.hasFinalHookFailure = true
284
+ }
285
+ })
286
+
287
+ this.addSub(jasmineReporterSuiteStartedEndCh, (ctx) => {
288
+ if (this.testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER) {
289
+ const suite = ctx.arguments?.[0]
290
+ const file = normalizeJasmineFile(suite?.filename)
291
+ if (suite?.id && file) {
292
+ this._webdriverioJasmineState.suiteFiles.set(suite.id, file)
293
+ }
294
+ }
295
+ })
296
+
297
+ this.addSub(jasmineReporterSuiteDoneEndCh, (ctx) => {
298
+ if (this.testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER) {
299
+ const state = this._webdriverioJasmineState
300
+ const suite = ctx.arguments?.[0]
301
+ const error = suite && getJasmineError(suite)
302
+ const file = normalizeJasmineFile(
303
+ suite?.filename ||
304
+ state.suiteFiles.get(suite?.id) ||
305
+ (state.specs.length === 1 ? state.specs[0] : undefined)
306
+ )
307
+ if (error && file) {
308
+ state.suiteErrors.set(file, error)
309
+ state.suiteStatuses.set(file, 'fail')
310
+ }
311
+ }
312
+ })
313
+
314
+ this.addSub(jasmineDoneCh, ({ result } = {}) => {
315
+ if (this.testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER) {
316
+ const state = this._webdriverioJasmineState
317
+ const error = result && getJasmineError(result)
318
+ const file = getJasmineFailureFile(result, state.specs)
319
+ if (error && file) {
320
+ state.suiteErrors.set(file, error)
321
+ state.suiteStatuses.set(file, 'fail')
322
+ }
323
+ }
324
+ })
325
+
326
+ this.addSub(jasmineReporterSpecStartedEndCh, (ctx) => {
327
+ if (this.testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER) {
328
+ this._webdriverioJasmineState.currentResult = ctx.arguments?.[0]
329
+ this.#startWebdriverioJasmineTest(ctx.arguments?.[0], ctx.self?._specs, ctx.self?.startedSuite)
330
+ }
331
+ })
332
+
333
+ this.addBind(jasmineReporterSpecDoneStartCh, (ctx) => {
334
+ if (this.testFrameworkAdapter !== WEBDRIVERIO_JASMINE_ADAPTER) {
335
+ return storage('legacy').getStore()
336
+ }
337
+
338
+ const result = ctx.arguments?.[0]
339
+ const test = this._webdriverioJasmineState?.tests.get(result?.id)
340
+ const recoveredEarlyFlakeDetection = test?.isEarlyFlakeDetection &&
341
+ !test.hasFinalHookFailure && test.statuses.includes('pass')
342
+ if (
343
+ result.status === 'failed' &&
344
+ ((test?.isQuarantined && !test.isAttemptToFix) || recoveredEarlyFlakeDetection)
345
+ ) {
346
+ test.reportedStatus = result.status
347
+ result.status = 'passed'
348
+ }
349
+ return test?.currentStore || storage('legacy').getStore()
350
+ })
351
+
352
+ this.addSub(jasmineReporterSpecDoneEndCh, (ctx) => {
353
+ if (this.testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER) {
354
+ const result = ctx.result
355
+ const finishPromise = this.#finishWebdriverioJasmineTest(ctx.arguments?.[0], ctx.self?._specs)
356
+ if (finishPromise) {
357
+ ctx.result = finishPromise.then(() => result)
358
+ }
359
+ }
360
+ })
361
+
362
+ this.addSub(jasmineAdapterRunAsyncEndCh, (ctx) => {
363
+ if (this.testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER) {
364
+ this.#finishWebdriverioJasmineWorker(ctx)
365
+ }
67
366
  })
68
367
 
69
368
  this.addSub('ci:mocha:test-suite:code-coverage', ({ coverageFiles, suiteFile }) => {
@@ -325,10 +624,13 @@ class MochaPlugin extends CiPlugin {
325
624
  this.addSub('ci:mocha:test:retry', ({
326
625
  span,
327
626
  isFirstAttempt,
627
+ isFirstFailure = isFirstAttempt,
328
628
  willBeRetried,
329
629
  err,
330
630
  test,
631
+ isAttemptToFixRetry,
331
632
  isAtrRetry,
633
+ isEfdRetry,
332
634
  promises,
333
635
  }) => {
334
636
  if (span) {
@@ -343,8 +645,12 @@ class MochaPlugin extends CiPlugin {
343
645
  span.setTag(TEST_STATUS, 'fail')
344
646
  if (!isFirstAttempt) {
345
647
  span.setTag(TEST_IS_RETRY, 'true')
346
- if (isAtrRetry) {
648
+ if (isAttemptToFixRetry) {
649
+ span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atf)
650
+ } else if (isAtrRetry) {
347
651
  span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atr)
652
+ } else if (isEfdRetry) {
653
+ span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.efd)
348
654
  } else {
349
655
  span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.ext)
350
656
  }
@@ -358,7 +664,7 @@ class MochaPlugin extends CiPlugin {
358
664
  'test',
359
665
  this.getTestTelemetryTags(span)
360
666
  )
361
- if (isFirstAttempt && willBeRetried && this.di && this.libraryConfig?.isDiEnabled) {
667
+ if (isFirstFailure && willBeRetried && this.di && this.libraryConfig?.isDiEnabled) {
362
668
  const probeInformation = this.addDiProbe(err)
363
669
  if (probeInformation) {
364
670
  const { file, line, stackIndex, setProbePromise } = probeInformation
@@ -372,7 +678,7 @@ class MochaPlugin extends CiPlugin {
372
678
  }
373
679
  }
374
680
 
375
- if (!isFirstAttempt &&
681
+ if (!isFirstFailure &&
376
682
  willBeRetried &&
377
683
  this.di &&
378
684
  this.libraryConfig?.isDiEnabled &&
@@ -484,6 +790,528 @@ class MochaPlugin extends CiPlugin {
484
790
  })
485
791
  }
486
792
 
793
+ /**
794
+ * Starts a Jasmine test span around WebdriverIO's test-function wrapper.
795
+ *
796
+ * @param {WebdriverioJasmineResult|undefined} result
797
+ * @param {string[]} [specs]
798
+ * @param {{filename?: string}|undefined} currentSuite
799
+ * @returns {object|undefined}
800
+ */
801
+ #startWebdriverioJasmineTest (result, specs, currentSuite) {
802
+ const state = this._webdriverioJasmineState
803
+ const currentStore = storage('legacy').getStore()
804
+ if (!state || !result?.id) {
805
+ return currentStore
806
+ }
807
+ if (state.completedTestStatuses.has(result.id)) {
808
+ return currentStore
809
+ }
810
+
811
+ const existingTest = state.tests.get(result.id)
812
+ if (existingTest) {
813
+ return existingTest.currentStore
814
+ }
815
+
816
+ const candidateSpecs = specs || state.specs
817
+ const testSuiteAbsolutePath = normalizeJasmineFile(
818
+ state.suiteFiles.get(result.parentSuiteId) ||
819
+ currentSuite?.filename ||
820
+ (candidateSpecs.length === 1 ? candidateSpecs[0] : undefined) ||
821
+ result.file ||
822
+ result.filename ||
823
+ candidateSpecs[0]
824
+ )
825
+ if (!testSuiteAbsolutePath) {
826
+ return currentStore
827
+ }
828
+
829
+ const testName = result.fullName || result.description
830
+ const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.sourceRoot)
831
+ const properties = this.libraryConfig?.testManagementTests?.mocha
832
+ ?.suites?.[testSuite]?.tests?.[testName]?.properties || {}
833
+ const isAttemptToFix = this.libraryConfig?.isTestManagementTestsEnabled && properties.attempt_to_fix
834
+ const isDisabled = this.libraryConfig?.isTestManagementTestsEnabled && properties.disabled
835
+ const isQuarantined = this.libraryConfig?.isTestManagementTestsEnabled && properties.quarantined
836
+ const isModified = this.libraryConfig?.isImpactedTestsEnabled && isModifiedTest(
837
+ getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot || this.sourceRoot),
838
+ null,
839
+ null,
840
+ this.libraryConfig.modifiedFiles,
841
+ this.constructor.id
842
+ )
843
+ const knownTests = this.libraryConfig?.knownTests?.mocha
844
+ const isNew = this.libraryConfig?.isKnownTestsEnabled && knownTests &&
845
+ !(knownTests[testSuite] || []).includes(testName)
846
+ const isEarlyFlakeDetection = this.libraryConfig?.isEarlyFlakeDetectionEnabled &&
847
+ hasEfdRetries(this.libraryConfig.earlyFlakeDetectionRetryPolicy) &&
848
+ !isAttemptToFix && !isDisabled && (isNew || isModified)
849
+ const isAtr = this.libraryConfig?.isFlakyTestRetriesEnabled &&
850
+ !isAttemptToFix && !isEarlyFlakeDetection
851
+ let retryCount = 0
852
+ if (isAttemptToFix) {
853
+ retryCount = this.libraryConfig.testManagementAttemptToFixRetries
854
+ } else if (isEarlyFlakeDetection) {
855
+ retryCount = this.libraryConfig.earlyFlakeDetectionRetryPolicy.schedulingRetryCount
856
+ } else if (isAtr) {
857
+ retryCount = this.libraryConfig.flakyTestRetriesCount
858
+ }
859
+
860
+ const test = {
861
+ attempt: 0,
862
+ attemptStart: undefined,
863
+ currentStore: undefined,
864
+ earlyFlakeAbortReason: undefined,
865
+ hasDynamicName: isNew && DYNAMIC_NAME_RE.test(testName),
866
+ hasFailedAttempt: false,
867
+ hasFinalHookFailure: false,
868
+ isAttemptToFix,
869
+ isAtr,
870
+ isDisabled,
871
+ isEarlyFlakeDetection,
872
+ isModified,
873
+ isNew,
874
+ isQuarantined,
875
+ reportedStatus: undefined,
876
+ retryCount,
877
+ retryWait: undefined,
878
+ runnerStatus: undefined,
879
+ span: undefined,
880
+ statuses: [],
881
+ testName,
882
+ testSuiteAbsolutePath,
883
+ title: result.description,
884
+ }
885
+ state.tests.set(result.id, test)
886
+ this.#startWebdriverioJasmineAttempt(test, currentStore)
887
+
888
+ return test.currentStore
889
+ }
890
+
891
+ /**
892
+ * Applies a managed Jasmine test's skip and retry policy to WebdriverIO's function wrapper.
893
+ *
894
+ * @param {object} context
895
+ * @param {WebdriverioJasmineResult|undefined} result
896
+ * @param {'Test'|'Hook'} type
897
+ * @returns {object|undefined}
898
+ */
899
+ #configureWebdriverioJasmineFunction (context, result, type) {
900
+ const currentStore = this.#startWebdriverioJasmineTest(result)
901
+ const test = this._webdriverioJasmineState?.tests.get(result?.id)
902
+ if (!test) {
903
+ return currentStore
904
+ }
905
+
906
+ if (test.isAttemptToFix || test.isAtr || test.isEarlyFlakeDetection) {
907
+ context.arguments[6] = 0
908
+ }
909
+ return {
910
+ ...test.currentStore,
911
+ [WEBDRIVERIO_JASMINE_FUNCTION_TYPE]: type,
912
+ }
913
+ }
914
+
915
+ /**
916
+ * Starts one WebdriverIO Jasmine test attempt under its suite span.
917
+ *
918
+ * @param {object} test
919
+ * @param {object|undefined} parentStore
920
+ * @returns {void}
921
+ */
922
+ #startWebdriverioJasmineAttempt (test, parentStore) {
923
+ test.attemptStart = performance.now()
924
+ test.hasFinalHookFailure = false
925
+ const span = this.startTestSpan({
926
+ hasDynamicName: test.hasDynamicName,
927
+ isAttemptToFix: test.isAttemptToFix,
928
+ isDisabled: test.isDisabled,
929
+ isEfdRetry: test.isEarlyFlakeDetection && test.attempt > 0,
930
+ isModified: test.isModified,
931
+ isNew: test.isNew,
932
+ isParallel: true,
933
+ isQuarantined: test.isQuarantined,
934
+ testName: test.testName,
935
+ testSuiteAbsolutePath: test.testSuiteAbsolutePath,
936
+ title: test.title,
937
+ })
938
+ test.span = span
939
+ test.currentStore = {
940
+ ...parentStore,
941
+ span,
942
+ [WEBDRIVERIO_JASMINE_TEST]: test,
943
+ }
944
+ this.activeTestSpan = span
945
+ }
946
+
947
+ /**
948
+ * Delays Jasmine's parent runner until every Datadog-managed spec execution has completed.
949
+ *
950
+ * @param {object} context
951
+ * @returns {void}
952
+ */
953
+ #configureWebdriverioJasmineLifecycle (context) {
954
+ const isLegacyJasmine = Boolean(context.self?.queueableFn)
955
+ const spec = isLegacyJasmine ? context.self : context.arguments?.[0]
956
+ const onComplete = context.arguments?.[1]
957
+ const originalTestFunction = spec?.queueableFn?.fn
958
+ if (!spec?.id || typeof onComplete !== 'function' || typeof originalTestFunction !== 'function') {
959
+ return
960
+ }
961
+
962
+ if (this.libraryConfig?.isTestManagementTestsEnabled) {
963
+ this.#startWebdriverioJasmineTest(spec.result)
964
+ const test = this._webdriverioJasmineState?.tests.get(spec.id)
965
+ if (test?.isDisabled && !test.isAttemptToFix) {
966
+ spec.pend('Skipped by Datadog Test Management')
967
+ }
968
+ }
969
+
970
+ const executionArguments = [...context.arguments]
971
+ context.arguments[1] = (...completeArguments) => {
972
+ const test = this._webdriverioJasmineState?.tests.get(spec.id)
973
+ if (!test?.willRetry) {
974
+ return onComplete(...completeArguments)
975
+ }
976
+
977
+ test.willRetry = false
978
+ const retry = () => {
979
+ try {
980
+ this.#startNextWebdriverioJasmineAttempt(test)
981
+ spec.reset()
982
+ spec.queueableFn.fn = originalTestFunction
983
+ if (isLegacyJasmine) {
984
+ spec.execute(
985
+ executionArguments[0],
986
+ onComplete,
987
+ executionArguments[2],
988
+ executionArguments[3]
989
+ )
990
+ } else {
991
+ context.self._executeSpec(spec, onComplete)
992
+ }
993
+ } catch (error) {
994
+ log.error('WebdriverIO Jasmine retry error', error)
995
+ onComplete(...completeArguments)
996
+ }
997
+ }
998
+ const retryWait = test.retryWait
999
+ test.retryWait = undefined
1000
+ if (retryWait?.then) {
1001
+ retryWait.then(retry, retry)
1002
+ } else {
1003
+ retry()
1004
+ }
1005
+ }
1006
+ }
1007
+
1008
+ /**
1009
+ * Selects the runner-visible status and whether Jasmine should execute the full spec again.
1010
+ *
1011
+ * @param {object|undefined} spec
1012
+ * @param {string|undefined} status
1013
+ * @returns {string|undefined}
1014
+ */
1015
+ #prepareWebdriverioJasmineAttempt (spec, status) {
1016
+ const state = this._webdriverioJasmineState
1017
+ const completedStatus = state?.completedTestStatuses.get(spec?.id)
1018
+ if (completedStatus) {
1019
+ return completedStatus
1020
+ }
1021
+
1022
+ const test = state?.tests.get(spec?.id)
1023
+ if (!test || !status) {
1024
+ return status
1025
+ }
1026
+
1027
+ const testStatus = getJasmineStatus(status)
1028
+ if (
1029
+ testStatus !== 'skip' &&
1030
+ test.isEarlyFlakeDetection &&
1031
+ test.attempt === 0
1032
+ ) {
1033
+ test.retryCount = getEfdRetryCountForDuration(
1034
+ performance.now() - test.attemptStart,
1035
+ this.libraryConfig.earlyFlakeDetectionRetryPolicy
1036
+ )
1037
+ if (test.retryCount === 0) {
1038
+ test.earlyFlakeAbortReason = 'slow'
1039
+ }
1040
+ }
1041
+
1042
+ const hasManagedRetry = testStatus !== 'skip' && test.attempt < test.retryCount
1043
+ test.willRetry = hasManagedRetry && (
1044
+ test.isAttemptToFix ||
1045
+ test.isEarlyFlakeDetection ||
1046
+ (test.isAtr && testStatus === 'fail' && !test.hasFinalHookFailure)
1047
+ )
1048
+
1049
+ let runnerStatus = status
1050
+ const recoveredEarlyFlakeDetection = test.isEarlyFlakeDetection &&
1051
+ !test.hasFinalHookFailure && test.statuses.includes('pass')
1052
+ if (
1053
+ testStatus === 'fail' &&
1054
+ (test.willRetry || (test.isQuarantined && !test.isAttemptToFix) || recoveredEarlyFlakeDetection)
1055
+ ) {
1056
+ runnerStatus = 'passed'
1057
+ } else if (testStatus !== 'skip' && !test.willRetry && test.isAttemptToFix) {
1058
+ runnerStatus = test.statuses.every(previousStatus => previousStatus === 'pass') && testStatus === 'pass'
1059
+ ? 'passed'
1060
+ : 'failed'
1061
+ }
1062
+
1063
+ if (runnerStatus !== status) {
1064
+ test.reportedStatus = status
1065
+ }
1066
+ if (!test.willRetry) {
1067
+ test.runnerStatus = runnerStatus
1068
+ }
1069
+ return runnerStatus
1070
+ }
1071
+
1072
+ /**
1073
+ * Finishes one Datadog-managed Jasmine attempt before the full spec is executed again.
1074
+ *
1075
+ * @param {object} test
1076
+ * @param {WebdriverioJasmineResult} result
1077
+ * @returns {void}
1078
+ */
1079
+ #finishWebdriverioJasmineRetry (test, result) {
1080
+ const status = getJasmineStatus(test.reportedStatus || result.status)
1081
+ const error = getJasmineError(result)
1082
+ test.statuses.push(status)
1083
+
1084
+ if (error) {
1085
+ const isFirstFailure = !test.hasFailedAttempt
1086
+ test.hasFailedAttempt = true
1087
+ const promises = {}
1088
+ testRetryCh.publish({
1089
+ err: error,
1090
+ isAttemptToFixRetry: test.isAttemptToFix && test.attempt > 0,
1091
+ isAtrRetry: test.isAtr && test.attempt > 0,
1092
+ isEfdRetry: test.isEarlyFlakeDetection && test.attempt > 0,
1093
+ isFirstAttempt: test.attempt === 0,
1094
+ isFirstFailure,
1095
+ promises,
1096
+ test,
1097
+ willBeRetried: true,
1098
+ ...test.currentStore,
1099
+ })
1100
+ test.retryWait = getFailedTestReplayPromise(promises)
1101
+ } else {
1102
+ testFinishCh.publish({
1103
+ hasBeenRetried: test.isAtr && test.attempt > 0,
1104
+ isAttemptToFixRetry: test.isAttemptToFix && test.attempt > 0,
1105
+ isAtrRetry: false,
1106
+ isLastRetry: false,
1107
+ status,
1108
+ ...test.currentStore,
1109
+ })
1110
+ }
1111
+ }
1112
+
1113
+ /**
1114
+ * Advances a Jasmine test and starts its next attempt span.
1115
+ *
1116
+ * @param {object} test
1117
+ * @returns {void}
1118
+ */
1119
+ #startNextWebdriverioJasmineAttempt (test) {
1120
+ test.attempt++
1121
+ test.reportedStatus = undefined
1122
+ this.#startWebdriverioJasmineAttempt(test, test.currentStore)
1123
+ }
1124
+
1125
+ /**
1126
+ * Finishes an intermediate native WebdriverIO retry and starts its next span.
1127
+ *
1128
+ * @param {object} test
1129
+ * @param {Error|undefined} error
1130
+ * @returns {Promise<void>|undefined}
1131
+ */
1132
+ #retryWebdriverioJasmineTest (test, error) {
1133
+ test.statuses.push('fail')
1134
+ const isFirstFailure = !test.hasFailedAttempt
1135
+ test.hasFailedAttempt = true
1136
+ const promises = {}
1137
+ testRetryCh.publish({
1138
+ err: error,
1139
+ isAttemptToFixRetry: false,
1140
+ isAtrRetry: false,
1141
+ isEfdRetry: false,
1142
+ isFirstAttempt: test.attempt === 0,
1143
+ isFirstFailure,
1144
+ promises,
1145
+ test,
1146
+ willBeRetried: true,
1147
+ ...test.currentStore,
1148
+ })
1149
+
1150
+ const retryWait = getFailedTestReplayPromise(promises)
1151
+ if (retryWait?.then) {
1152
+ const startNextAttempt = () => this.#startNextWebdriverioJasmineAttempt(test)
1153
+ return retryWait.then(startNextAttempt, startNextAttempt)
1154
+ }
1155
+ this.#startNextWebdriverioJasmineAttempt(test)
1156
+ }
1157
+
1158
+ /**
1159
+ * Finishes a Jasmine test after its reporter receives the final result.
1160
+ *
1161
+ * @param {WebdriverioJasmineResult|undefined} result
1162
+ * @param {string[]} [specs]
1163
+ * @returns {Promise<void>|undefined}
1164
+ */
1165
+ #finishWebdriverioJasmineTest (result, specs) {
1166
+ const state = this._webdriverioJasmineState
1167
+ if (!state || !result?.id) {
1168
+ return
1169
+ }
1170
+
1171
+ this.#startWebdriverioJasmineTest(result, specs)
1172
+ const test = state.tests.get(result.id)
1173
+ if (!test) {
1174
+ return
1175
+ }
1176
+
1177
+ if (state.currentResult?.id === result.id) {
1178
+ state.currentResult = undefined
1179
+ }
1180
+ if (test.willRetry) {
1181
+ this.#finishWebdriverioJasmineRetry(test, result)
1182
+ return
1183
+ }
1184
+ if (test._ddShouldWaitForHitProbe) {
1185
+ delete test._ddShouldWaitForHitProbe
1186
+ state.pendingTestFinishes++
1187
+ const finishTest = () => {
1188
+ this.#completeWebdriverioJasmineTest(test, result)
1189
+ state.pendingTestFinishes--
1190
+ if (state.pendingTestFinishes === 0) {
1191
+ const callbacks = state.pendingTestFinishCallbacks
1192
+ state.pendingTestFinishCallbacks = []
1193
+ for (const callback of callbacks) {
1194
+ callback()
1195
+ }
1196
+ }
1197
+ }
1198
+ return this.waitForDiBreakpointHits().then(finishTest, finishTest)
1199
+ }
1200
+
1201
+ this.#completeWebdriverioJasmineTest(test, result)
1202
+ }
1203
+
1204
+ /**
1205
+ * Finalizes the last reported attempt for one WebdriverIO Jasmine test.
1206
+ *
1207
+ * @param {object} test
1208
+ * @param {WebdriverioJasmineResult} result
1209
+ * @returns {void}
1210
+ */
1211
+ #completeWebdriverioJasmineTest (test, result) {
1212
+ const state = this._webdriverioJasmineState
1213
+
1214
+ const status = getJasmineStatus(test.reportedStatus || result.status)
1215
+ const error = getJasmineError(result)
1216
+ if (error) {
1217
+ test.span.setTag('error', error)
1218
+ }
1219
+ if (!test.isEarlyFlakeDetection || status !== 'skip') {
1220
+ test.statuses.push(status)
1221
+ }
1222
+ const hasFailedAllRetries = test.attempt > 0 &&
1223
+ (test.isAttemptToFix || test.isAtr || test.isEarlyFlakeDetection) &&
1224
+ test.statuses.every(testStatus => testStatus === 'fail')
1225
+ const isSkipped = status === 'skip'
1226
+ const attemptToFixPassed = !isSkipped && test.isAttemptToFix &&
1227
+ test.statuses.every(testStatus => testStatus === 'pass')
1228
+ const attemptToFixFailed = !isSkipped && test.isAttemptToFix && !attemptToFixPassed
1229
+ let finalStatus = status
1230
+ if (isSkipped || (!test.isAttemptToFix && (test.isDisabled || test.isQuarantined))) {
1231
+ finalStatus = 'skip'
1232
+ } else if (test.isAttemptToFix) {
1233
+ finalStatus = attemptToFixPassed ? 'pass' : 'fail'
1234
+ } else if (test.isEarlyFlakeDetection && status !== 'skip' && !test.hasFinalHookFailure) {
1235
+ finalStatus = test.statuses.includes('pass') ? 'pass' : 'fail'
1236
+ }
1237
+ testFinishCh.publish({
1238
+ attemptToFixFailed,
1239
+ attemptToFixPassed,
1240
+ earlyFlakeAbortReason: test.earlyFlakeAbortReason,
1241
+ finalStatus,
1242
+ hasBeenRetried: !test.isAttemptToFix && !test.isEarlyFlakeDetection && test.attempt > 0,
1243
+ hasFailedAllRetries,
1244
+ isAttemptToFixRetry: test.isAttemptToFix && test.attempt > 0,
1245
+ isAtrRetry: test.isAtr && test.attempt > 0,
1246
+ isLastRetry: true,
1247
+ status,
1248
+ ...test.currentStore,
1249
+ })
1250
+ state.completedTestStatuses.set(result.id, test.runnerStatus || result.status)
1251
+ state.tests.delete(result.id)
1252
+
1253
+ const suiteStatus = test.isQuarantined && !test.isAttemptToFix ? 'pass' : finalStatus
1254
+ const previousStatus = state.suiteStatuses.get(test.testSuiteAbsolutePath)
1255
+ if (suiteStatus === 'fail' || !previousStatus || previousStatus === 'skip') {
1256
+ state.suiteStatuses.set(test.testSuiteAbsolutePath, suiteStatus)
1257
+ }
1258
+ }
1259
+
1260
+ /**
1261
+ * Reports Jasmine suite statuses and flushes worker traces before its run settles.
1262
+ *
1263
+ * @param {{
1264
+ * resolveCallback?: (onDone: () => void) => void,
1265
+ * rejectCallback?: (onDone: () => void) => void
1266
+ * }} context
1267
+ * @returns {void}
1268
+ */
1269
+ #finishWebdriverioJasmineWorker (context) {
1270
+ const state = this._webdriverioJasmineState
1271
+ const reportWorker = onDone => {
1272
+ const results = []
1273
+ const reportedFiles = new Set()
1274
+ for (const [file, status] of state.suiteStatuses) {
1275
+ const error = state.suiteErrors.get(file)
1276
+ const result = { file, status }
1277
+ if (error) {
1278
+ result.error = {
1279
+ message: error.message,
1280
+ stack: error.stack,
1281
+ }
1282
+ }
1283
+ results.push(result)
1284
+ reportedFiles.add(file)
1285
+ }
1286
+ for (const spec of state.specs) {
1287
+ const file = normalizeJasmineFile(spec)
1288
+ if (!reportedFiles.has(file)) {
1289
+ results.push({ file, status: 'skip' })
1290
+ }
1291
+ }
1292
+
1293
+ sendWebdriverioWorkerMessage({
1294
+ origin: 'datadog',
1295
+ name: SUITE_FINISH,
1296
+ content: { results },
1297
+ }, error => {
1298
+ if (error) {
1299
+ log.error('WebdriverIO Test Optimization IPC error', error)
1300
+ }
1301
+ }, () => workerFinishCh.publish({ onDone }))
1302
+ }
1303
+
1304
+ const waitForWorker = onDone => {
1305
+ if (state?.pendingTestFinishes) {
1306
+ state.pendingTestFinishCallbacks.push(() => reportWorker(onDone))
1307
+ } else {
1308
+ reportWorker(onDone)
1309
+ }
1310
+ }
1311
+ context.resolveCallback = waitForWorker
1312
+ context.rejectCallback = waitForWorker
1313
+ }
1314
+
487
1315
  startTestSpan (testInfo) {
488
1316
  const {
489
1317
  testName,