as-test 0.5.0 → 0.5.2
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 +66 -0
- package/README.md +238 -2
- package/as-test.config.schema.json +153 -2
- 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 +79 -0
- package/bin/build.js +23 -18
- package/bin/index.js +66 -8
- package/bin/init.js +35 -11
- package/bin/reporters/tap.js +294 -0
- package/bin/run.js +314 -43
- package/bin/types.js +16 -0
- package/bin/util.js +196 -1
- package/package.json +2 -10
- package/transform/lib/coverage.js +5 -0
- package/transform/lib/index.js +1 -1
- 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/build.js
CHANGED
|
@@ -3,31 +3,45 @@ import { glob } from "glob";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { execSync } from "child_process";
|
|
5
5
|
import * as path from "path";
|
|
6
|
-
import { getPkgRunner, loadConfig } from "./util.js";
|
|
6
|
+
import { applyMode, getPkgRunner, loadConfig } from "./util.js";
|
|
7
7
|
const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
|
|
8
|
-
export async function build(configPath = DEFAULT_CONFIG_PATH, selectors = []) {
|
|
9
|
-
const
|
|
8
|
+
export async function build(configPath = DEFAULT_CONFIG_PATH, selectors = [], modeName) {
|
|
9
|
+
const loadedConfig = loadConfig(configPath, false);
|
|
10
|
+
const mode = applyMode(loadedConfig, modeName);
|
|
11
|
+
const config = mode.config;
|
|
10
12
|
ensureDeps(config);
|
|
11
13
|
const pkgRunner = getPkgRunner();
|
|
12
14
|
const inputPatterns = resolveInputPatterns(config.input, selectors);
|
|
13
|
-
const inputFiles = await glob(inputPatterns);
|
|
15
|
+
const inputFiles = (await glob(inputPatterns)).sort((a, b) => a.localeCompare(b));
|
|
14
16
|
const buildArgs = getBuildArgs(config);
|
|
15
17
|
for (const file of inputFiles) {
|
|
16
18
|
let cmd = `${pkgRunner} asc ${file}${buildArgs}`;
|
|
17
|
-
const outFile = `${config.outDir}/${
|
|
19
|
+
const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName)}`;
|
|
18
20
|
if (config.outDir) {
|
|
19
21
|
cmd += " -o " + outFile;
|
|
20
22
|
}
|
|
21
23
|
try {
|
|
22
|
-
buildFile(cmd);
|
|
24
|
+
buildFile(cmd, mode.env);
|
|
23
25
|
}
|
|
24
26
|
catch (error) {
|
|
25
27
|
throw new Error(`Failed to build ${path.basename(file)} with ${getBuildStderr(error)}`);
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
}
|
|
31
|
+
function resolveArtifactFileName(file, target, modeName) {
|
|
32
|
+
const base = path
|
|
33
|
+
.basename(file)
|
|
34
|
+
.replace(/\.spec\.ts$/, "")
|
|
35
|
+
.replace(/\.ts$/, "");
|
|
36
|
+
if (!modeName) {
|
|
37
|
+
return `${path.basename(file).replace(".ts", ".wasm")}`;
|
|
38
|
+
}
|
|
39
|
+
return `${base}.${modeName}.${target}.wasm`;
|
|
40
|
+
}
|
|
29
41
|
function resolveInputPatterns(configured, selectors) {
|
|
30
|
-
const configuredInputs = Array.isArray(configured)
|
|
42
|
+
const configuredInputs = Array.isArray(configured)
|
|
43
|
+
? configured
|
|
44
|
+
: [configured];
|
|
31
45
|
if (!selectors.length)
|
|
32
46
|
return configuredInputs;
|
|
33
47
|
const patterns = new Set();
|
|
@@ -60,15 +74,12 @@ function ensureDeps(config) {
|
|
|
60
74
|
process.exit(1);
|
|
61
75
|
}
|
|
62
76
|
}
|
|
63
|
-
if (!hasJsonAsTransform()) {
|
|
64
|
-
console.log(`${chalk.bgRed(" ERROR ")}${chalk.dim(":")} could not find json-as. Install it to compile as-test suites.`);
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
77
|
}
|
|
68
|
-
function buildFile(command) {
|
|
78
|
+
function buildFile(command, env) {
|
|
69
79
|
execSync(command, {
|
|
70
80
|
stdio: ["ignore", "pipe", "pipe"],
|
|
71
81
|
encoding: "utf8",
|
|
82
|
+
env,
|
|
72
83
|
});
|
|
73
84
|
}
|
|
74
85
|
function getBuildStderr(error) {
|
|
@@ -90,7 +101,6 @@ function getBuildStderr(error) {
|
|
|
90
101
|
function getBuildArgs(config) {
|
|
91
102
|
let buildArgs = "";
|
|
92
103
|
buildArgs += " --transform as-test/transform";
|
|
93
|
-
buildArgs += " --transform json-as/transform";
|
|
94
104
|
if (hasTryAsRuntime()) {
|
|
95
105
|
buildArgs += " --transform try-as/transform";
|
|
96
106
|
}
|
|
@@ -124,8 +134,3 @@ function hasTryAsRuntime() {
|
|
|
124
134
|
return (existsSync(path.join(process.cwd(), "node_modules/try-as")) ||
|
|
125
135
|
existsSync(path.join(process.cwd(), "node_modules/try-as/package.json")));
|
|
126
136
|
}
|
|
127
|
-
function hasJsonAsTransform() {
|
|
128
|
-
return (existsSync(path.join(process.cwd(), "node_modules/json-as/transform.js")) ||
|
|
129
|
-
existsSync(path.join(process.cwd(), "node_modules/json-as/transform.ts")) ||
|
|
130
|
-
existsSync(path.join(process.cwd(), "node_modules/json-as/transform")));
|
|
131
|
-
}
|
package/bin/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import chalk from "chalk";
|
|
|
3
3
|
import { build } from "./build.js";
|
|
4
4
|
import { createRunReporter, run } from "./run.js";
|
|
5
5
|
import { init } from "./init.js";
|
|
6
|
-
import { getCliVersion, loadConfig } from "./util.js";
|
|
6
|
+
import { getCliVersion, loadConfig, resolveModeNames } from "./util.js";
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import { glob } from "glob";
|
|
9
9
|
const _args = process.argv.slice(2);
|
|
@@ -12,6 +12,7 @@ const args = [];
|
|
|
12
12
|
const COMMANDS = ["run", "build", "test", "init"];
|
|
13
13
|
const version = getCliVersion();
|
|
14
14
|
const configPath = resolveConfigPath(_args);
|
|
15
|
+
const selectedModes = resolveModeNames(_args);
|
|
15
16
|
for (const arg of _args) {
|
|
16
17
|
if (arg.startsWith("-"))
|
|
17
18
|
flags.push(arg);
|
|
@@ -37,16 +38,19 @@ else if (COMMANDS.includes(args[0])) {
|
|
|
37
38
|
verbose: flags.includes("--verbose"),
|
|
38
39
|
};
|
|
39
40
|
if (command === "build") {
|
|
40
|
-
|
|
41
|
+
runBuildModes(configPath, commandArgs, selectedModes).catch((error) => {
|
|
41
42
|
printCliError(error);
|
|
42
43
|
process.exit(1);
|
|
43
44
|
});
|
|
44
45
|
}
|
|
45
46
|
else if (command === "run") {
|
|
46
|
-
|
|
47
|
+
runRuntimeModes(runFlags, configPath, commandArgs, selectedModes).catch((error) => {
|
|
48
|
+
printCliError(error);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
});
|
|
47
51
|
}
|
|
48
52
|
else if (command === "test") {
|
|
49
|
-
|
|
53
|
+
runTestModes(runFlags, configPath, commandArgs, selectedModes).catch((error) => {
|
|
50
54
|
printCliError(error);
|
|
51
55
|
process.exit(1);
|
|
52
56
|
});
|
|
@@ -109,6 +113,10 @@ function info() {
|
|
|
109
113
|
"Initialize an empty testing template");
|
|
110
114
|
console.log("");
|
|
111
115
|
console.log(chalk.bold("Flags:"));
|
|
116
|
+
console.log(" " +
|
|
117
|
+
chalk.bold.blue("--mode <name[,name...]>") +
|
|
118
|
+
" " +
|
|
119
|
+
"Run one or multiple named config modes");
|
|
112
120
|
console.log(" " +
|
|
113
121
|
chalk.bold.blue("--config <path>") +
|
|
114
122
|
" " +
|
|
@@ -133,6 +141,10 @@ function info() {
|
|
|
133
141
|
chalk.bold.blue("--verbose") +
|
|
134
142
|
" " +
|
|
135
143
|
"Print each suite start/end line");
|
|
144
|
+
console.log(" " +
|
|
145
|
+
chalk.bold.blue("--reporter <name|path>") +
|
|
146
|
+
" " +
|
|
147
|
+
"Use built-in reporter (default|tap) or custom module path");
|
|
136
148
|
console.log("");
|
|
137
149
|
console.log(chalk.dim("If your using this, consider dropping a star, it would help a lot!") + "\n");
|
|
138
150
|
console.log("View the repo: " +
|
|
@@ -176,9 +188,26 @@ function resolveCommandArgs(rawArgs, command) {
|
|
|
176
188
|
i++;
|
|
177
189
|
continue;
|
|
178
190
|
}
|
|
191
|
+
if (arg == "--mode") {
|
|
192
|
+
i++;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
179
195
|
if (arg.startsWith("--config=")) {
|
|
180
196
|
continue;
|
|
181
197
|
}
|
|
198
|
+
if (arg.startsWith("--mode=")) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (arg == "--reporter") {
|
|
202
|
+
i++;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (arg.startsWith("--reporter=")) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (arg == "--tap") {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
182
211
|
if (arg.startsWith("-")) {
|
|
183
212
|
continue;
|
|
184
213
|
}
|
|
@@ -200,7 +229,7 @@ function resolveCommandTokens(rawArgs, command) {
|
|
|
200
229
|
}
|
|
201
230
|
return values;
|
|
202
231
|
}
|
|
203
|
-
async function runTestSequential(runFlags, configPath, selectors) {
|
|
232
|
+
async function runTestSequential(runFlags, configPath, selectors, modeName) {
|
|
204
233
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
205
234
|
if (!files.length) {
|
|
206
235
|
const scope = selectors.length > 0
|
|
@@ -208,7 +237,7 @@ async function runTestSequential(runFlags, configPath, selectors) {
|
|
|
208
237
|
: "configured input patterns";
|
|
209
238
|
throw new Error(`No test files matched: ${scope}`);
|
|
210
239
|
}
|
|
211
|
-
const reporterSession = await createRunReporter(configPath);
|
|
240
|
+
const reporterSession = await createRunReporter(configPath, undefined, modeName);
|
|
212
241
|
const reporter = reporterSession.reporter;
|
|
213
242
|
const snapshotEnabled = runFlags.snapshot !== false;
|
|
214
243
|
reporter.onRunStart?.({
|
|
@@ -221,7 +250,7 @@ async function runTestSequential(runFlags, configPath, selectors) {
|
|
|
221
250
|
const results = [];
|
|
222
251
|
let failed = false;
|
|
223
252
|
for (const file of files) {
|
|
224
|
-
await build(configPath, [file]);
|
|
253
|
+
await build(configPath, [file], modeName);
|
|
225
254
|
const artifactKey = path.basename(file).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
226
255
|
const result = await run(runFlags, configPath, [file], false, {
|
|
227
256
|
reporter,
|
|
@@ -229,6 +258,7 @@ async function runTestSequential(runFlags, configPath, selectors) {
|
|
|
229
258
|
emitRunComplete: false,
|
|
230
259
|
logFileName: `test.${artifactKey}.log.json`,
|
|
231
260
|
coverageFileName: `coverage.${artifactKey}.log.json`,
|
|
261
|
+
modeName,
|
|
232
262
|
});
|
|
233
263
|
results.push(result);
|
|
234
264
|
if (result?.failed)
|
|
@@ -244,6 +274,34 @@ async function runTestSequential(runFlags, configPath, selectors) {
|
|
|
244
274
|
stats: summary.stats,
|
|
245
275
|
reports: summary.reports,
|
|
246
276
|
});
|
|
277
|
+
return failed;
|
|
278
|
+
}
|
|
279
|
+
async function runBuildModes(configPath, selectors, modes) {
|
|
280
|
+
const targets = modes.length ? modes : [undefined];
|
|
281
|
+
for (const modeName of targets) {
|
|
282
|
+
await build(configPath, selectors, modeName);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function runRuntimeModes(runFlags, configPath, selectors, modes) {
|
|
286
|
+
const targets = modes.length ? modes : [undefined];
|
|
287
|
+
let failed = false;
|
|
288
|
+
for (const modeName of targets) {
|
|
289
|
+
const result = await run(runFlags, configPath, selectors, false, {
|
|
290
|
+
modeName,
|
|
291
|
+
});
|
|
292
|
+
if (result.failed)
|
|
293
|
+
failed = true;
|
|
294
|
+
}
|
|
295
|
+
process.exit(failed ? 1 : 0);
|
|
296
|
+
}
|
|
297
|
+
async function runTestModes(runFlags, configPath, selectors, modes) {
|
|
298
|
+
const targets = modes.length ? modes : [undefined];
|
|
299
|
+
let failed = false;
|
|
300
|
+
for (const modeName of targets) {
|
|
301
|
+
const modeFailed = await runTestSequential(runFlags, configPath, selectors, modeName);
|
|
302
|
+
if (modeFailed)
|
|
303
|
+
failed = true;
|
|
304
|
+
}
|
|
247
305
|
process.exit(failed ? 1 : 0);
|
|
248
306
|
}
|
|
249
307
|
async function resolveSelectedFiles(configPath, selectors) {
|
|
@@ -252,7 +310,7 @@ async function resolveSelectedFiles(configPath, selectors) {
|
|
|
252
310
|
const patterns = resolveInputPatterns(config.input, selectors);
|
|
253
311
|
const matches = await glob(patterns);
|
|
254
312
|
const specs = matches.filter((file) => file.endsWith(".spec.ts"));
|
|
255
|
-
return [...new Set(specs)];
|
|
313
|
+
return [...new Set(specs)].sort((a, b) => a.localeCompare(b));
|
|
256
314
|
}
|
|
257
315
|
function resolveInputPatterns(configured, selectors) {
|
|
258
316
|
const configuredInputs = Array.isArray(configured) ? configured : [configured];
|
package/bin/init.js
CHANGED
|
@@ -23,7 +23,7 @@ export async function init(rawArgs) {
|
|
|
23
23
|
printPlan(root, target, example);
|
|
24
24
|
if (!options.yes) {
|
|
25
25
|
const cont = (await ask("Continue? [Y/n] ", rl)).toLowerCase().trim();
|
|
26
|
-
if (["
|
|
26
|
+
if (["n", "no"].includes(cont)) {
|
|
27
27
|
console.log("Exiting.");
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
@@ -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,6 +169,7 @@ 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";
|
|
@@ -179,7 +178,7 @@ function applyInit(root, target, example, force) {
|
|
|
179
178
|
config.runOptions.runtime.cmd = "node ./.as-test/runners/default.wasi.js <file>";
|
|
180
179
|
}
|
|
181
180
|
else {
|
|
182
|
-
config.runOptions.runtime.cmd = "node ./.as-test/runners/default.
|
|
181
|
+
config.runOptions.runtime.cmd = "node ./.as-test/runners/default.bindings.js <file>";
|
|
183
182
|
}
|
|
184
183
|
writeJson(configPath, config, summary, "as-test.config.json");
|
|
185
184
|
if (example != "none") {
|
|
@@ -187,13 +186,13 @@ function applyInit(root, target, example, force) {
|
|
|
187
186
|
const content = example == "minimal" ? buildMinimalExampleSpec() : buildFullExampleSpec();
|
|
188
187
|
writeManagedFile(examplePath, content, force, summary, "assembly/__tests__/example.spec.ts");
|
|
189
188
|
}
|
|
190
|
-
if (target == "wasi") {
|
|
189
|
+
if (target == "wasi" || target == "bindings") {
|
|
191
190
|
const runnerPath = path.join(root, ".as-test/runners/default.wasi.js");
|
|
192
191
|
writeManagedFile(runnerPath, buildWasiRunner(), force, summary, ".as-test/runners/default.wasi.js");
|
|
193
192
|
}
|
|
194
|
-
if (target == "bindings") {
|
|
195
|
-
const runnerPath = path.join(root, ".as-test/runners/default.
|
|
196
|
-
writeManagedFile(runnerPath, buildBindingsRunner(), force, summary, ".as-test/runners/default.
|
|
193
|
+
if (target == "wasi" || target == "bindings") {
|
|
194
|
+
const runnerPath = path.join(root, ".as-test/runners/default.bindings.js");
|
|
195
|
+
writeManagedFile(runnerPath, buildBindingsRunner(), force, summary, ".as-test/runners/default.bindings.js");
|
|
197
196
|
}
|
|
198
197
|
const pkgPath = path.join(root, "package.json");
|
|
199
198
|
const pkg = existsSync(pkgPath)
|
|
@@ -232,6 +231,31 @@ function ensureDir(root, rel, summary) {
|
|
|
232
231
|
mkdirSync(full, { recursive: true });
|
|
233
232
|
summary.created.push(rel + "/");
|
|
234
233
|
}
|
|
234
|
+
function ensureGitignoreIncludesAsTestDirs(root, summary) {
|
|
235
|
+
const rel = ".gitignore";
|
|
236
|
+
const fullPath = path.join(root, rel);
|
|
237
|
+
const entries = ["!.as-test/runners/", "!.as-test/snapshots/"];
|
|
238
|
+
const existed = existsSync(fullPath);
|
|
239
|
+
const source = existed ? readFileSync(fullPath, "utf8") : "";
|
|
240
|
+
const lines = source.split(/\r?\n/);
|
|
241
|
+
const missing = entries.filter((entry) => !lines.some((line) => line.trim() == entry));
|
|
242
|
+
if (!missing.length) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const eol = source.includes("\r\n") ? "\r\n" : "\n";
|
|
246
|
+
let output = source;
|
|
247
|
+
if (output.length &&
|
|
248
|
+
!output.endsWith("\n") &&
|
|
249
|
+
!output.endsWith("\r\n")) {
|
|
250
|
+
output += eol;
|
|
251
|
+
}
|
|
252
|
+
output += missing.join(eol) + eol;
|
|
253
|
+
writeFileSync(fullPath, output);
|
|
254
|
+
if (existed)
|
|
255
|
+
summary.updated.push(rel);
|
|
256
|
+
else
|
|
257
|
+
summary.created.push(rel);
|
|
258
|
+
}
|
|
235
259
|
function writeJson(fullPath, value, summary, displayPath) {
|
|
236
260
|
const rel = displayPath ??
|
|
237
261
|
path.relative(process.cwd(), fullPath) ??
|
|
@@ -449,7 +473,7 @@ function withNodeIo(imports = {}) {
|
|
|
449
473
|
|
|
450
474
|
const wasmPathArg = process.argv[2];
|
|
451
475
|
if (!wasmPathArg) {
|
|
452
|
-
process.stderr.write("usage: node ./.as-test/runners/default.
|
|
476
|
+
process.stderr.write("usage: node ./.as-test/runners/default.bindings.js <file.wasm>\\n");
|
|
453
477
|
process.exit(1);
|
|
454
478
|
}
|
|
455
479
|
|
|
@@ -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
|
+
}
|