@celilo/e2e 0.19.3 → 0.20.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.
- package/README.md +30 -13
- package/bin/e2e-bake-management +196 -12
- package/bin/e2e-infra +0 -1
- package/bin/e2e-up +14 -3
- package/docker/Dockerfile.observer +12 -1
- package/docker/Dockerfile.target-machine +22 -1
- package/npm-registry-server/package.json +1 -1
- package/package.json +3 -3
- package/registry-server/package.json +1 -1
- package/scripts/pack-celilo-packages.ts +15 -0
- package/src/block-timing.test.ts +559 -0
- package/src/block-timing.ts +366 -0
- package/src/cli/build.test.ts +135 -4
- package/src/cli/build.ts +237 -94
- package/src/cli/command-registry.ts +21 -0
- package/src/cli/command-tree-parser.ts +11 -2
- package/src/cli/completion.ts +9 -0
- package/src/cli/host.ts +252 -0
- package/src/cli/index.ts +78 -51
- package/src/cli/module-discovery.ts +108 -13
- package/src/cli/scaffold.ts +18 -26
- package/src/container-manager.cleanup.test.ts +284 -0
- package/src/container-manager.runner.test.ts +351 -0
- package/src/container-manager.test.ts +84 -0
- package/src/container-manager.ts +721 -185
- package/src/docker-compose-generator.ts +135 -61
- package/src/doctor.test.ts +259 -4
- package/src/doctor.ts +276 -3
- package/src/exit-cleanup.test.ts +83 -1
- package/src/fleet-nameserver-gate.test.ts +45 -0
- package/src/host-vm.test.ts +156 -0
- package/src/host-vm.ts +230 -0
- package/src/index.ts +11 -0
- package/src/live-stack.test.ts +300 -0
- package/src/live-stack.ts +355 -0
- package/src/no-unjustified-sleep.test.ts +90 -0
- package/src/proxmox-provisioner.test.ts +18 -2
- package/src/proxmox-provisioner.ts +22 -0
- package/src/public-sim-routes.test.ts +9 -2
- package/src/repo-root.ts +33 -0
- package/src/run-args.test.ts +76 -0
- package/src/run-args.ts +89 -0
- package/src/runner.ts +213 -8
- package/src/shared-infra.ts +88 -32
- package/src/socks-proxy.ts +2 -0
- package/src/source-fingerprint.test.ts +213 -0
- package/src/source-fingerprint.ts +212 -0
- package/src/stage-simulator-inputs.ts +93 -0
- package/src/stages.ts +133 -0
- package/src/wait-for-run.ts +1 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recurrence gate for celilo#1267: readiness waits in packages/e2e must poll
|
|
3
|
+
* an observable condition (waitFor or a poll loop), never sleep a fixed
|
|
4
|
+
* duration.
|
|
5
|
+
*
|
|
6
|
+
* apps/celilo/CLAUDE.md prohibits sleeping to mask race conditions, but until
|
|
7
|
+
* now nothing enforced it in this package. respondWith slept 500ms for the
|
|
8
|
+
* responder to boot; on a loaded host that was not enough, the next command
|
|
9
|
+
* hit an unanswered interview, and the error read as a missing fixture value
|
|
10
|
+
* rather than as harness timing.
|
|
11
|
+
*
|
|
12
|
+
* The rule here: every fixed sleep in packages/e2e/src must carry an inline
|
|
13
|
+
* `e2e-sleep-ok:` justification on the same line or the line above. A poll
|
|
14
|
+
* loop's cadence sleep carries one honestly; a bare readiness gate cannot, so
|
|
15
|
+
* it fails this gate instead of failing five suites under load.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, expect, test } from 'bun:test';
|
|
19
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
20
|
+
import { join, resolve } from 'node:path';
|
|
21
|
+
|
|
22
|
+
const SLEEP_PATTERN = /new Promise\(\s*\(?r\)?\s*=>\s*setTimeout\(r\s*,/;
|
|
23
|
+
|
|
24
|
+
function srcDir(): string {
|
|
25
|
+
let dir = import.meta.dir;
|
|
26
|
+
for (let i = 0; i < 8; i++) {
|
|
27
|
+
if (existsSync(join(dir, 'apps')) && existsSync(join(dir, 'modules')))
|
|
28
|
+
return join(dir, 'packages', 'e2e', 'src');
|
|
29
|
+
dir = resolve(dir, '..');
|
|
30
|
+
}
|
|
31
|
+
throw new Error('could not locate repo root (no ancestor with apps/ + modules/)');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function tsFiles(dir: string): string[] {
|
|
35
|
+
const out: string[] = [];
|
|
36
|
+
for (const entry of readdirSync(dir)) {
|
|
37
|
+
const full = join(dir, entry);
|
|
38
|
+
if (statSync(full).isDirectory()) out.push(...tsFiles(full));
|
|
39
|
+
else if (entry.endsWith('.ts') && !entry.endsWith('.test.ts')) out.push(full);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Violation {
|
|
45
|
+
file: string;
|
|
46
|
+
line: number;
|
|
47
|
+
text: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function unjustifiedSleeps(): Violation[] {
|
|
51
|
+
const violations: Violation[] = [];
|
|
52
|
+
for (const file of tsFiles(srcDir())) {
|
|
53
|
+
const lines = readFileSync(file, 'utf-8').split('\n');
|
|
54
|
+
lines.forEach((text, i) => {
|
|
55
|
+
if (!SLEEP_PATTERN.test(text)) return;
|
|
56
|
+
const justified = text.includes('e2e-sleep-ok:') || lines[i - 1]?.includes('e2e-sleep-ok:');
|
|
57
|
+
if (!justified) violations.push({ file, line: i + 1, text: text.trim() });
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return violations;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('recurrence gate: no bare readiness sleeps in packages/e2e (celilo#1267)', () => {
|
|
64
|
+
test('the scan reaches a non-trivial set of files (sanity — it actually ran)', () => {
|
|
65
|
+
const files = tsFiles(srcDir());
|
|
66
|
+
expect(files.length).toBeGreaterThan(10);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('every fixed sleep carries an e2e-sleep-ok justification', () => {
|
|
70
|
+
const violations = unjustifiedSleeps();
|
|
71
|
+
const report = violations
|
|
72
|
+
.map(
|
|
73
|
+
(v) =>
|
|
74
|
+
`${v.file}:${v.line}: ${v.text}\n Add an inline \`e2e-sleep-ok: <reason>\` comment on this line or the one above, or poll an observable condition with waitFor.`,
|
|
75
|
+
)
|
|
76
|
+
.join('\n');
|
|
77
|
+
expect(violations, `Unjustified fixed sleeps:\n${report}`).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('the known poll-cadence sleeps are still present and justified (the rule engages)', () => {
|
|
81
|
+
// Reach probe, not reasoning: if the justified count drops to zero the
|
|
82
|
+
// scan is matching nothing and the gate above proves nothing.
|
|
83
|
+
const violations = unjustifiedSleeps();
|
|
84
|
+
const total = tsFiles(srcDir()).reduce((count, file) => {
|
|
85
|
+
const lines = readFileSync(file, 'utf-8').split('\n');
|
|
86
|
+
return count + lines.filter((text) => SLEEP_PATTERN.test(text)).length;
|
|
87
|
+
}, 0);
|
|
88
|
+
expect(total - violations.length).toBeGreaterThan(3);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -9,6 +9,7 @@ import { describe, expect, test } from 'bun:test';
|
|
|
9
9
|
import type { GuestRecord } from '@celilo/terraform-fake';
|
|
10
10
|
import {
|
|
11
11
|
CONTAINER_PREFIX,
|
|
12
|
+
GUEST_PROJECT_LABEL,
|
|
12
13
|
containerNameFor,
|
|
13
14
|
createDockerProvisioner,
|
|
14
15
|
parseNet,
|
|
@@ -60,8 +61,10 @@ describe('createGuest', () => {
|
|
|
60
61
|
});
|
|
61
62
|
|
|
62
63
|
test('names the container so the debris sweep can find it', async () => {
|
|
63
|
-
// cele2e doctor
|
|
64
|
-
//
|
|
64
|
+
// cele2e doctor keys off this prefix; an unprefixed container leaks
|
|
65
|
+
// silently between runs. Project membership is a separate concern — the
|
|
66
|
+
// per-test sweeps cannot match this name (no timestamp in it), so the
|
|
67
|
+
// label test below is what teardown actually keys off.
|
|
65
68
|
const { runner, find } = stubDocker();
|
|
66
69
|
await provisionerWith(runner).createGuest(guest());
|
|
67
70
|
|
|
@@ -70,6 +73,19 @@ describe('createGuest', () => {
|
|
|
70
73
|
expect(name).toBe('celilo-e2e-lxc-203');
|
|
71
74
|
});
|
|
72
75
|
|
|
76
|
+
test('labels the guest with its compose project so teardown can find it (celilo#1247)', async () => {
|
|
77
|
+
// The name cannot carry project membership (no timestamp), and compose's
|
|
78
|
+
// own labels cannot be borrowed (the guest is not a compose service, and
|
|
79
|
+
// compose down removes orphans only on the full label set — verified:
|
|
80
|
+
// a bare com.docker.compose.project label is not enough). The explicit
|
|
81
|
+
// label is what projectTeardownCommands lists guests by.
|
|
82
|
+
const { runner, find } = stubDocker();
|
|
83
|
+
await provisionerWith(runner).createGuest(guest());
|
|
84
|
+
|
|
85
|
+
const run = find('run') ?? [];
|
|
86
|
+
expect(run[run.indexOf('--label') + 1]).toBe(`${GUEST_PROJECT_LABEL}=celilo-e2e-test`);
|
|
87
|
+
});
|
|
88
|
+
|
|
73
89
|
test('passes the gateway through, since target-setup routes from it', async () => {
|
|
74
90
|
const { runner, find } = stubDocker();
|
|
75
91
|
await provisionerWith(runner).createGuest(guest());
|
|
@@ -30,6 +30,20 @@ import { ZONE_GATEWAYS, type Zone } from './types';
|
|
|
30
30
|
*/
|
|
31
31
|
export const CONTAINER_PREFIX = 'celilo-e2e-';
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Marks a sim-created guest as belonging to a compose project.
|
|
35
|
+
*
|
|
36
|
+
* The name prefix alone is not enough for cleanup: a guest is named
|
|
37
|
+
* `celilo-e2e-lxc-<vmid>` with no timestamp in it, so every by-name filter the
|
|
38
|
+
* sweeps use (`celilo-e2e-1<ts>`, `celilo-e2e-<ts>`) misses it. A guest left
|
|
39
|
+
* behind keeps its zone network's endpoint alive, the network rm fails with
|
|
40
|
+
* "has active endpoints", and the subnet stays allocated — every later suite
|
|
41
|
+
* in the run then dies creating that network (celilo#1247). The label is what
|
|
42
|
+
* `projectTeardownCommands` filters on; it carries the project value, so
|
|
43
|
+
* teardown finds exactly the guests of the project it is tearing down.
|
|
44
|
+
*/
|
|
45
|
+
export const GUEST_PROJECT_LABEL = 'celilo-e2e.project';
|
|
46
|
+
|
|
33
47
|
/** Shells out to `docker`. Injected so the logic is testable without a daemon. */
|
|
34
48
|
export type DockerRunner = (args: string[]) => string;
|
|
35
49
|
|
|
@@ -71,6 +85,7 @@ export function zoneForGateway(gateway: string): Zone | undefined {
|
|
|
71
85
|
export function createDockerProvisioner(options: DockerProvisionerOptions) {
|
|
72
86
|
const docker = options.docker ?? realDocker;
|
|
73
87
|
const readyTimeoutMs = options.readyTimeoutMs ?? 60_000;
|
|
88
|
+
// e2e-sleep-ok: injectable test seam; real callers poll a condition between sleeps.
|
|
74
89
|
const sleep = options.sleep ?? ((ms: number) => new Promise((r) => setTimeout(r, ms)));
|
|
75
90
|
|
|
76
91
|
const assertOurs = (name: string): void => {
|
|
@@ -96,6 +111,12 @@ export function createDockerProvisioner(options: DockerProvisionerOptions) {
|
|
|
96
111
|
const name = containerNameFor(guest.vmid);
|
|
97
112
|
assertOurs(name);
|
|
98
113
|
|
|
114
|
+
// Project membership must be discoverable without the name: teardown
|
|
115
|
+
// matches on this label because the guest's name carries no timestamp.
|
|
116
|
+
// Added before the readiness wait, so even a guest that never finishes
|
|
117
|
+
// booting is still findable and removable.
|
|
118
|
+
const projectLabel = ['--label', `${GUEST_PROJECT_LABEL}=${options.project}`] as const;
|
|
119
|
+
|
|
99
120
|
// Same image selection rule the compose generator uses for machines: the
|
|
100
121
|
// app zone needs a dockerd-capable box.
|
|
101
122
|
const image =
|
|
@@ -106,6 +127,7 @@ export function createDockerProvisioner(options: DockerProvisionerOptions) {
|
|
|
106
127
|
'-d',
|
|
107
128
|
'--name',
|
|
108
129
|
name,
|
|
130
|
+
...projectLabel,
|
|
109
131
|
'--hostname',
|
|
110
132
|
guest.hostname,
|
|
111
133
|
'--network',
|
|
@@ -34,7 +34,7 @@ const ISP_EDGE = '100.64.0.1';
|
|
|
34
34
|
const EDGE_SERVICE = 'fw-ext';
|
|
35
35
|
|
|
36
36
|
interface ComposeService {
|
|
37
|
-
|
|
37
|
+
image?: string;
|
|
38
38
|
networks?: Record<string, unknown>;
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -61,7 +61,14 @@ function publicSimulators(yaml: string): Array<{ name: string; dockerfile: strin
|
|
|
61
61
|
const compose = parse(yaml) as { services?: Record<string, ComposeService> };
|
|
62
62
|
return Object.entries(compose.services ?? {})
|
|
63
63
|
.filter(([name, svc]) => name !== EDGE_SERVICE && svc.networks?.['internet-external'])
|
|
64
|
-
.map(([name, svc]) => ({
|
|
64
|
+
.map(([name, svc]) => ({
|
|
65
|
+
name,
|
|
66
|
+
// The compose carries the tag, not the Dockerfile: the bake convention
|
|
67
|
+
// is tag `celilo-e2e/<name>` <-> `docker/Dockerfile.<name>` (build-infra
|
|
68
|
+
// tags every Dockerfile exactly that way). A drift in the convention
|
|
69
|
+
// makes the reads below fail loudly, which is what we want.
|
|
70
|
+
dockerfile: `docker/Dockerfile.${(svc.image ?? '').replace('celilo-e2e/', '').replace(/:.*/, '')}`,
|
|
71
|
+
}));
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
/**
|
package/src/repo-root.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the celilo monorepo is, if we are inside one.
|
|
3
|
+
*
|
|
4
|
+
* Its own module because two unrelated consumers need it and neither may
|
|
5
|
+
* import the other: `cli/build.ts` uses it to find modules to package, and
|
|
6
|
+
* `doctor.ts` uses it to fingerprint the source a baked image came from —
|
|
7
|
+
* and `build.ts` already imports `doctor.ts`, so the reverse edge would be a
|
|
8
|
+
* cycle. Re-exported from `cli/build.ts` so its existing callers and its
|
|
9
|
+
* recurrence test are unchanged.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { join, resolve } from 'node:path';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Walk up from `startDir` to the celilo checkout, or null.
|
|
17
|
+
*
|
|
18
|
+
* The marker is `apps/celilo/package.json` and must stay that specific
|
|
19
|
+
* (ce-dbc): keying on a bare `modules/` directory made a stray empty
|
|
20
|
+
* `node_modules/modules` from a botched `bun add -g` read as the monorepo
|
|
21
|
+
* root, which skipped the published-staging path and crashed the packer for
|
|
22
|
+
* every npm consumer.
|
|
23
|
+
*/
|
|
24
|
+
export function findMonorepoRoot(startDir: string): string | null {
|
|
25
|
+
let dir = startDir;
|
|
26
|
+
for (let i = 0; i < 10; i++) {
|
|
27
|
+
if (existsSync(join(dir, 'apps', 'celilo', 'package.json'))) return dir;
|
|
28
|
+
const parent = resolve(dir, '..');
|
|
29
|
+
if (parent === dir) break;
|
|
30
|
+
dir = parent;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { KNOWN_RUN_FLAGS, filterTestFilesByPatterns, parseRunArgs } from './run-args';
|
|
3
|
+
|
|
4
|
+
describe('parseRunArgs', () => {
|
|
5
|
+
test('--help is recognized as help, never discarded into run-everything', () => {
|
|
6
|
+
const parsed = parseRunArgs(['--help']);
|
|
7
|
+
expect(parsed.helpRequested).toBe(true);
|
|
8
|
+
expect(parsed.unknownFlags).toEqual([]);
|
|
9
|
+
expect(parsed.patterns).toEqual([]);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('every known flag is accepted and yields no patterns', () => {
|
|
13
|
+
const parsed = parseRunArgs([...KNOWN_RUN_FLAGS, '--seed=42']);
|
|
14
|
+
expect(parsed.unknownFlags).toEqual([]);
|
|
15
|
+
expect(parsed.patterns).toEqual([]);
|
|
16
|
+
expect(parsed.helpRequested).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('positional patterns survive flag parsing in order', () => {
|
|
20
|
+
const parsed = parseRunArgs(['deploy', '--keep', 'caddy', '--verbose']);
|
|
21
|
+
expect(parsed.patterns).toEqual(['deploy', 'caddy']);
|
|
22
|
+
expect(parsed.unknownFlags).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('an unrecognised --flag is reported, not silently discarded', () => {
|
|
26
|
+
// The old runner stripped every --arg it did not know, leaving zero
|
|
27
|
+
// patterns: run everything. `--al` must error, not launch the catalogue.
|
|
28
|
+
const parsed = parseRunArgs(['--al', '--dry-run']);
|
|
29
|
+
expect(parsed.unknownFlags).toEqual(['--al', '--dry-run']);
|
|
30
|
+
expect(parsed.patterns).toEqual([]);
|
|
31
|
+
expect(parsed.helpRequested).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('the CLI discovery aliases are unknown to the runner', () => {
|
|
35
|
+
// `cele2e run` strips --all/--all-modules before exec. If one ever
|
|
36
|
+
// reaches the runner directly it must error, not run everything.
|
|
37
|
+
const parsed = parseRunArgs(['--all', '--all-modules']);
|
|
38
|
+
expect(parsed.unknownFlags).toEqual(['--all', '--all-modules']);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('filterTestFilesByPatterns', () => {
|
|
43
|
+
const files = [
|
|
44
|
+
'/mods/knot/e2e/aspect-fanout.test.ts',
|
|
45
|
+
'/top/tests/aspect-fanout-new-systems.test.ts',
|
|
46
|
+
'/top/tests/caddy-internal-private.test.ts',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
test('an exact pattern selects only its own file, not the sibling it prefixes', () => {
|
|
50
|
+
// The old substring-OR filter dragged aspect-fanout-new-systems into
|
|
51
|
+
// `cele2e run aspect-fanout caddy-internal-private` whenever the
|
|
52
|
+
// top-level dir was in the collected set.
|
|
53
|
+
const picked = filterTestFilesByPatterns(files, ['aspect-fanout', 'caddy-internal-private']);
|
|
54
|
+
expect(picked).toEqual([
|
|
55
|
+
'/mods/knot/e2e/aspect-fanout.test.ts',
|
|
56
|
+
'/top/tests/caddy-internal-private.test.ts',
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('a pattern with no exact match keeps substring semantics', () => {
|
|
61
|
+
const picked = filterTestFilesByPatterns(files, ['fanout-new']);
|
|
62
|
+
expect(picked).toEqual(['/top/tests/aspect-fanout-new-systems.test.ts']);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('several patterns union their matches', () => {
|
|
66
|
+
const picked = filterTestFilesByPatterns(files, ['aspect-fanout-new-systems', 'aspect-fanout']);
|
|
67
|
+
expect(picked).toEqual([
|
|
68
|
+
'/mods/knot/e2e/aspect-fanout.test.ts',
|
|
69
|
+
'/top/tests/aspect-fanout-new-systems.test.ts',
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('no patterns selects everything', () => {
|
|
74
|
+
expect(filterTestFilesByPatterns(files, [])).toEqual(files);
|
|
75
|
+
});
|
|
76
|
+
});
|
package/src/run-args.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Argument parsing for the e2e run script (runner.ts). Pure and side-effect
|
|
5
|
+
* free, so the runner's argv contract is unit-testable without touching
|
|
6
|
+
* Docker, the run-lock, or anything else main() reaches at import time.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Flags `e2e-run` understands. `--seed=<n>` is value-carrying and recognized
|
|
11
|
+
* by prefix, not listed here.
|
|
12
|
+
*/
|
|
13
|
+
export const KNOWN_RUN_FLAGS: readonly string[] = [
|
|
14
|
+
'--keep',
|
|
15
|
+
'--reuse',
|
|
16
|
+
'--live',
|
|
17
|
+
'--published',
|
|
18
|
+
'--source-cli',
|
|
19
|
+
'--verbose',
|
|
20
|
+
'--ci',
|
|
21
|
+
'--no-ci',
|
|
22
|
+
'--complete',
|
|
23
|
+
// Passed through by `cele2e run` (harmless/no-op there, see cli/index.ts).
|
|
24
|
+
'--ci-safe',
|
|
25
|
+
'--notify',
|
|
26
|
+
'--shuffle',
|
|
27
|
+
'--no-interactive',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export interface ParsedRunArgs {
|
|
31
|
+
/** Positional suite-name patterns (substring match against test file names). */
|
|
32
|
+
patterns: string[];
|
|
33
|
+
/** `--`-prefixed arguments the runner does not know. Must be empty before a run starts. */
|
|
34
|
+
unknownFlags: string[];
|
|
35
|
+
/** True when `--help` was requested. */
|
|
36
|
+
helpRequested: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Split e2e-run argv into patterns and flags. Reports unrecognized `--` args
|
|
41
|
+
* in `unknownFlags` instead of discarding them: the old filter stripped every
|
|
42
|
+
* `--`-prefixed argument it did not know, so `cele2e run --help` (and any
|
|
43
|
+
* typo'd flag) left zero patterns, which is the runner's encoding of "run
|
|
44
|
+
* everything" — the one command an unsure operator types took the single most
|
|
45
|
+
* expensive resource in the city.
|
|
46
|
+
*/
|
|
47
|
+
export function parseRunArgs(argv: readonly string[]): ParsedRunArgs {
|
|
48
|
+
const patterns: string[] = [];
|
|
49
|
+
const unknownFlags: string[] = [];
|
|
50
|
+
let helpRequested = false;
|
|
51
|
+
for (const arg of argv) {
|
|
52
|
+
if (!arg.startsWith('--')) {
|
|
53
|
+
patterns.push(arg);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (arg === '--help') {
|
|
57
|
+
helpRequested = true;
|
|
58
|
+
} else if (!isKnownFlag(arg)) {
|
|
59
|
+
unknownFlags.push(arg);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { patterns, unknownFlags, helpRequested };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Filter collected test files by the run's positional patterns. A pattern
|
|
67
|
+
* that exactly equals a test name selects only that file; otherwise the old
|
|
68
|
+
* substring semantics apply. Without the exact rule, `cele2e run
|
|
69
|
+
* aspect-fanout caddy-internal-private` also ran aspect-fanout-new-systems:
|
|
70
|
+
* the substring OR matched the prefix whenever the top-level dir was in the
|
|
71
|
+
* collected set.
|
|
72
|
+
*/
|
|
73
|
+
export function filterTestFilesByPatterns(
|
|
74
|
+
files: readonly string[],
|
|
75
|
+
patterns: readonly string[],
|
|
76
|
+
): string[] {
|
|
77
|
+
if (patterns.length === 0) return [...files];
|
|
78
|
+
const exactPatterns = new Set(
|
|
79
|
+
patterns.filter((p) => files.some((f) => basename(f, '.test.ts') === p)),
|
|
80
|
+
);
|
|
81
|
+
return files.filter((f) => {
|
|
82
|
+
const name = basename(f, '.test.ts');
|
|
83
|
+
return patterns.some((p) => (exactPatterns.has(p) ? name === p : name.includes(p)));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isKnownFlag(arg: string): boolean {
|
|
88
|
+
return KNOWN_RUN_FLAGS.includes(arg) || arg.startsWith('--seed=');
|
|
89
|
+
}
|