dd-trace 5.116.0 → 5.118.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 +162 -21
- package/ci/init.js +0 -1
- package/ci/runbook.md +187 -180
- package/ci/test-optimization-validation/approval-artifacts.js +3 -1
- package/ci/test-optimization-validation/approval.js +52 -15
- package/ci/test-optimization-validation/ci-command-candidate.js +26 -32
- package/ci/test-optimization-validation/ci-discovery.js +1 -1
- package/ci/test-optimization-validation/ci-package-scripts.js +141 -0
- package/ci/test-optimization-validation/ci-remediation.js +112 -39
- package/ci/test-optimization-validation/cli.js +516 -681
- package/ci/test-optimization-validation/command-blocker.js +188 -33
- package/ci/test-optimization-validation/command-output-policy.js +3 -26
- package/ci/test-optimization-validation/command-runner.js +189 -245
- package/ci/test-optimization-validation/environment.js +90 -0
- package/ci/test-optimization-validation/executable.js +159 -388
- package/ci/test-optimization-validation/framework-adapters/cucumber.js +119 -0
- package/ci/test-optimization-validation/framework-adapters/cypress.js +107 -0
- package/ci/test-optimization-validation/framework-adapters/playwright.js +181 -0
- package/ci/test-optimization-validation/generated-files.js +75 -13
- package/ci/test-optimization-validation/generated-test-contract.js +283 -0
- package/ci/test-optimization-validation/generated-verifier.js +49 -16
- package/ci/test-optimization-validation/literal-environment.js +57 -0
- package/ci/test-optimization-validation/manifest-loader.js +2 -22
- package/ci/test-optimization-validation/manifest-scaffold.js +816 -515
- package/ci/test-optimization-validation/manifest-schema.js +494 -702
- package/ci/test-optimization-validation/offline-fixtures.js +4 -1
- package/ci/test-optimization-validation/plan-writer.js +243 -984
- package/ci/test-optimization-validation/preflight-runner.js +49 -55
- package/ci/test-optimization-validation/redaction.js +2 -1
- package/ci/test-optimization-validation/report-writer.js +391 -1357
- package/ci/test-optimization-validation/result-semantics.js +86 -0
- package/ci/test-optimization-validation/runner-command.js +249 -0
- package/ci/test-optimization-validation/runner-contract.js +664 -0
- package/ci/test-optimization-validation/scenarios/auto-test-retries.js +29 -3
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +240 -543
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +450 -670
- package/ci/test-optimization-validation/scenarios/early-flake-detection.js +4 -3
- package/ci/test-optimization-validation/scenarios/helpers.js +67 -9
- package/ci/test-optimization-validation/scenarios/test-management.js +4 -3
- package/ci/test-optimization-validation/source-text.js +87 -0
- package/ci/test-optimization-validation/test-output.js +12 -22
- package/ext/tags.d.ts +1 -0
- package/index.d.ts +7 -0
- package/package.json +3 -3
- package/packages/datadog-instrumentations/src/cucumber.js +11 -12
- package/packages/datadog-instrumentations/src/helpers/channel.js +74 -0
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/openai-agents.js +31 -0
- package/packages/datadog-instrumentations/src/jest.js +18 -17
- package/packages/datadog-instrumentations/src/mocha/main.js +79 -30
- package/packages/datadog-instrumentations/src/openai-agents.js +159 -0
- package/packages/datadog-instrumentations/src/playwright.js +3 -24
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1 -1
- package/packages/datadog-instrumentations/src/vitest-main.js +14 -26
- package/packages/datadog-instrumentations/src/vitest-util.js +0 -7
- package/packages/datadog-instrumentations/src/vitest-worker.js +5 -8
- package/packages/datadog-plugin-cucumber/src/index.js +2 -1
- package/packages/datadog-plugin-mocha/src/index.js +2 -1
- package/packages/datadog-plugin-openai-agents/src/index.js +74 -0
- package/packages/datadog-plugin-openai-agents/src/integration.js +503 -0
- package/packages/datadog-plugin-openai-agents/src/processor.js +108 -0
- package/packages/datadog-plugin-openai-agents/src/util.js +60 -0
- package/packages/datadog-plugin-playwright/src/index.js +4 -1
- package/packages/datadog-plugin-vitest/src/index.js +5 -5
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +48 -49
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +161 -33
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +6 -4
- package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
- package/packages/dd-trace/src/config/supported-configurations.json +7 -0
- package/packages/dd-trace/src/encode/span-stats.js +7 -1
- package/packages/dd-trace/src/exporter.js +16 -9
- package/packages/dd-trace/src/llmobs/index.js +9 -1
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +104 -22
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +5 -9
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +20 -0
- package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +321 -0
- package/packages/dd-trace/src/llmobs/sdk.js +2 -1
- package/packages/dd-trace/src/llmobs/span_processor.js +7 -2
- package/packages/dd-trace/src/llmobs/tagger.js +23 -3
- package/packages/dd-trace/src/llmobs/util.js +56 -3
- package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +3 -6
- package/packages/dd-trace/src/openfeature/configuration_source.js +5 -8
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +5 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +9 -3
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/dd-trace/src/plugins/outbound.js +4 -1
- package/packages/dd-trace/src/profiling/profilers/space.js +1 -1
- package/packages/dd-trace/src/profiling/profilers/wall.js +1 -0
- package/packages/dd-trace/src/span_processor.js +1 -1
- package/packages/dd-trace/src/span_stats.js +22 -4
- package/ci/test-optimization-validation/command-suitability.js +0 -471
- package/ci/test-optimization-validation/init-probe-preload.js +0 -163
- package/ci/test-optimization-validation/init-probe.js +0 -246
- package/ci/test-optimization-validation/late-initialization.js +0 -63
- package/ci/test-optimization-validation/local-command.js +0 -163
- package/ci/test-optimization-validation/setup-runner.js +0 -97
- package/ci/test-optimization-validation-manifest.schema.json +0 -1
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
|
|
5
|
+
const cucumberAdapter = require('./framework-adapters/cucumber')
|
|
6
|
+
const cypressAdapter = require('./framework-adapters/cypress')
|
|
7
|
+
const playwrightAdapter = require('./framework-adapters/playwright')
|
|
8
|
+
|
|
9
|
+
const GENERATED_SCENARIOS = {
|
|
10
|
+
'basic-pass': {
|
|
11
|
+
expectedWithoutDatadog: { exitCode: 0, observedTestCount: 1 },
|
|
12
|
+
purpose: 'basic_reporting|efd_candidate',
|
|
13
|
+
suiteName: 'dd-test-optimization-validation',
|
|
14
|
+
testName: 'basic-pass',
|
|
15
|
+
},
|
|
16
|
+
'atr-fail-once': {
|
|
17
|
+
expectedWithoutDatadog: { exitCode: 1, observedTestCount: 1 },
|
|
18
|
+
purpose: 'auto_test_retries_candidate',
|
|
19
|
+
suiteName: 'dd-test-optimization-validation',
|
|
20
|
+
testName: 'atr-fail-once',
|
|
21
|
+
},
|
|
22
|
+
'test-management-target': {
|
|
23
|
+
expectedWithoutDatadog: { exitCode: 0, observedTestCount: 1 },
|
|
24
|
+
purpose: 'test_management_candidate',
|
|
25
|
+
suiteName: 'dd-test-optimization-validation',
|
|
26
|
+
testName: 'test-management-target',
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns the retry-state path used by generated Node.js test scenarios.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} framework test framework
|
|
34
|
+
* @param {string} filename generated ATR test file
|
|
35
|
+
* @returns {string} retry-state path
|
|
36
|
+
*/
|
|
37
|
+
function getGeneratedRetryStatePath (framework, filename) {
|
|
38
|
+
const namespace = `dd-test-optimization-validation-${framework}-`
|
|
39
|
+
const namespaced = path.basename(filename).includes(namespace) ||
|
|
40
|
+
path.basename(path.dirname(filename)).includes(namespace)
|
|
41
|
+
const stateFilename = namespaced
|
|
42
|
+
? `.dd-test-optimization-validation-${framework}-atr-state`
|
|
43
|
+
: '.dd-test-optimization-validation-atr-state'
|
|
44
|
+
return path.join(path.dirname(filename), stateFilename)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns validator-owned source for one generated scenario.
|
|
49
|
+
*
|
|
50
|
+
* @param {object} input generated source input
|
|
51
|
+
* @param {string} input.framework test framework
|
|
52
|
+
* @param {string} input.moduleSystem generated module system
|
|
53
|
+
* @param {string} input.scenarioId generated scenario id
|
|
54
|
+
* @param {string} [input.stateFile] persistent ATR state file
|
|
55
|
+
* @returns {string} canonical generated source
|
|
56
|
+
*/
|
|
57
|
+
function getGeneratedTestContent ({ framework, moduleSystem, scenarioId, stateFile }) {
|
|
58
|
+
const scenario = GENERATED_SCENARIOS[scenarioId]
|
|
59
|
+
if (!scenario) throw new Error(`Unknown generated Test Optimization scenario: ${scenarioId}`)
|
|
60
|
+
if (framework === 'cucumber') {
|
|
61
|
+
return cucumberAdapter.getGeneratedTestContent({ testName: scenario.testName })
|
|
62
|
+
}
|
|
63
|
+
if (framework === 'cypress') {
|
|
64
|
+
return cypressAdapter.getGeneratedTestContent({
|
|
65
|
+
scenarioId,
|
|
66
|
+
testName: scenario.testName,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
if (framework === 'playwright') {
|
|
70
|
+
return playwrightAdapter.getGeneratedTestContent({
|
|
71
|
+
moduleSystem,
|
|
72
|
+
scenarioId,
|
|
73
|
+
testName: scenario.testName,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const imports = []
|
|
78
|
+
if (framework === 'jest') {
|
|
79
|
+
const requireCall = 'require'
|
|
80
|
+
imports.push(moduleSystem === 'esm'
|
|
81
|
+
? "import { describe, expect, test } from '@jest/globals'"
|
|
82
|
+
: `const { describe, expect, test } = ${requireCall}('@jest/globals')`)
|
|
83
|
+
}
|
|
84
|
+
if (framework === 'vitest' && moduleSystem === 'esm') {
|
|
85
|
+
imports.push("import { describe, expect, it } from 'vitest'")
|
|
86
|
+
}
|
|
87
|
+
if (framework === 'mocha') {
|
|
88
|
+
imports.push(moduleSystem === 'esm'
|
|
89
|
+
? "import assert from 'node:assert/strict'"
|
|
90
|
+
: "const assert = require('node:assert/strict')")
|
|
91
|
+
}
|
|
92
|
+
if (scenarioId === 'atr-fail-once') {
|
|
93
|
+
if (moduleSystem === 'esm') {
|
|
94
|
+
imports.push("import { existsSync, writeFileSync } from 'node:fs'")
|
|
95
|
+
} else {
|
|
96
|
+
imports.push("const fs = require('node:fs')")
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const assertion = framework === 'mocha' ? 'assert.equal(1, 1)' : 'expect(true).toBe(true)'
|
|
101
|
+
const testFunction = framework === 'jest' ? 'test' : 'it'
|
|
102
|
+
const body = scenarioId === 'atr-fail-once'
|
|
103
|
+
? getAtrBody({ moduleSystem, assertion, stateFile })
|
|
104
|
+
: ` ${assertion}`
|
|
105
|
+
|
|
106
|
+
return [
|
|
107
|
+
...imports,
|
|
108
|
+
imports.length > 0 ? '' : undefined,
|
|
109
|
+
"describe('dd-test-optimization-validation', () => {",
|
|
110
|
+
` ${testFunction}('${scenario.testName}', () => {`,
|
|
111
|
+
body,
|
|
112
|
+
' })',
|
|
113
|
+
'})',
|
|
114
|
+
].filter(line => line !== undefined).join('\n')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Verifies that a runnable framework retained the validator-owned generated-test recipe.
|
|
119
|
+
*
|
|
120
|
+
* @param {object} framework manifest framework entry
|
|
121
|
+
* @returns {string|undefined} contract error
|
|
122
|
+
*/
|
|
123
|
+
function getGeneratedTestContractError (framework) {
|
|
124
|
+
const strategy = framework.generatedTestStrategy
|
|
125
|
+
if (!strategy || !['planned', 'verified'].includes(strategy.status)) return
|
|
126
|
+
|
|
127
|
+
if (!['cucumber', 'cypress', 'jest', 'mocha', 'playwright', 'vitest'].includes(framework.framework)) return
|
|
128
|
+
if (!['commonjs', 'esm'].includes(strategy.moduleSystem)) {
|
|
129
|
+
return 'must retain generatedTestStrategy.moduleSystem as "commonjs" or "esm".'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const files = strategy.files || []
|
|
133
|
+
const scenarios = strategy.scenarios || []
|
|
134
|
+
const projectRoot = framework.project?.root
|
|
135
|
+
if (typeof projectRoot !== 'string') return 'must declare a project root for generated tests.'
|
|
136
|
+
if ((strategy.cleanupPaths || []).some(filename => typeof filename !== 'string')) {
|
|
137
|
+
return 'must contain only string cleanup paths.'
|
|
138
|
+
}
|
|
139
|
+
const cleanupPaths = new Set((strategy.cleanupPaths || []).map(filename => path.normalize(filename)))
|
|
140
|
+
const expectedFileCount = ['cucumber', 'playwright'].includes(framework.framework) ? 4 : 3
|
|
141
|
+
if (files.length !== expectedFileCount || scenarios.length !== 3) {
|
|
142
|
+
return 'must contain exactly one validator-owned file for each of basic-pass, atr-fail-once, and ' +
|
|
143
|
+
`test-management-target${getAdditionalGeneratedFileDescription(framework.framework)}.`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const selectedFiles = new Set()
|
|
147
|
+
for (const [scenarioId, definition] of Object.entries(GENERATED_SCENARIOS)) {
|
|
148
|
+
const scenario = scenarios.find(entry => entry.id === scenarioId)
|
|
149
|
+
if (!scenario) return `is missing the validator-owned ${scenarioId} scenario.`
|
|
150
|
+
if (scenario.testIdentities?.length !== 1) {
|
|
151
|
+
return `scenario ${scenarioId} must declare exactly one generated test identity.`
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const filename = scenario.testIdentities[0].file
|
|
155
|
+
if (typeof filename !== 'string') return `scenario ${scenarioId} must identify a generated test file.`
|
|
156
|
+
if (!isPathInside(projectRoot, filename)) {
|
|
157
|
+
return `scenario ${scenarioId} file must remain inside project root ${projectRoot}.`
|
|
158
|
+
}
|
|
159
|
+
const file = files.find(entry => {
|
|
160
|
+
return typeof entry.path === 'string' && path.normalize(entry.path) === path.normalize(filename)
|
|
161
|
+
})
|
|
162
|
+
if (!file) return `scenario ${scenarioId} must identify exactly one file in generatedTestStrategy.files.`
|
|
163
|
+
selectedFiles.add(path.normalize(filename))
|
|
164
|
+
if (scenario.testIdentities[0].name !== definition.testName) {
|
|
165
|
+
return `scenario ${scenarioId} must retain test name ${JSON.stringify(definition.testName)}.`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const stateFile = ['cucumber', 'cypress', 'playwright'].includes(framework.framework)
|
|
169
|
+
? undefined
|
|
170
|
+
: getGeneratedRetryStatePath(framework.framework, filename)
|
|
171
|
+
const expectedSource = getGeneratedTestContent({
|
|
172
|
+
framework: framework.framework,
|
|
173
|
+
moduleSystem: strategy.moduleSystem,
|
|
174
|
+
scenarioId,
|
|
175
|
+
stateFile,
|
|
176
|
+
})
|
|
177
|
+
if (file.contentLines?.join('\n') !== expectedSource) {
|
|
178
|
+
return `scenario ${scenarioId} source differs from the validator-owned ${framework.framework} recipe. ` +
|
|
179
|
+
'Regenerate the manifest scaffold instead of rewriting temporary tests.'
|
|
180
|
+
}
|
|
181
|
+
if (!cleanupPaths.has(path.normalize(filename))) {
|
|
182
|
+
return `scenario ${scenarioId} file must be included in generatedTestStrategy.cleanupPaths.`
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (framework.framework === 'cucumber') {
|
|
187
|
+
const stepsFile = cucumberAdapter.getGeneratedStepsPath(strategy.testDirectory)
|
|
188
|
+
const steps = files.find(file => path.normalize(file.path) === path.normalize(stepsFile))
|
|
189
|
+
if (steps?.contentLines?.join('\n') !== cucumberAdapter.getGeneratedStepsContent()) {
|
|
190
|
+
return 'must retain the validator-owned Cucumber step definitions.'
|
|
191
|
+
}
|
|
192
|
+
if (!cleanupPaths.has(path.normalize(stepsFile))) {
|
|
193
|
+
return 'must include the isolated Cucumber step definitions in cleanupPaths.'
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (framework.framework === 'playwright') {
|
|
198
|
+
const configPath = playwrightAdapter.getGeneratedConfigPath(strategy.testDirectory)
|
|
199
|
+
const config = files.find(file => path.normalize(file.path) === path.normalize(configPath))
|
|
200
|
+
if (config?.contentLines?.join('\n') !== playwrightAdapter.getGeneratedConfigContent()) {
|
|
201
|
+
return 'must retain the validator-owned isolated Playwright config.'
|
|
202
|
+
}
|
|
203
|
+
if (!cleanupPaths.has(path.normalize(configPath))) {
|
|
204
|
+
return 'must include the isolated Playwright config in generatedTestStrategy.cleanupPaths.'
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const usesPersistentRetryState = !['cucumber', 'cypress', 'playwright'].includes(framework.framework)
|
|
209
|
+
if (usesPersistentRetryState) {
|
|
210
|
+
const atrScenario = scenarios.find(entry => entry.id === 'atr-fail-once')
|
|
211
|
+
const stateFile = getGeneratedRetryStatePath(framework.framework, atrScenario.testIdentities[0].file)
|
|
212
|
+
if (!cleanupPaths.has(path.normalize(stateFile))) {
|
|
213
|
+
return `scenario atr-fail-once must clean up its persistent retry state file ${stateFile}.`
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const expectedCleanupPathCount = usesPersistentRetryState ? 4 : expectedFileCount
|
|
217
|
+
if (selectedFiles.size !== 3 || cleanupPaths.size !== expectedCleanupPathCount) {
|
|
218
|
+
if (framework.framework === 'cucumber') {
|
|
219
|
+
return 'must use three distinct generated feature files and clean up those files plus the isolated Cucumber ' +
|
|
220
|
+
'step definitions.'
|
|
221
|
+
}
|
|
222
|
+
return usesPersistentRetryState
|
|
223
|
+
? 'must use three distinct generated test files and clean up exactly those files plus the persistent ATR ' +
|
|
224
|
+
'state file.'
|
|
225
|
+
: 'must use three distinct generated test files and clean up exactly those files.'
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Describes adapter-specific files in generated-contract errors.
|
|
231
|
+
*
|
|
232
|
+
* @param {string} framework framework name
|
|
233
|
+
* @returns {string} additional file description
|
|
234
|
+
*/
|
|
235
|
+
function getAdditionalGeneratedFileDescription (framework) {
|
|
236
|
+
if (framework === 'cucumber') return ' plus isolated Cucumber step definitions'
|
|
237
|
+
if (framework === 'playwright') return ' plus one isolated Playwright config'
|
|
238
|
+
return ''
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Checks whether a generated path is inside or equal to the declared project root.
|
|
243
|
+
*
|
|
244
|
+
* @param {string} root project root
|
|
245
|
+
* @param {string} filename generated path
|
|
246
|
+
* @returns {boolean} whether the path is contained
|
|
247
|
+
*/
|
|
248
|
+
function isPathInside (root, filename) {
|
|
249
|
+
const relative = path.relative(path.resolve(root), path.resolve(filename))
|
|
250
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Returns the persistent retry body used across independent test-runner processes.
|
|
255
|
+
*
|
|
256
|
+
* @param {object} input body input
|
|
257
|
+
* @param {string} input.moduleSystem generated module system
|
|
258
|
+
* @param {string} input.assertion passing assertion
|
|
259
|
+
* @param {string} input.stateFile persistent ATR state file
|
|
260
|
+
* @returns {string} retry test body
|
|
261
|
+
*/
|
|
262
|
+
function getAtrBody ({ moduleSystem, assertion, stateFile }) {
|
|
263
|
+
if (typeof stateFile !== 'string' || !path.isAbsolute(stateFile)) {
|
|
264
|
+
throw new Error('The validator-owned ATR recipe requires an absolute persistent state file path.')
|
|
265
|
+
}
|
|
266
|
+
const exists = moduleSystem === 'esm' ? 'existsSync' : 'fs.existsSync'
|
|
267
|
+
const write = moduleSystem === 'esm' ? 'writeFileSync' : 'fs.writeFileSync'
|
|
268
|
+
return [
|
|
269
|
+
` const stateFile = ${JSON.stringify(stateFile)}`,
|
|
270
|
+
` if (!${exists}(stateFile)) {`,
|
|
271
|
+
` ${write}(stateFile, 'failed-once')`,
|
|
272
|
+
" throw new Error('dd-test-optimization-validation atr first failure')",
|
|
273
|
+
' }',
|
|
274
|
+
` ${assertion}`,
|
|
275
|
+
].join('\n')
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = {
|
|
279
|
+
GENERATED_SCENARIOS,
|
|
280
|
+
getGeneratedRetryStatePath,
|
|
281
|
+
getGeneratedTestContent,
|
|
282
|
+
getGeneratedTestContractError,
|
|
283
|
+
}
|
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
const fs = require('node:fs')
|
|
4
4
|
const path = require('node:path')
|
|
5
5
|
|
|
6
|
+
const { getCommandBlocker } = require('./command-blocker')
|
|
6
7
|
const { runCommand, serializeDisplayCommand } = require('./command-runner')
|
|
7
8
|
const {
|
|
8
9
|
cleanupGeneratedRuntimeFiles,
|
|
9
10
|
writeGeneratedFiles,
|
|
10
11
|
} = require('./generated-files')
|
|
11
|
-
const {
|
|
12
|
-
getDatadogCleanCommand,
|
|
13
|
-
getLocalValidationCommand,
|
|
14
|
-
} = require('./local-command')
|
|
12
|
+
const { getGeneratedCommand } = require('./runner-command')
|
|
15
13
|
const { frameworkOutDir } = require('./scenarios/helpers')
|
|
16
14
|
const { getObservedTestCount } = require('./test-output')
|
|
17
15
|
|
|
@@ -42,11 +40,11 @@ async function verifyGeneratedTestStrategy ({ framework, out, options }) {
|
|
|
42
40
|
|
|
43
41
|
try {
|
|
44
42
|
cleanupGeneratedRuntimeFiles(framework)
|
|
45
|
-
writeGeneratedFiles(framework)
|
|
46
43
|
|
|
47
44
|
for (const scenario of getScenariosToVerify(strategy.scenarios, options.scenarios)) {
|
|
48
45
|
cleanupGeneratedRuntimeFiles(framework)
|
|
49
|
-
|
|
46
|
+
writeGeneratedFiles(framework, scenario)
|
|
47
|
+
const command = getGeneratedCommand(framework, scenario)
|
|
50
48
|
const outDir = frameworkOutDir(out, framework, `generated-verification-${scenario.id}`)
|
|
51
49
|
// Generated commands run serially because fail-once state and cleanup are scenario-local.
|
|
52
50
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -62,7 +60,7 @@ async function verifyGeneratedTestStrategy ({ framework, out, options }) {
|
|
|
62
60
|
const observedTestCount = getObservedTestCount(framework.framework, result.stdout, result.stderr)
|
|
63
61
|
const expected = scenario.expectedWithoutDatadog
|
|
64
62
|
const failOnceStateCreated = scenario.id === 'atr-fail-once'
|
|
65
|
-
?
|
|
63
|
+
? getGeneratedRuntimeFileStatus(strategy)
|
|
66
64
|
: undefined
|
|
67
65
|
const scenarioEvidence = {
|
|
68
66
|
id: scenario.id,
|
|
@@ -77,10 +75,19 @@ async function verifyGeneratedTestStrategy ({ framework, out, options }) {
|
|
|
77
75
|
evidence.scenarios.push(scenarioEvidence)
|
|
78
76
|
artifacts.push(...Object.values(result.artifacts))
|
|
79
77
|
|
|
78
|
+
const observedWrongTestCount = observedTestCount !== null &&
|
|
79
|
+
observedTestCount !== expected.observedTestCount
|
|
80
80
|
if (result.timedOut || result.exitCode !== expected.exitCode ||
|
|
81
|
-
|
|
81
|
+
observedWrongTestCount || failOnceStateCreated === false) {
|
|
82
82
|
cleanupGeneratedRuntimeFiles(framework)
|
|
83
|
-
return getVerificationFailure(
|
|
83
|
+
return getVerificationFailure(
|
|
84
|
+
framework,
|
|
85
|
+
evidence,
|
|
86
|
+
artifacts,
|
|
87
|
+
scenarioEvidence,
|
|
88
|
+
result,
|
|
89
|
+
observedTestCount
|
|
90
|
+
)
|
|
84
91
|
}
|
|
85
92
|
}
|
|
86
93
|
|
|
@@ -138,12 +145,36 @@ function getScenariosToVerify (scenarios, selectedFeatures) {
|
|
|
138
145
|
* @param {object} evidence collected verification evidence
|
|
139
146
|
* @param {string[]} artifacts command artifacts
|
|
140
147
|
* @param {object} scenario failed scenario evidence
|
|
141
|
-
* @param {
|
|
148
|
+
* @param {object} result command result
|
|
149
|
+
* @param {number|null} observedTestCount observed generated test count
|
|
142
150
|
* @returns {{ok: false, failure: object}} generated verification failure
|
|
143
151
|
*/
|
|
144
|
-
function getVerificationFailure (framework, evidence, artifacts, scenario,
|
|
152
|
+
function getVerificationFailure (framework, evidence, artifacts, scenario, result, observedTestCount) {
|
|
153
|
+
const commandFailure = getCommandBlocker(result, {
|
|
154
|
+
browserRequired: framework.browserRequired,
|
|
155
|
+
framework: framework.framework,
|
|
156
|
+
testsRan: Number.isInteger(observedTestCount) && observedTestCount > 0,
|
|
157
|
+
})
|
|
158
|
+
if (commandFailure) {
|
|
159
|
+
const domain = commandFailure.blockedByExecutionEnvironment
|
|
160
|
+
? 'execution_environment'
|
|
161
|
+
: commandFailure.localRuntimeBlocked
|
|
162
|
+
? 'local_runtime'
|
|
163
|
+
: 'project_setup'
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
failure: {
|
|
167
|
+
frameworkId: framework.id,
|
|
168
|
+
scenario: 'generated-test-verification',
|
|
169
|
+
status: 'blocked',
|
|
170
|
+
diagnosis: `${commandFailure.summary} Advanced-feature validation could not start reliably.`,
|
|
171
|
+
evidence: { ...evidence, commandFailure, domain, validationIncomplete: true },
|
|
172
|
+
artifacts,
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
}
|
|
145
176
|
let reason
|
|
146
|
-
if (timedOut) {
|
|
177
|
+
if (result.timedOut) {
|
|
147
178
|
reason = 'timed out'
|
|
148
179
|
} else if (scenario.id === 'atr-fail-once' && scenario.failOnceStateCreated === false) {
|
|
149
180
|
reason = 'failed without creating its declared fail-once state file, so it failed for an unrelated reason'
|
|
@@ -165,22 +196,24 @@ function getVerificationFailure (framework, evidence, artifacts, scenario, timed
|
|
|
165
196
|
}
|
|
166
197
|
|
|
167
198
|
/**
|
|
168
|
-
* Checks
|
|
199
|
+
* Checks a declared generated runtime state file when the adapter uses one.
|
|
169
200
|
*
|
|
170
201
|
* @param {object} strategy generated test strategy
|
|
171
|
-
* @returns {boolean} whether a
|
|
202
|
+
* @returns {boolean|undefined} whether a declared runtime file exists, or undefined when none is required
|
|
172
203
|
*/
|
|
173
|
-
function
|
|
204
|
+
function getGeneratedRuntimeFileStatus (strategy) {
|
|
174
205
|
const generatedFiles = new Set((strategy.files || []).map(file => path.resolve(file.path)))
|
|
206
|
+
let expectsRuntimeFile = false
|
|
175
207
|
for (const cleanupPath of strategy.cleanupPaths || []) {
|
|
176
208
|
const filename = path.resolve(cleanupPath)
|
|
177
209
|
if (generatedFiles.has(filename)) continue
|
|
210
|
+
expectsRuntimeFile = true
|
|
178
211
|
try {
|
|
179
212
|
const stat = fs.lstatSync(filename)
|
|
180
213
|
if (!stat.isSymbolicLink() && stat.isFile()) return true
|
|
181
214
|
} catch {}
|
|
182
215
|
}
|
|
183
|
-
return false
|
|
216
|
+
return expectsRuntimeFile ? false : undefined
|
|
184
217
|
}
|
|
185
218
|
|
|
186
219
|
/**
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { environmentNamesEqual } = require('./environment')
|
|
4
|
+
|
|
5
|
+
const ASSIGNMENT_PATTERN =
|
|
6
|
+
/([A-Za-z_][A-Za-z0-9_]*)=(?:"([^"\r\n]*)"|'([^'\r\n]*)'|([^\s]*))(?:[ \t]+|$)/y
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parses only complete literal environment assignments at the start of a command.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} command command text
|
|
12
|
+
* @returns {{assignments: Array<{name: string, source: string, value: string}>, length: number}} parsed prefix
|
|
13
|
+
*/
|
|
14
|
+
function parseLiteralEnvironmentPrefix (command) {
|
|
15
|
+
const source = String(command || '')
|
|
16
|
+
const assignments = []
|
|
17
|
+
let offset = 0
|
|
18
|
+
|
|
19
|
+
while (offset < source.length) {
|
|
20
|
+
ASSIGNMENT_PATTERN.lastIndex = offset
|
|
21
|
+
const match = ASSIGNMENT_PATTERN.exec(source)
|
|
22
|
+
if (!match) break
|
|
23
|
+
assignments.push({
|
|
24
|
+
name: match[1],
|
|
25
|
+
source: match[0].trimEnd(),
|
|
26
|
+
value: match[2] ?? match[3] ?? match[4],
|
|
27
|
+
})
|
|
28
|
+
offset = ASSIGNMENT_PATTERN.lastIndex
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { assignments, length: offset }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Removes complete leading assignments that explicitly clear one environment variable.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} command command text
|
|
38
|
+
* @param {string} name environment variable
|
|
39
|
+
* @param {string} [platform] target platform
|
|
40
|
+
* @returns {string} command without empty assignments
|
|
41
|
+
*/
|
|
42
|
+
function removeEmptyLiteralEnvironmentAssignments (command, name, platform = process.platform) {
|
|
43
|
+
const source = String(command || '')
|
|
44
|
+
const prefix = parseLiteralEnvironmentPrefix(source)
|
|
45
|
+
const retained = prefix.assignments.filter(assignment => {
|
|
46
|
+
return !environmentNamesEqual(assignment.name, name, platform) || assignment.value !== ''
|
|
47
|
+
})
|
|
48
|
+
if (retained.length === prefix.assignments.length) return source
|
|
49
|
+
return [...retained.map(assignment => assignment.source), source.slice(prefix.length)]
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.join(' ')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
parseLiteralEnvironmentPrefix,
|
|
56
|
+
removeEmptyLiteralEnvironmentAssignments,
|
|
57
|
+
}
|
|
@@ -76,18 +76,14 @@ function getManifestPaths (manifest) {
|
|
|
76
76
|
paths.push(
|
|
77
77
|
[`${prefix}.project.root`, framework.project?.root],
|
|
78
78
|
[`${prefix}.project.packageJson`, framework.project?.packageJson],
|
|
79
|
+
[`${prefix}.validation.runner`, framework.validation?.runner],
|
|
80
|
+
[`${prefix}.validation.testFile`, framework.validation?.testFile],
|
|
79
81
|
[`${prefix}.ciWiring.configFile`, framework.ciWiring?.configFile],
|
|
80
82
|
[`${prefix}.ciWiring.workingDirectory`, framework.ciWiring?.workingDirectory]
|
|
81
83
|
)
|
|
82
84
|
for (const [index, configFile] of (framework.project?.configFiles || []).entries()) {
|
|
83
85
|
paths.push([`${prefix}.project.configFiles[${index}]`, configFile])
|
|
84
86
|
}
|
|
85
|
-
for (const [name, command] of getCommands(framework)) {
|
|
86
|
-
paths.push([`${prefix}.${name}.cwd`, command.cwd])
|
|
87
|
-
for (const [outputIndex, outputPath] of (command.outputPaths || []).entries()) {
|
|
88
|
-
paths.push([`${prefix}.${name}.outputPaths[${outputIndex}]`, outputPath])
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
87
|
|
|
92
88
|
const strategy = framework.generatedTestStrategy
|
|
93
89
|
paths.push([`${prefix}.generatedTestStrategy.testDirectory`, strategy?.testDirectory])
|
|
@@ -109,22 +105,6 @@ function getManifestPaths (manifest) {
|
|
|
109
105
|
return paths
|
|
110
106
|
}
|
|
111
107
|
|
|
112
|
-
function getCommands (framework) {
|
|
113
|
-
const commands = []
|
|
114
|
-
for (const name of ['existingTestCommand', 'ciWiringCommand']) {
|
|
115
|
-
if (framework[name]) commands.push([name, framework[name]])
|
|
116
|
-
}
|
|
117
|
-
for (const [index, command] of (framework.setup?.commands || []).entries()) {
|
|
118
|
-
commands.push([`setup.commands[${index}]`, command])
|
|
119
|
-
}
|
|
120
|
-
for (const [index, scenario] of (framework.generatedTestStrategy?.scenarios || []).entries()) {
|
|
121
|
-
if (scenario?.runCommand) {
|
|
122
|
-
commands.push([`generatedTestStrategy.scenarios[${index}].runCommand`, scenario.runCommand])
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return commands
|
|
126
|
-
}
|
|
127
|
-
|
|
128
108
|
function isPathInside (root, filename) {
|
|
129
109
|
const relative = path.relative(root, filename)
|
|
130
110
|
return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative))
|