@jsenv/core 25.4.9 → 25.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/core",
|
|
3
|
-
"version": "25.
|
|
3
|
+
"version": "25.5.0",
|
|
4
4
|
"description": "Tool to develop, test and build js projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -102,6 +102,7 @@
|
|
|
102
102
|
"rollup-plugin-node-globals": "1.4.0",
|
|
103
103
|
"rollup-plugin-polyfill-node": "0.8.0",
|
|
104
104
|
"source-map": "0.7.3",
|
|
105
|
+
"strip-ansi": "7.0.1",
|
|
105
106
|
"systemjs": "6.11.0",
|
|
106
107
|
"terser": "5.10.0",
|
|
107
108
|
"v8-to-istanbul": "8.1.0",
|
package/src/executeTestPlan.js
CHANGED
|
@@ -28,6 +28,7 @@ import { jsenvCoverageConfig } from "./jsenvCoverageConfig.js"
|
|
|
28
28
|
* @param {boolean} [testPlanParameters.completedExecutionLogMerging=false] Merge completed execution logs to shorten terminal output
|
|
29
29
|
* @param {number} [testPlanParameters.maxExecutionsInParallel=1] Maximum amount of execution in parallel
|
|
30
30
|
* @param {number} [testPlanParameters.defaultMsAllocatedPerExecution=30000] Milliseconds after which execution is aborted and considered as failed by timeout
|
|
31
|
+
* @param {boolean} [testPlanParameters.failFast=false] Fails immediatly when a test execution fails
|
|
31
32
|
* @param {number} [testPlanParameters.cooldownBetweenExecutions=0] Millisecond to wait between each execution
|
|
32
33
|
* @param {boolean} [testPlanParameters.logMemoryHeapUsage=false] Add memory heap usage during logs
|
|
33
34
|
* @param {boolean} [testPlanParameters.coverage=false] Controls if coverage is collected during files executions
|
|
@@ -48,15 +49,17 @@ export const executeTestPlan = async ({
|
|
|
48
49
|
|
|
49
50
|
testPlan,
|
|
50
51
|
|
|
52
|
+
logSummary = true,
|
|
51
53
|
logMemoryHeapUsage = false,
|
|
54
|
+
logFileRelativeUrl = ".jsenv/test_plan_debug.txt",
|
|
52
55
|
completedExecutionLogAbbreviation = false,
|
|
53
56
|
completedExecutionLogMerging = false,
|
|
54
|
-
logSummary = true,
|
|
55
57
|
updateProcessExitCode = true,
|
|
56
58
|
windowsProcessExitFix = true,
|
|
57
59
|
|
|
58
60
|
maxExecutionsInParallel = 1,
|
|
59
61
|
defaultMsAllocatedPerExecution = 30000,
|
|
62
|
+
failFast = false,
|
|
60
63
|
// stopAfterExecute: true to ensure runtime is stopped once executed
|
|
61
64
|
// because we have what we wants: execution is completed and
|
|
62
65
|
// we have associated coverage and capturedConsole
|
|
@@ -103,14 +106,11 @@ export const executeTestPlan = async ({
|
|
|
103
106
|
jsenvDirectoryClean,
|
|
104
107
|
}) => {
|
|
105
108
|
const logger = createLogger({ logLevel })
|
|
106
|
-
|
|
107
109
|
projectDirectoryUrl = assertProjectDirectoryUrl({ projectDirectoryUrl })
|
|
108
110
|
await assertProjectDirectoryExists({ projectDirectoryUrl })
|
|
109
|
-
|
|
110
111
|
if (typeof testPlan !== "object") {
|
|
111
112
|
throw new Error(`testPlan must be an object, got ${testPlan}`)
|
|
112
113
|
}
|
|
113
|
-
|
|
114
114
|
if (coverage) {
|
|
115
115
|
if (typeof coverageConfig !== "object") {
|
|
116
116
|
throw new TypeError(
|
|
@@ -155,7 +155,6 @@ export const executeTestPlan = async ({
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
-
|
|
159
158
|
const result = await executePlan(testPlan, {
|
|
160
159
|
signal,
|
|
161
160
|
handleSIGINT,
|
|
@@ -171,13 +170,15 @@ export const executeTestPlan = async ({
|
|
|
171
170
|
importResolutionMethod,
|
|
172
171
|
importDefaultExtension,
|
|
173
172
|
|
|
173
|
+
logSummary,
|
|
174
174
|
logMemoryHeapUsage,
|
|
175
|
+
logFileRelativeUrl,
|
|
175
176
|
completedExecutionLogMerging,
|
|
176
177
|
completedExecutionLogAbbreviation,
|
|
177
|
-
logSummary,
|
|
178
178
|
|
|
179
|
-
defaultMsAllocatedPerExecution,
|
|
180
179
|
maxExecutionsInParallel,
|
|
180
|
+
defaultMsAllocatedPerExecution,
|
|
181
|
+
failFast,
|
|
181
182
|
stopAfterExecute,
|
|
182
183
|
cooldownBetweenExecutions,
|
|
183
184
|
gcBetweenExecutions,
|
|
@@ -205,11 +206,9 @@ export const executeTestPlan = async ({
|
|
|
205
206
|
importMapInWebWorkers,
|
|
206
207
|
customCompilers,
|
|
207
208
|
})
|
|
208
|
-
|
|
209
209
|
if (updateProcessExitCode && !executionIsPassed(result)) {
|
|
210
210
|
process.exitCode = 1
|
|
211
211
|
}
|
|
212
|
-
|
|
213
212
|
const planCoverage = result.planCoverage
|
|
214
213
|
// planCoverage can be null when execution is aborted
|
|
215
214
|
if (planCoverage) {
|
|
@@ -260,7 +259,6 @@ export const executeTestPlan = async ({
|
|
|
260
259
|
}
|
|
261
260
|
await Promise.all(promises)
|
|
262
261
|
}
|
|
263
|
-
|
|
264
262
|
// Sometimes on windows test plan scripts never ends
|
|
265
263
|
// I suspect it's some node process keeping the process alive
|
|
266
264
|
// because not properly killed for some reason.
|
|
@@ -271,7 +269,6 @@ export const executeTestPlan = async ({
|
|
|
271
269
|
process.exit()
|
|
272
270
|
}, 2000).unref()
|
|
273
271
|
}
|
|
274
|
-
|
|
275
272
|
return {
|
|
276
273
|
testPlanAborted: result.aborted,
|
|
277
274
|
testPlanSummary: result.planSummary,
|
|
@@ -1,11 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { memoryUsage } from "node:process"
|
|
3
|
+
import wrapAnsi from "wrap-ansi"
|
|
4
|
+
import stripAnsi from "strip-ansi"
|
|
5
|
+
import cuid from "cuid"
|
|
6
|
+
import { createDetailedMessage, loggerToLevels } from "@jsenv/logger"
|
|
7
|
+
import { createLog, startSpinner } from "@jsenv/log"
|
|
8
|
+
import {
|
|
9
|
+
Abort,
|
|
10
|
+
raceProcessTeardownEvents,
|
|
11
|
+
createCallbackListNotifiedOnce,
|
|
12
|
+
} from "@jsenv/abort"
|
|
13
|
+
import {
|
|
14
|
+
urlToFileSystemPath,
|
|
15
|
+
resolveUrl,
|
|
16
|
+
writeDirectory,
|
|
17
|
+
ensureEmptyDirectory,
|
|
18
|
+
normalizeStructuredMetaMap,
|
|
19
|
+
urlToMeta,
|
|
20
|
+
writeFile,
|
|
21
|
+
} from "@jsenv/filesystem"
|
|
3
22
|
|
|
4
|
-
import { mergeRuntimeSupport } from "@jsenv/core/src/internal/runtime_support/runtime_support.js"
|
|
5
23
|
import { startCompileServer } from "../compiling/startCompileServer.js"
|
|
6
24
|
import { babelPluginInstrument } from "./coverage/babel_plugin_instrument.js"
|
|
7
25
|
import { generateExecutionSteps } from "./generateExecutionSteps.js"
|
|
8
|
-
|
|
26
|
+
|
|
27
|
+
import { launchAndExecute } from "../executing/launchAndExecute.js"
|
|
28
|
+
import { reportToCoverage } from "./coverage/reportToCoverage.js"
|
|
29
|
+
import { formatExecuting, formatExecutionResult } from "./executionLogs.js"
|
|
30
|
+
import { createSummaryLog } from "./createSummaryLog.js"
|
|
31
|
+
import { ensureGlobalGc } from "./gc.js"
|
|
9
32
|
|
|
10
33
|
export const executePlan = async (
|
|
11
34
|
plan,
|
|
@@ -26,11 +49,13 @@ export const executePlan = async (
|
|
|
26
49
|
|
|
27
50
|
logSummary,
|
|
28
51
|
logMemoryHeapUsage,
|
|
52
|
+
logFileRelativeUrl,
|
|
29
53
|
completedExecutionLogMerging,
|
|
30
54
|
completedExecutionLogAbbreviation,
|
|
31
55
|
|
|
32
56
|
defaultMsAllocatedPerExecution,
|
|
33
57
|
maxExecutionsInParallel,
|
|
58
|
+
failFast,
|
|
34
59
|
gcBetweenExecutions,
|
|
35
60
|
stopAfterExecute,
|
|
36
61
|
cooldownBetweenExecutions,
|
|
@@ -56,6 +81,9 @@ export const executePlan = async (
|
|
|
56
81
|
serviceWorkers,
|
|
57
82
|
importMapInWebWorkers,
|
|
58
83
|
customCompilers,
|
|
84
|
+
|
|
85
|
+
beforeExecutionCallback = () => {},
|
|
86
|
+
afterExecutionCallback = () => {},
|
|
59
87
|
} = {},
|
|
60
88
|
) => {
|
|
61
89
|
if (coverage) {
|
|
@@ -67,27 +95,22 @@ export const executePlan = async (
|
|
|
67
95
|
],
|
|
68
96
|
}
|
|
69
97
|
}
|
|
70
|
-
|
|
71
|
-
const runtimeSupport = {}
|
|
98
|
+
const runtimes = {}
|
|
72
99
|
Object.keys(plan).forEach((filePattern) => {
|
|
73
100
|
const filePlan = plan[filePattern]
|
|
74
101
|
Object.keys(filePlan).forEach((executionName) => {
|
|
75
102
|
const executionConfig = filePlan[executionName]
|
|
76
103
|
const { runtime } = executionConfig
|
|
77
104
|
if (runtime) {
|
|
78
|
-
|
|
79
|
-
[runtime.name]: runtime.version,
|
|
80
|
-
})
|
|
105
|
+
runtimes[runtime.name] = runtime.version
|
|
81
106
|
}
|
|
82
107
|
})
|
|
83
108
|
})
|
|
84
|
-
|
|
85
109
|
logger.debug(
|
|
86
110
|
createDetailedMessage(`Prepare executing plan`, {
|
|
87
|
-
|
|
111
|
+
runtimes: JSON.stringify(runtimes, null, " "),
|
|
88
112
|
}),
|
|
89
113
|
)
|
|
90
|
-
|
|
91
114
|
const multipleExecutionsOperation = Abort.startOperation()
|
|
92
115
|
multipleExecutionsOperation.addAbortSignal(signal)
|
|
93
116
|
if (handleSIGINT) {
|
|
@@ -103,6 +126,10 @@ export const executePlan = async (
|
|
|
103
126
|
)
|
|
104
127
|
})
|
|
105
128
|
}
|
|
129
|
+
const failFastAbortController = new AbortController()
|
|
130
|
+
if (failFast) {
|
|
131
|
+
multipleExecutionsOperation.addAbortSignal(failFastAbortController.signal)
|
|
132
|
+
}
|
|
106
133
|
|
|
107
134
|
try {
|
|
108
135
|
const compileServer = await startCompileServer({
|
|
@@ -131,74 +158,303 @@ export const executePlan = async (
|
|
|
131
158
|
serviceWorkers,
|
|
132
159
|
importMapInWebWorkers,
|
|
133
160
|
customCompilers,
|
|
134
|
-
runtimeSupport,
|
|
135
161
|
})
|
|
136
|
-
|
|
162
|
+
babelPluginMap = compileServer.babelPluginMap
|
|
137
163
|
multipleExecutionsOperation.addEndCallback(async () => {
|
|
138
164
|
await compileServer.stop()
|
|
139
165
|
})
|
|
140
|
-
|
|
141
166
|
logger.debug(`Generate executions`)
|
|
167
|
+
const executionSteps = await getExecutionAsSteps({
|
|
168
|
+
plan,
|
|
169
|
+
compileServer,
|
|
170
|
+
multipleExecutionsOperation,
|
|
171
|
+
projectDirectoryUrl,
|
|
172
|
+
})
|
|
173
|
+
logger.debug(`${executionSteps.length} executions planned`)
|
|
142
174
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
...plan,
|
|
148
|
-
[compileServer.jsenvDirectoryRelativeUrl]: null,
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
signal: multipleExecutionsOperation.signal,
|
|
152
|
-
projectDirectoryUrl,
|
|
153
|
-
},
|
|
175
|
+
if (completedExecutionLogMerging && !process.stdout.isTTY) {
|
|
176
|
+
completedExecutionLogMerging = false
|
|
177
|
+
logger.debug(
|
|
178
|
+
`Force completedExecutionLogMerging to false because process.stdout.isTTY is false`,
|
|
154
179
|
)
|
|
155
|
-
} catch (e) {
|
|
156
|
-
if (Abort.isAbortError(e)) {
|
|
157
|
-
return {
|
|
158
|
-
aborted: true,
|
|
159
|
-
planSummary: {},
|
|
160
|
-
planReport: {},
|
|
161
|
-
planCoverage: null,
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
throw e
|
|
165
180
|
}
|
|
166
|
-
|
|
181
|
+
const executionLogsEnabled = loggerToLevels(logger).info
|
|
182
|
+
const executionSpinner = executionLogsEnabled && process.stdout.isTTY
|
|
167
183
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
184
|
+
const startMs = Date.now()
|
|
185
|
+
const report = {}
|
|
186
|
+
const executionCount = executionSteps.length
|
|
187
|
+
let rawOutput = ""
|
|
188
|
+
|
|
189
|
+
let transformReturnValue = (value) => value
|
|
172
190
|
|
|
191
|
+
if (gcBetweenExecutions) {
|
|
192
|
+
ensureGlobalGc()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const coverageTempDirectoryUrl = resolveUrl(
|
|
196
|
+
coverageTempDirectoryRelativeUrl,
|
|
173
197
|
projectDirectoryUrl,
|
|
174
|
-
|
|
198
|
+
)
|
|
199
|
+
const structuredMetaMapForCover = normalizeStructuredMetaMap(
|
|
200
|
+
{
|
|
201
|
+
cover: coverageConfig,
|
|
202
|
+
},
|
|
203
|
+
projectDirectoryUrl,
|
|
204
|
+
)
|
|
205
|
+
const coverageIgnorePredicate = (url) => {
|
|
206
|
+
return !urlToMeta({
|
|
207
|
+
url: resolveUrl(url, projectDirectoryUrl),
|
|
208
|
+
structuredMetaMap: structuredMetaMapForCover,
|
|
209
|
+
}).cover
|
|
210
|
+
}
|
|
175
211
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
212
|
+
if (coverage) {
|
|
213
|
+
// in case runned multiple times, we don't want to keep writing lot of files in this directory
|
|
214
|
+
if (!process.env.NODE_V8_COVERAGE) {
|
|
215
|
+
await ensureEmptyDirectory(coverageTempDirectoryUrl)
|
|
216
|
+
}
|
|
217
|
+
if (runtimes.node) {
|
|
218
|
+
// v8 coverage is written in a directoy and auto propagate to subprocesses
|
|
219
|
+
// through process.env.NODE_V8_COVERAGE.
|
|
220
|
+
if (!coverageForceIstanbul && !process.env.NODE_V8_COVERAGE) {
|
|
221
|
+
const v8CoverageDirectory = resolveUrl(
|
|
222
|
+
`./node_v8/${cuid()}`,
|
|
223
|
+
coverageTempDirectoryUrl,
|
|
224
|
+
)
|
|
225
|
+
await writeDirectory(v8CoverageDirectory, { allowUseless: true })
|
|
226
|
+
process.env.NODE_V8_COVERAGE =
|
|
227
|
+
urlToFileSystemPath(v8CoverageDirectory)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
transformReturnValue = async (value) => {
|
|
232
|
+
if (multipleExecutionsOperation.signal.aborted) {
|
|
233
|
+
// don't try to do the coverage stuff
|
|
234
|
+
return value
|
|
235
|
+
}
|
|
179
236
|
|
|
180
|
-
|
|
237
|
+
try {
|
|
238
|
+
value.coverage = await reportToCoverage(value.report, {
|
|
239
|
+
signal: multipleExecutionsOperation.signal,
|
|
240
|
+
logger,
|
|
241
|
+
projectDirectoryUrl,
|
|
242
|
+
babelPluginMap,
|
|
243
|
+
coverageConfig,
|
|
244
|
+
coverageIncludeMissing,
|
|
245
|
+
coverageForceIstanbul,
|
|
246
|
+
coverageIgnorePredicate,
|
|
247
|
+
coverageV8ConflictWarning,
|
|
248
|
+
})
|
|
249
|
+
} catch (e) {
|
|
250
|
+
if (Abort.isAbortError(e)) {
|
|
251
|
+
return value
|
|
252
|
+
}
|
|
253
|
+
throw e
|
|
254
|
+
}
|
|
255
|
+
return value
|
|
256
|
+
}
|
|
257
|
+
}
|
|
181
258
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
259
|
+
logger.info("")
|
|
260
|
+
let executionLog = createLog({ newLine: "" })
|
|
261
|
+
let abortedCount = 0
|
|
262
|
+
let timedoutCount = 0
|
|
263
|
+
let erroredCount = 0
|
|
264
|
+
let completedCount = 0
|
|
265
|
+
const stopAfterAllExecutionCallbackList = createCallbackListNotifiedOnce()
|
|
186
266
|
|
|
187
|
-
|
|
267
|
+
let executionDoneCount = 0
|
|
268
|
+
await executeInParallel({
|
|
269
|
+
multipleExecutionsOperation,
|
|
188
270
|
maxExecutionsInParallel,
|
|
189
|
-
stopAfterExecute,
|
|
190
|
-
gcBetweenExecutions,
|
|
191
271
|
cooldownBetweenExecutions,
|
|
272
|
+
executionSteps,
|
|
273
|
+
start: async (paramsFromStep) => {
|
|
274
|
+
const executionIndex = executionSteps.indexOf(paramsFromStep)
|
|
275
|
+
const { executionName, fileRelativeUrl, runtime } = paramsFromStep
|
|
276
|
+
const runtimeName = runtime.name
|
|
277
|
+
const runtimeVersion = runtime.version
|
|
192
278
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
279
|
+
const executionParams = {
|
|
280
|
+
// the params below can be overriden by executionDefaultParams
|
|
281
|
+
measurePerformance: false,
|
|
282
|
+
collectPerformance: false,
|
|
283
|
+
captureConsole: true,
|
|
284
|
+
stopAfterExecute,
|
|
285
|
+
stopAfterExecuteReason: "execution-done",
|
|
286
|
+
allocatedMs: defaultMsAllocatedPerExecution,
|
|
287
|
+
...paramsFromStep,
|
|
288
|
+
runtime,
|
|
289
|
+
// mirrorConsole: false because file will be executed in parallel
|
|
290
|
+
// so log would be a mess to read
|
|
291
|
+
mirrorConsole: false,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const beforeExecutionInfo = {
|
|
295
|
+
fileRelativeUrl,
|
|
296
|
+
runtimeName,
|
|
297
|
+
runtimeVersion,
|
|
298
|
+
executionIndex,
|
|
299
|
+
executionParams,
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let spinner
|
|
303
|
+
if (executionSpinner) {
|
|
304
|
+
spinner = startSpinner({
|
|
305
|
+
log: executionLog,
|
|
306
|
+
text: formatExecuting(beforeExecutionInfo, {
|
|
307
|
+
executionCount,
|
|
308
|
+
abortedCount,
|
|
309
|
+
timedoutCount,
|
|
310
|
+
erroredCount,
|
|
311
|
+
completedCount,
|
|
312
|
+
...(logMemoryHeapUsage
|
|
313
|
+
? { memoryHeap: memoryUsage().heapUsed }
|
|
314
|
+
: {}),
|
|
315
|
+
}),
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
beforeExecutionCallback(beforeExecutionInfo)
|
|
319
|
+
|
|
320
|
+
const filePath = urlToFileSystemPath(
|
|
321
|
+
`${projectDirectoryUrl}${fileRelativeUrl}`,
|
|
322
|
+
)
|
|
323
|
+
let executionResult
|
|
324
|
+
if (existsSync(filePath)) {
|
|
325
|
+
executionResult = await launchAndExecute({
|
|
326
|
+
signal: multipleExecutionsOperation.signal,
|
|
327
|
+
launchAndExecuteLogLevel,
|
|
328
|
+
|
|
329
|
+
...executionParams,
|
|
330
|
+
collectCoverage: coverage,
|
|
331
|
+
coverageTempDirectoryUrl,
|
|
332
|
+
runtimeParams: {
|
|
333
|
+
projectDirectoryUrl,
|
|
334
|
+
compileServerOrigin: compileServer.origin,
|
|
335
|
+
compileServerId: compileServer.id,
|
|
336
|
+
jsenvDirectoryRelativeUrl:
|
|
337
|
+
compileServer.jsenvDirectoryRelativeUrl,
|
|
338
|
+
|
|
339
|
+
collectCoverage: coverage,
|
|
340
|
+
coverageIgnorePredicate,
|
|
341
|
+
coverageForceIstanbul,
|
|
342
|
+
stopAfterAllExecutionCallbackList,
|
|
343
|
+
...executionParams.runtimeParams,
|
|
344
|
+
},
|
|
345
|
+
executeParams: {
|
|
346
|
+
fileRelativeUrl,
|
|
347
|
+
...executionParams.executeParams,
|
|
348
|
+
},
|
|
349
|
+
coverageV8ConflictWarning,
|
|
350
|
+
})
|
|
351
|
+
} else {
|
|
352
|
+
executionResult = {
|
|
353
|
+
status: "errored",
|
|
354
|
+
error: new Error(
|
|
355
|
+
`No file at ${fileRelativeUrl} for execution "${executionName}"`,
|
|
356
|
+
),
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
executionDoneCount++
|
|
360
|
+
if (fileRelativeUrl in report === false) {
|
|
361
|
+
report[fileRelativeUrl] = {}
|
|
362
|
+
}
|
|
363
|
+
report[fileRelativeUrl][executionName] = executionResult
|
|
364
|
+
const afterExecutionInfo = {
|
|
365
|
+
...beforeExecutionInfo,
|
|
366
|
+
endMs: Date.now(),
|
|
367
|
+
executionResult,
|
|
368
|
+
}
|
|
369
|
+
afterExecutionCallback(afterExecutionInfo)
|
|
370
|
+
|
|
371
|
+
if (executionResult.status === "aborted") {
|
|
372
|
+
abortedCount++
|
|
373
|
+
} else if (executionResult.status === "timedout") {
|
|
374
|
+
timedoutCount++
|
|
375
|
+
} else if (executionResult.status === "errored") {
|
|
376
|
+
erroredCount++
|
|
377
|
+
} else if (executionResult.status === "completed") {
|
|
378
|
+
completedCount++
|
|
379
|
+
}
|
|
380
|
+
if (gcBetweenExecutions) {
|
|
381
|
+
global.gc()
|
|
382
|
+
}
|
|
383
|
+
if (executionLogsEnabled) {
|
|
384
|
+
let log = formatExecutionResult(afterExecutionInfo, {
|
|
385
|
+
completedExecutionLogAbbreviation,
|
|
386
|
+
executionCount,
|
|
387
|
+
abortedCount,
|
|
388
|
+
timedoutCount,
|
|
389
|
+
erroredCount,
|
|
390
|
+
completedCount,
|
|
391
|
+
...(logMemoryHeapUsage
|
|
392
|
+
? { memoryHeap: memoryUsage().heapUsed }
|
|
393
|
+
: {}),
|
|
394
|
+
})
|
|
395
|
+
log = `${log}
|
|
396
|
+
|
|
397
|
+
`
|
|
398
|
+
const { columns = 80 } = process.stdout
|
|
399
|
+
log = wrapAnsi(log, columns, {
|
|
400
|
+
trim: false,
|
|
401
|
+
hard: true,
|
|
402
|
+
wordWrap: false,
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
// replace spinner with this execution result
|
|
406
|
+
if (spinner) spinner.stop()
|
|
407
|
+
executionLog.write(log)
|
|
408
|
+
rawOutput += stripAnsi(log)
|
|
409
|
+
|
|
410
|
+
const canOverwriteLog = canOverwriteLogGetter({
|
|
411
|
+
completedExecutionLogMerging,
|
|
412
|
+
executionResult,
|
|
413
|
+
})
|
|
414
|
+
if (canOverwriteLog) {
|
|
415
|
+
// nothing to do, we reuse the current executionLog object
|
|
416
|
+
} else {
|
|
417
|
+
executionLog.destroy()
|
|
418
|
+
executionLog = createLog({ newLine: "" })
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (
|
|
422
|
+
failFast &&
|
|
423
|
+
executionResult.status !== "completed" &&
|
|
424
|
+
executionDoneCount < executionCount
|
|
425
|
+
) {
|
|
426
|
+
logger.info(`"failFast" enabled -> cancel remaining executions`)
|
|
427
|
+
failFastAbortController.abort()
|
|
428
|
+
}
|
|
429
|
+
},
|
|
200
430
|
})
|
|
201
431
|
|
|
432
|
+
if (stopAfterExecute) {
|
|
433
|
+
stopAfterAllExecutionCallbackList.notify()
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const summaryCounts = reportToSummary(report)
|
|
437
|
+
const summary = {
|
|
438
|
+
executionCount,
|
|
439
|
+
...summaryCounts,
|
|
440
|
+
// when execution is aborted, the remaining executions are "cancelled"
|
|
441
|
+
cancelledCount: executionCount - executionDoneCount,
|
|
442
|
+
duration: Date.now() - startMs,
|
|
443
|
+
}
|
|
444
|
+
if (logSummary) {
|
|
445
|
+
const summaryLog = createSummaryLog(summary)
|
|
446
|
+
rawOutput += stripAnsi(summaryLog)
|
|
447
|
+
logger.info(summaryLog)
|
|
448
|
+
}
|
|
449
|
+
if (summary.executionCount !== summary.completedCount) {
|
|
450
|
+
const logFileUrl = new URL(logFileRelativeUrl, projectDirectoryUrl)
|
|
451
|
+
writeFile(logFileUrl, rawOutput)
|
|
452
|
+
logger.info(`-> ${urlToFileSystemPath(logFileUrl)}`)
|
|
453
|
+
}
|
|
454
|
+
const result = await transformReturnValue({
|
|
455
|
+
summary,
|
|
456
|
+
report,
|
|
457
|
+
})
|
|
202
458
|
return {
|
|
203
459
|
aborted: multipleExecutionsOperation.signal.aborted,
|
|
204
460
|
planSummary: result.summary,
|
|
@@ -209,3 +465,137 @@ export const executePlan = async (
|
|
|
209
465
|
await multipleExecutionsOperation.end()
|
|
210
466
|
}
|
|
211
467
|
}
|
|
468
|
+
|
|
469
|
+
const getExecutionAsSteps = async ({
|
|
470
|
+
plan,
|
|
471
|
+
compileServer,
|
|
472
|
+
multipleExecutionsOperation,
|
|
473
|
+
projectDirectoryUrl,
|
|
474
|
+
}) => {
|
|
475
|
+
try {
|
|
476
|
+
const executionSteps = await generateExecutionSteps(
|
|
477
|
+
{
|
|
478
|
+
...plan,
|
|
479
|
+
[compileServer.jsenvDirectoryRelativeUrl]: null,
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
signal: multipleExecutionsOperation.signal,
|
|
483
|
+
projectDirectoryUrl,
|
|
484
|
+
},
|
|
485
|
+
)
|
|
486
|
+
return executionSteps
|
|
487
|
+
} catch (e) {
|
|
488
|
+
if (Abort.isAbortError(e)) {
|
|
489
|
+
return {
|
|
490
|
+
aborted: true,
|
|
491
|
+
planSummary: {},
|
|
492
|
+
planReport: {},
|
|
493
|
+
planCoverage: null,
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
throw e
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const canOverwriteLogGetter = ({
|
|
501
|
+
completedExecutionLogMerging,
|
|
502
|
+
executionResult,
|
|
503
|
+
}) => {
|
|
504
|
+
if (!completedExecutionLogMerging) {
|
|
505
|
+
return false
|
|
506
|
+
}
|
|
507
|
+
if (executionResult.status === "aborted") {
|
|
508
|
+
return true
|
|
509
|
+
}
|
|
510
|
+
if (executionResult.status !== "completed") {
|
|
511
|
+
return false
|
|
512
|
+
}
|
|
513
|
+
const { consoleCalls = [] } = executionResult
|
|
514
|
+
if (consoleCalls.length > 0) {
|
|
515
|
+
return false
|
|
516
|
+
}
|
|
517
|
+
return true
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const executeInParallel = async ({
|
|
521
|
+
multipleExecutionsOperation,
|
|
522
|
+
maxExecutionsInParallel,
|
|
523
|
+
cooldownBetweenExecutions,
|
|
524
|
+
executionSteps,
|
|
525
|
+
start,
|
|
526
|
+
}) => {
|
|
527
|
+
const executionResults = []
|
|
528
|
+
let progressionIndex = 0
|
|
529
|
+
let remainingExecutionCount = executionSteps.length
|
|
530
|
+
|
|
531
|
+
const nextChunk = async () => {
|
|
532
|
+
if (multipleExecutionsOperation.signal.aborted) {
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
const outputPromiseArray = []
|
|
536
|
+
while (
|
|
537
|
+
remainingExecutionCount > 0 &&
|
|
538
|
+
outputPromiseArray.length < maxExecutionsInParallel
|
|
539
|
+
) {
|
|
540
|
+
remainingExecutionCount--
|
|
541
|
+
const outputPromise = executeOne(progressionIndex)
|
|
542
|
+
progressionIndex++
|
|
543
|
+
outputPromiseArray.push(outputPromise)
|
|
544
|
+
}
|
|
545
|
+
if (outputPromiseArray.length) {
|
|
546
|
+
await Promise.all(outputPromiseArray)
|
|
547
|
+
if (remainingExecutionCount > 0) {
|
|
548
|
+
await nextChunk()
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const executeOne = async (index) => {
|
|
554
|
+
const input = executionSteps[index]
|
|
555
|
+
const output = await start(input)
|
|
556
|
+
if (!multipleExecutionsOperation.signal.aborted) {
|
|
557
|
+
executionResults[index] = output
|
|
558
|
+
}
|
|
559
|
+
if (cooldownBetweenExecutions) {
|
|
560
|
+
await new Promise((resolve) =>
|
|
561
|
+
setTimeout(resolve, cooldownBetweenExecutions),
|
|
562
|
+
)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
await nextChunk()
|
|
567
|
+
|
|
568
|
+
return executionResults
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const reportToSummary = (report) => {
|
|
572
|
+
const fileNames = Object.keys(report)
|
|
573
|
+
const countResultMatching = (predicate) => {
|
|
574
|
+
return fileNames.reduce((previous, fileName) => {
|
|
575
|
+
const fileExecutionResult = report[fileName]
|
|
576
|
+
|
|
577
|
+
return (
|
|
578
|
+
previous +
|
|
579
|
+
Object.keys(fileExecutionResult).filter((executionName) => {
|
|
580
|
+
const fileExecutionResultForRuntime =
|
|
581
|
+
fileExecutionResult[executionName]
|
|
582
|
+
return predicate(fileExecutionResultForRuntime)
|
|
583
|
+
}).length
|
|
584
|
+
)
|
|
585
|
+
}, 0)
|
|
586
|
+
}
|
|
587
|
+
const abortedCount = countResultMatching(({ status }) => status === "aborted")
|
|
588
|
+
const timedoutCount = countResultMatching(
|
|
589
|
+
({ status }) => status === "timedout",
|
|
590
|
+
)
|
|
591
|
+
const erroredCount = countResultMatching(({ status }) => status === "errored")
|
|
592
|
+
const completedCount = countResultMatching(
|
|
593
|
+
({ status }) => status === "completed",
|
|
594
|
+
)
|
|
595
|
+
return {
|
|
596
|
+
abortedCount,
|
|
597
|
+
timedoutCount,
|
|
598
|
+
erroredCount,
|
|
599
|
+
completedCount,
|
|
600
|
+
}
|
|
601
|
+
}
|
|
@@ -56,7 +56,7 @@ const normalizeRuntimeVersion = (version) => {
|
|
|
56
56
|
return version
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
const mergeRuntimeSupport = (runtimeSupport, childRuntimeSupport) => {
|
|
60
60
|
Object.keys(childRuntimeSupport).forEach((runtimeName) => {
|
|
61
61
|
const childRuntimeVersion = normalizeRuntimeVersion(
|
|
62
62
|
childRuntimeSupport[runtimeName],
|
|
@@ -1,440 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs"
|
|
2
|
-
import { memoryUsage } from "node:process"
|
|
3
|
-
import wrapAnsi from "wrap-ansi"
|
|
4
|
-
import cuid from "cuid"
|
|
5
|
-
import { loggerToLevels } from "@jsenv/logger"
|
|
6
|
-
import { createLog, startSpinner } from "@jsenv/log"
|
|
7
|
-
import {
|
|
8
|
-
urlToFileSystemPath,
|
|
9
|
-
resolveUrl,
|
|
10
|
-
writeDirectory,
|
|
11
|
-
ensureEmptyDirectory,
|
|
12
|
-
normalizeStructuredMetaMap,
|
|
13
|
-
urlToMeta,
|
|
14
|
-
} from "@jsenv/filesystem"
|
|
15
|
-
import { Abort, createCallbackListNotifiedOnce } from "@jsenv/abort"
|
|
16
|
-
|
|
17
|
-
import { launchAndExecute } from "../executing/launchAndExecute.js"
|
|
18
|
-
import { reportToCoverage } from "./coverage/reportToCoverage.js"
|
|
19
|
-
import { formatExecuting, formatExecutionResult } from "./executionLogs.js"
|
|
20
|
-
import { createSummaryLog } from "./createSummaryLog.js"
|
|
21
|
-
import { ensureGlobalGc } from "./gc.js"
|
|
22
|
-
|
|
23
|
-
export const executeConcurrently = async (
|
|
24
|
-
executionSteps,
|
|
25
|
-
{
|
|
26
|
-
multipleExecutionsOperation,
|
|
27
|
-
|
|
28
|
-
logger,
|
|
29
|
-
launchAndExecuteLogLevel,
|
|
30
|
-
|
|
31
|
-
projectDirectoryUrl,
|
|
32
|
-
compileServer,
|
|
33
|
-
babelPluginMap,
|
|
34
|
-
|
|
35
|
-
logSummary,
|
|
36
|
-
logMemoryHeapUsage,
|
|
37
|
-
completedExecutionLogMerging,
|
|
38
|
-
completedExecutionLogAbbreviation,
|
|
39
|
-
|
|
40
|
-
maxExecutionsInParallel,
|
|
41
|
-
defaultMsAllocatedPerExecution,
|
|
42
|
-
stopAfterExecute,
|
|
43
|
-
cooldownBetweenExecutions,
|
|
44
|
-
gcBetweenExecutions,
|
|
45
|
-
|
|
46
|
-
coverage,
|
|
47
|
-
coverageConfig,
|
|
48
|
-
coverageIncludeMissing,
|
|
49
|
-
coverageForceIstanbul,
|
|
50
|
-
coverageV8ConflictWarning,
|
|
51
|
-
coverageTempDirectoryRelativeUrl,
|
|
52
|
-
runtimeSupport,
|
|
53
|
-
|
|
54
|
-
beforeExecutionCallback = () => {},
|
|
55
|
-
afterExecutionCallback = () => {},
|
|
56
|
-
},
|
|
57
|
-
) => {
|
|
58
|
-
if (completedExecutionLogMerging && !process.stdout.isTTY) {
|
|
59
|
-
completedExecutionLogMerging = false
|
|
60
|
-
logger.debug(
|
|
61
|
-
`Force completedExecutionLogMerging to false because process.stdout.isTTY is false`,
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
const executionLogsEnabled = loggerToLevels(logger).info
|
|
65
|
-
const executionSpinner = executionLogsEnabled && process.stdout.isTTY
|
|
66
|
-
|
|
67
|
-
const startMs = Date.now()
|
|
68
|
-
const report = {}
|
|
69
|
-
const executionCount = executionSteps.length
|
|
70
|
-
|
|
71
|
-
let transformReturnValue = (value) => value
|
|
72
|
-
|
|
73
|
-
if (gcBetweenExecutions) {
|
|
74
|
-
ensureGlobalGc()
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const coverageTempDirectoryUrl = resolveUrl(
|
|
78
|
-
coverageTempDirectoryRelativeUrl,
|
|
79
|
-
projectDirectoryUrl,
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
const structuredMetaMapForCover = normalizeStructuredMetaMap(
|
|
83
|
-
{
|
|
84
|
-
cover: coverageConfig,
|
|
85
|
-
},
|
|
86
|
-
projectDirectoryUrl,
|
|
87
|
-
)
|
|
88
|
-
const coverageIgnorePredicate = (url) => {
|
|
89
|
-
return !urlToMeta({
|
|
90
|
-
url: resolveUrl(url, projectDirectoryUrl),
|
|
91
|
-
structuredMetaMap: structuredMetaMapForCover,
|
|
92
|
-
}).cover
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (coverage) {
|
|
96
|
-
// in case runned multiple times, we don't want to keep writing lot of files in this directory
|
|
97
|
-
if (!process.env.NODE_V8_COVERAGE) {
|
|
98
|
-
await ensureEmptyDirectory(coverageTempDirectoryUrl)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (runtimeSupport.node) {
|
|
102
|
-
// v8 coverage is written in a directoy and auto propagate to subprocesses
|
|
103
|
-
// through process.env.NODE_V8_COVERAGE.
|
|
104
|
-
if (!coverageForceIstanbul && !process.env.NODE_V8_COVERAGE) {
|
|
105
|
-
const v8CoverageDirectory = resolveUrl(
|
|
106
|
-
`./node_v8/${cuid()}`,
|
|
107
|
-
coverageTempDirectoryUrl,
|
|
108
|
-
)
|
|
109
|
-
await writeDirectory(v8CoverageDirectory, { allowUseless: true })
|
|
110
|
-
process.env.NODE_V8_COVERAGE = urlToFileSystemPath(v8CoverageDirectory)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
transformReturnValue = async (value) => {
|
|
115
|
-
if (multipleExecutionsOperation.signal.aborted) {
|
|
116
|
-
// don't try to do the coverage stuff
|
|
117
|
-
return value
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
value.coverage = await reportToCoverage(value.report, {
|
|
122
|
-
signal: multipleExecutionsOperation.signal,
|
|
123
|
-
logger,
|
|
124
|
-
projectDirectoryUrl,
|
|
125
|
-
babelPluginMap,
|
|
126
|
-
coverageConfig,
|
|
127
|
-
coverageIncludeMissing,
|
|
128
|
-
coverageForceIstanbul,
|
|
129
|
-
coverageIgnorePredicate,
|
|
130
|
-
coverageV8ConflictWarning,
|
|
131
|
-
})
|
|
132
|
-
} catch (e) {
|
|
133
|
-
if (Abort.isAbortError(e)) {
|
|
134
|
-
return value
|
|
135
|
-
}
|
|
136
|
-
throw e
|
|
137
|
-
}
|
|
138
|
-
return value
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
logger.info("")
|
|
143
|
-
let executionLog = createLog({ newLine: "" })
|
|
144
|
-
let abortedCount = 0
|
|
145
|
-
let timedoutCount = 0
|
|
146
|
-
let erroredCount = 0
|
|
147
|
-
let completedCount = 0
|
|
148
|
-
const stopAfterAllExecutionCallbackList = createCallbackListNotifiedOnce()
|
|
149
|
-
|
|
150
|
-
const executionsDone = await executeInParallel({
|
|
151
|
-
multipleExecutionsOperation,
|
|
152
|
-
maxExecutionsInParallel,
|
|
153
|
-
cooldownBetweenExecutions,
|
|
154
|
-
executionSteps,
|
|
155
|
-
start: async (paramsFromStep) => {
|
|
156
|
-
const executionIndex = executionSteps.indexOf(paramsFromStep)
|
|
157
|
-
const { executionName, fileRelativeUrl, runtime } = paramsFromStep
|
|
158
|
-
const runtimeName = runtime.name
|
|
159
|
-
const runtimeVersion = runtime.version
|
|
160
|
-
|
|
161
|
-
const executionParams = {
|
|
162
|
-
// the params below can be overriden by executionDefaultParams
|
|
163
|
-
measurePerformance: false,
|
|
164
|
-
collectPerformance: false,
|
|
165
|
-
captureConsole: true,
|
|
166
|
-
stopAfterExecute,
|
|
167
|
-
stopAfterExecuteReason: "execution-done",
|
|
168
|
-
allocatedMs: defaultMsAllocatedPerExecution,
|
|
169
|
-
...paramsFromStep,
|
|
170
|
-
runtime,
|
|
171
|
-
// mirrorConsole: false because file will be executed in parallel
|
|
172
|
-
// so log would be a mess to read
|
|
173
|
-
mirrorConsole: false,
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const beforeExecutionInfo = {
|
|
177
|
-
fileRelativeUrl,
|
|
178
|
-
runtimeName,
|
|
179
|
-
runtimeVersion,
|
|
180
|
-
executionIndex,
|
|
181
|
-
executionParams,
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
let spinner
|
|
185
|
-
if (executionSpinner) {
|
|
186
|
-
spinner = startSpinner({
|
|
187
|
-
log: executionLog,
|
|
188
|
-
text: formatExecuting(beforeExecutionInfo, {
|
|
189
|
-
executionCount,
|
|
190
|
-
abortedCount,
|
|
191
|
-
timedoutCount,
|
|
192
|
-
erroredCount,
|
|
193
|
-
completedCount,
|
|
194
|
-
...(logMemoryHeapUsage
|
|
195
|
-
? { memoryHeap: memoryUsage().heapUsed }
|
|
196
|
-
: {}),
|
|
197
|
-
}),
|
|
198
|
-
})
|
|
199
|
-
}
|
|
200
|
-
beforeExecutionCallback(beforeExecutionInfo)
|
|
201
|
-
|
|
202
|
-
const filePath = urlToFileSystemPath(
|
|
203
|
-
`${projectDirectoryUrl}${fileRelativeUrl}`,
|
|
204
|
-
)
|
|
205
|
-
let executionResult
|
|
206
|
-
if (existsSync(filePath)) {
|
|
207
|
-
executionResult = await launchAndExecute({
|
|
208
|
-
signal: multipleExecutionsOperation.signal,
|
|
209
|
-
launchAndExecuteLogLevel,
|
|
210
|
-
|
|
211
|
-
...executionParams,
|
|
212
|
-
collectCoverage: coverage,
|
|
213
|
-
coverageTempDirectoryUrl,
|
|
214
|
-
runtimeParams: {
|
|
215
|
-
projectDirectoryUrl,
|
|
216
|
-
compileServerOrigin: compileServer.origin,
|
|
217
|
-
compileServerId: compileServer.id,
|
|
218
|
-
jsenvDirectoryRelativeUrl: compileServer.jsenvDirectoryRelativeUrl,
|
|
219
|
-
|
|
220
|
-
collectCoverage: coverage,
|
|
221
|
-
coverageIgnorePredicate,
|
|
222
|
-
coverageForceIstanbul,
|
|
223
|
-
stopAfterAllExecutionCallbackList,
|
|
224
|
-
...executionParams.runtimeParams,
|
|
225
|
-
},
|
|
226
|
-
executeParams: {
|
|
227
|
-
fileRelativeUrl,
|
|
228
|
-
...executionParams.executeParams,
|
|
229
|
-
},
|
|
230
|
-
coverageV8ConflictWarning,
|
|
231
|
-
})
|
|
232
|
-
} else {
|
|
233
|
-
executionResult = {
|
|
234
|
-
status: "errored",
|
|
235
|
-
error: new Error(
|
|
236
|
-
`No file at ${fileRelativeUrl} for execution "${executionName}"`,
|
|
237
|
-
),
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
if (fileRelativeUrl in report === false) {
|
|
241
|
-
report[fileRelativeUrl] = {}
|
|
242
|
-
}
|
|
243
|
-
report[fileRelativeUrl][executionName] = executionResult
|
|
244
|
-
const afterExecutionInfo = {
|
|
245
|
-
...beforeExecutionInfo,
|
|
246
|
-
endMs: Date.now(),
|
|
247
|
-
executionResult,
|
|
248
|
-
}
|
|
249
|
-
afterExecutionCallback(afterExecutionInfo)
|
|
250
|
-
|
|
251
|
-
if (executionResult.status === "aborted") {
|
|
252
|
-
abortedCount++
|
|
253
|
-
} else if (executionResult.status === "timedout") {
|
|
254
|
-
timedoutCount++
|
|
255
|
-
} else if (executionResult.status === "errored") {
|
|
256
|
-
erroredCount++
|
|
257
|
-
} else if (executionResult.status === "completed") {
|
|
258
|
-
completedCount++
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (gcBetweenExecutions) {
|
|
262
|
-
global.gc()
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (executionLogsEnabled) {
|
|
266
|
-
let log = formatExecutionResult(afterExecutionInfo, {
|
|
267
|
-
completedExecutionLogAbbreviation,
|
|
268
|
-
executionCount,
|
|
269
|
-
abortedCount,
|
|
270
|
-
timedoutCount,
|
|
271
|
-
erroredCount,
|
|
272
|
-
completedCount,
|
|
273
|
-
...(logMemoryHeapUsage ? { memoryHeap: memoryUsage().heapUsed } : {}),
|
|
274
|
-
})
|
|
275
|
-
log = `${log}
|
|
276
|
-
|
|
277
|
-
`
|
|
278
|
-
const { columns = 80 } = process.stdout
|
|
279
|
-
log = wrapAnsi(log, columns, {
|
|
280
|
-
trim: false,
|
|
281
|
-
hard: true,
|
|
282
|
-
wordWrap: false,
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
// replace spinner with this execution result
|
|
286
|
-
if (spinner) spinner.stop()
|
|
287
|
-
executionLog.write(log)
|
|
288
|
-
|
|
289
|
-
const canOverwriteLog = canOverwriteLogGetter({
|
|
290
|
-
completedExecutionLogMerging,
|
|
291
|
-
executionResult,
|
|
292
|
-
})
|
|
293
|
-
if (canOverwriteLog) {
|
|
294
|
-
// nothing to do, we reuse the current executionLog object
|
|
295
|
-
} else {
|
|
296
|
-
executionLog.destroy()
|
|
297
|
-
executionLog = createLog({ newLine: "" })
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
},
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
if (stopAfterExecute) {
|
|
304
|
-
stopAfterAllExecutionCallbackList.notify()
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const summaryCounts = reportToSummary(report)
|
|
308
|
-
|
|
309
|
-
const summary = {
|
|
310
|
-
executionCount,
|
|
311
|
-
...summaryCounts,
|
|
312
|
-
// when execution is aborted, the remaining executions are "cancelled"
|
|
313
|
-
cancelledCount:
|
|
314
|
-
executionCount -
|
|
315
|
-
executionsDone.length -
|
|
316
|
-
// we substract abortedCount because they are not pushed into executionsDone
|
|
317
|
-
summaryCounts.abortedCount,
|
|
318
|
-
duration: Date.now() - startMs,
|
|
319
|
-
}
|
|
320
|
-
if (logSummary) {
|
|
321
|
-
logger.info(createSummaryLog(summary))
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return transformReturnValue({
|
|
325
|
-
summary,
|
|
326
|
-
report,
|
|
327
|
-
})
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const canOverwriteLogGetter = ({
|
|
331
|
-
completedExecutionLogMerging,
|
|
332
|
-
executionResult,
|
|
333
|
-
}) => {
|
|
334
|
-
if (!completedExecutionLogMerging) {
|
|
335
|
-
return false
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (executionResult.status === "aborted") {
|
|
339
|
-
return true
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (executionResult.status !== "completed") {
|
|
343
|
-
return false
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const { consoleCalls = [] } = executionResult
|
|
347
|
-
if (consoleCalls.length > 0) {
|
|
348
|
-
return false
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return true
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const executeInParallel = async ({
|
|
355
|
-
multipleExecutionsOperation,
|
|
356
|
-
executionSteps,
|
|
357
|
-
start,
|
|
358
|
-
maxExecutionsInParallel = 1,
|
|
359
|
-
cooldownBetweenExecutions,
|
|
360
|
-
}) => {
|
|
361
|
-
const executionResults = []
|
|
362
|
-
let progressionIndex = 0
|
|
363
|
-
let remainingExecutionCount = executionSteps.length
|
|
364
|
-
|
|
365
|
-
const nextChunk = async () => {
|
|
366
|
-
if (multipleExecutionsOperation.signal.aborted) {
|
|
367
|
-
return
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const outputPromiseArray = []
|
|
371
|
-
while (
|
|
372
|
-
remainingExecutionCount > 0 &&
|
|
373
|
-
outputPromiseArray.length < maxExecutionsInParallel
|
|
374
|
-
) {
|
|
375
|
-
remainingExecutionCount--
|
|
376
|
-
const outputPromise = executeOne(progressionIndex)
|
|
377
|
-
progressionIndex++
|
|
378
|
-
outputPromiseArray.push(outputPromise)
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
if (outputPromiseArray.length) {
|
|
382
|
-
await Promise.all(outputPromiseArray)
|
|
383
|
-
if (remainingExecutionCount > 0) {
|
|
384
|
-
await nextChunk()
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const executeOne = async (index) => {
|
|
390
|
-
const input = executionSteps[index]
|
|
391
|
-
const output = await start(input)
|
|
392
|
-
if (!multipleExecutionsOperation.signal.aborted) {
|
|
393
|
-
executionResults[index] = output
|
|
394
|
-
}
|
|
395
|
-
if (cooldownBetweenExecutions) {
|
|
396
|
-
await new Promise((resolve) =>
|
|
397
|
-
setTimeout(resolve, cooldownBetweenExecutions),
|
|
398
|
-
)
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
await nextChunk()
|
|
403
|
-
|
|
404
|
-
return executionResults
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const reportToSummary = (report) => {
|
|
408
|
-
const fileNames = Object.keys(report)
|
|
409
|
-
|
|
410
|
-
const countResultMatching = (predicate) => {
|
|
411
|
-
return fileNames.reduce((previous, fileName) => {
|
|
412
|
-
const fileExecutionResult = report[fileName]
|
|
413
|
-
|
|
414
|
-
return (
|
|
415
|
-
previous +
|
|
416
|
-
Object.keys(fileExecutionResult).filter((executionName) => {
|
|
417
|
-
const fileExecutionResultForRuntime =
|
|
418
|
-
fileExecutionResult[executionName]
|
|
419
|
-
return predicate(fileExecutionResultForRuntime)
|
|
420
|
-
}).length
|
|
421
|
-
)
|
|
422
|
-
}, 0)
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const abortedCount = countResultMatching(({ status }) => status === "aborted")
|
|
426
|
-
const timedoutCount = countResultMatching(
|
|
427
|
-
({ status }) => status === "timedout",
|
|
428
|
-
)
|
|
429
|
-
const erroredCount = countResultMatching(({ status }) => status === "errored")
|
|
430
|
-
const completedCount = countResultMatching(
|
|
431
|
-
({ status }) => status === "completed",
|
|
432
|
-
)
|
|
433
|
-
|
|
434
|
-
return {
|
|
435
|
-
abortedCount,
|
|
436
|
-
timedoutCount,
|
|
437
|
-
erroredCount,
|
|
438
|
-
completedCount,
|
|
439
|
-
}
|
|
440
|
-
}
|