dd-trace 6.7.0 → 6.9.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/ci/diagnose.js +27 -25
- package/ci/init.js +6 -6
- package/ci/runbook.md +114 -20
- package/ci/test-optimization-validation/approval-artifacts.js +36 -3
- package/ci/test-optimization-validation/approval.js +78 -22
- package/ci/test-optimization-validation/blocker-category.js +24 -0
- package/ci/test-optimization-validation/ci-discovery.js +23 -17
- package/ci/test-optimization-validation/ci-package-scripts.js +34 -6
- package/ci/test-optimization-validation/ci-remediation.js +1 -1
- package/ci/test-optimization-validation/cli.js +280 -52
- package/ci/test-optimization-validation/command-blocker.js +159 -20
- package/ci/test-optimization-validation/command-output-policy.js +14 -8
- package/ci/test-optimization-validation/command-runner.js +9 -7
- package/ci/test-optimization-validation/environment.js +4 -2
- package/ci/test-optimization-validation/executable.js +15 -13
- package/ci/test-optimization-validation/execution-lock.js +70 -0
- package/ci/test-optimization-validation/framework-adapters/cucumber-validation.json +3 -0
- package/ci/test-optimization-validation/framework-adapters/cucumber.js +657 -5
- package/ci/test-optimization-validation/framework-adapters/playwright.js +5 -5
- package/ci/test-optimization-validation/framework-adapters/vitest.js +517 -0
- package/ci/test-optimization-validation/generated-files.js +47 -37
- package/ci/test-optimization-validation/generated-test-contract.js +9 -7
- package/ci/test-optimization-validation/generated-verifier.js +24 -17
- package/ci/test-optimization-validation/literal-glob.js +52 -0
- package/ci/test-optimization-validation/manifest-loader.js +3 -0
- package/ci/test-optimization-validation/manifest-scaffold.js +301 -52
- package/ci/test-optimization-validation/manifest-schema.js +80 -17
- package/ci/test-optimization-validation/offline-fixtures.js +13 -4
- package/ci/test-optimization-validation/offline-output.js +6 -6
- package/ci/test-optimization-validation/package-check.js +94 -0
- package/ci/test-optimization-validation/plan-writer.js +183 -10
- package/ci/test-optimization-validation/preflight-runner.js +110 -31
- package/ci/test-optimization-validation/project-build-artifact.js +31 -0
- package/ci/test-optimization-validation/redaction.js +18 -27
- package/ci/test-optimization-validation/report-writer.js +182 -48
- package/ci/test-optimization-validation/result-semantics.js +20 -3
- package/ci/test-optimization-validation/runner-command.js +41 -21
- package/ci/test-optimization-validation/runner-contract.js +79 -43
- package/ci/test-optimization-validation/safe-files.js +4 -4
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +60 -6
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +141 -35
- package/ci/test-optimization-validation/scenarios/helpers.js +30 -6
- package/ci/test-optimization-validation/scenarios/test-management.js +2 -2
- package/ci/test-optimization-validation/static-diagnosis.js +10 -3
- package/ci/test-optimization-validation/test-output.js +2 -1
- package/ci/test-optimization-validation/validation-blocker.js +21 -0
- package/ci/vitest-no-worker-init-setup.mjs +377 -84
- package/index.d.ts +175 -14
- package/init.js +1 -17
- package/initialize.mjs +11 -0
- package/loader-hook.mjs +6 -4
- package/package.json +12 -4
- package/packages/datadog-core/src/utils/src/kebabcase.js +8 -2
- package/packages/datadog-core/src/utils/src/parse-tags.js +0 -1
- package/packages/datadog-esbuild/index.js +4 -2
- package/packages/datadog-esbuild/src/utils.js +17 -2
- package/packages/datadog-instrumentations/src/ai.js +61 -34
- package/packages/datadog-instrumentations/src/bluebird.js +6 -6
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +85 -9
- package/packages/datadog-instrumentations/src/cucumber.js +131 -51
- package/packages/datadog-instrumentations/src/dns.js +21 -2
- package/packages/datadog-instrumentations/src/electron.js +1 -0
- package/packages/datadog-instrumentations/src/fastify.js +3 -1
- package/packages/datadog-instrumentations/src/grpc/client.js +4 -2
- package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +3 -1
- package/packages/datadog-instrumentations/src/helpers/extract-package-and-module-path.js +10 -0
- package/packages/datadog-instrumentations/src/helpers/hook.js +19 -2
- package/packages/datadog-instrumentations/src/helpers/hooks.js +4 -0
- package/packages/datadog-instrumentations/src/helpers/register.js +9 -3
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +34 -9
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +175 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +20 -2
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +32 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +61 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +50 -18
- package/packages/datadog-instrumentations/src/helpers/router-helper.js +38 -7
- package/packages/datadog-instrumentations/src/helpers/shared-utils.js +39 -0
- package/packages/datadog-instrumentations/src/http/client.js +1 -3
- package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +1 -1
- package/packages/datadog-instrumentations/src/jest.js +986 -246
- package/packages/datadog-instrumentations/src/mocha/common.js +1 -3
- package/packages/datadog-instrumentations/src/mocha/main.js +109 -51
- package/packages/datadog-instrumentations/src/mocha/utils.js +120 -32
- package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +44 -0
- package/packages/datadog-instrumentations/src/mocha/worker.js +441 -43
- package/packages/datadog-instrumentations/src/moleculer/client.js +1 -1
- package/packages/datadog-instrumentations/src/mysql.js +1 -1
- package/packages/datadog-instrumentations/src/next.js +155 -23
- package/packages/datadog-instrumentations/src/openai.js +14 -0
- package/packages/datadog-instrumentations/src/pg.js +180 -6
- package/packages/datadog-instrumentations/src/playwright.js +176 -81
- package/packages/datadog-instrumentations/src/promise-js.js +4 -4
- package/packages/datadog-instrumentations/src/promise.js +3 -3
- package/packages/datadog-instrumentations/src/rhea.js +1 -1
- package/packages/datadog-instrumentations/src/router.js +195 -19
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +567 -65
- package/packages/datadog-instrumentations/src/vitest-main.js +438 -54
- package/packages/datadog-instrumentations/src/vitest-util.js +42 -7
- package/packages/datadog-instrumentations/src/vitest-worker.js +234 -17
- package/packages/datadog-instrumentations/src/webdriverio.js +1090 -0
- package/packages/datadog-instrumentations/src/when.js +3 -3
- package/packages/datadog-instrumentations/src/ws.js +5 -1
- package/packages/datadog-plugin-aerospike/src/index.js +1 -3
- package/packages/datadog-plugin-amqplib/src/client.js +1 -1
- package/packages/datadog-plugin-amqplib/src/producer.js +1 -1
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +59 -48
- package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +2 -2
- package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +2 -2
- package/packages/datadog-plugin-azure-event-hubs/src/producer.js +1 -3
- package/packages/datadog-plugin-azure-service-bus/src/producer.js +5 -3
- package/packages/datadog-plugin-bullmq/src/producer.js +3 -3
- package/packages/datadog-plugin-child_process/src/index.js +3 -3
- package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +6 -11
- package/packages/datadog-plugin-cucumber/src/index.js +5 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +50 -46
- package/packages/datadog-plugin-cypress/src/source-map-utils.js +1 -1
- package/packages/datadog-plugin-cypress/src/support.js +7 -4
- package/packages/datadog-plugin-electron/src/net.js +10 -4
- package/packages/datadog-plugin-fastify/src/tracing.js +1 -1
- package/packages/datadog-plugin-fs/src/index.js +0 -4
- package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +14 -7
- package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +19 -10
- package/packages/datadog-plugin-graphql/src/execute.js +1 -0
- package/packages/datadog-plugin-graphql/src/tools/signature.js +1 -1
- package/packages/datadog-plugin-http/src/client.js +3 -2
- package/packages/datadog-plugin-http2/src/client.js +1 -1
- package/packages/datadog-plugin-jest/src/index.js +3 -21
- package/packages/datadog-plugin-jest/src/util.js +4 -1
- package/packages/datadog-plugin-kafkajs/src/producer.js +2 -1
- package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
- package/packages/datadog-plugin-mocha/src/index.js +342 -6
- package/packages/datadog-plugin-next/src/index.js +57 -9
- package/packages/datadog-plugin-openai/src/tracing.js +4 -0
- package/packages/datadog-plugin-pg/src/index.js +65 -1
- package/packages/datadog-plugin-playwright/src/index.js +1 -1
- package/packages/datadog-plugin-prisma/src/index.js +5 -1
- package/packages/datadog-plugin-rhea/src/consumer.js +16 -13
- package/packages/datadog-plugin-rhea/src/producer.js +1 -1
- package/packages/datadog-plugin-undici/src/index.js +2 -1
- package/packages/datadog-plugin-vitest/src/index.js +179 -18
- package/packages/datadog-shimmer/src/shimmer.js +6 -2
- package/packages/datadog-webpack/index.js +2 -2
- package/packages/dd-trace/src/aiguard/sdk.js +3 -0
- package/packages/dd-trace/src/appsec/{blocking.js → blocking/index.js} +26 -6
- package/packages/dd-trace/src/appsec/downstream_requests.js +1 -1
- package/packages/dd-trace/src/appsec/handlers/auth.js +62 -0
- package/packages/dd-trace/src/appsec/handlers/http-request.js +215 -0
- package/packages/dd-trace/src/appsec/handlers/http-response.js +78 -0
- package/packages/dd-trace/src/appsec/handlers/http-shared.js +24 -0
- package/packages/dd-trace/src/appsec/handlers/payments.js +104 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-base-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +1 -3
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +2 -2
- package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +2 -2
- package/packages/dd-trace/src/appsec/index.js +48 -476
- package/packages/dd-trace/src/appsec/sdk/track_event.js +10 -1
- package/packages/dd-trace/src/appsec/waf/waf_manager.js +1 -1
- package/packages/dd-trace/src/ci-visibility/coverage-report-discovery.js +46 -14
- package/packages/dd-trace/src/ci-visibility/efd-retry-policy.js +89 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +7 -6
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +29 -15
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +19 -1
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +25 -1
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/webdriverio.js +26 -0
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -1
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +22 -7
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -14
- package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -3
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +21 -5
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +2 -2
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +5 -1
- package/packages/dd-trace/src/config/defaults.js +14 -6
- package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -2
- package/packages/dd-trace/src/config/helper.js +2 -0
- package/packages/dd-trace/src/config/index.js +31 -5
- package/packages/dd-trace/src/config/parsers.js +19 -1
- package/packages/dd-trace/src/config/remote_config.js +1 -1
- package/packages/dd-trace/src/config/supported-configurations.json +13 -6
- package/packages/dd-trace/src/datastreams/encoding.js +3 -4
- package/packages/dd-trace/src/debugger/devtools_client/log.js +2 -1
- package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +1 -4
- package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -2
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +2 -2
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +16 -2
- package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
- package/packages/dd-trace/src/encode/coverage-ci-visibility.js +0 -1
- package/packages/dd-trace/src/encode/tags-processors.js +2 -2
- package/packages/dd-trace/src/exporters/common/url.js +3 -5
- package/packages/dd-trace/src/exporters/electron/index.js +4 -1
- package/packages/dd-trace/src/external-logger/src/index.js +1 -2
- package/packages/dd-trace/src/flare/index.js +2 -2
- package/packages/dd-trace/src/guardrails/apply-pm2-env.js +16 -0
- package/packages/dd-trace/src/guardrails/log.js +1 -10
- package/packages/dd-trace/src/id.js +1 -1
- package/packages/dd-trace/src/llmobs/constants/tags.js +1 -1
- package/packages/dd-trace/src/llmobs/eval-metric.js +249 -0
- package/packages/dd-trace/src/llmobs/experiments/client.js +4 -2
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +66 -14
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +289 -95
- package/packages/dd-trace/src/llmobs/experiments/index.js +66 -19
- package/packages/dd-trace/src/llmobs/experiments/noop.js +95 -10
- package/packages/dd-trace/src/llmobs/experiments/result.js +13 -2
- package/packages/dd-trace/src/llmobs/experiments/util.js +220 -0
- package/packages/dd-trace/src/llmobs/noop.js +2 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +5 -5
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/genai/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +1 -1
- package/packages/dd-trace/src/llmobs/sdk.js +152 -78
- package/packages/dd-trace/src/llmobs/span_processor.js +9 -7
- package/packages/dd-trace/src/llmobs/tagger.js +1 -2
- package/packages/dd-trace/src/llmobs/telemetry.js +18 -0
- package/packages/dd-trace/src/llmobs/writers/base.js +3 -0
- package/packages/dd-trace/src/llmobs/writers/spans.js +12 -6
- package/packages/dd-trace/src/llmobs/writers/util.js +2 -0
- package/packages/dd-trace/src/log/channels.js +1 -9
- package/packages/dd-trace/src/log/levels.js +12 -0
- package/packages/dd-trace/src/noop/proxy.js +2 -2
- package/packages/dd-trace/src/openfeature/writers/exposures.js +1 -1
- package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
- package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +1 -1
- package/packages/dd-trace/src/opentelemetry/metrics/meter.js +8 -4
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +1 -1
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +3 -2
- package/packages/dd-trace/src/opentelemetry/span.js +4 -0
- package/packages/dd-trace/src/opentracing/span.js +1 -1
- package/packages/dd-trace/src/opentracing/span_context.js +1 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +124 -23
- package/packages/dd-trace/src/plugins/index.js +2 -0
- package/packages/dd-trace/src/plugins/plugin.js +1 -1
- package/packages/dd-trace/src/plugins/tracing.js +1 -0
- package/packages/dd-trace/src/plugins/util/ci.js +13 -6
- package/packages/dd-trace/src/plugins/util/git.js +7 -5
- package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +3 -3
- package/packages/dd-trace/src/plugins/util/ip_extractor.js +1 -1
- package/packages/dd-trace/src/plugins/util/llm.js +2 -2
- package/packages/dd-trace/src/plugins/util/stacktrace.js +3 -0
- package/packages/dd-trace/src/plugins/util/tags.js +2 -0
- package/packages/dd-trace/src/plugins/util/test.js +356 -86
- package/packages/dd-trace/src/plugins/util/url.js +5 -5
- package/packages/dd-trace/src/plugins/util/user-provided-git.js +1 -1
- package/packages/dd-trace/src/plugins/util/web.js +24 -2
- package/packages/dd-trace/src/priority_sampler.js +6 -3
- package/packages/dd-trace/src/profiler.js +1 -1
- package/packages/dd-trace/src/profiling/exporters/event_serializer.js +1 -1
- package/packages/dd-trace/src/profiling/profilers/events.js +2 -2
- package/packages/dd-trace/src/profiling/profilers/poisson.js +1 -1
- package/packages/dd-trace/src/profiling/profilers/shared.js +2 -2
- package/packages/dd-trace/src/proxy.js +1 -0
- package/packages/dd-trace/src/ritm.js +10 -20
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +4 -2
- package/packages/dd-trace/src/sampling_rule.js +4 -2
- package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +1 -1
- package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +1 -1
- package/packages/dd-trace/src/startup-log.js +3 -0
- package/packages/dd-trace/src/telemetry/logs/index.js +3 -0
- package/packages/dd-trace/src/telemetry/logs/log-collector.js +3 -1
- package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
- package/packages/dd-trace/src/util.js +16 -0
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +12 -12
- package/vendor/dist/pprof-format/index.js +1 -1
- package/vendor/dist/source-map/index.js +1 -1
- package/version.js +10 -8
- /package/packages/dd-trace/src/appsec/{blocked_templates.js → blocking/templates.js} +0 -0
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
// Capture real timers at module load time, before any test can install fake timers.
|
|
4
|
+
const realClearTimeout = clearTimeout
|
|
4
5
|
const realSetTimeout = setTimeout
|
|
5
6
|
|
|
6
7
|
const { readFileSync } = require('node:fs')
|
|
7
8
|
const { builtinModules, createRequire } = require('node:module')
|
|
9
|
+
const { performance } = require('node:perf_hooks')
|
|
8
10
|
const path = require('path')
|
|
9
11
|
const satisfies = require('../../../vendor/dist/semifies')
|
|
10
12
|
const { DD_MAJOR } = require('../../../version')
|
|
11
13
|
const shimmer = require('../../datadog-shimmer')
|
|
12
14
|
const { getEnvironmentVariable } = require('../../dd-trace/src/config/helper')
|
|
13
15
|
const log = require('../../dd-trace/src/log')
|
|
16
|
+
const {
|
|
17
|
+
EMPTY_EFD_RETRY_POLICY,
|
|
18
|
+
getEfdRetryCountForDuration,
|
|
19
|
+
hasEfdRetries,
|
|
20
|
+
} = require('../../dd-trace/src/ci-visibility/efd-retry-policy')
|
|
14
21
|
const {
|
|
15
22
|
getCoveredFilesFromCoverage,
|
|
16
23
|
getExecutableFilesFromCoverage,
|
|
@@ -27,10 +34,11 @@ const {
|
|
|
27
34
|
isModifiedTest,
|
|
28
35
|
DYNAMIC_NAME_RE,
|
|
29
36
|
collectDynamicNamesFromTraces,
|
|
37
|
+
recordTestManagementExecution,
|
|
30
38
|
recordAttemptToFixExecution,
|
|
31
39
|
logAttemptToFixTestExecution,
|
|
32
40
|
logTestOptimizationSummary,
|
|
33
|
-
|
|
41
|
+
TEST_IMPACT_ANALYSIS_ALL_TESTS_SKIPPED_MESSAGE,
|
|
34
42
|
getTestCoverageLinesPercentage,
|
|
35
43
|
applySkippedCoverageToCoverage,
|
|
36
44
|
getTestOptimizationRequestResults,
|
|
@@ -96,7 +104,7 @@ const DD_JEST_HANDLE_TEST_EVENT_WRAPPED = Symbol('dd-trace:jest:handle-test-even
|
|
|
96
104
|
const DD_JEST_HANDLE_TEST_EVENT_DATADOG = Symbol('dd-trace:jest:handle-test-event-datadog')
|
|
97
105
|
const DD_JEST_CONCURRENT_TEST_ORIGINAL = Symbol('dd-trace:jest:concurrent-test-original')
|
|
98
106
|
const isJestWorker = !!getEnvironmentVariable('JEST_WORKER_ID')
|
|
99
|
-
const jestSessionState =
|
|
107
|
+
const jestSessionState = (globalThis[JEST_SESSION_STATE] ||= {})
|
|
100
108
|
|
|
101
109
|
// https://github.com/jestjs/jest/blob/41f842a46bb2691f828c3a5f27fc1d6290495b82/packages/jest-circus/src/types.ts#L9C8-L9C54
|
|
102
110
|
const RETRY_TIMES = Symbol.for('RETRY_TIMES')
|
|
@@ -115,8 +123,6 @@ let numSkippedSuites = 0
|
|
|
115
123
|
let hasUnskippableSuites = false
|
|
116
124
|
let hasForcedToRunSuites = false
|
|
117
125
|
let isEarlyFlakeDetectionEnabled = false
|
|
118
|
-
let earlyFlakeDetectionNumRetries = 0
|
|
119
|
-
let earlyFlakeDetectionSlowTestRetries = {}
|
|
120
126
|
let earlyFlakeDetectionFaultyThreshold = 30
|
|
121
127
|
let isEarlyFlakeDetectionFaulty = false
|
|
122
128
|
let hasFilteredSkippableSuites = false
|
|
@@ -141,8 +147,10 @@ const originalTestFns = new WeakMap()
|
|
|
141
147
|
const originalHookFns = new WeakMap()
|
|
142
148
|
const concurrentHookContextQueues = new WeakMap()
|
|
143
149
|
const concurrentHookFns = new WeakMap()
|
|
150
|
+
const efdRetryMetadataByTest = new WeakMap()
|
|
151
|
+
const removedRetryTests = new WeakSet()
|
|
144
152
|
const retriedTestsToNumAttempts = new Map()
|
|
145
|
-
const
|
|
153
|
+
const efdTestStatuses = new Map()
|
|
146
154
|
const attemptToFixRetriedTestsStatuses = new Map()
|
|
147
155
|
const wrappedWorkerChannels = new WeakMap()
|
|
148
156
|
// New tests whose names contain likely dynamic data (timestamps, UUIDs, etc.)
|
|
@@ -153,10 +161,12 @@ const testSuiteMockedFiles = new Map()
|
|
|
153
161
|
const testsToBeRetried = new Set()
|
|
154
162
|
// Per-test: how many EFD retries were determined after the first execution.
|
|
155
163
|
const efdDeterminedRetries = new Map()
|
|
164
|
+
// Per-test: total executions to report, including retries Jest ran before the test they belong to.
|
|
165
|
+
const efdExpectedExecutions = new Map()
|
|
156
166
|
// Tests whose first run exceeded the 5-min threshold — tagged "slow".
|
|
157
167
|
const efdSlowAbortedTests = new Set()
|
|
158
|
-
// Tests
|
|
159
|
-
const
|
|
168
|
+
// Tests whose first execution determines the duration-based EFD retry count.
|
|
169
|
+
const efdCandidates = new Set()
|
|
160
170
|
// Tests that are genuinely new (not in known tests list).
|
|
161
171
|
const newTests = new Set()
|
|
162
172
|
const testSuiteJestObjects = new Map()
|
|
@@ -173,13 +183,39 @@ const handledJestEvents = new WeakSet()
|
|
|
173
183
|
* @typedef {object} ConcurrentTestOptions
|
|
174
184
|
* @property {(...args: unknown[]) => unknown} [concurrentTest]
|
|
175
185
|
* @property {unknown} [concurrentTestThisArg]
|
|
186
|
+
* @property {EfdRetryGate[]} [efdRetryGates]
|
|
176
187
|
* @property {boolean} [isAttemptToFixRetry]
|
|
177
188
|
* @property {boolean} [isEfdRetry]
|
|
189
|
+
* @property {number} [efdRetryIndex]
|
|
178
190
|
* @property {boolean} [isModified]
|
|
179
|
-
* @property {(...args: unknown[]) => unknown} [serialTest]
|
|
180
|
-
* @property {unknown} [serialTestThisArg]
|
|
181
191
|
* @property {(...args: unknown[]) => unknown} [sourceTestFn]
|
|
182
192
|
* @property {string} [testParameters]
|
|
193
|
+
* @property {number} [timeout]
|
|
194
|
+
*/
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @typedef {object} JestRetryOptions
|
|
198
|
+
* @property {object} [concurrentTestState]
|
|
199
|
+
* @property {EfdRetryGate[]} [efdRetryGates]
|
|
200
|
+
* @property {object} jestEvent
|
|
201
|
+
* @property {object} state
|
|
202
|
+
* @property {boolean} [isModified]
|
|
203
|
+
* @property {number} retryCount
|
|
204
|
+
* @property {string} retryType
|
|
205
|
+
* @property {string} testFullName
|
|
206
|
+
*/
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @typedef {object} EfdRetryGate
|
|
210
|
+
* @property {Promise<boolean>} promise
|
|
211
|
+
* @property {(shouldRun: boolean) => void} resolve
|
|
212
|
+
*/
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @typedef {object} DetachedEfdRetry
|
|
216
|
+
* @property {object} ctx
|
|
217
|
+
* @property {() => void} resolve
|
|
218
|
+
* @property {() => void} start
|
|
183
219
|
*/
|
|
184
220
|
|
|
185
221
|
const ATR_RETRY_SUPPRESSION_FLAG = '_ddDisableAtrRetry'
|
|
@@ -278,6 +314,10 @@ function getTestEnvironmentOptions (config) {
|
|
|
278
314
|
|
|
279
315
|
const MAX_IGNORED_TEST_NAMES = 10
|
|
280
316
|
|
|
317
|
+
/**
|
|
318
|
+
* @typedef {Parameters<typeof logTestOptimizationSummary>[0]} TestOptimizationSummary
|
|
319
|
+
*/
|
|
320
|
+
|
|
281
321
|
function getTestStats (testStatuses) {
|
|
282
322
|
return testStatuses.reduce((acc, testStatus) => {
|
|
283
323
|
acc[testStatus]++
|
|
@@ -288,22 +328,20 @@ function getTestStats (testStatuses) {
|
|
|
288
328
|
/**
|
|
289
329
|
* Formats the ignored-failure section for the Test Optimization summary.
|
|
290
330
|
*
|
|
291
|
-
* @param {{
|
|
331
|
+
* @param {{
|
|
332
|
+
* efdNames: string[],
|
|
333
|
+
* quarantineNames: string[],
|
|
334
|
+
* totalCount: number,
|
|
335
|
+
* efdFailureCount: number
|
|
336
|
+
* } | undefined} ignoredFailures
|
|
292
337
|
* @returns {string}
|
|
293
338
|
*/
|
|
294
339
|
function formatIgnoredFailuresSummary (ignoredFailures) {
|
|
295
|
-
if (!ignoredFailures) return ''
|
|
296
|
-
|
|
297
|
-
const items = []
|
|
340
|
+
if (!ignoredFailures?.efdFailureCount) return ''
|
|
298
341
|
|
|
299
|
-
|
|
300
|
-
items.push({ text: n, suffix: 'Early Flake Detection' })
|
|
301
|
-
}
|
|
302
|
-
for (const n of ignoredFailures.quarantineNames) {
|
|
303
|
-
items.push({ text: n, suffix: 'Quarantine' })
|
|
304
|
-
}
|
|
342
|
+
const items = ignoredFailures.efdNames.map(text => ({ text, suffix: 'Early Flake Detection' }))
|
|
305
343
|
|
|
306
|
-
if (items.length === 0
|
|
344
|
+
if (items.length === 0) return ''
|
|
307
345
|
|
|
308
346
|
const shown = items.slice(0, MAX_IGNORED_TEST_NAMES)
|
|
309
347
|
const more = items.length - shown.length
|
|
@@ -312,18 +350,28 @@ function formatIgnoredFailuresSummary (ignoredFailures) {
|
|
|
312
350
|
.map(({ text, suffix }) => ` • ${text}${suffix ? ` (${suffix})` : ''}`)
|
|
313
351
|
.join('\n') + moreSuffix
|
|
314
352
|
|
|
315
|
-
return `${ignoredFailures.
|
|
353
|
+
return `${ignoredFailures.efdFailureCount} test failure(s) were ignored. Exit code set to 0.\n\n${formattedItems}`
|
|
316
354
|
}
|
|
317
355
|
|
|
318
356
|
/**
|
|
319
357
|
* Logs a single "Datadog Test Optimization" summary at session end.
|
|
320
358
|
*
|
|
321
|
-
* @param {{
|
|
359
|
+
* @param {{
|
|
360
|
+
* efdNames: string[],
|
|
361
|
+
* quarantineNames: string[],
|
|
362
|
+
* totalCount: number,
|
|
363
|
+
* efdFailureCount: number
|
|
364
|
+
* } | undefined} ignoredFailures
|
|
365
|
+
* @param {NonNullable<TestOptimizationSummary['attemptToFixExecutions']>} attemptToFixExecutions
|
|
366
|
+
* @param {boolean} allTestsSkipped
|
|
322
367
|
*/
|
|
323
|
-
function logSessionSummary (ignoredFailures, attemptToFixExecutions) {
|
|
368
|
+
function logSessionSummary (ignoredFailures, attemptToFixExecutions, allTestsSkipped) {
|
|
324
369
|
logTestOptimizationSummary({
|
|
325
370
|
attemptToFixExecutions,
|
|
326
|
-
extraSections: [
|
|
371
|
+
extraSections: [
|
|
372
|
+
formatIgnoredFailuresSummary(ignoredFailures),
|
|
373
|
+
allTestsSkipped ? TEST_IMPACT_ANALYSIS_ALL_TESTS_SKIPPED_MESSAGE : '',
|
|
374
|
+
],
|
|
327
375
|
newTestsWithDynamicNames,
|
|
328
376
|
})
|
|
329
377
|
loggedAttemptToFixTests.clear()
|
|
@@ -349,11 +397,11 @@ function getAttemptToFixExecutionsFromJestResults (result) {
|
|
|
349
397
|
|
|
350
398
|
for (const { fullName, status } of testResults) {
|
|
351
399
|
const testName = removeSeedSuffixFromTestName(fullName)
|
|
352
|
-
const testStatus = getTestStatusFromJestResult(status)
|
|
353
|
-
if (!testStatus) continue
|
|
354
|
-
|
|
355
400
|
const testManagementTest = testManagementTestsForSuite[testName]?.properties
|
|
356
401
|
if (!testManagementTest?.attempt_to_fix) continue
|
|
402
|
+
const testStatus = getTestStatusFromJestResult(status) ||
|
|
403
|
+
(status === 'pending' || status === 'todo' ? 'skip' : undefined)
|
|
404
|
+
if (!testStatus) continue
|
|
357
405
|
|
|
358
406
|
recordAttemptToFixExecution(executions, {
|
|
359
407
|
testSuite,
|
|
@@ -368,6 +416,51 @@ function getAttemptToFixExecutionsFromJestResults (result) {
|
|
|
368
416
|
return executions
|
|
369
417
|
}
|
|
370
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Records test-management results reported by Jest after all retries have finished.
|
|
421
|
+
*
|
|
422
|
+
* @param {{
|
|
423
|
+
* globalConfig?: { rootDir?: string },
|
|
424
|
+
* results: {
|
|
425
|
+
* testResults: Array<{
|
|
426
|
+
* testResults: Array<{ fullName: string, status: string }>,
|
|
427
|
+
* testFilePath: string
|
|
428
|
+
* }>
|
|
429
|
+
* }
|
|
430
|
+
* }} result
|
|
431
|
+
* @param {string[]} quarantineFailureNames
|
|
432
|
+
*/
|
|
433
|
+
function recordTestManagementExecutionsFromJestResults (result, quarantineFailureNames) {
|
|
434
|
+
const rootDir = result.globalConfig?.rootDir || process.cwd()
|
|
435
|
+
const failedQuarantinedTests = new Set(quarantineFailureNames)
|
|
436
|
+
|
|
437
|
+
for (const { testResults, testFilePath } of result.results.testResults) {
|
|
438
|
+
const testSuite = getTestSuitePath(testFilePath, rootDir)
|
|
439
|
+
const testManagementTestsForSuite = testManagementTests
|
|
440
|
+
?.jest
|
|
441
|
+
?.suites
|
|
442
|
+
?.[testSuite]
|
|
443
|
+
?.tests
|
|
444
|
+
if (!testManagementTestsForSuite) continue
|
|
445
|
+
|
|
446
|
+
for (const { fullName, status } of testResults) {
|
|
447
|
+
const testName = removeSeedSuffixFromTestName(fullName)
|
|
448
|
+
const testManagementTest = testManagementTestsForSuite[testName]?.properties
|
|
449
|
+
if (!testManagementTest) continue
|
|
450
|
+
|
|
451
|
+
const name = `${testSuite} › ${testName}`
|
|
452
|
+
recordTestManagementExecution({
|
|
453
|
+
testSuite,
|
|
454
|
+
testName,
|
|
455
|
+
status: failedQuarantinedTests.has(name) ? 'fail' : getTestStatusFromJestResult(status),
|
|
456
|
+
isAttemptToFix: testManagementTest.attempt_to_fix,
|
|
457
|
+
isDisabled: testManagementTest.disabled,
|
|
458
|
+
isQuarantined: testManagementTest.quarantined,
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
371
464
|
function wrapConsoleErrorForJestReferenceErrors () {
|
|
372
465
|
if (isConsoleErrorWrapped) return
|
|
373
466
|
|
|
@@ -465,8 +558,52 @@ function getWrappedCustomHandleTestEvent (handleTestEvent, datadogHandleTestEven
|
|
|
465
558
|
return wrappedHandleTestEvent
|
|
466
559
|
}
|
|
467
560
|
|
|
561
|
+
/**
|
|
562
|
+
* Mirrors the test selection checks Jest Circus performs after publishing `test_start`.
|
|
563
|
+
*
|
|
564
|
+
* @param {object} test
|
|
565
|
+
* @param {boolean} hasFocusedTests
|
|
566
|
+
* @param {RegExp|undefined} testNamePattern
|
|
567
|
+
* @returns {boolean}
|
|
568
|
+
*/
|
|
569
|
+
function isJestTestSkipped (test, hasFocusedTests, testNamePattern) {
|
|
570
|
+
if (
|
|
571
|
+
test.mode === 'skip' ||
|
|
572
|
+
(hasFocusedTests && test.mode !== 'only') ||
|
|
573
|
+
(testNamePattern && !testNamePattern.test(getRawJestTestName(test)))
|
|
574
|
+
) {
|
|
575
|
+
return true
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
let parent = test.parent
|
|
579
|
+
while (parent) {
|
|
580
|
+
if (parent.mode === 'skip') return true
|
|
581
|
+
parent = parent.parent
|
|
582
|
+
}
|
|
583
|
+
return false
|
|
584
|
+
}
|
|
585
|
+
|
|
468
586
|
function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
587
|
+
const hasConcurrentTestsStartEvent = satisfies(jestVersion, '>=30.0.0')
|
|
588
|
+
const hasTestsInChildren = satisfies(jestVersion, '>=26.0.0')
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* @param {object} describeBlock
|
|
592
|
+
* @returns {object[]|undefined}
|
|
593
|
+
*/
|
|
594
|
+
function getTestEntries (describeBlock) {
|
|
595
|
+
return hasTestsInChildren ? describeBlock?.children : describeBlock?.tests
|
|
596
|
+
}
|
|
597
|
+
|
|
469
598
|
return class DatadogEnvironment extends BaseEnvironment {
|
|
599
|
+
#activeDetachedEfdRetries
|
|
600
|
+
#detachedEfdRetryConcurrency
|
|
601
|
+
#detachedEfdRetryQueue
|
|
602
|
+
#discardedEfdRetryTests
|
|
603
|
+
#earlyFlakeDetectionRetryPolicy
|
|
604
|
+
#efdRetryGatesByName
|
|
605
|
+
#pendingPre30ConcurrentTests
|
|
606
|
+
|
|
470
607
|
constructor (config, context) {
|
|
471
608
|
super(config, context)
|
|
472
609
|
const rootDir = config.globalConfig ? config.globalConfig.rootDir : config.rootDir
|
|
@@ -504,11 +641,16 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
504
641
|
this.concurrentTestContexts = new Map()
|
|
505
642
|
this.concurrentTestStates = new WeakMap()
|
|
506
643
|
this.concurrentTestSourceFns = new WeakMap()
|
|
644
|
+
this.testParametersByFunction = new WeakMap()
|
|
507
645
|
this.wrappedConcurrentTestFunctions = new WeakSet()
|
|
508
646
|
this.jestEachBind = undefined
|
|
647
|
+
this.#activeDetachedEfdRetries = 0
|
|
648
|
+
this.#detachedEfdRetryConcurrency = 1
|
|
649
|
+
this.#pendingPre30ConcurrentTests = 0
|
|
650
|
+
this.#earlyFlakeDetectionRetryPolicy =
|
|
651
|
+
this.testEnvironmentOptions._ddEarlyFlakeDetectionRetryPolicy ?? EMPTY_EFD_RETRY_POLICY
|
|
509
652
|
|
|
510
653
|
if (this.isKnownTestsEnabled) {
|
|
511
|
-
earlyFlakeDetectionSlowTestRetries = this.testEnvironmentOptions._ddEarlyFlakeDetectionSlowTestRetries ?? {}
|
|
512
654
|
try {
|
|
513
655
|
this.knownTestsForThisSuite = this.getKnownTestsForSuite(this.testEnvironmentOptions._ddKnownTests)
|
|
514
656
|
|
|
@@ -579,6 +721,53 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
579
721
|
}
|
|
580
722
|
}
|
|
581
723
|
|
|
724
|
+
/**
|
|
725
|
+
* Rebuilds serial `test.each` so every generated row function keeps its parameters.
|
|
726
|
+
*
|
|
727
|
+
* @param {Function|undefined} test
|
|
728
|
+
* @returns {void}
|
|
729
|
+
*/
|
|
730
|
+
bindTestEach (test) {
|
|
731
|
+
if (typeof test?.each !== 'function') return
|
|
732
|
+
|
|
733
|
+
const bind = this.getJestEachBind()
|
|
734
|
+
if (typeof bind !== 'function') {
|
|
735
|
+
const environment = this
|
|
736
|
+
shimmer.wrap(test, 'each', each => function (...args) {
|
|
737
|
+
const testParameters = getFormattedJestTestParameters(args)
|
|
738
|
+
const eachBind = each.apply(this, args)
|
|
739
|
+
return function (...args) {
|
|
740
|
+
const [testName] = args
|
|
741
|
+
environment.setNameToParams(testName, testParameters)
|
|
742
|
+
return eachBind.apply(this, args)
|
|
743
|
+
}
|
|
744
|
+
})
|
|
745
|
+
return
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Jest creates each row function at runtime, so Orchestrion cannot associate parameters statically.
|
|
749
|
+
const environment = this
|
|
750
|
+
test.each = function wrappedTestEach (...eachArgs) {
|
|
751
|
+
const testParameters = getFormattedJestTestParameters(eachArgs)
|
|
752
|
+
return function (...testArgs) {
|
|
753
|
+
let parameterIndex = 0
|
|
754
|
+
const eachTest = function (testName, testFn, ...callArgs) {
|
|
755
|
+
const parameters = testParameters?.[parameterIndex++]
|
|
756
|
+
if (parameters !== undefined && typeof testFn === 'function') {
|
|
757
|
+
environment.appendNameToParams(testName, parameters)
|
|
758
|
+
environment.testParametersByFunction.set(
|
|
759
|
+
testFn,
|
|
760
|
+
getTestParametersString(environment.nameToParams, testName)
|
|
761
|
+
)
|
|
762
|
+
}
|
|
763
|
+
return test.call(this, testName, testFn, ...callArgs)
|
|
764
|
+
}
|
|
765
|
+
const eachBind = bind(eachTest).apply(this, eachArgs)
|
|
766
|
+
return eachBind.apply(this, testArgs)
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
582
771
|
/**
|
|
583
772
|
* Wraps Jest's concurrent test registration methods so eager concurrent bodies
|
|
584
773
|
* in older Jest versions execute inside their test span context.
|
|
@@ -604,10 +793,10 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
604
793
|
const concurrentTest = test?.concurrent
|
|
605
794
|
if (typeof concurrentTest !== 'function') return
|
|
606
795
|
|
|
607
|
-
this.wrapConcurrentTestFunction(test, 'concurrent', state
|
|
608
|
-
this.wrapConcurrentTestFunction(test.concurrent, 'only', state
|
|
609
|
-
this.wrapConcurrentTestFunction(test.concurrent, 'failing', state
|
|
610
|
-
this.wrapConcurrentTestFunction(test.concurrent.only, 'failing', state
|
|
796
|
+
this.wrapConcurrentTestFunction(test, 'concurrent', state)
|
|
797
|
+
this.wrapConcurrentTestFunction(test.concurrent, 'only', state)
|
|
798
|
+
this.wrapConcurrentTestFunction(test.concurrent, 'failing', state)
|
|
799
|
+
this.wrapConcurrentTestFunction(test.concurrent.only, 'failing', state)
|
|
611
800
|
}
|
|
612
801
|
|
|
613
802
|
/**
|
|
@@ -616,11 +805,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
616
805
|
* @param {object} target
|
|
617
806
|
* @param {string} methodName
|
|
618
807
|
* @param {object} state
|
|
619
|
-
* @param {Function|undefined} serialTest
|
|
620
|
-
* @param {unknown} serialTestThisArg
|
|
621
808
|
* @returns {void}
|
|
622
809
|
*/
|
|
623
|
-
wrapConcurrentTestFunction (target, methodName, state
|
|
810
|
+
wrapConcurrentTestFunction (target, methodName, state) {
|
|
624
811
|
let concurrentTest = target?.[methodName]
|
|
625
812
|
if (typeof concurrentTest !== 'function') return
|
|
626
813
|
if (this.wrappedConcurrentTestFunctions.has(concurrentTest)) return
|
|
@@ -646,9 +833,8 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
646
833
|
const wrappedTestFn = environment.createConcurrentTestFn(testName, testFn, asyncError, state, {
|
|
647
834
|
concurrentTest,
|
|
648
835
|
concurrentTestThisArg: this,
|
|
649
|
-
serialTest,
|
|
650
|
-
serialTestThisArg,
|
|
651
836
|
sourceTestFn: environment.concurrentTestSourceFns.get(testFn),
|
|
837
|
+
timeout: args[0],
|
|
652
838
|
})
|
|
653
839
|
return concurrentTest.call(this, testName, wrappedTestFn, ...args)
|
|
654
840
|
}
|
|
@@ -711,8 +897,6 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
711
897
|
concurrentTestThisArg: options?.concurrentTestThisArg,
|
|
712
898
|
ctx: this.createConcurrentTestContext(testName, testFn, asyncError, state, options),
|
|
713
899
|
numExecutions: 0,
|
|
714
|
-
serialTest: options?.serialTest,
|
|
715
|
-
serialTestThisArg: options?.serialTestThisArg,
|
|
716
900
|
state,
|
|
717
901
|
testFn,
|
|
718
902
|
}
|
|
@@ -777,6 +961,8 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
777
961
|
frameworkVersion: jestVersion,
|
|
778
962
|
isNew: isNewTest,
|
|
779
963
|
isEfdRetry: options?.isEfdRetry === true,
|
|
964
|
+
efdRetryGates: options?.efdRetryGates,
|
|
965
|
+
efdRetryIndex: options?.efdRetryIndex,
|
|
780
966
|
isAttemptToFix,
|
|
781
967
|
isAttemptToFixRetry: options?.isAttemptToFixRetry === true,
|
|
782
968
|
isJestRetry: false,
|
|
@@ -785,6 +971,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
785
971
|
isModified: options?.isModified ?? this.isTestModified(asyncError, sourceTestFn),
|
|
786
972
|
hasDynamicName: isNewTest && DYNAMIC_NAME_RE.test(testFullName),
|
|
787
973
|
testSuiteAbsolutePath: this.testSuiteAbsolutePath,
|
|
974
|
+
testTimeout: options?.timeout || state.testTimeout,
|
|
788
975
|
}
|
|
789
976
|
let contexts = this.concurrentTestContexts.get(testFullName)
|
|
790
977
|
if (contexts) {
|
|
@@ -880,6 +1067,183 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
880
1067
|
}
|
|
881
1068
|
}
|
|
882
1069
|
|
|
1070
|
+
/**
|
|
1071
|
+
* @param {object} concurrentTestState
|
|
1072
|
+
* @param {number|undefined} startedAt
|
|
1073
|
+
* @returns {void}
|
|
1074
|
+
*/
|
|
1075
|
+
completePre30ConcurrentTest (concurrentTestState, startedAt) {
|
|
1076
|
+
if (!concurrentTestState.trackEfdCompletion) return
|
|
1077
|
+
|
|
1078
|
+
concurrentTestState.trackEfdCompletion = false
|
|
1079
|
+
const { ctx } = concurrentTestState
|
|
1080
|
+
if (startedAt !== undefined) {
|
|
1081
|
+
ctx.efdDurationMs = performance.now() - startedAt
|
|
1082
|
+
this.determineEfdRetries(ctx.name, ctx.efdDurationMs)
|
|
1083
|
+
}
|
|
1084
|
+
this.#pendingPre30ConcurrentTests--
|
|
1085
|
+
this.drainDetachedEfdRetries()
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* @param {object} concurrentTestState
|
|
1090
|
+
* @param {unknown} result
|
|
1091
|
+
* @param {number|undefined} startedAt
|
|
1092
|
+
* @returns {unknown}
|
|
1093
|
+
*/
|
|
1094
|
+
trackPre30ConcurrentTest (concurrentTestState, result, startedAt) {
|
|
1095
|
+
if (typeof result?.then !== 'function') {
|
|
1096
|
+
this.completePre30ConcurrentTest(concurrentTestState, startedAt)
|
|
1097
|
+
return result
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return Promise.resolve(result).finally(() => {
|
|
1101
|
+
this.completePre30ConcurrentTest(concurrentTestState, startedAt)
|
|
1102
|
+
})
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* @param {EfdRetryGate} retryGate
|
|
1107
|
+
* @param {object} ctx
|
|
1108
|
+
* @param {() => unknown} run
|
|
1109
|
+
* @returns {Promise<void>}
|
|
1110
|
+
*/
|
|
1111
|
+
enqueueDetachedEfdRetry (retryGate, ctx, run) {
|
|
1112
|
+
/**
|
|
1113
|
+
* @param {() => void} resolve
|
|
1114
|
+
* @returns {void}
|
|
1115
|
+
*/
|
|
1116
|
+
const enqueue = (resolve) => {
|
|
1117
|
+
/**
|
|
1118
|
+
* @param {boolean} shouldRun
|
|
1119
|
+
* @returns {void}
|
|
1120
|
+
*/
|
|
1121
|
+
const onDecision = (shouldRun) => {
|
|
1122
|
+
if (!shouldRun) {
|
|
1123
|
+
ctx.isDiscardedEfdRetry = true
|
|
1124
|
+
resolve()
|
|
1125
|
+
return
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* @type {() => void}
|
|
1130
|
+
*/
|
|
1131
|
+
let start
|
|
1132
|
+
const startPromise = new Promise(resolve => {
|
|
1133
|
+
start = resolve
|
|
1134
|
+
})
|
|
1135
|
+
const retry = { ctx, resolve, start }
|
|
1136
|
+
/**
|
|
1137
|
+
* @param {unknown} error
|
|
1138
|
+
* @returns {void}
|
|
1139
|
+
*/
|
|
1140
|
+
const onRejected = (error) => {
|
|
1141
|
+
retry.ctx.detachedEfdRetryError = error
|
|
1142
|
+
this.finishDetachedEfdRetry(retry)
|
|
1143
|
+
}
|
|
1144
|
+
startPromise.then(() => this.runDetachedEfdRetryWithTimeout(ctx, run)).then(
|
|
1145
|
+
() => this.finishDetachedEfdRetry(retry),
|
|
1146
|
+
onRejected
|
|
1147
|
+
)
|
|
1148
|
+
this.#detachedEfdRetryQueue ??= []
|
|
1149
|
+
this.#detachedEfdRetryQueue.push(retry)
|
|
1150
|
+
this.drainDetachedEfdRetries()
|
|
1151
|
+
}
|
|
1152
|
+
retryGate.promise.then(onDecision)
|
|
1153
|
+
}
|
|
1154
|
+
return new Promise(enqueue)
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* Applies the timeout Jest 28/29 cannot start around a detached concurrent retry body.
|
|
1159
|
+
*
|
|
1160
|
+
* @param {object} ctx
|
|
1161
|
+
* @param {() => unknown} run
|
|
1162
|
+
* @returns {Promise<void>}
|
|
1163
|
+
*/
|
|
1164
|
+
runDetachedEfdRetryWithTimeout (ctx, run) {
|
|
1165
|
+
return new Promise((resolve, reject) => {
|
|
1166
|
+
let completed = false
|
|
1167
|
+
const timeoutId = realSetTimeout(() => {
|
|
1168
|
+
if (completed) return
|
|
1169
|
+
|
|
1170
|
+
completed = true
|
|
1171
|
+
reject(new Error(`Exceeded timeout of ${ctx.testTimeout} ms for a test.`))
|
|
1172
|
+
}, ctx.testTimeout)
|
|
1173
|
+
|
|
1174
|
+
const settle = (callback, value) => {
|
|
1175
|
+
if (completed) return
|
|
1176
|
+
|
|
1177
|
+
completed = true
|
|
1178
|
+
timeoutId.unref?.()
|
|
1179
|
+
realClearTimeout(timeoutId)
|
|
1180
|
+
callback(value)
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
let result
|
|
1184
|
+
try {
|
|
1185
|
+
result = run()
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
settle(reject, error)
|
|
1188
|
+
return
|
|
1189
|
+
}
|
|
1190
|
+
Promise.resolve(result).then(
|
|
1191
|
+
() => settle(resolve),
|
|
1192
|
+
error => settle(reject, error)
|
|
1193
|
+
)
|
|
1194
|
+
})
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* @returns {void}
|
|
1199
|
+
*/
|
|
1200
|
+
drainDetachedEfdRetries () {
|
|
1201
|
+
const queue = this.#detachedEfdRetryQueue
|
|
1202
|
+
if (!queue) return
|
|
1203
|
+
|
|
1204
|
+
while (
|
|
1205
|
+
this.#pendingPre30ConcurrentTests === 0 &&
|
|
1206
|
+
this.#activeDetachedEfdRetries < this.#detachedEfdRetryConcurrency &&
|
|
1207
|
+
queue.length > 0
|
|
1208
|
+
) {
|
|
1209
|
+
this.#activeDetachedEfdRetries++
|
|
1210
|
+
queue.shift().start()
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* @param {DetachedEfdRetry} retry
|
|
1216
|
+
* @returns {void}
|
|
1217
|
+
*/
|
|
1218
|
+
finishDetachedEfdRetry (retry) {
|
|
1219
|
+
this.#activeDetachedEfdRetries--
|
|
1220
|
+
retry.resolve()
|
|
1221
|
+
this.drainDetachedEfdRetries()
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Preserves Jest's immediate error when a callback test also returns a value.
|
|
1226
|
+
*
|
|
1227
|
+
* @param {() => unknown} run
|
|
1228
|
+
* @param {(error?: Error) => void} done
|
|
1229
|
+
* @returns {void}
|
|
1230
|
+
*/
|
|
1231
|
+
runCallbackEfdRetry (run, done) {
|
|
1232
|
+
let result
|
|
1233
|
+
try {
|
|
1234
|
+
result = run()
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
done(error)
|
|
1237
|
+
return
|
|
1238
|
+
}
|
|
1239
|
+
if (result !== undefined) {
|
|
1240
|
+
done(new Error(
|
|
1241
|
+
"Test functions cannot both take a 'done' callback and return something. " +
|
|
1242
|
+
"Either use a 'done' callback, or return a promise."
|
|
1243
|
+
))
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
883
1247
|
/**
|
|
884
1248
|
* Runs a concurrent test function inside its test span context.
|
|
885
1249
|
*
|
|
@@ -896,24 +1260,51 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
896
1260
|
if (typeof done === 'function') {
|
|
897
1261
|
done()
|
|
898
1262
|
}
|
|
1263
|
+
this.completePre30ConcurrentTest(concurrentTestState)
|
|
899
1264
|
return
|
|
900
1265
|
}
|
|
901
1266
|
|
|
902
1267
|
const runTest = () => testFn.apply(thisArg, args)
|
|
903
|
-
|
|
904
|
-
ctx.
|
|
1268
|
+
const runTestInContext = () => {
|
|
1269
|
+
if (!ctx.isAttemptToFixRetry && !ctx.isEfdRetry) {
|
|
1270
|
+
ctx.isJestRetry = concurrentTestState.numExecutions > 0
|
|
1271
|
+
}
|
|
1272
|
+
concurrentTestState.numExecutions++
|
|
1273
|
+
|
|
1274
|
+
if (ctx.currentStore) {
|
|
1275
|
+
return testFnCh.runStores(ctx, runTest)
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
let result
|
|
1279
|
+
testStartCh.runStores(ctx, () => {
|
|
1280
|
+
result = testFnCh.runStores(ctx, runTest)
|
|
1281
|
+
})
|
|
1282
|
+
return result
|
|
905
1283
|
}
|
|
906
|
-
concurrentTestState.numExecutions++
|
|
907
1284
|
|
|
908
|
-
|
|
909
|
-
|
|
1285
|
+
const retryGate = ctx.efdRetryGates?.[ctx.efdRetryIndex - 1]
|
|
1286
|
+
if (retryGate && !hasConcurrentTestsStartEvent) {
|
|
1287
|
+
ctx.detachedEfdRetryPromise = this.enqueueDetachedEfdRetry(retryGate, ctx, runTestInContext)
|
|
1288
|
+
return
|
|
1289
|
+
}
|
|
1290
|
+
if (retryGate && typeof args[0] === 'function') {
|
|
1291
|
+
this.runCallbackEfdRetry(runTestInContext, args[0])
|
|
1292
|
+
return
|
|
910
1293
|
}
|
|
911
1294
|
|
|
1295
|
+
if (!concurrentTestState.trackEfdCompletion) {
|
|
1296
|
+
return runTestInContext()
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const startedAt = efdCandidates.has(ctx.name) ? performance.now() : undefined
|
|
912
1300
|
let result
|
|
913
|
-
|
|
914
|
-
result =
|
|
915
|
-
})
|
|
916
|
-
|
|
1301
|
+
try {
|
|
1302
|
+
result = runTestInContext()
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
this.completePre30ConcurrentTest(concurrentTestState, startedAt)
|
|
1305
|
+
throw error
|
|
1306
|
+
}
|
|
1307
|
+
return this.trackPre30ConcurrentTest(concurrentTestState, result, startedAt)
|
|
917
1308
|
}
|
|
918
1309
|
|
|
919
1310
|
/**
|
|
@@ -1047,30 +1438,63 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
1047
1438
|
return result
|
|
1048
1439
|
}
|
|
1049
1440
|
|
|
1050
|
-
|
|
1441
|
+
/**
|
|
1442
|
+
* @param {number} retryCount
|
|
1443
|
+
* @returns {EfdRetryGate[]}
|
|
1444
|
+
*/
|
|
1445
|
+
createEfdRetryGates (retryCount) {
|
|
1446
|
+
const gates = []
|
|
1447
|
+
for (let retryIndex = 0; retryIndex < retryCount; retryIndex++) {
|
|
1448
|
+
/** @type {(shouldRun: boolean) => void} */
|
|
1449
|
+
let resolveGate
|
|
1450
|
+
const promise = new Promise(resolve => {
|
|
1451
|
+
resolveGate = resolve
|
|
1452
|
+
})
|
|
1453
|
+
gates.push({ promise, resolve: resolveGate })
|
|
1454
|
+
}
|
|
1455
|
+
return gates
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* Registers retry tests while Jest still accepts additions to the test tree.
|
|
1460
|
+
*
|
|
1461
|
+
* @param {JestRetryOptions} options
|
|
1462
|
+
* @returns {number}
|
|
1463
|
+
*/
|
|
1051
1464
|
retryTest ({
|
|
1465
|
+
concurrentTestState: registeredConcurrentTestState,
|
|
1466
|
+
efdRetryGates,
|
|
1052
1467
|
jestEvent,
|
|
1053
|
-
|
|
1468
|
+
state,
|
|
1469
|
+
isModified,
|
|
1054
1470
|
retryCount,
|
|
1055
1471
|
retryType,
|
|
1056
1472
|
}) {
|
|
1057
|
-
const { testName, fn, timeout } = jestEvent
|
|
1473
|
+
const { testName, fn, mode, timeout } = jestEvent
|
|
1058
1474
|
const isAttemptToFixRetry = retryType === 'Test Management (Attempt to Fix)'
|
|
1059
1475
|
const isEfdRetry = retryType === 'Impacted tests' || retryType === 'Early flake detection'
|
|
1060
|
-
|
|
1061
|
-
|
|
1476
|
+
const concurrentTestState = registeredConcurrentTestState || this.concurrentTestStates.get(fn)
|
|
1477
|
+
let originalTest
|
|
1478
|
+
if (isEfdRetry && !efdRetryGates) {
|
|
1479
|
+
originalTest = getTestEntries(state.currentDescribeBlock)?.at(-1)
|
|
1480
|
+
if (originalTest?.fn !== fn) {
|
|
1481
|
+
log.error('%s could not retain its original Jest test', retryType)
|
|
1482
|
+
return 0
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
let registeredRetryCount = 0
|
|
1486
|
+
for (let retryIndex = 1; retryIndex <= retryCount; retryIndex++) {
|
|
1487
|
+
const testEntries = getTestEntries(state.currentDescribeBlock)
|
|
1488
|
+
const initialTestCount = testEntries?.length
|
|
1489
|
+
let retryFn
|
|
1062
1490
|
if (concurrentTestState) {
|
|
1063
|
-
const test =
|
|
1064
|
-
|
|
1065
|
-
: (
|
|
1066
|
-
concurrentTestState.concurrentTest ||
|
|
1067
|
-
getOriginalConcurrentTest(this.global.test?.concurrent)
|
|
1068
|
-
)
|
|
1491
|
+
const test = concurrentTestState.concurrentTest ??
|
|
1492
|
+
getOriginalConcurrentTest(this.global.test?.concurrent)
|
|
1069
1493
|
if (typeof test !== 'function') {
|
|
1070
1494
|
log.error('%s could not retry concurrent test because test is undefined', retryType)
|
|
1071
1495
|
continue
|
|
1072
1496
|
}
|
|
1073
|
-
|
|
1497
|
+
retryFn = this.createConcurrentTestFn(
|
|
1074
1498
|
testName,
|
|
1075
1499
|
concurrentTestState.testFn,
|
|
1076
1500
|
undefined,
|
|
@@ -1078,65 +1502,427 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
1078
1502
|
{
|
|
1079
1503
|
concurrentTest: concurrentTestState.concurrentTest,
|
|
1080
1504
|
concurrentTestThisArg: concurrentTestState.concurrentTestThisArg,
|
|
1505
|
+
efdRetryGates,
|
|
1506
|
+
efdRetryIndex: retryIndex,
|
|
1081
1507
|
isAttemptToFixRetry,
|
|
1082
1508
|
isEfdRetry,
|
|
1083
|
-
isModified: concurrentTestState.ctx.isModified,
|
|
1084
|
-
serialTest: concurrentTestState.serialTest,
|
|
1085
|
-
serialTestThisArg: concurrentTestState.serialTestThisArg,
|
|
1509
|
+
isModified: isModified ?? concurrentTestState.ctx.isModified,
|
|
1086
1510
|
testParameters: concurrentTestState.ctx.testParameters,
|
|
1511
|
+
timeout,
|
|
1087
1512
|
}
|
|
1088
1513
|
)
|
|
1089
|
-
|
|
1090
|
-
? concurrentTestState.serialTestThisArg
|
|
1091
|
-
: concurrentTestState.concurrentTestThisArg
|
|
1092
|
-
test.call(thisArg, testName, retryFn, timeout)
|
|
1514
|
+
test.call(concurrentTestState.concurrentTestThisArg, testName, retryFn, timeout)
|
|
1093
1515
|
} else if (this.global.test) {
|
|
1094
|
-
this.global.test
|
|
1516
|
+
const test = mode === 'only' ? this.global.test.only : this.global.test
|
|
1517
|
+
// The child-count check below proves Jest added a node, so retries can share the test
|
|
1518
|
+
// function instead of retaining one wrapper per scheduled execution.
|
|
1519
|
+
retryFn = fn
|
|
1520
|
+
test(testName, retryFn, timeout)
|
|
1095
1521
|
} else {
|
|
1096
1522
|
log.error('%s could not retry test because global.test is undefined', retryType)
|
|
1523
|
+
continue
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
if (isEfdRetry) {
|
|
1527
|
+
const retryTest = testEntries?.length === initialTestCount + 1 ? testEntries.at(-1) : undefined
|
|
1528
|
+
if (retryTest?.fn !== retryFn) {
|
|
1529
|
+
log.error('%s could not retain its pre-registered Jest retry', retryType)
|
|
1530
|
+
continue
|
|
1531
|
+
}
|
|
1532
|
+
efdRetryMetadataByTest.set(retryTest, {
|
|
1533
|
+
isModified: Boolean(isModified),
|
|
1534
|
+
originalTest,
|
|
1535
|
+
retryIndex,
|
|
1536
|
+
})
|
|
1097
1537
|
}
|
|
1538
|
+
registeredRetryCount++
|
|
1098
1539
|
}
|
|
1540
|
+
return registeredRetryCount
|
|
1099
1541
|
}
|
|
1100
1542
|
|
|
1101
|
-
|
|
1543
|
+
/**
|
|
1544
|
+
* Registers retries for the test Jest just added, keeping a registration failure out of the
|
|
1545
|
+
* `add_test` dispatch, where Jest would report it as a test file error.
|
|
1546
|
+
*
|
|
1547
|
+
* @param {JestRetryOptions} retryOptions
|
|
1548
|
+
* @returns {boolean}
|
|
1549
|
+
*/
|
|
1550
|
+
registerRetryTests (retryOptions) {
|
|
1551
|
+
// Jest turns later additions into "Cannot add a test after tests have started running".
|
|
1552
|
+
if (retryOptions.state.hasStarted) {
|
|
1553
|
+
log.error('%s could not register retries after Jest started', retryOptions.retryType)
|
|
1554
|
+
return false
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
const testEntries = getTestEntries(retryOptions.state.currentDescribeBlock)
|
|
1558
|
+
const initialTestCount = testEntries?.length
|
|
1559
|
+
try {
|
|
1560
|
+
const registeredRetryCount = this.retryTest(retryOptions)
|
|
1561
|
+
if (registeredRetryCount === retryOptions.retryCount) return true
|
|
1562
|
+
} catch (error) {
|
|
1563
|
+
log.error('%s could not register retries', retryOptions.retryType, error)
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
if (testEntries && initialTestCount !== undefined) {
|
|
1567
|
+
const retryTests = testEntries.splice(initialTestCount)
|
|
1568
|
+
for (const retryTest of retryTests) {
|
|
1569
|
+
removedRetryTests.add(retryTest)
|
|
1570
|
+
const retryCtx = this.concurrentTestStates.get(retryTest.fn)?.ctx
|
|
1571
|
+
if (retryCtx) {
|
|
1572
|
+
this.removeConcurrentTestContext(retryOptions.testFullName, retryCtx)
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
const tests = retryOptions.state.currentDescribeBlock?.tests
|
|
1576
|
+
if (tests) {
|
|
1577
|
+
let writeIndex = 0
|
|
1578
|
+
for (const test of tests) {
|
|
1579
|
+
if (!removedRetryTests.has(test)) {
|
|
1580
|
+
tests[writeIndex++] = test
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
tests.length = writeIndex
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
this.#efdRetryGatesByName?.delete(retryOptions.testFullName)
|
|
1587
|
+
log.error('%s did not register every retry', retryOptions.retryType)
|
|
1588
|
+
return false
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/**
|
|
1592
|
+
* Drops the pre-registered EFD retries that exceed the retry count Jest may still run.
|
|
1593
|
+
*
|
|
1594
|
+
* @param {string} testName
|
|
1595
|
+
* @param {object} executedTest The Jest test node that selected the retry count.
|
|
1596
|
+
* @param {number} [retryCount] Retries to keep. Every retry is dropped when omitted.
|
|
1597
|
+
* @returns {number} Retries left to run.
|
|
1598
|
+
*/
|
|
1599
|
+
discardEfdRetries (testName, executedTest, retryCount = 0) {
|
|
1600
|
+
const siblings = getTestEntries(executedTest.parent)
|
|
1601
|
+
const executedTestIndex = siblings ? siblings.indexOf(executedTest) : -1
|
|
1602
|
+
if (executedTestIndex === -1) return 0
|
|
1603
|
+
|
|
1604
|
+
const originalTest = efdRetryMetadataByTest.get(executedTest)?.originalTest ?? executedTest
|
|
1605
|
+
|
|
1606
|
+
// Circus iterates children live, so only entries after the test that just ran may move.
|
|
1607
|
+
// Whichever pre-registered slot runs first is the original execution; the next retryCount
|
|
1608
|
+
// slots are its retries, independent of Jest's randomized physical node order.
|
|
1609
|
+
let runnableRetryCount = 0
|
|
1610
|
+
let writeIndex = executedTestIndex + 1
|
|
1611
|
+
for (let readIndex = writeIndex; readIndex < siblings.length; readIndex++) {
|
|
1612
|
+
const sibling = siblings[readIndex]
|
|
1613
|
+
const siblingOriginalTest = efdRetryMetadataByTest.get(sibling)?.originalTest
|
|
1614
|
+
if (sibling === originalTest || siblingOriginalTest === originalTest) {
|
|
1615
|
+
if (runnableRetryCount >= retryCount) {
|
|
1616
|
+
removedRetryTests.add(sibling)
|
|
1617
|
+
const discardedCtx = this.concurrentTestStates.get(sibling.fn)?.ctx
|
|
1618
|
+
if (discardedCtx) {
|
|
1619
|
+
this.removeConcurrentTestContext(testName, discardedCtx)
|
|
1620
|
+
}
|
|
1621
|
+
continue
|
|
1622
|
+
}
|
|
1623
|
+
runnableRetryCount++
|
|
1624
|
+
}
|
|
1625
|
+
siblings[writeIndex++] = sibling
|
|
1626
|
+
}
|
|
1627
|
+
siblings.length = writeIndex
|
|
1628
|
+
|
|
1629
|
+
const tests = executedTest.parent?.tests
|
|
1630
|
+
if (tests) {
|
|
1631
|
+
writeIndex = 0
|
|
1632
|
+
for (const test of tests) {
|
|
1633
|
+
if (!removedRetryTests.has(test)) {
|
|
1634
|
+
tests[writeIndex++] = test
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
tests.length = writeIndex
|
|
1638
|
+
}
|
|
1639
|
+
return runnableRetryCount
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
/**
|
|
1643
|
+
* @param {object} describeBlock
|
|
1644
|
+
* @param {boolean} hasFocusedTests
|
|
1645
|
+
* @param {RegExp|undefined} testNamePattern
|
|
1646
|
+
* @returns {number}
|
|
1647
|
+
*/
|
|
1648
|
+
markPre30ConcurrentTests (describeBlock, hasFocusedTests, testNamePattern) {
|
|
1649
|
+
if (describeBlock.mode === 'skip') return 0
|
|
1650
|
+
|
|
1651
|
+
let testCount = 0
|
|
1652
|
+
for (const child of describeBlock.children) {
|
|
1653
|
+
if (child.type === 'describeBlock') {
|
|
1654
|
+
testCount += this.markPre30ConcurrentTests(child, hasFocusedTests, testNamePattern)
|
|
1655
|
+
continue
|
|
1656
|
+
}
|
|
1657
|
+
if (
|
|
1658
|
+
child.type !== 'test' ||
|
|
1659
|
+
!child.concurrent ||
|
|
1660
|
+
efdRetryMetadataByTest.has(child) ||
|
|
1661
|
+
isJestTestSkipped(child, hasFocusedTests, testNamePattern)
|
|
1662
|
+
) {
|
|
1663
|
+
continue
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
const concurrentTestState = this.concurrentTestStates.get(child.fn)
|
|
1667
|
+
if (concurrentTestState) {
|
|
1668
|
+
concurrentTestState.trackEfdCompletion = true
|
|
1669
|
+
testCount++
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
return testCount
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
/**
|
|
1676
|
+
* @param {object} describeBlock
|
|
1677
|
+
* @param {object} state
|
|
1678
|
+
* @returns {void}
|
|
1679
|
+
*/
|
|
1680
|
+
preparePre30ConcurrentTests (describeBlock, state) {
|
|
1681
|
+
const testNamePattern = state.testNamePattern
|
|
1682
|
+
? new RegExp(state.testNamePattern.source, state.testNamePattern.flags)
|
|
1683
|
+
: undefined
|
|
1684
|
+
this.#detachedEfdRetryConcurrency = state.maxConcurrency ?? 1
|
|
1685
|
+
this.#detachedEfdRetryQueue = []
|
|
1686
|
+
this.#pendingPre30ConcurrentTests = this.markPre30ConcurrentTests(
|
|
1687
|
+
describeBlock,
|
|
1688
|
+
state.hasFocusedTests,
|
|
1689
|
+
testNamePattern
|
|
1690
|
+
)
|
|
1691
|
+
this.drainDetachedEfdRetries()
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
/**
|
|
1695
|
+
* @param {object[]} tests
|
|
1696
|
+
* @returns {void}
|
|
1697
|
+
*/
|
|
1698
|
+
orderConcurrentEfdRetries (tests) {
|
|
1699
|
+
if (!this.#efdRetryGatesByName) return
|
|
1700
|
+
|
|
1701
|
+
const retryTests = []
|
|
1702
|
+
let originalTestIndex = 0
|
|
1703
|
+
for (const test of tests) {
|
|
1704
|
+
if (efdRetryMetadataByTest.has(test)) {
|
|
1705
|
+
retryTests.push(test)
|
|
1706
|
+
} else {
|
|
1707
|
+
tests[originalTestIndex++] = test
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
for (const retryTest of retryTests) {
|
|
1711
|
+
tests[originalTestIndex++] = retryTest
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
/**
|
|
1716
|
+
* @param {string} testName
|
|
1717
|
+
* @param {number} durationMs
|
|
1718
|
+
* @returns {void}
|
|
1719
|
+
*/
|
|
1720
|
+
determineEfdRetries (testName, durationMs) {
|
|
1721
|
+
if (efdDeterminedRetries.has(testName)) return
|
|
1722
|
+
|
|
1723
|
+
const retryCount = getEfdRetryCountForDuration(durationMs, this.#earlyFlakeDetectionRetryPolicy)
|
|
1724
|
+
efdDeterminedRetries.set(testName, retryCount)
|
|
1725
|
+
efdExpectedExecutions.set(testName, retryCount + 1)
|
|
1726
|
+
const retryGates = this.#efdRetryGatesByName?.get(testName)
|
|
1727
|
+
if (retryGates) {
|
|
1728
|
+
for (let retryIndex = 0; retryIndex < retryGates.length; retryIndex++) {
|
|
1729
|
+
retryGates[retryIndex].resolve(retryIndex < retryCount)
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
if (retryCount === 0) {
|
|
1733
|
+
efdSlowAbortedTests.add(testName)
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
/**
|
|
1738
|
+
* @returns {void}
|
|
1739
|
+
*/
|
|
1740
|
+
removeDiscardedEfdRetries () {
|
|
1741
|
+
if (!this.#discardedEfdRetryTests) return
|
|
1742
|
+
|
|
1743
|
+
for (const retryTest of this.#discardedEfdRetryTests) {
|
|
1744
|
+
const siblings = getTestEntries(retryTest.parent)
|
|
1745
|
+
const retryTestIndex = siblings?.indexOf(retryTest) ?? -1
|
|
1746
|
+
if (retryTestIndex !== -1) {
|
|
1747
|
+
siblings.splice(retryTestIndex, 1)
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
this.#discardedEfdRetryTests = undefined
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// The `add_test` payload has no test node, so derive its full name from the current describe block.
|
|
1102
1754
|
getTestNameFromAddTestEvent (event, state) {
|
|
1103
1755
|
const describeSuffix = getRawJestTestName(state.currentDescribeBlock)
|
|
1104
1756
|
const testName = describeSuffix ? `${describeSuffix} ${event.testName}` : event.testName
|
|
1105
1757
|
return removeSeedSuffixFromTestName(testName)
|
|
1106
1758
|
}
|
|
1107
1759
|
|
|
1760
|
+
/**
|
|
1761
|
+
* Classifies a newly registered test and pre-registers every retry Jest may need.
|
|
1762
|
+
*
|
|
1763
|
+
* @param {object} event
|
|
1764
|
+
* @param {object} state
|
|
1765
|
+
* @returns {void}
|
|
1766
|
+
*/
|
|
1767
|
+
handleAddTestEvent (event, state) {
|
|
1768
|
+
if (event.concurrent) {
|
|
1769
|
+
this.hasConcurrentTests = true
|
|
1770
|
+
}
|
|
1771
|
+
if (event.failing) {
|
|
1772
|
+
return
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
const testFullName = this.getTestNameFromAddTestEvent(event, state)
|
|
1776
|
+
const concurrentTestContexts = this.concurrentTestContexts.get(testFullName)
|
|
1777
|
+
const concurrentTestState = this.concurrentTestStates.get(event.fn) ||
|
|
1778
|
+
concurrentTestContexts?.at(-1)?.concurrentTestState
|
|
1779
|
+
const registeredTest = getTestEntries(state.currentDescribeBlock)?.at(-1)
|
|
1780
|
+
if (concurrentTestState && registeredTest?.fn === event.fn) {
|
|
1781
|
+
testContexts.set(registeredTest, concurrentTestState.ctx)
|
|
1782
|
+
}
|
|
1783
|
+
if (retriedTestsToNumAttempts.has(testFullName)) {
|
|
1784
|
+
return
|
|
1785
|
+
}
|
|
1786
|
+
const isSkipped = event.mode === 'todo' || event.mode === 'skip'
|
|
1787
|
+
const isAttemptToFix = this.isTestManagementTestsEnabled &&
|
|
1788
|
+
this.testManagementTestsForThisSuite?.attemptToFix?.includes(testFullName)
|
|
1789
|
+
const isDisabled = this.isTestManagementTestsEnabled &&
|
|
1790
|
+
this.testManagementTestsForThisSuite?.disabled?.includes(testFullName)
|
|
1791
|
+
if (isAttemptToFix && !isSkipped) {
|
|
1792
|
+
retriedTestsToNumAttempts.set(testFullName, 0)
|
|
1793
|
+
testsToBeRetried.add(testFullName)
|
|
1794
|
+
const retriesRegistered = this.registerRetryTests({
|
|
1795
|
+
concurrentTestState,
|
|
1796
|
+
jestEvent: event,
|
|
1797
|
+
state,
|
|
1798
|
+
retryCount: testManagementAttemptToFixRetries,
|
|
1799
|
+
retryType: 'Test Management (Attempt to Fix)',
|
|
1800
|
+
testFullName,
|
|
1801
|
+
})
|
|
1802
|
+
if (!retriesRegistered) {
|
|
1803
|
+
retriedTestsToNumAttempts.delete(testFullName)
|
|
1804
|
+
testsToBeRetried.delete(testFullName)
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
const isModified = this.isImpactedTestsEnabled && (
|
|
1809
|
+
concurrentTestState?.ctx?.isModified ||
|
|
1810
|
+
this.isTestModified(event.asyncError, event.fn)
|
|
1811
|
+
)
|
|
1812
|
+
const isNew = this.isKnownTestsEnabled && !this.knownTestsForThisSuite.includes(testFullName)
|
|
1813
|
+
|
|
1814
|
+
if (isNew && !isSkipped && !isAttemptToFix) {
|
|
1815
|
+
newTests.add(testFullName)
|
|
1816
|
+
if (DYNAMIC_NAME_RE.test(testFullName)) {
|
|
1817
|
+
// Populated directly for runInBand; for parallel workers the main process
|
|
1818
|
+
// collects these from the TEST_HAS_DYNAMIC_NAME span tag via worker-report:trace.
|
|
1819
|
+
newTestsWithDynamicNames.add(`${this.testSuite} › ${testFullName}`)
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
if (
|
|
1824
|
+
isAttemptToFix ||
|
|
1825
|
+
isDisabled ||
|
|
1826
|
+
isSkipped ||
|
|
1827
|
+
!this.isEarlyFlakeDetectionEnabled ||
|
|
1828
|
+
!hasEfdRetries(this.#earlyFlakeDetectionRetryPolicy) ||
|
|
1829
|
+
(!isNew && !isModified)
|
|
1830
|
+
) {
|
|
1831
|
+
return
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
retriedTestsToNumAttempts.set(testFullName, 0)
|
|
1835
|
+
testsToBeRetried.add(testFullName)
|
|
1836
|
+
const schedulingRetryCount = this.#earlyFlakeDetectionRetryPolicy.schedulingRetryCount
|
|
1837
|
+
const efdRetryGates = concurrentTestState
|
|
1838
|
+
? this.createEfdRetryGates(schedulingRetryCount)
|
|
1839
|
+
: undefined
|
|
1840
|
+
if (efdRetryGates) {
|
|
1841
|
+
this.#efdRetryGatesByName ??= new Map()
|
|
1842
|
+
this.#efdRetryGatesByName.set(testFullName, efdRetryGates)
|
|
1843
|
+
}
|
|
1844
|
+
const retriesRegistered = this.registerRetryTests({
|
|
1845
|
+
concurrentTestState,
|
|
1846
|
+
efdRetryGates,
|
|
1847
|
+
isModified,
|
|
1848
|
+
jestEvent: event,
|
|
1849
|
+
state,
|
|
1850
|
+
retryCount: schedulingRetryCount,
|
|
1851
|
+
retryType: isModified ? 'Impacted tests' : 'Early flake detection',
|
|
1852
|
+
testFullName,
|
|
1853
|
+
})
|
|
1854
|
+
if (retriesRegistered) {
|
|
1855
|
+
efdCandidates.add(testFullName)
|
|
1856
|
+
} else {
|
|
1857
|
+
retriedTestsToNumAttempts.delete(testFullName)
|
|
1858
|
+
testsToBeRetried.delete(testFullName)
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1108
1862
|
async handleTestEvent (event, state) {
|
|
1109
1863
|
if (isDatadogJestEventHandled(event)) return
|
|
1110
1864
|
markDatadogJestEventHandled(event)
|
|
1111
1865
|
|
|
1866
|
+
if (event.name === 'add_test') {
|
|
1867
|
+
this.handleAddTestEvent(event, state)
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1112
1870
|
if (super.handleTestEvent) {
|
|
1113
1871
|
await super.handleTestEvent(event, state)
|
|
1114
1872
|
}
|
|
1115
1873
|
|
|
1116
1874
|
if (event.name === 'setup') {
|
|
1117
1875
|
this.wrapConcurrentTest(state)
|
|
1876
|
+
this.bindTestEach(this.global.test)
|
|
1118
1877
|
}
|
|
1119
|
-
if (
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
environment.setNameToParams(testName, testParameters)
|
|
1127
|
-
return eachBind.apply(this, args)
|
|
1128
|
-
}
|
|
1129
|
-
})
|
|
1878
|
+
if (
|
|
1879
|
+
event.name === 'run_describe_start' &&
|
|
1880
|
+
!hasConcurrentTestsStartEvent &&
|
|
1881
|
+
!event.describeBlock.parent &&
|
|
1882
|
+
this.#efdRetryGatesByName?.size > 0
|
|
1883
|
+
) {
|
|
1884
|
+
this.preparePre30ConcurrentTests(event.describeBlock, state)
|
|
1130
1885
|
}
|
|
1131
|
-
if (event.name === '
|
|
1886
|
+
if (event.name === 'concurrent_tests_start') {
|
|
1887
|
+
this.orderConcurrentEfdRetries(event.tests)
|
|
1888
|
+
} else if (event.name === 'test_start') {
|
|
1132
1889
|
const testName = getJestTestName(event.test)
|
|
1133
|
-
|
|
1890
|
+
let isAttemptToFix = false
|
|
1891
|
+
let isDisabled = false
|
|
1892
|
+
let isQuarantined = false
|
|
1893
|
+
if (this.isTestManagementTestsEnabled) {
|
|
1894
|
+
isAttemptToFix = this.testManagementTestsForThisSuite?.attemptToFix?.includes(testName)
|
|
1895
|
+
isDisabled = this.testManagementTestsForThisSuite?.disabled?.includes(testName)
|
|
1896
|
+
isQuarantined = this.testManagementTestsForThisSuite?.quarantined?.includes(testName)
|
|
1897
|
+
if (!isAttemptToFix && isDisabled) {
|
|
1898
|
+
event.test.mode = 'skip'
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
if (
|
|
1902
|
+
event.test.mode === 'todo' ||
|
|
1903
|
+
isJestTestSkipped(event.test, state.hasFocusedTests, state.testNamePattern)
|
|
1904
|
+
) {
|
|
1905
|
+
return
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
const registeredConcurrentTestState = testContexts.get(event.test)?.concurrentTestState
|
|
1909
|
+
const concurrentTestState = this.concurrentTestStates.get(event.test.fn) ||
|
|
1910
|
+
registeredConcurrentTestState
|
|
1134
1911
|
let concurrentCtx = concurrentTestState?.ctx
|
|
1135
1912
|
if (concurrentCtx) {
|
|
1136
1913
|
this.removeConcurrentTestContext(testName, concurrentCtx)
|
|
1137
1914
|
} else {
|
|
1138
1915
|
concurrentCtx = this.getNextConcurrentTestContext(testName)
|
|
1139
1916
|
}
|
|
1917
|
+
if (hasConcurrentTestsStartEvent && concurrentCtx?.efdRetryGates) {
|
|
1918
|
+
const retryGate = concurrentCtx.efdRetryGates[concurrentCtx.efdRetryIndex - 1]
|
|
1919
|
+
const shouldRun = await retryGate.promise
|
|
1920
|
+
if (!shouldRun) {
|
|
1921
|
+
concurrentCtx.isDiscardedEfdRetry = true
|
|
1922
|
+
event.test.mode = 'skip'
|
|
1923
|
+
return
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1140
1926
|
if (testsToBeRetried.has(testName)) {
|
|
1141
1927
|
// This is needed because we're retrying tests with the same name
|
|
1142
1928
|
this.resetSnapshotState()
|
|
@@ -1148,24 +1934,17 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
1148
1934
|
let numOfAttemptsToFixRetries = null
|
|
1149
1935
|
const testParameters = concurrentCtx?.testParameters ||
|
|
1150
1936
|
concurrentTestState?.ctx?.testParameters ||
|
|
1937
|
+
this.testParametersByFunction.get(event.test.fn) ||
|
|
1151
1938
|
getTestParametersString(this.nameToParams, event.test.name)
|
|
1152
1939
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
if (this.isTestManagementTestsEnabled) {
|
|
1157
|
-
isAttemptToFix = this.testManagementTestsForThisSuite?.attemptToFix?.includes(testName)
|
|
1158
|
-
isDisabled = this.testManagementTestsForThisSuite?.disabled?.includes(testName)
|
|
1159
|
-
isQuarantined = this.testManagementTestsForThisSuite?.quarantined?.includes(testName)
|
|
1160
|
-
if (isAttemptToFix) {
|
|
1161
|
-
numOfAttemptsToFixRetries = retriedTestsToNumAttempts.get(testName)
|
|
1162
|
-
retriedTestsToNumAttempts.set(testName, numOfAttemptsToFixRetries + 1)
|
|
1163
|
-
} else if (isDisabled) {
|
|
1164
|
-
event.test.mode = 'skip'
|
|
1165
|
-
}
|
|
1940
|
+
if (isAttemptToFix) {
|
|
1941
|
+
numOfAttemptsToFixRetries = retriedTestsToNumAttempts.get(testName)
|
|
1942
|
+
retriedTestsToNumAttempts.set(testName, numOfAttemptsToFixRetries + 1)
|
|
1166
1943
|
}
|
|
1167
1944
|
|
|
1168
|
-
|
|
1945
|
+
const efdRetryMetadata = efdRetryMetadataByTest.get(event.test)
|
|
1946
|
+
let isModified = efdRetryMetadata?.isModified ||
|
|
1947
|
+
this.isTestModified(event.test.asyncError, event.test.fn)
|
|
1169
1948
|
if (concurrentCtx?.isModified || concurrentTestState?.ctx?.isModified) {
|
|
1170
1949
|
isModified = true
|
|
1171
1950
|
}
|
|
@@ -1174,7 +1953,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
1174
1953
|
isNewTest = newTests.has(testName)
|
|
1175
1954
|
}
|
|
1176
1955
|
|
|
1177
|
-
const willRunEfd = this.isEarlyFlakeDetectionEnabled &&
|
|
1956
|
+
const willRunEfd = this.isEarlyFlakeDetectionEnabled &&
|
|
1957
|
+
hasEfdRetries(this.#earlyFlakeDetectionRetryPolicy) &&
|
|
1958
|
+
(isNewTest || isModified)
|
|
1178
1959
|
event.test[ATR_RETRY_SUPPRESSION_FLAG] = Boolean(isAttemptToFix || willRunEfd)
|
|
1179
1960
|
|
|
1180
1961
|
if (!isAttemptToFix && willRunEfd) {
|
|
@@ -1192,9 +1973,10 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
1192
1973
|
ctx.testParameters = testParameters
|
|
1193
1974
|
ctx.frameworkVersion = jestVersion
|
|
1194
1975
|
ctx.isNew = isNewTest
|
|
1195
|
-
ctx.isEfdRetry
|
|
1976
|
+
ctx.isEfdRetry ||= numEfdRetry > 0
|
|
1977
|
+
ctx.efdRetryIndex ??= efdRetryMetadata?.retryIndex
|
|
1196
1978
|
ctx.isAttemptToFix = isAttemptToFix
|
|
1197
|
-
ctx.isAttemptToFixRetry
|
|
1979
|
+
ctx.isAttemptToFixRetry ||= numOfAttemptsToFixRetries > 0
|
|
1198
1980
|
ctx.isJestRetry = isJestRetry
|
|
1199
1981
|
ctx.isDisabled = isDisabled
|
|
1200
1982
|
ctx.isQuarantined = isQuarantined
|
|
@@ -1301,68 +2083,23 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
1301
2083
|
})
|
|
1302
2084
|
}
|
|
1303
2085
|
|
|
1304
|
-
if (event.name === '
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
if (event.failing) {
|
|
1310
|
-
return
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
const testFullName = this.getTestNameFromAddTestEvent(event, state)
|
|
1314
|
-
const isSkipped = event.mode === 'todo' || event.mode === 'skip'
|
|
1315
|
-
const isAttemptToFix = this.isTestManagementTestsEnabled &&
|
|
1316
|
-
this.testManagementTestsForThisSuite?.attemptToFix?.includes(testFullName)
|
|
1317
|
-
if (
|
|
1318
|
-
isAttemptToFix &&
|
|
1319
|
-
!isSkipped &&
|
|
1320
|
-
!retriedTestsToNumAttempts.has(testFullName)
|
|
1321
|
-
) {
|
|
1322
|
-
retriedTestsToNumAttempts.set(testFullName, 0)
|
|
1323
|
-
testsToBeRetried.add(testFullName)
|
|
1324
|
-
this.retryTest({
|
|
1325
|
-
jestEvent: event,
|
|
1326
|
-
retryCount: testManagementAttemptToFixRetries,
|
|
1327
|
-
retryType: 'Test Management (Attempt to Fix)',
|
|
1328
|
-
})
|
|
2086
|
+
if (event.name === 'test_done') {
|
|
2087
|
+
const retryCtx = testContexts.get(event.test)
|
|
2088
|
+
if (!hasConcurrentTestsStartEvent && retryCtx?.concurrentTestState) {
|
|
2089
|
+
this.completePre30ConcurrentTest(retryCtx.concurrentTestState)
|
|
1329
2090
|
}
|
|
1330
|
-
if (
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
if (isModified && !retriedTestsToNumAttempts.has(testFullName) && this.isEarlyFlakeDetectionEnabled) {
|
|
1335
|
-
retriedTestsToNumAttempts.set(testFullName, 0)
|
|
1336
|
-
testsToBeRetried.add(testFullName)
|
|
1337
|
-
this.retryTest({
|
|
1338
|
-
jestEvent: event,
|
|
1339
|
-
retryCount: earlyFlakeDetectionNumRetries,
|
|
1340
|
-
retryType: 'Impacted tests',
|
|
1341
|
-
})
|
|
2091
|
+
if (retryCtx?.detachedEfdRetryPromise) {
|
|
2092
|
+
await retryCtx.detachedEfdRetryPromise
|
|
2093
|
+
if (retryCtx.detachedEfdRetryError !== undefined) {
|
|
2094
|
+
event.test.errors.push(retryCtx.detachedEfdRetryError)
|
|
1342
2095
|
}
|
|
1343
2096
|
}
|
|
1344
|
-
if (
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
}
|
|
1349
|
-
if (isNew && !isSkipped && !retriedTestsToNumAttempts.has(testFullName)) {
|
|
1350
|
-
if (DYNAMIC_NAME_RE.test(testFullName)) {
|
|
1351
|
-
// Populated directly for runInBand; for parallel workers the main process
|
|
1352
|
-
// collects these from the TEST_HAS_DYNAMIC_NAME span tag via worker-report:trace.
|
|
1353
|
-
newTestsWithDynamicNames.add(`${this.testSuite} › ${testFullName}`)
|
|
1354
|
-
}
|
|
1355
|
-
retriedTestsToNumAttempts.set(testFullName, 0)
|
|
1356
|
-
if (this.isEarlyFlakeDetectionEnabled) {
|
|
1357
|
-
testsToBeRetried.add(testFullName)
|
|
1358
|
-
efdNewTestCandidates.add(testFullName)
|
|
1359
|
-
// Cloning is deferred to test_done after the first execution,
|
|
1360
|
-
// when we know the duration and can choose the right retry count.
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
2097
|
+
if (retryCtx?.isDiscardedEfdRetry) {
|
|
2098
|
+
this.#discardedEfdRetryTests ??= new Set()
|
|
2099
|
+
this.#discardedEfdRetryTests.add(event.test)
|
|
2100
|
+
return
|
|
1363
2101
|
}
|
|
1364
|
-
|
|
1365
|
-
if (event.name === 'test_done') {
|
|
2102
|
+
|
|
1366
2103
|
const originalError = event.test?.errors?.[0]
|
|
1367
2104
|
let status = 'pass'
|
|
1368
2105
|
if (event.test.errors && event.test.errors.length) {
|
|
@@ -1410,71 +2147,32 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
1410
2147
|
}
|
|
1411
2148
|
}
|
|
1412
2149
|
|
|
1413
|
-
//
|
|
1414
|
-
// determine the retry count from the test's duration.
|
|
2150
|
+
// The first execution selects how many pre-registered EFD retries remain runnable.
|
|
1415
2151
|
if (
|
|
1416
2152
|
this.isEarlyFlakeDetectionEnabled &&
|
|
1417
|
-
|
|
1418
|
-
efdNewTestCandidates.has(testName) &&
|
|
2153
|
+
efdCandidates.has(testName) &&
|
|
1419
2154
|
event.test.invocations === 1 &&
|
|
1420
2155
|
!efdDeterminedRetries.has(testName)
|
|
1421
2156
|
) {
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
if (retryCount > 0) {
|
|
1426
|
-
// Temporarily adjust jest-circus state so that retry tests are registered
|
|
1427
|
-
// into the correct describe block and bypass the "tests have started" guard.
|
|
1428
|
-
//
|
|
1429
|
-
// Problem 1 (jest-circus ≤24): currentDescribeBlock points to ROOT during
|
|
1430
|
-
// execution, and ROOT's tests loop already finished before children ran.
|
|
1431
|
-
//
|
|
1432
|
-
// Problem 2 (jest-circus ≥27): `hasStarted = true` causes `test()` to throw
|
|
1433
|
-
// "Cannot add a test after tests have started running".
|
|
1434
|
-
//
|
|
1435
|
-
// Fix: temporarily point currentDescribeBlock to the test's parent (so retries
|
|
1436
|
-
// land in the still-iterating children array) and set hasStarted = false (so the
|
|
1437
|
-
// guard is bypassed). Both are restored immediately after scheduling the retries.
|
|
1438
|
-
const originalDescribeBlock = state.currentDescribeBlock
|
|
1439
|
-
const originalHasStarted = state.hasStarted
|
|
1440
|
-
state.currentDescribeBlock = event.test.parent ?? originalDescribeBlock
|
|
1441
|
-
state.hasStarted = false
|
|
1442
|
-
this.retryTest({
|
|
1443
|
-
forceSerial: true,
|
|
1444
|
-
jestEvent: {
|
|
1445
|
-
testName: event.test.name,
|
|
1446
|
-
fn: event.test.fn,
|
|
1447
|
-
timeout: event.test.timeout,
|
|
1448
|
-
},
|
|
1449
|
-
retryCount,
|
|
1450
|
-
retryType: 'Early flake detection',
|
|
1451
|
-
})
|
|
1452
|
-
state.currentDescribeBlock = originalDescribeBlock
|
|
1453
|
-
state.hasStarted = originalHasStarted
|
|
1454
|
-
} else {
|
|
1455
|
-
efdSlowAbortedTests.add(testName)
|
|
2157
|
+
this.determineEfdRetries(testName, event.test.duration ?? 0)
|
|
2158
|
+
if (!this.#efdRetryGatesByName?.has(testName)) {
|
|
2159
|
+
this.discardEfdRetries(testName, event.test, efdDeterminedRetries.get(testName))
|
|
1456
2160
|
}
|
|
1457
2161
|
}
|
|
1458
2162
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
// If it is, we'll set the failedAllTests flag to true if all the tests failed
|
|
1473
|
-
const efdRetryCount = efdDeterminedRetries.get(testName) ?? 0
|
|
1474
|
-
if (efdRetryCount > 0 && testStatuses.length === efdRetryCount + 1 &&
|
|
1475
|
-
testStatuses.every(status => status === 'fail')) {
|
|
1476
|
-
failedAllTests = true
|
|
1477
|
-
}
|
|
2163
|
+
const isEfdRetry = retryCtx?.isEfdRetry === true
|
|
2164
|
+
if (efdCandidates.has(testName)) {
|
|
2165
|
+
let testStatuses = efdTestStatuses.get(testName)
|
|
2166
|
+
if (testStatuses) {
|
|
2167
|
+
testStatuses.push(status)
|
|
2168
|
+
} else {
|
|
2169
|
+
testStatuses = [status]
|
|
2170
|
+
efdTestStatuses.set(testName, testStatuses)
|
|
2171
|
+
}
|
|
2172
|
+
const efdRetryCount = efdDeterminedRetries.get(testName) ?? 0
|
|
2173
|
+
if (efdRetryCount > 0 && testStatuses.length === efdExpectedExecutions.get(testName) &&
|
|
2174
|
+
testStatuses.every(status => status === 'fail')) {
|
|
2175
|
+
failedAllTests = true
|
|
1478
2176
|
}
|
|
1479
2177
|
}
|
|
1480
2178
|
|
|
@@ -1511,6 +2209,16 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
1511
2209
|
log.warn('"ci:jest:test_done": no context found for test "%s"', testName)
|
|
1512
2210
|
return
|
|
1513
2211
|
}
|
|
2212
|
+
if (!isJestWorker) {
|
|
2213
|
+
recordTestManagementExecution({
|
|
2214
|
+
testSuite: ctx.suite,
|
|
2215
|
+
testName: ctx.name,
|
|
2216
|
+
status,
|
|
2217
|
+
isAttemptToFix: ctx.isAttemptToFix,
|
|
2218
|
+
isDisabled: ctx.isDisabled,
|
|
2219
|
+
isQuarantined: ctx.isQuarantined,
|
|
2220
|
+
})
|
|
2221
|
+
}
|
|
1514
2222
|
if (ctx.concurrentTestState && !ctx.currentStore && !ctx.isDisabled) {
|
|
1515
2223
|
testStartCh.runStores(ctx, () => {})
|
|
1516
2224
|
}
|
|
@@ -1565,8 +2273,8 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
1565
2273
|
if (ctx.concurrentTestState) {
|
|
1566
2274
|
ctx.currentStore = undefined
|
|
1567
2275
|
}
|
|
1568
|
-
}
|
|
1569
|
-
|
|
2276
|
+
} else if (event.name === 'run_finish') {
|
|
2277
|
+
this.removeDiscardedEfdRetries()
|
|
1570
2278
|
for (const [test, errors] of atrSuppressedErrors) {
|
|
1571
2279
|
// Do not restore errors for non-ATF quarantined tests — they should stay suppressed
|
|
1572
2280
|
// so Jest doesn't see the failure (prevents --bail from stopping the run).
|
|
@@ -1588,16 +2296,31 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
1588
2296
|
}
|
|
1589
2297
|
|
|
1590
2298
|
efdDeterminedRetries.clear()
|
|
2299
|
+
efdExpectedExecutions.clear()
|
|
1591
2300
|
efdSlowAbortedTests.clear()
|
|
1592
|
-
|
|
2301
|
+
efdCandidates.clear()
|
|
1593
2302
|
newTests.clear()
|
|
1594
2303
|
retriedTestsToNumAttempts.clear()
|
|
1595
2304
|
attemptToFixRetriedTestsStatuses.clear()
|
|
1596
2305
|
testsToBeRetried.clear()
|
|
2306
|
+
this.#efdRetryGatesByName = undefined
|
|
2307
|
+
this.#detachedEfdRetryQueue = undefined
|
|
1597
2308
|
testSuiteDatadogEnvironments.delete(this.testSuiteAbsolutePath)
|
|
1598
|
-
}
|
|
1599
|
-
if (event.name === 'test_skip' || event.name === 'test_todo') {
|
|
2309
|
+
} else if (event.name === 'test_skip' || event.name === 'test_todo') {
|
|
1600
2310
|
const testName = getJestTestName(event.test)
|
|
2311
|
+
const retryGates = this.#efdRetryGatesByName?.get(testName)
|
|
2312
|
+
if (retryGates) {
|
|
2313
|
+
if (efdRetryMetadataByTest.has(event.test)) {
|
|
2314
|
+
this.#discardedEfdRetryTests ??= new Set()
|
|
2315
|
+
this.#discardedEfdRetryTests.add(event.test)
|
|
2316
|
+
return
|
|
2317
|
+
}
|
|
2318
|
+
for (const gate of retryGates) {
|
|
2319
|
+
gate.resolve(false)
|
|
2320
|
+
}
|
|
2321
|
+
} else {
|
|
2322
|
+
this.discardEfdRetries(testName, event.test)
|
|
2323
|
+
}
|
|
1601
2324
|
testSkippedCh.publish({
|
|
1602
2325
|
test: {
|
|
1603
2326
|
name: testName,
|
|
@@ -1612,18 +2335,18 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
1612
2335
|
}
|
|
1613
2336
|
}
|
|
1614
2337
|
|
|
1615
|
-
getEfdResult ({ testName, isNewTest, isModifiedTest
|
|
2338
|
+
getEfdResult ({ testName, isNewTest, isModifiedTest }) {
|
|
1616
2339
|
const isEfdEnabled = this.isEarlyFlakeDetectionEnabled
|
|
1617
|
-
const isEfdActive = isEfdEnabled &&
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
const
|
|
1621
|
-
const isFinalEfdTestExecution = isEfdActive &&
|
|
2340
|
+
const isEfdActive = isEfdEnabled &&
|
|
2341
|
+
hasEfdRetries(this.#earlyFlakeDetectionRetryPolicy) &&
|
|
2342
|
+
(isNewTest || isModifiedTest)
|
|
2343
|
+
const testStatuses = efdTestStatuses.get(testName)
|
|
2344
|
+
const isFinalEfdTestExecution = isEfdActive &&
|
|
2345
|
+
(efdSlowAbortedTests.has(testName) || testStatuses?.length === (efdExpectedExecutions.get(testName) ?? 0))
|
|
1622
2346
|
|
|
1623
2347
|
let finalStatus
|
|
1624
2348
|
if (isEfdActive && isFinalEfdTestExecution) {
|
|
1625
2349
|
// For EFD: The framework reports 'pass' if ANY attempt passed (flaky but not failing)
|
|
1626
|
-
const testStatuses = newTestsTestStatuses.get(testName)
|
|
1627
2350
|
finalStatus = testStatuses && testStatuses.includes('pass') ? 'pass' : 'fail'
|
|
1628
2351
|
}
|
|
1629
2352
|
|
|
@@ -1662,15 +2385,21 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
1662
2385
|
return { isAttemptToFixEnabled, isFinalAttemptToFixExecution, finalStatus }
|
|
1663
2386
|
}
|
|
1664
2387
|
|
|
1665
|
-
getFinalStatus (
|
|
2388
|
+
getFinalStatus (
|
|
2389
|
+
testName,
|
|
2390
|
+
status,
|
|
2391
|
+
isNewTest,
|
|
2392
|
+
isModifiedTest,
|
|
2393
|
+
isEfdRetry,
|
|
2394
|
+
isAttemptToFix,
|
|
2395
|
+
numberOfTestInvocations
|
|
2396
|
+
) {
|
|
1666
2397
|
const numberOfExecutedRetries = retriedTestsToNumAttempts.get(testName) ?? 0
|
|
1667
2398
|
|
|
1668
2399
|
const efdResult = this.getEfdResult({
|
|
1669
2400
|
testName,
|
|
1670
2401
|
isNewTest,
|
|
1671
2402
|
isModifiedTest,
|
|
1672
|
-
isEfdRetry,
|
|
1673
|
-
numberOfExecutedRetries,
|
|
1674
2403
|
})
|
|
1675
2404
|
const atrResult = this.getAtrResult({ status, isEfdRetry, isAttemptToFix, numberOfTestInvocations })
|
|
1676
2405
|
const attemptToFixResult = this.getAttemptToFixResult({
|
|
@@ -1869,8 +2598,6 @@ function resetLibraryConfiguration () {
|
|
|
1869
2598
|
isItrEnabled = false
|
|
1870
2599
|
isSuitesSkippingEnabled = false
|
|
1871
2600
|
isEarlyFlakeDetectionEnabled = false
|
|
1872
|
-
earlyFlakeDetectionNumRetries = 0
|
|
1873
|
-
earlyFlakeDetectionSlowTestRetries = {}
|
|
1874
2601
|
earlyFlakeDetectionFaultyThreshold = 30
|
|
1875
2602
|
isEarlyFlakeDetectionFaulty = false
|
|
1876
2603
|
isKnownTestsEnabled = false
|
|
@@ -1898,8 +2625,8 @@ function applySuiteSkipping (originalTests, rootDir, frameworkVersion) {
|
|
|
1898
2625
|
hasUnskippableSuites = jestSuitesToRun.hasUnskippableSuites
|
|
1899
2626
|
hasForcedToRunSuites = jestSuitesToRun.hasForcedToRunSuites
|
|
1900
2627
|
|
|
1901
|
-
isSuitesSkipped
|
|
1902
|
-
numSkippedSuites
|
|
2628
|
+
isSuitesSkipped ||= jestSuitesToRun.suitesToRun.length !== originalTests.length
|
|
2629
|
+
numSkippedSuites += jestSuitesToRun.skippedSuites.length
|
|
1903
2630
|
skippedSuitesCoverage = isSuitesSkipped && isTiaCoverageBackfillEnabled() && hasSkippableSuitesCoverage()
|
|
1904
2631
|
? skippableSuitesCoverage
|
|
1905
2632
|
: {}
|
|
@@ -2245,8 +2972,6 @@ function getCliWrapper (isNewJestVersion) {
|
|
|
2245
2972
|
isItrEnabled = libraryConfig.isItrEnabled
|
|
2246
2973
|
isSuitesSkippingEnabled = isItrEnabled && libraryConfig.isSuitesSkippingEnabled
|
|
2247
2974
|
isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
|
|
2248
|
-
earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
|
|
2249
|
-
earlyFlakeDetectionSlowTestRetries = libraryConfig.earlyFlakeDetectionSlowTestRetries ?? {}
|
|
2250
2975
|
earlyFlakeDetectionFaultyThreshold = libraryConfig.earlyFlakeDetectionFaultyThreshold
|
|
2251
2976
|
isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled
|
|
2252
2977
|
isTestManagementTestsEnabled = libraryConfig.isTestManagementEnabled
|
|
@@ -2298,12 +3023,11 @@ function getCliWrapper (isNewJestVersion) {
|
|
|
2298
3023
|
} = skippableSuitesResponse || await getChannelPromise(skippableSuitesCh)
|
|
2299
3024
|
if (err) {
|
|
2300
3025
|
skippableSuitesCoverage = {}
|
|
2301
|
-
skippedSuitesCoverage = {}
|
|
2302
3026
|
} else {
|
|
2303
3027
|
skippableSuites = receivedSkippableSuites
|
|
2304
3028
|
skippableSuitesCoverage = receivedSkippableSuitesCoverage || {}
|
|
2305
|
-
skippedSuitesCoverage = {}
|
|
2306
3029
|
}
|
|
3030
|
+
skippedSuitesCoverage = {}
|
|
2307
3031
|
} catch (err) {
|
|
2308
3032
|
log.error('Jest test-suite skippable error', err)
|
|
2309
3033
|
}
|
|
@@ -2381,15 +3105,24 @@ function getCliWrapper (isNewJestVersion) {
|
|
|
2381
3105
|
}
|
|
2382
3106
|
}
|
|
2383
3107
|
|
|
2384
|
-
/**
|
|
3108
|
+
/**
|
|
3109
|
+
* @type {{
|
|
3110
|
+
* efdNames: string[],
|
|
3111
|
+
* quarantineNames: string[],
|
|
3112
|
+
* totalCount: number,
|
|
3113
|
+
* efdFailureCount: number
|
|
3114
|
+
* } | undefined}
|
|
3115
|
+
*/
|
|
2385
3116
|
let ignoredFailuresSummary
|
|
2386
3117
|
if (isEarlyFlakeDetectionEnabled) {
|
|
2387
|
-
for (const [testName, testStatuses] of
|
|
3118
|
+
for (const [testName, testStatuses] of efdTestStatuses) {
|
|
2388
3119
|
const { pass, fail } = getTestStats(testStatuses)
|
|
2389
3120
|
if (pass > 0) { // as long as one passes, we'll consider the test passed
|
|
2390
3121
|
numEfdFailedTestsToIgnore += fail
|
|
2391
|
-
|
|
2392
|
-
|
|
3122
|
+
if (fail > 0) {
|
|
3123
|
+
const suite = fullNameToSuite.get(testName)
|
|
3124
|
+
efdIgnoredNames.push(suite ? `${suite} › ${testName}` : testName)
|
|
3125
|
+
}
|
|
2393
3126
|
}
|
|
2394
3127
|
}
|
|
2395
3128
|
// If every test that failed was an EFD retry, we'll consider the suite passed
|
|
@@ -2403,6 +3136,7 @@ function getCliWrapper (isNewJestVersion) {
|
|
|
2403
3136
|
efdNames: efdIgnoredNames,
|
|
2404
3137
|
quarantineNames: [],
|
|
2405
3138
|
totalCount: numEfdFailedTestsToIgnore,
|
|
3139
|
+
efdFailureCount: numEfdFailedTestsToIgnore,
|
|
2406
3140
|
}
|
|
2407
3141
|
}
|
|
2408
3142
|
}
|
|
@@ -2475,6 +3209,7 @@ function getCliWrapper (isNewJestVersion) {
|
|
|
2475
3209
|
efdNames: [],
|
|
2476
3210
|
quarantineNames: quarantineIgnoredNames,
|
|
2477
3211
|
totalCount: totalQuarantineFailures,
|
|
3212
|
+
efdFailureCount: 0,
|
|
2478
3213
|
}
|
|
2479
3214
|
}
|
|
2480
3215
|
}
|
|
@@ -2500,6 +3235,7 @@ function getCliWrapper (isNewJestVersion) {
|
|
|
2500
3235
|
efdNames: efdIgnoredNames,
|
|
2501
3236
|
quarantineNames: quarantineIgnoredNames,
|
|
2502
3237
|
totalCount: visibleIgnoredFailures + numSuppressedQuarantinedTests,
|
|
3238
|
+
efdFailureCount: numEfdFailedTestsToIgnore,
|
|
2503
3239
|
}
|
|
2504
3240
|
}
|
|
2505
3241
|
}
|
|
@@ -2522,7 +3258,13 @@ function getCliWrapper (isNewJestVersion) {
|
|
|
2522
3258
|
await getChannelPromise(codeCoverageReportCh, { rootDir })
|
|
2523
3259
|
}
|
|
2524
3260
|
|
|
2525
|
-
|
|
3261
|
+
recordTestManagementExecutionsFromJestResults(result, quarantineIgnoredNames)
|
|
3262
|
+
const allTestsSkipped = isSuitesSkipped && numTotalTests === 0 && numTotalTestSuites === 0
|
|
3263
|
+
logSessionSummary(
|
|
3264
|
+
ignoredFailuresSummary,
|
|
3265
|
+
getAttemptToFixExecutionsFromJestResults(result),
|
|
3266
|
+
allTestsSkipped
|
|
3267
|
+
)
|
|
2526
3268
|
|
|
2527
3269
|
resetSuiteSkippingRunState()
|
|
2528
3270
|
|
|
@@ -2865,7 +3607,7 @@ const DD_TEST_ENVIRONMENT_OPTION_KEYS = [
|
|
|
2865
3607
|
'_ddItrCorrelationId',
|
|
2866
3608
|
'_ddKnownTests',
|
|
2867
3609
|
'_ddIsEarlyFlakeDetectionEnabled',
|
|
2868
|
-
'
|
|
3610
|
+
'_ddEarlyFlakeDetectionRetryPolicy',
|
|
2869
3611
|
'_ddRepositoryRoot',
|
|
2870
3612
|
'_ddTestCodeCoverageEnabled',
|
|
2871
3613
|
'_ddIsFlakyTestRetriesEnabled',
|
|
@@ -3037,9 +3779,7 @@ function wrapJestObject (jestObject, suiteFilePath) {
|
|
|
3037
3779
|
|
|
3038
3780
|
shimmer.wrap(jestObject, 'mock', mock => function (moduleName) {
|
|
3039
3781
|
// If the library is mocked with `jest.mock`, we don't want to bypass jest's own require engine
|
|
3040
|
-
|
|
3041
|
-
LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.delete(moduleName)
|
|
3042
|
-
}
|
|
3782
|
+
LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.delete(moduleName)
|
|
3043
3783
|
recordMockedFile(suiteFilePath, moduleName)
|
|
3044
3784
|
return mock.apply(this, arguments)
|
|
3045
3785
|
})
|