@c9up/helix 0.1.7 → 0.1.9
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/README.md +282 -11
- package/bin/helix.js +329 -13
- package/dist/cli/failed-cache.d.ts +27 -0
- package/dist/cli/failed-cache.d.ts.map +1 -0
- package/dist/cli/failed-cache.js +64 -0
- package/dist/cli/failed-cache.js.map +1 -0
- package/dist/cli/glob.d.ts +36 -0
- package/dist/cli/glob.d.ts.map +1 -0
- package/dist/cli/glob.js +185 -0
- package/dist/cli/glob.js.map +1 -0
- package/dist/cli/native.d.ts +4 -0
- package/dist/cli/native.d.ts.map +1 -1
- package/dist/cli/native.js.map +1 -1
- package/dist/cli/pool.d.ts +11 -0
- package/dist/cli/pool.d.ts.map +1 -1
- package/dist/cli/pool.js +12 -0
- package/dist/cli/pool.js.map +1 -1
- package/dist/cli/reporter.d.ts +11 -0
- package/dist/cli/reporter.d.ts.map +1 -1
- package/dist/cli/reporter.js +53 -0
- package/dist/cli/reporter.js.map +1 -1
- package/dist/cli/run.d.ts +41 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +181 -21
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/runner.d.ts +19 -0
- package/dist/cli/runner.d.ts.map +1 -0
- package/dist/cli/runner.js +15 -0
- package/dist/cli/runner.js.map +1 -0
- package/dist/cli/suites.d.ts +102 -0
- package/dist/cli/suites.d.ts.map +1 -0
- package/dist/cli/suites.js +226 -0
- package/dist/cli/suites.js.map +1 -0
- package/dist/index.d.ts +10 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -8
- package/dist/index.js.map +1 -1
- package/dist/japa/core.d.ts +106 -0
- package/dist/japa/core.d.ts.map +1 -0
- package/dist/japa/core.js +128 -0
- package/dist/japa/core.js.map +1 -0
- package/dist/runtime/assert.d.ts +174 -0
- package/dist/runtime/assert.d.ts.map +1 -0
- package/dist/runtime/assert.js +591 -0
- package/dist/runtime/assert.js.map +1 -0
- package/dist/runtime/asymmetric.d.ts +34 -0
- package/dist/runtime/asymmetric.d.ts.map +1 -0
- package/dist/runtime/asymmetric.js +78 -0
- package/dist/runtime/asymmetric.js.map +1 -0
- package/dist/runtime/bootstrap.d.ts +47 -0
- package/dist/runtime/bootstrap.d.ts.map +1 -0
- package/dist/runtime/bootstrap.js +166 -0
- package/dist/runtime/bootstrap.js.map +1 -0
- package/dist/runtime/cli-args.d.ts +77 -0
- package/dist/runtime/cli-args.d.ts.map +1 -0
- package/dist/runtime/cli-args.js +85 -0
- package/dist/runtime/cli-args.js.map +1 -0
- package/dist/runtime/configure.d.ts +214 -0
- package/dist/runtime/configure.d.ts.map +1 -0
- package/dist/runtime/configure.js +164 -0
- package/dist/runtime/configure.js.map +1 -0
- package/dist/runtime/context.d.ts +84 -0
- package/dist/runtime/context.d.ts.map +1 -0
- package/dist/runtime/context.js +146 -0
- package/dist/runtime/context.js.map +1 -0
- package/dist/runtime/emitter.d.ts +141 -0
- package/dist/runtime/emitter.d.ts.map +1 -0
- package/dist/runtime/emitter.js +79 -0
- package/dist/runtime/emitter.js.map +1 -0
- package/dist/runtime/equals.d.ts.map +1 -1
- package/dist/runtime/equals.js +21 -0
- package/dist/runtime/equals.js.map +1 -1
- package/dist/runtime/expect.d.ts +22 -1
- package/dist/runtime/expect.d.ts.map +1 -1
- package/dist/runtime/expect.js +15 -1
- package/dist/runtime/expect.js.map +1 -1
- package/dist/runtime/global-hooks.d.ts +31 -0
- package/dist/runtime/global-hooks.d.ts.map +1 -0
- package/dist/runtime/global-hooks.js +126 -0
- package/dist/runtime/global-hooks.js.map +1 -0
- package/dist/runtime/index.d.ts +16 -3
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +7 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/matchers.d.ts +5 -0
- package/dist/runtime/matchers.d.ts.map +1 -1
- package/dist/runtime/matchers.js +117 -0
- package/dist/runtime/matchers.js.map +1 -1
- package/dist/runtime/run.d.ts +63 -1
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +696 -79
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/runner.d.ts +97 -0
- package/dist/runtime/runner.d.ts.map +1 -0
- package/dist/runtime/runner.js +160 -0
- package/dist/runtime/runner.js.map +1 -0
- package/dist/runtime/suite-config.d.ts +23 -0
- package/dist/runtime/suite-config.d.ts.map +1 -0
- package/dist/runtime/suite-config.js +52 -0
- package/dist/runtime/suite-config.js.map +1 -0
- package/dist/runtime/suite-taps.d.ts +122 -0
- package/dist/runtime/suite-taps.d.ts.map +1 -0
- package/dist/runtime/suite-taps.js +218 -0
- package/dist/runtime/suite-taps.js.map +1 -0
- package/dist/runtime/suite.d.ts +316 -10
- package/dist/runtime/suite.d.ts.map +1 -1
- package/dist/runtime/suite.js +307 -8
- package/dist/runtime/suite.js.map +1 -1
- package/dist/runtime/test-context.d.ts +88 -1
- package/dist/runtime/test-context.d.ts.map +1 -1
- package/dist/runtime/test-context.js +170 -2
- package/dist/runtime/test-context.js.map +1 -1
- package/dist/runtime/vi/fake-timers.d.ts.map +1 -1
- package/dist/runtime/vi/fake-timers.js +3 -4
- package/dist/runtime/vi/fake-timers.js.map +1 -1
- package/dist/runtime/worker.d.ts.map +1 -1
- package/dist/runtime/worker.js +87 -5
- package/dist/runtime/worker.js.map +1 -1
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +10 -1
package/bin/helix.js
CHANGED
|
@@ -38,7 +38,62 @@ const FLAG_SPEC = {
|
|
|
38
38
|
tsx: { kind: "boolean", help: "Use tsx loader for workers (default: true)" },
|
|
39
39
|
include: { kind: "string[]", help: "Glob patterns to include" },
|
|
40
40
|
exclude: { kind: "string[]", help: "Glob patterns to exclude" },
|
|
41
|
-
grep: {
|
|
41
|
+
grep: {
|
|
42
|
+
kind: "string",
|
|
43
|
+
help: "Only run tests whose full name matches (regex or substring)",
|
|
44
|
+
},
|
|
45
|
+
retries: { kind: "number", help: "Extra attempts on failure (default 0)" },
|
|
46
|
+
tags: {
|
|
47
|
+
kind: "string",
|
|
48
|
+
help: "Comma-separated tag filter (@fast, ~@slow to exclude); OR by default",
|
|
49
|
+
},
|
|
50
|
+
"match-all": {
|
|
51
|
+
kind: "boolean",
|
|
52
|
+
help: "Require ALL --tags instead of any (Japa parity)",
|
|
53
|
+
},
|
|
54
|
+
// Japa's parser accepts both spellings (`matchAll` with `match-all` as its
|
|
55
|
+
// alias), so both work here too.
|
|
56
|
+
matchAll: {
|
|
57
|
+
kind: "boolean",
|
|
58
|
+
help: "Alias of --match-all",
|
|
59
|
+
},
|
|
60
|
+
tests: {
|
|
61
|
+
kind: "string",
|
|
62
|
+
help: "Comma-separated exact test titles to run (Japa --tests)",
|
|
63
|
+
},
|
|
64
|
+
groups: {
|
|
65
|
+
kind: "string",
|
|
66
|
+
help: "Comma-separated exact group titles to run (Japa --groups)",
|
|
67
|
+
},
|
|
68
|
+
suite: {
|
|
69
|
+
kind: "string",
|
|
70
|
+
help: "Name of the suite these files belong to (Japa meta.suite)",
|
|
71
|
+
},
|
|
72
|
+
files: {
|
|
73
|
+
kind: "string",
|
|
74
|
+
help: "Comma-separated substrings matched against test file paths (Japa --files)",
|
|
75
|
+
},
|
|
76
|
+
reporters: {
|
|
77
|
+
kind: "string",
|
|
78
|
+
help: "Comma-separated reporters to activate, e.g. spec,json (Japa --reporters)",
|
|
79
|
+
},
|
|
80
|
+
bail: { kind: "boolean", help: "Stop at the first failure (Japa --bail)" },
|
|
81
|
+
"bail-layer": {
|
|
82
|
+
kind: "string",
|
|
83
|
+
help: "How far a bail reaches: group|suite|runner (Japa --bail-layer)",
|
|
84
|
+
},
|
|
85
|
+
failed: {
|
|
86
|
+
kind: "boolean",
|
|
87
|
+
help: "Re-run only the tests that failed last run (Japa --failed)",
|
|
88
|
+
},
|
|
89
|
+
"list-pinned": {
|
|
90
|
+
kind: "boolean",
|
|
91
|
+
help: "Print the tests marked .pin() and run nothing (Japa --list-pinned)",
|
|
92
|
+
},
|
|
93
|
+
"force-exit": {
|
|
94
|
+
kind: "boolean",
|
|
95
|
+
help: "process.exit() as soon as the run ends, without draining the event loop (Japa --force-exit)",
|
|
96
|
+
},
|
|
42
97
|
watch: { kind: "boolean", help: "Watch mode — re-run on file changes" },
|
|
43
98
|
"watch-debounce": {
|
|
44
99
|
kind: "number",
|
|
@@ -188,12 +243,50 @@ function findTsxLoader() {
|
|
|
188
243
|
return undefined;
|
|
189
244
|
}
|
|
190
245
|
|
|
246
|
+
/** Trailing `.test.ts` / `.spec.js` / … stripped, so `--files=user` matches `user.test.ts`. */
|
|
247
|
+
const TEST_SUFFIX = /(\.(test|spec))?\.[cm]?[jt]sx?$/;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Japa's `--files` rule: keep a file when its path ENDS WITH the filter, or
|
|
251
|
+
* when every segment of the filter (read right-to-left, `*` matching anything)
|
|
252
|
+
* matches the corresponding path segment of the file minus its test suffix.
|
|
253
|
+
* `--files=user` keeps `tests/unit/user.test.ts`; `--files=unit/*` keeps
|
|
254
|
+
* everything under a `unit` directory.
|
|
255
|
+
*/
|
|
256
|
+
function filterByFileFilters(files, raw) {
|
|
257
|
+
if (raw === undefined) return files;
|
|
258
|
+
const filters = String(raw)
|
|
259
|
+
.split(",")
|
|
260
|
+
.map((f) => f.trim())
|
|
261
|
+
.filter((f) => f.length > 0);
|
|
262
|
+
if (filters.length === 0) return files;
|
|
263
|
+
return files.filter((file) => {
|
|
264
|
+
const unix = file.split(path.sep).join("/");
|
|
265
|
+
const withoutSuffix = unix.replace(TEST_SUFFIX, "");
|
|
266
|
+
return filters.some((filter) => {
|
|
267
|
+
if (unix.endsWith(filter)) return true;
|
|
268
|
+
const filterSegments = filter.split("/").reverse();
|
|
269
|
+
const fileSegments = withoutSuffix.split("/").reverse();
|
|
270
|
+
return filterSegments.every(
|
|
271
|
+
(segment, i) =>
|
|
272
|
+
fileSegments[i] !== undefined &&
|
|
273
|
+
(segment === "*" || fileSegments[i].endsWith(segment)),
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
191
279
|
function printHelp() {
|
|
192
280
|
const lines = [
|
|
193
281
|
"helix — Vitest-compatible test runner",
|
|
194
282
|
"",
|
|
195
283
|
"Usage:",
|
|
196
|
-
" helix test [files...|dirs...]",
|
|
284
|
+
" helix test [files...|dirs...|suites...]",
|
|
285
|
+
"",
|
|
286
|
+
" Positionals name test files or directories. When every one of them",
|
|
287
|
+
" matches a suite declared in helix.config.*, they name SUITES instead",
|
|
288
|
+
" (AdonisJS `node ace test unit functional`). With no positional and a",
|
|
289
|
+
" config present, every suite runs, in order.",
|
|
197
290
|
"",
|
|
198
291
|
"Flags:",
|
|
199
292
|
];
|
|
@@ -290,15 +383,66 @@ async function main() {
|
|
|
290
383
|
).href;
|
|
291
384
|
try {
|
|
292
385
|
// Probe by resolving through dynamic import; Node throws synchronously.
|
|
293
|
-
const { run } = await import(runModule);
|
|
386
|
+
const { run, runSuites } = await import(runModule);
|
|
294
387
|
const discoverModule = pathToFileURL(
|
|
295
388
|
useDist
|
|
296
389
|
? path.resolve(here, "../dist/cli/discover.js")
|
|
297
390
|
: path.resolve(here, "../src/cli/discover.ts"),
|
|
298
391
|
).href;
|
|
299
392
|
const { discover } = await import(discoverModule);
|
|
393
|
+
const suitesModule = pathToFileURL(
|
|
394
|
+
useDist
|
|
395
|
+
? path.resolve(here, "../dist/cli/suites.js")
|
|
396
|
+
: path.resolve(here, "../src/cli/suites.ts"),
|
|
397
|
+
).href;
|
|
398
|
+
const {
|
|
399
|
+
loadHelixConfig,
|
|
400
|
+
resolveHelixConfig,
|
|
401
|
+
resolveSuiteFiles,
|
|
402
|
+
selectSuites,
|
|
403
|
+
} = await import(suitesModule);
|
|
404
|
+
|
|
405
|
+
// AdonisJS parity: positionals may name SUITES declared in
|
|
406
|
+
// `helix.config.*` (`helix test unit`). When they don't — or there is no
|
|
407
|
+
// config — they stay what they have always been: paths.
|
|
408
|
+
const helixConfig = await loadHelixConfig(process.cwd());
|
|
409
|
+
const selectedSuites = selectSuites(helixConfig, parsed.positional);
|
|
410
|
+
|
|
411
|
+
// AdonisJS `tests/bootstrap.ts`: resolved once here, imported by every
|
|
412
|
+
// worker before its test file. Forwarded through the env so it reaches
|
|
413
|
+
// the worker under BOTH orchestrators.
|
|
414
|
+
const bootstrapModule = pathToFileURL(
|
|
415
|
+
useDist
|
|
416
|
+
? path.resolve(here, "../dist/runtime/bootstrap.js")
|
|
417
|
+
: path.resolve(here, "../src/runtime/bootstrap.ts"),
|
|
418
|
+
).href;
|
|
419
|
+
const { resolveBootstrap } = await import(bootstrapModule);
|
|
420
|
+
const { runGlobalHooks } = await import(
|
|
421
|
+
pathToFileURL(
|
|
422
|
+
useDist
|
|
423
|
+
? path.resolve(here, "../dist/runtime/global-hooks.js")
|
|
424
|
+
: path.resolve(here, "../src/runtime/global-hooks.ts"),
|
|
425
|
+
).href
|
|
426
|
+
);
|
|
427
|
+
process.env.HELIX_BOOTSTRAP =
|
|
428
|
+
resolveBootstrap(process.cwd(), helixConfig.bootstrap) ?? "";
|
|
429
|
+
// A `suites[].configure` callback lives in the config module; the worker
|
|
430
|
+
// re-imports it by path, since the function itself cannot be forwarded.
|
|
431
|
+
// Only named when a suite actually declares one, so a project without any
|
|
432
|
+
// pays nothing.
|
|
433
|
+
const declaresConfigure = (helixConfig.suites ?? []).some(
|
|
434
|
+
(suite) => typeof suite.configure === "function",
|
|
435
|
+
);
|
|
436
|
+
process.env.HELIX_SUITE_CONFIG = declaresConfigure
|
|
437
|
+
? (resolveHelixConfig(process.cwd()) ?? "")
|
|
438
|
+
: "";
|
|
300
439
|
|
|
301
|
-
const expanded =
|
|
440
|
+
const expanded = selectedSuites
|
|
441
|
+
? []
|
|
442
|
+
: filterByFileFilters(
|
|
443
|
+
await expandPositionals(parsed.positional, discover),
|
|
444
|
+
parsed.flags.files,
|
|
445
|
+
);
|
|
302
446
|
const tsxLoader = findTsxLoader();
|
|
303
447
|
if (parsed.flags.tsx !== false && !tsxLoader) {
|
|
304
448
|
process.stderr.write(
|
|
@@ -340,17 +484,41 @@ async function main() {
|
|
|
340
484
|
root: process.cwd(),
|
|
341
485
|
files: expanded,
|
|
342
486
|
threads: parsed.flags.threads,
|
|
343
|
-
timeoutMs: parsed.flags.timeout,
|
|
487
|
+
timeoutMs: parsed.flags.timeout ?? helixConfig.timeout,
|
|
344
488
|
reporter: parsed.flags.reporter,
|
|
489
|
+
reporters: parsed.flags.reporters
|
|
490
|
+
? String(parsed.flags.reporters)
|
|
491
|
+
.split(",")
|
|
492
|
+
.map((r) => r.trim())
|
|
493
|
+
.filter((r) => r.length > 0)
|
|
494
|
+
: undefined,
|
|
495
|
+
bail: parsed.flags.bail === true,
|
|
496
|
+
listPinned: parsed.flags["list-pinned"] === true,
|
|
497
|
+
failed: parsed.flags.failed === true,
|
|
345
498
|
useColors: parsed.flags.colors,
|
|
346
499
|
discovery: {
|
|
347
500
|
suffixes: parsed.flags.include,
|
|
348
501
|
hardExcludes: parsed.flags.exclude,
|
|
349
502
|
},
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
503
|
+
// `japaPlugins` points `@japa/runner/core` at helix's shim in every
|
|
504
|
+
// worker, so an official Japa plugin instruments helix. Appended to
|
|
505
|
+
// the loader list rather than replacing it: the test files still need
|
|
506
|
+
// their TS loader.
|
|
507
|
+
nodeArgs: [
|
|
508
|
+
...(parsed.flags.tsx === false || !tsxLoader
|
|
509
|
+
? []
|
|
510
|
+
: ["--import", tsxLoader]),
|
|
511
|
+
...(helixConfig.japaPlugins === true
|
|
512
|
+
? [
|
|
513
|
+
"--import",
|
|
514
|
+
pathToFileURL(
|
|
515
|
+
useDist
|
|
516
|
+
? path.resolve(here, "../dist/japa/japa-alias.mjs")
|
|
517
|
+
: path.resolve(here, "../src/japa/japa-alias.mjs"),
|
|
518
|
+
).href,
|
|
519
|
+
]
|
|
520
|
+
: []),
|
|
521
|
+
],
|
|
354
522
|
coverage: parsed.flags.coverage
|
|
355
523
|
? {
|
|
356
524
|
enabled: true,
|
|
@@ -373,8 +541,140 @@ async function main() {
|
|
|
373
541
|
}
|
|
374
542
|
: undefined,
|
|
375
543
|
};
|
|
376
|
-
|
|
377
|
-
|
|
544
|
+
// Per-test runtime filters travel to workers via env so they survive
|
|
545
|
+
// BOTH the TS pool and the Rust native orchestrator (child processes
|
|
546
|
+
// inherit this process's env; see runtime/worker.ts).
|
|
547
|
+
if (parsed.flags.grep !== undefined) {
|
|
548
|
+
process.env.HELIX_GREP = String(parsed.flags.grep);
|
|
549
|
+
}
|
|
550
|
+
if (parsed.flags.retries !== undefined) {
|
|
551
|
+
process.env.HELIX_RETRIES = String(parsed.flags.retries);
|
|
552
|
+
}
|
|
553
|
+
// Not used to resolve the timeout (the orchestrator carries it in the
|
|
554
|
+
// run instruction) — exported so a plugin can read it off `api.cliArgs`.
|
|
555
|
+
if (parsed.flags.timeout !== undefined) {
|
|
556
|
+
process.env.HELIX_TIMEOUT = String(parsed.flags.timeout);
|
|
557
|
+
}
|
|
558
|
+
if (parsed.flags.tags !== undefined) {
|
|
559
|
+
process.env.HELIX_TAGS = String(parsed.flags.tags);
|
|
560
|
+
}
|
|
561
|
+
if (parsed.flags["match-all"] === true || parsed.flags.matchAll === true) {
|
|
562
|
+
process.env.HELIX_MATCH_ALL = "1";
|
|
563
|
+
}
|
|
564
|
+
if (parsed.flags.tests !== undefined) {
|
|
565
|
+
process.env.HELIX_TESTS = String(parsed.flags.tests);
|
|
566
|
+
}
|
|
567
|
+
if (parsed.flags.groups !== undefined) {
|
|
568
|
+
process.env.HELIX_GROUPS = String(parsed.flags.groups);
|
|
569
|
+
}
|
|
570
|
+
if (parsed.flags.suite !== undefined) {
|
|
571
|
+
process.env.HELIX_SUITE = String(parsed.flags.suite);
|
|
572
|
+
}
|
|
573
|
+
if (parsed.flags.files !== undefined) {
|
|
574
|
+
process.env.HELIX_FILES = String(parsed.flags.files);
|
|
575
|
+
}
|
|
576
|
+
if (parsed.flags.bail === true) {
|
|
577
|
+
process.env.HELIX_BAIL = "1";
|
|
578
|
+
}
|
|
579
|
+
// Flags a plugin reads off `api.cliArgs` but the runtime itself doesn't
|
|
580
|
+
// act on — forwarded for the same reason as the filters: Japa hands its
|
|
581
|
+
// plugins the whole flag set.
|
|
582
|
+
if (cfg.reporters !== undefined) {
|
|
583
|
+
process.env.HELIX_REPORTERS = cfg.reporters.join(",");
|
|
584
|
+
} else if (parsed.flags.reporter !== undefined) {
|
|
585
|
+
process.env.HELIX_REPORTERS = String(parsed.flags.reporter);
|
|
586
|
+
}
|
|
587
|
+
if (parsed.flags.failed === true) {
|
|
588
|
+
process.env.HELIX_FAILED = "1";
|
|
589
|
+
}
|
|
590
|
+
// `--force-exit`, or `forceExit` in the config (AdonisJS `tests.forceExit`).
|
|
591
|
+
// Read back by `finish()` below, and by a plugin off `api.cliArgs`.
|
|
592
|
+
if (parsed.flags["list-pinned"] === true) {
|
|
593
|
+
process.env.HELIX_LIST_PINNED = "1";
|
|
594
|
+
}
|
|
595
|
+
// Positionals reach a plugin as `api.cliArgs._`, like Japa's.
|
|
596
|
+
if (parsed.positional.length > 0) {
|
|
597
|
+
process.env.HELIX_POSITIONALS = parsed.positional.join(",");
|
|
598
|
+
}
|
|
599
|
+
if (parsed.flags["force-exit"] === true || helixConfig.forceExit === true) {
|
|
600
|
+
process.env.HELIX_FORCE_EXIT = "1";
|
|
601
|
+
}
|
|
602
|
+
if (parsed.flags["bail-layer"] !== undefined) {
|
|
603
|
+
process.env.HELIX_BAIL_LAYER = String(parsed.flags["bail-layer"]);
|
|
604
|
+
}
|
|
605
|
+
// `--failed` replays the previous run's failures as a `--tests` filter,
|
|
606
|
+
// exactly like Japa's retry plugin.
|
|
607
|
+
if (parsed.flags.failed === true) {
|
|
608
|
+
const failedModule = pathToFileURL(
|
|
609
|
+
useDist
|
|
610
|
+
? path.resolve(here, "../dist/cli/failed-cache.js")
|
|
611
|
+
: path.resolve(here, "../src/cli/failed-cache.ts"),
|
|
612
|
+
).href;
|
|
613
|
+
const { readFailedCache } = await import(failedModule);
|
|
614
|
+
const failedTests = await readFailedCache(process.cwd());
|
|
615
|
+
if (failedTests.length === 0) {
|
|
616
|
+
process.stdout.write(
|
|
617
|
+
"helix: no failing tests cached — running all of them\n",
|
|
618
|
+
);
|
|
619
|
+
} else {
|
|
620
|
+
process.env.HELIX_TESTS = failedTests.join(",");
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// `runnerHooks` run ONCE around the whole run, here, and the workers are
|
|
625
|
+
// told to skip them — Japa's semantics, and the difference between
|
|
626
|
+
// migrating once and migrating once per test file.
|
|
627
|
+
const dropGlobalHooks = await runGlobalHooks(process.env.HELIX_BOOTSTRAP, {
|
|
628
|
+
japaPlugins: helixConfig.japaPlugins === true,
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
if (!selectedSuites) {
|
|
632
|
+
try {
|
|
633
|
+
const outcome = await run(cfg);
|
|
634
|
+
return outcome.exitCode;
|
|
635
|
+
} finally {
|
|
636
|
+
await dropGlobalHooks();
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Suites run one after another (Japa runs them in sequence too), each
|
|
641
|
+
// with its own files, timeout, retries and `meta.suite` name. The
|
|
642
|
+
// sequence is handed to the orchestrator as a whole so watch mode
|
|
643
|
+
// wraps ALL of it in one watcher and the `--failed` cache holds every
|
|
644
|
+
// suite's failures.
|
|
645
|
+
const steps = [];
|
|
646
|
+
for (const suite of selectedSuites) {
|
|
647
|
+
const suiteFiles = filterByFileFilters(
|
|
648
|
+
await resolveSuiteFiles(suite, process.cwd(), cfg.discovery),
|
|
649
|
+
parsed.flags.files,
|
|
650
|
+
);
|
|
651
|
+
if (suiteFiles.length === 0) {
|
|
652
|
+
process.stderr.write(`helix: suite "${suite.name}": no test files\n`);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
// A suite's `retries` overrides `--retries` for that suite only. Set
|
|
656
|
+
// on EVERY step (empty = unset, see `envCount`) so a suite that
|
|
657
|
+
// declares none doesn't inherit the previous suite's value.
|
|
658
|
+
const retries = suite.retries ?? parsed.flags.retries;
|
|
659
|
+
const env = {
|
|
660
|
+
HELIX_SUITE: suite.name,
|
|
661
|
+
HELIX_RETRIES: retries === undefined ? "" : String(retries),
|
|
662
|
+
};
|
|
663
|
+
steps.push({
|
|
664
|
+
env,
|
|
665
|
+
config: {
|
|
666
|
+
...cfg,
|
|
667
|
+
files: suiteFiles,
|
|
668
|
+
timeoutMs: cfg.timeoutMs ?? suite.timeout,
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
try {
|
|
673
|
+
const outcome = await runSuites(steps, cfg);
|
|
674
|
+
return outcome.exitCode;
|
|
675
|
+
} finally {
|
|
676
|
+
await dropGlobalHooks();
|
|
677
|
+
}
|
|
378
678
|
} catch (err) {
|
|
379
679
|
// Re-exec under tsx when Node can't satisfy the TS-source imports
|
|
380
680
|
// natively. Two failure shapes seen in the wild:
|
|
@@ -424,9 +724,25 @@ async function main() {
|
|
|
424
724
|
}
|
|
425
725
|
}
|
|
426
726
|
|
|
727
|
+
/**
|
|
728
|
+
* Japa semantics: the process exits on its own once the event loop drains, so a
|
|
729
|
+
* resource a test left open surfaces as a hang you can diagnose — rather than
|
|
730
|
+
* being swallowed by an unconditional `process.exit`, which also truncates
|
|
731
|
+
* pending stdout writes. `--force-exit` is the escape hatch, exactly as in Japa.
|
|
732
|
+
*/
|
|
733
|
+
function finish(code) {
|
|
734
|
+
process.exitCode = code;
|
|
735
|
+
// The env var carries the config-declared `forceExit`; the argv check also
|
|
736
|
+
// covers a failure that happened before the config was ever read.
|
|
737
|
+
const forced =
|
|
738
|
+
process.env.HELIX_FORCE_EXIT === "1" ||
|
|
739
|
+
process.argv.includes("--force-exit");
|
|
740
|
+
if (forced) process.exit(code);
|
|
741
|
+
}
|
|
742
|
+
|
|
427
743
|
main()
|
|
428
|
-
.then(
|
|
744
|
+
.then(finish)
|
|
429
745
|
.catch((err) => {
|
|
430
746
|
process.stderr.write(`helix: ${err instanceof Error ? err.stack : err}\n`);
|
|
431
|
-
|
|
747
|
+
finish(2);
|
|
432
748
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `--failed` cache — Japa's "run what failed last time".
|
|
3
|
+
*
|
|
4
|
+
* Japa writes the failed test TITLES to a cache file after every run, and
|
|
5
|
+
* `--failed` turns them into a `--tests` filter on the next one. Same contract
|
|
6
|
+
* here, same file shape (`{ tests: string[] }`), under
|
|
7
|
+
* `node_modules/.cache/helix/summary.json`.
|
|
8
|
+
*
|
|
9
|
+
* Named deviation: the cache is written only when the run produced per-test
|
|
10
|
+
* detail — i.e. the TypeScript pool. The native (Rust) engine reports totals
|
|
11
|
+
* only, so a run it owns leaves the previous cache untouched rather than
|
|
12
|
+
* blanking it. `--failed` therefore forces the TypeScript pool.
|
|
13
|
+
*/
|
|
14
|
+
import type { Summary } from "./summary.js";
|
|
15
|
+
/** Titles of the tests that failed, in run order, without duplicates. */
|
|
16
|
+
export declare function failedTestTitles(summary: Summary): string[];
|
|
17
|
+
/**
|
|
18
|
+
* Persist the failed titles for a later `--failed`. Never throws: a run must
|
|
19
|
+
* not fail because its cache could not be written.
|
|
20
|
+
*/
|
|
21
|
+
export declare function writeFailedCache(root: string, summary: Summary): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* The titles cached by the previous run, or an empty list when there is no
|
|
24
|
+
* usable cache (never run, unreadable, or malformed).
|
|
25
|
+
*/
|
|
26
|
+
export declare function readFailedCache(root: string): Promise<string[]>;
|
|
27
|
+
//# sourceMappingURL=failed-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"failed-cache.d.ts","sourceRoot":"","sources":["../../src/cli/failed-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAO5C,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAQ3D;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,GACd,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAWrE"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `--failed` cache — Japa's "run what failed last time".
|
|
3
|
+
*
|
|
4
|
+
* Japa writes the failed test TITLES to a cache file after every run, and
|
|
5
|
+
* `--failed` turns them into a `--tests` filter on the next one. Same contract
|
|
6
|
+
* here, same file shape (`{ tests: string[] }`), under
|
|
7
|
+
* `node_modules/.cache/helix/summary.json`.
|
|
8
|
+
*
|
|
9
|
+
* Named deviation: the cache is written only when the run produced per-test
|
|
10
|
+
* detail — i.e. the TypeScript pool. The native (Rust) engine reports totals
|
|
11
|
+
* only, so a run it owns leaves the previous cache untouched rather than
|
|
12
|
+
* blanking it. `--failed` therefore forces the TypeScript pool.
|
|
13
|
+
*/
|
|
14
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
/** Where the cache lives, relative to the project root. */
|
|
17
|
+
function cacheFile(root) {
|
|
18
|
+
return path.join(root, "node_modules", ".cache", "helix", "summary.json");
|
|
19
|
+
}
|
|
20
|
+
/** Titles of the tests that failed, in run order, without duplicates. */
|
|
21
|
+
export function failedTestTitles(summary) {
|
|
22
|
+
const titles = new Set();
|
|
23
|
+
for (const file of summary.files) {
|
|
24
|
+
for (const test of file.tests) {
|
|
25
|
+
if (test.status === "fail")
|
|
26
|
+
titles.add(test.name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return [...titles];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Persist the failed titles for a later `--failed`. Never throws: a run must
|
|
33
|
+
* not fail because its cache could not be written.
|
|
34
|
+
*/
|
|
35
|
+
export async function writeFailedCache(root, summary) {
|
|
36
|
+
const file = cacheFile(root);
|
|
37
|
+
try {
|
|
38
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
39
|
+
await writeFile(file, JSON.stringify({ tests: failedTestTitles(summary) }), "utf8");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Best-effort — a read-only or missing node_modules is not a run failure.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The titles cached by the previous run, or an empty list when there is no
|
|
47
|
+
* usable cache (never run, unreadable, or malformed).
|
|
48
|
+
*/
|
|
49
|
+
export async function readFailedCache(root) {
|
|
50
|
+
try {
|
|
51
|
+
const raw = await readFile(cacheFile(root), "utf8");
|
|
52
|
+
const parsed = JSON.parse(raw);
|
|
53
|
+
if (parsed === null || typeof parsed !== "object")
|
|
54
|
+
return [];
|
|
55
|
+
const tests = Reflect.get(parsed, "tests");
|
|
56
|
+
if (!Array.isArray(tests))
|
|
57
|
+
return [];
|
|
58
|
+
return tests.filter((t) => typeof t === "string");
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=failed-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"failed-cache.js","sourceRoot":"","sources":["../../src/cli/failed-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,2DAA2D;AAC3D,SAAS,SAAS,CAAC,IAAY;IAC9B,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC;AAC3E,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAChD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QAClC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC/B,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM;gBAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnD,CAAC;IACF,CAAC;IACD,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC;AACpB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACrC,IAAY,EACZ,OAAgB;IAEhB,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC7B,IAAI,CAAC;QACJ,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,MAAM,SAAS,CACd,IAAI,EACJ,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAAC,EAAE,CAAC,EACpD,MAAM,CACN,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACR,0EAA0E;IAC3E,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAY;IACjD,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;QACpD,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxC,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO,EAAE,CAAC;QAC7D,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC3C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QACrC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,CAAC;IACX,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glob patterns for suite `files` entries.
|
|
3
|
+
*
|
|
4
|
+
* AdonisJS declares suites with real globs — its own defaults are
|
|
5
|
+
* `tests/unit/**/*.spec.(js|ts)` — so an `adonisrc.ts` suite list has to be
|
|
6
|
+
* portable verbatim. Compiles to a `RegExp` over root-relative POSIX paths:
|
|
7
|
+
* `*`, `**`, `?`, `{a,b}`, `(a|b)`, `[abc]`, and the extglob quantifiers with an
|
|
8
|
+
* exact regex equivalent — `@(a|b)`, `?(a|b)`, `*(a|b)`, `+(a|b)`.
|
|
9
|
+
*
|
|
10
|
+
* An ENTRY may also start with `!` to exclude what it matches from the suite,
|
|
11
|
+
* the way a file list is usually written.
|
|
12
|
+
*
|
|
13
|
+
* One form stays refused: `!(a|b)`, the negated extglob GROUP. Its semantics
|
|
14
|
+
* ("anything in this segment other than a or b", composing with whatever
|
|
15
|
+
* follows) have no faithful regex rendering, and a near-miss would silently
|
|
16
|
+
* select the wrong files — the one thing a file selector must not do.
|
|
17
|
+
*/
|
|
18
|
+
/** Whether an entry is a pattern rather than a plain path. */
|
|
19
|
+
export declare function isGlob(entry: string): boolean;
|
|
20
|
+
/** Whether an entry excludes rather than selects (a leading `!`). */
|
|
21
|
+
export declare function isNegated(entry: string): boolean;
|
|
22
|
+
/** The pattern an entry carries, with any leading `!` stripped. */
|
|
23
|
+
export declare function withoutNegation(entry: string): string;
|
|
24
|
+
/** The reason an entry cannot be compiled, or `undefined` when it can. */
|
|
25
|
+
export declare function globRejection(entry: string): string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* The leading path that contains no pattern character — the directory a walk
|
|
28
|
+
* can start from. `tests/unit/**/*.spec.ts` → `tests/unit`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function globBaseDir(entry: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Compile a glob to an anchored `RegExp`. Paths are matched with `/`
|
|
33
|
+
* separators, relative to the root the pattern was declared against.
|
|
34
|
+
*/
|
|
35
|
+
export declare function globToRegExp(entry: string): RegExp;
|
|
36
|
+
//# sourceMappingURL=glob.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glob.d.ts","sourceRoot":"","sources":["../../src/cli/glob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,8DAA8D;AAC9D,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE7C;AAKD,qEAAqE;AACrE,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,mEAAmE;AACnE,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAK/D;AAeD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKjD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAwElD"}
|