@ultimat3/cli 22.6.2 → 22.7.1

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.
@@ -2,13 +2,16 @@
2
2
  // files belong to a type and this one owns what happens to them once selected — a wrong file list
3
3
  // is never a race, and a race is never a selection bug.
4
4
 
5
+ // why: Bun ships no path-join primitive; the timings cache lives under the run's root.
6
+ import { join } from 'node:path';
5
7
  import type { Runner } from './exec';
6
8
  import type { Finding } from './output';
7
9
  import { runBatches, testBatches } from './test-batches';
8
10
  import { countsOf } from './test-counts';
9
11
  import { testEnvOverrides } from './test-dotenv';
10
12
  import type { TestFile } from './test-select';
11
- import { failureOf, testArgs } from './test-shards';
13
+ import { failureOf, ISOLATED_TEST_ENV, TEST_TIMINGS_FILE, testArgs } from './test-shards';
14
+ import { BATCH_FILES_PER_WORKER, SHARED_BATCH_FILES_PER_WORKER } from './test-workers';
12
15
  import type { StepOutcome } from './verify-step';
13
16
  // Type-only, so nothing here evaluates verify-tests.ts and the two files cannot form a cycle.
14
17
  import type { TestType } from './verify-tests';
@@ -23,6 +26,12 @@ export interface ParallelRunOptions {
23
26
  readonly type: TestType;
24
27
  /** This process's own environment. Optional and defaulted to `Bun.env` — see `VerifyContext.env`. */
25
28
  readonly env?: Readonly<Record<string, string | undefined>>;
29
+ /** A fresh global per file — `x.verify.json` `"isolate": true` or `--isolate`. Default off. */
30
+ readonly isolate?: boolean;
31
+ /** Why the width is what it is — printed on the step line. */
32
+ readonly widthReason?: string;
33
+ /** The machine-pool lease (`test-slots.ts`), for a default-width run. */
34
+ readonly lease?: (want: number) => Promise<{ readonly count: number; release(): void }>;
26
35
  }
27
36
 
