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/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 config = loadConfig(configPath, false);
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}/${file.slice(file.lastIndexOf("/") + 1).replace(".ts", ".wasm")}`;
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) ? configured : [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
- build(configPath).catch((error) => {
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
- run(runFlags, configPath);
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
- runTestSequential(runFlags, configPath, commandArgs).catch((error) => {
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 (["y", "yes"].includes(cont)) {
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.run.js <file>";
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.run.js");
196
- writeManagedFile(runnerPath, buildBindingsRunner(), force, summary, ".as-test/runners/default.run.js");
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.run.js <file.wasm>\\n");
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
+ }