sounding 0.0.4 → 0.1.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.md +334 -1
- package/bin/sounding.js +156 -0
- package/index.js +23 -0
- package/lib/create-app-manager.js +380 -26
- package/lib/create-auth-helpers.js +168 -21
- package/lib/create-browser-manager.js +578 -31
- package/lib/create-error.js +35 -0
- package/lib/create-expect.js +1070 -27
- package/lib/create-helper-runner.js +38 -2
- package/lib/create-mail-capture.js +125 -16
- package/lib/create-mailbox.js +20 -0
- package/lib/create-request-client.js +635 -57
- package/lib/create-runtime.js +222 -21
- package/lib/create-socket-manager.js +706 -0
- package/lib/create-test-api.js +491 -102
- package/lib/create-visit-client.js +40 -2
- package/lib/create-world-engine.js +106 -7
- package/lib/create-world-loader.js +150 -8
- package/lib/default-config.js +25 -0
- package/lib/define-world.js +27 -2
- package/lib/init-project.js +403 -0
- package/lib/merge-config.js +11 -0
- package/lib/normalize-config.js +16 -19
- package/lib/resolve-auth-config.js +36 -0
- package/lib/resolve-datastore.js +50 -7
- package/lib/resolve-dependency.js +145 -0
- package/lib/test-runner.js +427 -0
- package/lib/trial-context.js +29 -0
- package/lib/types.js +675 -0
- package/lib/validate-config.js +633 -0
- package/lib/validate-test-args.js +480 -0
- package/package.json +16 -2
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const path = require('node:path')
|
|
2
|
+
|
|
3
|
+
const { createSoundingError } = require('./create-error')
|
|
4
|
+
|
|
5
|
+
/** @typedef {import('./types').AnyRecord} AnyRecord */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {unknown} error
|
|
9
|
+
* @returns {error is Error & { code: string }}
|
|
10
|
+
*/
|
|
11
|
+
function isMissingModuleError(error) {
|
|
12
|
+
return Boolean(
|
|
13
|
+
error &&
|
|
14
|
+
typeof error === 'object' &&
|
|
15
|
+
'code' in error &&
|
|
16
|
+
error.code === 'MODULE_NOT_FOUND'
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {{
|
|
22
|
+
* moduleId: string,
|
|
23
|
+
* dependency?: string,
|
|
24
|
+
* purpose?: string,
|
|
25
|
+
* install?: string,
|
|
26
|
+
* suggestion?: string,
|
|
27
|
+
* appPath?: string,
|
|
28
|
+
* cause?: unknown,
|
|
29
|
+
* }} input
|
|
30
|
+
* @returns {Error}
|
|
31
|
+
*/
|
|
32
|
+
function createMissingDependencyError({
|
|
33
|
+
moduleId,
|
|
34
|
+
dependency = moduleId,
|
|
35
|
+
purpose,
|
|
36
|
+
install,
|
|
37
|
+
suggestion,
|
|
38
|
+
appPath,
|
|
39
|
+
cause,
|
|
40
|
+
}) {
|
|
41
|
+
const reason = purpose ? ` Sounding needs it to ${purpose}.` : ''
|
|
42
|
+
const installHint = install ? ` Install it with: \`${install}\`.` : ''
|
|
43
|
+
const suggestionHint = suggestion ? ` ${suggestion}` : ''
|
|
44
|
+
|
|
45
|
+
return createSoundingError({
|
|
46
|
+
code: 'E_SOUNDING_DEPENDENCY_MISSING',
|
|
47
|
+
name: 'SoundingDependencyError',
|
|
48
|
+
message: `Sounding could not find dependency \`${dependency}\`.${reason}${installHint}${suggestionHint}`,
|
|
49
|
+
details: {
|
|
50
|
+
moduleId,
|
|
51
|
+
dependency,
|
|
52
|
+
purpose,
|
|
53
|
+
install,
|
|
54
|
+
suggestion,
|
|
55
|
+
appPath,
|
|
56
|
+
},
|
|
57
|
+
cause,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {{
|
|
63
|
+
* appPath?: string,
|
|
64
|
+
* moduleId: string,
|
|
65
|
+
* dependency?: string,
|
|
66
|
+
* purpose?: string,
|
|
67
|
+
* install?: string,
|
|
68
|
+
* suggestion?: string,
|
|
69
|
+
* optional?: boolean,
|
|
70
|
+
* resolveImplementation?: (moduleId: string, options?: { paths?: string[] }) => string,
|
|
71
|
+
* paths?: string[],
|
|
72
|
+
* }} input
|
|
73
|
+
* @returns {string | null}
|
|
74
|
+
*/
|
|
75
|
+
function resolveDependencyFromApp({
|
|
76
|
+
appPath = process.cwd(),
|
|
77
|
+
moduleId,
|
|
78
|
+
dependency = moduleId,
|
|
79
|
+
purpose,
|
|
80
|
+
install,
|
|
81
|
+
suggestion,
|
|
82
|
+
optional = false,
|
|
83
|
+
resolveImplementation = require.resolve,
|
|
84
|
+
paths,
|
|
85
|
+
}) {
|
|
86
|
+
const resolvedAppPath = path.resolve(appPath)
|
|
87
|
+
const searchPaths = paths || [resolvedAppPath, process.cwd(), __dirname]
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
return resolveImplementation(moduleId, { paths: searchPaths })
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (optional && isMissingModuleError(error)) {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (isMissingModuleError(error)) {
|
|
97
|
+
throw createMissingDependencyError({
|
|
98
|
+
moduleId,
|
|
99
|
+
dependency,
|
|
100
|
+
purpose,
|
|
101
|
+
install,
|
|
102
|
+
suggestion,
|
|
103
|
+
appPath: resolvedAppPath,
|
|
104
|
+
cause: error,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
throw error
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {{
|
|
114
|
+
* appPath?: string,
|
|
115
|
+
* moduleId: string,
|
|
116
|
+
* dependency?: string,
|
|
117
|
+
* purpose?: string,
|
|
118
|
+
* install?: string,
|
|
119
|
+
* suggestion?: string,
|
|
120
|
+
* optional?: boolean,
|
|
121
|
+
* resolveImplementation?: (moduleId: string, options?: { paths?: string[] }) => string,
|
|
122
|
+
* requireImplementation?: (resolvedPath: string) => any,
|
|
123
|
+
* paths?: string[],
|
|
124
|
+
* }} input
|
|
125
|
+
* @returns {any}
|
|
126
|
+
*/
|
|
127
|
+
function loadDependencyFromApp({
|
|
128
|
+
requireImplementation = require,
|
|
129
|
+
...options
|
|
130
|
+
}) {
|
|
131
|
+
const resolvedPath = resolveDependencyFromApp(options)
|
|
132
|
+
|
|
133
|
+
if (!resolvedPath) {
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return requireImplementation(resolvedPath)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
createMissingDependencyError,
|
|
142
|
+
isMissingModuleError,
|
|
143
|
+
loadDependencyFromApp,
|
|
144
|
+
resolveDependencyFromApp,
|
|
145
|
+
}
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
const fs = require('node:fs')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
const { execFileSync, spawn } = require('node:child_process')
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TEST_DIRECTORIES = ['tests', 'test']
|
|
6
|
+
const DEFAULT_JUNIT_DESTINATION = 'reports/sounding-junit.xml'
|
|
7
|
+
const NODE_VALUE_FLAGS = new Set([
|
|
8
|
+
'--test-name-pattern',
|
|
9
|
+
'--test-reporter',
|
|
10
|
+
'--test-reporter-destination',
|
|
11
|
+
'--test-shard',
|
|
12
|
+
'--test-timeout',
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {{
|
|
17
|
+
* appPath?: string,
|
|
18
|
+
* argv?: string[],
|
|
19
|
+
* nodeExecutable?: string,
|
|
20
|
+
* }} BuildTestCommandOptions
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {{
|
|
25
|
+
* command: string,
|
|
26
|
+
* args: string[],
|
|
27
|
+
* cwd: string,
|
|
28
|
+
* files: string[],
|
|
29
|
+
* dryRun: boolean,
|
|
30
|
+
* }} SoundingTestCommand
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} filePath
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
function isTestFile(filePath) {
|
|
38
|
+
return /\.test\.(js|cjs|mjs)$/.test(filePath)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} directory
|
|
43
|
+
* @returns {string[]}
|
|
44
|
+
*/
|
|
45
|
+
function listTestFiles(directory) {
|
|
46
|
+
if (!fs.existsSync(directory)) {
|
|
47
|
+
return []
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const files = []
|
|
51
|
+
|
|
52
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
53
|
+
const nextPath = path.join(directory, entry.name)
|
|
54
|
+
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
files.push(...listTestFiles(nextPath))
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (entry.isFile() && isTestFile(nextPath)) {
|
|
65
|
+
files.push(nextPath)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return files.sort()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {string} value
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
function hasGlob(value) {
|
|
77
|
+
return /[*?[\]{}]/.test(value)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} value
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
function escapeRegExp(value) {
|
|
85
|
+
return value.replace(/[|\\{}()[\]^$+?.]/g, '\\$&')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {string} pattern
|
|
90
|
+
* @returns {RegExp}
|
|
91
|
+
*/
|
|
92
|
+
function globToRegExp(pattern) {
|
|
93
|
+
const normalized = pattern.split(path.sep).join('/')
|
|
94
|
+
let output = '^'
|
|
95
|
+
|
|
96
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
97
|
+
const char = normalized[index]
|
|
98
|
+
const nextChar = normalized[index + 1]
|
|
99
|
+
const followingChar = normalized[index + 2]
|
|
100
|
+
|
|
101
|
+
if (char === '*' && nextChar === '*') {
|
|
102
|
+
if (followingChar === '/') {
|
|
103
|
+
output += '(?:.*/)?'
|
|
104
|
+
index += 2
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
output += '.*'
|
|
109
|
+
index += 1
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (char === '*') {
|
|
114
|
+
output += '[^/]*'
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (char === '?') {
|
|
119
|
+
output += '[^/]'
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
output += escapeRegExp(char)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
output += '$'
|
|
127
|
+
return new RegExp(output)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {string} pattern
|
|
132
|
+
* @returns {string}
|
|
133
|
+
*/
|
|
134
|
+
function resolveGlobBase(pattern) {
|
|
135
|
+
const segments = pattern.split(/[\\/]/)
|
|
136
|
+
const baseSegments = []
|
|
137
|
+
|
|
138
|
+
for (const segment of segments) {
|
|
139
|
+
if (hasGlob(segment)) {
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
baseSegments.push(segment)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return baseSegments.length > 0 ? baseSegments.join(path.sep) : '.'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {string} appPath
|
|
151
|
+
* @param {string} target
|
|
152
|
+
* @returns {string[]}
|
|
153
|
+
*/
|
|
154
|
+
function resolveTargetFiles(appPath, target) {
|
|
155
|
+
const absoluteTarget = path.resolve(appPath, target)
|
|
156
|
+
|
|
157
|
+
if (hasGlob(target)) {
|
|
158
|
+
const relativeBase = resolveGlobBase(target)
|
|
159
|
+
const absoluteBase = path.resolve(appPath, relativeBase)
|
|
160
|
+
const matcher = globToRegExp(target.split(path.sep).join('/'))
|
|
161
|
+
|
|
162
|
+
return listTestFiles(absoluteBase).filter((filePath) => {
|
|
163
|
+
const relativePath = path.relative(appPath, filePath).split(path.sep).join('/')
|
|
164
|
+
return matcher.test(relativePath)
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!fs.existsSync(absoluteTarget)) {
|
|
169
|
+
return []
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const stat = fs.statSync(absoluteTarget)
|
|
173
|
+
|
|
174
|
+
if (stat.isDirectory()) {
|
|
175
|
+
return listTestFiles(absoluteTarget)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return isTestFile(absoluteTarget) ? [absoluteTarget] : []
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @param {string} appPath
|
|
183
|
+
* @returns {string[]}
|
|
184
|
+
*/
|
|
185
|
+
function getChangedTestFiles(appPath) {
|
|
186
|
+
try {
|
|
187
|
+
const output = execFileSync('git', ['-C', appPath, 'diff', '--name-only', 'HEAD'], {
|
|
188
|
+
encoding: 'utf8',
|
|
189
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
return output
|
|
193
|
+
.split(/\r?\n/)
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
.filter(isTestFile)
|
|
196
|
+
} catch (_error) {
|
|
197
|
+
return []
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @param {string} appPath
|
|
203
|
+
* @param {string[]} targets
|
|
204
|
+
* @returns {string[]}
|
|
205
|
+
*/
|
|
206
|
+
function resolveTestFiles(appPath, targets) {
|
|
207
|
+
const nextTargets =
|
|
208
|
+
targets.length > 0
|
|
209
|
+
? targets
|
|
210
|
+
: DEFAULT_TEST_DIRECTORIES.filter((directory) => fs.existsSync(path.join(appPath, directory)))
|
|
211
|
+
|
|
212
|
+
const files = new Set()
|
|
213
|
+
|
|
214
|
+
for (const target of nextTargets) {
|
|
215
|
+
for (const filePath of resolveTargetFiles(appPath, target)) {
|
|
216
|
+
files.add(path.relative(appPath, filePath))
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return Array.from(files).sort()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {string[]} argv
|
|
225
|
+
* @param {string} appPath
|
|
226
|
+
* @returns {{
|
|
227
|
+
* appPath: string,
|
|
228
|
+
* dryRun: boolean,
|
|
229
|
+
* targets: string[],
|
|
230
|
+
* nodeArgs: string[],
|
|
231
|
+
* }}
|
|
232
|
+
*/
|
|
233
|
+
function parseTestArgs(argv = [], appPath = process.cwd()) {
|
|
234
|
+
const args = [...argv]
|
|
235
|
+
const targets = []
|
|
236
|
+
const nodeArgs = []
|
|
237
|
+
let dryRun = false
|
|
238
|
+
let useChanged = false
|
|
239
|
+
let resolvedAppPath = path.resolve(appPath)
|
|
240
|
+
|
|
241
|
+
function readValue(flag) {
|
|
242
|
+
const value = args.shift()
|
|
243
|
+
|
|
244
|
+
if (!value || value.startsWith('--')) {
|
|
245
|
+
throw new Error(`Sounding test option \`${flag}\` requires a value.`)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return value
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
while (args.length > 0) {
|
|
252
|
+
const arg = args.shift()
|
|
253
|
+
|
|
254
|
+
if (arg === '--dry-run') {
|
|
255
|
+
dryRun = true
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (arg === '--app') {
|
|
260
|
+
resolvedAppPath = path.resolve(readValue(arg))
|
|
261
|
+
continue
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (arg === '--grep') {
|
|
265
|
+
nodeArgs.push('--test-name-pattern', readValue(arg))
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (arg === '--file') {
|
|
270
|
+
targets.push(readValue(arg))
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (arg === '--lane') {
|
|
275
|
+
const lane = readValue(arg)
|
|
276
|
+
targets.push(`tests/${lane}`)
|
|
277
|
+
targets.push(`test/${lane}`)
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (arg === '--changed') {
|
|
282
|
+
useChanged = true
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (arg === '--reporter') {
|
|
287
|
+
nodeArgs.push('--test-reporter', readValue(arg))
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (arg === '--reporter-destination') {
|
|
292
|
+
nodeArgs.push('--test-reporter-destination', readValue(arg))
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (arg === '--junit') {
|
|
297
|
+
nodeArgs.push('--test-reporter', 'junit')
|
|
298
|
+
|
|
299
|
+
if (args[0] && !args[0].startsWith('-')) {
|
|
300
|
+
nodeArgs.push('--test-reporter-destination', args.shift())
|
|
301
|
+
} else {
|
|
302
|
+
nodeArgs.push('--test-reporter-destination', DEFAULT_JUNIT_DESTINATION)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (arg === '--json') {
|
|
309
|
+
nodeArgs.push('--test-reporter', 'json')
|
|
310
|
+
continue
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (arg === '--coverage') {
|
|
314
|
+
nodeArgs.push('--experimental-test-coverage')
|
|
315
|
+
continue
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (arg === '--watch') {
|
|
319
|
+
nodeArgs.push('--watch')
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (arg === '--') {
|
|
324
|
+
nodeArgs.push(...args)
|
|
325
|
+
break
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (NODE_VALUE_FLAGS.has(arg)) {
|
|
329
|
+
nodeArgs.push(arg, readValue(arg))
|
|
330
|
+
continue
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (arg.startsWith('--')) {
|
|
334
|
+
nodeArgs.push(arg)
|
|
335
|
+
continue
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
targets.push(arg)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (useChanged) {
|
|
342
|
+
targets.push(...getChangedTestFiles(resolvedAppPath))
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
appPath: resolvedAppPath,
|
|
347
|
+
dryRun,
|
|
348
|
+
targets,
|
|
349
|
+
nodeArgs,
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* @param {BuildTestCommandOptions} [options]
|
|
355
|
+
* @returns {SoundingTestCommand}
|
|
356
|
+
*/
|
|
357
|
+
function buildTestCommand(options = {}) {
|
|
358
|
+
const parsed = parseTestArgs(options.argv || [], options.appPath)
|
|
359
|
+
const files = resolveTestFiles(parsed.appPath, parsed.targets)
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
command: options.nodeExecutable || process.execPath,
|
|
363
|
+
args: ['--test', ...parsed.nodeArgs, ...files],
|
|
364
|
+
cwd: parsed.appPath,
|
|
365
|
+
files,
|
|
366
|
+
dryRun: parsed.dryRun,
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* @param {string} value
|
|
372
|
+
* @returns {string}
|
|
373
|
+
*/
|
|
374
|
+
function quoteShell(value) {
|
|
375
|
+
if (/^[A-Za-z0-9_./:=@-]+$/.test(value)) {
|
|
376
|
+
return value
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return JSON.stringify(value)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* @param {SoundingTestCommand} command
|
|
384
|
+
* @returns {string}
|
|
385
|
+
*/
|
|
386
|
+
function formatTestCommand(command) {
|
|
387
|
+
return [command.command, ...command.args].map(quoteShell).join(' ')
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* @param {BuildTestCommandOptions & { stdio?: 'inherit' | 'pipe' }} [options]
|
|
392
|
+
* @returns {Promise<{ status: number, command: SoundingTestCommand }>}
|
|
393
|
+
*/
|
|
394
|
+
function runTests(options = {}) {
|
|
395
|
+
const command = buildTestCommand(options)
|
|
396
|
+
|
|
397
|
+
if (command.dryRun) {
|
|
398
|
+
return Promise.resolve({
|
|
399
|
+
status: 0,
|
|
400
|
+
command,
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return new Promise((resolve) => {
|
|
405
|
+
const child = spawn(command.command, command.args, {
|
|
406
|
+
cwd: command.cwd,
|
|
407
|
+
stdio: options.stdio || 'inherit',
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
child.on('exit', (code, signal) => {
|
|
411
|
+
resolve({
|
|
412
|
+
status: code ?? (signal ? 1 : 0),
|
|
413
|
+
command,
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
module.exports = {
|
|
420
|
+
DEFAULT_JUNIT_DESTINATION,
|
|
421
|
+
DEFAULT_TEST_DIRECTORIES,
|
|
422
|
+
buildTestCommand,
|
|
423
|
+
formatTestCommand,
|
|
424
|
+
parseTestArgs,
|
|
425
|
+
resolveTestFiles,
|
|
426
|
+
runTests,
|
|
427
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const { AsyncLocalStorage } = require('node:async_hooks')
|
|
2
|
+
|
|
3
|
+
/** @typedef {import('./types').SoundingMailbox} SoundingMailbox */
|
|
4
|
+
/** @typedef {import('./types').SoundingRuntime} SoundingRuntime */
|
|
5
|
+
|
|
6
|
+
/** @type {AsyncLocalStorage<{ runtime?: SoundingRuntime, mailbox?: SoundingMailbox, getConfig?: () => any }>} */
|
|
7
|
+
const trialContextStorage = new AsyncLocalStorage()
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @template T
|
|
11
|
+
* @param {{ runtime?: SoundingRuntime, mailbox?: SoundingMailbox, getConfig?: () => any }} context
|
|
12
|
+
* @param {() => T | Promise<T>} handler
|
|
13
|
+
* @returns {T | Promise<T>}
|
|
14
|
+
*/
|
|
15
|
+
function runWithTrialContext(context, handler) {
|
|
16
|
+
return trialContextStorage.run(context, handler)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @returns {{ runtime?: SoundingRuntime, mailbox?: SoundingMailbox, getConfig?: () => any } | null}
|
|
21
|
+
*/
|
|
22
|
+
function getTrialContext() {
|
|
23
|
+
return trialContextStorage.getStore() || null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
getTrialContext,
|
|
28
|
+
runWithTrialContext,
|
|
29
|
+
}
|