as-test 0.5.1 → 0.5.3
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/CHANGELOG.md +101 -0
- package/README.md +268 -3
- package/as-test.config.schema.json +171 -2
- package/assembly/coverage.ts +20 -0
- package/assembly/index.ts +196 -12
- package/assembly/src/expectation.ts +15 -29
- package/assembly/src/log.ts +13 -1
- package/assembly/src/suite.ts +53 -9
- package/assembly/src/tests.ts +25 -5
- package/assembly/util/helpers.ts +0 -1
- package/assembly/util/json.ts +78 -0
- package/bin/build.js +118 -33
- package/bin/index.js +524 -35
- package/bin/init.js +35 -10
- package/bin/reporters/default.js +26 -9
- package/bin/reporters/tap.js +294 -0
- package/bin/run.js +368 -44
- package/bin/types.js +18 -0
- package/bin/util.js +229 -1
- package/package.json +40 -50
- package/transform/lib/coverage.js +135 -124
- package/transform/lib/index.js +57 -23
- package/transform/lib/log.js +2 -39
- package/transform/lib/mock.js +42 -22
- package/transform/lib/builder.js.map +0 -1
- package/transform/lib/coverage.js.map +0 -1
- package/transform/lib/index.js.map +0 -1
- package/transform/lib/linker.js.map +0 -1
- package/transform/lib/location.js.map +0 -1
- package/transform/lib/log.js.map +0 -1
- package/transform/lib/mock.js.map +0 -1
- package/transform/lib/range.js.map +0 -1
- package/transform/lib/types.js.map +0 -1
- package/transform/lib/util.js.map +0 -1
- package/transform/lib/visitor.js.map +0 -1
package/bin/init.js
CHANGED
|
@@ -148,11 +148,9 @@ function printPlan(root, target, example) {
|
|
|
148
148
|
if (example != "none") {
|
|
149
149
|
console.log(chalk.dim(" assembly/__tests__/example.spec.ts"));
|
|
150
150
|
}
|
|
151
|
-
if (target == "wasi") {
|
|
151
|
+
if (target == "wasi" || target == "bindings") {
|
|
152
152
|
console.log(chalk.dim(" .as-test/runners/default.wasi.js"));
|
|
153
|
-
|
|
154
|
-
if (target == "bindings") {
|
|
155
|
-
console.log(chalk.dim(" .as-test/runners/default.run.js"));
|
|
153
|
+
console.log(chalk.dim(" .as-test/runners/default.bindings.js"));
|
|
156
154
|
}
|
|
157
155
|
console.log(chalk.dim(" package.json"));
|
|
158
156
|
console.log("");
|
|
@@ -171,15 +169,17 @@ function applyInit(root, target, example, force) {
|
|
|
171
169
|
if (target == "wasi" || target == "bindings") {
|
|
172
170
|
ensureDir(root, ".as-test/runners", summary);
|
|
173
171
|
}
|
|
172
|
+
ensureGitignoreIncludesAsTestDirs(root, summary);
|
|
174
173
|
const configPath = path.join(root, "as-test.config.json");
|
|
175
174
|
const config = loadConfig(configPath, false);
|
|
176
175
|
config.$schema = "./node_modules/as-test/as-test.config.schema.json";
|
|
177
176
|
config.buildOptions.target = target;
|
|
177
|
+
config.runOptions.reporter = "default";
|
|
178
178
|
if (target == "wasi") {
|
|
179
179
|
config.runOptions.runtime.cmd = "node ./.as-test/runners/default.wasi.js <file>";
|
|
180
180
|
}
|
|
181
181
|
else {
|
|
182
|
-
config.runOptions.runtime.cmd = "node ./.as-test/runners/default.
|
|
182
|
+
config.runOptions.runtime.cmd = "node ./.as-test/runners/default.bindings.js <file>";
|
|
183
183
|
}
|
|
184
184
|
writeJson(configPath, config, summary, "as-test.config.json");
|
|
185
185
|
if (example != "none") {
|
|
@@ -187,13 +187,13 @@ function applyInit(root, target, example, force) {
|
|
|
187
187
|
const content = example == "minimal" ? buildMinimalExampleSpec() : buildFullExampleSpec();
|
|
188
188
|
writeManagedFile(examplePath, content, force, summary, "assembly/__tests__/example.spec.ts");
|
|
189
189
|
}
|
|
190
|
-
if (target == "wasi") {
|
|
190
|
+
if (target == "wasi" || target == "bindings") {
|
|
191
191
|
const runnerPath = path.join(root, ".as-test/runners/default.wasi.js");
|
|
192
192
|
writeManagedFile(runnerPath, buildWasiRunner(), force, summary, ".as-test/runners/default.wasi.js");
|
|
193
193
|
}
|
|
194
|
-
if (target == "bindings") {
|
|
195
|
-
const runnerPath = path.join(root, ".as-test/runners/default.
|
|
196
|
-
writeManagedFile(runnerPath, buildBindingsRunner(), force, summary, ".as-test/runners/default.
|
|
194
|
+
if (target == "wasi" || target == "bindings") {
|
|
195
|
+
const runnerPath = path.join(root, ".as-test/runners/default.bindings.js");
|
|
196
|
+
writeManagedFile(runnerPath, buildBindingsRunner(), force, summary, ".as-test/runners/default.bindings.js");
|
|
197
197
|
}
|
|
198
198
|
const pkgPath = path.join(root, "package.json");
|
|
199
199
|
const pkg = existsSync(pkgPath)
|
|
@@ -232,6 +232,31 @@ function ensureDir(root, rel, summary) {
|
|
|
232
232
|
mkdirSync(full, { recursive: true });
|
|
233
233
|
summary.created.push(rel + "/");
|
|
234
234
|
}
|
|
235
|
+
function ensureGitignoreIncludesAsTestDirs(root, summary) {
|
|
236
|
+
const rel = ".gitignore";
|
|
237
|
+
const fullPath = path.join(root, rel);
|
|
238
|
+
const entries = ["!.as-test/runners/", "!.as-test/snapshots/"];
|
|
239
|
+
const existed = existsSync(fullPath);
|
|
240
|
+
const source = existed ? readFileSync(fullPath, "utf8") : "";
|
|
241
|
+
const lines = source.split(/\r?\n/);
|
|
242
|
+
const missing = entries.filter((entry) => !lines.some((line) => line.trim() == entry));
|
|
243
|
+
if (!missing.length) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const eol = source.includes("\r\n") ? "\r\n" : "\n";
|
|
247
|
+
let output = source;
|
|
248
|
+
if (output.length &&
|
|
249
|
+
!output.endsWith("\n") &&
|
|
250
|
+
!output.endsWith("\r\n")) {
|
|
251
|
+
output += eol;
|
|
252
|
+
}
|
|
253
|
+
output += missing.join(eol) + eol;
|
|
254
|
+
writeFileSync(fullPath, output);
|
|
255
|
+
if (existed)
|
|
256
|
+
summary.updated.push(rel);
|
|
257
|
+
else
|
|
258
|
+
summary.created.push(rel);
|
|
259
|
+
}
|
|
235
260
|
function writeJson(fullPath, value, summary, displayPath) {
|
|
236
261
|
const rel = displayPath ??
|
|
237
262
|
path.relative(process.cwd(), fullPath) ??
|
|
@@ -449,7 +474,7 @@ function withNodeIo(imports = {}) {
|
|
|
449
474
|
|
|
450
475
|
const wasmPathArg = process.argv[2];
|
|
451
476
|
if (!wasmPathArg) {
|
|
452
|
-
process.stderr.write("usage: node ./.as-test/runners/default.
|
|
477
|
+
process.stderr.write("usage: node ./.as-test/runners/default.bindings.js <file.wasm>\\n");
|
|
453
478
|
process.exit(1);
|
|
454
479
|
}
|
|
455
480
|
|
package/bin/reporters/default.js
CHANGED
|
@@ -13,9 +13,11 @@ class DefaultReporter {
|
|
|
13
13
|
this.renderedLines = 0;
|
|
14
14
|
this.fileHasWarning = false;
|
|
15
15
|
this.verboseMode = false;
|
|
16
|
+
this.cleanMode = false;
|
|
16
17
|
}
|
|
17
18
|
canRewriteLine() {
|
|
18
|
-
return
|
|
19
|
+
return (!this.cleanMode &&
|
|
20
|
+
Boolean(this.context.stdout.isTTY));
|
|
19
21
|
}
|
|
20
22
|
badgeRunning() {
|
|
21
23
|
return chalk.bgBlackBright.white(" .... ");
|
|
@@ -115,18 +117,15 @@ class DefaultReporter {
|
|
|
115
117
|
}
|
|
116
118
|
onRunStart(event) {
|
|
117
119
|
this.verboseMode = Boolean(event.verbose);
|
|
118
|
-
|
|
119
|
-
return;
|
|
120
|
-
if (event.snapshotEnabled) {
|
|
121
|
-
this.context.stdout.write(chalk.bgBlue(" SNAPSHOT ") +
|
|
122
|
-
` ${chalk.dim(event.updateSnapshots ? "update mode enabled" : "read-only mode")}\n\n`);
|
|
123
|
-
}
|
|
120
|
+
this.cleanMode = Boolean(event.clean);
|
|
124
121
|
}
|
|
125
122
|
onFileStart(event) {
|
|
126
123
|
this.currentFile = event.file;
|
|
127
124
|
this.openSuites = [];
|
|
128
125
|
this.verboseSuites = [];
|
|
129
126
|
this.fileHasWarning = false;
|
|
127
|
+
if (this.cleanMode)
|
|
128
|
+
return;
|
|
130
129
|
if (this.verboseMode && this.canRewriteLine()) {
|
|
131
130
|
this.renderVerboseState();
|
|
132
131
|
return;
|
|
@@ -157,6 +156,8 @@ class DefaultReporter {
|
|
|
157
156
|
this.fileHasWarning = false;
|
|
158
157
|
}
|
|
159
158
|
onSuiteStart(event) {
|
|
159
|
+
if (this.cleanMode)
|
|
160
|
+
return;
|
|
160
161
|
const depth = Math.max(event.depth, 0);
|
|
161
162
|
if (this.verboseMode && this.canRewriteLine()) {
|
|
162
163
|
if (this.currentFile !== event.file)
|
|
@@ -180,6 +181,8 @@ class DefaultReporter {
|
|
|
180
181
|
this.renderLiveState();
|
|
181
182
|
}
|
|
182
183
|
onSuiteEnd(event) {
|
|
184
|
+
if (this.cleanMode)
|
|
185
|
+
return;
|
|
183
186
|
const depth = Math.max(event.depth, 0);
|
|
184
187
|
const verdict = String(event.verdict ?? "none");
|
|
185
188
|
if (this.verboseMode && this.canRewriteLine()) {
|
|
@@ -239,7 +242,7 @@ class DefaultReporter {
|
|
|
239
242
|
renderCoveragePoints(event.coverageSummary.files);
|
|
240
243
|
}
|
|
241
244
|
}
|
|
242
|
-
renderTotals(event.stats);
|
|
245
|
+
renderTotals(event.stats, event);
|
|
243
246
|
}
|
|
244
247
|
}
|
|
245
248
|
function renderFailedSuites(failedEntries) {
|
|
@@ -311,7 +314,7 @@ function renderSnapshotSummary(snapshotSummary) {
|
|
|
311
314
|
console.log("");
|
|
312
315
|
console.log(`${chalk.bold("Snapshots:")} ${chalk.greenBright(snapshotSummary.matched)} matched, ${chalk.blueBright(snapshotSummary.created)} created, ${chalk.blueBright(snapshotSummary.updated)} updated, ${snapshotSummary.failed ? chalk.red(snapshotSummary.failed) : chalk.greenBright("0")} failed`);
|
|
313
316
|
}
|
|
314
|
-
function renderTotals(stats) {
|
|
317
|
+
function renderTotals(stats, event) {
|
|
315
318
|
console.log("");
|
|
316
319
|
process.stdout.write(chalk.bold("Files: "));
|
|
317
320
|
process.stdout.write(stats.failedFiles
|
|
@@ -344,8 +347,22 @@ function renderTotals(stats) {
|
|
|
344
347
|
? chalk.gray(stats.skippedTests + " skipped")
|
|
345
348
|
: chalk.gray("0 skipped")));
|
|
346
349
|
process.stdout.write(", " + (stats.failedTests + stats.passedTests + stats.skippedTests) + " total\n");
|
|
350
|
+
if (event.modeSummary) {
|
|
351
|
+
renderModeSummary(event.modeSummary);
|
|
352
|
+
}
|
|
347
353
|
process.stdout.write(chalk.bold("Time: ") + formatTime(stats.time) + "\n");
|
|
348
354
|
}
|
|
355
|
+
function renderModeSummary(summary) {
|
|
356
|
+
process.stdout.write(chalk.bold("Modes: "));
|
|
357
|
+
process.stdout.write(summary.failed
|
|
358
|
+
? chalk.bold.red(summary.failed + " failed")
|
|
359
|
+
: chalk.bold.greenBright("0 failed"));
|
|
360
|
+
process.stdout.write(", " +
|
|
361
|
+
(summary.skipped
|
|
362
|
+
? chalk.gray(summary.skipped + " skipped")
|
|
363
|
+
: chalk.gray("0 skipped")));
|
|
364
|
+
process.stdout.write(", " + summary.total + " total\n");
|
|
365
|
+
}
|
|
349
366
|
function renderCoverageSummary(summary) {
|
|
350
367
|
const pct = summary.total
|
|
351
368
|
? ((summary.covered * 100) / summary.total).toFixed(2)
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
3
|
+
export function createTapReporter(context, config = {}) {
|
|
4
|
+
return new TapReporter(context, normalizeTapConfig(config));
|
|
5
|
+
}
|
|
6
|
+
export const createReporter = (context) => {
|
|
7
|
+
return createTapReporter(context);
|
|
8
|
+
};
|
|
9
|
+
class TapReporter {
|
|
10
|
+
constructor(context, config) {
|
|
11
|
+
this.context = context;
|
|
12
|
+
this.config = config;
|
|
13
|
+
}
|
|
14
|
+
onRunComplete(event) {
|
|
15
|
+
const points = collectTapPoints(event.reports);
|
|
16
|
+
const output = buildTapDocument(points);
|
|
17
|
+
this.context.stdout.write(output);
|
|
18
|
+
for (const point of points) {
|
|
19
|
+
if (point.status != "fail")
|
|
20
|
+
continue;
|
|
21
|
+
emitGitHubAnnotation(this.context, point);
|
|
22
|
+
}
|
|
23
|
+
this.writeArtifacts(points, output);
|
|
24
|
+
}
|
|
25
|
+
writeArtifacts(points, output) {
|
|
26
|
+
if (this.config.mode == "per-file") {
|
|
27
|
+
this.writePerFileArtifacts(points);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const outFile = path.resolve(process.cwd(), this.config.outFile);
|
|
31
|
+
mkdirSync(path.dirname(outFile), { recursive: true });
|
|
32
|
+
writeFileSync(outFile, output);
|
|
33
|
+
}
|
|
34
|
+
writePerFileArtifacts(points) {
|
|
35
|
+
const groups = new Map();
|
|
36
|
+
for (const point of points) {
|
|
37
|
+
const key = point.file?.length ? point.file : "unknown";
|
|
38
|
+
if (!groups.has(key))
|
|
39
|
+
groups.set(key, []);
|
|
40
|
+
groups.get(key).push(point);
|
|
41
|
+
}
|
|
42
|
+
let unknownIndex = 0;
|
|
43
|
+
const outDir = path.resolve(process.cwd(), this.config.outDir);
|
|
44
|
+
mkdirSync(outDir, { recursive: true });
|
|
45
|
+
for (const [fileKey, filePoints] of groups) {
|
|
46
|
+
const fileName = fileKey == "unknown"
|
|
47
|
+
? `unknown-${++unknownIndex}.tap`
|
|
48
|
+
: toTapFileName(fileKey);
|
|
49
|
+
const outFile = path.join(outDir, fileName);
|
|
50
|
+
writeFileSync(outFile, buildTapDocument(filePoints));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function normalizeTapConfig(config) {
|
|
55
|
+
const mode = config.mode == "per-file" ? "per-file" : "single-file";
|
|
56
|
+
const outDir = typeof config.outDir == "string" && config.outDir.trim().length
|
|
57
|
+
? config.outDir.trim()
|
|
58
|
+
: "./.as-test/reports";
|
|
59
|
+
const outFile = typeof config.outFile == "string" && config.outFile.trim().length
|
|
60
|
+
? config.outFile.trim()
|
|
61
|
+
: path.join(outDir, "report.tap");
|
|
62
|
+
return {
|
|
63
|
+
mode,
|
|
64
|
+
outDir,
|
|
65
|
+
outFile,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function toTapFileName(file) {
|
|
69
|
+
const normalized = file
|
|
70
|
+
.replace(/^[.\\/]+/, "")
|
|
71
|
+
.replace(/[\\/]/g, "__")
|
|
72
|
+
.replace(/\.[^.]+$/, "");
|
|
73
|
+
return `${normalized}.tap`;
|
|
74
|
+
}
|
|
75
|
+
function buildTapDocument(points) {
|
|
76
|
+
const totals = {
|
|
77
|
+
pass: 0,
|
|
78
|
+
fail: 0,
|
|
79
|
+
skip: 0,
|
|
80
|
+
};
|
|
81
|
+
const lines = [];
|
|
82
|
+
lines.push("TAP version 13");
|
|
83
|
+
lines.push(`1..${points.length}`);
|
|
84
|
+
for (let i = 0; i < points.length; i++) {
|
|
85
|
+
const point = points[i];
|
|
86
|
+
const id = i + 1;
|
|
87
|
+
const name = sanitizeTap(point.name.length ? point.name : `test ${id}`);
|
|
88
|
+
if (point.status == "fail") {
|
|
89
|
+
totals.fail++;
|
|
90
|
+
lines.push(`not ok ${id} - ${name}`);
|
|
91
|
+
lines.push(...buildFailDetails(point));
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (point.status == "skip") {
|
|
95
|
+
totals.skip++;
|
|
96
|
+
lines.push(`ok ${id} - ${name} # SKIP`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
totals.pass++;
|
|
100
|
+
lines.push(`ok ${id} - ${name}`);
|
|
101
|
+
}
|
|
102
|
+
lines.push(`# tests ${points.length}`);
|
|
103
|
+
lines.push(`# pass ${totals.pass}`);
|
|
104
|
+
if (totals.skip) {
|
|
105
|
+
lines.push(`# skip ${totals.skip}`);
|
|
106
|
+
}
|
|
107
|
+
lines.push(`# fail ${totals.fail}`);
|
|
108
|
+
return lines.join("\n") + "\n";
|
|
109
|
+
}
|
|
110
|
+
function buildFailDetails(point) {
|
|
111
|
+
const lines = [" ---", ` message: ${JSON.stringify(point.message ?? "assertion failed")}`];
|
|
112
|
+
if (point.file) {
|
|
113
|
+
lines.push(` file: ${JSON.stringify(point.file)}`);
|
|
114
|
+
}
|
|
115
|
+
if (point.line) {
|
|
116
|
+
lines.push(` line: ${point.line}`);
|
|
117
|
+
}
|
|
118
|
+
if (point.column) {
|
|
119
|
+
lines.push(` column: ${point.column}`);
|
|
120
|
+
}
|
|
121
|
+
if (point.matcher) {
|
|
122
|
+
lines.push(` matcher: ${JSON.stringify(point.matcher)}`);
|
|
123
|
+
}
|
|
124
|
+
if (point.expected != null) {
|
|
125
|
+
lines.push(` expected: ${JSON.stringify(point.expected)}`);
|
|
126
|
+
}
|
|
127
|
+
if (point.actual != null) {
|
|
128
|
+
lines.push(` actual: ${JSON.stringify(point.actual)}`);
|
|
129
|
+
}
|
|
130
|
+
if (point.durationMs != null) {
|
|
131
|
+
lines.push(` duration_ms: ${Math.round(point.durationMs * 1000) / 1000}`);
|
|
132
|
+
}
|
|
133
|
+
lines.push(" ...");
|
|
134
|
+
return lines;
|
|
135
|
+
}
|
|
136
|
+
function collectTapPoints(reports) {
|
|
137
|
+
const points = [];
|
|
138
|
+
if (!Array.isArray(reports))
|
|
139
|
+
return points;
|
|
140
|
+
for (const report of reports) {
|
|
141
|
+
const reportAny = report;
|
|
142
|
+
const file = String(reportAny.file ?? "");
|
|
143
|
+
const suites = Array.isArray(reportAny.suites)
|
|
144
|
+
? reportAny.suites
|
|
145
|
+
: [];
|
|
146
|
+
for (const suite of suites) {
|
|
147
|
+
collectTapPointsFromSuite(suite, file, [], points);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return points;
|
|
151
|
+
}
|
|
152
|
+
function collectTapPointsFromSuite(suite, file, pathStack, points) {
|
|
153
|
+
const suiteAny = suite;
|
|
154
|
+
const description = String(suiteAny.description ?? "suite");
|
|
155
|
+
const fullPath = [...pathStack, description];
|
|
156
|
+
const localFile = suiteAny.file ? String(suiteAny.file) : file;
|
|
157
|
+
const childSuites = Array.isArray(suiteAny.suites)
|
|
158
|
+
? suiteAny.suites
|
|
159
|
+
: [];
|
|
160
|
+
const tests = Array.isArray(suiteAny.tests)
|
|
161
|
+
? suiteAny.tests
|
|
162
|
+
: [];
|
|
163
|
+
const suiteKind = String(suiteAny.kind ?? "");
|
|
164
|
+
const durationMs = suiteDuration(suiteAny.time);
|
|
165
|
+
if (tests.length > 0) {
|
|
166
|
+
for (let i = 0; i < tests.length; i++) {
|
|
167
|
+
const test = tests[i];
|
|
168
|
+
const location = parseLocation(test.location);
|
|
169
|
+
const name = tests.length > 1
|
|
170
|
+
? `${fullPath.join(" > ")} #${i + 1}`
|
|
171
|
+
: fullPath.join(" > ");
|
|
172
|
+
const status = normalizeStatus(test.verdict);
|
|
173
|
+
const matcher = stringifyValue(test.instr);
|
|
174
|
+
const expected = stringifyValue(test.right);
|
|
175
|
+
const actual = stringifyValue(test.left);
|
|
176
|
+
const message = buildFailureMessage(stringifyValue(test.message), matcher, expected, actual);
|
|
177
|
+
points.push({
|
|
178
|
+
name,
|
|
179
|
+
status,
|
|
180
|
+
file: localFile,
|
|
181
|
+
line: location.line,
|
|
182
|
+
column: location.column,
|
|
183
|
+
matcher,
|
|
184
|
+
expected,
|
|
185
|
+
actual,
|
|
186
|
+
message,
|
|
187
|
+
durationMs,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else if (childSuites.length == 0 &&
|
|
192
|
+
(suiteKind == "test" ||
|
|
193
|
+
suiteKind == "it" ||
|
|
194
|
+
suiteKind == "xtest" ||
|
|
195
|
+
suiteKind == "xit")) {
|
|
196
|
+
points.push({
|
|
197
|
+
name: fullPath.join(" > "),
|
|
198
|
+
status: normalizeStatus(suiteAny.verdict),
|
|
199
|
+
file: localFile,
|
|
200
|
+
durationMs,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
for (const child of childSuites) {
|
|
204
|
+
collectTapPointsFromSuite(child, localFile, fullPath, points);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function suiteDuration(value) {
|
|
208
|
+
const time = value;
|
|
209
|
+
if (!time)
|
|
210
|
+
return undefined;
|
|
211
|
+
const start = Number(time.start ?? 0);
|
|
212
|
+
const end = Number(time.end ?? 0);
|
|
213
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
return end - start;
|
|
217
|
+
}
|
|
218
|
+
function parseLocation(value) {
|
|
219
|
+
const text = String(value ?? "").trim();
|
|
220
|
+
if (!text.length)
|
|
221
|
+
return {};
|
|
222
|
+
const match = /^(\d+)(?::(\d+))?$/.exec(text);
|
|
223
|
+
if (!match)
|
|
224
|
+
return {};
|
|
225
|
+
const line = Number(match[1]);
|
|
226
|
+
const column = match[2] ? Number(match[2]) : undefined;
|
|
227
|
+
return {
|
|
228
|
+
line: Number.isFinite(line) && line > 0 ? line : undefined,
|
|
229
|
+
column: typeof column == "number" && Number.isFinite(column) && column > 0
|
|
230
|
+
? column
|
|
231
|
+
: undefined,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function normalizeStatus(verdict) {
|
|
235
|
+
const value = String(verdict ?? "none");
|
|
236
|
+
if (value == "fail")
|
|
237
|
+
return "fail";
|
|
238
|
+
if (value == "ok")
|
|
239
|
+
return "ok";
|
|
240
|
+
return "skip";
|
|
241
|
+
}
|
|
242
|
+
function stringifyValue(value) {
|
|
243
|
+
if (value == null)
|
|
244
|
+
return "";
|
|
245
|
+
if (typeof value == "string")
|
|
246
|
+
return value;
|
|
247
|
+
try {
|
|
248
|
+
return JSON.stringify(value);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return String(value);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function buildFailureMessage(message, matcher, expected, actual) {
|
|
255
|
+
if (message.length)
|
|
256
|
+
return message;
|
|
257
|
+
if (matcher.length && expected.length && actual.length) {
|
|
258
|
+
return `${matcher} expected ${expected} but received ${actual}`;
|
|
259
|
+
}
|
|
260
|
+
if (matcher.length)
|
|
261
|
+
return `${matcher} failed`;
|
|
262
|
+
return "assertion failed";
|
|
263
|
+
}
|
|
264
|
+
function sanitizeTap(name) {
|
|
265
|
+
return name.replace(/\s+/g, " ").replace(/#/g, "\\#").trim();
|
|
266
|
+
}
|
|
267
|
+
function emitGitHubAnnotation(context, point) {
|
|
268
|
+
if (process.env.GITHUB_ACTIONS != "true" || point.status != "fail")
|
|
269
|
+
return;
|
|
270
|
+
const properties = [];
|
|
271
|
+
if (point.file) {
|
|
272
|
+
properties.push(`file=${escapeGithubValue(point.file, true)}`);
|
|
273
|
+
}
|
|
274
|
+
if (point.line) {
|
|
275
|
+
properties.push(`line=${point.line}`);
|
|
276
|
+
}
|
|
277
|
+
if (point.column) {
|
|
278
|
+
properties.push(`col=${point.column}`);
|
|
279
|
+
}
|
|
280
|
+
properties.push(`title=${escapeGithubValue("as-test", true)}`);
|
|
281
|
+
const message = point.message?.length ? point.message : "assertion failed";
|
|
282
|
+
const detail = `${message} | test=${point.name}`;
|
|
283
|
+
context.stdout.write(`::error ${properties.join(",")}::${escapeGithubValue(detail)}\n`);
|
|
284
|
+
}
|
|
285
|
+
function escapeGithubValue(value, property = false) {
|
|
286
|
+
let output = value
|
|
287
|
+
.replace(/%/g, "%25")
|
|
288
|
+
.replace(/\r/g, "%0D")
|
|
289
|
+
.replace(/\n/g, "%0A");
|
|
290
|
+
if (property) {
|
|
291
|
+
output = output.replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
292
|
+
}
|
|
293
|
+
return output;
|
|
294
|
+
}
|