28
37
  /**
@@ -42,14 +51,29 @@ export interface ParallelRunOptions {
42
51
  export async function runParallel(options: ParallelRunOptions): Promise<StepOutcome> {
43
52
  const files = options.files.map((file) => file.path);
44
53
  const workers = Math.max(1, Math.min(Math.trunc(options.workers), files.length || 1));
45
- const envOverrides = testEnvOverrides(options.root, options.env ?? Bun.env);
54
+ const envOverrides = {
55
+ ...testEnvOverrides(options.root, options.env ?? Bun.env),
56
+ ...(options.isolate === true ? { [ISOLATED_TEST_ENV]: '1' } : {}),
57
+ };
46
58
  // Batched exactly as `x test` batches the same selection at the same width, so the gate's
47
59
  // `fix:` line — `x test <type> --workers N` — reruns the very processes that failed.
48
- const batches = testBatches(files, workers);
60
+ const batches = testBatches(
61
+ files,
62
+ workers,
63
+ options.isolate === true ? BATCH_FILES_PER_WORKER : SHARED_BATCH_FILES_PER_WORKER,
64
+ );
49
65
  const result = await runBatches({
50
66
  runner: options.runner,
51
67
  batches,
52
- argsFor: (batch) => testArgs({ files: batch, workers: Math.min(workers, batch.length) }),
68
+ workers,
69
+ ...(options.lease === undefined || workers === 1 ? {} : { lease: options.lease }),
70
+ argsFor: (batch, width) =>
71
+ testArgs({
72
+ files: batch,
73
+ workers: width,
74
+ isolate: options.isolate === true,
75
+ timings: join(options.root, TEST_TIMINGS_FILE),
76
+ }),
53
77
  options: {
54
78
  cwd: options.root,
55
79
  ...(Object.keys(envOverrides).length === 0 ? {} : { env: envOverrides }),
@@ -60,11 +84,28 @@ export async function runParallel(options: ParallelRunOptions): Promise<StepOutc
60
84
  // to drift — and the one that drifts is the gate's, which is the one an agent reads first.
61
85
  const findings: readonly Finding[] = result.ok
62
86
  ? []
63
- : [failureOf(result.code, files.length, { workers, type: options.type })];
87
+ : [
88
+ failureOf(result.code, files.length, {
89
+ workers,
90
+ type: options.type,
91
+ ...(options.isolate === true ? { isolate: true } : {}),
92
+ }),
93
+ ];
94
+ // The width the run REACHED: a batch the machine pool narrowed ran narrower than planned, and a
95
+ // step line that printed the plan would name a width that never ran.
96
+ const reached = Math.max(1, ...result.widths);
97
+ const narrowed = reached < workers;
64
98
  return {
65
99
  ok: findings.length === 0,
66
100
  findings,
67
- workers,
101
+ workers: reached,
102
+ ...(options.widthReason === undefined
103
+ ? {}
104
+ : {
105
+ widthReason: narrowed
106
+ ? `${String(reached)} of ${options.widthReason} — machine slots held by another run`
107
+ : options.widthReason,
108
+ }),
68
109
  // Per batch: each process printed its own summary, and one parse of the joined text would
69
110
  // read only the last one.
70
111
  tests: countsOf(result.results),
@@ -18,8 +18,11 @@ import { countsOf } from './test-counts';
18
18
  import { testEnvOverrides } from './test-dotenv';
19
19
  import type { TestFile } from './test-select';
20
20
  import { discoverTests } from './test-select';
21
- import { defaultWorkers, SERIAL_TYPES } from './test-workers';
21
+ import { machineLease } from './test-slots';
22
+ import type { WorkerPlan } from './test-workers';
23
+ import { availableCpus, SERIAL_TYPES, totalMemory, workerPlan } from './test-workers';
22
24
  import { withE2eApp } from './verify-e2e';
25
+ import { shardFiles } from './verify-shard';
23
26
  import type { StepOutcome, VerifyContext, VerifyStep } from './verify-step';
24
27
  import { fromExec, fromFindings } from './verify-step';
25
28
  import { runParallel } from './verify-test-run';
@@ -212,16 +215,48 @@ const runSerial = async (ctx: VerifyContext, type: TestType): Promise<StepOutcom
212
215
 
213
216
  const runType = async (ctx: VerifyContext, type: TestType): Promise<StepOutcome> => {
214
217
  if (isSerial(type)) return runSerial(ctx, type);
215
- const files = await filesFor(ctx.root, type);
218
+ const discoveredFiles = await filesFor(ctx.root, type);
219
+ // Under `--shard`, this job runs its slice and says which — `x verify merge` checks the slices.
220
+ // An EMPTY slice is a pass with zero tests, never the serial fallback below (which would run
221
+ // the whole type): the zero-tests floor is `merge`'s to apply, on the summed counts.
222
+ const shard =
223
+ ctx.shard === undefined
224
+ ? undefined
225
+ : shardFiles(
226
+ discoveredFiles.map((file) => file.path),
227
+ ctx.shard,
228
+ ctx.shard.timings,
229
+ );
230
+ if (shard !== undefined && shard.files.length === 0) {
231
+ return { ok: true, findings: [], workers: 0, tests: { ran: 0, skipped: 0 }, shard };
232
+ }
233
+ const files =
234
+ shard === undefined
235
+ ? discoveredFiles
236
+ : discoveredFiles.filter((file) => shard.files.includes(file.path));
216
237
  if (files.length === 0) return runSerial(ctx, type);
217
- return runParallel({
238
+ const env = ctx.env ?? Bun.env;
239
+ const plan =
240
+ ctx.workers === undefined ? workerPlan(availableCpus(), totalMemory(), env) : undefined;
241
+ const workers = ctx.workers ?? (plan as WorkerPlan).workers;
242
+ const lease = plan === undefined ? undefined : machineLease(plan.workers, env);
243
+ const outcome = await runParallel({
218
244
  root: ctx.root,
219
245
  runner: ctx.runner,
220
246
  files,
221
- workers: ctx.workers ?? defaultWorkers(),
247
+ workers,
222
248
  type,
249
+ isolate: ctx.isolate === true,
250
+ widthReason:
251
+ plan === undefined
252
+ ? `${String(Math.min(workers, files.length))} workers (--workers)`
253
+ : files.length < plan.workers
254
+ ? `${String(files.length)} of ${plan.reason}`
255
+ : plan.reason,
256
+ ...(lease === undefined ? {} : { lease }),
223
257
  ...(ctx.env === undefined ? {} : { env: ctx.env }),
224
258
  });
259
+ return shard === undefined ? outcome : { ...outcome, shard };
225
260
  };
226
261
 
227
262
  const isApp = (root: string): boolean => existsSync(join(root, APP_CONFIG_FILE));