@jsenv/test 3.3.12 → 3.3.14

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.
Files changed (32) hide show
  1. package/dist/js/execute_using_dynamic_import.js +58 -58
  2. package/dist/jsenv_test.js +1476 -1464
  3. package/dist/node_child_process_controlled.mjs +1 -1
  4. package/dist/node_worker_thread_controlled.mjs +2 -2
  5. package/package.json +8 -8
  6. package/src/coverage/empty_coverage_factory.js +2 -2
  7. package/src/coverage/file_by_file_coverage.js +1 -1
  8. package/src/coverage/generate_coverage.js +6 -6
  9. package/src/coverage/missing_coverage.js +1 -1
  10. package/src/coverage/report_coverage_as_html.js +3 -3
  11. package/src/coverage/report_coverage_as_json.js +1 -1
  12. package/src/coverage/v8_coverage_node_directory.js +2 -2
  13. package/src/coverage/v8_coverage_to_istanbul.js +1 -1
  14. package/src/execution/exception.js +1 -1
  15. package/src/execution/execute.js +1 -1
  16. package/src/execution/execute_test_plan.js +20 -20
  17. package/src/execution/format_error_for_terminal.js +2 -2
  18. package/src/execution/reporters/reporter_list.js +21 -9
  19. package/src/execution/run.js +1 -1
  20. package/src/execution/web_server/start_using_command.js +1 -1
  21. package/src/execution/web_server_param.js +3 -3
  22. package/src/helpers/cpu_usage.test_manual.js +1 -1
  23. package/src/helpers/web_url_converter.js +1 -1
  24. package/src/main.js +4 -2
  25. package/src/runtime_browsers/middleware_istanbul.js +2 -2
  26. package/src/runtime_browsers/middleware_js_supervisor.js +2 -2
  27. package/src/runtime_browsers/using_playwright.js +5 -5
  28. package/src/runtime_node/execute_using_dynamic_import.js +2 -2
  29. package/src/runtime_node/importmap_hooks.mjs +1 -1
  30. package/src/runtime_node/node_child_process.js +4 -4
  31. package/src/runtime_node/node_worker_thread.js +3 -3
  32. package/src/runtime_node/node_worker_thread_controlled.mjs +1 -1
@@ -1,70 +1,26 @@
1
- import os, { cpus, release, totalmem, availableParallelism, freemem } from "node:os";
2
1
  import process$1, { cpuUsage, memoryUsage } from "node:process";
3
- import { readdir, chmod, stat, lstat, chmodSync, statSync, lstatSync, promises, readFile as readFile$1, readFileSync, writeFileSync as writeFileSync$1, mkdirSync, unlink, openSync, closeSync, rmdir, unlinkSync, readdirSync, rmdirSync, existsSync } from "node:fs";
4
- import { takeCoverage } from "node:v8";
5
- import stripAnsi from "strip-ansi";
6
- import { URL_META, createException } from "./js/exception.js";
2
+ import os, { cpus, release, totalmem, availableParallelism, freemem } from "node:os";
7
3
  import tty from "node:tty";
8
4
  import stringWidth from "string-width";
9
5
  import { pathToFileURL, fileURLToPath } from "node:url";
6
+ import { URL_META, createException } from "./js/exception.js";
7
+ import { readdir, chmod, stat, lstat, chmodSync, statSync, lstatSync, promises, readFile as readFile$1, readFileSync, writeFileSync as writeFileSync$1, mkdirSync, unlink, openSync, closeSync, rmdir, unlinkSync, readdirSync, rmdirSync, existsSync } from "node:fs";
10
8
  import { dirname } from "node:path";
11
9
  import crypto from "node:crypto";
12
10
  import { readGitHubWorkflowEnv, startGithubCheckRun } from "@jsenv/github-check-run";
13
- import { filterV8Coverage } from "./js/v8_coverage.js";
11
+ import { takeCoverage } from "node:v8";
12
+ import stripAnsi from "strip-ansi";
14
13
  import { createRequire } from "node:module";
15
14
  import { applyBabelPlugins } from "@jsenv/ast";
16
- import { spawn, spawnSync, fork } from "node:child_process";
15
+ import { filterV8Coverage } from "./js/v8_coverage.js";
17
16
  import { createServer } from "node:net";
18
- import { injectSupervisorIntoHTML, supervisorFileUrl } from "@jsenv/plugin-supervisor";
17
+ import { spawn, spawnSync, fork } from "node:child_process";
19
18
  import { SOURCEMAP, generateSourcemapDataUrl } from "@jsenv/sourcemap";
19
+ import { injectSupervisorIntoHTML, supervisorFileUrl } from "@jsenv/plugin-supervisor";
20
20
  import { findFreePort } from "@jsenv/server";
21
21
  import { Worker } from "node:worker_threads";
22
22
  import he from "he";
23
23
 
