@jsenv/test 1.0.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/dist/controllable_child_process.mjs +129 -0
- package/dist/controllable_worker_thread.mjs +91 -0
- package/dist/importmap_node_loader.mjs +551 -0
- package/dist/js/execute_using_dynamic_import.js +850 -0
- package/dist/js/v8_coverage.js +508 -0
- package/dist/jsenv_test.js +5993 -0
- package/dist/no_experimental_warnings.cjs +10 -0
- package/package.json +56 -0
- package/src/coverage/babel_plugin_instrument.js +48 -0
- package/src/coverage/coverage_reporter_html_directory.js +32 -0
- package/src/coverage/coverage_reporter_json_file.js +17 -0
- package/src/coverage/coverage_reporter_text_log.js +19 -0
- package/src/coverage/empty_coverage_factory.js +52 -0
- package/src/coverage/file_by_file_coverage.js +25 -0
- package/src/coverage/istanbul_coverage_composition.js +28 -0
- package/src/coverage/istanbul_coverage_map_from_coverage.js +16 -0
- package/src/coverage/list_files_not_covered.js +15 -0
- package/src/coverage/missing_coverage.js +41 -0
- package/src/coverage/report_to_coverage.js +198 -0
- package/src/coverage/v8_and_istanbul.js +37 -0
- package/src/coverage/v8_coverage.js +26 -0
- package/src/coverage/v8_coverage_composition.js +24 -0
- package/src/coverage/v8_coverage_node_directory.js +85 -0
- package/src/coverage/v8_coverage_to_istanbul.js +99 -0
- package/src/execution/execute.js +111 -0
- package/src/execution/execute_steps.js +423 -0
- package/src/execution/execute_test_plan.js +372 -0
- package/src/execution/execution_colors.js +10 -0
- package/src/execution/execution_steps.js +65 -0
- package/src/execution/gc.js +9 -0
- package/src/execution/logs_file_execution.js +427 -0
- package/src/execution/logs_file_execution.test.mjs +41 -0
- package/src/execution/run.js +160 -0
- package/src/execution/web_server_param.js +73 -0
- package/src/main.js +10 -0
- package/src/runtime_browsers/chromium.js +24 -0
- package/src/runtime_browsers/firefox.js +23 -0
- package/src/runtime_browsers/middleware_istanbul.js +65 -0
- package/src/runtime_browsers/middleware_js_supervisor.js +98 -0
- package/src/runtime_browsers/using_playwright.js +573 -0
- package/src/runtime_browsers/webkit.js +40 -0
- package/src/runtime_node/child_exec_options.js +166 -0
- package/src/runtime_node/controllable_child_process.mjs +135 -0
- package/src/runtime_node/controllable_worker_thread.mjs +103 -0
- package/src/runtime_node/exec_options.js +57 -0
- package/src/runtime_node/execute_using_dynamic_import.js +53 -0
- package/src/runtime_node/exit_codes.js +9 -0
- package/src/runtime_node/importmap_node_loader.mjs +51 -0
- package/src/runtime_node/importmap_node_loader_file_url.js +4 -0
- package/src/runtime_node/kill_process_tree.js +76 -0
- package/src/runtime_node/no_experimental_warnings.cjs +13 -0
- package/src/runtime_node/no_experimental_warnings_file_url.js +4 -0
- package/src/runtime_node/node_child_process.js +374 -0
- package/src/runtime_node/node_execution_performance.js +67 -0
- package/src/runtime_node/node_worker_thread.js +301 -0
- package/src/runtime_node/profiler_v8_coverage.js +56 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import wrapAnsi from "wrap-ansi"
|
|
2
|
+
import {
|
|
3
|
+
ANSI,
|
|
4
|
+
UNICODE,
|
|
5
|
+
msAsEllapsedTime,
|
|
6
|
+
msAsDuration,
|
|
7
|
+
byteAsMemoryUsage,
|
|
8
|
+
} from "@jsenv/log"
|
|
9
|
+
|
|
10
|
+
import { EXECUTION_COLORS } from "./execution_colors.js"
|
|
11
|
+
|
|
12
|
+
export const createExecutionLog = (
|
|
13
|
+
{
|
|
14
|
+
executionIndex,
|
|
15
|
+
fileRelativeUrl,
|
|
16
|
+
runtimeName,
|
|
17
|
+
runtimeVersion,
|
|
18
|
+
executionParams,
|
|
19
|
+
executionResult,
|
|
20
|
+
startMs,
|
|
21
|
+
endMs,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
completedExecutionLogAbbreviation,
|
|
25
|
+
counters,
|
|
26
|
+
logRuntime,
|
|
27
|
+
logEachDuration,
|
|
28
|
+
timeEllapsed,
|
|
29
|
+
memoryHeap,
|
|
30
|
+
},
|
|
31
|
+
) => {
|
|
32
|
+
const { status } = executionResult
|
|
33
|
+
const descriptionFormatter = descriptionFormatters[status]
|
|
34
|
+
const description = descriptionFormatter({
|
|
35
|
+
index: executionIndex,
|
|
36
|
+
total: counters.total,
|
|
37
|
+
executionParams,
|
|
38
|
+
})
|
|
39
|
+
const summary = createIntermediateSummary({
|
|
40
|
+
executionIndex,
|
|
41
|
+
counters,
|
|
42
|
+
timeEllapsed,
|
|
43
|
+
memoryHeap,
|
|
44
|
+
})
|
|
45
|
+
let log
|
|
46
|
+
if (completedExecutionLogAbbreviation && status === "completed") {
|
|
47
|
+
log = `${description}${summary}`
|
|
48
|
+
} else {
|
|
49
|
+
const { consoleCalls = [], errors = [] } = executionResult
|
|
50
|
+
const consoleOutput = formatConsoleCalls(consoleCalls)
|
|
51
|
+
const errorsOutput = formatErrors(errors)
|
|
52
|
+
log = formatExecution({
|
|
53
|
+
label: `${description}${summary}`,
|
|
54
|
+
details: {
|
|
55
|
+
file: fileRelativeUrl,
|
|
56
|
+
...(logRuntime ? { runtime: `${runtimeName}/${runtimeVersion}` } : {}),
|
|
57
|
+
...(logEachDuration
|
|
58
|
+
? {
|
|
59
|
+
duration:
|
|
60
|
+
status === "executing"
|
|
61
|
+
? msAsEllapsedTime(Date.now() - startMs)
|
|
62
|
+
: msAsDuration(endMs - startMs),
|
|
63
|
+
}
|
|
64
|
+
: {}),
|
|
65
|
+
},
|
|
66
|
+
consoleOutput,
|
|
67
|
+
errorsOutput,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { columns = 80 } = process.stdout
|
|
72
|
+
log = wrapAnsi(log, columns, {
|
|
73
|
+
trim: false,
|
|
74
|
+
hard: true,
|
|
75
|
+
wordWrap: false,
|
|
76
|
+
})
|
|
77
|
+
if (endMs) {
|
|
78
|
+
if (completedExecutionLogAbbreviation) {
|
|
79
|
+
return `${log}\n`
|
|
80
|
+
}
|
|
81
|
+
if (executionIndex === counters.total - 1) {
|
|
82
|
+
return `${log}\n`
|
|
83
|
+
}
|
|
84
|
+
return `${log}\n\n`
|
|
85
|
+
}
|
|
86
|
+
return log
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const formatErrors = (errors) => {
|
|
90
|
+
if (errors.length === 0) {
|
|
91
|
+
return ""
|
|
92
|
+
}
|
|
93
|
+
const formatError = (error) => error.stack || error.message || error
|
|
94
|
+
|
|
95
|
+
if (errors.length === 1) {
|
|
96
|
+
return `${ANSI.color(`-------- error --------`, ANSI.RED)}
|
|
97
|
+
${formatError(errors[0])}
|
|
98
|
+
${ANSI.color(`-------------------------`, ANSI.RED)}`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let output = []
|
|
102
|
+
errors.forEach((error) => {
|
|
103
|
+
output.push(
|
|
104
|
+
prefixFirstAndIndentRemainingLines({
|
|
105
|
+
prefix: `${UNICODE.CIRCLE_CROSS} `,
|
|
106
|
+
indentation: " ",
|
|
107
|
+
text: formatError(error),
|
|
108
|
+
}),
|
|
109
|
+
)
|
|
110
|
+
})
|
|
111
|
+
return `${ANSI.color(`-------- errors (${errors.length}) --------`, ANSI.RED)}
|
|
112
|
+
${output.join(`\n`)}
|
|
113
|
+
${ANSI.color(`-------------------------`, ANSI.RED)}`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const createSummaryLog = (
|
|
117
|
+
summary,
|
|
118
|
+
) => `-------------- summary -----------------
|
|
119
|
+
${createAllExecutionsSummary(summary)}
|
|
120
|
+
total duration: ${msAsDuration(summary.duration)}
|
|
121
|
+
----------------------------------------`
|
|
122
|
+
|
|
123
|
+
const createAllExecutionsSummary = ({ counters }) => {
|
|
124
|
+
if (counters.total === 0) {
|
|
125
|
+
return `no execution`
|
|
126
|
+
}
|
|
127
|
+
const executionLabel =
|
|
128
|
+
counters.total === 1 ? `1 execution` : `${counters.total} executions`
|
|
129
|
+
return `${executionLabel}: ${createStatusSummary({
|
|
130
|
+
counters,
|
|
131
|
+
})}`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const createIntermediateSummary = ({
|
|
135
|
+
executionIndex,
|
|
136
|
+
counters,
|
|
137
|
+
memoryHeap,
|
|
138
|
+
timeEllapsed,
|
|
139
|
+
}) => {
|
|
140
|
+
const parts = []
|
|
141
|
+
if (executionIndex > 0 || counters.done > 0) {
|
|
142
|
+
parts.push(
|
|
143
|
+
createStatusSummary({
|
|
144
|
+
counters: {
|
|
145
|
+
...counters,
|
|
146
|
+
total: executionIndex + 1,
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
if (timeEllapsed) {
|
|
152
|
+
parts.push(`duration: ${msAsEllapsedTime(timeEllapsed)}`)
|
|
153
|
+
}
|
|
154
|
+
if (memoryHeap) {
|
|
155
|
+
parts.push(`memory heap: ${byteAsMemoryUsage(memoryHeap)}`)
|
|
156
|
+
}
|
|
157
|
+
if (parts.length === 0) {
|
|
158
|
+
return ""
|
|
159
|
+
}
|
|
160
|
+
return ` (${parts.join(` / `)})`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const createStatusSummary = ({ counters }) => {
|
|
164
|
+
if (counters.aborted === counters.total) {
|
|
165
|
+
return `all ${ANSI.color(`aborted`, EXECUTION_COLORS.aborted)}`
|
|
166
|
+
}
|
|
167
|
+
if (counters.timedout === counters.total) {
|
|
168
|
+
return `all ${ANSI.color(`timed out`, EXECUTION_COLORS.timedout)}`
|
|
169
|
+
}
|
|
170
|
+
if (counters.failed === counters.total) {
|
|
171
|
+
return `all ${ANSI.color(`failed`, EXECUTION_COLORS.failed)}`
|
|
172
|
+
}
|
|
173
|
+
if (counters.completed === counters.total) {
|
|
174
|
+
return `all ${ANSI.color(`completed`, EXECUTION_COLORS.completed)}`
|
|
175
|
+
}
|
|
176
|
+
if (counters.cancelled === counters.total) {
|
|
177
|
+
return `all ${ANSI.color(`cancelled`, EXECUTION_COLORS.cancelled)}`
|
|
178
|
+
}
|
|
179
|
+
return createMixedDetails({
|
|
180
|
+
counters,
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const createMixedDetails = ({ counters }) => {
|
|
185
|
+
const parts = []
|
|
186
|
+
if (counters.timedout) {
|
|
187
|
+
parts.push(
|
|
188
|
+
`${counters.timedout} ${ANSI.color(
|
|
189
|
+
`timed out`,
|
|
190
|
+
EXECUTION_COLORS.timedout,
|
|
191
|
+
)}`,
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
if (counters.failed) {
|
|
195
|
+
parts.push(
|
|
196
|
+
`${counters.failed} ${ANSI.color(`failed`, EXECUTION_COLORS.failed)}`,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
if (counters.completed) {
|
|
200
|
+
parts.push(
|
|
201
|
+
`${counters.completed} ${ANSI.color(
|
|
202
|
+
`completed`,
|
|
203
|
+
EXECUTION_COLORS.completed,
|
|
204
|
+
)}`,
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
if (counters.aborted) {
|
|
208
|
+
parts.push(
|
|
209
|
+
`${counters.aborted} ${ANSI.color(`aborted`, EXECUTION_COLORS.aborted)}`,
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
if (counters.cancelled) {
|
|
213
|
+
parts.push(
|
|
214
|
+
`${counters.cancelled} ${ANSI.color(
|
|
215
|
+
`cancelled`,
|
|
216
|
+
EXECUTION_COLORS.cancelled,
|
|
217
|
+
)}`,
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
return `${parts.join(", ")}`
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const descriptionFormatters = {
|
|
224
|
+
executing: ({ index, total }) => {
|
|
225
|
+
return ANSI.color(
|
|
226
|
+
`executing ${padNumber(index, total)} of ${total}`,
|
|
227
|
+
EXECUTION_COLORS.executing,
|
|
228
|
+
)
|
|
229
|
+
},
|
|
230
|
+
aborted: ({ index, total }) => {
|
|
231
|
+
return ANSI.color(
|
|
232
|
+
`${UNICODE.FAILURE_RAW} execution ${padNumber(
|
|
233
|
+
index,
|
|
234
|
+
total,
|
|
235
|
+
)} of ${total} aborted`,
|
|
236
|
+
EXECUTION_COLORS.aborted,
|
|
237
|
+
)
|
|
238
|
+
},
|
|
239
|
+
timedout: ({ index, total, executionParams }) => {
|
|
240
|
+
return ANSI.color(
|
|
241
|
+
`${UNICODE.FAILURE_RAW} execution ${padNumber(
|
|
242
|
+
index,
|
|
243
|
+
total,
|
|
244
|
+
)} of ${total} timeout after ${executionParams.allocatedMs}ms`,
|
|
245
|
+
EXECUTION_COLORS.timedout,
|
|
246
|
+
)
|
|
247
|
+
},
|
|
248
|
+
failed: ({ index, total }) => {
|
|
249
|
+
return ANSI.color(
|
|
250
|
+
`${UNICODE.FAILURE_RAW} execution ${padNumber(
|
|
251
|
+
index,
|
|
252
|
+
total,
|
|
253
|
+
)} of ${total} failed`,
|
|
254
|
+
EXECUTION_COLORS.failed,
|
|
255
|
+
)
|
|
256
|
+
},
|
|
257
|
+
completed: ({ index, total }) => {
|
|
258
|
+
return ANSI.color(
|
|
259
|
+
`${UNICODE.OK_RAW} execution ${padNumber(
|
|
260
|
+
index,
|
|
261
|
+
total,
|
|
262
|
+
)} of ${total} completed`,
|
|
263
|
+
EXECUTION_COLORS.completed,
|
|
264
|
+
)
|
|
265
|
+
},
|
|
266
|
+
cancelled: ({ index, total }) => {
|
|
267
|
+
return ANSI.color(
|
|
268
|
+
`${UNICODE.FAILURE_RAW} execution ${padNumber(
|
|
269
|
+
index,
|
|
270
|
+
total,
|
|
271
|
+
)} of ${total} cancelled`,
|
|
272
|
+
EXECUTION_COLORS.cancelled,
|
|
273
|
+
)
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const padNumber = (index, total) => {
|
|
278
|
+
const number = index + 1
|
|
279
|
+
const numberWidth = String(number).length
|
|
280
|
+
const totalWith = String(total).length
|
|
281
|
+
let missingWidth = totalWith - numberWidth
|
|
282
|
+
let padded = ""
|
|
283
|
+
while (missingWidth--) {
|
|
284
|
+
padded += "0"
|
|
285
|
+
}
|
|
286
|
+
padded += number
|
|
287
|
+
return padded
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const formatConsoleCalls = (consoleCalls) => {
|
|
291
|
+
if (consoleCalls.length === 0) {
|
|
292
|
+
return ""
|
|
293
|
+
}
|
|
294
|
+
const repartition = {
|
|
295
|
+
debug: 0,
|
|
296
|
+
info: 0,
|
|
297
|
+
warning: 0,
|
|
298
|
+
error: 0,
|
|
299
|
+
log: 0,
|
|
300
|
+
}
|
|
301
|
+
consoleCalls.forEach((consoleCall) => {
|
|
302
|
+
repartition[consoleCall.type]++
|
|
303
|
+
})
|
|
304
|
+
const consoleOutput = formatConsoleOutput(consoleCalls)
|
|
305
|
+
|
|
306
|
+
return `${ANSI.color(
|
|
307
|
+
`-------- ${formatConsoleSummary(repartition)} --------`,
|
|
308
|
+
ANSI.GREY,
|
|
309
|
+
)}
|
|
310
|
+
${consoleOutput}
|
|
311
|
+
${ANSI.color(`-------------------------`, ANSI.GREY)}`
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export const formatConsoleOutput = (consoleCalls) => {
|
|
315
|
+
// inside Node.js you can do process.stdout.write()
|
|
316
|
+
// and in that case the consoleCall is not suffixed with "\n"
|
|
317
|
+
// we want to keep these calls together in the output
|
|
318
|
+
const regroupedCalls = []
|
|
319
|
+
consoleCalls.forEach((consoleCall, index) => {
|
|
320
|
+
if (index === 0) {
|
|
321
|
+
regroupedCalls.push(consoleCall)
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
const previousCall = consoleCalls[index - 1]
|
|
325
|
+
if (previousCall.type !== consoleCall.type) {
|
|
326
|
+
regroupedCalls.push(consoleCall)
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
if (previousCall.text.endsWith("\n")) {
|
|
330
|
+
regroupedCalls.push(consoleCall)
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
if (previousCall.text.endsWith("\r")) {
|
|
334
|
+
regroupedCalls.push(consoleCall)
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
const previousRegroupedCallIndex = regroupedCalls.length - 1
|
|
338
|
+
const previousRegroupedCall = regroupedCalls[previousRegroupedCallIndex]
|
|
339
|
+
previousRegroupedCall.text = `${previousRegroupedCall.text}${consoleCall.text}`
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
let consoleOutput = ``
|
|
343
|
+
regroupedCalls.forEach((regroupedCall, index) => {
|
|
344
|
+
const text = regroupedCall.text
|
|
345
|
+
const textFormatted = prefixFirstAndIndentRemainingLines({
|
|
346
|
+
prefix: CONSOLE_ICONS[regroupedCall.type],
|
|
347
|
+
text,
|
|
348
|
+
trimLines: true,
|
|
349
|
+
trimLastLine: index === regroupedCalls.length - 1,
|
|
350
|
+
})
|
|
351
|
+
consoleOutput += textFormatted
|
|
352
|
+
})
|
|
353
|
+
return consoleOutput
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const prefixFirstAndIndentRemainingLines = ({
|
|
357
|
+
prefix,
|
|
358
|
+
indentation = " ",
|
|
359
|
+
text,
|
|
360
|
+
trimLines,
|
|
361
|
+
trimLastLine,
|
|
362
|
+
}) => {
|
|
363
|
+
const lines = text.split(/\r?\n/)
|
|
364
|
+
const firstLine = lines.shift()
|
|
365
|
+
let result = `${prefix} ${firstLine}`
|
|
366
|
+
let i = 0
|
|
367
|
+
while (i < lines.length) {
|
|
368
|
+
const line = trimLines ? lines[i].trim() : lines[i]
|
|
369
|
+
i++
|
|
370
|
+
result += line.length
|
|
371
|
+
? `\n${indentation}${line}`
|
|
372
|
+
: trimLastLine && i === lines.length
|
|
373
|
+
? ""
|
|
374
|
+
: `\n`
|
|
375
|
+
}
|
|
376
|
+
return result
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const CONSOLE_ICONS = {
|
|
380
|
+
debug: UNICODE.DEBUG,
|
|
381
|
+
info: UNICODE.INFO,
|
|
382
|
+
warning: UNICODE.WARNING,
|
|
383
|
+
error: UNICODE.FAILURE,
|
|
384
|
+
log: " ",
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const formatConsoleSummary = (repartition) => {
|
|
388
|
+
const { debug, info, warning, error } = repartition
|
|
389
|
+
const parts = []
|
|
390
|
+
if (error) {
|
|
391
|
+
parts.push(`${CONSOLE_ICONS.error} ${error}`)
|
|
392
|
+
}
|
|
393
|
+
if (warning) {
|
|
394
|
+
parts.push(`${CONSOLE_ICONS.warning} ${warning}`)
|
|
395
|
+
}
|
|
396
|
+
if (info) {
|
|
397
|
+
parts.push(`${CONSOLE_ICONS.info} ${info}`)
|
|
398
|
+
}
|
|
399
|
+
if (debug) {
|
|
400
|
+
parts.push(`${CONSOLE_ICONS.debug} ${debug}`)
|
|
401
|
+
}
|
|
402
|
+
if (parts.length === 0) {
|
|
403
|
+
return `console`
|
|
404
|
+
}
|
|
405
|
+
return `console (${parts.join(" ")})`
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const formatExecution = ({
|
|
409
|
+
label,
|
|
410
|
+
details = {},
|
|
411
|
+
consoleOutput,
|
|
412
|
+
errorsOutput,
|
|
413
|
+
}) => {
|
|
414
|
+
let message = ``
|
|
415
|
+
message += label
|
|
416
|
+
Object.keys(details).forEach((key) => {
|
|
417
|
+
message += `
|
|
418
|
+
${key}: ${details[key]}`
|
|
419
|
+
})
|
|
420
|
+
if (consoleOutput) {
|
|
421
|
+
message += `\n${consoleOutput}`
|
|
422
|
+
}
|
|
423
|
+
if (errorsOutput) {
|
|
424
|
+
message += `\n${errorsOutput}`
|
|
425
|
+
}
|
|
426
|
+
return message
|
|
427
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { assert } from "@jsenv/assert"
|
|
2
|
+
|
|
3
|
+
import { formatConsoleOutput } from "./logs_file_execution.js"
|
|
4
|
+
|
|
5
|
+
{
|
|
6
|
+
const actual = formatConsoleOutput([
|
|
7
|
+
{ type: "log", text: "a\n" },
|
|
8
|
+
{ type: "log", text: "b\n" },
|
|
9
|
+
])
|
|
10
|
+
const expected = ` a
|
|
11
|
+
b`
|
|
12
|
+
assert({ actual, expected })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
const actual = formatConsoleOutput([
|
|
17
|
+
{ type: "log", text: "a" },
|
|
18
|
+
{ type: "log", text: "b" },
|
|
19
|
+
])
|
|
20
|
+
const expected = ` ab`
|
|
21
|
+
assert({ actual, expected })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
const actual = formatConsoleOutput([
|
|
26
|
+
{
|
|
27
|
+
type: "log",
|
|
28
|
+
text: `1
|
|
29
|
+
2`,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: "log",
|
|
33
|
+
text: `alpha
|
|
34
|
+
beta`,
|
|
35
|
+
},
|
|
36
|
+
])
|
|
37
|
+
const expected = ` 1
|
|
38
|
+
2alpha
|
|
39
|
+
beta`
|
|
40
|
+
assert({ actual, expected })
|
|
41
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Export a function capable to run a file on a runtime.
|
|
3
|
+
*
|
|
4
|
+
* - Used internally by "executeTestPlan" part of the documented API
|
|
5
|
+
* - Used internally by "execute" an advanced API not documented
|
|
6
|
+
* - logs generated during file execution can be collected
|
|
7
|
+
* - logs generated during file execution can be mirrored (re-logged to the console)
|
|
8
|
+
* - File is given allocatedMs to complete
|
|
9
|
+
* - Errors are collected
|
|
10
|
+
* - File execution result is returned, it contains status/errors/namespace/consoleCalls
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createId } from "@paralleldrive/cuid2"
|
|
14
|
+
import { Abort, raceCallbacks } from "@jsenv/abort"
|
|
15
|
+
import { ensureParentDirectories } from "@jsenv/filesystem"
|
|
16
|
+
|
|
17
|
+
export const run = async ({
|
|
18
|
+
signal = new AbortController().signal,
|
|
19
|
+
logger,
|
|
20
|
+
allocatedMs,
|
|
21
|
+
keepRunning = false,
|
|
22
|
+
mirrorConsole = false,
|
|
23
|
+
collectConsole = false,
|
|
24
|
+
coverageEnabled = false,
|
|
25
|
+
coverageTempDirectoryUrl,
|
|
26
|
+
collectPerformance = false,
|
|
27
|
+
runtime,
|
|
28
|
+
runtimeParams,
|
|
29
|
+
}) => {
|
|
30
|
+
const result = {
|
|
31
|
+
status: "pending",
|
|
32
|
+
errors: [],
|
|
33
|
+
namespace: null,
|
|
34
|
+
}
|
|
35
|
+
const callbacks = []
|
|
36
|
+
|
|
37
|
+
const onConsoleRef = { current: () => {} }
|
|
38
|
+
const stopSignal = { notify: () => {} }
|
|
39
|
+
const runtimeLabel = `${runtime.name}/${runtime.version}`
|
|
40
|
+
|
|
41
|
+
const runOperation = Abort.startOperation()
|
|
42
|
+
runOperation.addAbortSignal(signal)
|
|
43
|
+
let timeoutAbortSource
|
|
44
|
+
if (
|
|
45
|
+
// ideally we would rather log than the timeout is ignored
|
|
46
|
+
// when keepRunning is true
|
|
47
|
+
!keepRunning &&
|
|
48
|
+
typeof allocatedMs === "number" &&
|
|
49
|
+
allocatedMs !== Infinity
|
|
50
|
+
) {
|
|
51
|
+
timeoutAbortSource = runOperation.timeout(allocatedMs)
|
|
52
|
+
}
|
|
53
|
+
const consoleCalls = []
|
|
54
|
+
onConsoleRef.current = ({ type, text }) => {
|
|
55
|
+
if (mirrorConsole) {
|
|
56
|
+
if (type === "error") {
|
|
57
|
+
process.stderr.write(text)
|
|
58
|
+
} else {
|
|
59
|
+
process.stdout.write(text)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (collectConsole) {
|
|
63
|
+
consoleCalls.push({ type, text })
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (collectConsole) {
|
|
67
|
+
result.consoleCalls = consoleCalls
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// we do not keep coverage in memory, it can grow very big
|
|
71
|
+
// instead we store it on the filesystem
|
|
72
|
+
// and they can be read later at "coverageFileUrl"
|
|
73
|
+
let coverageFileUrl
|
|
74
|
+
if (coverageEnabled) {
|
|
75
|
+
coverageFileUrl = new URL(
|
|
76
|
+
`./${runtime.name}/${createId()}.json`,
|
|
77
|
+
coverageTempDirectoryUrl,
|
|
78
|
+
).href
|
|
79
|
+
await ensureParentDirectories(coverageFileUrl)
|
|
80
|
+
if (coverageEnabled) {
|
|
81
|
+
result.coverageFileUrl = coverageFileUrl
|
|
82
|
+
// written within the child_process/worker_thread or during runtime.run()
|
|
83
|
+
// for browsers
|
|
84
|
+
// (because it takes time to serialize and transfer the coverage object)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const startMs = Date.now()
|
|
89
|
+
callbacks.push(() => {
|
|
90
|
+
result.duration = Date.now() - startMs
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
logger.debug(`run() ${runtimeLabel}`)
|
|
95
|
+
runOperation.throwIfAborted()
|
|
96
|
+
const winnerPromise = new Promise((resolve) => {
|
|
97
|
+
raceCallbacks(
|
|
98
|
+
{
|
|
99
|
+
aborted: (cb) => {
|
|
100
|
+
runOperation.signal.addEventListener("abort", cb)
|
|
101
|
+
return () => {
|
|
102
|
+
runOperation.signal.removeEventListener("abort", cb)
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
runned: async (cb) => {
|
|
106
|
+
try {
|
|
107
|
+
const runResult = await runtime.run({
|
|
108
|
+
signal: runOperation.signal,
|
|
109
|
+
logger,
|
|
110
|
+
...runtimeParams,
|
|
111
|
+
collectConsole,
|
|
112
|
+
collectPerformance,
|
|
113
|
+
coverageFileUrl,
|
|
114
|
+
keepRunning,
|
|
115
|
+
stopSignal,
|
|
116
|
+
onConsole: (log) => onConsoleRef.current(log),
|
|
117
|
+
})
|
|
118
|
+
cb(runResult)
|
|
119
|
+
} catch (e) {
|
|
120
|
+
cb({
|
|
121
|
+
status: "failed",
|
|
122
|
+
errors: [e],
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
resolve,
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
const winner = await winnerPromise
|
|
131
|
+
if (winner.name === "aborted") {
|
|
132
|
+
runOperation.throwIfAborted()
|
|
133
|
+
}
|
|
134
|
+
const { status, namespace, errors, performance } = winner.data
|
|
135
|
+
result.status = status
|
|
136
|
+
result.errors.push(...errors)
|
|
137
|
+
result.namespace = namespace
|
|
138
|
+
if (collectPerformance) {
|
|
139
|
+
result.performance = performance
|
|
140
|
+
}
|
|
141
|
+
} catch (e) {
|
|
142
|
+
if (Abort.isAbortError(e)) {
|
|
143
|
+
if (timeoutAbortSource && timeoutAbortSource.signal.aborted) {
|
|
144
|
+
result.status = "timedout"
|
|
145
|
+
} else {
|
|
146
|
+
result.status = "aborted"
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
result.status = "failed"
|
|
150
|
+
result.errors.push(e)
|
|
151
|
+
}
|
|
152
|
+
} finally {
|
|
153
|
+
await runOperation.end()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
callbacks.forEach((callback) => {
|
|
157
|
+
callback()
|
|
158
|
+
})
|
|
159
|
+
return result
|
|
160
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"
|
|
2
|
+
import { pingServer } from "@jsenv/core/src/helpers/ping_server.js"
|
|
3
|
+
import { basicFetch } from "@jsenv/core/src/helpers/basic_fetch.js"
|
|
4
|
+
|
|
5
|
+
export const assertAndNormalizeWebServer = async (webServer) => {
|
|
6
|
+
if (!webServer) {
|
|
7
|
+
throw new TypeError(
|
|
8
|
+
`webServer is required when running tests on browser(s)`,
|
|
9
|
+
)
|
|
10
|
+
}
|
|
11
|
+
const unexpectedParamNames = Object.keys(webServer).filter((key) => {
|
|
12
|
+
return !["origin", "moduleUrl", "rootDirectoryUrl"].includes(key)
|
|
13
|
+
})
|
|
14
|
+
if (unexpectedParamNames.length > 0) {
|
|
15
|
+
throw new TypeError(
|
|
16
|
+
`${unexpectedParamNames.join(",")}: there is no such param to webServer`,
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let aServerIsListening = await pingServer(webServer.origin)
|
|
21
|
+
if (!aServerIsListening) {
|
|
22
|
+
if (!webServer.moduleUrl) {
|
|
23
|
+
throw new TypeError(
|
|
24
|
+
`webServer.moduleUrl is required as there is no server listening "${webServer.origin}"`,
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
process.env.IMPORTED_BY_TEST_PLAN = "1"
|
|
29
|
+
await import(webServer.moduleUrl)
|
|
30
|
+
delete process.env.IMPORTED_BY_TEST_PLAN
|
|
31
|
+
} catch (e) {
|
|
32
|
+
if (e.code === "ERR_MODULE_NOT_FOUND") {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`webServer.moduleUrl does not lead to a file at "${webServer.moduleUrl}"`,
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
throw e
|
|
38
|
+
}
|
|
39
|
+
aServerIsListening = await pingServer(webServer.origin)
|
|
40
|
+
if (!aServerIsListening) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`webServer.moduleUrl did not start a server listening at "${webServer.origin}", check file at "${webServer.moduleUrl}"`,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const { headers } = await basicFetch(webServer.origin, {
|
|
47
|
+
method: "GET",
|
|
48
|
+
rejectUnauthorized: false,
|
|
49
|
+
headers: {
|
|
50
|
+
"x-server-inspect": "1",
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
if (String(headers["server"]).includes("jsenv_dev_server")) {
|
|
54
|
+
webServer.isJsenvDevServer = true
|
|
55
|
+
const { json } = await basicFetch(`${webServer.origin}/__params__.json`, {
|
|
56
|
+
rejectUnauthorized: false,
|
|
57
|
+
})
|
|
58
|
+
if (webServer.rootDirectoryUrl === undefined) {
|
|
59
|
+
const jsenvDevServerParams = await json()
|
|
60
|
+
webServer.rootDirectoryUrl = jsenvDevServerParams.sourceDirectoryUrl
|
|
61
|
+
} else {
|
|
62
|
+
webServer.rootDirectoryUrl = assertAndNormalizeDirectoryUrl(
|
|
63
|
+
webServer.rootDirectoryUrl,
|
|
64
|
+
"webServer.rootDirectoryUrl",
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
webServer.rootDirectoryUrl = assertAndNormalizeDirectoryUrl(
|
|
69
|
+
webServer.rootDirectoryUrl,
|
|
70
|
+
"webServer.rootDirectoryUrl",
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/main.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// test
|
|
2
|
+
export { executeTestPlan } from "./execution/execute_test_plan.js"
|
|
3
|
+
// runtimes (used to execute tests)
|
|
4
|
+
export { chromium, chromiumIsolatedTab } from "./runtime_browsers/chromium.js"
|
|
5
|
+
export { firefox, firefoxIsolatedTab } from "./runtime_browsers/firefox.js"
|
|
6
|
+
export { webkit, webkitIsolatedTab } from "./runtime_browsers/webkit.js"
|
|
7
|
+
export { nodeChildProcess } from "./runtime_node/node_child_process.js"
|
|
8
|
+
export { nodeWorkerThread } from "./runtime_node/node_worker_thread.js"
|
|
9
|
+
// advanced
|
|
10
|
+
export { execute } from "./execution/execute.js"
|