@jsenv/test 1.6.9 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/js/execute_using_dynamic_import.js +118 -771
- package/dist/jsenv_test.js +409 -99
- package/dist/{controllable_child_process.mjs → node_child_process_controlled.mjs} +6 -13
- package/dist/{controllable_worker_thread.mjs → node_worker_thread_controlled.mjs} +6 -13
- package/package.json +3 -3
- package/src/execution/exception.js +137 -0
- package/src/execution/execute_steps.js +8 -13
- package/src/execution/execute_test_plan.js +132 -6
- package/src/execution/github_annotation_from_error.js +132 -0
- package/src/execution/logs_file_execution.js +134 -62
- package/src/runtime_browsers/using_playwright.js +2 -8
- package/src/runtime_node/node_child_process.js +5 -6
- package/src/runtime_node/{controllable_child_process.mjs → node_child_process_controlled.mjs} +6 -14
- package/src/runtime_node/node_worker_thread.js +4 -5
- package/src/runtime_node/{controllable_worker_thread.mjs → node_worker_thread_controlled.mjs} +6 -13
- package/src/execution/logs_file_execution.test.mjs +0 -41
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createException, executeUsingDynamicImport } from "./js/execute_using_dynamic_import.js";
|
|
2
2
|
import "node:fs";
|
|
3
3
|
import "node:inspector";
|
|
4
4
|
import "node:perf_hooks";
|
|
@@ -28,20 +28,13 @@ const ACTION_RESPONSE_STATUS_FAILED = "action-failed";
|
|
|
28
28
|
const ACTION_RESPONSE_STATUS_COMPLETED = "action-completed";
|
|
29
29
|
|
|
30
30
|
const sendActionFailed = (error) => {
|
|
31
|
-
|
|
32
|
-
delete error.toString;
|
|
33
|
-
}
|
|
31
|
+
const exception = createException(error);
|
|
34
32
|
sendToParent(
|
|
35
33
|
ACTION_RESPONSE_EVENT_NAME,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
status: ACTION_RESPONSE_STATUS_FAILED,
|
|
41
|
-
value: error,
|
|
42
|
-
},
|
|
43
|
-
{ ignoreSymbols: true },
|
|
44
|
-
),
|
|
34
|
+
JSON.stringify({
|
|
35
|
+
status: ACTION_RESPONSE_STATUS_FAILED,
|
|
36
|
+
value: exception,
|
|
37
|
+
}),
|
|
45
38
|
);
|
|
46
39
|
};
|
|
47
40
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parentPort } from "node:worker_threads";
|
|
2
|
-
import {
|
|
2
|
+
import { createException, executeUsingDynamicImport } from "./js/execute_using_dynamic_import.js";
|
|
3
3
|
import "node:fs";
|
|
4
4
|
import "node:inspector";
|
|
5
5
|
import "node:perf_hooks";
|
|
@@ -13,20 +13,13 @@ const ACTION_RESPONSE_STATUS_FAILED = "action-failed";
|
|
|
13
13
|
const ACTION_RESPONSE_STATUS_COMPLETED = "action-completed";
|
|
14
14
|
|
|
15
15
|
const sendActionFailed = (error) => {
|
|
16
|
-
|
|
17
|
-
delete error.toString;
|
|
18
|
-
}
|
|
16
|
+
const exception = createException(error);
|
|
19
17
|
sendToParent(
|
|
20
18
|
ACTION_RESPONSE_EVENT_NAME,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
status: ACTION_RESPONSE_STATUS_FAILED,
|
|
26
|
-
value: error,
|
|
27
|
-
},
|
|
28
|
-
{ ignoreSymbols: true },
|
|
29
|
-
),
|
|
19
|
+
JSON.stringify({
|
|
20
|
+
status: ACTION_RESPONSE_STATUS_FAILED,
|
|
21
|
+
value: exception,
|
|
22
|
+
}),
|
|
30
23
|
);
|
|
31
24
|
};
|
|
32
25
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/test",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -37,11 +37,11 @@
|
|
|
37
37
|
"@jsenv/ast": "5.1.4",
|
|
38
38
|
"@jsenv/filesystem": "4.3.2",
|
|
39
39
|
"@jsenv/log": "3.4.1",
|
|
40
|
-
"@jsenv/plugin-supervisor": "1.3.
|
|
40
|
+
"@jsenv/plugin-supervisor": "1.3.5",
|
|
41
41
|
"@jsenv/urls": "2.2.1",
|
|
42
42
|
"@jsenv/url-meta": "8.1.0",
|
|
43
|
-
"@jsenv/uneval": "1.6.0",
|
|
44
43
|
"@jsenv/utils": "2.0.1",
|
|
44
|
+
"@jsenv/github-check-run": "0.1.0",
|
|
45
45
|
"v8-to-istanbul": "9.1.0",
|
|
46
46
|
"istanbul-lib-coverage": "3.2.0",
|
|
47
47
|
"istanbul-lib-instrument": "6.0.0",
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Exception are objects used to wrap a value that is thrown
|
|
3
|
+
* Usually they wrap error but the value can be anything as
|
|
4
|
+
* throw "toto" can be used in JS
|
|
5
|
+
*
|
|
6
|
+
* - provide a common API to interact with value that can be thrown
|
|
7
|
+
* - enrich usual errors with a bit more information (line, column)
|
|
8
|
+
* - normalize error properties
|
|
9
|
+
* - exception.stackTrace: only the stack trace as string (no error.name or error.message)
|
|
10
|
+
* - error.stack: error.name + error.message + error.stackTrace
|
|
11
|
+
*
|
|
12
|
+
* It is used mostly internally by jsenv but can also be found
|
|
13
|
+
* value returned by "executeTestPlan" public export (for failed executions)
|
|
14
|
+
* and value returned by "execute" public export (when execution fails)
|
|
15
|
+
*
|
|
16
|
+
* This file is responsible to wrap error hapenning in Node.js runtime
|
|
17
|
+
* The browser part can be found in "supervisor.js"
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const createException = (reason) => {
|
|
21
|
+
const exception = {
|
|
22
|
+
isException: true,
|
|
23
|
+
isError: false,
|
|
24
|
+
name: "",
|
|
25
|
+
message: "",
|
|
26
|
+
stack: "",
|
|
27
|
+
stackTrace: "",
|
|
28
|
+
site: null,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (reason === undefined) {
|
|
32
|
+
exception.message = "undefined";
|
|
33
|
+
return exception;
|
|
34
|
+
}
|
|
35
|
+
if (reason === undefined) {
|
|
36
|
+
exception.message = "undefined";
|
|
37
|
+
return exception;
|
|
38
|
+
}
|
|
39
|
+
if (typeof reason === "string") {
|
|
40
|
+
exception.message = reason;
|
|
41
|
+
return exception;
|
|
42
|
+
}
|
|
43
|
+
if (reason instanceof Error) {
|
|
44
|
+
exception.isError = true;
|
|
45
|
+
exception.name = reason.name;
|
|
46
|
+
exception.message = reason.message;
|
|
47
|
+
const stackInfo = getStackInfo(reason);
|
|
48
|
+
if (stackInfo) {
|
|
49
|
+
const { stackTrace, stackObject } = stackInfo;
|
|
50
|
+
exception.stackTrace = stackTrace;
|
|
51
|
+
const [firstCallSite] = stackObject;
|
|
52
|
+
writePropertiesFromCallSite(exception, firstCallSite);
|
|
53
|
+
}
|
|
54
|
+
exception.stack = reason.stack;
|
|
55
|
+
return exception;
|
|
56
|
+
}
|
|
57
|
+
if (typeof reason === "object") {
|
|
58
|
+
exception.code = reason.code;
|
|
59
|
+
exception.message = reason.message;
|
|
60
|
+
exception.stack = reason.stack;
|
|
61
|
+
return exception;
|
|
62
|
+
}
|
|
63
|
+
exception.message = JSON.stringify(reason);
|
|
64
|
+
return exception;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const getStackInfo = (reason) => {
|
|
68
|
+
if (reason instanceof Error) {
|
|
69
|
+
const { prepareStackTrace } = Error;
|
|
70
|
+
let stackTrace;
|
|
71
|
+
let stackObject;
|
|
72
|
+
Error.prepareStackTrace = (e, secondArg) => {
|
|
73
|
+
Error.prepareStackTrace = prepareStackTrace;
|
|
74
|
+
stackObject = secondArg;
|
|
75
|
+
stackTrace = secondArg.map((callSite) => ` at ${callSite}`).join("\n");
|
|
76
|
+
const name = e.name || "Error";
|
|
77
|
+
const message = e.message || "";
|
|
78
|
+
let stack = `${name}: ${message}`;
|
|
79
|
+
if (stackTrace) {
|
|
80
|
+
stack += `\n${stackTrace}`;
|
|
81
|
+
}
|
|
82
|
+
return stack;
|
|
83
|
+
};
|
|
84
|
+
// eslint-disable-next-line no-unused-expressions
|
|
85
|
+
reason.stack;
|
|
86
|
+
if (stackTrace === undefined) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
stackTrace,
|
|
91
|
+
stackObject,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
};
|
|
96
|
+
const writePropertiesFromCallSite = (exception, callSite) => {
|
|
97
|
+
const source = callSite.getFileName() || callSite.getScriptNameOrSourceURL();
|
|
98
|
+
if (source) {
|
|
99
|
+
const line = callSite.getLineNumber();
|
|
100
|
+
const column = callSite.getColumnNumber() - 1;
|
|
101
|
+
exception.site = {
|
|
102
|
+
url: source,
|
|
103
|
+
line,
|
|
104
|
+
column,
|
|
105
|
+
};
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Code called using eval() needs special handling
|
|
109
|
+
if (callSite.isEval()) {
|
|
110
|
+
const origin = callSite.getEvalOrigin();
|
|
111
|
+
if (origin) {
|
|
112
|
+
writePropertiesFromEvalOrigin(exception, origin);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const writePropertiesFromEvalOrigin = (exception, origin) => {
|
|
117
|
+
// Most eval() calls are in this format
|
|
118
|
+
const topLevelEvalMatch = /^eval at ([^(]+) \((.+):(\d+):(\d+)\)$/.exec(
|
|
119
|
+
origin,
|
|
120
|
+
);
|
|
121
|
+
if (topLevelEvalMatch) {
|
|
122
|
+
const source = topLevelEvalMatch[2];
|
|
123
|
+
const line = Number(topLevelEvalMatch[3]);
|
|
124
|
+
const column = topLevelEvalMatch[4] - 1;
|
|
125
|
+
exception.site = {
|
|
126
|
+
url: source,
|
|
127
|
+
line,
|
|
128
|
+
column,
|
|
129
|
+
};
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Parse nested eval() calls using recursion
|
|
133
|
+
const nestedEvalMatch = /^eval at ([^(]+) \((.+)\)$/.exec(origin);
|
|
134
|
+
if (nestedEvalMatch) {
|
|
135
|
+
writePropertiesFromEvalOrigin(exception, nestedEvalMatch[2]);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
@@ -10,7 +10,7 @@ import { ensureEmptyDirectory, writeFileSync } from "@jsenv/filesystem";
|
|
|
10
10
|
import { reportToCoverage } from "../coverage/report_to_coverage.js";
|
|
11
11
|
import { run } from "./run.js";
|
|
12
12
|
import { ensureGlobalGc } from "./gc.js";
|
|
13
|
-
import { createExecutionLog,
|
|
13
|
+
import { createExecutionLog, formatSummaryLog } from "./logs_file_execution.js";
|
|
14
14
|
|
|
15
15
|
export const executeSteps = async (
|
|
16
16
|
executionSteps,
|
|
@@ -177,6 +177,9 @@ export const executeSteps = async (
|
|
|
177
177
|
executionResult: {
|
|
178
178
|
status: "executing",
|
|
179
179
|
},
|
|
180
|
+
counters,
|
|
181
|
+
timeEllapsed: Date.now() - startMs,
|
|
182
|
+
memoryHeap: memoryUsage().heapUsed,
|
|
180
183
|
};
|
|
181
184
|
if (typeof executionParams.allocatedMs === "function") {
|
|
182
185
|
executionParams.allocatedMs =
|
|
@@ -188,17 +191,10 @@ export const executeSteps = async (
|
|
|
188
191
|
log: executionLog,
|
|
189
192
|
render: () => {
|
|
190
193
|
return createExecutionLog(beforeExecutionInfo, {
|
|
191
|
-
counters,
|
|
192
194
|
logRuntime,
|
|
193
195
|
logEachDuration,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
timeEllapsed: Date.now() - startMs,
|
|
197
|
-
}
|
|
198
|
-
: {}),
|
|
199
|
-
...(logMemoryHeapUsage
|
|
200
|
-
? { memoryHeap: memoryUsage().heapUsed }
|
|
201
|
-
: {}),
|
|
196
|
+
logTimeUsage,
|
|
197
|
+
logMemoryHeapUsage,
|
|
202
198
|
});
|
|
203
199
|
},
|
|
204
200
|
});
|
|
@@ -249,7 +245,6 @@ export const executeSteps = async (
|
|
|
249
245
|
endMs: Date.now(),
|
|
250
246
|
executionResult,
|
|
251
247
|
};
|
|
252
|
-
afterExecutionCallback(afterExecutionInfo);
|
|
253
248
|
|
|
254
249
|
if (executionResult.status === "aborted") {
|
|
255
250
|
counters.aborted++;
|
|
@@ -266,7 +261,6 @@ export const executeSteps = async (
|
|
|
266
261
|
if (executionLogsEnabled) {
|
|
267
262
|
const log = createExecutionLog(afterExecutionInfo, {
|
|
268
263
|
logShortForCompletedExecutions,
|
|
269
|
-
counters,
|
|
270
264
|
logRuntime,
|
|
271
265
|
logEachDuration,
|
|
272
266
|
...(logTimeUsage
|
|
@@ -294,6 +288,7 @@ export const executeSteps = async (
|
|
|
294
288
|
executionLog = createLog({ newLine: "" });
|
|
295
289
|
}
|
|
296
290
|
}
|
|
291
|
+
afterExecutionCallback(afterExecutionInfo);
|
|
297
292
|
const isLastExecutionLog = executionIndex === executionSteps.length - 1;
|
|
298
293
|
const cancelRemaining =
|
|
299
294
|
failFast &&
|
|
@@ -321,7 +316,7 @@ export const executeSteps = async (
|
|
|
321
316
|
duration: Date.now() - startMs,
|
|
322
317
|
};
|
|
323
318
|
if (logSummary) {
|
|
324
|
-
const summaryLog =
|
|
319
|
+
const summaryLog = formatSummaryLog(summary);
|
|
325
320
|
rawOutput += stripAnsi(summaryLog);
|
|
326
321
|
logger.info(summaryLog);
|
|
327
322
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import stripAnsi from "strip-ansi";
|
|
2
3
|
import { Abort, raceProcessTeardownEvents } from "@jsenv/abort";
|
|
3
4
|
import { URL_META } from "@jsenv/url-meta";
|
|
4
5
|
import { urlToFileSystemPath, urlToRelativeUrl } from "@jsenv/urls";
|
|
@@ -7,7 +8,11 @@ import {
|
|
|
7
8
|
assertAndNormalizeDirectoryUrl,
|
|
8
9
|
assertAndNormalizeFileUrl,
|
|
9
10
|
} from "@jsenv/filesystem";
|
|
10
|
-
import { createLogger, createDetailedMessage } from "@jsenv/log";
|
|
11
|
+
import { createLogger, createDetailedMessage, UNICODE } from "@jsenv/log";
|
|
12
|
+
import {
|
|
13
|
+
startGithubCheckRun,
|
|
14
|
+
readGitHubWorkflowEnv,
|
|
15
|
+
} from "@jsenv/github-check-run";
|
|
11
16
|
|
|
12
17
|
import { createTeardown } from "../helpers/teardown.js";
|
|
13
18
|
import { generateCoverageJsonFile } from "../coverage/coverage_reporter_json_file.js";
|
|
@@ -16,6 +21,8 @@ import { generateCoverageTextLog } from "../coverage/coverage_reporter_text_log.
|
|
|
16
21
|
import { assertAndNormalizeWebServer } from "./web_server_param.js";
|
|
17
22
|
import { executionStepsFromTestPlan } from "./execution_steps.js";
|
|
18
23
|
import { executeSteps } from "./execute_steps.js";
|
|
24
|
+
import { formatExecutionLabel, formatSummary } from "./logs_file_execution.js";
|
|
25
|
+
import { githubAnnotationFromError } from "./github_annotation_from_error.js";
|
|
19
26
|
|
|
20
27
|
/**
|
|
21
28
|
* Execute a list of files and log how it goes.
|
|
@@ -64,6 +71,15 @@ export const executeTestPlan = async ({
|
|
|
64
71
|
cooldownBetweenExecutions = 0,
|
|
65
72
|
gcBetweenExecutions = logMemoryHeapUsage,
|
|
66
73
|
|
|
74
|
+
githubCheckEnabled = Boolean(process.env.GITHUB_WORKFLOW),
|
|
75
|
+
githubCheckLogLevel,
|
|
76
|
+
githubCheckName = "Jsenv tests",
|
|
77
|
+
githubCheckTitle,
|
|
78
|
+
githubCheckToken,
|
|
79
|
+
githubCheckRepositoryOwner,
|
|
80
|
+
githubCheckRepositoryName,
|
|
81
|
+
githubCheckCommitSha,
|
|
82
|
+
|
|
67
83
|
coverageEnabled = process.argv.includes("--coverage"),
|
|
68
84
|
coverageConfig = {
|
|
69
85
|
"file:///**/node_modules/": false,
|
|
@@ -178,6 +194,47 @@ export const executeTestPlan = async ({
|
|
|
178
194
|
});
|
|
179
195
|
}
|
|
180
196
|
|
|
197
|
+
if (githubCheckEnabled && !process.env.GITHUB_TOKEN) {
|
|
198
|
+
githubCheckEnabled = false;
|
|
199
|
+
const suggestions = [];
|
|
200
|
+
if (process.env.GITHUB_WORKFLOW_REF) {
|
|
201
|
+
const workflowFileRef = process.env.GITHUB_WORKFLOW_REF;
|
|
202
|
+
const refsIndex = workflowFileRef.indexOf("@refs/");
|
|
203
|
+
// see "GITHUB_WORKFLOW_REF" in https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
|
|
204
|
+
const workflowFilePath =
|
|
205
|
+
refsIndex === -1
|
|
206
|
+
? workflowFileRef
|
|
207
|
+
: workflowFileRef.slice(0, refsIndex);
|
|
208
|
+
suggestions.push(`Pass github token in ${workflowFilePath} during job "${process.env.GITHUB_JOB}"
|
|
209
|
+
\`\`\`yml
|
|
210
|
+
env:
|
|
211
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
212
|
+
\`\`\``);
|
|
213
|
+
}
|
|
214
|
+
suggestions.push(`Disable github check with githubCheckEnabled: false`);
|
|
215
|
+
logger.warn(
|
|
216
|
+
`${
|
|
217
|
+
UNICODE.WARNING
|
|
218
|
+
} githubCheckEnabled but process.env.GITHUB_TOKEN is missing.
|
|
219
|
+
Integration with Github check API is disabled
|
|
220
|
+
To fix this warning:
|
|
221
|
+
- ${suggestions.join("\n- ")}
|
|
222
|
+
`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (githubCheckEnabled) {
|
|
226
|
+
const githubCheckInfoFromEnv = process.env.GITHUB_WORKFLOW
|
|
227
|
+
? readGitHubWorkflowEnv()
|
|
228
|
+
: {};
|
|
229
|
+
githubCheckToken = githubCheckToken || githubCheckInfoFromEnv.githubToken;
|
|
230
|
+
githubCheckRepositoryOwner =
|
|
231
|
+
githubCheckRepositoryOwner || githubCheckInfoFromEnv.repositoryOwner;
|
|
232
|
+
githubCheckRepositoryName =
|
|
233
|
+
githubCheckRepositoryName || githubCheckInfoFromEnv.repositoryName;
|
|
234
|
+
githubCheckCommitSha =
|
|
235
|
+
githubCheckCommitSha || githubCheckInfoFromEnv.commitSha;
|
|
236
|
+
}
|
|
237
|
+
|
|
181
238
|
if (coverageEnabled) {
|
|
182
239
|
if (coverageMethodForBrowsers === undefined) {
|
|
183
240
|
coverageMethodForBrowsers = someHasCoverageV8
|
|
@@ -311,6 +368,61 @@ export const executeTestPlan = async ({
|
|
|
311
368
|
rootDirectoryUrl,
|
|
312
369
|
});
|
|
313
370
|
logger.debug(`${executionSteps.length} executions planned`);
|
|
371
|
+
let beforeExecutionCallback;
|
|
372
|
+
let afterExecutionCallback;
|
|
373
|
+
let afterAllExecutionCallback = () => {};
|
|
374
|
+
if (githubCheckEnabled) {
|
|
375
|
+
const githubCheckRun = await startGithubCheckRun({
|
|
376
|
+
logLevel: githubCheckLogLevel,
|
|
377
|
+
githubToken: githubCheckToken,
|
|
378
|
+
repositoryOwner: githubCheckRepositoryOwner,
|
|
379
|
+
repositoryName: githubCheckRepositoryName,
|
|
380
|
+
commitSha: githubCheckCommitSha,
|
|
381
|
+
checkName: githubCheckName,
|
|
382
|
+
checkTitle: `Tests executions`,
|
|
383
|
+
checkSummary: `${executionSteps.length} files will be executed`,
|
|
384
|
+
});
|
|
385
|
+
afterExecutionCallback = (afterExecutionInfo) => {
|
|
386
|
+
const summary = stripAnsi(
|
|
387
|
+
formatExecutionLabel(afterExecutionInfo, {
|
|
388
|
+
logTimeUsage,
|
|
389
|
+
logMemoryHeapUsage,
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
const annotations = [];
|
|
393
|
+
const { executionResult } = afterExecutionInfo;
|
|
394
|
+
const { errors = [] } = executionResult;
|
|
395
|
+
for (const error of errors) {
|
|
396
|
+
const annotation = githubAnnotationFromError(error, {
|
|
397
|
+
rootDirectoryUrl,
|
|
398
|
+
executionInfo: afterExecutionInfo,
|
|
399
|
+
});
|
|
400
|
+
annotations.push(annotation);
|
|
401
|
+
}
|
|
402
|
+
githubCheckRun.progress({
|
|
403
|
+
title: "Jsenv test executions",
|
|
404
|
+
summary,
|
|
405
|
+
annotations,
|
|
406
|
+
});
|
|
407
|
+
};
|
|
408
|
+
afterAllExecutionCallback = async ({ testPlanSummary }) => {
|
|
409
|
+
const title = "Jsenv test results";
|
|
410
|
+
const summary = stripAnsi(formatSummary(testPlanSummary));
|
|
411
|
+
if (
|
|
412
|
+
testPlanSummary.counters.total !== testPlanSummary.counters.completed
|
|
413
|
+
) {
|
|
414
|
+
await githubCheckRun.fail({
|
|
415
|
+
title,
|
|
416
|
+
summary,
|
|
417
|
+
});
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
await githubCheckRun.pass({
|
|
421
|
+
title,
|
|
422
|
+
summary,
|
|
423
|
+
});
|
|
424
|
+
};
|
|
425
|
+
}
|
|
314
426
|
|
|
315
427
|
const result = await executeSteps(executionSteps, {
|
|
316
428
|
signal,
|
|
@@ -335,6 +447,14 @@ export const executeTestPlan = async ({
|
|
|
335
447
|
cooldownBetweenExecutions,
|
|
336
448
|
gcBetweenExecutions,
|
|
337
449
|
|
|
450
|
+
githubCheckEnabled,
|
|
451
|
+
githubCheckName,
|
|
452
|
+
githubCheckTitle,
|
|
453
|
+
githubCheckToken,
|
|
454
|
+
githubCheckRepositoryOwner,
|
|
455
|
+
githubCheckRepositoryName,
|
|
456
|
+
githubCheckCommitSha,
|
|
457
|
+
|
|
338
458
|
coverageEnabled,
|
|
339
459
|
coverageConfig,
|
|
340
460
|
coverageIncludeMissing,
|
|
@@ -342,11 +462,14 @@ export const executeTestPlan = async ({
|
|
|
342
462
|
coverageMethodForNodeJs,
|
|
343
463
|
coverageV8ConflictWarning,
|
|
344
464
|
coverageTempDirectoryUrl,
|
|
465
|
+
|
|
466
|
+
beforeExecutionCallback,
|
|
467
|
+
afterExecutionCallback,
|
|
345
468
|
});
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
result.planSummary.counters.total !== result.planSummary.counters.completed
|
|
349
|
-
) {
|
|
469
|
+
|
|
470
|
+
const hasFailed =
|
|
471
|
+
result.planSummary.counters.total !== result.planSummary.counters.completed;
|
|
472
|
+
if (updateProcessExitCode && hasFailed) {
|
|
350
473
|
process.exitCode = 1;
|
|
351
474
|
}
|
|
352
475
|
const planCoverage = result.planCoverage;
|
|
@@ -393,10 +516,13 @@ export const executeTestPlan = async ({
|
|
|
393
516
|
}
|
|
394
517
|
await Promise.all(promises);
|
|
395
518
|
}
|
|
396
|
-
|
|
519
|
+
|
|
520
|
+
const returnValue = {
|
|
397
521
|
testPlanAborted: result.aborted,
|
|
398
522
|
testPlanSummary: result.planSummary,
|
|
399
523
|
testPlanReport: result.planReport,
|
|
400
524
|
testPlanCoverage: planCoverage,
|
|
401
525
|
};
|
|
526
|
+
await afterAllExecutionCallback(returnValue);
|
|
527
|
+
return returnValue;
|
|
402
528
|
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { urlIsInsideOf, urlToRelativeUrl } from "@jsenv/urls";
|
|
2
|
+
|
|
3
|
+
export const githubAnnotationFromError = (
|
|
4
|
+
error,
|
|
5
|
+
{ rootDirectoryUrl, executionInfo },
|
|
6
|
+
) => {
|
|
7
|
+
const annotation = {
|
|
8
|
+
annotation_level: "failure",
|
|
9
|
+
path: executionInfo.fileRelativeUrl,
|
|
10
|
+
start_line: 1,
|
|
11
|
+
end_line: 1,
|
|
12
|
+
title: `Error while executing ${executionInfo.fileRelativeUrl} on ${executionInfo.runtimeName}@${executionInfo.runtimeVersion}`,
|
|
13
|
+
};
|
|
14
|
+
const exception = asException(error, { rootDirectoryUrl });
|
|
15
|
+
if (typeof exception.site.line === "number") {
|
|
16
|
+
annotation.path = urlToRelativeUrl(exception.site.url, rootDirectoryUrl);
|
|
17
|
+
annotation.start_line = exception.site.line;
|
|
18
|
+
annotation.end_line = exception.site.line;
|
|
19
|
+
annotation.start_column = exception.site.column;
|
|
20
|
+
annotation.end_column = exception.site.column;
|
|
21
|
+
}
|
|
22
|
+
annotation.message = exception.stack;
|
|
23
|
+
return annotation;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const asException = (error, { rootDirectoryUrl }) => {
|
|
27
|
+
const exception = {
|
|
28
|
+
isException: true,
|
|
29
|
+
stack: "",
|
|
30
|
+
site: {},
|
|
31
|
+
};
|
|
32
|
+
if (error === null || error === undefined || typeof error === "string") {
|
|
33
|
+
exception.message = String(error);
|
|
34
|
+
return exception;
|
|
35
|
+
}
|
|
36
|
+
if (error) {
|
|
37
|
+
exception.message = error.message;
|
|
38
|
+
if (error.isException) {
|
|
39
|
+
Object.assign(exception, error);
|
|
40
|
+
exception.stack = replaceUrls(
|
|
41
|
+
error.stack,
|
|
42
|
+
({ match, url, line = 1, column = 1 }) => {
|
|
43
|
+
if (urlIsInsideOf(url, rootDirectoryUrl)) {
|
|
44
|
+
const relativeUrl = urlToRelativeUrl(url, rootDirectoryUrl);
|
|
45
|
+
match = stringifyUrlSite({ url: relativeUrl, line, column });
|
|
46
|
+
}
|
|
47
|
+
return match;
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
} else if (error.stack) {
|
|
51
|
+
let firstSite = true;
|
|
52
|
+
exception.stack = replaceUrls(
|
|
53
|
+
error.stack,
|
|
54
|
+
({ match, url, line = 1, column = 1 }) => {
|
|
55
|
+
if (urlIsInsideOf(url, rootDirectoryUrl)) {
|
|
56
|
+
const relativeUrl = urlToRelativeUrl(url, rootDirectoryUrl);
|
|
57
|
+
match = stringifyUrlSite({ url: relativeUrl, line, column });
|
|
58
|
+
}
|
|
59
|
+
if (firstSite) {
|
|
60
|
+
firstSite = false;
|
|
61
|
+
exception.site.url = url;
|
|
62
|
+
exception.site.line = line;
|
|
63
|
+
exception.site.column = column;
|
|
64
|
+
}
|
|
65
|
+
return match;
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return exception;
|
|
71
|
+
}
|
|
72
|
+
exception.message = error;
|
|
73
|
+
return exception;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const stringifyUrlSite = ({ url, line, column }) => {
|
|
77
|
+
let string = url;
|
|
78
|
+
if (typeof line === "number") {
|
|
79
|
+
string += `:${line}`;
|
|
80
|
+
if (typeof column === "number") {
|
|
81
|
+
string += `:${column}`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const replaceUrls = (source, replace) => {
|
|
88
|
+
return source.replace(/(?:https?|ftp|file):\/\/\S+/gm, (match) => {
|
|
89
|
+
let replacement = "";
|
|
90
|
+
const lastChar = match[match.length - 1];
|
|
91
|
+
|
|
92
|
+
// hotfix because our url regex sucks a bit
|
|
93
|
+
const endsWithSeparationChar = lastChar === ")" || lastChar === ":";
|
|
94
|
+
if (endsWithSeparationChar) {
|
|
95
|
+
match = match.slice(0, -1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const lineAndColumnPattern = /:([0-9]+):([0-9]+)$/;
|
|
99
|
+
const lineAndColumMatch = match.match(lineAndColumnPattern);
|
|
100
|
+
if (lineAndColumMatch) {
|
|
101
|
+
const lineAndColumnString = lineAndColumMatch[0];
|
|
102
|
+
const lineString = lineAndColumMatch[1];
|
|
103
|
+
const columnString = lineAndColumMatch[2];
|
|
104
|
+
replacement = replace({
|
|
105
|
+
match: lineAndColumMatch,
|
|
106
|
+
url: match.slice(0, -lineAndColumnString.length),
|
|
107
|
+
line: lineString ? parseInt(lineString) : undefined,
|
|
108
|
+
column: columnString ? parseInt(columnString) : undefined,
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
const linePattern = /:([0-9]+)$/;
|
|
112
|
+
const lineMatch = match.match(linePattern);
|
|
113
|
+
if (lineMatch) {
|
|
114
|
+
const lineString = lineMatch[0];
|
|
115
|
+
replacement = replace({
|
|
116
|
+
match: lineMatch,
|
|
117
|
+
url: match.slice(0, -lineString.length),
|
|
118
|
+
line: lineString ? parseInt(lineString) : undefined,
|
|
119
|
+
});
|
|
120
|
+
} else {
|
|
121
|
+
replacement = replace({
|
|
122
|
+
match: lineMatch,
|
|
123
|
+
url: match,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (endsWithSeparationChar) {
|
|
128
|
+
return `${replacement}${lastChar}`;
|
|
129
|
+
}
|
|
130
|
+
return replacement;
|
|
131
|
+
});
|
|
132
|
+
};
|