24
- /*
25
- * See callback_race.md
26
- */
27
-
28
- const raceCallbacks = (raceDescription, winnerCallback) => {
29
- let cleanCallbacks = [];
30
- let status = "racing";
31
-
32
- const clean = () => {
33
- cleanCallbacks.forEach((clean) => {
34
- clean();
35
- });
36
- cleanCallbacks = null;
37
- };
38
-
39
- const cancel = () => {
40
- if (status !== "racing") {
41
- return;
42
- }
43
- status = "cancelled";
44
- clean();
45
- };
46
-
47
- Object.keys(raceDescription).forEach((candidateName) => {
48
- const register = raceDescription[candidateName];
49
- const returnValue = register((data) => {
50
- if (status !== "racing") {
51
- return;
52
- }
53
- status = "done";
54
- clean();
55
- winnerCallback({
56
- name: candidateName,
57
- data,
58
- });
59
- });
60
- if (typeof returnValue === "function") {
61
- cleanCallbacks.push(returnValue);
62
- }
63
- });
64
-
65
- return cancel;
66
- };
67
-
68
24
  const createCallbackListNotifiedOnce = () => {
69
25
  let callbacks = [];
70
26
  let status = "waiting";
@@ -177,6 +133,50 @@ const emitCallbackDuplicationWarning = () => {
177
133
 
178
134
  const removeNoop = () => {};
179
135
 
136
+ /*
137
+ * See callback_race.md
138
+ */
139
+
140
+ const raceCallbacks = (raceDescription, winnerCallback) => {
141
+ let cleanCallbacks = [];
142
+ let status = "racing";
143
+
144
+ const clean = () => {
145
+ cleanCallbacks.forEach((clean) => {
146
+ clean();
147
+ });
148
+ cleanCallbacks = null;
149
+ };
150
+
151
+ const cancel = () => {
152
+ if (status !== "racing") {
153
+ return;
154
+ }
155
+ status = "cancelled";
156
+ clean();
157
+ };
158
+
159
+ Object.keys(raceDescription).forEach((candidateName) => {
160
+ const register = raceDescription[candidateName];
161
+ const returnValue = register((data) => {
162
+ if (status !== "racing") {
163
+ return;
164
+ }
165
+ status = "done";
166
+ clean();
167
+ winnerCallback({
168
+ name: candidateName,
169
+ data,
170
+ });
171
+ });
172
+ if (typeof returnValue === "function") {
173
+ cleanCallbacks.push(returnValue);
174
+ }
175
+ });
176
+
177
+ return cancel;
178
+ };
179
+
180
180
  /*
181
181
  * https://github.com/whatwg/dom/issues/920
182
182
  */
@@ -3097,262 +3097,305 @@ process.platform === "win32";
3097
3097
 
3098
3098
  process.platform === "linux";
3099
3099
 
3100
- // https://gist.github.com/GaetanoPiazzolla/c40e1ebb9f709d091208e89baf9f4e00
3101
-
3100
+ const normalizeFileByFileCoveragePaths = (
3101
+ fileByFileCoverage,
3102
+ rootDirectoryUrl,
3103
+ ) => {
3104
+ const fileByFileNormalized = {};
3105
+ Object.keys(fileByFileCoverage).forEach((key) => {
3106
+ const fileCoverage = fileByFileCoverage[key];
3107
+ const { path } = fileCoverage;
3108
+ const url = isFileSystemPath(path)
3109
+ ? fileSystemPathToUrl(path)
3110
+ : new URL(path, rootDirectoryUrl).href;
3111
+ const relativeUrl = urlToRelativeUrl(url, rootDirectoryUrl);
3112
+ fileByFileNormalized[`./${relativeUrl}`] = {
3113
+ ...fileCoverage,
3114
+ path: `./${relativeUrl}`,
3115
+ };
3116
+ });
3117
+ return fileByFileNormalized;
3118
+ };
3102
3119
 
3103
- const startMeasuringTotalCpuUsage = () => {
3104
- let previousCpuArray = cpus();
3105
- let previousMs = Date.now();
3106
- let previousCpuUsage = cpuUsage();
3120
+ const importWithRequire = createRequire(import.meta.url);
3107
3121
 
3108
- const overall = {
3109
- inactive: 100,
3110
- active: 0,
3111
- system: 0,
3112
- user: 0,
3113
- };
3114
- const thisProcess = {
3115
- active: 0,
3116
- system: 0,
3117
- user: 0,
3118
- };
3119
- const details = previousCpuArray.map(() => {
3120
- return {
3121
- inactive: 100,
3122
- active: 0,
3123
- system: 0,
3124
- user: 0,
3125
- };
3122
+ const composeTwoFileByFileIstanbulCoverages = (
3123
+ firstFileByFileIstanbulCoverage,
3124
+ secondFileByFileIstanbulCoverage,
3125
+ ) => {
3126
+ const fileByFileIstanbulCoverage = {};
3127
+ Object.keys(firstFileByFileIstanbulCoverage).forEach((key) => {
3128
+ fileByFileIstanbulCoverage[key] = firstFileByFileIstanbulCoverage[key];
3129
+ });
3130
+ Object.keys(secondFileByFileIstanbulCoverage).forEach((key) => {
3131
+ const firstCoverage = firstFileByFileIstanbulCoverage[key];
3132
+ const secondCoverage = secondFileByFileIstanbulCoverage[key];
3133
+ fileByFileIstanbulCoverage[key] = firstCoverage
3134
+ ? merge(firstCoverage, secondCoverage)
3135
+ : secondCoverage;
3126
3136
  });
3127
3137
 
3128
- const samples = [];
3129
- const interval = setInterval(() => {
3130
- let cpuArray = cpus();
3131
- const ms = Date.now();
3132
- const ellapsedMs = ms - previousMs;
3133
- const cpuUsageSampleArray = [];
3134
- let overallSystemMs = 0;
3135
- let overallUserMs = 0;
3136
- let overallInactiveMs = 0;
3137
- let overallActiveMs = 0;
3138
- let overallMsEllapsed = 0;
3139
- let index = 0;
3140
- for (const cpu of cpuArray) {
3141
- const previousCpuTimes = previousCpuArray[index].times;
3142
- const cpuTimes = cpu.times;
3143
- const systemMs = cpuTimes.sys - previousCpuTimes.sys;
3144
- const userMs = cpuTimes.user - previousCpuTimes.user;
3145
- const activeMs = systemMs + userMs;
3146
- const inactiveMs = ellapsedMs - activeMs;
3147
- const cpuUsageSample = {
3148
- inactive: inactiveMs / ellapsedMs,
3149
- active: activeMs / ellapsedMs,
3150
- system: systemMs / ellapsedMs,
3151
- user: userMs / ellapsedMs,
3152
- };
3153
- cpuUsageSampleArray.push(cpuUsageSample);
3138
+ return fileByFileIstanbulCoverage;
3139
+ };
3154
3140
 
3155
- overallSystemMs += systemMs;
3156
- overallUserMs += userMs;
3157
- overallInactiveMs += inactiveMs;
3158
- overallActiveMs += activeMs;
3159
- overallMsEllapsed += ellapsedMs;
3160
- index++;
3161
- }
3162
- const overallUsageSample = {
3163
- inactive: overallInactiveMs / overallMsEllapsed,
3164
- active: overallActiveMs / overallMsEllapsed,
3165
- system: overallSystemMs / overallMsEllapsed,
3166
- user: overallUserMs / overallMsEllapsed,
3167
- };
3168
- previousCpuArray = cpuArray;
3169
- previousMs = ms;
3141
+ const merge = (firstIstanbulCoverage, secondIstanbulCoverage) => {
3142
+ const { createFileCoverage } = importWithRequire("istanbul-lib-coverage");
3143
+ const istanbulFileCoverageObject = createFileCoverage(firstIstanbulCoverage);
3144
+ istanbulFileCoverageObject.merge(secondIstanbulCoverage);
3145
+ const istanbulCoverage = istanbulFileCoverageObject.toJSON();
3146
+ return istanbulCoverage;
3147
+ };
3170
3148
 
3171
- const processCpuUsage = cpuUsage();
3172
- const thisProcessSystemMs = Math.round(
3173
- (processCpuUsage.system - previousCpuUsage.system) / 1000,
3174
- );
3175
- const thisProcessUserMs = Math.round(
3176
- (processCpuUsage.user - previousCpuUsage.user) / 1000,
3177
- );
3178
- previousCpuUsage = processCpuUsage;
3149
+ // https://github.com/istanbuljs/babel-plugin-istanbul/blob/321740f7b25d803f881466ea819d870f7ed6a254/src/index.js
3179
3150
 
3180
- const thisProcessActiveMs = thisProcessSystemMs + thisProcessUserMs;
3181
- const thisProcessInactiveMs = overallMsEllapsed - thisProcessActiveMs;
3182
- const thisProcessSample = {
3183
- inactive: thisProcessInactiveMs / overallMsEllapsed,
3184
- active: thisProcessActiveMs / overallMsEllapsed,
3185
- system: thisProcessSystemMs / overallMsEllapsed,
3186
- user: thisProcessUserMs / overallMsEllapsed,
3187
- };
3188
- samples.push({
3189
- cpuUsageSampleArray,
3190
- overallUsageSample,
3191
- thisProcessSample,
3192
- });
3193
- if (samples.length === 10) {
3194
- {
3195
- let index = 0;
3196
- for (const detail of details) {
3197
- let systemSum = 0;
3198
- let userSum = 0;
3199
- let inactiveSum = 0;
3200
- let activeSum = 0;
3201
- for (const sample of samples) {
3202
- const { cpuUsageSampleArray } = sample;
3203
- const cpuUsageSample = cpuUsageSampleArray[index];
3204
- inactiveSum += cpuUsageSample.inactive;
3205
- activeSum += cpuUsageSample.active;
3206
- systemSum += cpuUsageSample.system;
3207
- userSum += cpuUsageSample.user;
3208
- }
3209
- Object.assign(detail, {
3210
- inactive: inactiveSum / samples.length,
3211
- active: activeSum / samples.length,
3212
- system: systemSum / samples.length,
3213
- user: userSum / samples.length,
3214
- });
3215
- index++;
3216
- }
3217
- }
3218
- {
3219
- let overallSystemSum = 0;
3220
- let overallUserSum = 0;
3221
- let overallInactiveSum = 0;
3222
- let overallActiveSum = 0;
3223
- for (const sample of samples) {
3224
- const { overallUsageSample } = sample;
3225
- overallSystemSum += overallUsageSample.system;
3226
- overallUserSum += overallUsageSample.user;
3227
- overallInactiveSum += overallUsageSample.inactive;
3228
- overallActiveSum += overallUsageSample.active;
3229
- }
3230
- Object.assign(overall, {
3231
- inactive: overallInactiveSum / samples.length,
3232
- active: overallActiveSum / samples.length,
3233
- system: overallSystemSum / samples.length,
3234
- user: overallUserSum / samples.length,
3235
- });
3236
- }
3237
- {
3238
- let thisProcessSystemSum = 0;
3239
- let thisProcessUserSum = 0;
3240
- let thisProcessInactiveSum = 0;
3241
- let thisProcessActiveSum = 0;
3242
- for (const sample of samples) {
3243
- const { thisProcessSample } = sample;
3244
- thisProcessSystemSum += thisProcessSample.system;
3245
- thisProcessUserSum += thisProcessSample.user;
3246
- thisProcessInactiveSum += thisProcessSample.inactive;
3247
- thisProcessActiveSum += thisProcessSample.active;
3248
- }
3249
- Object.assign(thisProcess, {
3250
- inactive: thisProcessInactiveSum / samples.length,
3251
- active: thisProcessActiveSum / samples.length,
3252
- system: thisProcessSystemSum / samples.length,
3253
- user: thisProcessUserSum / samples.length,
3254
- });
3255
- }
3256
- samples.length = 0;
3257
- }
3258
- }, 15);
3259
- interval.unref();
3151
+ const babelPluginInstrument = (api, { useInlineSourceMaps = false }) => {
3152
+ const { programVisitor } = importWithRequire("istanbul-lib-instrument");
3153
+ const { types } = api;
3260
3154
 
3261
3155
  return {
3262
- overall,
3263
- thisProcess,
3264
- details,
3265
- stop: () => {
3266
- clearInterval(interval);
3156
+ name: "transform-instrument",
3157
+ visitor: {
3158
+ Program: {
3159
+ enter(path, state) {
3160
+ const { file } = state;
3161
+ const { opts } = file;
3162
+ let inputSourceMap;
3163
+ if (useInlineSourceMaps) {
3164
+ // https://github.com/istanbuljs/babel-plugin-istanbul/commit/a9e15643d249a2985e4387e4308022053b2cd0ad#diff-1fdf421c05c1140f6d71444ea2b27638R65
3165
+ inputSourceMap =
3166
+ opts.inputSourceMap || file.inputMap
3167
+ ? file.inputMap.sourcemap
3168
+ : null;
3169
+ } else {
3170
+ inputSourceMap = opts.inputSourceMap;
3171
+ }
3172
+ const __dv__ = programVisitor(
3173
+ types,
3174
+ opts.filenameRelative || opts.filename,
3175
+ {
3176
+ coverageVariable: "__coverage__",
3177
+ inputSourceMap,
3178
+ },
3179
+ );
3180
+ __dv__.enter(path);
3181
+ file.metadata.__dv__ = __dv__;
3182
+ },
3183
+
3184
+ exit(path, state) {
3185
+ const { __dv__ } = state.file.metadata;
3186
+ if (!__dv__) {
3187
+ return;
3188
+ }
3189
+ const object = __dv__.exit(path);
3190
+ // object got two properties: fileCoverage and sourceMappingURL
3191
+ state.file.metadata.coverage = object.fileCoverage;
3192
+ },
3193
+ },
3267
3194
  },
3268
3195
  };
3269
3196
  };
3270
3197
 
3271
- const createCallOrderer = () => {
3272
- const queue = [];
3273
- const callWhenPreviousExecutionAreDone = (executionIndex, callback) => {
3274
- if (queue[executionIndex]) {
3275
- throw new Error(`${executionIndex} already used`);
3276
- }
3198
+ const relativeUrlToEmptyCoverage = async (
3199
+ relativeUrl,
3200
+ { signal, rootDirectoryUrl },
3201
+ ) => {
3202
+ const operation = Abort.startOperation();
3203
+ operation.addAbortSignal(signal);
3277
3204
 
3278
- let allBeforeAreDone = true;
3279
- if (executionIndex > 0) {
3280
- let beforeIndex = executionIndex - 1;
3281
- do {
3282
- const value = queue[beforeIndex];
3283
- if (!value) {
3284
- allBeforeAreDone = false;
3285
- break;
3286
- }
3287
- } while (beforeIndex--);
3288
- }
3289
- if (!allBeforeAreDone) {
3290
- queue[executionIndex] = callback;
3291
- return;
3205
+ try {
3206
+ const fileUrl = resolveUrl(relativeUrl, rootDirectoryUrl);
3207
+ const content = await readFile(fileUrl, { as: "string" });
3208
+
3209
+ operation.throwIfAborted();
3210
+ const { metadata } = await applyBabelPlugins({
3211
+ babelPlugins: [babelPluginInstrument],
3212
+ input: content,
3213
+ inputIsJsModule: true,
3214
+ inputUrl: fileUrl,
3215
+ });
3216
+ const { coverage } = metadata;
3217
+ if (!coverage) {
3218
+ throw new Error(`missing coverage for file`);
3292
3219
  }
3293
- queue[executionIndex] = true;
3294
- callback();
3295
- let afterIndex = executionIndex + 1;
3296
- while (afterIndex < queue.length) {
3297
- const value = queue[afterIndex];
3298
- if (value === undefined) {
3299
- break;
3300
- }
3301
- if (typeof value === "function") {
3302
- queue[afterIndex] = true;
3303
- value();
3304
- }
3305
- afterIndex++;
3220
+ // https://github.com/gotwarlost/istanbul/blob/bc84c315271a5dd4d39bcefc5925cfb61a3d174a/lib/command/common/run-with-cover.js#L229
3221
+ Object.keys(coverage.s).forEach(function (key) {
3222
+ coverage.s[key] = 0;
3223
+ });
3224
+ return coverage;
3225
+ } catch (e) {
3226
+ if (e && e.code === "PARSE_ERROR") {
3227
+ // return an empty coverage for that file when
3228
+ // it contains a syntax error
3229
+ return createEmptyCoverage(relativeUrl);
3306
3230
  }
3307
- };
3308
- return callWhenPreviousExecutionAreDone;
3231
+ throw e;
3232
+ } finally {
3233
+ await operation.end();
3234
+ }
3309
3235
  };
3310
3236
 
3311
- const readNodeV8CoverageDirectory = async ({
3312
- logger,
3313
- warn,
3237
+ const createEmptyCoverage = (relativeUrl) => {
3238
+ const { createFileCoverage } = importWithRequire("istanbul-lib-coverage");
3239
+ return createFileCoverage(relativeUrl).toJSON();
3240
+ };
3241
+
3242
+ const listRelativeFileUrlToCover = async ({
3314
3243
  signal,
3315
- onV8Coverage,
3316
- maxMsWaitingForNodeToWriteCoverageFile = 2000,
3244
+ rootDirectoryUrl,
3245
+ coverageInclude,
3317
3246
  }) => {
3318
- const NODE_V8_COVERAGE = process.env.NODE_V8_COVERAGE;
3319
- const operation = Abort.startOperation();
3320
- operation.addAbortSignal(signal);
3247
+ const matchingFileResultArray = await collectFiles({
3248
+ signal,
3249
+ directoryUrl: rootDirectoryUrl,
3250
+ associations: { cover: coverageInclude },
3251
+ predicate: ({ cover }) => cover,
3252
+ });
3253
+ return matchingFileResultArray.map(({ relativeUrl }) => relativeUrl);
3254
+ };
3321
3255
 
3322
- let timeSpentTrying = 0;
3323
- const tryReadDirectory = async () => {
3324
- const dirContent = readdirSync(NODE_V8_COVERAGE);
3325
- if (dirContent.length > 0) {
3326
- return dirContent;
3327
- }
3328
- if (timeSpentTrying < maxMsWaitingForNodeToWriteCoverageFile) {
3329
- await new Promise((resolve) => setTimeout(resolve, 200));
3330
- timeSpentTrying += 200;
3331
- logger.debug("retry to read coverage directory");
3332
- return tryReadDirectory();
3333
- }
3334
- warn({
3335
- code: "V8_COVERAGE_EMPTY",
3336
- message: `v8 coverage directory is empty at ${NODE_V8_COVERAGE}`,
3337
- });
3338
- return dirContent;
3339
- };
3256
+ const getMissingFileByFileCoverage = async ({
3257
+ signal,
3258
+ rootDirectoryUrl,
3259
+ coverageInclude,
3260
+ fileByFileCoverage,
3261
+ }) => {
3262
+ const relativeUrlsToCover = await listRelativeFileUrlToCover({
3263
+ signal,
3264
+ rootDirectoryUrl,
3265
+ coverageInclude,
3266
+ });
3267
+ const relativeUrlsMissing = relativeUrlsToCover.filter((relativeUrlToCover) =>
3268
+ Object.keys(fileByFileCoverage).every((key) => {
3269
+ return key !== `./${relativeUrlToCover}`;
3270
+ }),
3271
+ );
3340
3272
 
3341
- try {
3273
+ const operation = Abort.startOperation();
3274
+ operation.addAbortSignal(signal);
3275
+ const missingFileByFileCoverage = {};
3276
+ await relativeUrlsMissing.reduce(async (previous, relativeUrlMissing) => {
3342
3277
  operation.throwIfAborted();
3343
- const dirContent = await tryReadDirectory();
3344
-
3345
- const coverageDirectoryUrl = assertAndNormalizeDirectoryUrl(
3346
- NODE_V8_COVERAGE,
3347
- "NODE_V8_COVERAGE",
3348
- );
3278
+ await previous;
3279
+ await operation.withSignal(async (signal) => {
3280
+ const emptyCoverage = await relativeUrlToEmptyCoverage(
3281
+ relativeUrlMissing,
3282
+ {
3283
+ signal,
3284
+ rootDirectoryUrl,
3285
+ },
3286
+ );
3287
+ missingFileByFileCoverage[`./${relativeUrlMissing}`] = emptyCoverage;
3288
+ });
3289
+ }, Promise.resolve());
3290
+ return missingFileByFileCoverage;
3291
+ };
3349
3292
 
3350
- await dirContent.reduce(async (previous, dirEntry) => {
3351
- operation.throwIfAborted();
3352
- await previous;
3293
+ const composeV8AndIstanbul = (
3294
+ v8FileByFileCoverage,
3295
+ istanbulFileByFileCoverage,
3296
+ { warn, v8ConflictWarning },
3297
+ ) => {
3298
+ const fileByFileCoverage = {};
3299
+ const v8Files = Object.keys(v8FileByFileCoverage);
3300
+ const istanbulFiles = Object.keys(istanbulFileByFileCoverage);
3353
3301
 
3354
- const dirEntryUrl = new URL(dirEntry, coverageDirectoryUrl);
3355
- const tryReadJsonFile = async () => {
3302
+ v8Files.forEach((key) => {
3303
+ fileByFileCoverage[key] = v8FileByFileCoverage[key];
3304
+ });
3305
+ istanbulFiles.forEach((key) => {
3306
+ const v8Coverage = v8FileByFileCoverage[key];
3307
+ if (v8Coverage) {
3308
+ if (v8ConflictWarning) {
3309
+ warn({
3310
+ code: "V8_COVERAGE_CONFLICT",
3311
+ message: createDetailedMessage(
3312
+ `Coverage conflict on "${key}", found two coverage that cannot be merged together: v8 and istanbul. The istanbul coverage will be ignored.`,
3313
+ {
3314
+ "details": `This happens when a file is executed on a runtime using v8 coverage (node or chromium) and on runtime using istanbul coverage (firefox or webkit)`,
3315
+ "suggestion":
3316
+ "disable this warning with coverage.v8ConflictWarning: false",
3317
+ "suggestion 2": `force coverage using istanbul with coverage.methodForBrowsers: "istanbul"`,
3318
+ },
3319
+ ),
3320
+ });
3321
+ }
3322
+ fileByFileCoverage[key] = v8Coverage;
3323
+ } else {
3324
+ fileByFileCoverage[key] = istanbulFileByFileCoverage[key];
3325
+ }
3326
+ });
3327
+
3328
+ return fileByFileCoverage;
3329
+ };
3330
+
3331
+ const composeTwoV8Coverages = (firstV8Coverage, secondV8Coverage) => {
3332
+ if (secondV8Coverage.result.length === 0) {
3333
+ return firstV8Coverage;
3334
+ }
3335
+
3336
+ // eslint-disable-next-line import/no-unresolved
3337
+ const { mergeProcessCovs } = importWithRequire("@c88/v8-coverage");
3338
+ // "mergeProcessCovs" do not preserves source-map-cache during the merge
3339
+ // so we store sourcemap cache now
3340
+ const sourceMapCache = {};
3341
+ const visit = (coverageReport) => {
3342
+ if (coverageReport["source-map-cache"]) {
3343
+ Object.assign(sourceMapCache, coverageReport["source-map-cache"]);
3344
+ }
3345
+ };
3346
+ visit(firstV8Coverage);
3347
+ visit(secondV8Coverage);
3348
+ const v8Coverage = mergeProcessCovs([firstV8Coverage, secondV8Coverage]);
3349
+ v8Coverage["source-map-cache"] = sourceMapCache;
3350
+
3351
+ return v8Coverage;
3352
+ };
3353
+
3354
+ const readNodeV8CoverageDirectory = async ({
3355
+ logger,
3356
+ warn,
3357
+ signal,
3358
+ onV8Coverage,
3359
+ maxMsWaitingForNodeToWriteCoverageFile = 2000,
3360
+ }) => {
3361
+ const NODE_V8_COVERAGE = process.env.NODE_V8_COVERAGE;
3362
+ const operation = Abort.startOperation();
3363
+ operation.addAbortSignal(signal);
3364
+
3365
+ let timeSpentTrying = 0;
3366
+ const tryReadDirectory = async () => {
3367
+ const dirContent = readdirSync(NODE_V8_COVERAGE);
3368
+ if (dirContent.length > 0) {
3369
+ return dirContent;
3370
+ }
3371
+ if (timeSpentTrying < maxMsWaitingForNodeToWriteCoverageFile) {
3372
+ await new Promise((resolve) => setTimeout(resolve, 200));
3373
+ timeSpentTrying += 200;
3374
+ logger.debug("retry to read coverage directory");
3375
+ return tryReadDirectory();
3376
+ }
3377
+ warn({
3378
+ code: "V8_COVERAGE_EMPTY",
3379
+ message: `v8 coverage directory is empty at ${NODE_V8_COVERAGE}`,
3380
+ });
3381
+ return dirContent;
3382
+ };
3383
+
3384
+ try {
3385
+ operation.throwIfAborted();
3386
+ const dirContent = await tryReadDirectory();
3387
+
3388
+ const coverageDirectoryUrl = assertAndNormalizeDirectoryUrl(
3389
+ NODE_V8_COVERAGE,
3390
+ "NODE_V8_COVERAGE",
3391
+ );
3392
+
3393
+ await dirContent.reduce(async (previous, dirEntry) => {
3394
+ operation.throwIfAborted();
3395
+ await previous;
3396
+
3397
+ const dirEntryUrl = new URL(dirEntry, coverageDirectoryUrl);
3398
+ const tryReadJsonFile = async () => {
3356
3399
  const fileContent = String(readFileSync(dirEntryUrl));
3357
3400
  if (fileContent === "") {
3358
3401
  if (timeSpentTrying < maxMsWaitingForNodeToWriteCoverageFile) {
@@ -3393,58 +3436,6 @@ const readNodeV8CoverageDirectory = async ({
3393
3436
  }
3394
3437
  };
3395
3438
 
3396
- const importWithRequire = createRequire(import.meta.url);
3397
-
3398
- const composeTwoV8Coverages = (firstV8Coverage, secondV8Coverage) => {
3399
- if (secondV8Coverage.result.length === 0) {
3400
- return firstV8Coverage;
3401
- }
3402
-
3403
- // eslint-disable-next-line import/no-unresolved
3404
- const { mergeProcessCovs } = importWithRequire("@c88/v8-coverage");
3405
- // "mergeProcessCovs" do not preserves source-map-cache during the merge
3406
- // so we store sourcemap cache now
3407
- const sourceMapCache = {};
3408
- const visit = (coverageReport) => {
3409
- if (coverageReport["source-map-cache"]) {
3410
- Object.assign(sourceMapCache, coverageReport["source-map-cache"]);
3411
- }
3412
- };
3413
- visit(firstV8Coverage);
3414
- visit(secondV8Coverage);
3415
- const v8Coverage = mergeProcessCovs([firstV8Coverage, secondV8Coverage]);
3416
- v8Coverage["source-map-cache"] = sourceMapCache;
3417
-
3418
- return v8Coverage;
3419
- };
3420
-
3421
- const composeTwoFileByFileIstanbulCoverages = (
3422
- firstFileByFileIstanbulCoverage,
3423
- secondFileByFileIstanbulCoverage,
3424
- ) => {
3425
- const fileByFileIstanbulCoverage = {};
3426
- Object.keys(firstFileByFileIstanbulCoverage).forEach((key) => {
3427
- fileByFileIstanbulCoverage[key] = firstFileByFileIstanbulCoverage[key];
3428
- });
3429
- Object.keys(secondFileByFileIstanbulCoverage).forEach((key) => {
3430
- const firstCoverage = firstFileByFileIstanbulCoverage[key];
3431
- const secondCoverage = secondFileByFileIstanbulCoverage[key];
3432
- fileByFileIstanbulCoverage[key] = firstCoverage
3433
- ? merge(firstCoverage, secondCoverage)
3434
- : secondCoverage;
3435
- });
3436
-
3437
- return fileByFileIstanbulCoverage;
3438
- };
3439
-
3440
- const merge = (firstIstanbulCoverage, secondIstanbulCoverage) => {
3441
- const { createFileCoverage } = importWithRequire("istanbul-lib-coverage");
3442
- const istanbulFileCoverageObject = createFileCoverage(firstIstanbulCoverage);
3443
- istanbulFileCoverageObject.merge(secondIstanbulCoverage);
3444
- const istanbulCoverage = istanbulFileCoverageObject.toJSON();
3445
- return istanbulCoverage;
3446
- };
3447
-
3448
3439
  const v8CoverageToIstanbul = async (v8Coverage, { signal }) => {
3449
3440
  const operation = Abort.startOperation();
3450
3441
  operation.addAbortSignal(signal);
@@ -3539,246 +3530,44 @@ const sourcesFromLineLengths = (lineLengths) => {
3539
3530
  return source;
3540
3531
  };
3541
3532
 
3542
- const composeV8AndIstanbul = (
3543
- v8FileByFileCoverage,
3544
- istanbulFileByFileCoverage,
3545
- { warn, v8ConflictWarning },
3546
- ) => {
3547
- const fileByFileCoverage = {};
3548
- const v8Files = Object.keys(v8FileByFileCoverage);
3549
- const istanbulFiles = Object.keys(istanbulFileByFileCoverage);
3550
-
3551
- v8Files.forEach((key) => {
3552
- fileByFileCoverage[key] = v8FileByFileCoverage[key];
3553
- });
3554
- istanbulFiles.forEach((key) => {
3555
- const v8Coverage = v8FileByFileCoverage[key];
3556
- if (v8Coverage) {
3557
- if (v8ConflictWarning) {
3558
- warn({
3559
- code: "V8_COVERAGE_CONFLICT",
3560
- message: createDetailedMessage(
3561
- `Coverage conflict on "${key}", found two coverage that cannot be merged together: v8 and istanbul. The istanbul coverage will be ignored.`,
3562
- {
3563
- "details": `This happens when a file is executed on a runtime using v8 coverage (node or chromium) and on runtime using istanbul coverage (firefox or webkit)`,
3564
- "suggestion":
3565
- "disable this warning with coverage.v8ConflictWarning: false",
3566
- "suggestion 2": `force coverage using istanbul with coverage.methodForBrowsers: "istanbul"`,
3567
- },
3568
- ),
3569
- });
3570
- }
3571
- fileByFileCoverage[key] = v8Coverage;
3572
- } else {
3573
- fileByFileCoverage[key] = istanbulFileByFileCoverage[key];
3574
- }
3575
- });
3576
-
3577
- return fileByFileCoverage;
3578
- };
3579
-
3580
- const normalizeFileByFileCoveragePaths = (
3581
- fileByFileCoverage,
3582
- rootDirectoryUrl,
3533
+ const generateCoverage = async (
3534
+ testPlanResult,
3535
+ { signal, logger, warn, rootDirectoryUrl, coverage },
3583
3536
  ) => {
3584
- const fileByFileNormalized = {};
3585
- Object.keys(fileByFileCoverage).forEach((key) => {
3586
- const fileCoverage = fileByFileCoverage[key];
3587
- const { path } = fileCoverage;
3588
- const url = isFileSystemPath(path)
3589
- ? fileSystemPathToUrl(path)
3590
- : new URL(path, rootDirectoryUrl).href;
3591
- const relativeUrl = urlToRelativeUrl(url, rootDirectoryUrl);
3592
- fileByFileNormalized[`./${relativeUrl}`] = {
3593
- ...fileCoverage,
3594
- path: `./${relativeUrl}`,
3595
- };
3596
- });
3597
- return fileByFileNormalized;
3598
- };
3537
+ // collect v8 and istanbul coverage from executions
3538
+ let { v8Coverage, fileByFileIstanbulCoverage } =
3539
+ await getCoverageFromTestPlanResults(testPlanResult.results, {
3540
+ signal,
3541
+ onMissing: ({ file, executionResult, executionName }) => {
3542
+ // several reasons not to have coverage here:
3543
+ // 1. the file we executed did not import an instrumented file.
3544
+ // - a test file without import
3545
+ // - a test file importing only file excluded from coverage
3546
+ // - a coverDescription badly configured so that we don't realize
3547
+ // a file should be covered
3599
3548
 
3600
- const listRelativeFileUrlToCover = async ({
3601
- signal,
3602
- rootDirectoryUrl,
3603
- coverageInclude,
3604
- }) => {
3605
- const matchingFileResultArray = await collectFiles({
3606
- signal,
3607
- directoryUrl: rootDirectoryUrl,
3608
- associations: { cover: coverageInclude },
3609
- predicate: ({ cover }) => cover,
3610
- });
3611
- return matchingFileResultArray.map(({ relativeUrl }) => relativeUrl);
3612
- };
3549
+ // 2. the file we wanted to executed timedout
3550
+ // - infinite loop
3551
+ // - too extensive operation
3552
+ // - a badly configured or too low allocatedMs for that execution.
3613
3553
 
3614
- // https://github.com/istanbuljs/babel-plugin-istanbul/blob/321740f7b25d803f881466ea819d870f7ed6a254/src/index.js
3554
+ // 3. the file we wanted to execute contains syntax-error
3615
3555
 
3616
- const babelPluginInstrument = (api, { useInlineSourceMaps = false }) => {
3617
- const { programVisitor } = importWithRequire("istanbul-lib-instrument");
3618
- const { types } = api;
3619
-
3620
- return {
3621
- name: "transform-instrument",
3622
- visitor: {
3623
- Program: {
3624
- enter(path, state) {
3625
- const { file } = state;
3626
- const { opts } = file;
3627
- let inputSourceMap;
3628
- if (useInlineSourceMaps) {
3629
- // https://github.com/istanbuljs/babel-plugin-istanbul/commit/a9e15643d249a2985e4387e4308022053b2cd0ad#diff-1fdf421c05c1140f6d71444ea2b27638R65
3630
- inputSourceMap =
3631
- opts.inputSourceMap || file.inputMap
3632
- ? file.inputMap.sourcemap
3633
- : null;
3634
- } else {
3635
- inputSourceMap = opts.inputSourceMap;
3636
- }
3637
- const __dv__ = programVisitor(
3638
- types,
3639
- opts.filenameRelative || opts.filename,
3640
- {
3641
- coverageVariable: "__coverage__",
3642
- inputSourceMap,
3643
- },
3644
- );
3645
- __dv__.enter(path);
3646
- file.metadata.__dv__ = __dv__;
3647
- },
3648
-
3649
- exit(path, state) {
3650
- const { __dv__ } = state.file.metadata;
3651
- if (!__dv__) {
3652
- return;
3653
- }
3654
- const object = __dv__.exit(path);
3655
- // object got two properties: fileCoverage and sourceMappingURL
3656
- state.file.metadata.coverage = object.fileCoverage;
3657
- },
3658
- },
3659
- },
3660
- };
3661
- };
3662
-
3663
- const relativeUrlToEmptyCoverage = async (
3664
- relativeUrl,
3665
- { signal, rootDirectoryUrl },
3666
- ) => {
3667
- const operation = Abort.startOperation();
3668
- operation.addAbortSignal(signal);
3669
-
3670
- try {
3671
- const fileUrl = resolveUrl(relativeUrl, rootDirectoryUrl);
3672
- const content = await readFile(fileUrl, { as: "string" });
3673
-
3674
- operation.throwIfAborted();
3675
- const { metadata } = await applyBabelPlugins({
3676
- babelPlugins: [babelPluginInstrument],
3677
- input: content,
3678
- inputIsJsModule: true,
3679
- inputUrl: fileUrl,
3680
- });
3681
- const { coverage } = metadata;
3682
- if (!coverage) {
3683
- throw new Error(`missing coverage for file`);
3684
- }
3685
- // https://github.com/gotwarlost/istanbul/blob/bc84c315271a5dd4d39bcefc5925cfb61a3d174a/lib/command/common/run-with-cover.js#L229
3686
- Object.keys(coverage.s).forEach(function (key) {
3687
- coverage.s[key] = 0;
3688
- });
3689
- return coverage;
3690
- } catch (e) {
3691
- if (e && e.code === "PARSE_ERROR") {
3692
- // return an empty coverage for that file when
3693
- // it contains a syntax error
3694
- return createEmptyCoverage(relativeUrl);
3695
- }
3696
- throw e;
3697
- } finally {
3698
- await operation.end();
3699
- }
3700
- };
3701
-
3702
- const createEmptyCoverage = (relativeUrl) => {
3703
- const { createFileCoverage } = importWithRequire("istanbul-lib-coverage");
3704
- return createFileCoverage(relativeUrl).toJSON();
3705
- };
3706
-
3707
- const getMissingFileByFileCoverage = async ({
3708
- signal,
3709
- rootDirectoryUrl,
3710
- coverageInclude,
3711
- fileByFileCoverage,
3712
- }) => {
3713
- const relativeUrlsToCover = await listRelativeFileUrlToCover({
3714
- signal,
3715
- rootDirectoryUrl,
3716
- coverageInclude,
3717
- });
3718
- const relativeUrlsMissing = relativeUrlsToCover.filter((relativeUrlToCover) =>
3719
- Object.keys(fileByFileCoverage).every((key) => {
3720
- return key !== `./${relativeUrlToCover}`;
3721
- }),
3722
- );
3723
-
3724
- const operation = Abort.startOperation();
3725
- operation.addAbortSignal(signal);
3726
- const missingFileByFileCoverage = {};
3727
- await relativeUrlsMissing.reduce(async (previous, relativeUrlMissing) => {
3728
- operation.throwIfAborted();
3729
- await previous;
3730
- await operation.withSignal(async (signal) => {
3731
- const emptyCoverage = await relativeUrlToEmptyCoverage(
3732
- relativeUrlMissing,
3733
- {
3734
- signal,
3735
- rootDirectoryUrl,
3736
- },
3737
- );
3738
- missingFileByFileCoverage[`./${relativeUrlMissing}`] = emptyCoverage;
3739
- });
3740
- }, Promise.resolve());
3741
- return missingFileByFileCoverage;
3742
- };
3743
-
3744
- const generateCoverage = async (
3745
- testPlanResult,
3746
- { signal, logger, warn, rootDirectoryUrl, coverage },
3747
- ) => {
3748
- // collect v8 and istanbul coverage from executions
3749
- let { v8Coverage, fileByFileIstanbulCoverage } =
3750
- await getCoverageFromTestPlanResults(testPlanResult.results, {
3751
- signal,
3752
- onMissing: ({ file, executionResult, executionName }) => {
3753
- // several reasons not to have coverage here:
3754
- // 1. the file we executed did not import an instrumented file.
3755
- // - a test file without import
3756
- // - a test file importing only file excluded from coverage
3757
- // - a coverDescription badly configured so that we don't realize
3758
- // a file should be covered
3759
-
3760
- // 2. the file we wanted to executed timedout
3761
- // - infinite loop
3762
- // - too extensive operation
3763
- // - a badly configured or too low allocatedMs for that execution.
3764
-
3765
- // 3. the file we wanted to execute contains syntax-error
3766
-
3767
- // in any scenario we are fine because
3768
- // coverDescription will generate empty coverage for files
3769
- // that were suppose to be coverage but were not.
3770
- if (
3771
- executionResult.status === "completed" &&
3772
- executionResult.type === "node" &&
3773
- coverage.methodForNodeJs !== "NODE_V8_COVERAGE"
3774
- ) {
3775
- warn({
3776
- code: "EXECUTION_COVERAGE_FILE_NOT_FOUND",
3777
- message: `"${executionName}" execution of ${file} did not properly write coverage into ${executionResult.coverageFileUrl}`,
3778
- });
3779
- }
3780
- },
3781
- });
3556
+ // in any scenario we are fine because
3557
+ // coverDescription will generate empty coverage for files
3558
+ // that were suppose to be coverage but were not.
3559
+ if (
3560
+ executionResult.status === "completed" &&
3561
+ executionResult.type === "node" &&
3562
+ coverage.methodForNodeJs !== "NODE_V8_COVERAGE"
3563
+ ) {
3564
+ warn({
3565
+ code: "EXECUTION_COVERAGE_FILE_NOT_FOUND",
3566
+ message: `"${executionName}" execution of ${file} did not properly write coverage into ${executionResult.coverageFileUrl}`,
3567
+ });
3568
+ }
3569
+ },
3570
+ });
3782
3571
 
3783
3572
  if (coverage.methodForNodeJs === "NODE_V8_COVERAGE") {
3784
3573
  await readNodeV8CoverageDirectory({
@@ -3921,323 +3710,215 @@ const getCoverageFromTestPlanResults = async (
3921
3710
 
3922
3711
  const isV8Coverage = (coverage) => Boolean(coverage.result);
3923
3712
 
3924
- const pingServer = async (url) => {
3925
- const server = createServer();
3926
- const { hostname, port } = new URL(url);
3713
+ const createCallOrderer = () => {
3714
+ const queue = [];
3715
+ const callWhenPreviousExecutionAreDone = (executionIndex, callback) => {
3716
+ if (queue[executionIndex]) {
3717
+ throw new Error(`${executionIndex} already used`);
3718
+ }
3927
3719
 
3928
- try {
3929
- await new Promise((resolve, reject) => {
3930
- server.on("error", reject);
3931
- server.on("listening", () => {
3932
- resolve();
3933
- });
3934
- server.listen(port, hostname);
3935
- });
3936
- } catch (error) {
3937
- if (error && error.code === "EADDRINUSE") {
3938
- return true;
3720
+ let allBeforeAreDone = true;
3721
+ if (executionIndex > 0) {
3722
+ let beforeIndex = executionIndex - 1;
3723
+ do {
3724
+ const value = queue[beforeIndex];
3725
+ if (!value) {
3726
+ allBeforeAreDone = false;
3727
+ break;
3728
+ }
3729
+ } while (beforeIndex--);
3939
3730
  }
3940
- if (error && error.code === "EACCES") {
3941
- return true;
3731
+ if (!allBeforeAreDone) {
3732
+ queue[executionIndex] = callback;
3733
+ return;
3942
3734
  }
3943
- throw error;
3944
- }
3945
- await new Promise((resolve, reject) => {
3946
- server.on("error", reject);
3947
- server.on("close", resolve);
3948
- server.close();
3949
- });
3950
- return false;
3735
+ queue[executionIndex] = true;
3736
+ callback();
3737
+ let afterIndex = executionIndex + 1;
3738
+ while (afterIndex < queue.length) {
3739
+ const value = queue[afterIndex];
3740
+ if (value === undefined) {
3741
+ break;
3742
+ }
3743
+ if (typeof value === "function") {
3744
+ queue[afterIndex] = true;
3745
+ value();
3746
+ }
3747
+ afterIndex++;
3748
+ }
3749
+ };
3750
+ return callWhenPreviousExecutionAreDone;
3951
3751
  };
3952
3752
 
3953
- const startServerUsingCommand = async (
3954
- webServer,
3955
- { signal, allocatedMs, logger, teardownCallbackSet },
3956
- ) => {
3957
- const spawnedProcess = spawn(webServer.command, [], {
3958
- // On non-windows platforms, `detached: true` makes child process a leader of a new
3959
- // process group, making it possible to kill child process tree with `.kill(-pid)` command.
3960
- // @see https://nodejs.org/api/child_process.html#child_process_options_detached
3961
- detached: process.platform !== "win32",
3962
- stdio: ["pipe", "pipe", "pipe"],
3963
- shell: true,
3964
- cwd: webServer.cwd,
3965
- });
3966
- if (!spawnedProcess.pid) {
3967
- await new Promise((resolve, reject) => {
3968
- spawnedProcess.once("error", (error) => {
3969
- reject(new Error(`Failed to launch: ${error}`));
3970
- });
3971
- });
3972
- }
3753
+ // https://gist.github.com/GaetanoPiazzolla/c40e1ebb9f709d091208e89baf9f4e00
3973
3754
 
3974
- let errorReceived = false;
3975
- const errorPromise = new Promise((resolve, reject) => {
3976
- spawnedProcess.on("error", (e) => {
3977
- errorReceived = true;
3978
- reject(e);
3979
- });
3980
- });
3981
3755
 
3982
- // const stdout = readline.createInterface({ input: spawnedProcess.stdout });
3983
- // stdout.on("line", () => {
3984
- // logger.debug(`[pid=${spawnedProcess.pid}][out] ${data}`);
3985
- // });
3986
- // const stderr = readline.createInterface({ input: spawnedProcess.stderr });
3987
- // stderr.on("line", (data) => {
3988
- // logger.debug(`[pid=${spawnedProcess.pid}][err] ${data}`);
3989
- // });
3990
- let processClosed = false;
3991
- const closedPromise = new Promise((resolve) => {
3992
- spawnedProcess.once("exit", (exitCode, signal) => {
3993
- logger.debug(
3994
- `[pid=${spawnedProcess.pid}] <process did exit: exitCode=${exitCode}, signal=${signal}>`,
3995
- );
3996
- processClosed = true;
3997
- resolve();
3998
- });
3999
- });
4000
- const killProcess = async () => {
4001
- logger.debug(`[pid=${spawnedProcess.pid}] <kill>`);
4002
- if (!spawnedProcess.pid || spawnedProcess.killed || processClosed) {
4003
- logger.debug(
4004
- `[pid=${spawnedProcess.pid}] <skipped force kill spawnedProcess.killed=${spawnedProcess.killed} processClosed=${processClosed}>`,
4005
- );
4006
- return;
4007
- }
4008
- logger.debug(`[pid=${spawnedProcess.pid}] <will force kill>`);
4009
- // Force kill the browser.
4010
- try {
4011
- if (process.platform === "win32") {
4012
- const taskkillProcess = spawnSync(
4013
- `taskkill /pid ${spawnedProcess.pid} /T /F`,
4014
- { shell: true },
4015
- );
4016
- const [stdout, stderr] = [
4017
- taskkillProcess.stdout.toString(),
4018
- taskkillProcess.stderr.toString(),
4019
- ];
4020
- if (stdout)
4021
- logger.debug(
4022
- `[pid=${spawnedProcess.pid}] taskkill stdout: ${stdout}`,
4023
- );
4024
- if (stderr)
4025
- logger.info(`[pid=${spawnedProcess.pid}] taskkill stderr: ${stderr}`);
4026
- } else {
4027
- process.kill(-spawnedProcess.pid, "SIGKILL");
4028
- }
4029
- } catch (e) {
4030
- logger.info(
4031
- `[pid=${spawnedProcess.pid}] exception while trying to kill process: ${e}`,
4032
- );
4033
- // the process might have already stopped
4034
- }
4035
- await closedPromise;
3756
+ const startMeasuringTotalCpuUsage = () => {
3757
+ let previousCpuArray = cpus();
3758
+ let previousMs = Date.now();
3759
+ let previousCpuUsage = cpuUsage();
3760
+
3761
+ const overall = {
3762
+ inactive: 100,
3763
+ active: 0,
3764
+ system: 0,
3765
+ user: 0,
3766
+ };
3767
+ const thisProcess = {
3768
+ active: 0,
3769
+ system: 0,
3770
+ user: 0,
4036
3771
  };
3772
+ const details = previousCpuArray.map(() => {
3773
+ return {
3774
+ inactive: 100,
3775
+ active: 0,
3776
+ system: 0,
3777
+ user: 0,
3778
+ };
3779
+ });
4037
3780
 
4038
- const startOperation = Abort.startOperation();
4039
- startOperation.addAbortSignal(signal);
4040
- const timeoutAbortSource = startOperation.timeout(allocatedMs);
4041
- startOperation.addAbortCallback(killProcess);
4042
- teardownCallbackSet.add(killProcess);
3781
+ const samples = [];
3782
+ const interval = setInterval(() => {
3783
+ let cpuArray = cpus();
3784
+ const ms = Date.now();
3785
+ const ellapsedMs = ms - previousMs;
3786
+ const cpuUsageSampleArray = [];
3787
+ let overallSystemMs = 0;
3788
+ let overallUserMs = 0;
3789
+ let overallInactiveMs = 0;
3790
+ let overallActiveMs = 0;
3791
+ let overallMsEllapsed = 0;
3792
+ let index = 0;
3793
+ for (const cpu of cpuArray) {
3794
+ const previousCpuTimes = previousCpuArray[index].times;
3795
+ const cpuTimes = cpu.times;
3796
+ const systemMs = cpuTimes.sys - previousCpuTimes.sys;
3797
+ const userMs = cpuTimes.user - previousCpuTimes.user;
3798
+ const activeMs = systemMs + userMs;
3799
+ const inactiveMs = ellapsedMs - activeMs;
3800
+ const cpuUsageSample = {
3801
+ inactive: inactiveMs / ellapsedMs,
3802
+ active: activeMs / ellapsedMs,
3803
+ system: systemMs / ellapsedMs,
3804
+ user: userMs / ellapsedMs,
3805
+ };
3806
+ cpuUsageSampleArray.push(cpuUsageSample);
4043
3807
 
4044
- const startedPromise = (async () => {
4045
- const logScale = [100, 250, 500];
4046
- // eslint-disable-next-line no-constant-condition
4047
- while (true) {
4048
- if (errorReceived) {
4049
- break;
4050
- }
4051
- const connected = await pingServer(webServer.origin);
4052
- if (connected) {
4053
- break;
4054
- }
4055
- startOperation.throwIfAborted();
4056
- const delay = logScale.shift() || 1000;
4057
- logger.debug(`Waiting ${delay}ms`);
4058
- await new Promise((x) => setTimeout(x, delay));
3808
+ overallSystemMs += systemMs;
3809
+ overallUserMs += userMs;
3810
+ overallInactiveMs += inactiveMs;
3811
+ overallActiveMs += activeMs;
3812
+ overallMsEllapsed += ellapsedMs;
3813
+ index++;
4059
3814
  }
4060
- })();
3815
+ const overallUsageSample = {
3816
+ inactive: overallInactiveMs / overallMsEllapsed,
3817
+ active: overallActiveMs / overallMsEllapsed,
3818
+ system: overallSystemMs / overallMsEllapsed,
3819
+ user: overallUserMs / overallMsEllapsed,
3820
+ };
3821
+ previousCpuArray = cpuArray;
3822
+ previousMs = ms;
4061
3823
 
4062
- try {
4063
- await Promise.race([errorPromise, startedPromise]);
4064
- } catch (e) {
4065
- if (Abort.isAbortError(e)) {
4066
- if (timeoutAbortSource.signal.aborted) {
4067
- // aborted by timeout
4068
- throw new Error(
4069
- `"${webServer.command}" command did not start a server in less than ${allocatedMs}ms`,
4070
- );
3824
+ const processCpuUsage = cpuUsage();
3825
+ const thisProcessSystemMs = Math.round(
3826
+ (processCpuUsage.system - previousCpuUsage.system) / 1000,
3827
+ );
3828
+ const thisProcessUserMs = Math.round(
3829
+ (processCpuUsage.user - previousCpuUsage.user) / 1000,
3830
+ );
3831
+ previousCpuUsage = processCpuUsage;
3832
+
3833
+ const thisProcessActiveMs = thisProcessSystemMs + thisProcessUserMs;
3834
+ const thisProcessInactiveMs = overallMsEllapsed - thisProcessActiveMs;
3835
+ const thisProcessSample = {
3836
+ inactive: thisProcessInactiveMs / overallMsEllapsed,
3837
+ active: thisProcessActiveMs / overallMsEllapsed,
3838
+ system: thisProcessSystemMs / overallMsEllapsed,
3839
+ user: thisProcessUserMs / overallMsEllapsed,
3840
+ };
3841
+ samples.push({
3842
+ cpuUsageSampleArray,
3843
+ overallUsageSample,
3844
+ thisProcessSample,
3845
+ });
3846
+ if (samples.length === 10) {
3847
+ {
3848
+ let index = 0;
3849
+ for (const detail of details) {
3850
+ let systemSum = 0;
3851
+ let userSum = 0;
3852
+ let inactiveSum = 0;
3853
+ let activeSum = 0;
3854
+ for (const sample of samples) {
3855
+ const { cpuUsageSampleArray } = sample;
3856
+ const cpuUsageSample = cpuUsageSampleArray[index];
3857
+ inactiveSum += cpuUsageSample.inactive;
3858
+ activeSum += cpuUsageSample.active;
3859
+ systemSum += cpuUsageSample.system;
3860
+ userSum += cpuUsageSample.user;
3861
+ }
3862
+ Object.assign(detail, {
3863
+ inactive: inactiveSum / samples.length,
3864
+ active: activeSum / samples.length,
3865
+ system: systemSum / samples.length,
3866
+ user: userSum / samples.length,
3867
+ });
3868
+ index++;
3869
+ }
4071
3870
  }
4072
- if (signal.aborted) {
4073
- // aborted from outside
4074
- return;
3871
+ {
3872
+ let overallSystemSum = 0;
3873
+ let overallUserSum = 0;
3874
+ let overallInactiveSum = 0;
3875
+ let overallActiveSum = 0;
3876
+ for (const sample of samples) {
3877
+ const { overallUsageSample } = sample;
3878
+ overallSystemSum += overallUsageSample.system;
3879
+ overallUserSum += overallUsageSample.user;
3880
+ overallInactiveSum += overallUsageSample.inactive;
3881
+ overallActiveSum += overallUsageSample.active;
3882
+ }
3883
+ Object.assign(overall, {
3884
+ inactive: overallInactiveSum / samples.length,
3885
+ active: overallActiveSum / samples.length,
3886
+ system: overallSystemSum / samples.length,
3887
+ user: overallUserSum / samples.length,
3888
+ });
3889
+ }
3890
+ {
3891
+ let thisProcessSystemSum = 0;
3892
+ let thisProcessUserSum = 0;
3893
+ let thisProcessInactiveSum = 0;
3894
+ let thisProcessActiveSum = 0;
3895
+ for (const sample of samples) {
3896
+ const { thisProcessSample } = sample;
3897
+ thisProcessSystemSum += thisProcessSample.system;
3898
+ thisProcessUserSum += thisProcessSample.user;
3899
+ thisProcessInactiveSum += thisProcessSample.inactive;
3900
+ thisProcessActiveSum += thisProcessSample.active;
3901
+ }
3902
+ Object.assign(thisProcess, {
3903
+ inactive: thisProcessInactiveSum / samples.length,
3904
+ active: thisProcessActiveSum / samples.length,
3905
+ system: thisProcessSystemSum / samples.length,
3906
+ user: thisProcessUserSum / samples.length,
3907
+ });
4075
3908
  }
3909
+ samples.length = 0;
4076
3910
  }
4077
- throw e;
4078
- } finally {
4079
- await startOperation.end();
4080
- }
4081
- };
3911
+ }, 15);
3912
+ interval.unref();
4082
3913
 
4083
- const startServerUsingModuleUrl = async (webServer, params) => {
4084
- if (!existsSync(new URL(webServer.moduleUrl))) {
4085
- throw new Error(`"${webServer.moduleUrl}" does not lead to a file`);
4086
- }
4087
- return startServerUsingCommand(
4088
- {
4089
- ...webServer,
4090
- command: `node ${fileURLToPath(webServer.moduleUrl)}`,
3914
+ return {
3915
+ overall,
3916
+ thisProcess,
3917
+ details,
3918
+ stop: () => {
3919
+ clearInterval(interval);
4091
3920
  },
4092
- params,
4093
- );
4094
- };
4095
-
4096
- const basicFetch = async (
4097
- url,
4098
- { rejectUnauthorized = true, method = "GET", headers = {} } = {},
4099
- ) => {
4100
- let requestModule;
4101
- if (url.startsWith("http:")) {
4102
- requestModule = await import("node:http");
4103
- } else {
4104
- requestModule = await import("node:https");
4105
- }
4106
- const { request } = requestModule;
4107
-
4108
- const urlObject = new URL(url);
4109
-
4110
- return new Promise((resolve, reject) => {
4111
- const req = request({
4112
- rejectUnauthorized,
4113
- hostname: urlObject.hostname,
4114
- port: urlObject.port,
4115
- path: urlObject.pathname,
4116
- method,
4117
- headers,
4118
- });
4119
- req.on("response", (response) => {
4120
- resolve({
4121
- status: response.statusCode,
4122
- headers: response.headers,
4123
- json: () => {
4124
- req.setTimeout(0);
4125
- req.destroy();
4126
- return new Promise((resolve) => {
4127
- if (response.headers["content-type"] !== "application/json") {
4128
- console.warn("not json");
4129
- }
4130
- let responseBody = "";
4131
- response.setEncoding("utf8");
4132
- response.on("data", (chunk) => {
4133
- responseBody += chunk;
4134
- });
4135
- response.on("end", () => {
4136
- resolve(JSON.parse(responseBody));
4137
- });
4138
- response.on("error", (e) => {
4139
- reject(e);
4140
- });
4141
- });
4142
- },
4143
- });
4144
- });
4145
- req.on("error", reject);
4146
- req.end();
4147
- });
4148
- };
4149
-
4150
- const assertAndNormalizeWebServer = async (
4151
- webServer,
4152
- { signal, logger, teardownCallbackSet },
4153
- ) => {
4154
- if (!webServer) {
4155
- throw new TypeError(
4156
- `webServer is required when running tests on browser(s)`,
4157
- );
4158
- }
4159
- const unexpectedParamNames = Object.keys(webServer).filter((key) => {
4160
- return ![
4161
- "origin",
4162
- "moduleUrl",
4163
- "command",
4164
- "cwd",
4165
- "rootDirectoryUrl",
4166
- ].includes(key);
4167
- });
4168
- if (unexpectedParamNames.length > 0) {
4169
- throw new TypeError(
4170
- `${unexpectedParamNames.join(",")}: there is no such param to webServer`,
4171
- );
4172
- }
4173
- if (typeof webServer.origin !== "string") {
4174
- throw new TypeError(
4175
- `webServer.origin must be a string, got ${webServer.origin}`,
4176
- );
4177
- }
4178
- await ensureWebServerIsStarted(webServer, {
4179
- signal,
4180
- teardownCallbackSet,
4181
- logger,
4182
- });
4183
- const { headers } = await basicFetch(webServer.origin, {
4184
- method: "GET",
4185
- rejectUnauthorized: false,
4186
- headers: {
4187
- "x-server-inspect": "1",
4188
- },
4189
- });
4190
- if (String(headers["server"]).includes("jsenv_dev_server")) {
4191
- webServer.isJsenvDevServer = true;
4192
- const { json } = await basicFetch(`${webServer.origin}/__params__.json`, {
4193
- rejectUnauthorized: false,
4194
- });
4195
- if (webServer.rootDirectoryUrl === undefined) {
4196
- const jsenvDevServerParams = await json();
4197
- webServer.rootDirectoryUrl = jsenvDevServerParams.sourceDirectoryUrl;
4198
- } else {
4199
- webServer.rootDirectoryUrl = assertAndNormalizeDirectoryUrl(
4200
- webServer.rootDirectoryUrl,
4201
- "webServer.rootDirectoryUrl",
4202
- );
4203
- }
4204
- } else {
4205
- webServer.rootDirectoryUrl = assertAndNormalizeDirectoryUrl(
4206
- webServer.rootDirectoryUrl,
4207
- "webServer.rootDirectoryUrl",
4208
- );
4209
- }
4210
- };
4211
-
4212
- const ensureWebServerIsStarted = async (
4213
- webServer,
4214
- { signal, teardownCallbackSet, logger, allocatedMs = 5_000 },
4215
- ) => {
4216
- const aServerIsListening = await pingServer(webServer.origin);
4217
- if (aServerIsListening) {
4218
- return;
4219
- }
4220
- if (webServer.moduleUrl) {
4221
- await startServerUsingModuleUrl(webServer, {
4222
- signal,
4223
- allocatedMs,
4224
- teardownCallbackSet,
4225
- logger,
4226
- });
4227
- return;
4228
- }
4229
- if (webServer.command) {
4230
- await startServerUsingCommand(webServer, {
4231
- signal,
4232
- allocatedMs,
4233
- teardownCallbackSet,
4234
- logger,
4235
- });
4236
- return;
4237
- }
4238
- throw new TypeError(
4239
- `webServer.moduleUrl or webServer.command is required as there is no server listening "${webServer.origin}"`,
4240
- );
3921
+ };
4241
3922
  };
4242
3923
 
4243
3924
  const githubAnnotationFromError = (
@@ -4371,226 +4052,38 @@ const replaceUrls$1 = (source, replace) => {
4371
4052
  });
4372
4053
  };
4373
4054
 
4374
- /*
4375
- * Export a function capable to run a file on a runtime.
4376
- *
4377
- * - Used internally by "executeTestPlan" part of the documented API
4378
- * - Used internally by "execute" an advanced API not documented
4379
- * - logs generated during file execution can be collected
4380
- * - logs generated during file execution can be mirrored (re-logged to the console)
4381
- * - File is given allocatedMs to complete
4382
- * - Errors are collected
4383
- * - File execution result is returned, it contains status/errors/namespace/consoleCalls
4384
- */
4385
-
4386
-
4387
- const run = async ({
4388
- signal = new AbortController().signal,
4389
- logger,
4390
- allocatedMs,
4391
- keepRunning = false,
4392
- mirrorConsole = false,
4393
- collectConsole = false,
4394
- measureMemoryUsage = false,
4395
- onMeasureMemoryAvailable,
4396
- collectPerformance = false,
4397
- coverageEnabled = false,
4398
- coverageTempDirectoryUrl,
4399
- runtime,
4400
- runtimeParams,
4401
- }) => {
4402
- if (keepRunning) {
4403
- allocatedMs = Infinity;
4404
- }
4405
- if (allocatedMs === Infinity) {
4406
- allocatedMs = 0;
4407
- }
4408
-
4409
- const timingsOrigin = Date.now();
4410
- const takeTiming = () => {
4411
- return Date.now() - timingsOrigin;
4412
- };
4413
- const result = {
4414
- status: "pending",
4415
- errors: [],
4416
- namespace: null,
4417
- consoleCalls: null,
4418
- timings: {
4419
- origin: timingsOrigin,
4420
- start: 0,
4421
- runtimeStart: null,
4422
- executionStart: null,
4423
- executionEnd: null,
4424
- runtimeEnd: null,
4425
- end: null,
4426
- },
4427
- memoryUsage: null,
4428
- performance: null,
4429
- coverageFileUrl: null,
4430
- };
4431
- const onConsoleRef = { current: () => {} };
4432
- const stopSignal = { notify: () => {} };
4433
- const runtimeLabel = `${runtime.name}/${runtime.version}`;
4434
-
4435
- const runOperation = Abort.startOperation();
4436
- runOperation.addAbortSignal(signal);
4437
- let timeoutAbortSource;
4438
- if (allocatedMs) {
4439
- timeoutAbortSource = runOperation.timeout(allocatedMs);
4440
- }
4441
- const consoleCalls = [];
4442
- onConsoleRef.current = ({ type, text }) => {
4443
- if (mirrorConsole) {
4444
- if (type === "error") {
4445
- process.stderr.write(text);
4446
- } else {
4447
- process.stdout.write(text);
4448
- }
4055
+ const createIsInsideFragment = (fragment, total) => {
4056
+ let [dividend, divisor] = fragment.split("/");
4057
+ dividend = parseInt(dividend);
4058
+ divisor = parseInt(divisor);
4059
+ const groupSize = Math.ceil(total / divisor);
4060
+ let from;
4061
+ let to;
4062
+ if (groupSize === 0) {
4063
+ if (dividend === 1) {
4064
+ from = 0;
4065
+ to = 0;
4066
+ } else {
4067
+ from = Infinity;
4068
+ to = -1;
4449
4069
  }
4450
- if (collectConsole) {
4451
- consoleCalls.push({ type, text });
4070
+ } else if (dividend === 1) {
4071
+ from = 0;
4072
+ to = groupSize;
4073
+ } else {
4074
+ from = (dividend - 1) * groupSize;
4075
+ if (dividend === divisor) {
4076
+ to = total;
4077
+ } else {
4078
+ to = from + groupSize;
4452
4079
  }
4453
- };
4454
- if (collectConsole) {
4455
- result.consoleCalls = consoleCalls;
4456
4080
  }
4457
-
4458
- // we do not keep coverage in memory, it can grow very big
4459
- // instead we store it on the filesystem
4460
- // and they can be read later at "coverageFileUrl"
4461
- let coverageFileUrl;
4462
- if (coverageEnabled) {
4463
- coverageFileUrl = new URL(
4464
- `./${runtime.name}/${crypto.randomUUID()}.json`,
4465
- coverageTempDirectoryUrl,
4466
- ).href;
4467
- await ensureParentDirectories(coverageFileUrl);
4468
- result.coverageFileUrl = coverageFileUrl;
4469
- // written within the child_process/worker_thread or during runtime.run()
4470
- // for browsers
4471
- // (because it takes time to serialize and transfer the coverage object)
4472
- }
4473
-
4474
- try {
4475
- logger.debug(`run() ${runtimeLabel}`);
4476
- runOperation.throwIfAborted();
4477
- const winnerPromise = new Promise((resolve) => {
4478
- raceCallbacks(
4479
- {
4480
- aborted: (cb) => {
4481
- runOperation.signal.addEventListener("abort", cb);
4482
- return () => {
4483
- runOperation.signal.removeEventListener("abort", cb);
4484
- };
4485
- },
4486
- runned: async (cb) => {
4487
- let runtimeStatus = "starting";
4488
- try {
4489
- const runResult = await runtime.run({
4490
- signal: runOperation.signal,
4491
- logger,
4492
- ...runtimeParams,
4493
- collectConsole,
4494
- measureMemoryUsage,
4495
- onMeasureMemoryAvailable,
4496
- collectPerformance,
4497
- coverageFileUrl,
4498
- keepRunning,
4499
- stopSignal,
4500
- onConsole: (log) => onConsoleRef.current(log),
4501
- onRuntimeStarted: () => {
4502
- runtimeStatus = "started";
4503
- result.timings.runtimeStart = takeTiming();
4504
- },
4505
- onRuntimeStopped: () => {
4506
- if (runtimeStatus === "stopped") return; // ignore double calls
4507
- runtimeStatus = "stopped";
4508
- result.timings.runtimeEnd = takeTiming();
4509
- },
4510
- });
4511
- cb(runResult);
4512
- } catch (e) {
4513
- cb({
4514
- status: "failed",
4515
- errors: [e],
4516
- });
4517
- }
4518
- },
4519
- },
4520
- resolve,
4521
- );
4522
- });
4523
- const winner = await winnerPromise;
4524
- if (winner.name === "aborted") {
4525
- runOperation.throwIfAborted();
4526
- }
4527
- const {
4528
- status,
4529
- errors,
4530
- namespace,
4531
- timings = {},
4532
- memoryUsage,
4533
- performance,
4534
- } = winner.data;
4535
- result.status = status;
4536
- for (let error of errors) {
4537
- const errorProxy = normalizeRuntimeError(error);
4538
- result.errors.push(errorProxy);
4539
- }
4540
- result.namespace = namespace;
4541
- if (timings && typeof timings.start === "number") {
4542
- const diff = timings.origin - result.timings.origin;
4543
- result.timings.executionStart = timings.start + diff;
4544
- result.timings.executionEnd = timings.end + diff;
4545
-
4546
- if (result.timings.executionStart < result.timings.runtimeStart) {
4547
- // can happen for browsers where navigationStart can be earlier
4548
- // than when node.js calls runtimeStarted()
4549
- result.timings.runtimeStart = result.timings.executionStart;
4550
- }
4551
- }
4552
- result.memoryUsage =
4553
- typeof memoryUsage === "number"
4554
- ? memoryUsage < 0
4555
- ? 0
4556
- : memoryUsage
4557
- : memoryUsage;
4558
- result.performance = performance;
4559
- } catch (e) {
4560
- if (Abort.isAbortError(e)) {
4561
- if (timeoutAbortSource && timeoutAbortSource.signal.aborted) {
4562
- result.status = "timedout";
4563
- } else {
4564
- result.status = "aborted";
4565
- }
4566
- } else {
4567
- result.status = "failed";
4568
- result.errors.push(e);
4569
- }
4570
- } finally {
4571
- await runOperation.end();
4572
- result.timings.end = takeTiming();
4573
- return result;
4574
- }
4575
- };
4576
- const normalizeRuntimeError = (runtimeError) => {
4577
- // the goal here is to obtain a "regular error"
4578
- // that can be thrown using "throw" keyword
4579
- // so we wrap the error hapenning inside the runtime
4580
- // into "errorProxy" and put .stack property on it
4581
- // the other properties are set by defineProperty so they are not enumerable
4582
- // otherwise they would pollute the error displayed by Node.js
4583
- const errorProxy = new Error(runtimeError.message);
4584
- const exception = createException(runtimeError);
4585
- for (const ownPropertyName of Object.getOwnPropertyNames(exception)) {
4586
- Object.defineProperty(errorProxy, ownPropertyName, {
4587
- writable: true,
4588
- configurable: true,
4589
- value: runtimeError[ownPropertyName],
4590
- });
4591
- }
4592
- return errorProxy;
4593
- };
4081
+ return (index) => {
4082
+ if (index < from) return false;
4083
+ if (index >= to) return false;
4084
+ return true;
4085
+ };
4086
+ };
4594
4087
 
4595
4088
  // `Error: yo
4596
4089
  // at Object.execute (http://127.0.0.1:57300/build/src/__test__/file-throw.js:9:13)
@@ -5329,7 +4822,11 @@ const renderConsoleOutput = (consoleCalls) => {
5329
4822
  // and in that case the consoleCall is not suffixed with "\n"
5330
4823
  // we want to keep these calls together in the output
5331
4824
  const regroupedCalls = [];
4825
+ let everyCallIsLog = true;
5332
4826
  consoleCalls.forEach((consoleCall, index) => {
4827
+ if (everyCallIsLog && consoleCall.type !== "log") {
4828
+ everyCallIsLog = false;
4829
+ }
5333
4830
  if (index === 0) {
5334
4831
  regroupedCalls.push(consoleCall);
5335
4832
  return;
@@ -5351,30 +4848,38 @@ const renderConsoleOutput = (consoleCalls) => {
5351
4848
  const previousRegroupedCall = regroupedCalls[previousRegroupedCallIndex];
5352
4849
  previousRegroupedCall.text = `${previousRegroupedCall.text}${consoleCall.text}`;
5353
4850
  });
5354
-
5355
4851
  let consoleOutput = ``;
5356
- regroupedCalls.forEach((regroupedCall, index) => {
4852
+ let index = 0;
4853
+ for (const regroupedCall of regroupedCalls) {
5357
4854
  const text = regroupedCall.text;
5358
4855
  const textFormatted = prefixFirstAndIndentRemainingLines({
5359
- prefix: CONSOLE_ICONS[regroupedCall.type],
4856
+ prefix: everyCallIsLog ? "" : CONSOLE_ICONS[regroupedCall.type],
5360
4857
  text,
5361
4858
  trimLines: false,
5362
4859
  trimLastLine: index === regroupedCalls.length - 1,
5363
4860
  });
5364
4861
  consoleOutput += textFormatted;
5365
- });
4862
+ index++;
4863
+ }
5366
4864
  return consoleOutput;
5367
4865
  };
5368
4866
  const prefixFirstAndIndentRemainingLines = ({
5369
4867
  prefix,
5370
- indentation = " ",
4868
+ indentation,
5371
4869
  text,
5372
4870
  trimLines,
5373
4871
  trimLastLine,
5374
4872
  }) => {
5375
4873
  const lines = text.split(/\r?\n/);
5376
4874
  const firstLine = lines.shift();
5377
- let result = `${prefix} ${firstLine}`;
4875
+ if (indentation === undefined) {
4876
+ if (prefix) {
4877
+ indentation = " "; // prefix + space
4878
+ } else {
4879
+ indentation = "";
4880
+ }
4881
+ }
4882
+ let result = prefix ? `${prefix} ${firstLine}` : firstLine;
5378
4883
  let i = 0;
5379
4884
  while (i < lines.length) {
5380
4885
  const line = trimLines ? lines[i].trim() : lines[i];
@@ -5385,316 +4890,823 @@ const prefixFirstAndIndentRemainingLines = ({
5385
4890
  ? ""
5386
4891
  : `\n`;
5387
4892
  }
5388
- return result;
5389
- };
5390
- const CONSOLE_ICONS = {
5391
- get debug() {
5392
- return UNICODE.DEBUG;
5393
- },
5394
- get info() {
5395
- return UNICODE.INFO;
5396
- },
5397
- get warning() {
5398
- return UNICODE.WARNING;
5399
- },
5400
- get error() {
5401
- return UNICODE.FAILURE;
5402
- },
5403
- log: " ",
4893
+ return result;
4894
+ };
4895
+ const CONSOLE_ICONS = {
4896
+ get debug() {
4897
+ return UNICODE.DEBUG;
4898
+ },
4899
+ get info() {
4900
+ return UNICODE.INFO;
4901
+ },
4902
+ get warning() {
4903
+ return UNICODE.WARNING;
4904
+ },
4905
+ get error() {
4906
+ return UNICODE.FAILURE;
4907
+ },
4908
+ log: " ",
4909
+ };
4910
+
4911
+ const renderErrors = (execution, logOptions) => {
4912
+ const { errors = [] } = execution.result;
4913
+ if (errors.length === 0) {
4914
+ return "";
4915
+ }
4916
+
4917
+ if (errors.length === 1) {
4918
+ return renderSection({
4919
+ dashColor: ANSI.GREY,
4920
+ title: "error",
4921
+ content: formatErrorForTerminal(errors[0], {
4922
+ rootDirectoryUrl: execution.rootDirectoryUrl,
4923
+ mainFileRelativeUrl: execution.fileRelativeUrl,
4924
+ mockFluctuatingValues: logOptions.mockFluctuatingValues,
4925
+ tryColors: true,
4926
+ }),
4927
+ });
4928
+ }
4929
+
4930
+ let output = [];
4931
+ errors.forEach((error) => {
4932
+ output.push(
4933
+ prefixFirstAndIndentRemainingLines({
4934
+ prefix: `${UNICODE.CIRCLE_CROSS} `,
4935
+ indentation: " ",
4936
+ text: formatErrorForTerminal(error, {
4937
+ rootDirectoryUrl: execution.rootDirectoryUrl,
4938
+ mainFileRelativeUrl: execution.fileRelativeUrl,
4939
+ mockFluctuatingValues: logOptions.mockFluctuatingValues,
4940
+ tryColors: true,
4941
+ }),
4942
+ }),
4943
+ );
4944
+ });
4945
+ return renderSection({
4946
+ dashColor: ANSI.GREY,
4947
+ title: `errors (${errors.length})`,
4948
+ content: output.join(`\n`),
4949
+ });
4950
+ };
4951
+
4952
+ const renderOutro = (testPlanResult, logOptions = {}) => {
4953
+ const { counters } = testPlanResult;
4954
+ const { planified } = counters;
4955
+ if (planified === 0) {
4956
+ return "";
4957
+ }
4958
+ let title;
4959
+ if (planified === 0) {
4960
+ title = `no execution`;
4961
+ } else if (planified === 1) {
4962
+ title = `1 execution done`;
4963
+ } else {
4964
+ title = `${planified} executions done`;
4965
+ }
4966
+ return `${renderBigSection({
4967
+ title,
4968
+ content: renderOutroContent(testPlanResult, logOptions),
4969
+ })}\n`;
4970
+ };
4971
+
4972
+ const renderOutroContent = (testPlanResult, logOptions = {}) => {
4973
+ const lines = [];
4974
+ const { counters } = testPlanResult;
4975
+
4976
+ let executionLine = `status: ${renderStatusRepartition(counters)}`;
4977
+ lines.push(executionLine);
4978
+
4979
+ let durationLine = `duration: `;
4980
+ const { timings } = testPlanResult;
4981
+ if (logOptions.mockFluctuatingValues) {
4982
+ durationLine += "<mock>s";
4983
+ } else {
4984
+ durationLine += humanizeDuration(timings.end, { short: true });
4985
+ const namedTimings = {
4986
+ setup: humanizeTiming(timings.executionStart),
4987
+ execution: humanizeTiming(timings.executionEnd),
4988
+ teardown: humanizeTiming(timings.teardownEnd - timings.executionEnd),
4989
+ ...(testPlanResult.coverage
4990
+ ? {
4991
+ coverage: humanizeTiming(
4992
+ timings.coverageTeardownEnd - timings.teardownEnd,
4993
+ ),
4994
+ }
4995
+ : {}),
4996
+ };
4997
+ durationLine += renderDetails(namedTimings);
4998
+ }
4999
+ lines.push(durationLine);
5000
+
5001
+ if (logOptions.cpuUsage) {
5002
+ const processCpuUsage = testPlanResult.cpuUsage.process;
5003
+ let cpuUsageLine = "cpu: ";
5004
+ cpuUsageLine += `${humanizeProcessCpuUsage(processCpuUsage.end)}`;
5005
+ cpuUsageLine += renderDetails({
5006
+ med: humanizeProcessCpuUsage(processCpuUsage.median),
5007
+ min: humanizeProcessCpuUsage(processCpuUsage.min),
5008
+ max: humanizeProcessCpuUsage(processCpuUsage.max),
5009
+ });
5010
+ lines.push(cpuUsageLine);
5011
+ }
5012
+ if (logOptions.memoryUsage) {
5013
+ const processMemoryUsage = testPlanResult.memoryUsage.process;
5014
+ let memoryUsageLine = "memory: ";
5015
+ memoryUsageLine += `${humanizeProcessMemoryUsage(processMemoryUsage.end)}`;
5016
+ memoryUsageLine += renderDetails({
5017
+ med: humanizeProcessMemoryUsage(processMemoryUsage.median),
5018
+ min: humanizeProcessMemoryUsage(processMemoryUsage.min),
5019
+ max: humanizeProcessMemoryUsage(processMemoryUsage.max),
5020
+ });
5021
+ lines.push(memoryUsageLine);
5022
+ }
5023
+ return lines.join("\n");
5024
+ };
5025
+
5026
+ const humanizeTiming = (value) => {
5027
+ return humanizeDuration(value, { short: true });
5028
+ };
5029
+
5030
+ const humanizeProcessCpuUsage = (ratio) => {
5031
+ const percentageAsNumber = ratio * 100;
5032
+ const percentageAsNumberRounded = Math.round(percentageAsNumber);
5033
+ const percentage = `${percentageAsNumberRounded}%`;
5034
+ return percentage;
5035
+ };
5036
+
5037
+ const humanizeProcessMemoryUsage = (value) => {
5038
+ return humanizeMemory(value, { short: true, decimals: 0 });
5039
+ };
5040
+
5041
+ const renderStatusRepartition = (counters, { showProgression } = {}) => {
5042
+ if (counters.planified === 0) {
5043
+ return ``;
5044
+ }
5045
+ if (counters.skipped === counters.planified) {
5046
+ return `all ${ANSI.color(`skipped`, COLOR_SKIPPED)}`;
5047
+ }
5048
+ if (counters.aborted === counters.planified) {
5049
+ return `all ${ANSI.color(`aborted`, COLOR_ABORTED)}`;
5050
+ }
5051
+ if (counters.cancelled === counters.planified) {
5052
+ return `all ${ANSI.color(`cancelled`, COLOR_CANCELLED)}`;
5053
+ }
5054
+ if (counters.timedout === counters.planified) {
5055
+ return `all ${ANSI.color(`timed out`, COLOR_TIMEOUT)}`;
5056
+ }
5057
+ if (counters.failed === counters.planified) {
5058
+ return `all ${ANSI.color(`failed`, COLOR_FAILED)}`;
5059
+ }
5060
+ if (counters.completed === counters.planified) {
5061
+ return `all ${ANSI.color(`completed`, COLOR_COMPLETED)}`;
5062
+ }
5063
+ const parts = [];
5064
+ if (showProgression) {
5065
+ if (counters.executing) {
5066
+ parts.push(`${counters.executing} executing`);
5067
+ }
5068
+ if (counters.waiting) {
5069
+ parts.push(`${counters.waiting} waiting`);
5070
+ }
5071
+ }
5072
+ if (counters.timedout) {
5073
+ parts.push(
5074
+ `${counters.timedout} ${ANSI.color(`timed out`, COLOR_TIMEOUT)}`,
5075
+ );
5076
+ }
5077
+ if (counters.failed) {
5078
+ parts.push(`${counters.failed} ${ANSI.color(`failed`, COLOR_FAILED)}`);
5079
+ }
5080
+ if (counters.completed) {
5081
+ parts.push(
5082
+ `${counters.completed} ${ANSI.color(`completed`, COLOR_COMPLETED)}`,
5083
+ );
5084
+ }
5085
+ if (counters.aborted) {
5086
+ parts.push(`${counters.aborted} ${ANSI.color(`aborted`, COLOR_ABORTED)}`);
5087
+ }
5088
+ if (counters.cancelled) {
5089
+ parts.push(
5090
+ `${counters.cancelled} ${ANSI.color(`cancelled`, COLOR_CANCELLED)}`,
5091
+ );
5092
+ }
5093
+ if (counters.skipped) {
5094
+ parts.push(`${counters.skipped} ${ANSI.color(`skipped`, COLOR_SKIPPED)}`);
5095
+ }
5096
+ return `${parts.join(", ")}`;
5097
+ };
5098
+
5099
+ const renderBigSection = (params) => {
5100
+ return renderSection({
5101
+ width: 45,
5102
+ ...params,
5103
+ });
5104
+ };
5105
+
5106
+ const renderSection = ({
5107
+ title,
5108
+ content,
5109
+ dashColor = ANSI.GREY,
5110
+ width = 38,
5111
+ bottomSeparator = true,
5112
+ }) => {
5113
+ let section = "";
5114
+
5115
+ if (title) {
5116
+ const titleWidth = stripAnsi(title).length;
5117
+ const minWidthRequired = `--- … ---`.length;
5118
+ const needsTruncate = titleWidth + minWidthRequired >= width;
5119
+ if (needsTruncate) {
5120
+ const titleTruncated = title.slice(0, width - minWidthRequired);
5121
+ const leftDashes = ANSI.color("---", dashColor);
5122
+ const rightDashes = ANSI.color("---", dashColor);
5123
+ section += `${leftDashes} ${titleTruncated}… ${rightDashes}`;
5124
+ } else {
5125
+ const remainingWidth = width - titleWidth - 2; // 2 for spaces around the title
5126
+ const dashLeftCount = Math.floor(remainingWidth / 2);
5127
+ const dashRightCount = remainingWidth - dashLeftCount;
5128
+ const leftDashes = ANSI.color("-".repeat(dashLeftCount), dashColor);
5129
+ const rightDashes = ANSI.color("-".repeat(dashRightCount), dashColor);
5130
+ section += `${leftDashes} ${title} ${rightDashes}`;
5131
+ }
5132
+ section += "\n";
5133
+ } else {
5134
+ const topDashes = ANSI.color(`-`.repeat(width), dashColor);
5135
+ section += topDashes;
5136
+ section += "\n";
5137
+ }
5138
+ section += `${content}`;
5139
+ if (bottomSeparator) {
5140
+ section += "\n";
5141
+ const bottomDashes = ANSI.color(`-`.repeat(width), dashColor);
5142
+ section += bottomDashes;
5143
+ }
5144
+ return section;
5145
+ };
5146
+
5147
+ const renderDetails = (data) => {
5148
+ const details = [];
5149
+ for (const key of Object.keys(data)) {
5150
+ const value = data[key];
5151
+ let valueString = "";
5152
+ valueString += ANSI.color(`${key}:`, ANSI.GREY);
5153
+ const useNonGreyAnsiColor =
5154
+ typeof value === "string" && value.includes("\x1b");
5155
+ valueString += " ";
5156
+ valueString += useNonGreyAnsiColor
5157
+ ? value
5158
+ : ANSI.color(String(value), ANSI.GREY);
5159
+ details.push(valueString);
5160
+ }
5161
+ if (details.length === 0) {
5162
+ return "";
5163
+ }
5164
+
5165
+ let string = "";
5166
+ string += ` ${ANSI.color("(", ANSI.GREY)}`;
5167
+ string += details.join(ANSI.color(", ", ANSI.GREY));
5168
+ string += ANSI.color(")", ANSI.GREY);
5169
+ return string;
5170
+ };
5171
+
5172
+ /*
5173
+ * Export a function capable to run a file on a runtime.
5174
+ *
5175
+ * - Used internally by "executeTestPlan" part of the documented API
5176
+ * - Used internally by "execute" an advanced API not documented
5177
+ * - logs generated during file execution can be collected
5178
+ * - logs generated during file execution can be mirrored (re-logged to the console)
5179
+ * - File is given allocatedMs to complete
5180
+ * - Errors are collected
5181
+ * - File execution result is returned, it contains status/errors/namespace/consoleCalls
5182
+ */
5183
+
5184
+
5185
+ const run = async ({
5186
+ signal = new AbortController().signal,
5187
+ logger,
5188
+ allocatedMs,
5189
+ keepRunning = false,
5190
+ mirrorConsole = false,
5191
+ collectConsole = false,
5192
+ measureMemoryUsage = false,
5193
+ onMeasureMemoryAvailable,
5194
+ collectPerformance = false,
5195
+ coverageEnabled = false,
5196
+ coverageTempDirectoryUrl,
5197
+ runtime,
5198
+ runtimeParams,
5199
+ }) => {
5200
+ if (keepRunning) {
5201
+ allocatedMs = Infinity;
5202
+ }
5203
+ if (allocatedMs === Infinity) {
5204
+ allocatedMs = 0;
5205
+ }
5206
+
5207
+ const timingsOrigin = Date.now();
5208
+ const takeTiming = () => {
5209
+ return Date.now() - timingsOrigin;
5210
+ };
5211
+ const result = {
5212
+ status: "pending",
5213
+ errors: [],
5214
+ namespace: null,
5215
+ consoleCalls: null,
5216
+ timings: {
5217
+ origin: timingsOrigin,
5218
+ start: 0,
5219
+ runtimeStart: null,
5220
+ executionStart: null,
5221
+ executionEnd: null,
5222
+ runtimeEnd: null,
5223
+ end: null,
5224
+ },
5225
+ memoryUsage: null,
5226
+ performance: null,
5227
+ coverageFileUrl: null,
5228
+ };
5229
+ const onConsoleRef = { current: () => {} };
5230
+ const stopSignal = { notify: () => {} };
5231
+ const runtimeLabel = `${runtime.name}/${runtime.version}`;
5232
+
5233
+ const runOperation = Abort.startOperation();
5234
+ runOperation.addAbortSignal(signal);
5235
+ let timeoutAbortSource;
5236
+ if (allocatedMs) {
5237
+ timeoutAbortSource = runOperation.timeout(allocatedMs);
5238
+ }
5239
+ const consoleCalls = [];
5240
+ onConsoleRef.current = ({ type, text }) => {
5241
+ if (mirrorConsole) {
5242
+ if (type === "error") {
5243
+ process.stderr.write(text);
5244
+ } else {
5245
+ process.stdout.write(text);
5246
+ }
5247
+ }
5248
+ if (collectConsole) {
5249
+ consoleCalls.push({ type, text });
5250
+ }
5251
+ };
5252
+ if (collectConsole) {
5253
+ result.consoleCalls = consoleCalls;
5254
+ }
5255
+
5256
+ // we do not keep coverage in memory, it can grow very big
5257
+ // instead we store it on the filesystem
5258
+ // and they can be read later at "coverageFileUrl"
5259
+ let coverageFileUrl;
5260
+ if (coverageEnabled) {
5261
+ coverageFileUrl = new URL(
5262
+ `./${runtime.name}/${crypto.randomUUID()}.json`,
5263
+ coverageTempDirectoryUrl,
5264
+ ).href;
5265
+ await ensureParentDirectories(coverageFileUrl);
5266
+ result.coverageFileUrl = coverageFileUrl;
5267
+ // written within the child_process/worker_thread or during runtime.run()
5268
+ // for browsers
5269
+ // (because it takes time to serialize and transfer the coverage object)
5270
+ }
5271
+
5272
+ try {
5273
+ logger.debug(`run() ${runtimeLabel}`);
5274
+ runOperation.throwIfAborted();
5275
+ const winnerPromise = new Promise((resolve) => {
5276
+ raceCallbacks(
5277
+ {
5278
+ aborted: (cb) => {
5279
+ runOperation.signal.addEventListener("abort", cb);
5280
+ return () => {
5281
+ runOperation.signal.removeEventListener("abort", cb);
5282
+ };
5283
+ },
5284
+ runned: async (cb) => {
5285
+ let runtimeStatus = "starting";
5286
+ try {
5287
+ const runResult = await runtime.run({
5288
+ signal: runOperation.signal,
5289
+ logger,
5290
+ ...runtimeParams,
5291
+ collectConsole,
5292
+ measureMemoryUsage,
5293
+ onMeasureMemoryAvailable,
5294
+ collectPerformance,
5295
+ coverageFileUrl,
5296
+ keepRunning,
5297
+ stopSignal,
5298
+ onConsole: (log) => onConsoleRef.current(log),
5299
+ onRuntimeStarted: () => {
5300
+ runtimeStatus = "started";
5301
+ result.timings.runtimeStart = takeTiming();
5302
+ },
5303
+ onRuntimeStopped: () => {
5304
+ if (runtimeStatus === "stopped") return; // ignore double calls
5305
+ runtimeStatus = "stopped";
5306
+ result.timings.runtimeEnd = takeTiming();
5307
+ },
5308
+ });
5309
+ cb(runResult);
5310
+ } catch (e) {
5311
+ cb({
5312
+ status: "failed",
5313
+ errors: [e],
5314
+ });
5315
+ }
5316
+ },
5317
+ },
5318
+ resolve,
5319
+ );
5320
+ });
5321
+ const winner = await winnerPromise;
5322
+ if (winner.name === "aborted") {
5323
+ runOperation.throwIfAborted();
5324
+ }
5325
+ const {
5326
+ status,
5327
+ errors,
5328
+ namespace,
5329
+ timings = {},
5330
+ memoryUsage,
5331
+ performance,
5332
+ } = winner.data;
5333
+ result.status = status;
5334
+ for (let error of errors) {
5335
+ const errorProxy = normalizeRuntimeError(error);
5336
+ result.errors.push(errorProxy);
5337
+ }
5338
+ result.namespace = namespace;
5339
+ if (timings && typeof timings.start === "number") {
5340
+ const diff = timings.origin - result.timings.origin;
5341
+ result.timings.executionStart = timings.start + diff;
5342
+ result.timings.executionEnd = timings.end + diff;
5343
+
5344
+ if (result.timings.executionStart < result.timings.runtimeStart) {
5345
+ // can happen for browsers where navigationStart can be earlier
5346
+ // than when node.js calls runtimeStarted()
5347
+ result.timings.runtimeStart = result.timings.executionStart;
5348
+ }
5349
+ }
5350
+ result.memoryUsage =
5351
+ typeof memoryUsage === "number"
5352
+ ? memoryUsage < 0
5353
+ ? 0
5354
+ : memoryUsage
5355
+ : memoryUsage;
5356
+ result.performance = performance;
5357
+ } catch (e) {
5358
+ if (Abort.isAbortError(e)) {
5359
+ if (timeoutAbortSource && timeoutAbortSource.signal.aborted) {
5360
+ result.status = "timedout";
5361
+ } else {
5362
+ result.status = "aborted";
5363
+ }
5364
+ } else {
5365
+ result.status = "failed";
5366
+ result.errors.push(e);
5367
+ }
5368
+ } finally {
5369
+ await runOperation.end();
5370
+ result.timings.end = takeTiming();
5371
+ return result;
5372
+ }
5404
5373
  };
5405
-
5406
- const renderErrors = (execution, logOptions) => {
5407
- const { errors = [] } = execution.result;
5408
- if (errors.length === 0) {
5409
- return "";
5410
- }
5411
-
5412
- if (errors.length === 1) {
5413
- return renderSection({
5414
- dashColor: ANSI.GREY,
5415
- title: "error",
5416
- content: formatErrorForTerminal(errors[0], {
5417
- rootDirectoryUrl: execution.rootDirectoryUrl,
5418
- mainFileRelativeUrl: execution.fileRelativeUrl,
5419
- mockFluctuatingValues: logOptions.mockFluctuatingValues,
5420
- tryColors: true,
5421
- }),
5374
+ const normalizeRuntimeError = (runtimeError) => {
5375
+ // the goal here is to obtain a "regular error"
5376
+ // that can be thrown using "throw" keyword
5377
+ // so we wrap the error hapenning inside the runtime
5378
+ // into "errorProxy" and put .stack property on it
5379
+ // the other properties are set by defineProperty so they are not enumerable
5380
+ // otherwise they would pollute the error displayed by Node.js
5381
+ const errorProxy = new Error(runtimeError.message);
5382
+ const exception = createException(runtimeError);
5383
+ for (const ownPropertyName of Object.getOwnPropertyNames(exception)) {
5384
+ Object.defineProperty(errorProxy, ownPropertyName, {
5385
+ writable: true,
5386
+ configurable: true,
5387
+ value: runtimeError[ownPropertyName],
5422
5388
  });
5423
5389
  }
5424
-
5425
- let output = [];
5426
- errors.forEach((error) => {
5427
- output.push(
5428
- prefixFirstAndIndentRemainingLines({
5429
- prefix: `${UNICODE.CIRCLE_CROSS} `,
5430
- indentation: " ",
5431
- text: formatErrorForTerminal(error, {
5432
- rootDirectoryUrl: execution.rootDirectoryUrl,
5433
- mainFileRelativeUrl: execution.fileRelativeUrl,
5434
- mockFluctuatingValues: logOptions.mockFluctuatingValues,
5435
- tryColors: true,
5436
- }),
5437
- }),
5438
- );
5439
- });
5440
- return renderSection({
5441
- dashColor: ANSI.GREY,
5442
- title: `errors (${errors.length})`,
5443
- content: output.join(`\n`),
5444
- });
5390
+ return errorProxy;
5445
5391
  };
5446
5392
 
5447
- const renderOutro = (testPlanResult, logOptions = {}) => {
5448
- const { counters } = testPlanResult;
5449
- const { planified } = counters;
5450
- if (planified === 0) {
5451
- return "";
5452
- }
5453
- let title;
5454
- if (planified === 0) {
5455
- title = `no execution`;
5456
- } else if (planified === 1) {
5457
- title = `1 execution done`;
5393
+ const basicFetch = async (
5394
+ url,
5395
+ { rejectUnauthorized = true, method = "GET", headers = {} } = {},
5396
+ ) => {
5397
+ let requestModule;
5398
+ if (url.startsWith("http:")) {
5399
+ requestModule = await import("node:http");
5458
5400
  } else {
5459
- title = `${planified} executions done`;
5401
+ requestModule = await import("node:https");
5460
5402
  }
5461
- return `${renderBigSection({
5462
- title,
5463
- content: renderOutroContent(testPlanResult, logOptions),
5464
- })}\n`;
5465
- };
5466
-
5467
- const renderOutroContent = (testPlanResult, logOptions = {}) => {
5468
- const lines = [];
5469
- const { counters } = testPlanResult;
5470
-
5471
- let executionLine = `status: ${renderStatusRepartition(counters)}`;
5472
- lines.push(executionLine);
5403
+ const { request } = requestModule;
5473
5404
 
5474
- let durationLine = `duration: `;
5475
- const { timings } = testPlanResult;
5476
- if (logOptions.mockFluctuatingValues) {
5477
- durationLine += "<mock>s";
5478
- } else {
5479
- durationLine += humanizeDuration(timings.end, { short: true });
5480
- const namedTimings = {
5481
- setup: humanizeTiming(timings.executionStart),
5482
- execution: humanizeTiming(timings.executionEnd),
5483
- teardown: humanizeTiming(timings.teardownEnd - timings.executionEnd),
5484
- ...(testPlanResult.coverage
5485
- ? {
5486
- coverage: humanizeTiming(
5487
- timings.coverageTeardownEnd - timings.teardownEnd,
5488
- ),
5489
- }
5490
- : {}),
5491
- };
5492
- durationLine += renderDetails(namedTimings);
5493
- }
5494
- lines.push(durationLine);
5405
+ const urlObject = new URL(url);
5495
5406
 
5496
- if (logOptions.cpuUsage) {
5497
- const processCpuUsage = testPlanResult.cpuUsage.process;
5498
- let cpuUsageLine = "cpu: ";
5499
- cpuUsageLine += `${humanizeProcessCpuUsage(processCpuUsage.end)}`;
5500
- cpuUsageLine += renderDetails({
5501
- med: humanizeProcessCpuUsage(processCpuUsage.median),
5502
- min: humanizeProcessCpuUsage(processCpuUsage.min),
5503
- max: humanizeProcessCpuUsage(processCpuUsage.max),
5407
+ return new Promise((resolve, reject) => {
5408
+ const req = request({
5409
+ rejectUnauthorized,
5410
+ hostname: urlObject.hostname,
5411
+ port: urlObject.port,
5412
+ path: urlObject.pathname,
5413
+ method,
5414
+ headers,
5504
5415
  });
5505
- lines.push(cpuUsageLine);
5506
- }
5507
- if (logOptions.memoryUsage) {
5508
- const processMemoryUsage = testPlanResult.memoryUsage.process;
5509
- let memoryUsageLine = "memory: ";
5510
- memoryUsageLine += `${humanizeProcessMemoryUsage(processMemoryUsage.end)}`;
5511
- memoryUsageLine += renderDetails({
5512
- med: humanizeProcessMemoryUsage(processMemoryUsage.median),
5513
- min: humanizeProcessMemoryUsage(processMemoryUsage.min),
5514
- max: humanizeProcessMemoryUsage(processMemoryUsage.max),
5416
+ req.on("response", (response) => {
5417
+ resolve({
5418
+ status: response.statusCode,
5419
+ headers: response.headers,
5420
+ json: () => {
5421
+ req.setTimeout(0);
5422
+ req.destroy();
5423
+ return new Promise((resolve) => {
5424
+ if (response.headers["content-type"] !== "application/json") {
5425
+ console.warn("not json");
5426
+ }
5427
+ let responseBody = "";
5428
+ response.setEncoding("utf8");
5429
+ response.on("data", (chunk) => {
5430
+ responseBody += chunk;
5431
+ });
5432
+ response.on("end", () => {
5433
+ resolve(JSON.parse(responseBody));
5434
+ });
5435
+ response.on("error", (e) => {
5436
+ reject(e);
5437
+ });
5438
+ });
5439
+ },
5440
+ });
5515
5441
  });
5516
- lines.push(memoryUsageLine);
5517
- }
5518
- return lines.join("\n");
5519
- };
5520
-
5521
- const humanizeTiming = (value) => {
5522
- return humanizeDuration(value, { short: true });
5523
- };
5524
-
5525
- const humanizeProcessCpuUsage = (ratio) => {
5526
- const percentageAsNumber = ratio * 100;
5527
- const percentageAsNumberRounded = Math.round(percentageAsNumber);
5528
- const percentage = `${percentageAsNumberRounded}%`;
5529
- return percentage;
5442
+ req.on("error", reject);
5443
+ req.end();
5444
+ });
5530
5445
  };
5531
5446
 
5532
- const humanizeProcessMemoryUsage = (value) => {
5533
- return humanizeMemory(value, { short: true, decimals: 0 });
5534
- };
5447
+ const pingServer = async (url) => {
5448
+ const server = createServer();
5449
+ const { hostname, port } = new URL(url);
5535
5450
 
5536
- const renderStatusRepartition = (counters, { showProgression } = {}) => {
5537
- if (counters.planified === 0) {
5538
- return ``;
5539
- }
5540
- if (counters.skipped === counters.planified) {
5541
- return `all ${ANSI.color(`skipped`, COLOR_SKIPPED)}`;
5542
- }
5543
- if (counters.aborted === counters.planified) {
5544
- return `all ${ANSI.color(`aborted`, COLOR_ABORTED)}`;
5545
- }
5546
- if (counters.cancelled === counters.planified) {
5547
- return `all ${ANSI.color(`cancelled`, COLOR_CANCELLED)}`;
5548
- }
5549
- if (counters.timedout === counters.planified) {
5550
- return `all ${ANSI.color(`timed out`, COLOR_TIMEOUT)}`;
5551
- }
5552
- if (counters.failed === counters.planified) {
5553
- return `all ${ANSI.color(`failed`, COLOR_FAILED)}`;
5554
- }
5555
- if (counters.completed === counters.planified) {
5556
- return `all ${ANSI.color(`completed`, COLOR_COMPLETED)}`;
5557
- }
5558
- const parts = [];
5559
- if (showProgression) {
5560
- if (counters.executing) {
5561
- parts.push(`${counters.executing} executing`);
5562
- }
5563
- if (counters.waiting) {
5564
- parts.push(`${counters.waiting} waiting`);
5565
- }
5566
- }
5567
- if (counters.timedout) {
5568
- parts.push(
5569
- `${counters.timedout} ${ANSI.color(`timed out`, COLOR_TIMEOUT)}`,
5570
- );
5571
- }
5572
- if (counters.failed) {
5573
- parts.push(`${counters.failed} ${ANSI.color(`failed`, COLOR_FAILED)}`);
5574
- }
5575
- if (counters.completed) {
5576
- parts.push(
5577
- `${counters.completed} ${ANSI.color(`completed`, COLOR_COMPLETED)}`,
5578
- );
5579
- }
5580
- if (counters.aborted) {
5581
- parts.push(`${counters.aborted} ${ANSI.color(`aborted`, COLOR_ABORTED)}`);
5582
- }
5583
- if (counters.cancelled) {
5584
- parts.push(
5585
- `${counters.cancelled} ${ANSI.color(`cancelled`, COLOR_CANCELLED)}`,
5586
- );
5587
- }
5588
- if (counters.skipped) {
5589
- parts.push(`${counters.skipped} ${ANSI.color(`skipped`, COLOR_SKIPPED)}`);
5451
+ try {
5452
+ await new Promise((resolve, reject) => {
5453
+ server.on("error", reject);
5454
+ server.on("listening", () => {
5455
+ resolve();
5456
+ });
5457
+ server.listen(port, hostname);
5458
+ });
5459
+ } catch (error) {
5460
+ if (error && error.code === "EADDRINUSE") {
5461
+ return true;
5462
+ }
5463
+ if (error && error.code === "EACCES") {
5464
+ return true;
5465
+ }
5466
+ throw error;
5590
5467
  }
5591
- return `${parts.join(", ")}`;
5468
+ await new Promise((resolve, reject) => {
5469
+ server.on("error", reject);
5470
+ server.on("close", resolve);
5471
+ server.close();
5472
+ });
5473
+ return false;
5592
5474
  };
5593
5475
 
5594
- const renderBigSection = (params) => {
5595
- return renderSection({
5596
- width: 45,
5597
- ...params,
5476
+ const startServerUsingCommand = async (
5477
+ webServer,
5478
+ { signal, allocatedMs, logger, teardownCallbackSet },
5479
+ ) => {
5480
+ const spawnedProcess = spawn(webServer.command, [], {
5481
+ // On non-windows platforms, `detached: true` makes child process a leader of a new
5482
+ // process group, making it possible to kill child process tree with `.kill(-pid)` command.
5483
+ // @see https://nodejs.org/api/child_process.html#child_process_options_detached
5484
+ detached: process.platform !== "win32",
5485
+ stdio: ["pipe", "pipe", "pipe"],
5486
+ shell: true,
5487
+ cwd: webServer.cwd,
5598
5488
  });
5599
- };
5489
+ if (!spawnedProcess.pid) {
5490
+ await new Promise((resolve, reject) => {
5491
+ spawnedProcess.once("error", (error) => {
5492
+ reject(new Error(`Failed to launch: ${error}`));
5493
+ });
5494
+ });
5495
+ }
5600
5496
 
5601
- const renderSection = ({
5602
- title,
5603
- content,
5604
- dashColor = ANSI.GREY,
5605
- width = 38,
5606
- bottomSeparator = true,
5607
- }) => {
5608
- let section = "";
5497
+ let errorReceived = false;
5498
+ const errorPromise = new Promise((resolve, reject) => {
5499
+ spawnedProcess.on("error", (e) => {
5500
+ errorReceived = true;
5501
+ reject(e);
5502
+ });
5503
+ });
5609
5504
 
5610
- if (title) {
5611
- const titleWidth = stripAnsi(title).length;
5612
- const minWidthRequired = `--- … ---`.length;
5613
- const needsTruncate = titleWidth + minWidthRequired >= width;
5614
- if (needsTruncate) {
5615
- const titleTruncated = title.slice(0, width - minWidthRequired);
5616
- const leftDashes = ANSI.color("---", dashColor);
5617
- const rightDashes = ANSI.color("---", dashColor);
5618
- section += `${leftDashes} ${titleTruncated}… ${rightDashes}`;
5619
- } else {
5620
- const remainingWidth = width - titleWidth - 2; // 2 for spaces around the title
5621
- const dashLeftCount = Math.floor(remainingWidth / 2);
5622
- const dashRightCount = remainingWidth - dashLeftCount;
5623
- const leftDashes = ANSI.color("-".repeat(dashLeftCount), dashColor);
5624
- const rightDashes = ANSI.color("-".repeat(dashRightCount), dashColor);
5625
- section += `${leftDashes} ${title} ${rightDashes}`;
5505
+ // const stdout = readline.createInterface({ input: spawnedProcess.stdout });
5506
+ // stdout.on("line", () => {
5507
+ // logger.debug(`[pid=${spawnedProcess.pid}][out] ${data}`);
5508
+ // });
5509
+ // const stderr = readline.createInterface({ input: spawnedProcess.stderr });
5510
+ // stderr.on("line", (data) => {
5511
+ // logger.debug(`[pid=${spawnedProcess.pid}][err] ${data}`);
5512
+ // });
5513
+ let processClosed = false;
5514
+ const closedPromise = new Promise((resolve) => {
5515
+ spawnedProcess.once("exit", (exitCode, signal) => {
5516
+ logger.debug(
5517
+ `[pid=${spawnedProcess.pid}] <process did exit: exitCode=${exitCode}, signal=${signal}>`,
5518
+ );
5519
+ processClosed = true;
5520
+ resolve();
5521
+ });
5522
+ });
5523
+ const killProcess = async () => {
5524
+ logger.debug(`[pid=${spawnedProcess.pid}] <kill>`);
5525
+ if (!spawnedProcess.pid || spawnedProcess.killed || processClosed) {
5526
+ logger.debug(
5527
+ `[pid=${spawnedProcess.pid}] <skipped force kill spawnedProcess.killed=${spawnedProcess.killed} processClosed=${processClosed}>`,
5528
+ );
5529
+ return;
5626
5530
  }
5627
- section += "\n";
5628
- } else {
5629
- const topDashes = ANSI.color(`-`.repeat(width), dashColor);
5630
- section += topDashes;
5631
- section += "\n";
5632
- }
5633
- section += `${content}`;
5634
- if (bottomSeparator) {
5635
- section += "\n";
5636
- const bottomDashes = ANSI.color(`-`.repeat(width), dashColor);
5637
- section += bottomDashes;
5531
+ logger.debug(`[pid=${spawnedProcess.pid}] <will force kill>`);
5532
+ // Force kill the browser.
5533
+ try {
5534
+ if (process.platform === "win32") {
5535
+ const taskkillProcess = spawnSync(
5536
+ `taskkill /pid ${spawnedProcess.pid} /T /F`,
5537
+ { shell: true },
5538
+ );
5539
+ const [stdout, stderr] = [
5540
+ taskkillProcess.stdout.toString(),
5541
+ taskkillProcess.stderr.toString(),
5542
+ ];
5543
+ if (stdout)
5544
+ logger.debug(
5545
+ `[pid=${spawnedProcess.pid}] taskkill stdout: ${stdout}`,
5546
+ );
5547
+ if (stderr)
5548
+ logger.info(`[pid=${spawnedProcess.pid}] taskkill stderr: ${stderr}`);
5549
+ } else {
5550
+ process.kill(-spawnedProcess.pid, "SIGKILL");
5551
+ }
5552
+ } catch (e) {
5553
+ logger.info(
5554
+ `[pid=${spawnedProcess.pid}] exception while trying to kill process: ${e}`,
5555
+ );
5556
+ // the process might have already stopped
5557
+ }
5558
+ await closedPromise;
5559
+ };
5560
+
5561
+ const startOperation = Abort.startOperation();
5562
+ startOperation.addAbortSignal(signal);
5563
+ const timeoutAbortSource = startOperation.timeout(allocatedMs);
5564
+ startOperation.addAbortCallback(killProcess);
5565
+ teardownCallbackSet.add(killProcess);
5566
+
5567
+ const startedPromise = (async () => {
5568
+ const logScale = [100, 250, 500];
5569
+ // eslint-disable-next-line no-constant-condition
5570
+ while (true) {
5571
+ if (errorReceived) {
5572
+ break;
5573
+ }
5574
+ const connected = await pingServer(webServer.origin);
5575
+ if (connected) {
5576
+ break;
5577
+ }
5578
+ startOperation.throwIfAborted();
5579
+ const delay = logScale.shift() || 1000;
5580
+ logger.debug(`Waiting ${delay}ms`);
5581
+ await new Promise((x) => setTimeout(x, delay));
5582
+ }
5583
+ })();
5584
+
5585
+ try {
5586
+ await Promise.race([errorPromise, startedPromise]);
5587
+ } catch (e) {
5588
+ if (Abort.isAbortError(e)) {
5589
+ if (timeoutAbortSource.signal.aborted) {
5590
+ // aborted by timeout
5591
+ throw new Error(
5592
+ `"${webServer.command}" command did not start a server in less than ${allocatedMs}ms`,
5593
+ );
5594
+ }
5595
+ if (signal.aborted) {
5596
+ // aborted from outside
5597
+ return;
5598
+ }
5599
+ }
5600
+ throw e;
5601
+ } finally {
5602
+ await startOperation.end();
5638
5603
  }
5639
- return section;
5640
5604
  };
5641
5605
 
5642
- const renderDetails = (data) => {
5643
- const details = [];
5644
- for (const key of Object.keys(data)) {
5645
- const value = data[key];
5646
- let valueString = "";
5647
- valueString += ANSI.color(`${key}:`, ANSI.GREY);
5648
- const useNonGreyAnsiColor =
5649
- typeof value === "string" && value.includes("\x1b");
5650
- valueString += " ";
5651
- valueString += useNonGreyAnsiColor
5652
- ? value
5653
- : ANSI.color(String(value), ANSI.GREY);
5654
- details.push(valueString);
5655
- }
5656
- if (details.length === 0) {
5657
- return "";
5606
+ const startServerUsingModuleUrl = async (webServer, params) => {
5607
+ if (!existsSync(new URL(webServer.moduleUrl))) {
5608
+ throw new Error(`"${webServer.moduleUrl}" does not lead to a file`);
5658
5609
  }
5659
-
5660
- let string = "";
5661
- string += ` ${ANSI.color("(", ANSI.GREY)}`;
5662
- string += details.join(ANSI.color(", ", ANSI.GREY));
5663
- string += ANSI.color(")", ANSI.GREY);
5664
- return string;
5610
+ return startServerUsingCommand(
5611
+ {
5612
+ ...webServer,
5613
+ command: `node ${fileURLToPath(webServer.moduleUrl)}`,
5614
+ },
5615
+ params,
5616
+ );
5665
5617
  };
5666
5618
 
5667
- const createIsInsideFragment = (fragment, total) => {
5668
- let [dividend, divisor] = fragment.split("/");
5669
- dividend = parseInt(dividend);
5670
- divisor = parseInt(divisor);
5671
- const groupSize = Math.ceil(total / divisor);
5672
- let from;
5673
- let to;
5674
- if (groupSize === 0) {
5675
- if (dividend === 1) {
5676
- from = 0;
5677
- to = 0;
5619
+ const assertAndNormalizeWebServer = async (
5620
+ webServer,
5621
+ { signal, logger, teardownCallbackSet },
5622
+ ) => {
5623
+ if (!webServer) {
5624
+ throw new TypeError(
5625
+ `webServer is required when running tests on browser(s)`,
5626
+ );
5627
+ }
5628
+ const unexpectedParamNames = Object.keys(webServer).filter((key) => {
5629
+ return ![
5630
+ "origin",
5631
+ "moduleUrl",
5632
+ "command",
5633
+ "cwd",
5634
+ "rootDirectoryUrl",
5635
+ ].includes(key);
5636
+ });
5637
+ if (unexpectedParamNames.length > 0) {
5638
+ throw new TypeError(
5639
+ `${unexpectedParamNames.join(",")}: there is no such param to webServer`,
5640
+ );
5641
+ }
5642
+ if (typeof webServer.origin !== "string") {
5643
+ throw new TypeError(
5644
+ `webServer.origin must be a string, got ${webServer.origin}`,
5645
+ );
5646
+ }
5647
+ await ensureWebServerIsStarted(webServer, {
5648
+ signal,
5649
+ teardownCallbackSet,
5650
+ logger,
5651
+ });
5652
+ const { headers } = await basicFetch(webServer.origin, {
5653
+ method: "GET",
5654
+ rejectUnauthorized: false,
5655
+ headers: {
5656
+ "x-server-inspect": "1",
5657
+ },
5658
+ });
5659
+ if (String(headers["server"]).includes("jsenv_dev_server")) {
5660
+ webServer.isJsenvDevServer = true;
5661
+ const { json } = await basicFetch(`${webServer.origin}/__params__.json`, {
5662
+ rejectUnauthorized: false,
5663
+ });
5664
+ if (webServer.rootDirectoryUrl === undefined) {
5665
+ const jsenvDevServerParams = await json();
5666
+ webServer.rootDirectoryUrl = jsenvDevServerParams.sourceDirectoryUrl;
5678
5667
  } else {
5679
- from = Infinity;
5680
- to = -1;
5668
+ webServer.rootDirectoryUrl = assertAndNormalizeDirectoryUrl(
5669
+ webServer.rootDirectoryUrl,
5670
+ "webServer.rootDirectoryUrl",
5671
+ );
5681
5672
  }
5682
- } else if (dividend === 1) {
5683
- from = 0;
5684
- to = groupSize;
5685
5673
  } else {
5686
- from = (dividend - 1) * groupSize;
5687
- if (dividend === divisor) {
5688
- to = total;
5689
- } else {
5690
- to = from + groupSize;
5691
- }
5674
+ webServer.rootDirectoryUrl = assertAndNormalizeDirectoryUrl(
5675
+ webServer.rootDirectoryUrl,
5676
+ "webServer.rootDirectoryUrl",
5677
+ );
5692
5678
  }
5693
- return (index) => {
5694
- if (index < from) return false;
5695
- if (index >= to) return false;
5696
- return true;
5697
- };
5679
+ };
5680
+
5681
+ const ensureWebServerIsStarted = async (
5682
+ webServer,
5683
+ { signal, teardownCallbackSet, logger, allocatedMs = 5_000 },
5684
+ ) => {
5685
+ const aServerIsListening = await pingServer(webServer.origin);
5686
+ if (aServerIsListening) {
5687
+ return;
5688
+ }
5689
+ if (webServer.moduleUrl) {
5690
+ await startServerUsingModuleUrl(webServer, {
5691
+ signal,
5692
+ allocatedMs,
5693
+ teardownCallbackSet,
5694
+ logger,
5695
+ });
5696
+ return;
5697
+ }
5698
+ if (webServer.command) {
5699
+ await startServerUsingCommand(webServer, {
5700
+ signal,
5701
+ allocatedMs,
5702
+ teardownCallbackSet,
5703
+ logger,
5704
+ });
5705
+ return;
5706
+ }
5707
+ throw new TypeError(
5708
+ `webServer.moduleUrl or webServer.command is required as there is no server listening "${webServer.origin}"`,
5709
+ );
5698
5710
  };
5699
5711
 
5700
5712
  /*
@@ -6922,6 +6934,63 @@ const WEB_URL_CONVERTER = {
6922
6934
  },
6923
6935
  };
6924
6936
 
6937
+ const initIstanbulMiddleware = async (
6938
+ page,
6939
+ { webServer, rootDirectoryUrl, coverageInclude },
6940
+ ) => {
6941
+ const associations = URL_META.resolveAssociations(
6942
+ { cover: coverageInclude },
6943
+ rootDirectoryUrl,
6944
+ );
6945
+ await page.route("**", async (route) => {
6946
+ const request = route.request();
6947
+ const url = request.url(); // transform into a local url
6948
+ const fileUrl = WEB_URL_CONVERTER.asFileUrl(url, webServer);
6949
+ const needsInstrumentation = URL_META.applyAssociations({
6950
+ url: fileUrl,
6951
+ associations,
6952
+ }).cover;
6953
+ if (!needsInstrumentation) {
6954
+ route.fallback();
6955
+ return;
6956
+ }
6957
+ const response = await route.fetch();
6958
+ const originalBody = await response.text();
6959
+ try {
6960
+ const result = await applyBabelPlugins({
6961
+ babelPlugins: [babelPluginInstrument],
6962
+ input: originalBody,
6963
+ // jsenv server could send info to know it's a js module or js classic
6964
+ // but in the end it's not super important
6965
+ // - it's ok to parse js classic as js module considering it's only for istanbul instrumentation
6966
+ inputIsJsModule: true,
6967
+ inputUrl: fileUrl,
6968
+ });
6969
+ let code = result.code;
6970
+ code = SOURCEMAP.writeComment({
6971
+ contentType: "text/javascript",
6972
+ content: code,
6973
+ specifier: generateSourcemapDataUrl(result.map),
6974
+ });
6975
+ route.fulfill({
6976
+ response,
6977
+ body: code,
6978
+ headers: {
6979
+ ...response.headers(),
6980
+ "content-length": Buffer.byteLength(code),
6981
+ },
6982
+ });
6983
+ } catch (e) {
6984
+ if (e.code === "PARSE_ERROR") {
6985
+ route.fulfill({ response });
6986
+ } else {
6987
+ console.error(e);
6988
+ route.fulfill({ response });
6989
+ }
6990
+ }
6991
+ });
6992
+ };
6993
+
6925
6994
  const initJsSupervisorMiddleware = async (
6926
6995
  page,
6927
6996
  { webServer, fileUrl, fileServerUrl },
@@ -7014,63 +7083,6 @@ const initJsSupervisorMiddleware = async (
7014
7083
  });
7015
7084
  };
7016
7085
 
7017
- const initIstanbulMiddleware = async (
7018
- page,
7019
- { webServer, rootDirectoryUrl, coverageInclude },
7020
- ) => {
7021
- const associations = URL_META.resolveAssociations(
7022
- { cover: coverageInclude },
7023
- rootDirectoryUrl,
7024
- );
7025
- await page.route("**", async (route) => {
7026
- const request = route.request();
7027
- const url = request.url(); // transform into a local url
7028
- const fileUrl = WEB_URL_CONVERTER.asFileUrl(url, webServer);
7029
- const needsInstrumentation = URL_META.applyAssociations({
7030
- url: fileUrl,
7031
- associations,
7032
- }).cover;
7033
- if (!needsInstrumentation) {
7034
- route.fallback();
7035
- return;
7036
- }
7037
- const response = await route.fetch();
7038
- const originalBody = await response.text();
7039
- try {
7040
- const result = await applyBabelPlugins({
7041
- babelPlugins: [babelPluginInstrument],
7042
- input: originalBody,
7043
- // jsenv server could send info to know it's a js module or js classic
7044
- // but in the end it's not super important
7045
- // - it's ok to parse js classic as js module considering it's only for istanbul instrumentation
7046
- inputIsJsModule: true,
7047
- inputUrl: fileUrl,
7048
- });
7049
- let code = result.code;
7050
- code = SOURCEMAP.writeComment({
7051
- contentType: "text/javascript",
7052
- content: code,
7053
- specifier: generateSourcemapDataUrl(result.map),
7054
- });
7055
- route.fulfill({
7056
- response,
7057
- body: code,
7058
- headers: {
7059
- ...response.headers(),
7060
- "content-length": Buffer.byteLength(code),
7061
- },
7062
- });
7063
- } catch (e) {
7064
- if (e.code === "PARSE_ERROR") {
7065
- route.fulfill({ response });
7066
- } else {
7067
- console.error(e);
7068
- route.fulfill({ response });
7069
- }
7070
- }
7071
- });
7072
- };
7073
-
7074
7086
  const browserPromiseCache = new Map();
7075
7087
 
7076
7088
  const createRuntimeUsingPlaywright = ({
@@ -8006,6 +8018,21 @@ const getDebugInfo = (processOptions) => {
8006
8018
  // return typeof process.env.VSCODE_PID === "string"
8007
8019
  // }
8008
8020
 
8021
+ // https://nodejs.org/api/process.html#process_signal_events
8022
+ const SIGINT_SIGNAL_NUMBER = 2;
8023
+ const SIGABORT_SIGNAL_NUMBER = 6;
8024
+ const SIGTERM_SIGNAL_NUMBER = 15;
8025
+ const EXIT_CODES = {
8026
+ SIGINT: 128 + SIGINT_SIGNAL_NUMBER,
8027
+ SIGABORT: 128 + SIGABORT_SIGNAL_NUMBER,
8028
+ SIGTERM: 128 + SIGTERM_SIGNAL_NUMBER,
8029
+ };
8030
+
8031
+ const IMPORTMAP_NODE_LOADER_FILE_URL = new URL(
8032
+ "./importmap_node_loader.mjs",
8033
+ import.meta.url,
8034
+ ).href;
8035
+
8009
8036
  // see also https://github.com/sindresorhus/execa/issues/96
8010
8037
  const killProcessTree = async (
8011
8038
  processId,
@@ -8081,21 +8108,6 @@ const killProcessTree = async (
8081
8108
  await check();
8082
8109
  };
8083
8110
 
8084
- // https://nodejs.org/api/process.html#process_signal_events
8085
- const SIGINT_SIGNAL_NUMBER = 2;
8086
- const SIGABORT_SIGNAL_NUMBER = 6;
8087
- const SIGTERM_SIGNAL_NUMBER = 15;
8088
- const EXIT_CODES = {
8089
- SIGINT: 128 + SIGINT_SIGNAL_NUMBER,
8090
- SIGABORT: 128 + SIGABORT_SIGNAL_NUMBER,
8091
- SIGTERM: 128 + SIGTERM_SIGNAL_NUMBER,
8092
- };
8093
-
8094
- const IMPORTMAP_NODE_LOADER_FILE_URL = new URL(
8095
- "./importmap_node_loader.mjs",
8096
- import.meta.url,
8097
- ).href;
8098
-
8099
8111
  const NO_EXPERIMENTAL_WARNING_FILE_URL = new URL(
8100
8112
  "./no_experimental_warnings.cjs",
8101
8113
  import.meta.url,
@@ -8973,24 +8985,39 @@ const istanbulCoverageMapFromCoverage = (coverage) => {
8973
8985
  return coverageMap;
8974
8986
  };
8975
8987
 
8976
- const reportCoverageInConsole = (
8988
+ const reportCoverageAsHtml = (
8977
8989
  testPlanResult,
8990
+ directoryUrl,
8978
8991
  { skipEmpty, skipFull } = {},
8979
8992
  ) => {
8980
8993
  if (testPlanResult.aborted) {
8981
8994
  return;
8982
8995
  }
8983
8996
  const testPlanCoverage = testPlanResult.coverage;
8997
+
8998
+ const { rootDirectoryUrl } = testPlanResult;
8999
+ ensureEmptyDirectorySync(directoryUrl);
9000
+ const coverageHtmlDirectoryRelativeUrl = urlToRelativeUrl(
9001
+ directoryUrl,
9002
+ rootDirectoryUrl,
9003
+ );
9004
+
8984
9005
  const libReport = importWithRequire("istanbul-lib-report");
8985
9006
  const reports = importWithRequire("istanbul-reports");
8986
9007
  const context = libReport.createContext({
9008
+ dir: fileURLToPath(rootDirectoryUrl),
8987
9009
  coverageMap: istanbulCoverageMapFromCoverage(testPlanCoverage),
9010
+ sourceFinder: (path) =>
9011
+ readFileSync(new URL(path, rootDirectoryUrl), "utf8"),
8988
9012
  });
8989
- const report = reports.create("text", {
9013
+ const report = reports.create("html", {
8990
9014
  skipEmpty,
8991
9015
  skipFull,
9016
+ subdir: coverageHtmlDirectoryRelativeUrl,
8992
9017
  });
8993
9018
  report.execute(context);
9019
+ // const htmlCoverageDirectoryIndexFileUrl = `${directoryUrl}index.html`;
9020
+ // console.log(`-> ${urlToFileSystemPath(htmlCoverageDirectoryIndexFileUrl)}`);
8994
9021
  };
8995
9022
 
8996
9023
  const reportCoverageAsJson = (
@@ -9013,39 +9040,24 @@ const reportCoverageAsJson = (
9013
9040
  }
9014
9041
  };
9015
9042
 
9016
- const reportCoverageAsHtml = (
9043
+ const reportCoverageInConsole = (
9017
9044
  testPlanResult,
9018
- directoryUrl,
9019
9045
  { skipEmpty, skipFull } = {},
9020
9046
  ) => {
9021
9047
  if (testPlanResult.aborted) {
9022
9048
  return;
9023
9049
  }
9024
9050
  const testPlanCoverage = testPlanResult.coverage;
9025
-
9026
- const { rootDirectoryUrl } = testPlanResult;
9027
- ensureEmptyDirectorySync(directoryUrl);
9028
- const coverageHtmlDirectoryRelativeUrl = urlToRelativeUrl(
9029
- directoryUrl,
9030
- rootDirectoryUrl,
9031
- );
9032
-
9033
9051
  const libReport = importWithRequire("istanbul-lib-report");
9034
9052
  const reports = importWithRequire("istanbul-reports");
9035
9053
  const context = libReport.createContext({
9036
- dir: fileURLToPath(rootDirectoryUrl),
9037
9054
  coverageMap: istanbulCoverageMapFromCoverage(testPlanCoverage),
9038
- sourceFinder: (path) =>
9039
- readFileSync(new URL(path, rootDirectoryUrl), "utf8"),
9040
9055
  });
9041
- const report = reports.create("html", {
9056
+ const report = reports.create("text", {
9042
9057
  skipEmpty,
9043
9058
  skipFull,
9044
- subdir: coverageHtmlDirectoryRelativeUrl,
9045
9059
  });
9046
9060
  report.execute(context);
9047
- // const htmlCoverageDirectoryIndexFileUrl = `${directoryUrl}index.html`;
9048
- // console.log(`-> ${urlToFileSystemPath(htmlCoverageDirectoryIndexFileUrl)}`);
9049
9061
  };
9050
9062
 
9051
9063
  const createXmlGenerator = ({