nomen-lang 0.6.8 → 0.6.10
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/audit_runtime.c +188 -0
- package/core/System/StringBuilder.nm +10 -1
- package/core/System/Test.nm +18 -0
- package/dist/index.mjs +354 -191
- package/package.json +3 -2
- package/src/args.ts +7 -2
- package/src/index.ts +15 -4
- package/src/init.ts +10 -0
- package/src/test.ts +155 -55
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nomen-lang",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.10",
|
|
4
4
|
"description": "The CLI for the Nomen programming language.",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"license": "ISC",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"dist",
|
|
18
18
|
"src",
|
|
19
19
|
"core",
|
|
20
|
-
"NOMEN_AGENTS.md"
|
|
20
|
+
"NOMEN_AGENTS.md",
|
|
21
|
+
"audit_runtime.c"
|
|
21
22
|
],
|
|
22
23
|
"type": "module",
|
|
23
24
|
"devDependencies": {
|
package/src/args.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface Args {
|
|
|
13
13
|
audit_runtime?: string;
|
|
14
14
|
check: boolean;
|
|
15
15
|
release: boolean;
|
|
16
|
+
/** `nomen test` concurrency (max test files in flight); default = CPU count. */
|
|
17
|
+
jobs?: string;
|
|
16
18
|
/** Opt in to FP reassociation: float reductions vectorize under NEON
|
|
17
19
|
* (results may differ in the last ulp vs the scalar loop). */
|
|
18
20
|
fast_math: boolean;
|
|
@@ -39,6 +41,8 @@ const STRING_OPTIONS: Map<string, string> = new Map([
|
|
|
39
41
|
["lib", "lib"],
|
|
40
42
|
["l", "lib"],
|
|
41
43
|
["audit-runtime", "audit_runtime"],
|
|
44
|
+
["jobs", "jobs"],
|
|
45
|
+
["j", "jobs"],
|
|
42
46
|
]);
|
|
43
47
|
|
|
44
48
|
const BOOLEAN_OPTIONS: Map<string, string> = new Map([
|
|
@@ -174,8 +178,9 @@ export function print_help(): void {
|
|
|
174
178
|
" --platform, -p <name> Target platform: macos, ios, linux, android, windows, web",
|
|
175
179
|
" --lib, -l <path> Path to System library directory (containing package.jsonc)",
|
|
176
180
|
" --audit Audit the generated program for memory issues",
|
|
177
|
-
" --audit-runtime <path> Path to audit_runtime.c
|
|
178
|
-
" --
|
|
181
|
+
" --audit-runtime <path> Path to audit_runtime.c (default: auto-discovered — walk-up or the CLI-bundled copy)",
|
|
182
|
+
" --jobs, -j <n> For `nomen test`: max test files run in parallel (default: CPU count)",
|
|
183
|
+
" --release, -r Build with optimizations (clang -O2; aarch64 asm cleanup passes)",
|
|
179
184
|
" --fast-math Allow FP reassociation (float loop reductions vectorize on aarch64)",
|
|
180
185
|
" --check For `nomen format`: report files that would change",
|
|
181
186
|
" -h, --help Show this help",
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import parse from "../../src/parse.ts";
|
|
|
13
13
|
import { parse_args, print_help, type Args } from "./args.ts";
|
|
14
14
|
import { run_docs } from "./docs.ts";
|
|
15
15
|
import render_errors, { render_warnings } from "./format_errors.ts";
|
|
16
|
-
import { find_bundled, run_init } from "./init.ts";
|
|
16
|
+
import { find_bundled, find_bundled_audit_runtime, run_init } from "./init.ts";
|
|
17
17
|
import { build_dir_for, outfile_for } from "./paths.ts";
|
|
18
18
|
import { runTests } from "./test.ts";
|
|
19
19
|
import type Config from "./types/Config.ts";
|
|
@@ -104,12 +104,13 @@ try {
|
|
|
104
104
|
const root = args.in ?? process.cwd();
|
|
105
105
|
const filter = args.filter ? new RegExp(args.filter) : undefined;
|
|
106
106
|
const arch = args.arch ?? "aarch64";
|
|
107
|
-
const ok = runTests(root, {
|
|
107
|
+
const ok = await runTests(root, {
|
|
108
108
|
arch,
|
|
109
109
|
filter,
|
|
110
110
|
audit: args.audit,
|
|
111
111
|
audit_runtime: args.audit_runtime,
|
|
112
112
|
release: args.release,
|
|
113
|
+
jobs: args.jobs !== undefined ? parseInt(args.jobs, 10) : undefined,
|
|
113
114
|
});
|
|
114
115
|
process.exit(ok ? 0 : 1);
|
|
115
116
|
}
|
|
@@ -283,7 +284,10 @@ function resolve_audit_runtime(config: Config, input_path: string): string | und
|
|
|
283
284
|
if (parent === dir) break;
|
|
284
285
|
dir = parent;
|
|
285
286
|
}
|
|
286
|
-
|
|
287
|
+
// Neither the config nor the walk-up found it: fall back to the runtime
|
|
288
|
+
// bundled with the CLI (live repo copy in the dev tree, shipped copy when
|
|
289
|
+
// published) so `--audit` works outside the nomen checkout with no flag.
|
|
290
|
+
return find_bundled_audit_runtime();
|
|
287
291
|
}
|
|
288
292
|
|
|
289
293
|
function compile_audit_runtime(config: Config, input_path: string, buildDir: string): string {
|
|
@@ -405,7 +409,14 @@ function processFile(filename: string, config: Config, mode: Mode, program_args:
|
|
|
405
409
|
// `build` links the executable but does not run it; `run` links and runs.
|
|
406
410
|
startTime = performance.now();
|
|
407
411
|
|
|
408
|
-
|
|
412
|
+
// The audit runtime is linked under --audit, and also whenever the
|
|
413
|
+
// program references it: `Tester.audit_count` (System::Test) has a raw
|
|
414
|
+
// body calling `nomen_audit_count`, so a Tester-using program run without
|
|
415
|
+
// --audit still needs the (inert) counter object at link time.
|
|
416
|
+
const needs_audit_runtime = config.audit || result.code.includes("nomen_audit_count");
|
|
417
|
+
const audit_obj = needs_audit_runtime
|
|
418
|
+
? compile_audit_runtime(config, resolved, buildDir)
|
|
419
|
+
: undefined;
|
|
409
420
|
let link_inputs = codefile;
|
|
410
421
|
if (companionfile) link_inputs += ` ${companionfile}`;
|
|
411
422
|
if (audit_obj) link_inputs += ` ${audit_obj}`;
|
package/src/init.ts
CHANGED
|
@@ -85,6 +85,16 @@ export function find_bundled(filename: string): string | undefined {
|
|
|
85
85
|
return undefined;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// The audit runtime for `--audit` builds: the dev tree's LIVE copy
|
|
89
|
+
// (repo_root/src/audit_runtime.c — a stale bundled snapshot must not shadow
|
|
90
|
+
// it, same posture as the cli/core rule above), else the copy shipped at the
|
|
91
|
+
// package root by `scripts/bundle-assets.mjs` in a published install. Lets
|
|
92
|
+
// `nomen test/run --audit` work outside the nomen checkout with no
|
|
93
|
+
// --audit-runtime flag.
|
|
94
|
+
export function find_bundled_audit_runtime(): string | undefined {
|
|
95
|
+
return find_bundled("src/audit_runtime.c") ?? find_bundled("audit_runtime.c");
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
/**
|
|
89
99
|
* `nomen init <name>`: scaffold a new project under `./<name>` with a sensible
|
|
90
100
|
* starting layout (package.jsonc, src/main.nm, a starter test, README and an
|
package/src/test.ts
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { promisify } from "node:util";
|
|
5
7
|
|
|
6
8
|
import build, { default_platform } from "../../src/build.ts";
|
|
7
9
|
import join from "../../src/join.ts";
|
|
8
10
|
import { get_library } from "../../src/lib.ts";
|
|
9
11
|
import parse from "../../src/parse.ts";
|
|
10
|
-
import { find_bundled } from "./init.ts";
|
|
12
|
+
import { find_bundled, find_bundled_audit_runtime } from "./init.ts";
|
|
11
13
|
import { build_dir_for } from "./paths.ts";
|
|
12
14
|
|
|
13
15
|
const RECORD_PREFIX = "\\nomen|";
|
|
14
16
|
|
|
17
|
+
// Promise-returning `execFile`: `nomen test` builds and runs each `*.test.nm`
|
|
18
|
+
// file in its own build folder and its own process, so the (latency-bound)
|
|
19
|
+
// clang/link/run subprocesses overlap across files instead of serializing.
|
|
20
|
+
// A rejected error carries the same `.stdout` / `.stderr` / `.signal` /
|
|
21
|
+
// `.code` fields the previous synchronous `execFileSync` catch sites read.
|
|
22
|
+
const execFile_async = promisify(execFile);
|
|
23
|
+
|
|
15
24
|
// A discovered test function: `pub func <name> = (ref Tester t)`.
|
|
16
25
|
export interface TestFunction {
|
|
17
26
|
name: string;
|
|
@@ -40,6 +49,10 @@ export interface TestFileResult {
|
|
|
40
49
|
// "LEAK: N allocation(s)" lines the audits runtime printed at exit. Empty
|
|
41
50
|
// unless the file was run with --audit; a non-empty list fails the file.
|
|
42
51
|
leaks: string[];
|
|
52
|
+
// Per-test leak attribution (from the harness's `\nomen|leaks` records):
|
|
53
|
+
// the tests whose allocation balance grew while they ran. Collected in
|
|
54
|
+
// every run; only --audit fails the file on them.
|
|
55
|
+
leaking_tests: LeakRecord[];
|
|
43
56
|
// Set when the compiled binary crashed before finishing (e.g. a segfault
|
|
44
57
|
// in a test). We still surface whatever records it managed to emit.
|
|
45
58
|
crashed?: string;
|
|
@@ -74,6 +87,13 @@ interface BenchRecord {
|
|
|
74
87
|
stddev: number;
|
|
75
88
|
}
|
|
76
89
|
|
|
90
|
+
// A `\nomen|leaks|<test>|<n>` record: the test's allocation balance grew by
|
|
91
|
+
// `n` between the harness's counter snapshot and the test's end.
|
|
92
|
+
interface LeakRecord {
|
|
93
|
+
test: string;
|
|
94
|
+
count: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
/** Recursively collect `*.test.nm` files under `root`, skipping build output. */
|
|
78
98
|
export function collect_test_files(root: string): string[] {
|
|
79
99
|
const out: string[] = [];
|
|
@@ -192,11 +212,25 @@ export function generate_harness(tests: TestFunction[], benches: BenchFunction[]
|
|
|
192
212
|
})
|
|
193
213
|
.join("\n");
|
|
194
214
|
|
|
195
|
-
let main = "\nimport System\nimport System::Test\n
|
|
215
|
+
let main = "\nimport System\nimport System::Test\n";
|
|
216
|
+
// Per-test leak attribution: the harness snapshots the audit counter
|
|
217
|
+
// (Tester.audit_count — the audit runtime is linked into every test
|
|
218
|
+
// build, see run_test_file) before every test and diffs it after. A
|
|
219
|
+
// positive delta emits a `\nomen|leaks|<test>|<n>` record, which the CLI
|
|
220
|
+
// reports as a leaking test instead of leaving the file's exit-time
|
|
221
|
+
// total unattributed.
|
|
222
|
+
main += "\npub func main = () {\n";
|
|
196
223
|
main += "\tvar Tester t = Tester()\n";
|
|
224
|
+
main += "\tvar int64 audit_mark = 0\n";
|
|
225
|
+
main += "\tvar int64 audit_leaked = 0\n";
|
|
197
226
|
for (const test of tests) {
|
|
227
|
+
main += `\taudit_mark = Tester.audit_count()\n`;
|
|
198
228
|
main += `\tt.begin_test("${escape_nm_string(test.name)}")\n`;
|
|
199
229
|
main += `\t${test.name}(ref t)\n`;
|
|
230
|
+
main += `\taudit_leaked = Tester.audit_count() - audit_mark\n`;
|
|
231
|
+
main += `\tif audit_leaked > 0 {\n`;
|
|
232
|
+
main += `\t\tConsole.write_line("\\\\nomen|leaks|" + "${escape_nm_string(test.name)}" + "|\\{audit_leaked}")\n`;
|
|
233
|
+
main += `\t}\n`;
|
|
200
234
|
main += `\tt.end_test()\n`;
|
|
201
235
|
}
|
|
202
236
|
for (const bench of benches) {
|
|
@@ -218,6 +252,7 @@ interface RunRecord {
|
|
|
218
252
|
tests: TestRecord[];
|
|
219
253
|
fails: FailRecord[];
|
|
220
254
|
benches: BenchRecord[];
|
|
255
|
+
leaks: LeakRecord[];
|
|
221
256
|
other: string[];
|
|
222
257
|
}
|
|
223
258
|
|
|
@@ -226,6 +261,7 @@ export function parse_records(stdout: string): RunRecord {
|
|
|
226
261
|
const tests: TestRecord[] = [];
|
|
227
262
|
const fails: FailRecord[] = [];
|
|
228
263
|
const benches: BenchRecord[] = [];
|
|
264
|
+
const leaks: LeakRecord[] = [];
|
|
229
265
|
const other: string[] = [];
|
|
230
266
|
for (const line of stdout.split("\n")) {
|
|
231
267
|
if (!line.startsWith(RECORD_PREFIX)) {
|
|
@@ -245,6 +281,8 @@ export function parse_records(stdout: string): RunRecord {
|
|
|
245
281
|
});
|
|
246
282
|
} else if (kind === "fail") {
|
|
247
283
|
fails.push({ test: parts[1], message: parts.slice(2).join("|") });
|
|
284
|
+
} else if (kind === "leaks") {
|
|
285
|
+
leaks.push({ test: parts[1], count: parseInt(parts[2] || "0", 10) });
|
|
248
286
|
} else if (kind === "bench") {
|
|
249
287
|
benches.push({
|
|
250
288
|
label: parts[1],
|
|
@@ -257,22 +295,28 @@ export function parse_records(stdout: string): RunRecord {
|
|
|
257
295
|
});
|
|
258
296
|
}
|
|
259
297
|
}
|
|
260
|
-
return { tests, fails, benches, other };
|
|
298
|
+
return { tests, fails, benches, leaks, other };
|
|
261
299
|
}
|
|
262
300
|
|
|
263
|
-
/**
|
|
301
|
+
/**
|
|
302
|
+
* Pull the exit-time audit lines out of stdout: "LEAK: N allocation(s)" (a
|
|
303
|
+
* nonzero malloc/free balance) and "AUDIT-STALE-FREE: N pointer(s) freed that
|
|
304
|
+
* were not live audit allocations" (a free that matched no live block — a
|
|
305
|
+
* foreign pointer or a double free; the runtime no longer passes those to
|
|
306
|
+
* libc free, so this is the only record they leave). Both fail the file.
|
|
307
|
+
*/
|
|
264
308
|
export function extract_leaks(stdout: string): string[] {
|
|
265
309
|
return stdout
|
|
266
310
|
.split("\n")
|
|
267
311
|
.map((l) => l.trim())
|
|
268
|
-
.filter((l) => l.startsWith("LEAK:"));
|
|
312
|
+
.filter((l) => l.startsWith("LEAK:") || l.startsWith("AUDIT-STALE-FREE:"));
|
|
269
313
|
}
|
|
270
314
|
|
|
271
315
|
/**
|
|
272
316
|
* Compile one `*.test.nm` file (with the generated harness) and run it,
|
|
273
317
|
* returning its parsed records and any crash info.
|
|
274
318
|
*/
|
|
275
|
-
export function run_test_file(
|
|
319
|
+
export async function run_test_file(
|
|
276
320
|
entry_path: string,
|
|
277
321
|
lib_path: string | undefined,
|
|
278
322
|
arch: string,
|
|
@@ -280,7 +324,7 @@ export function run_test_file(
|
|
|
280
324
|
audit_runtime?: string,
|
|
281
325
|
release = false,
|
|
282
326
|
cwd?: string,
|
|
283
|
-
): TestFileResult {
|
|
327
|
+
): Promise<TestFileResult> {
|
|
284
328
|
const start = performance.now();
|
|
285
329
|
const source_text = fs.readFileSync(entry_path, "utf8");
|
|
286
330
|
const tests = extract_test_functions(source_text);
|
|
@@ -293,8 +337,9 @@ export function run_test_file(
|
|
|
293
337
|
tests: [],
|
|
294
338
|
fails: [],
|
|
295
339
|
benches: [],
|
|
296
|
-
other: [],
|
|
297
340
|
leaks: [],
|
|
341
|
+
leaking_tests: [],
|
|
342
|
+
other: [],
|
|
298
343
|
ms: 0,
|
|
299
344
|
};
|
|
300
345
|
|
|
@@ -334,25 +379,27 @@ export function run_test_file(
|
|
|
334
379
|
|
|
335
380
|
const buildDir = build_dir_for(resolved, true);
|
|
336
381
|
if (!fs.existsSync(buildDir)) fs.mkdirSync(buildDir, { recursive: true });
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
382
|
+
// The audit runtime is linked into EVERY test build: the generated
|
|
383
|
+
// harness polls nomen_audit_count() around every test for per-test leak
|
|
384
|
+
// attribution, and under --audit the generated main additionally calls
|
|
385
|
+
// nomen_audit_check() at exit, printing "LEAK: N allocation(s)" when the
|
|
386
|
+
// balance is nonzero.
|
|
387
|
+
// Runtime discovery: explicit flag wins; else walk up from the test file
|
|
388
|
+
// (a nomen checkout's src/audit_runtime.c); else the runtime bundled with
|
|
389
|
+
// the CLI (live repo copy in the dev tree, shipped copy when published)
|
|
390
|
+
// so it works outside the nomen checkout with no flag.
|
|
391
|
+
const runtime_src = audit_runtime
|
|
392
|
+
? path.resolve(audit_runtime)
|
|
393
|
+
: (find_audit_runtime(path.dirname(resolved)) ?? find_bundled_audit_runtime());
|
|
394
|
+
if (!runtime_src || !fs.existsSync(runtime_src)) {
|
|
395
|
+
result.ok = false;
|
|
396
|
+
result.phase = "setup";
|
|
397
|
+
result.crashed = "audit_runtime.c was not found. Pass --audit-runtime <path>.";
|
|
398
|
+
result.ms = performance.now() - start;
|
|
399
|
+
return result;
|
|
355
400
|
}
|
|
401
|
+
const audit_obj = path.join(buildDir, "audit_runtime.o");
|
|
402
|
+
await execFile_async("clang", ["-c", runtime_src, "-o", audit_obj]);
|
|
356
403
|
const platform = default_platform();
|
|
357
404
|
// Match the run/build command's emission: ObjC-bearing sources (GUI code
|
|
358
405
|
// pulled in through the library graph) must land in a `.m` TU on Apple
|
|
@@ -401,10 +448,7 @@ export function run_test_file(
|
|
|
401
448
|
);
|
|
402
449
|
}
|
|
403
450
|
try {
|
|
404
|
-
|
|
405
|
-
encoding: "utf8",
|
|
406
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
407
|
-
});
|
|
451
|
+
await execFile_async("clang", link_args, { encoding: "utf8" });
|
|
408
452
|
} catch (err: any) {
|
|
409
453
|
const stderr = err.stderr ? err.stderr.toString() : (err.message ?? "");
|
|
410
454
|
result.ok = false;
|
|
@@ -424,13 +468,13 @@ export function run_test_file(
|
|
|
424
468
|
let runStdout = "";
|
|
425
469
|
let crashed: string | undefined;
|
|
426
470
|
try {
|
|
427
|
-
|
|
471
|
+
const run = await execFile_async(outfile, [], {
|
|
428
472
|
encoding: "utf8",
|
|
429
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
430
473
|
timeout: 30_000,
|
|
431
474
|
maxBuffer: 16 * 1024 * 1024,
|
|
432
475
|
cwd: cwd ?? process.cwd(),
|
|
433
476
|
});
|
|
477
|
+
runStdout = run.stdout;
|
|
434
478
|
} catch (err: any) {
|
|
435
479
|
runStdout = err.stdout ? err.stdout.toString() : "";
|
|
436
480
|
if (err.signal === "SIGTERM") {
|
|
@@ -444,15 +488,22 @@ export function run_test_file(
|
|
|
444
488
|
result.tests = records.tests;
|
|
445
489
|
result.fails = records.fails;
|
|
446
490
|
result.benches = records.benches;
|
|
491
|
+
result.leaking_tests = records.leaks;
|
|
447
492
|
result.other = records.other;
|
|
448
493
|
// Under --audit the exit-time audit check prints "LEAK: N allocation(s)"
|
|
449
|
-
// to stdout when the malloc/free balance is nonzero
|
|
450
|
-
//
|
|
494
|
+
// to stdout when the malloc/free balance is nonzero, and
|
|
495
|
+
// "AUDIT-STALE-FREE: N ..." for frees that matched no live block. Treat
|
|
496
|
+
// any such line as a failure so a leaked (or double-freeing) test file
|
|
497
|
+
// cannot stay green. (The per-test `leaks` records are collected always —
|
|
498
|
+
// they name the leaking tests — but only --audit makes them fail the
|
|
499
|
+
// file.)
|
|
451
500
|
result.leaks = audit ? extract_leaks(runStdout) : [];
|
|
452
|
-
//
|
|
501
|
+
// Audit lines are surfaced explicitly above; drop them from the generic
|
|
453
502
|
// stdout dump so they are not printed twice.
|
|
454
503
|
if (result.leaks.length) {
|
|
455
|
-
result.other = result.other.filter(
|
|
504
|
+
result.other = result.other.filter(
|
|
505
|
+
(l) => !l.trim().startsWith("LEAK:") && !l.trim().startsWith("AUDIT-STALE-FREE:"),
|
|
506
|
+
);
|
|
456
507
|
}
|
|
457
508
|
result.crashed = crashed;
|
|
458
509
|
if (crashed) result.phase = "run";
|
|
@@ -525,6 +576,20 @@ export function report_file(result: TestFileResult): void {
|
|
|
525
576
|
for (const leak of result.leaks) {
|
|
526
577
|
console.log(` ${C.yellow("⚠")} ${C.yellow(leak)}`);
|
|
527
578
|
}
|
|
579
|
+
// Per-test attribution: which tests leaked (the exit-time LEAK line above
|
|
580
|
+
// is the file total; these name the culprits). Shown under --audit only —
|
|
581
|
+
// without it the file cannot fail on leaks, so the warning would be noise
|
|
582
|
+
// on every run.
|
|
583
|
+
if (result.leaks.length) {
|
|
584
|
+
for (const lt of result.leaking_tests) {
|
|
585
|
+
console.log(` ${C.yellow("⚠")} ${lt.test} ${C.dim(`leaked ${lt.count} allocation(s)`)}`);
|
|
586
|
+
}
|
|
587
|
+
if (!result.leaking_tests.length) {
|
|
588
|
+
console.log(
|
|
589
|
+
` ${C.yellow("⚠")} ${C.yellow("LEAK: no per-test attribution — leaked outside a test (harness/setup/teardown) or crashed mid-test")}`,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
528
593
|
for (const b of result.benches) {
|
|
529
594
|
console.log(
|
|
530
595
|
` ${C.cyan("⏱")} ${b.label} ${C.dim(`(n=${b.n})`)} ` +
|
|
@@ -545,12 +610,27 @@ export interface RunTestsOptions {
|
|
|
545
610
|
audit?: boolean;
|
|
546
611
|
audit_runtime?: string;
|
|
547
612
|
release?: boolean;
|
|
613
|
+
/**
|
|
614
|
+
* Maximum test files built and run concurrently. Each file compiles into
|
|
615
|
+
* its own build folder and runs in its own process, so the latency-bound
|
|
616
|
+
* clang/link/run steps overlap across files. Defaults to the machine's
|
|
617
|
+
* available parallelism; 1 forces the historical sequential order.
|
|
618
|
+
*/
|
|
619
|
+
jobs?: number;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** The default concurrency for `runTests` when no `jobs` is given. */
|
|
623
|
+
export function default_jobs(): number {
|
|
624
|
+
const n =
|
|
625
|
+
typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length;
|
|
626
|
+
return Math.max(1, n);
|
|
548
627
|
}
|
|
549
628
|
|
|
550
629
|
/**
|
|
551
630
|
* Locate audit_runtime.c for an audited test build: an explicit path wins,
|
|
552
631
|
* otherwise walk up from `dir` looking for a `src/audit_runtime.c` (the same
|
|
553
|
-
* convention the run/build commands use).
|
|
632
|
+
* convention the run/build commands use). When both miss, the caller falls
|
|
633
|
+
* back to the CLI-bundled runtime (`find_bundled_audit_runtime`).
|
|
554
634
|
*/
|
|
555
635
|
function find_audit_runtime(dir: string): string | undefined {
|
|
556
636
|
let cur = dir;
|
|
@@ -565,7 +645,7 @@ function find_audit_runtime(dir: string): string | undefined {
|
|
|
565
645
|
}
|
|
566
646
|
|
|
567
647
|
/** Discover, run, and report every `*.test.nm` under `root`. */
|
|
568
|
-
export function runTests(root: string, options: RunTestsOptions = {}): boolean {
|
|
648
|
+
export async function runTests(root: string, options: RunTestsOptions = {}): Promise<boolean> {
|
|
569
649
|
const arch = options.arch ?? "aarch64";
|
|
570
650
|
const lib = resolve_lib_for(root);
|
|
571
651
|
const files = collect_test_files(root).filter((f) => !options.filter || options.filter.test(f));
|
|
@@ -577,20 +657,33 @@ export function runTests(root: string, options: RunTestsOptions = {}): boolean {
|
|
|
577
657
|
// fixture paths resolve there no matter where the CLI was invoked from.
|
|
578
658
|
const binary_cwd = path.resolve(root);
|
|
579
659
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
660
|
+
// Bounded-concurrency pool: at most `jobs` files are in flight. The
|
|
661
|
+
// parse/build halves are synchronous (no internal awaits), so they run
|
|
662
|
+
// atomically on the main thread between the subprocess awaits — the
|
|
663
|
+
// compiler's in-process state never interleaves. Results are collected by
|
|
664
|
+
// file INDEX and reported after the pool drains, so the report stays in
|
|
665
|
+
// discovery order regardless of completion order (and stdout isn't
|
|
666
|
+
// interleaved).
|
|
667
|
+
const jobs = Math.max(1, Math.min(options.jobs ?? default_jobs(), files.length));
|
|
668
|
+
const results: TestFileResult[] = new Array(files.length);
|
|
669
|
+
let next = 0;
|
|
670
|
+
const worker = async (): Promise<void> => {
|
|
671
|
+
for (;;) {
|
|
672
|
+
const i = next++;
|
|
673
|
+
if (i >= files.length) return;
|
|
674
|
+
results[i] = await run_test_file(
|
|
675
|
+
files[i],
|
|
676
|
+
lib,
|
|
677
|
+
arch,
|
|
678
|
+
options.audit,
|
|
679
|
+
options.audit_runtime,
|
|
680
|
+
options.release,
|
|
681
|
+
binary_cwd,
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
await Promise.all(Array.from({ length: jobs }, worker));
|
|
686
|
+
for (const result of results) report_file(result);
|
|
594
687
|
|
|
595
688
|
const elapsed = performance.now() - startTime;
|
|
596
689
|
const totalFiles = results.length;
|
|
@@ -603,6 +696,12 @@ export function runTests(root: string, options: RunTestsOptions = {}): boolean {
|
|
|
603
696
|
const totalFailed = results.reduce((a, r) => a + r.tests.reduce((x, t) => x + t.failed, 0), 0);
|
|
604
697
|
const totalPassed = totalTests - totalFailed;
|
|
605
698
|
const totalLeaks = results.reduce((a, r) => a + r.leaks.length, 0);
|
|
699
|
+
// Leak attribution: the count of TESTS that leaked (the old summary
|
|
700
|
+
// counted FILES — one aggregate LEAK line per file told you nothing about
|
|
701
|
+
// which test to blame). The per-file report lists the leaking tests; the
|
|
702
|
+
// files' exit-time totals can exceed the per-test sum when allocations
|
|
703
|
+
// leak outside any test (harness/setup/teardown).
|
|
704
|
+
const totalLeakingTests = results.reduce((a, r) => a + r.leaking_tests.length, 0);
|
|
606
705
|
const anyFailed = results.some((r) => !r.ok);
|
|
607
706
|
|
|
608
707
|
console.log("");
|
|
@@ -621,8 +720,9 @@ export function runTests(root: string, options: RunTestsOptions = {}): boolean {
|
|
|
621
720
|
);
|
|
622
721
|
if (totalLeaks) {
|
|
623
722
|
console.log(
|
|
624
|
-
` ${C.dim("Leaks ")} ${C.yellow(`${
|
|
625
|
-
C.dim(
|
|
723
|
+
` ${C.dim("Leaks ")} ${C.yellow(`${totalLeakingTests} leaking test(s)`)}` +
|
|
724
|
+
(totalLeakingTests !== totalLeaks ? C.dim(` (${totalLeaks} leaked file(s))`) : "") +
|
|
725
|
+
C.dim(" (--audit; leaking tests fail their file)"),
|
|
626
726
|
);
|
|
627
727
|
}
|
|
628
728
|
console.log(` ${C.dim(" Time ")} ${format_duration(elapsed)}`);
|