@c9up/helix 0.1.4 → 0.1.5
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/dist/runtime/suite.d.ts +26 -0
- package/dist/runtime/suite.d.ts.map +1 -1
- package/dist/runtime/suite.js +21 -18
- package/dist/runtime/suite.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 +2 -2
- package/src/cli/coverage/aggregate.ts +0 -231
- package/src/cli/coverage/collect.ts +0 -63
- package/src/cli/coverage/diff/base.ts +0 -46
- package/src/cli/coverage/diff/index.ts +0 -160
- package/src/cli/coverage/diff/overlay.ts +0 -62
- package/src/cli/coverage/diff/parse.ts +0 -121
- package/src/cli/coverage/diff/reporters.ts +0 -82
- package/src/cli/coverage/diff/types.ts +0 -46
- package/src/cli/coverage/filter.ts +0 -71
- package/src/cli/coverage/glob.ts +0 -0
- package/src/cli/coverage/index.ts +0 -126
- package/src/cli/coverage/reporters/json.ts +0 -40
- package/src/cli/coverage/reporters/lcov.ts +0 -54
- package/src/cli/coverage/reporters/text.ts +0 -48
- package/src/cli/coverage/thresholds.ts +0 -73
- package/src/cli/coverage/types.ts +0 -93
- package/src/cli/discover.ts +0 -174
- package/src/cli/native.ts +0 -104
- package/src/cli/pool.ts +0 -486
- package/src/cli/reporter.ts +0 -155
- package/src/cli/run.ts +0 -440
- package/src/cli/summary.ts +0 -42
- package/src/cli/watch/loop.ts +0 -159
- package/src/cli/watch/types.ts +0 -22
- package/src/cli/watch/watcher.ts +0 -145
- package/src/container/index.ts +0 -16
- package/src/container/override.ts +0 -86
- package/src/container/spy.ts +0 -25
- package/src/index.ts +0 -42
- package/src/runtime/assertion-error.ts +0 -38
- package/src/runtime/cli-worker.ts +0 -140
- package/src/runtime/equals.ts +0 -400
- package/src/runtime/expect.ts +0 -173
- package/src/runtime/index.ts +0 -50
- package/src/runtime/lifecycle.ts +0 -17
- package/src/runtime/matchers.ts +0 -452
- package/src/runtime/run.ts +0 -573
- package/src/runtime/suite.ts +0 -310
- package/src/runtime/test-context.ts +0 -59
- package/src/runtime/vi/fake-timers.ts +0 -410
- package/src/runtime/vi/index.ts +0 -254
- package/src/runtime/vi/spy.ts +0 -224
- package/src/runtime/vi/spyOn.ts +0 -155
- package/src/runtime/vi/system-time.ts +0 -121
- package/src/runtime/worker.ts +0 -239
- package/src/time/freeze.ts +0 -229
- package/src/time/index.ts +0 -16
package/src/cli/run.ts
DELETED
|
@@ -1,440 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Orchestrator entry point — the TS equivalent of `ream-test-napi::run`.
|
|
3
|
-
*
|
|
4
|
-
* Discovers test files (or uses the explicit list), spawns the worker
|
|
5
|
-
* pool, collects framed results, aggregates into a `Summary`, and invokes
|
|
6
|
-
* the reporter at each milestone. When coverage is enabled, also threads
|
|
7
|
-
* the V8 coverage lifecycle (open session → forward `NODE_V8_COVERAGE`
|
|
8
|
-
* env to workers → aggregate after pool drain → write reports → enforce
|
|
9
|
-
* thresholds).
|
|
10
|
-
*
|
|
11
|
-
* Designed so the future NAPI binding can be plugged in as a drop-in
|
|
12
|
-
* replacement: both sides accept the same `RunConfig` shape and return
|
|
13
|
-
* the same `Summary`.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { existsSync } from "node:fs";
|
|
17
|
-
import { rm } from "node:fs/promises";
|
|
18
|
-
import os from "node:os";
|
|
19
|
-
import path from "node:path";
|
|
20
|
-
import { fileURLToPath } from "node:url";
|
|
21
|
-
import {
|
|
22
|
-
type DiffOptions,
|
|
23
|
-
type DiffSummary,
|
|
24
|
-
diffViolationSummary,
|
|
25
|
-
finaliseDiff,
|
|
26
|
-
} from "./coverage/diff/index.js";
|
|
27
|
-
import {
|
|
28
|
-
type CoverageOptions,
|
|
29
|
-
type CoverageSession,
|
|
30
|
-
type CoverageSummary,
|
|
31
|
-
finalise as finaliseCoverage,
|
|
32
|
-
openSession,
|
|
33
|
-
type ThresholdViolation,
|
|
34
|
-
violationSummary,
|
|
35
|
-
} from "./coverage/index.js";
|
|
36
|
-
import { type DiscoveryOptions, discover } from "./discover.js";
|
|
37
|
-
import { getNative } from "./native.js";
|
|
38
|
-
import { runPool } from "./pool.js";
|
|
39
|
-
import { makeReporter, type Reporter } from "./reporter.js";
|
|
40
|
-
import {
|
|
41
|
-
buildSummary,
|
|
42
|
-
exitCode,
|
|
43
|
-
type Summary,
|
|
44
|
-
type Totals,
|
|
45
|
-
} from "./summary.js";
|
|
46
|
-
import { runWatch } from "./watch/loop.js";
|
|
47
|
-
import type { WatchOptions } from "./watch/types.js";
|
|
48
|
-
|
|
49
|
-
export type { WatchOptions } from "./watch/types.js";
|
|
50
|
-
|
|
51
|
-
const DEFAULT_WATCH_DEBOUNCE_MS = 200;
|
|
52
|
-
const DEFAULT_WATCH_INCLUDE = [
|
|
53
|
-
"src/**/*.{ts,tsx,js,mjs,cjs}",
|
|
54
|
-
"tests/**/*.{ts,tsx,js,mjs,cjs}",
|
|
55
|
-
"test/**/*.{ts,tsx,js,mjs,cjs}",
|
|
56
|
-
];
|
|
57
|
-
const DEFAULT_WATCH_EXCLUDE = [
|
|
58
|
-
"node_modules/**",
|
|
59
|
-
"dist/**",
|
|
60
|
-
"build/**",
|
|
61
|
-
"coverage/**",
|
|
62
|
-
".helix-coverage/**",
|
|
63
|
-
".git/**",
|
|
64
|
-
".wolf/**",
|
|
65
|
-
"target/**",
|
|
66
|
-
".next/**",
|
|
67
|
-
];
|
|
68
|
-
|
|
69
|
-
export interface RunConfig {
|
|
70
|
-
/** Absolute root directory to discover from. */
|
|
71
|
-
root: string;
|
|
72
|
-
/** Explicit files to run. When non-empty, discovery is skipped. */
|
|
73
|
-
files?: string[];
|
|
74
|
-
/** Discovery options (suffixes, excludes). */
|
|
75
|
-
discovery?: DiscoveryOptions;
|
|
76
|
-
/** Number of concurrent workers. Default: os.cpus().length. */
|
|
77
|
-
threads?: number;
|
|
78
|
-
/** Per-file timeout (ms). Default 60 000. */
|
|
79
|
-
timeoutMs?: number;
|
|
80
|
-
/** Reporter name: `"dot" | "spec" | "json"`. Default `"spec"`. */
|
|
81
|
-
reporter?: string;
|
|
82
|
-
/** Enable ANSI colours. Default: stdout is TTY. */
|
|
83
|
-
useColors?: boolean;
|
|
84
|
-
/** Pluggable reporter instance (overrides `reporter` when provided). */
|
|
85
|
-
reporterInstance?: Reporter;
|
|
86
|
-
/** Override the node binary. Default: `process.execPath`. */
|
|
87
|
-
nodeBin?: string;
|
|
88
|
-
/** Extra args for node (before the worker entry). */
|
|
89
|
-
nodeArgs?: string[];
|
|
90
|
-
/** Override the worker entry path (defaults to bundled `cli-worker.ts`). */
|
|
91
|
-
workerEntry?: string;
|
|
92
|
-
/** Coverage collection + reporting. Defaults to `{ enabled: false }`. */
|
|
93
|
-
coverage?: CoverageOptions;
|
|
94
|
-
/** Diff coverage vs a base ref. Requires `coverage.enabled === true`. */
|
|
95
|
-
diffCoverage?: DiffOptions;
|
|
96
|
-
/** Watch mode: re-run on file changes. */
|
|
97
|
-
watch?: WatchOptions;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export interface RunOutcome {
|
|
101
|
-
summary: Summary;
|
|
102
|
-
coverage?: CoverageSummary;
|
|
103
|
-
coverageViolations?: ThresholdViolation[];
|
|
104
|
-
diffCoverage?: DiffSummary;
|
|
105
|
-
diffCoverageViolations?: ThresholdViolation[];
|
|
106
|
-
exitCode: number;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function defaultWorkerEntry(): string {
|
|
110
|
-
// Resolve relative to THIS module's directory so it works whether we run
|
|
111
|
-
// from `src/cli/run.ts` (dev: src sibling is `.ts`) or from the compiled
|
|
112
|
-
// `dist/cli/run.js` (publish: sibling is `.js`). Hardcoding `.ts` made
|
|
113
|
-
// the spawned worker open a non-existent `dist/runtime/cli-worker.ts`
|
|
114
|
-
// when consumed via the dist tarball, dying silently before the pool's
|
|
115
|
-
// frame handler could observe anything.
|
|
116
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
117
|
-
const compiled = path.resolve(here, "../runtime/cli-worker.js");
|
|
118
|
-
if (existsSync(compiled)) return compiled;
|
|
119
|
-
return path.resolve(here, "../runtime/cli-worker.ts");
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Plain-path execution via the native `ream-test-napi` engine: it discovers
|
|
124
|
-
* (skipped here — we pass the resolved `files`), spawns the worker pool, drives
|
|
125
|
-
* the reporter, and returns aggregated totals. Per-file detail is carried in
|
|
126
|
-
* `payload.json` (same shape as `Summary`) for callers that need it; the CLI
|
|
127
|
-
* only consumes `exitCode`, and the Rust reporter already streamed per-file
|
|
128
|
-
* output, so the reconstructed `Summary` keeps the detail arrays empty.
|
|
129
|
-
*/
|
|
130
|
-
async function runNative(
|
|
131
|
-
config: RunConfig,
|
|
132
|
-
root: string,
|
|
133
|
-
files: string[],
|
|
134
|
-
): Promise<RunOutcome> {
|
|
135
|
-
const { run: nativeRun } = getNative();
|
|
136
|
-
const payload = await nativeRun({
|
|
137
|
-
root,
|
|
138
|
-
files,
|
|
139
|
-
threads: config.threads,
|
|
140
|
-
timeoutMs: config.timeoutMs,
|
|
141
|
-
reporter: config.reporter,
|
|
142
|
-
workerEntry: config.workerEntry ?? defaultWorkerEntry(),
|
|
143
|
-
nodeBin: config.nodeBin ?? process.execPath,
|
|
144
|
-
nodeArgs: config.nodeArgs,
|
|
145
|
-
useColors: config.useColors ?? process.stdout.isTTY === true,
|
|
146
|
-
});
|
|
147
|
-
const totals: Totals = {
|
|
148
|
-
pass: payload.pass,
|
|
149
|
-
fail: payload.fail,
|
|
150
|
-
skip: payload.skip,
|
|
151
|
-
todo: payload.todo,
|
|
152
|
-
fileErrors: payload.fileErrors,
|
|
153
|
-
};
|
|
154
|
-
const summary: Summary = {
|
|
155
|
-
totals,
|
|
156
|
-
files: [],
|
|
157
|
-
fileErrors: [],
|
|
158
|
-
durationMs: payload.durationMs,
|
|
159
|
-
};
|
|
160
|
-
return { summary, exitCode: payload.exitCode };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export async function run(config: RunConfig): Promise<RunOutcome> {
|
|
164
|
-
if (!config.watch?.enabled) return runOnce(config);
|
|
165
|
-
const root = path.isAbsolute(config.root)
|
|
166
|
-
? config.root
|
|
167
|
-
: path.resolve(config.root);
|
|
168
|
-
const debounceMs = config.watch.debounceMs ?? DEFAULT_WATCH_DEBOUNCE_MS;
|
|
169
|
-
// When the user has configured custom coverage globs, mirror them in
|
|
170
|
-
// the watcher so the two surfaces agree on "files I care about".
|
|
171
|
-
// Otherwise fall back to the watch defaults (which intentionally
|
|
172
|
-
// include `tests/**` even though coverage doesn't, so editing a test
|
|
173
|
-
// triggers a re-run).
|
|
174
|
-
const include =
|
|
175
|
-
config.watch.include ?? config.coverage?.include ?? DEFAULT_WATCH_INCLUDE;
|
|
176
|
-
const exclude = [
|
|
177
|
-
...(config.watch.exclude ??
|
|
178
|
-
config.coverage?.exclude ??
|
|
179
|
-
DEFAULT_WATCH_EXCLUDE),
|
|
180
|
-
];
|
|
181
|
-
return runWatch(
|
|
182
|
-
{
|
|
183
|
-
root,
|
|
184
|
-
include,
|
|
185
|
-
exclude,
|
|
186
|
-
debounceMs,
|
|
187
|
-
signal: config.watch.signal,
|
|
188
|
-
},
|
|
189
|
-
() => runOnce(config),
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Aggregated coverage + diff-coverage results threaded back into `RunOutcome`. */
|
|
194
|
-
interface CoverageOutcome {
|
|
195
|
-
coverage?: CoverageSummary;
|
|
196
|
-
coverageViolations?: ThresholdViolation[];
|
|
197
|
-
diffCoverage?: DiffSummary;
|
|
198
|
-
diffCoverageViolations?: ThresholdViolation[];
|
|
199
|
-
// Tracks unrecoverable diff-cov failures so the run still exits non-zero
|
|
200
|
-
// when diff-cov was supposed to gate the PR (otherwise a thrown error
|
|
201
|
-
// in finaliseDiff would silently downgrade exit to the full-tree code).
|
|
202
|
-
diffCoverageFailed: boolean;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** Resolve the explicit file list, or discover, warning when nothing matches. */
|
|
206
|
-
async function resolveRunFiles(
|
|
207
|
-
config: RunConfig,
|
|
208
|
-
root: string,
|
|
209
|
-
): Promise<string[]> {
|
|
210
|
-
const files =
|
|
211
|
-
config.files && config.files.length > 0
|
|
212
|
-
? config.files.map((f) => (path.isAbsolute(f) ? f : path.resolve(f)))
|
|
213
|
-
: await discover(root, config.discovery);
|
|
214
|
-
|
|
215
|
-
if (files.length === 0) {
|
|
216
|
-
process.stderr.write(
|
|
217
|
-
`helix: no test files found under ${root} — check your include/exclude patterns.\n`,
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
return files;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Refuse mis-configured diff coverage up front (before spawning workers) so the
|
|
225
|
-
* user fixes the config rather than getting a silent no-op or 0% diff coverage.
|
|
226
|
-
*/
|
|
227
|
-
function assertDiffCoverageConfig(config: RunConfig, root: string): void {
|
|
228
|
-
if (!config.diffCoverage?.enabled) return;
|
|
229
|
-
|
|
230
|
-
// Diff-cov requires the full-tree coverage to be enabled — it has nothing
|
|
231
|
-
// to overlay otherwise.
|
|
232
|
-
if (config.coverage?.enabled !== true) {
|
|
233
|
-
throw new Error(
|
|
234
|
-
"diffCoverage.enabled requires coverage.enabled — diff-cov has nothing to overlay otherwise.",
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Diff-cov spawns git in `diffCoverage.cwd` (defaulting to `coverage.root`).
|
|
239
|
-
// The two paths must share a common ancestry — either coverage.root is
|
|
240
|
-
// under diffCoverage.cwd (common monorepo case: `cwd` = git root,
|
|
241
|
-
// `root` = a sub-package), or diffCoverage.cwd is under coverage.root
|
|
242
|
-
// (legacy single-repo case). When they're fully disjoint, every diff
|
|
243
|
-
// entry resolves to a path the coverage summary never indexes →
|
|
244
|
-
// silent 0% diff coverage. Refuse only that case.
|
|
245
|
-
if (!config.diffCoverage.cwd) return;
|
|
246
|
-
const covRoot = path.resolve(config.coverage?.root ?? root);
|
|
247
|
-
const diffCwd = path.resolve(config.diffCoverage.cwd);
|
|
248
|
-
const covUnderDiff = path.relative(diffCwd, covRoot);
|
|
249
|
-
const diffUnderCov = path.relative(covRoot, diffCwd);
|
|
250
|
-
const covDescendantOfDiff =
|
|
251
|
-
covUnderDiff !== "" &&
|
|
252
|
-
!covUnderDiff.startsWith("..") &&
|
|
253
|
-
!path.isAbsolute(covUnderDiff);
|
|
254
|
-
const diffDescendantOfCov =
|
|
255
|
-
diffUnderCov !== "" &&
|
|
256
|
-
!diffUnderCov.startsWith("..") &&
|
|
257
|
-
!path.isAbsolute(diffUnderCov);
|
|
258
|
-
const equal = covUnderDiff === "";
|
|
259
|
-
if (!equal && !covDescendantOfDiff && !diffDescendantOfCov) {
|
|
260
|
-
throw new Error(
|
|
261
|
-
`diffCoverage.cwd (${diffCwd}) and coverage.root (${covRoot}) are disjoint paths; diff entries would never overlay coverage.`,
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Finalise full-tree coverage, then overlay diff coverage on top. Each stage is
|
|
268
|
-
* isolated: a failed coverage finalise (disk full, permissions) or a thrown
|
|
269
|
-
* diff-cov is surfaced to stderr without crashing the run, but a diff-cov
|
|
270
|
-
* failure still flips `diffCoverageFailed` so the CI gate stays red.
|
|
271
|
-
*/
|
|
272
|
-
async function finaliseCoverageOutcome(
|
|
273
|
-
config: RunConfig & { coverage: CoverageOptions },
|
|
274
|
-
root: string,
|
|
275
|
-
session: CoverageSession,
|
|
276
|
-
): Promise<CoverageOutcome> {
|
|
277
|
-
const out: CoverageOutcome = { diffCoverageFailed: false };
|
|
278
|
-
try {
|
|
279
|
-
const finalised = await finaliseCoverage({
|
|
280
|
-
root: config.coverage.root ?? root,
|
|
281
|
-
session,
|
|
282
|
-
enabled: true,
|
|
283
|
-
include: config.coverage.include,
|
|
284
|
-
exclude: config.coverage.exclude,
|
|
285
|
-
reporters: config.coverage.reporters,
|
|
286
|
-
outputDir: config.coverage.outputDir,
|
|
287
|
-
thresholds: config.coverage.thresholds,
|
|
288
|
-
});
|
|
289
|
-
out.coverage = finalised.summary;
|
|
290
|
-
out.coverageViolations = finalised.violations;
|
|
291
|
-
if (finalised.textReport) process.stdout.write(finalised.textReport);
|
|
292
|
-
if (finalised.violations.length > 0) {
|
|
293
|
-
process.stderr.write(`${violationSummary(finalised.violations)}\n`);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Diff coverage runs AFTER full-tree finalise, using its summary.
|
|
297
|
-
if (config.diffCoverage?.enabled && out.coverage) {
|
|
298
|
-
await overlayDiffCoverage(config, root, out.coverage, out);
|
|
299
|
-
}
|
|
300
|
-
} catch (err) {
|
|
301
|
-
// Coverage finalisation failed (disk full, permission, etc.) — the test
|
|
302
|
-
// summary already printed; surface the failure but don't crash the whole
|
|
303
|
-
// run with a stack trace.
|
|
304
|
-
process.stderr.write(
|
|
305
|
-
`helix-coverage: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
306
|
-
);
|
|
307
|
-
out.coverageViolations = [];
|
|
308
|
-
}
|
|
309
|
-
return out;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/** Overlay diff coverage onto an already-finalised full-tree summary. */
|
|
313
|
-
async function overlayDiffCoverage(
|
|
314
|
-
config: RunConfig & { coverage: CoverageOptions },
|
|
315
|
-
root: string,
|
|
316
|
-
coverage: CoverageSummary,
|
|
317
|
-
out: CoverageOutcome,
|
|
318
|
-
): Promise<void> {
|
|
319
|
-
if (!config.diffCoverage?.enabled) return;
|
|
320
|
-
try {
|
|
321
|
-
const diff = await finaliseDiff({
|
|
322
|
-
enabled: true,
|
|
323
|
-
root: config.coverage.root ?? root,
|
|
324
|
-
coverage,
|
|
325
|
-
base: config.diffCoverage.base,
|
|
326
|
-
thresholds: config.diffCoverage.thresholds,
|
|
327
|
-
outputDir: config.diffCoverage.outputDir ?? config.coverage.outputDir,
|
|
328
|
-
cwd: config.diffCoverage.cwd,
|
|
329
|
-
});
|
|
330
|
-
if (diff.warning) {
|
|
331
|
-
process.stderr.write(`${diff.warning}\n`);
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
out.diffCoverage = diff.summary;
|
|
335
|
-
out.diffCoverageViolations = diff.violations;
|
|
336
|
-
if (diff.textReport) process.stdout.write(diff.textReport);
|
|
337
|
-
if (diff.violations.length > 0) {
|
|
338
|
-
process.stderr.write(`${diffViolationSummary(diff.violations)}\n`);
|
|
339
|
-
}
|
|
340
|
-
} catch (err) {
|
|
341
|
-
process.stderr.write(
|
|
342
|
-
`helix-diff-cov: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
343
|
-
);
|
|
344
|
-
// Don't let a thrown finaliseDiff masquerade as a clean run — the user
|
|
345
|
-
// explicitly opted into diff-cov and a CI gate around it must still fail.
|
|
346
|
-
out.diffCoverageFailed = true;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
async function runOnce(config: RunConfig): Promise<RunOutcome> {
|
|
351
|
-
const started = Date.now();
|
|
352
|
-
const root = path.isAbsolute(config.root)
|
|
353
|
-
? config.root
|
|
354
|
-
: path.resolve(config.root);
|
|
355
|
-
|
|
356
|
-
const files = await resolveRunFiles(config, root);
|
|
357
|
-
|
|
358
|
-
// Cutover: the Rust NAPI engine owns the plain discovery + worker-pool +
|
|
359
|
-
// reporter + summary path (42-N-orchestrator). Delegate unless a TS-only
|
|
360
|
-
// layer is active — a pluggable reporter instance, coverage, or diff-cov,
|
|
361
|
-
// none of which the Rust `run` exposes yet. Watch is already unwrapped by
|
|
362
|
-
// `run()` above, so it never reaches here.
|
|
363
|
-
if (
|
|
364
|
-
!config.reporterInstance &&
|
|
365
|
-
config.coverage?.enabled !== true &&
|
|
366
|
-
config.diffCoverage?.enabled !== true
|
|
367
|
-
) {
|
|
368
|
-
return runNative(config, root, files);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const reporter =
|
|
372
|
-
config.reporterInstance ??
|
|
373
|
-
makeReporter(
|
|
374
|
-
config.reporter,
|
|
375
|
-
config.useColors ?? process.stdout.isTTY === true,
|
|
376
|
-
);
|
|
377
|
-
|
|
378
|
-
const threads = config.threads ?? os.cpus().length;
|
|
379
|
-
|
|
380
|
-
assertDiffCoverageConfig(config, root);
|
|
381
|
-
|
|
382
|
-
// Open a coverage session BEFORE spawning workers so `NODE_V8_COVERAGE`
|
|
383
|
-
// is set in their env. The pool forwards `extraEnv` to every spawn.
|
|
384
|
-
const coverageEnabled = config.coverage?.enabled === true;
|
|
385
|
-
const session = coverageEnabled ? await openSession() : undefined;
|
|
386
|
-
|
|
387
|
-
try {
|
|
388
|
-
const { results, errors } = await runPool(
|
|
389
|
-
files,
|
|
390
|
-
{
|
|
391
|
-
workerEntry: config.workerEntry ?? defaultWorkerEntry(),
|
|
392
|
-
nodeBin: config.nodeBin,
|
|
393
|
-
nodeArgs: config.nodeArgs,
|
|
394
|
-
threads,
|
|
395
|
-
timeoutMs: config.timeoutMs,
|
|
396
|
-
extraEnv: session?.env,
|
|
397
|
-
},
|
|
398
|
-
reporter,
|
|
399
|
-
);
|
|
400
|
-
|
|
401
|
-
const summary = buildSummary(results, errors, Date.now() - started);
|
|
402
|
-
reporter.onSummary(summary);
|
|
403
|
-
|
|
404
|
-
const cov: CoverageOutcome =
|
|
405
|
-
session && config.coverage
|
|
406
|
-
? await finaliseCoverageOutcome(
|
|
407
|
-
{ ...config, coverage: config.coverage },
|
|
408
|
-
root,
|
|
409
|
-
session,
|
|
410
|
-
)
|
|
411
|
-
: { diffCoverageFailed: false };
|
|
412
|
-
|
|
413
|
-
const baseExit = exitCode(summary);
|
|
414
|
-
const coverageExit =
|
|
415
|
-
cov.coverageViolations && cov.coverageViolations.length > 0 ? 1 : 0;
|
|
416
|
-
const diffCoverageExit =
|
|
417
|
-
(cov.diffCoverageViolations && cov.diffCoverageViolations.length > 0) ||
|
|
418
|
-
cov.diffCoverageFailed
|
|
419
|
-
? 1
|
|
420
|
-
: 0;
|
|
421
|
-
return {
|
|
422
|
-
summary,
|
|
423
|
-
coverage: cov.coverage,
|
|
424
|
-
coverageViolations: cov.coverageViolations,
|
|
425
|
-
diffCoverage: cov.diffCoverage,
|
|
426
|
-
diffCoverageViolations: cov.diffCoverageViolations,
|
|
427
|
-
exitCode: Math.max(baseExit, coverageExit, diffCoverageExit),
|
|
428
|
-
};
|
|
429
|
-
} finally {
|
|
430
|
-
// Belt-and-braces: if `runPool` itself rejected before
|
|
431
|
-
// `finaliseCoverage` ran (which normally cleans the temp dir), we
|
|
432
|
-
// still remove the session dir here so failed runs don't leak under
|
|
433
|
-
// `.helix-coverage/`.
|
|
434
|
-
if (session) {
|
|
435
|
-
await rm(session.envDir, { recursive: true, force: true }).catch(
|
|
436
|
-
() => {},
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
package/src/cli/summary.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { FileResult } from "../runtime/run.js";
|
|
2
|
-
import type { WorkerErrorMessage } from "./pool.js";
|
|
3
|
-
|
|
4
|
-
export interface Totals {
|
|
5
|
-
pass: number;
|
|
6
|
-
fail: number;
|
|
7
|
-
skip: number;
|
|
8
|
-
todo: number;
|
|
9
|
-
fileErrors: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface Summary {
|
|
13
|
-
totals: Totals;
|
|
14
|
-
files: FileResult[];
|
|
15
|
-
fileErrors: WorkerErrorMessage[];
|
|
16
|
-
durationMs: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function buildSummary(
|
|
20
|
-
files: FileResult[],
|
|
21
|
-
fileErrors: WorkerErrorMessage[],
|
|
22
|
-
durationMs: number,
|
|
23
|
-
): Summary {
|
|
24
|
-
const totals: Totals = {
|
|
25
|
-
pass: 0,
|
|
26
|
-
fail: 0,
|
|
27
|
-
skip: 0,
|
|
28
|
-
todo: 0,
|
|
29
|
-
fileErrors: fileErrors.length,
|
|
30
|
-
};
|
|
31
|
-
for (const f of files) {
|
|
32
|
-
totals.pass += f.totals.pass;
|
|
33
|
-
totals.fail += f.totals.fail;
|
|
34
|
-
totals.skip += f.totals.skip;
|
|
35
|
-
totals.todo += f.totals.todo;
|
|
36
|
-
}
|
|
37
|
-
return { totals, files, fileErrors, durationMs };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function exitCode(summary: Summary): number {
|
|
41
|
-
return summary.totals.fail > 0 || summary.totals.fileErrors > 0 ? 1 : 0;
|
|
42
|
-
}
|
package/src/cli/watch/loop.ts
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Watch-mode run loop: runs `runOnce` once on startup, installs the
|
|
3
|
-
* watcher, and re-invokes `runOnce` whenever a coalesced change burst
|
|
4
|
-
* fires. Re-runs are serialised — at most one queued follow-up while a
|
|
5
|
-
* run is in flight; further bursts during that window are dropped (the
|
|
6
|
-
* queued run will pick up the latest disk state).
|
|
7
|
-
*
|
|
8
|
-
* On SIGINT (or an aborted external `AbortSignal`), the loop closes
|
|
9
|
-
* the watcher, waits for any in-flight run, and resolves with the
|
|
10
|
-
* LAST `RunOutcome` (with `exitCode: 0`, since a failing run during
|
|
11
|
-
* interactive watch should not poison the exit status — failures
|
|
12
|
-
* already surface as test output).
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { RunOutcome } from "../run.js";
|
|
16
|
-
import { createWatcher, type WatcherHandle } from "./watcher.js";
|
|
17
|
-
|
|
18
|
-
export interface RunWatchOptions {
|
|
19
|
-
root: string;
|
|
20
|
-
include: string[];
|
|
21
|
-
exclude: string[];
|
|
22
|
-
debounceMs: number;
|
|
23
|
-
/** Optional external abort. When triggered, behaves like SIGINT.
|
|
24
|
-
* Lets tests drive shutdown without `process.emit("SIGINT")` (which
|
|
25
|
-
* collides with vitest's own signal handlers). */
|
|
26
|
-
signal?: AbortSignal;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const BANNER_WAITING = "[helix] watching for changes… (Ctrl-C to exit)";
|
|
30
|
-
const BANNER_RERUN = "[helix] change detected, re-running…";
|
|
31
|
-
|
|
32
|
-
function fallbackOutcome(): RunOutcome {
|
|
33
|
-
return {
|
|
34
|
-
summary: {
|
|
35
|
-
totals: { pass: 0, fail: 0, skip: 0, todo: 0, fileErrors: 0 },
|
|
36
|
-
files: [],
|
|
37
|
-
fileErrors: [],
|
|
38
|
-
durationMs: 0,
|
|
39
|
-
},
|
|
40
|
-
exitCode: 0,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function runWatch(
|
|
45
|
-
opts: RunWatchOptions,
|
|
46
|
-
runOnce: () => Promise<RunOutcome>,
|
|
47
|
-
): Promise<RunOutcome> {
|
|
48
|
-
// Initialise upfront so the final return never reads `undefined`,
|
|
49
|
-
// even if `runOnce` throws and the catch path's fallback construction
|
|
50
|
-
// itself throws (defence-in-depth against a brittle cast).
|
|
51
|
-
let lastOutcome: RunOutcome = fallbackOutcome();
|
|
52
|
-
let inFlight: Promise<void> | undefined;
|
|
53
|
-
let queued = false;
|
|
54
|
-
let closing = false;
|
|
55
|
-
let watcher: WatcherHandle | undefined;
|
|
56
|
-
let resolveSettled: () => void;
|
|
57
|
-
const settled = new Promise<void>((resolve) => {
|
|
58
|
-
resolveSettled = resolve;
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
function beginShutdown(): void {
|
|
62
|
-
if (closing) return;
|
|
63
|
-
closing = true;
|
|
64
|
-
void (async () => {
|
|
65
|
-
if (watcher) await watcher.close();
|
|
66
|
-
if (inFlight) await inFlight;
|
|
67
|
-
resolveSettled();
|
|
68
|
-
})();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const onSigint = (): void => beginShutdown();
|
|
72
|
-
const onAbort = (): void => beginShutdown();
|
|
73
|
-
process.on("SIGINT", onSigint);
|
|
74
|
-
if (opts.signal) {
|
|
75
|
-
if (opts.signal.aborted) {
|
|
76
|
-
closing = true;
|
|
77
|
-
} else {
|
|
78
|
-
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function trigger(): void {
|
|
83
|
-
if (closing) return;
|
|
84
|
-
if (inFlight) {
|
|
85
|
-
queued = true;
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
process.stdout.write(`${BANNER_RERUN}\n`);
|
|
89
|
-
inFlight = (async () => {
|
|
90
|
-
try {
|
|
91
|
-
lastOutcome = await runOnce();
|
|
92
|
-
} catch (err) {
|
|
93
|
-
process.stderr.write(
|
|
94
|
-
`helix-watch: run failed: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
95
|
-
);
|
|
96
|
-
} finally {
|
|
97
|
-
inFlight = undefined;
|
|
98
|
-
if (!closing) {
|
|
99
|
-
process.stdout.write(`${BANNER_WAITING}\n`);
|
|
100
|
-
}
|
|
101
|
-
if (queued && !closing) {
|
|
102
|
-
queued = false;
|
|
103
|
-
trigger();
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
})();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Initial run — drive it through the same in-flight machinery so a
|
|
110
|
-
// SIGINT/abort arriving during the first run is handled exactly like
|
|
111
|
-
// one arriving during a re-run (handler awaits `inFlight`, then
|
|
112
|
-
// resolves).
|
|
113
|
-
inFlight = (async () => {
|
|
114
|
-
try {
|
|
115
|
-
lastOutcome = await runOnce();
|
|
116
|
-
} catch (err) {
|
|
117
|
-
process.stderr.write(
|
|
118
|
-
`helix-watch: run failed: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
119
|
-
);
|
|
120
|
-
} finally {
|
|
121
|
-
inFlight = undefined;
|
|
122
|
-
}
|
|
123
|
-
})();
|
|
124
|
-
await inFlight;
|
|
125
|
-
|
|
126
|
-
if (closing) {
|
|
127
|
-
process.off("SIGINT", onSigint);
|
|
128
|
-
opts.signal?.removeEventListener("abort", onAbort);
|
|
129
|
-
return { ...lastOutcome, exitCode: 0 };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
process.stdout.write(`${BANNER_WAITING}\n`);
|
|
133
|
-
|
|
134
|
-
let watcherFatalError: Error | undefined;
|
|
135
|
-
watcher = createWatcher({
|
|
136
|
-
root: opts.root,
|
|
137
|
-
include: opts.include,
|
|
138
|
-
exclude: opts.exclude,
|
|
139
|
-
debounceMs: opts.debounceMs,
|
|
140
|
-
onChange: () => trigger(),
|
|
141
|
-
onError: (err) => {
|
|
142
|
-
watcherFatalError = err;
|
|
143
|
-
beginShutdown();
|
|
144
|
-
},
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
await settled;
|
|
148
|
-
|
|
149
|
-
process.off("SIGINT", onSigint);
|
|
150
|
-
opts.signal?.removeEventListener("abort", onAbort);
|
|
151
|
-
|
|
152
|
-
if (watcherFatalError) {
|
|
153
|
-
process.stderr.write(
|
|
154
|
-
`helix-watch: watcher error — ${watcherFatalError.message}\n`,
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return { ...lastOutcome, exitCode: 0 };
|
|
159
|
-
}
|
package/src/cli/watch/types.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Watch-mode public types. Kept in their own file so `bin/helix.js` and
|
|
3
|
-
* `run.ts` can import without pulling in the watcher implementation.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export interface WatchOptions {
|
|
7
|
-
enabled: boolean;
|
|
8
|
-
/** Debounce window in ms — events within the window collapse to one
|
|
9
|
-
* re-run. Default 200, range [1, 5_000]. */
|
|
10
|
-
debounceMs?: number;
|
|
11
|
-
/** Override the watcher's include globs. Defaults to `coverage.include`
|
|
12
|
-
* when set, otherwise the watch defaults (`src/**`, `tests/**`,
|
|
13
|
-
* `test/**`). */
|
|
14
|
-
include?: string[];
|
|
15
|
-
/** Override the watcher's exclude globs. Defaults to `coverage.exclude`
|
|
16
|
-
* when set, otherwise the watch defaults (`node_modules/**`,
|
|
17
|
-
* `.helix-coverage/**`, etc.). */
|
|
18
|
-
exclude?: string[];
|
|
19
|
-
/** Optional `AbortSignal` to drive shutdown without a real SIGINT —
|
|
20
|
-
* primarily a test seam. */
|
|
21
|
-
signal?: AbortSignal;
|
|
22
|
-
}
|