dd-trace 5.117.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 +2 -2
- 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/openai-agents.js +159 -0
- 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/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/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,119 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
|
|
5
|
+
const CUCUMBER_PACKAGE = '@cucumber/cucumber'
|
|
6
|
+
const CONFIG_PATTERN = /^cucumber\.(?:[cm]?js|json|ya?ml)$/
|
|
7
|
+
const GENERATED_STEPS_FILENAME = 'dd-test-optimization-validation.steps.cjs'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reports whether a file follows the Cucumber feature convention.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} filename candidate filename
|
|
13
|
+
* @returns {boolean} whether the candidate can be selected by Cucumber
|
|
14
|
+
*/
|
|
15
|
+
function isTestFile (filename) {
|
|
16
|
+
return filename.endsWith('.feature')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Counts statically declared Cucumber scenarios in a feature.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} source feature source
|
|
23
|
+
* @returns {number} declared scenario count
|
|
24
|
+
*/
|
|
25
|
+
function getScenarioCount (source) {
|
|
26
|
+
return [...source.matchAll(/^\s*Scenario(?: Outline)?:\s*\S/gm)].length
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns validator-owned Cucumber feature source for one generated scenario.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} input generated source input
|
|
33
|
+
* @param {string} input.testName generated scenario name
|
|
34
|
+
* @returns {string} canonical generated feature source
|
|
35
|
+
*/
|
|
36
|
+
function getGeneratedTestContent ({ testName }) {
|
|
37
|
+
return [
|
|
38
|
+
'Feature: Datadog Test Optimization validation',
|
|
39
|
+
'',
|
|
40
|
+
` Scenario: ${testName}`,
|
|
41
|
+
` Given the Datadog validation scenario ${JSON.stringify(testName)}`,
|
|
42
|
+
].join('\n')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns the validator-owned Cucumber step definitions shared by generated scenarios.
|
|
47
|
+
*
|
|
48
|
+
* @returns {string} canonical generated step-definition source
|
|
49
|
+
*/
|
|
50
|
+
function getGeneratedStepsContent () {
|
|
51
|
+
return [
|
|
52
|
+
"'use strict'",
|
|
53
|
+
'',
|
|
54
|
+
`const { Given } = require(${JSON.stringify(CUCUMBER_PACKAGE)})`,
|
|
55
|
+
'',
|
|
56
|
+
'let atrAttempt = 0',
|
|
57
|
+
'',
|
|
58
|
+
"Given('the Datadog validation scenario {string}', function (scenario) {",
|
|
59
|
+
" if (scenario === 'atr-fail-once' && atrAttempt++ === 0) {",
|
|
60
|
+
" throw new Error('dd-test-optimization-validation atr first failure')",
|
|
61
|
+
' }',
|
|
62
|
+
'})',
|
|
63
|
+
].join('\n')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns the generated Cucumber step-definition path for a feature directory.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} testDirectory generated feature directory
|
|
70
|
+
* @returns {string} generated step-definition path
|
|
71
|
+
*/
|
|
72
|
+
function getGeneratedStepsPath (testDirectory) {
|
|
73
|
+
return path.join(testDirectory, GENERATED_STEPS_FILENAME)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Returns Cucumber arguments that select one existing feature.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} filename selected Cucumber feature
|
|
80
|
+
* @returns {string[]} focused Cucumber arguments
|
|
81
|
+
*/
|
|
82
|
+
function getFocusedTestArgs (filename) {
|
|
83
|
+
return [filename, '--format', 'progress']
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Returns Cucumber arguments for one isolated generated scenario.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} filename generated Cucumber feature
|
|
90
|
+
* @param {string} stepsFile generated Cucumber step definitions
|
|
91
|
+
* @returns {string[]} generated scenario arguments
|
|
92
|
+
*/
|
|
93
|
+
function getGeneratedTestArgs (filename, stepsFile) {
|
|
94
|
+
return ['--require', stepsFile, '--format', 'progress', filename]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extracts the executed-scenario count from the final Cucumber summary.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} output Cucumber output without ANSI escapes
|
|
101
|
+
* @returns {number|null} final scenario count when present
|
|
102
|
+
*/
|
|
103
|
+
function getObservedTestCount (output) {
|
|
104
|
+
let count = null
|
|
105
|
+
for (const match of output.matchAll(/\b(\d+)\s+scenarios?\b/gi)) count = Number(match[1])
|
|
106
|
+
return count
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
CONFIG_PATTERN,
|
|
111
|
+
getFocusedTestArgs,
|
|
112
|
+
getGeneratedStepsContent,
|
|
113
|
+
getGeneratedStepsPath,
|
|
114
|
+
getGeneratedTestArgs,
|
|
115
|
+
getGeneratedTestContent,
|
|
116
|
+
getObservedTestCount,
|
|
117
|
+
getScenarioCount,
|
|
118
|
+
isTestFile,
|
|
119
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
|
|
5
|
+
const CONFIG_PATTERN = /^cypress(?:\.config)?\.(?:[cm]?[jt]s|json)$/
|
|
6
|
+
const TEST_FILE_PATTERN = /(?:\.cy\.[cm]?[jt]sx?|\.(?:spec|test)\.[cm]?[jt]sx?)$/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reports whether a file follows a Cypress spec convention.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} filename candidate filename
|
|
12
|
+
* @param {string} directory candidate parent directory
|
|
13
|
+
* @param {string} projectRoot detected project root
|
|
14
|
+
* @returns {boolean} whether the candidate can be selected by Cypress
|
|
15
|
+
*/
|
|
16
|
+
function isTestFile (filename, directory, projectRoot) {
|
|
17
|
+
if (/\.cy\.[cm]?[jt]sx?$/.test(filename)) return true
|
|
18
|
+
const relativeDirectory = new Set(path.relative(projectRoot, directory).split(path.sep))
|
|
19
|
+
return relativeDirectory.has('cypress') &&
|
|
20
|
+
(relativeDirectory.has('e2e') || relativeDirectory.has('integration')) &&
|
|
21
|
+
TEST_FILE_PATTERN.test(filename)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns the complete suffix that a generated Cypress spec must preserve.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} filename representative Cypress spec
|
|
28
|
+
* @returns {string} generated spec suffix
|
|
29
|
+
*/
|
|
30
|
+
function getTestExtension (filename) {
|
|
31
|
+
return TEST_FILE_PATTERN.exec(path.basename(filename))?.[0] || '.cy.js'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns validator-owned Cypress source for one generated scenario.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} input generated source input
|
|
38
|
+
* @param {string} input.scenarioId generated scenario id
|
|
39
|
+
* @param {string} input.testName generated test name
|
|
40
|
+
* @returns {string} canonical generated Cypress source
|
|
41
|
+
*/
|
|
42
|
+
function getGeneratedTestContent ({ scenarioId, testName }) {
|
|
43
|
+
const lines = []
|
|
44
|
+
if (scenarioId === 'atr-fail-once') {
|
|
45
|
+
lines.push('let attempt = 0', '')
|
|
46
|
+
}
|
|
47
|
+
lines.push(
|
|
48
|
+
"describe('dd-test-optimization-validation', () => {",
|
|
49
|
+
` it(${JSON.stringify(testName)}, () => {`,
|
|
50
|
+
scenarioId === 'atr-fail-once'
|
|
51
|
+
? ' expect(attempt++).to.equal(1)'
|
|
52
|
+
: ' expect(true).to.equal(true)',
|
|
53
|
+
' })',
|
|
54
|
+
'})'
|
|
55
|
+
)
|
|
56
|
+
return lines.join('\n')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Returns Cypress arguments that select one existing project spec.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} filename selected Cypress spec
|
|
63
|
+
* @returns {string[]} focused Cypress arguments
|
|
64
|
+
*/
|
|
65
|
+
function getFocusedTestArgs (filename) {
|
|
66
|
+
return ['--spec', filename]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Returns Cypress arguments for one isolated generated scenario.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} filename generated Cypress spec
|
|
73
|
+
* @param {string[]} configurationArgs approved Cypress configuration arguments
|
|
74
|
+
* @returns {string[]} generated scenario arguments
|
|
75
|
+
*/
|
|
76
|
+
function getGeneratedTestArgs (filename, configurationArgs) {
|
|
77
|
+
return [
|
|
78
|
+
'run',
|
|
79
|
+
...configurationArgs,
|
|
80
|
+
'--spec',
|
|
81
|
+
filename,
|
|
82
|
+
'--config',
|
|
83
|
+
'video=false,screenshotOnRunFailure=false,retries=0',
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extracts the executed-test count from the final Cypress run summary.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} output Cypress output without ANSI escapes
|
|
91
|
+
* @returns {number|null} final test count when present
|
|
92
|
+
*/
|
|
93
|
+
function getObservedTestCount (output) {
|
|
94
|
+
let count = null
|
|
95
|
+
for (const match of output.matchAll(/\bTests\s*:\s*(\d+)\b/gi)) count = Number(match[1])
|
|
96
|
+
return count
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
CONFIG_PATTERN,
|
|
101
|
+
getFocusedTestArgs,
|
|
102
|
+
getGeneratedTestArgs,
|
|
103
|
+
getGeneratedTestContent,
|
|
104
|
+
getObservedTestCount,
|
|
105
|
+
getTestExtension,
|
|
106
|
+
isTestFile,
|
|
107
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
|
|
5
|
+
const CONFIG_PATTERN = /^playwright\.config\.[cm]?[jt]s$/
|
|
6
|
+
const TEST_FILE_PATTERN = /(?:\.(?:spec|test)\.[cm]?[jt]sx?)$/
|
|
7
|
+
const GENERATED_CONFIG_FILENAME = 'dd-test-optimization-validation.playwright.config.cjs'
|
|
8
|
+
const VALIDATION_OUTPUT_DIRECTORY = '.dd-test-optimization-validation-playwright-output'
|
|
9
|
+
const PLAYWRIGHT_PACKAGE = '@playwright/test'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reports whether a file follows a Playwright Test convention.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} filename candidate filename
|
|
15
|
+
* @param {string} directory candidate parent directory
|
|
16
|
+
* @param {string} projectRoot detected project root
|
|
17
|
+
* @returns {boolean} whether the candidate can be selected by Playwright Test
|
|
18
|
+
*/
|
|
19
|
+
function isTestFile (filename, directory, projectRoot) {
|
|
20
|
+
if (!TEST_FILE_PATTERN.test(filename)) return false
|
|
21
|
+
const relative = path.relative(projectRoot, path.join(directory, filename))
|
|
22
|
+
return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns the complete suffix that a generated Playwright spec must preserve.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} filename representative Playwright spec
|
|
29
|
+
* @returns {string} generated spec suffix
|
|
30
|
+
*/
|
|
31
|
+
function getTestExtension (filename) {
|
|
32
|
+
return TEST_FILE_PATTERN.exec(path.basename(filename))?.[0] || '.spec.js'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns validator-owned Playwright source for one generated scenario.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} input generated source input
|
|
39
|
+
* @param {string} input.moduleSystem generated module system
|
|
40
|
+
* @param {string} input.scenarioId generated scenario id
|
|
41
|
+
* @param {string} input.testName generated test name
|
|
42
|
+
* @returns {string} canonical generated Playwright source
|
|
43
|
+
*/
|
|
44
|
+
function getGeneratedTestContent ({ moduleSystem, scenarioId, testName }) {
|
|
45
|
+
const assertion = scenarioId === 'atr-fail-once'
|
|
46
|
+
? ' expect(test.info().retry).toBe(1)'
|
|
47
|
+
: ' expect(true).toBe(true)'
|
|
48
|
+
const imports = moduleSystem === 'commonjs'
|
|
49
|
+
? `const { expect, test } = require(${JSON.stringify(PLAYWRIGHT_PACKAGE)})`
|
|
50
|
+
: `import { expect, test } from ${JSON.stringify(PLAYWRIGHT_PACKAGE)}`
|
|
51
|
+
return [
|
|
52
|
+
imports,
|
|
53
|
+
'',
|
|
54
|
+
`test(${JSON.stringify(testName)}, async () => {`,
|
|
55
|
+
assertion,
|
|
56
|
+
'})',
|
|
57
|
+
].join('\n')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns the isolated Playwright config shared by generated validation tests.
|
|
62
|
+
*
|
|
63
|
+
* @returns {string} canonical generated Playwright config
|
|
64
|
+
*/
|
|
65
|
+
function getGeneratedConfigContent () {
|
|
66
|
+
return [
|
|
67
|
+
`const { defineConfig } = require(${JSON.stringify(PLAYWRIGHT_PACKAGE)})`,
|
|
68
|
+
'',
|
|
69
|
+
'module.exports = defineConfig({',
|
|
70
|
+
' fullyParallel: false,',
|
|
71
|
+
' forbidOnly: true,',
|
|
72
|
+
" reporter: 'line',",
|
|
73
|
+
' retries: 0,',
|
|
74
|
+
' testDir: __dirname,',
|
|
75
|
+
' workers: 1,',
|
|
76
|
+
'})',
|
|
77
|
+
].join('\n')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns the generated Playwright config path for a test directory.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} testDirectory generated test directory
|
|
84
|
+
* @returns {string} generated config path
|
|
85
|
+
*/
|
|
86
|
+
function getGeneratedConfigPath (testDirectory) {
|
|
87
|
+
return path.join(testDirectory, GENERATED_CONFIG_FILENAME)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Returns Playwright arguments that select one existing project spec.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} filename selected Playwright spec
|
|
94
|
+
* @returns {string[]} focused Playwright arguments
|
|
95
|
+
*/
|
|
96
|
+
function getFocusedTestArgs (filename) {
|
|
97
|
+
return [
|
|
98
|
+
filename,
|
|
99
|
+
'--reporter=line',
|
|
100
|
+
'--workers=1',
|
|
101
|
+
`--output=${getOutputPath(filename)}`,
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Returns Playwright arguments for one isolated generated scenario.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} filename generated Playwright spec
|
|
109
|
+
* @param {string} configFile generated Playwright config
|
|
110
|
+
* @returns {string[]} generated scenario arguments
|
|
111
|
+
*/
|
|
112
|
+
function getGeneratedTestArgs (filename, configFile) {
|
|
113
|
+
return [
|
|
114
|
+
'test',
|
|
115
|
+
'--config',
|
|
116
|
+
configFile,
|
|
117
|
+
filename,
|
|
118
|
+
'--reporter=line',
|
|
119
|
+
'--workers=1',
|
|
120
|
+
`--output=${getOutputPath(filename)}`,
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Returns the validator-owned Playwright output directory for a selected spec.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} filename selected or generated Playwright spec
|
|
128
|
+
* @returns {string} absolute output directory
|
|
129
|
+
*/
|
|
130
|
+
function getOutputPath (filename) {
|
|
131
|
+
return path.join(path.dirname(filename), VALIDATION_OUTPUT_DIRECTORY)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Extracts the executed-test count from the final Playwright summary.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} output Playwright output without ANSI escapes
|
|
138
|
+
* @returns {number|null} executed test count when present
|
|
139
|
+
*/
|
|
140
|
+
function getObservedTestCount (output) {
|
|
141
|
+
const observed = sumLastMatchCounts(output, [
|
|
142
|
+
/^\s*(\d+)\s+passed\b/gim,
|
|
143
|
+
/^\s*(\d+)\s+failed\b/gim,
|
|
144
|
+
/^\s*(\d+)\s+flaky\b/gim,
|
|
145
|
+
])
|
|
146
|
+
if (observed !== null) return observed
|
|
147
|
+
return /^\s*\d+\s+skipped\b/im.test(output) ? 0 : null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Sums the final count for each Playwright summary pattern.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} output Playwright output
|
|
154
|
+
* @param {RegExp[]} patterns summary patterns
|
|
155
|
+
* @returns {number|null} summed count when any pattern matched
|
|
156
|
+
*/
|
|
157
|
+
function sumLastMatchCounts (output, patterns) {
|
|
158
|
+
let found = false
|
|
159
|
+
let total = 0
|
|
160
|
+
for (const pattern of patterns) {
|
|
161
|
+
let count
|
|
162
|
+
for (const match of output.matchAll(pattern)) count = Number(match[1])
|
|
163
|
+
if (count === undefined) continue
|
|
164
|
+
found = true
|
|
165
|
+
total += count
|
|
166
|
+
}
|
|
167
|
+
return found ? total : null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
CONFIG_PATTERN,
|
|
172
|
+
getFocusedTestArgs,
|
|
173
|
+
getGeneratedConfigContent,
|
|
174
|
+
getGeneratedConfigPath,
|
|
175
|
+
getGeneratedTestArgs,
|
|
176
|
+
getGeneratedTestContent,
|
|
177
|
+
getObservedTestCount,
|
|
178
|
+
getOutputPath,
|
|
179
|
+
getTestExtension,
|
|
180
|
+
isTestFile,
|
|
181
|
+
}
|
|
@@ -15,7 +15,7 @@ const initializedCleanupStrategies = new WeakSet()
|
|
|
15
15
|
const authorizedRuntimeCleanupFiles = new Map()
|
|
16
16
|
const writtenGeneratedFiles = new Map()
|
|
17
17
|
|
|
18
|
-
function writeGeneratedFiles (framework) {
|
|
18
|
+
function writeGeneratedFiles (framework, scenario) {
|
|
19
19
|
const strategy = framework.generatedTestStrategy
|
|
20
20
|
if (!strategy || !['planned', 'verified'].includes(strategy.status)) {
|
|
21
21
|
return []
|
|
@@ -26,8 +26,9 @@ function writeGeneratedFiles (framework) {
|
|
|
26
26
|
initializeRuntimeCleanupFiles(framework, strategy)
|
|
27
27
|
|
|
28
28
|
const written = []
|
|
29
|
+
const files = getScenarioFiles(strategy, scenario)
|
|
29
30
|
try {
|
|
30
|
-
for (const file of
|
|
31
|
+
for (const file of files) {
|
|
31
32
|
const filename = validateGeneratedFilePath(framework, file.path)
|
|
32
33
|
validateContentLines(file.contentLines, filename)
|
|
33
34
|
const content = `${file.contentLines.join('\n')}\n`
|
|
@@ -62,14 +63,47 @@ function writeGeneratedFiles (framework) {
|
|
|
62
63
|
return written
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Returns one scenario file plus adapter support files, or every file when no scenario is selected.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} strategy generated strategy
|
|
70
|
+
* @param {object|undefined} scenario selected generated scenario
|
|
71
|
+
* @returns {object[]} generated files to write
|
|
72
|
+
*/
|
|
73
|
+
function getScenarioFiles (strategy, scenario) {
|
|
74
|
+
if (!scenario) return strategy.files || []
|
|
75
|
+
const scenarioPaths = new Set((strategy.scenarios || []).map(candidate => {
|
|
76
|
+
return path.resolve(candidate.testIdentities[0].file)
|
|
77
|
+
}))
|
|
78
|
+
const selectedPath = path.resolve(scenario.testIdentities[0].file)
|
|
79
|
+
return (strategy.files || []).filter(file => {
|
|
80
|
+
const filename = path.resolve(file.path)
|
|
81
|
+
return filename === selectedPath || !scenarioPaths.has(filename)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
65
85
|
function cleanupGeneratedFiles (manifest, { keep = false } = {}) {
|
|
66
|
-
if (keep) return
|
|
86
|
+
if (keep) return { status: 'retained_by_request' }
|
|
67
87
|
|
|
88
|
+
const outcome = {
|
|
89
|
+
directoriesRemoved: 0,
|
|
90
|
+
directoriesRetained: 0,
|
|
91
|
+
filesRemoved: 0,
|
|
92
|
+
filesRetained: 0,
|
|
93
|
+
}
|
|
68
94
|
for (const framework of manifest.frameworks || []) {
|
|
69
95
|
const strategy = framework.generatedTestStrategy
|
|
70
|
-
|
|
71
|
-
|
|
96
|
+
addCleanupOutcome(
|
|
97
|
+
outcome,
|
|
98
|
+
cleanupPaths(getSafeCleanupPaths(framework, strategy, { includeGeneratedFiles: true })),
|
|
99
|
+
'files'
|
|
100
|
+
)
|
|
101
|
+
addCleanupOutcome(outcome, cleanupCreatedDirectories(framework.project.root), 'directories')
|
|
72
102
|
}
|
|
103
|
+
outcome.status = outcome.filesRetained > 0 || outcome.directoriesRetained > 0
|
|
104
|
+
? 'incomplete'
|
|
105
|
+
: 'completed'
|
|
106
|
+
return outcome
|
|
73
107
|
}
|
|
74
108
|
|
|
75
109
|
/**
|
|
@@ -96,21 +130,30 @@ function getMissingDirectories (root, directory) {
|
|
|
96
130
|
* @param {string} root project root
|
|
97
131
|
*/
|
|
98
132
|
function cleanupCreatedDirectories (root) {
|
|
133
|
+
const outcome = { removed: 0, retained: 0 }
|
|
99
134
|
const resolvedRoot = path.resolve(root)
|
|
100
135
|
const directories = [...createdGeneratedDirectories.entries()]
|
|
101
|
-
.filter(([directory
|
|
102
|
-
return isPathInside(resolvedRoot, directory) && isCleanupAuthorizationValid(directory, authorization)
|
|
103
|
-
})
|
|
136
|
+
.filter(([directory]) => isPathInside(resolvedRoot, directory))
|
|
104
137
|
.sort(([left], [right]) => right.length - left.length)
|
|
105
138
|
|
|
106
|
-
for (const [directory] of directories) {
|
|
139
|
+
for (const [directory, authorization] of directories) {
|
|
140
|
+
if (!isCleanupAuthorizationValid(directory, authorization)) {
|
|
141
|
+
if (pathExists(directory)) outcome.retained++
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
107
144
|
try {
|
|
108
145
|
fs.rmdirSync(directory)
|
|
109
146
|
createdGeneratedDirectories.delete(directory)
|
|
147
|
+
outcome.removed++
|
|
110
148
|
} catch (error) {
|
|
111
|
-
if (error.code === 'ENOENT')
|
|
149
|
+
if (error.code === 'ENOENT') {
|
|
150
|
+
createdGeneratedDirectories.delete(directory)
|
|
151
|
+
} else {
|
|
152
|
+
outcome.retained++
|
|
153
|
+
}
|
|
112
154
|
}
|
|
113
155
|
}
|
|
156
|
+
return outcome
|
|
114
157
|
}
|
|
115
158
|
|
|
116
159
|
function cleanupGeneratedRuntimeFiles (framework) {
|
|
@@ -166,18 +209,37 @@ function getSafeCleanupPaths (framework, strategy, { includeGeneratedFiles }) {
|
|
|
166
209
|
}
|
|
167
210
|
|
|
168
211
|
function cleanupPaths (cleanupPaths) {
|
|
212
|
+
const outcome = { removed: 0, retained: 0 }
|
|
169
213
|
for (const cleanupPath of cleanupPaths) {
|
|
170
214
|
try {
|
|
171
215
|
const authorization = writtenGeneratedFiles.get(cleanupPath) ||
|
|
172
216
|
authorizedRuntimeCleanupFiles.get(cleanupPath)
|
|
173
|
-
if (!authorization || !isCleanupAuthorizationValid(cleanupPath, authorization))
|
|
174
|
-
|
|
217
|
+
if (!authorization || !isCleanupAuthorizationValid(cleanupPath, authorization) || isDirectory(cleanupPath)) {
|
|
218
|
+
if (pathExists(cleanupPath)) outcome.retained++
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
175
221
|
fs.rmSync(cleanupPath, { force: true })
|
|
176
222
|
writtenGeneratedFiles.delete(cleanupPath)
|
|
223
|
+
outcome.removed++
|
|
177
224
|
} catch {
|
|
178
|
-
|
|
225
|
+
if (pathExists(cleanupPath)) outcome.retained++
|
|
179
226
|
}
|
|
180
227
|
}
|
|
228
|
+
return outcome
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function addCleanupOutcome (target, outcome, kind) {
|
|
232
|
+
target[`${kind}Removed`] += outcome.removed
|
|
233
|
+
target[`${kind}Retained`] += outcome.retained
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function pathExists (filename) {
|
|
237
|
+
try {
|
|
238
|
+
fs.lstatSync(filename)
|
|
239
|
+
return true
|
|
240
|
+
} catch {
|
|
241
|
+
return false
|
|
242
|
+
}
|
|
181
243
|
}
|
|
182
244
|
|
|
183
245
|
function forgetWrittenGeneratedFiles (filenames) {
|