dd-trace 6.4.0 → 6.5.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/README.electron.md +7 -0
- package/README.md +17 -0
- package/ci/diagnose.js +2100 -0
- package/ci/init.js +23 -2
- package/ci/runbook.md +198 -0
- package/ci/test-optimization-validation/approval-artifacts.js +143 -0
- package/ci/test-optimization-validation/approval.js +299 -0
- package/ci/test-optimization-validation/artifact-id.js +20 -0
- package/ci/test-optimization-validation/bounded-json.js +121 -0
- package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
- package/ci/test-optimization-validation/ci-discovery.js +181 -0
- package/ci/test-optimization-validation/ci-remediation.js +210 -0
- package/ci/test-optimization-validation/cli.js +903 -0
- package/ci/test-optimization-validation/command-blocker.js +81 -0
- package/ci/test-optimization-validation/command-output-policy.js +206 -0
- package/ci/test-optimization-validation/command-runner.js +707 -0
- package/ci/test-optimization-validation/command-suitability.js +471 -0
- package/ci/test-optimization-validation/executable-approval.js +48 -0
- package/ci/test-optimization-validation/executable.js +480 -0
- package/ci/test-optimization-validation/generated-file-policy.js +63 -0
- package/ci/test-optimization-validation/generated-files.js +351 -0
- package/ci/test-optimization-validation/generated-verifier.js +196 -0
- package/ci/test-optimization-validation/init-probe-preload.js +163 -0
- package/ci/test-optimization-validation/init-probe.js +246 -0
- package/ci/test-optimization-validation/late-initialization.js +63 -0
- package/ci/test-optimization-validation/local-command.js +163 -0
- package/ci/test-optimization-validation/manifest-loader.js +133 -0
- package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
- package/ci/test-optimization-validation/manifest-schema.js +862 -0
- package/ci/test-optimization-validation/offline-fixtures.js +327 -0
- package/ci/test-optimization-validation/offline-output.js +406 -0
- package/ci/test-optimization-validation/payload-normalizer.js +72 -0
- package/ci/test-optimization-validation/plan-writer.js +1120 -0
- package/ci/test-optimization-validation/preflight-runner.js +114 -0
- package/ci/test-optimization-validation/redaction.js +331 -0
- package/ci/test-optimization-validation/report-writer.js +1508 -0
- package/ci/test-optimization-validation/safe-files.js +203 -0
- package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
- package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
- package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
- package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
- package/ci/test-optimization-validation/setup-runner.js +97 -0
- package/ci/test-optimization-validation/static-diagnosis.js +159 -0
- package/ci/test-optimization-validation/test-output.js +129 -0
- package/ci/test-optimization-validation-manifest.schema.json +1 -0
- package/ci/validate-test-optimization.js +3 -0
- package/ext/exporters.js +1 -0
- package/index.d.ts +121 -8
- package/init.js +18 -0
- package/initialize.mjs +1 -1
- package/loader-hook.mjs +28 -18
- package/openfeature.d.ts +1 -0
- package/openfeature.js +4 -0
- package/package.json +10 -7
- package/packages/datadog-instrumentations/src/ai.js +8 -2
- package/packages/datadog-instrumentations/src/child_process.js +1 -1
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
- package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
- package/packages/datadog-instrumentations/src/cucumber.js +20 -7
- package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
- package/packages/datadog-instrumentations/src/fastify.js +7 -11
- package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
- package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
- package/packages/datadog-instrumentations/src/jest.js +26 -0
- package/packages/datadog-instrumentations/src/mariadb.js +1 -1
- package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
- package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
- package/packages/datadog-instrumentations/src/mongodb.js +3 -3
- package/packages/datadog-instrumentations/src/mongoose.js +3 -3
- package/packages/datadog-instrumentations/src/mquery.js +2 -2
- package/packages/datadog-instrumentations/src/mysql.js +1 -1
- package/packages/datadog-instrumentations/src/next.js +24 -0
- package/packages/datadog-instrumentations/src/nyc.js +0 -2
- package/packages/datadog-instrumentations/src/oracledb.js +2 -2
- package/packages/datadog-instrumentations/src/pg.js +2 -2
- package/packages/datadog-instrumentations/src/playwright.js +159 -22
- package/packages/datadog-instrumentations/src/process.js +3 -0
- package/packages/datadog-instrumentations/src/router.js +18 -8
- package/packages/datadog-instrumentations/src/selenium.js +52 -26
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
- package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
- package/packages/datadog-instrumentations/src/ws.js +2 -1
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
- package/packages/datadog-plugin-aws-sdk/src/util.js +2 -2
- package/packages/datadog-plugin-cucumber/src/index.js +1 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -57
- package/packages/datadog-plugin-cypress/src/index.js +6 -1
- package/packages/datadog-plugin-cypress/src/support.js +92 -26
- package/packages/datadog-plugin-electron/src/ipc.js +1 -1
- package/packages/datadog-plugin-graphql/src/execute.js +87 -45
- package/packages/datadog-plugin-graphql/src/validate.js +2 -2
- package/packages/datadog-plugin-http2/src/client.js +1 -1
- package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
- package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
- package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
- package/packages/datadog-plugin-playwright/src/index.js +177 -64
- package/packages/datadog-plugin-router/src/index.js +11 -2
- package/packages/datadog-plugin-selenium/src/index.js +5 -36
- package/packages/datadog-plugin-ws/src/close.js +2 -1
- package/packages/datadog-plugin-ws/src/producer.js +2 -1
- package/packages/datadog-plugin-ws/src/receiver.js +2 -1
- package/packages/datadog-plugin-ws/src/server.js +2 -1
- package/packages/dd-trace/src/aiguard/sdk.js +1 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
- package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
- package/packages/dd-trace/src/azure_metadata.js +2 -1
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +41 -3
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -2
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +1 -1
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
- package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
- package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +168 -8
- package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +16 -0
- package/packages/dd-trace/src/config/helper.js +1 -0
- package/packages/dd-trace/src/config/index.js +10 -0
- package/packages/dd-trace/src/config/supported-configurations.json +66 -0
- package/packages/dd-trace/src/exporter.js +2 -0
- package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
- package/packages/dd-trace/src/exporters/common/request.js +59 -30
- package/packages/dd-trace/src/exporters/common/url.js +15 -1
- package/packages/dd-trace/src/feature-registry.js +10 -3
- package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
- package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
- package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
- package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
- package/packages/dd-trace/src/llmobs/noop.js +6 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
- package/packages/dd-trace/src/llmobs/sdk.js +17 -0
- package/packages/dd-trace/src/llmobs/tagger.js +68 -0
- package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
- package/packages/dd-trace/src/llmobs/util.js +32 -0
- package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
- package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
- package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
- package/packages/dd-trace/src/openfeature/index.js +0 -2
- package/packages/dd-trace/src/openfeature/noop.js +1 -28
- package/packages/dd-trace/src/openfeature/register.js +16 -27
- package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
- package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
- package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
- package/packages/dd-trace/src/opentelemetry/span.js +5 -0
- package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
- package/packages/dd-trace/src/plugins/util/git.js +3 -2
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
- package/packages/dd-trace/src/plugins/util/test.js +56 -4
- package/packages/dd-trace/src/proxy.js +84 -7
- package/packages/dd-trace/src/span_processor.js +5 -0
- package/packages/dd-trace/src/standalone/index.js +0 -14
- package/packages/dd-trace/src/telemetry/send-data.js +2 -2
- package/packages/dd-trace/src/util.js +26 -0
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/vendor/dist/shell-quote/index.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
MAX_GENERATED_FILES,
|
|
8
|
+
getGeneratedFileContentError,
|
|
9
|
+
} = require('./generated-file-policy')
|
|
10
|
+
const { createFileSafely, ensureSafeDirectory } = require('./safe-files')
|
|
11
|
+
|
|
12
|
+
const RUNTIME_FILE_NAMESPACE = 'dd-test-optimization-validation'
|
|
13
|
+
const createdGeneratedDirectories = new Map()
|
|
14
|
+
const initializedCleanupStrategies = new WeakSet()
|
|
15
|
+
const authorizedRuntimeCleanupFiles = new Map()
|
|
16
|
+
const writtenGeneratedFiles = new Map()
|
|
17
|
+
|
|
18
|
+
function writeGeneratedFiles (framework) {
|
|
19
|
+
const strategy = framework.generatedTestStrategy
|
|
20
|
+
if (!strategy || !['planned', 'verified'].includes(strategy.status)) {
|
|
21
|
+
return []
|
|
22
|
+
}
|
|
23
|
+
if ((strategy.files || []).length > MAX_GENERATED_FILES) {
|
|
24
|
+
throw new Error(`Generated validation strategy must contain at most ${MAX_GENERATED_FILES} files.`)
|
|
25
|
+
}
|
|
26
|
+
initializeRuntimeCleanupFiles(framework, strategy)
|
|
27
|
+
|
|
28
|
+
const written = []
|
|
29
|
+
try {
|
|
30
|
+
for (const file of strategy.files || []) {
|
|
31
|
+
const filename = validateGeneratedFilePath(framework, file.path)
|
|
32
|
+
validateContentLines(file.contentLines, filename)
|
|
33
|
+
const content = `${file.contentLines.join('\n')}\n`
|
|
34
|
+
if (fs.existsSync(filename)) {
|
|
35
|
+
if (fs.readFileSync(filename, 'utf8') === content) continue
|
|
36
|
+
throw new Error(`Refusing to overwrite existing generated validation file with different content: ${filename}`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const directory = path.dirname(filename)
|
|
40
|
+
const missingDirectories = getMissingDirectories(framework.project.root, directory)
|
|
41
|
+
ensureSafeDirectory(framework.project.root, directory, 'generated validation file directory', {
|
|
42
|
+
allowRootSymlink: true,
|
|
43
|
+
})
|
|
44
|
+
for (const createdDirectory of missingDirectories) {
|
|
45
|
+
createdGeneratedDirectories.set(
|
|
46
|
+
createdDirectory,
|
|
47
|
+
authorizePathForCleanup(framework.project.root, createdDirectory)
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
validateGeneratedFilePath(framework, filename)
|
|
51
|
+
createFileSafely(framework.project.root, filename, content, 'generated validation file')
|
|
52
|
+
writtenGeneratedFiles.set(filename, authorizePathForCleanup(framework.project.root, filename))
|
|
53
|
+
written.push(filename)
|
|
54
|
+
}
|
|
55
|
+
pinRuntimeCleanupParents(strategy)
|
|
56
|
+
} catch (err) {
|
|
57
|
+
cleanupPaths(written)
|
|
58
|
+
cleanupCreatedDirectories(framework.project.root)
|
|
59
|
+
forgetWrittenGeneratedFiles(written)
|
|
60
|
+
throw err
|
|
61
|
+
}
|
|
62
|
+
return written
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function cleanupGeneratedFiles (manifest, { keep = false } = {}) {
|
|
66
|
+
if (keep) return
|
|
67
|
+
|
|
68
|
+
for (const framework of manifest.frameworks || []) {
|
|
69
|
+
const strategy = framework.generatedTestStrategy
|
|
70
|
+
cleanupPaths(getSafeCleanupPaths(framework, strategy, { includeGeneratedFiles: true }))
|
|
71
|
+
cleanupCreatedDirectories(framework.project.root)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Finds missing directories that the validator will create for one generated file.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} root project root
|
|
79
|
+
* @param {string} directory generated file directory
|
|
80
|
+
* @returns {string[]} missing directories, from deepest to shallowest
|
|
81
|
+
*/
|
|
82
|
+
function getMissingDirectories (root, directory) {
|
|
83
|
+
const missing = []
|
|
84
|
+
let current = path.resolve(directory)
|
|
85
|
+
const resolvedRoot = path.resolve(root)
|
|
86
|
+
while (current !== resolvedRoot && isPathInside(resolvedRoot, current) && !fs.existsSync(current)) {
|
|
87
|
+
missing.push(current)
|
|
88
|
+
current = path.dirname(current)
|
|
89
|
+
}
|
|
90
|
+
return missing
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Removes empty generated directories created by this validator process.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} root project root
|
|
97
|
+
*/
|
|
98
|
+
function cleanupCreatedDirectories (root) {
|
|
99
|
+
const resolvedRoot = path.resolve(root)
|
|
100
|
+
const directories = [...createdGeneratedDirectories.entries()]
|
|
101
|
+
.filter(([directory, authorization]) => {
|
|
102
|
+
return isPathInside(resolvedRoot, directory) && isCleanupAuthorizationValid(directory, authorization)
|
|
103
|
+
})
|
|
104
|
+
.sort(([left], [right]) => right.length - left.length)
|
|
105
|
+
|
|
106
|
+
for (const [directory] of directories) {
|
|
107
|
+
try {
|
|
108
|
+
fs.rmdirSync(directory)
|
|
109
|
+
createdGeneratedDirectories.delete(directory)
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error.code === 'ENOENT') createdGeneratedDirectories.delete(directory)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function cleanupGeneratedRuntimeFiles (framework) {
|
|
117
|
+
const strategy = framework.generatedTestStrategy
|
|
118
|
+
if (!strategy) return
|
|
119
|
+
|
|
120
|
+
initializeRuntimeCleanupFiles(framework, strategy)
|
|
121
|
+
cleanupPaths(getSafeCleanupPaths(framework, strategy, { includeGeneratedFiles: false }))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function initializeRuntimeCleanupFiles (framework, strategy) {
|
|
125
|
+
if (initializedCleanupStrategies.has(strategy)) return
|
|
126
|
+
|
|
127
|
+
const generatedFiles = new Set((strategy.files || []).map(file => validateGeneratedFilePath(framework, file.path)))
|
|
128
|
+
for (const cleanupPath of strategy.cleanupPaths || []) {
|
|
129
|
+
const filename = validateCleanupPath(framework, cleanupPath)
|
|
130
|
+
if (generatedFiles.has(filename) || isDirectory(filename) || !isNamespacedRuntimeFile(filename)) continue
|
|
131
|
+
if (fs.existsSync(filename)) {
|
|
132
|
+
throw new Error(`Refusing to delete pre-existing generated validation runtime file: ${filename}`)
|
|
133
|
+
}
|
|
134
|
+
authorizedRuntimeCleanupFiles.set(filename, authorizePathForCleanup(framework.project.root, filename))
|
|
135
|
+
}
|
|
136
|
+
initializedCleanupStrategies.add(strategy)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getSafeCleanupPaths (framework, strategy, { includeGeneratedFiles }) {
|
|
140
|
+
if (!strategy) return []
|
|
141
|
+
|
|
142
|
+
const generatedFiles = new Set()
|
|
143
|
+
for (const file of strategy.files || []) {
|
|
144
|
+
generatedFiles.add(validateGeneratedFilePath(framework, file.path))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const cleanupPaths = []
|
|
148
|
+
for (const cleanupPath of strategy.cleanupPaths || []) {
|
|
149
|
+
const filename = validateCleanupPath(framework, cleanupPath)
|
|
150
|
+
if (generatedFiles.has(filename)) {
|
|
151
|
+
if (includeGeneratedFiles && writtenGeneratedFiles.has(filename)) cleanupPaths.push(filename)
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (authorizedRuntimeCleanupFiles.has(filename)) {
|
|
156
|
+
cleanupPaths.push(filename)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (includeGeneratedFiles) {
|
|
161
|
+
for (const filename of generatedFiles) {
|
|
162
|
+
if (writtenGeneratedFiles.has(filename)) cleanupPaths.push(filename)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return [...new Set(cleanupPaths)]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function cleanupPaths (cleanupPaths) {
|
|
169
|
+
for (const cleanupPath of cleanupPaths) {
|
|
170
|
+
try {
|
|
171
|
+
const authorization = writtenGeneratedFiles.get(cleanupPath) ||
|
|
172
|
+
authorizedRuntimeCleanupFiles.get(cleanupPath)
|
|
173
|
+
if (!authorization || !isCleanupAuthorizationValid(cleanupPath, authorization)) continue
|
|
174
|
+
if (isDirectory(cleanupPath)) continue
|
|
175
|
+
fs.rmSync(cleanupPath, { force: true })
|
|
176
|
+
writtenGeneratedFiles.delete(cleanupPath)
|
|
177
|
+
} catch {
|
|
178
|
+
// Cleanup should be best-effort. The report will contain the command artifacts.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function forgetWrittenGeneratedFiles (filenames) {
|
|
184
|
+
for (const filename of filenames) {
|
|
185
|
+
writtenGeneratedFiles.delete(filename)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function authorizePathForCleanup (root, filename) {
|
|
190
|
+
const lexicalRoot = path.resolve(root)
|
|
191
|
+
const physicalRoot = fs.realpathSync(lexicalRoot)
|
|
192
|
+
const rootStat = fs.statSync(physicalRoot)
|
|
193
|
+
const authorization = {
|
|
194
|
+
lexicalRoot,
|
|
195
|
+
physicalRoot,
|
|
196
|
+
rootDevice: rootStat.dev,
|
|
197
|
+
rootInode: rootStat.ino,
|
|
198
|
+
}
|
|
199
|
+
pinCleanupParent(authorization, filename)
|
|
200
|
+
pinCleanupTarget(authorization, filename)
|
|
201
|
+
return authorization
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function pinRuntimeCleanupParents (strategy) {
|
|
205
|
+
for (const cleanupPath of strategy.cleanupPaths || []) {
|
|
206
|
+
const authorization = authorizedRuntimeCleanupFiles.get(path.resolve(cleanupPath))
|
|
207
|
+
if (authorization && authorization.physicalParent === undefined) {
|
|
208
|
+
pinCleanupParent(authorization, cleanupPath)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function pinCleanupParent (authorization, filename) {
|
|
214
|
+
try {
|
|
215
|
+
const physicalParent = fs.realpathSync(path.dirname(filename))
|
|
216
|
+
if (!isPathInside(authorization.physicalRoot, physicalParent)) return
|
|
217
|
+
const parentStat = fs.statSync(physicalParent)
|
|
218
|
+
authorization.physicalParent = physicalParent
|
|
219
|
+
authorization.parentDevice = parentStat.dev
|
|
220
|
+
authorization.parentInode = parentStat.ino
|
|
221
|
+
} catch {}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function pinCleanupTarget (authorization, filename) {
|
|
225
|
+
try {
|
|
226
|
+
const targetStat = fs.lstatSync(filename)
|
|
227
|
+
authorization.targetDevice = targetStat.dev
|
|
228
|
+
authorization.targetInode = targetStat.ino
|
|
229
|
+
} catch {}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isCleanupAuthorizationValid (filename, authorization) {
|
|
233
|
+
try {
|
|
234
|
+
const currentPhysicalRoot = fs.realpathSync(authorization.lexicalRoot)
|
|
235
|
+
const rootStat = fs.statSync(currentPhysicalRoot)
|
|
236
|
+
if (currentPhysicalRoot !== authorization.physicalRoot ||
|
|
237
|
+
rootStat.dev !== authorization.rootDevice || rootStat.ino !== authorization.rootInode) {
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (authorization.physicalParent === undefined) return false
|
|
242
|
+
const physicalParent = fs.realpathSync(path.dirname(filename))
|
|
243
|
+
const parentStat = fs.statSync(physicalParent)
|
|
244
|
+
if (physicalParent !== authorization.physicalParent ||
|
|
245
|
+
parentStat.dev !== authorization.parentDevice || parentStat.ino !== authorization.parentInode) {
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (authorization.targetDevice !== undefined) {
|
|
250
|
+
const targetStat = fs.lstatSync(filename)
|
|
251
|
+
if (targetStat.dev !== authorization.targetDevice || targetStat.ino !== authorization.targetInode) {
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return true
|
|
256
|
+
} catch {
|
|
257
|
+
return false
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function validateGeneratedFilePath (framework, filename) {
|
|
262
|
+
return validatePathUnderProjectRoot(framework, filename, 'generated validation file')
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function validateCleanupPath (framework, filename) {
|
|
266
|
+
return validatePathUnderProjectRoot(framework, filename, 'generated validation cleanup')
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function validatePathUnderProjectRoot (framework, filename, label) {
|
|
270
|
+
const root = getProjectRoot(framework)
|
|
271
|
+
const resolved = path.resolve(filename || '')
|
|
272
|
+
if (!root || !isPathInside(root, resolved)) {
|
|
273
|
+
throw new Error(`Refusing ${label} path outside project root: ${filename}`)
|
|
274
|
+
}
|
|
275
|
+
validatePhysicalPath(root, resolved, label)
|
|
276
|
+
return resolved
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Verifies that an existing parent resolves inside the physical project root.
|
|
281
|
+
*
|
|
282
|
+
* @param {string} root project root
|
|
283
|
+
* @param {string} filename candidate filename
|
|
284
|
+
* @param {string} label customer-facing path label
|
|
285
|
+
*/
|
|
286
|
+
function validatePhysicalPath (root, filename, label) {
|
|
287
|
+
const parent = path.dirname(filename)
|
|
288
|
+
let physicalRoot
|
|
289
|
+
let physicalParent
|
|
290
|
+
try {
|
|
291
|
+
physicalRoot = fs.realpathSync(root)
|
|
292
|
+
physicalParent = fs.realpathSync(parent)
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (error.code === 'ENOENT') return
|
|
295
|
+
throw error
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!isPathInside(physicalRoot, physicalParent)) {
|
|
299
|
+
throw new Error(`Refusing ${label} path outside physical project root: ${filename}`)
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
if (fs.lstatSync(filename).isSymbolicLink()) {
|
|
303
|
+
throw new Error(`Refusing ${label} symbolic-link target: ${filename}`)
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (error.code !== 'ENOENT') throw error
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function getProjectRoot (framework) {
|
|
311
|
+
const root = framework.project?.root
|
|
312
|
+
return typeof root === 'string' && path.isAbsolute(root) ? path.resolve(root) : null
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function isPathInside (root, filename) {
|
|
316
|
+
const relative = path.relative(root, filename)
|
|
317
|
+
return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative))
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function validateContentLines (contentLines, filename) {
|
|
321
|
+
if (!Array.isArray(contentLines) || contentLines.some(line => typeof line !== 'string')) {
|
|
322
|
+
throw new Error(`Generated validation file contentLines must be an array of strings: ${filename}`)
|
|
323
|
+
}
|
|
324
|
+
const policyError = getGeneratedFileContentError(contentLines)
|
|
325
|
+
if (policyError) {
|
|
326
|
+
throw new Error(`Generated validation file ${policyError}: ${filename}`)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function isNamespacedRuntimeFile (filename) {
|
|
331
|
+
return path.basename(filename).includes(RUNTIME_FILE_NAMESPACE)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function isDirectory (filename) {
|
|
335
|
+
try {
|
|
336
|
+
return fs.statSync(filename).isDirectory()
|
|
337
|
+
} catch {
|
|
338
|
+
return false
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function findGeneratedScenario (framework, scenarioId) {
|
|
343
|
+
return (framework.generatedTestStrategy?.scenarios || []).find(scenario => scenario.id === scenarioId)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
module.exports = {
|
|
347
|
+
cleanupGeneratedFiles,
|
|
348
|
+
cleanupGeneratedRuntimeFiles,
|
|
349
|
+
findGeneratedScenario,
|
|
350
|
+
writeGeneratedFiles,
|
|
351
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
|
|
6
|
+
const { runCommand, serializeDisplayCommand } = require('./command-runner')
|
|
7
|
+
const {
|
|
8
|
+
cleanupGeneratedRuntimeFiles,
|
|
9
|
+
writeGeneratedFiles,
|
|
10
|
+
} = require('./generated-files')
|
|
11
|
+
const {
|
|
12
|
+
getDatadogCleanCommand,
|
|
13
|
+
getLocalValidationCommand,
|
|
14
|
+
} = require('./local-command')
|
|
15
|
+
const { frameworkOutDir } = require('./scenarios/helpers')
|
|
16
|
+
const { getObservedTestCount } = require('./test-output')
|
|
17
|
+
|
|
18
|
+
const GENERATED_SCENARIO_BY_FEATURE = {
|
|
19
|
+
efd: 'basic-pass',
|
|
20
|
+
atr: 'atr-fail-once',
|
|
21
|
+
'test-management': 'test-management-target',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Verifies generated scenario commands without Datadog initialization.
|
|
26
|
+
*
|
|
27
|
+
* @param {object} input verification inputs
|
|
28
|
+
* @param {object} input.framework manifest framework entry
|
|
29
|
+
* @param {string} input.out validation output directory
|
|
30
|
+
* @param {object} input.options validator options
|
|
31
|
+
* @returns {Promise<{ok: boolean, failure?: object}>} verification outcome
|
|
32
|
+
*/
|
|
33
|
+
async function verifyGeneratedTestStrategy ({ framework, out, options }) {
|
|
34
|
+
const strategy = framework.generatedTestStrategy
|
|
35
|
+
if (!strategy || !['planned', 'verified'].includes(strategy.status)) return { ok: true }
|
|
36
|
+
|
|
37
|
+
const evidence = {
|
|
38
|
+
scenarios: [],
|
|
39
|
+
}
|
|
40
|
+
const artifacts = []
|
|
41
|
+
const startedAt = Date.now()
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
cleanupGeneratedRuntimeFiles(framework)
|
|
45
|
+
writeGeneratedFiles(framework)
|
|
46
|
+
|
|
47
|
+
for (const scenario of getScenariosToVerify(strategy.scenarios, options.scenarios)) {
|
|
48
|
+
cleanupGeneratedRuntimeFiles(framework)
|
|
49
|
+
const command = getDatadogCleanCommand(getLocalValidationCommand(framework, scenario.runCommand))
|
|
50
|
+
const outDir = frameworkOutDir(out, framework, `generated-verification-${scenario.id}`)
|
|
51
|
+
// Generated commands run serially because fail-once state and cleanup are scenario-local.
|
|
52
|
+
// eslint-disable-next-line no-await-in-loop
|
|
53
|
+
const result = await runCommand(command, {
|
|
54
|
+
artifactRoot: out,
|
|
55
|
+
envMode: 'clean',
|
|
56
|
+
label: `${framework.id}:generated-verification:${scenario.id}`,
|
|
57
|
+
outDir,
|
|
58
|
+
repositoryRoot: options.repositoryRoot,
|
|
59
|
+
requireExecutableApproval: options.requireExecutableApproval,
|
|
60
|
+
verbose: options.verbose,
|
|
61
|
+
})
|
|
62
|
+
const observedTestCount = getObservedTestCount(framework.framework, result.stdout, result.stderr)
|
|
63
|
+
const expected = scenario.expectedWithoutDatadog
|
|
64
|
+
const failOnceStateCreated = scenario.id === 'atr-fail-once'
|
|
65
|
+
? hasGeneratedRuntimeFile(strategy)
|
|
66
|
+
: undefined
|
|
67
|
+
const scenarioEvidence = {
|
|
68
|
+
id: scenario.id,
|
|
69
|
+
command: serializeDisplayCommand(command),
|
|
70
|
+
exitCode: result.exitCode,
|
|
71
|
+
expectedExitCode: expected.exitCode,
|
|
72
|
+
observedTestCount,
|
|
73
|
+
expectedTestCount: expected.observedTestCount,
|
|
74
|
+
localAdjustments: command.localAdjustments || [],
|
|
75
|
+
}
|
|
76
|
+
if (failOnceStateCreated !== undefined) scenarioEvidence.failOnceStateCreated = failOnceStateCreated
|
|
77
|
+
evidence.scenarios.push(scenarioEvidence)
|
|
78
|
+
artifacts.push(...Object.values(result.artifacts))
|
|
79
|
+
|
|
80
|
+
if (result.timedOut || result.exitCode !== expected.exitCode ||
|
|
81
|
+
observedTestCount !== expected.observedTestCount || failOnceStateCreated === false) {
|
|
82
|
+
cleanupGeneratedRuntimeFiles(framework)
|
|
83
|
+
return getVerificationFailure(framework, evidence, artifacts, scenarioEvidence, result.timedOut)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
cleanupGeneratedRuntimeFiles(framework)
|
|
88
|
+
strategy.status = 'verified'
|
|
89
|
+
strategy.verification = {
|
|
90
|
+
source: 'validator',
|
|
91
|
+
ran: true,
|
|
92
|
+
durationMs: Date.now() - startedAt,
|
|
93
|
+
observedScenarios: evidence.scenarios,
|
|
94
|
+
cleanupCompleted: true,
|
|
95
|
+
}
|
|
96
|
+
return { ok: true }
|
|
97
|
+
} catch (error) {
|
|
98
|
+
cleanupGeneratedRuntimeFiles(framework)
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
failure: {
|
|
102
|
+
frameworkId: framework.id,
|
|
103
|
+
scenario: 'generated-test-verification',
|
|
104
|
+
status: 'error',
|
|
105
|
+
diagnosis: 'The validator could not run the temporary validation test as expected. No advanced-feature ' +
|
|
106
|
+
`conclusion was reached: ${error.message || error}`,
|
|
107
|
+
evidence,
|
|
108
|
+
artifacts,
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Selects only generated tests required by the requested advanced checks.
|
|
116
|
+
*
|
|
117
|
+
* @param {object[]} scenarios generated test scenarios
|
|
118
|
+
* @param {Set<string>} [selectedFeatures] validator scenario selection
|
|
119
|
+
* @returns {object[]} generated scenarios to verify
|
|
120
|
+
*/
|
|
121
|
+
function getScenariosToVerify (scenarios, selectedFeatures) {
|
|
122
|
+
if (!(selectedFeatures instanceof Set)) return scenarios
|
|
123
|
+
|
|
124
|
+
const selectedScenarioIds = new Set()
|
|
125
|
+
for (const feature of selectedFeatures) {
|
|
126
|
+
const scenarioId = GENERATED_SCENARIO_BY_FEATURE[feature]
|
|
127
|
+
if (scenarioId) selectedScenarioIds.add(scenarioId)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (selectedScenarioIds.size === 0) return scenarios
|
|
131
|
+
return scenarios.filter(scenario => selectedScenarioIds.has(scenario.id))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Builds a failure when a generated scenario does not behave as declared.
|
|
136
|
+
*
|
|
137
|
+
* @param {object} framework manifest framework entry
|
|
138
|
+
* @param {object} evidence collected verification evidence
|
|
139
|
+
* @param {string[]} artifacts command artifacts
|
|
140
|
+
* @param {object} scenario failed scenario evidence
|
|
141
|
+
* @param {boolean} timedOut whether the scenario timed out
|
|
142
|
+
* @returns {{ok: false, failure: object}} generated verification failure
|
|
143
|
+
*/
|
|
144
|
+
function getVerificationFailure (framework, evidence, artifacts, scenario, timedOut) {
|
|
145
|
+
let reason
|
|
146
|
+
if (timedOut) {
|
|
147
|
+
reason = 'timed out'
|
|
148
|
+
} else if (scenario.id === 'atr-fail-once' && scenario.failOnceStateCreated === false) {
|
|
149
|
+
reason = 'failed without creating its declared fail-once state file, so it failed for an unrelated reason'
|
|
150
|
+
} else {
|
|
151
|
+
reason = `exited ${scenario.exitCode} with ${formatObservedCount(scenario.observedTestCount)}; expected exit ` +
|
|
152
|
+
`${scenario.expectedExitCode} and ${scenario.expectedTestCount} test`
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
failure: {
|
|
157
|
+
frameworkId: framework.id,
|
|
158
|
+
scenario: 'generated-test-verification',
|
|
159
|
+
status: 'error',
|
|
160
|
+
diagnosis: `Temporary validation test "${scenario.id}" ${reason}. No advanced-feature conclusion was reached.`,
|
|
161
|
+
evidence,
|
|
162
|
+
artifacts,
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Checks whether the generated fail-once scenario created a declared runtime state file.
|
|
169
|
+
*
|
|
170
|
+
* @param {object} strategy generated test strategy
|
|
171
|
+
* @returns {boolean} whether a regular declared runtime file exists
|
|
172
|
+
*/
|
|
173
|
+
function hasGeneratedRuntimeFile (strategy) {
|
|
174
|
+
const generatedFiles = new Set((strategy.files || []).map(file => path.resolve(file.path)))
|
|
175
|
+
for (const cleanupPath of strategy.cleanupPaths || []) {
|
|
176
|
+
const filename = path.resolve(cleanupPath)
|
|
177
|
+
if (generatedFiles.has(filename)) continue
|
|
178
|
+
try {
|
|
179
|
+
const stat = fs.lstatSync(filename)
|
|
180
|
+
if (!stat.isSymbolicLink() && stat.isFile()) return true
|
|
181
|
+
} catch {}
|
|
182
|
+
}
|
|
183
|
+
return false
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Formats a parsed test count for a customer-facing diagnosis.
|
|
188
|
+
*
|
|
189
|
+
* @param {number|null} count observed test count
|
|
190
|
+
* @returns {string} formatted count
|
|
191
|
+
*/
|
|
192
|
+
function formatObservedCount (count) {
|
|
193
|
+
return count === null ? 'an unknown test count' : `${count} observed tests`
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = { verifyGeneratedTestStrategy }
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/* eslint-disable eslint-rules/eslint-process-env */
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs')
|
|
6
|
+
const Module = require('node:module')
|
|
7
|
+
const path = require('node:path')
|
|
8
|
+
|
|
9
|
+
const { sanitizeForReport } = require('./redaction')
|
|
10
|
+
|
|
11
|
+
const PROBE_FILE_ENV = 'DD_TEST_OPTIMIZATION_INIT_PROBE_FILE'
|
|
12
|
+
const probeFile = process.env[PROBE_FILE_ENV]
|
|
13
|
+
const seenModuleLoads = new Set()
|
|
14
|
+
|
|
15
|
+
if (probeFile) {
|
|
16
|
+
writeRecord('process-start', {
|
|
17
|
+
argv: process.argv,
|
|
18
|
+
cwd: process.cwd(),
|
|
19
|
+
detectedTools: detectTools(process.argv),
|
|
20
|
+
execArgv: process.execArgv,
|
|
21
|
+
nodeOptionsPresent: Boolean(process.env.NODE_OPTIONS),
|
|
22
|
+
pid: process.pid,
|
|
23
|
+
ppid: process.ppid,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const originalLoad = Module._load
|
|
27
|
+
Module._load = function loadWithProbe (request, parent, isMain) {
|
|
28
|
+
const loaded = originalLoad.apply(this, arguments)
|
|
29
|
+
const tool = detectTool(request) || detectTool(resolveFromParent(request, parent))
|
|
30
|
+
|
|
31
|
+
if (tool) {
|
|
32
|
+
const key = `${process.pid}:${request}:${tool.name}`
|
|
33
|
+
if (!seenModuleLoads.has(key)) {
|
|
34
|
+
seenModuleLoads.add(key)
|
|
35
|
+
writeRecord('module-load', {
|
|
36
|
+
argv: process.argv,
|
|
37
|
+
cwd: process.cwd(),
|
|
38
|
+
isMain: Boolean(isMain),
|
|
39
|
+
parentFilename: parent && parent.filename,
|
|
40
|
+
pid: process.pid,
|
|
41
|
+
ppid: process.ppid,
|
|
42
|
+
request,
|
|
43
|
+
tool,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return loaded
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveFromParent (request, parent) {
|
|
53
|
+
try {
|
|
54
|
+
return Module._resolveFilename(request, parent)
|
|
55
|
+
} catch {
|
|
56
|
+
return ''
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function detectTools (values) {
|
|
61
|
+
const tools = []
|
|
62
|
+
const seen = new Set()
|
|
63
|
+
|
|
64
|
+
for (const value of values) {
|
|
65
|
+
const tool = detectTool(value)
|
|
66
|
+
if (!tool || seen.has(tool.name)) continue
|
|
67
|
+
seen.add(tool.name)
|
|
68
|
+
tools.push(tool)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return tools
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function detectTool (value) {
|
|
75
|
+
if (typeof value !== 'string' || value.length === 0) return null
|
|
76
|
+
|
|
77
|
+
const normalized = value.split(path.sep).join('/').toLowerCase()
|
|
78
|
+
|
|
79
|
+
if (/(^|\/)(jest|jest\.js)$/.test(normalized) ||
|
|
80
|
+
/\/(?:jest|jest-cli|@jest\/core)\//.test(normalized) ||
|
|
81
|
+
normalized === 'jest' ||
|
|
82
|
+
normalized === 'jest-cli' ||
|
|
83
|
+
normalized === '@jest/core') {
|
|
84
|
+
return { name: 'jest', kind: 'test-runner' }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (/(^|\/)(vitest|vitest\.mjs)$/.test(normalized) ||
|
|
88
|
+
/\/(?:vitest|@vitest\/runner)\//.test(normalized) ||
|
|
89
|
+
normalized === 'vitest' ||
|
|
90
|
+
normalized === '@vitest/runner') {
|
|
91
|
+
return { name: 'vitest', kind: 'test-runner' }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (/(^|\/)(mocha|mocha\.js|_mocha)$/.test(normalized) ||
|
|
95
|
+
/\/mocha\//.test(normalized) ||
|
|
96
|
+
normalized === 'mocha') {
|
|
97
|
+
return { name: 'mocha', kind: 'test-runner' }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (/\/@cucumber\/cucumber\//.test(normalized) ||
|
|
101
|
+
normalized === '@cucumber/cucumber' ||
|
|
102
|
+
normalized === 'cucumber' ||
|
|
103
|
+
normalized === 'cucumber-js') {
|
|
104
|
+
return { name: 'cucumber', kind: 'test-runner' }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (/(^|\/)playwright(?:\.js)?$/.test(normalized) ||
|
|
108
|
+
/\/(?:@playwright\/test|playwright)\//.test(normalized) ||
|
|
109
|
+
normalized === '@playwright/test' ||
|
|
110
|
+
normalized === 'playwright' ||
|
|
111
|
+
normalized === 'playwright/test') {
|
|
112
|
+
return { name: 'playwright', kind: 'test-runner' }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (/(^|\/)cypress(?:\.js)?$/.test(normalized) || /\/cypress\//.test(normalized) || normalized === 'cypress') {
|
|
116
|
+
return { name: 'cypress', kind: 'test-runner' }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (/(^|\/)(nx|nx\.js)$/.test(normalized) || /\/nx\//.test(normalized) || normalized === 'nx') {
|
|
120
|
+
return { name: 'nx', kind: 'wrapper' }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (/(^|\/)(turbo|turbo\.js)$/.test(normalized) || /\/turbo\//.test(normalized) || normalized === 'turbo') {
|
|
124
|
+
return { name: 'turbo', kind: 'wrapper' }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (/(^|\/)(lage|lage\.js)$/.test(normalized) || /\/lage\//.test(normalized) || normalized === 'lage') {
|
|
128
|
+
return { name: 'lage', kind: 'wrapper' }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (/(^|\/)(pnpm|pnpm\.cjs)$/.test(normalized) || /\/pnpm\//.test(normalized) || normalized === 'pnpm') {
|
|
132
|
+
return { name: 'pnpm', kind: 'package-manager' }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (/(^|\/)(npm|npm-cli\.js)$/.test(normalized) || /\/npm\//.test(normalized) || normalized === 'npm') {
|
|
136
|
+
return { name: 'npm', kind: 'package-manager' }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (/(^|\/)(yarn|yarn\.js)$/.test(normalized) || /\/yarn\//.test(normalized) || normalized === 'yarn') {
|
|
140
|
+
return { name: 'yarn', kind: 'package-manager' }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function writeRecord (type, data) {
|
|
147
|
+
try {
|
|
148
|
+
const record = sanitizeForReport({
|
|
149
|
+
type,
|
|
150
|
+
time: new Date().toISOString(),
|
|
151
|
+
...data,
|
|
152
|
+
})
|
|
153
|
+
const flags = fs.constants.O_WRONLY | fs.constants.O_APPEND | (fs.constants.O_NOFOLLOW || 0)
|
|
154
|
+
const file = fs.openSync(probeFile, flags)
|
|
155
|
+
try {
|
|
156
|
+
fs.writeFileSync(file, `${JSON.stringify(record)}\n`)
|
|
157
|
+
} finally {
|
|
158
|
+
fs.closeSync(file)
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// The probe must never change test behavior.
|
|
162
|
+
}
|
|
163
|
+
}
|