@celilo/e2e 0.19.2 → 0.20.0
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 +171 -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 +54 -4
- package/src/cli/build.ts +204 -88
- 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 +184 -0
- package/src/live-stack.ts +145 -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 +83 -32
- package/src/socks-proxy.ts +2 -0
- package/src/source-fingerprint.test.ts +213 -0
- package/src/source-fingerprint.ts +201 -0
- package/src/stage-simulator-inputs.ts +93 -0
- package/src/stages.ts +133 -0
- package/src/wait-for-run.ts +1 -0
package/src/cli/host.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cele2e host` — bring the Docker host up and down in the shape the rig needs.
|
|
3
|
+
*
|
|
4
|
+
* On Linux this is a no-op: docker runs on the host kernel. On macOS docker
|
|
5
|
+
* runs inside a colima VM whose memory and mount transport dominate how long a
|
|
6
|
+
* suite takes, and both are set at `colima start` — so "start colima" is not a
|
|
7
|
+
* neutral act, and everyone who typed it by hand got whatever colima's defaults
|
|
8
|
+
* or the last person's flags left behind. That is what this command replaces:
|
|
9
|
+
* one invocation, one policy, derived from the host it is running on.
|
|
10
|
+
*
|
|
11
|
+
* Three verbs, and the split between them is deliberate.
|
|
12
|
+
*
|
|
13
|
+
* `status` reads and reports. No side effects.
|
|
14
|
+
* `up` starts the VM, or reports precisely why a running one cannot be
|
|
15
|
+
* brought into policy. It NEVER destroys anything.
|
|
16
|
+
* `reset` destroys the VM and recreates it. The only way to change the
|
|
17
|
+
* mount type, and it costs the whole image cache, so it demands
|
|
18
|
+
* `--yes` and refuses while the run-lock is held.
|
|
19
|
+
*
|
|
20
|
+
* `up` cannot fix a wrong mount type, and pretending otherwise would be worse
|
|
21
|
+
* than useless: colima accepts `--mount-type` on an existing VM, prints
|
|
22
|
+
* `'volume mount type' cannot be updated after initial setup, discarded`, and
|
|
23
|
+
* carries on with the old one. A command that appeared to fix it and did not
|
|
24
|
+
* would be exactly the "check that cannot reach its subject" failure this rig
|
|
25
|
+
* has a whole section about.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { spawnSync } from 'node:child_process';
|
|
29
|
+
import {
|
|
30
|
+
type HostVmBudget,
|
|
31
|
+
activeProfile,
|
|
32
|
+
evaluateHostVm,
|
|
33
|
+
readHostFacts,
|
|
34
|
+
readHostVmFacts,
|
|
35
|
+
recommendedBudget,
|
|
36
|
+
} from '../host-vm';
|
|
37
|
+
import { lockStatus } from '../run-lock';
|
|
38
|
+
|
|
39
|
+
const bold = '\x1b[1m';
|
|
40
|
+
const dim = '\x1b[2m';
|
|
41
|
+
const green = '\x1b[32m';
|
|
42
|
+
const red = '\x1b[31m';
|
|
43
|
+
const yellow = '\x1b[33m';
|
|
44
|
+
const reset = '\x1b[0m';
|
|
45
|
+
|
|
46
|
+
/** Is there a colima binary to drive? */
|
|
47
|
+
function haveColima(): boolean {
|
|
48
|
+
return spawnSync('colima', ['version'], { stdio: 'ignore' }).status === 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function colimaRunning(profile: string): boolean {
|
|
52
|
+
return spawnSync('colima', ['status', '-p', profile], { stdio: 'ignore' }).status === 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The `colima start` a fresh VM should get.
|
|
57
|
+
*
|
|
58
|
+
* `vz` (Apple Virtualization) rather than qemu, because virtiofs needs it.
|
|
59
|
+
* Exported so the docs and the tests quote the same string the command runs
|
|
60
|
+
* rather than a transcription of it.
|
|
61
|
+
*/
|
|
62
|
+
export function colimaStartArgs(budget: HostVmBudget, profile: string): string[] {
|
|
63
|
+
return [
|
|
64
|
+
'start',
|
|
65
|
+
'-p',
|
|
66
|
+
profile,
|
|
67
|
+
'--vm-type',
|
|
68
|
+
'vz',
|
|
69
|
+
'--cpu',
|
|
70
|
+
String(budget.cpus),
|
|
71
|
+
'--memory',
|
|
72
|
+
String(budget.memoryGiB),
|
|
73
|
+
'--mount-type',
|
|
74
|
+
budget.mountType,
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function reportStatus(): number {
|
|
79
|
+
const profile = activeProfile();
|
|
80
|
+
const host = readHostFacts();
|
|
81
|
+
const facts = readHostVmFacts(profile);
|
|
82
|
+
|
|
83
|
+
console.log(`${bold}=== Docker host ===${reset}`);
|
|
84
|
+
console.log(` host ${host.cpus} CPU, ${host.memoryGiB} GiB`);
|
|
85
|
+
|
|
86
|
+
if (!facts) {
|
|
87
|
+
console.log(' VM none — docker runs natively, no host policy applies');
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const budget = recommendedBudget(host, facts.vmType);
|
|
92
|
+
console.log(
|
|
93
|
+
` VM colima/${facts.profile} (${facts.vmType}): ${facts.cpus} CPU, ${facts.memoryGiB} GiB, ${facts.mountType} mounts`,
|
|
94
|
+
);
|
|
95
|
+
console.log(
|
|
96
|
+
` policy ${budget.cpus} CPU, ${budget.memoryGiB} GiB, ${budget.mountType} mounts`,
|
|
97
|
+
);
|
|
98
|
+
console.log(` running ${colimaRunning(profile) ? 'yes' : 'no'}`);
|
|
99
|
+
console.log('');
|
|
100
|
+
|
|
101
|
+
const { problems, needsRecreate } = evaluateHostVm(facts, host, budget);
|
|
102
|
+
if (problems.length === 0) {
|
|
103
|
+
console.log(`${green}The VM matches the policy.${reset}`);
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
for (const problem of problems) console.log(` ${yellow}!${reset} ${problem}`);
|
|
107
|
+
console.log('');
|
|
108
|
+
console.log(
|
|
109
|
+
needsRecreate
|
|
110
|
+
? ` fix: ${bold}cele2e host reset --yes${reset} ${dim}(destroys the VM and its image cache; budget one \`cele2e build-infra\`)${reset}`
|
|
111
|
+
: ` fix: ${bold}cele2e host up${reset} ${dim}(restarts the VM; images survive)${reset}`,
|
|
112
|
+
);
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function bringUp(): number {
|
|
117
|
+
const profile = activeProfile();
|
|
118
|
+
const host = readHostFacts();
|
|
119
|
+
const existing = readHostVmFacts(profile);
|
|
120
|
+
const budget = recommendedBudget(host, existing?.vmType ?? 'vz');
|
|
121
|
+
|
|
122
|
+
if (!existing) {
|
|
123
|
+
console.log(
|
|
124
|
+
`${bold}Creating the colima VM${reset} ${dim}(${budget.cpus} CPU, ${budget.memoryGiB} GiB, ${budget.mountType} mounts)${reset}`,
|
|
125
|
+
);
|
|
126
|
+
const result = spawnSync('colima', colimaStartArgs(budget, profile), { stdio: 'inherit' });
|
|
127
|
+
return result.status ?? 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { problems, needsRecreate } = evaluateHostVm(existing, host, budget);
|
|
131
|
+
if (needsRecreate) {
|
|
132
|
+
console.error(`${red}The running VM cannot be brought into policy by restarting it.${reset}`);
|
|
133
|
+
for (const problem of problems) console.error(` ! ${problem}`);
|
|
134
|
+
console.error('');
|
|
135
|
+
console.error('colima discards a mount-type change on an existing VM, so the only fix is to');
|
|
136
|
+
console.error('recreate it. That deletes the local image cache and costs one full');
|
|
137
|
+
console.error(`${bold}cele2e build-infra${reset}. When you are ready:`);
|
|
138
|
+
console.error('');
|
|
139
|
+
console.error(` ${bold}cele2e host reset --yes${reset}`);
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (colimaRunning(profile) && problems.length === 0) {
|
|
144
|
+
console.log(`${green}Docker host already up and in policy.${reset}`);
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Memory and CPU DO take on a restart, so a plain `colima start` with the
|
|
149
|
+
// budget is the fix for those.
|
|
150
|
+
console.log(
|
|
151
|
+
`${bold}Starting the colima VM${reset} ${dim}(${budget.cpus} CPU, ${budget.memoryGiB} GiB)${reset}`,
|
|
152
|
+
);
|
|
153
|
+
const result = spawnSync(
|
|
154
|
+
'colima',
|
|
155
|
+
['start', '-p', profile, '--cpu', String(budget.cpus), '--memory', String(budget.memoryGiB)],
|
|
156
|
+
{ stdio: 'inherit' },
|
|
157
|
+
);
|
|
158
|
+
return result.status ?? 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Refuse to touch the VM while a run holds the lock.
|
|
163
|
+
*
|
|
164
|
+
* Not hypothetical: stopping the VM under a live run killed a colleague's
|
|
165
|
+
* suite mid-flight on 2026-09-05, and because every per-test container carries
|
|
166
|
+
* `restart: unless-stopped`, the next VM boot resurrected that dead run's
|
|
167
|
+
* containers as orphans.
|
|
168
|
+
*/
|
|
169
|
+
function lockBlocks(action: string): boolean {
|
|
170
|
+
const lock = lockStatus();
|
|
171
|
+
if (lock.free || !lock.holder) return false;
|
|
172
|
+
console.error(`${red}Refusing to ${action}: the e2e run-lock is held.${reset}`);
|
|
173
|
+
console.error(
|
|
174
|
+
` ${lock.holder.session} — ${lock.holder.state} ${lock.holder.test} (pid ${lock.holder.pid})`,
|
|
175
|
+
);
|
|
176
|
+
console.error('');
|
|
177
|
+
console.error('Poll with `cele2e status --json` (exit 0 = free, 3 = busy), or release a stack');
|
|
178
|
+
console.error('you know is abandoned with `cele2e down`.');
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function bringDown(): number {
|
|
183
|
+
if (lockBlocks('stop the Docker host')) return 3;
|
|
184
|
+
const profile = activeProfile();
|
|
185
|
+
if (!readHostVmFacts(profile)) {
|
|
186
|
+
console.log('No colima VM to stop.');
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
console.log(`${bold}Stopping the colima VM${reset}`);
|
|
190
|
+
return spawnSync('colima', ['stop', '-p', profile], { stdio: 'inherit' }).status ?? 1;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function resetVm(args: string[]): number {
|
|
194
|
+
if (!args.includes('--yes')) {
|
|
195
|
+
console.error(`${red}cele2e host reset DESTROYS the VM, including every built image.${reset}`);
|
|
196
|
+
console.error('');
|
|
197
|
+
console.error('It is the only way to change the mount transport, which colima fixes at');
|
|
198
|
+
console.error('creation. After it you must run `cele2e build-infra` before any test.');
|
|
199
|
+
console.error('');
|
|
200
|
+
console.error(`Re-run as ${bold}cele2e host reset --yes${reset} when that is what you want.`);
|
|
201
|
+
return 1;
|
|
202
|
+
}
|
|
203
|
+
if (lockBlocks('reset the Docker host')) return 3;
|
|
204
|
+
|
|
205
|
+
const profile = activeProfile();
|
|
206
|
+
const host = readHostFacts();
|
|
207
|
+
const budget = recommendedBudget(host, 'vz');
|
|
208
|
+
|
|
209
|
+
console.log(
|
|
210
|
+
`${bold}Deleting the colima VM${reset} ${dim}(every local image goes with it)${reset}`,
|
|
211
|
+
);
|
|
212
|
+
const deleted = spawnSync('colima', ['delete', '-p', profile, '--force'], { stdio: 'inherit' });
|
|
213
|
+
if ((deleted.status ?? 1) !== 0) return deleted.status ?? 1;
|
|
214
|
+
|
|
215
|
+
console.log(
|
|
216
|
+
`${bold}Creating it again${reset} ${dim}(${budget.cpus} CPU, ${budget.memoryGiB} GiB, ${budget.mountType} mounts)${reset}`,
|
|
217
|
+
);
|
|
218
|
+
const created = spawnSync('colima', colimaStartArgs(budget, profile), { stdio: 'inherit' });
|
|
219
|
+
if ((created.status ?? 1) !== 0) return created.status ?? 1;
|
|
220
|
+
|
|
221
|
+
console.log('');
|
|
222
|
+
console.log(
|
|
223
|
+
`${yellow}The image cache is empty. Run \`cele2e build-infra\` before any test.${reset}`,
|
|
224
|
+
);
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Route a `cele2e host <verb>`; returns the process exit code. */
|
|
229
|
+
export function runHost(args: string[]): number {
|
|
230
|
+
const verb = args[0] ?? 'status';
|
|
231
|
+
|
|
232
|
+
if (!haveColima() && verb !== 'status') {
|
|
233
|
+
console.error('No `colima` binary found — nothing for `cele2e host` to drive.');
|
|
234
|
+
console.error('On Linux docker runs natively and there is no host VM to shape.');
|
|
235
|
+
return 1;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
switch (verb) {
|
|
239
|
+
case 'status':
|
|
240
|
+
return reportStatus();
|
|
241
|
+
case 'up':
|
|
242
|
+
return bringUp();
|
|
243
|
+
case 'down':
|
|
244
|
+
return bringDown();
|
|
245
|
+
case 'reset':
|
|
246
|
+
return resetVm(args.slice(1));
|
|
247
|
+
default:
|
|
248
|
+
console.error(`Unknown host verb: ${verb}`);
|
|
249
|
+
console.error('Usage: cele2e host <status|up|down|reset>');
|
|
250
|
+
return 1;
|
|
251
|
+
}
|
|
252
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -12,14 +12,15 @@
|
|
|
12
12
|
* cele2e shell [container]
|
|
13
13
|
* cele2e status
|
|
14
14
|
* cele2e doctor
|
|
15
|
+
* cele2e host <status|up|down|reset>
|
|
15
16
|
* cele2e last [--json]
|
|
16
17
|
* cele2e load
|
|
17
18
|
* cele2e scaffold <test-name>
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import { spawnSync } from 'node:child_process';
|
|
21
|
-
import { existsSync, readFileSync,
|
|
22
|
-
import {
|
|
22
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
23
|
+
import { dirname, join, resolve } from 'node:path';
|
|
23
24
|
import { parse as parseYaml } from 'yaml';
|
|
24
25
|
import { diagnose, formatHolderLine, formatReport } from '../doctor';
|
|
25
26
|
import { readLastRun } from '../last-run';
|
|
@@ -33,7 +34,13 @@ import {
|
|
|
33
34
|
} from '../run-lock';
|
|
34
35
|
import { runBuild } from './build';
|
|
35
36
|
import { generateBashCompletion, generateZshCompletion, getCompletions } from './completion';
|
|
36
|
-
import {
|
|
37
|
+
import { runHost } from './host';
|
|
38
|
+
import {
|
|
39
|
+
discoverSuiteTests,
|
|
40
|
+
findAllModules,
|
|
41
|
+
resolvePatternTarget,
|
|
42
|
+
resolveRunTargets,
|
|
43
|
+
} from './module-discovery';
|
|
37
44
|
import { runScaffold } from './scaffold';
|
|
38
45
|
|
|
39
46
|
const PKG_DIR = resolve(import.meta.dir, '../..');
|
|
@@ -96,43 +103,6 @@ function resolveSuiteRoot(): string {
|
|
|
96
103
|
return start;
|
|
97
104
|
}
|
|
98
105
|
|
|
99
|
-
/**
|
|
100
|
-
* Enumerate every discoverable Docker e2e test (all module suites + the repo's
|
|
101
|
-
* top-level e2e/tests/) as `{ name, file }`, sorted, with module tests first.
|
|
102
|
-
*/
|
|
103
|
-
function discoverSuiteTests(suiteRoot: string): { name: string; file: string; group: string }[] {
|
|
104
|
-
const out: { name: string; file: string; group: string }[] = [];
|
|
105
|
-
for (const moduleDir of findAllModules(suiteRoot)) {
|
|
106
|
-
const tp = join(moduleDir, 'e2e');
|
|
107
|
-
const testsDir = existsSync(join(moduleDir, 'manifest.yml'))
|
|
108
|
-
? resolve(moduleDir, readManifestTestsDir(join(moduleDir, 'manifest.yml')) ?? 'e2e')
|
|
109
|
-
: tp;
|
|
110
|
-
if (!existsSync(testsDir)) continue;
|
|
111
|
-
for (const f of readdirSync(testsDir)
|
|
112
|
-
.filter((f) => f.endsWith('.test.ts'))
|
|
113
|
-
.sort()) {
|
|
114
|
-
out.push({
|
|
115
|
-
name: f.replace(/\.test\.ts$/, ''),
|
|
116
|
-
file: join(testsDir, f),
|
|
117
|
-
group: basename(moduleDir),
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
const topLevel = join(suiteRoot, 'e2e', 'tests');
|
|
122
|
-
if (existsSync(topLevel)) {
|
|
123
|
-
for (const f of readdirSync(topLevel)
|
|
124
|
-
.filter((f) => f.endsWith('.test.ts'))
|
|
125
|
-
.sort()) {
|
|
126
|
-
out.push({
|
|
127
|
-
name: f.replace(/\.test\.ts$/, ''),
|
|
128
|
-
file: join(topLevel, f),
|
|
129
|
-
group: '(top-level)',
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return out;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
106
|
/**
|
|
137
107
|
* Say out loud when we cleared this session's own kept stack. Silence would be
|
|
138
108
|
* worse than the old refusal: the operator asked for a kept stack, and it is
|
|
@@ -173,6 +143,7 @@ Commands:
|
|
|
173
143
|
shell [container] Shell into a running container (default: management)
|
|
174
144
|
status Show run-lock holder + network status (exit 3 if busy)
|
|
175
145
|
doctor Check the environment a run needs (run's implicit preflight)
|
|
146
|
+
host <verb> Bring the Docker host VM up/down in the shape the rig needs
|
|
176
147
|
last Show the most recent run's results dir + counts
|
|
177
148
|
release Free a run-lock left by --keep/up (does not tear down)
|
|
178
149
|
load Load cached Docker images from tarball
|
|
@@ -187,6 +158,9 @@ Options for \`run\`:
|
|
|
187
158
|
--live Use live (non-simulated) internet
|
|
188
159
|
--published Use published .netapp packages
|
|
189
160
|
--notify Desktop notification when the run finishes (best-effort)
|
|
161
|
+
--source-cli Run the celilo CLI from the mounted workspace instead of the
|
|
162
|
+
one baked into the management image. Roughly doubles each
|
|
163
|
+
celilo command's start-up; iterate without a \`build-infra\`
|
|
190
164
|
--ci Plain log output: one ✔/✗ line per step, no spinner
|
|
191
165
|
(auto-on when a CI env var is set; --no-ci forces off)
|
|
192
166
|
|
|
@@ -195,6 +169,13 @@ Options for \`up\`:
|
|
|
195
169
|
--full-stack Start with caddy, IDP, and DB machines
|
|
196
170
|
--custom <json> Custom machine spec JSON
|
|
197
171
|
|
|
172
|
+
Verbs for \`host\` (macOS/colima; a no-op where docker runs natively):
|
|
173
|
+
status Report the VM against the policy (exit 1 if out of policy)
|
|
174
|
+
up Start it, or say exactly why a running one is out of policy
|
|
175
|
+
down Stop it (refuses while the e2e run-lock is held)
|
|
176
|
+
reset --yes Recreate it — the ONLY way to change the mount transport.
|
|
177
|
+
DESTROYS every built image; budget one \`cele2e build-infra\`
|
|
178
|
+
|
|
198
179
|
Options for \`down\`:
|
|
199
180
|
--keep Stop containers but preserve volumes
|
|
200
181
|
--all Also stop competing module containers
|
|
@@ -219,6 +200,8 @@ Examples:
|
|
|
219
200
|
cele2e shell caddy # debug running caddy container
|
|
220
201
|
cele2e build-infra # rebuild Docker images + repackage all modules
|
|
221
202
|
cele2e build-infra --skip-modules # rebuild Docker images only (faster)
|
|
203
|
+
cele2e host status # is the Docker host VM shaped for the rig?
|
|
204
|
+
cele2e run caddy-uninstall --source-cli # run the WORKSPACE cli, not the baked one
|
|
222
205
|
cele2e scaffold lunacycle-auth # create tests/lunacycle-auth.test.ts
|
|
223
206
|
cele2e completion zsh # print zsh completion script
|
|
224
207
|
`);
|
|
@@ -302,21 +285,29 @@ switch (command) {
|
|
|
302
285
|
|
|
303
286
|
if (!cwdHasManifest) {
|
|
304
287
|
if (positionals.length === 1) {
|
|
305
|
-
|
|
306
|
-
|
|
288
|
+
// Resolve the pattern against the FULL discovered suite (every module +
|
|
289
|
+
// the repo's top-level e2e/tests/), rooted at resolveSuiteRoot() like
|
|
290
|
+
// `list` and `--all`. The wrapper cd's into packages/e2e before
|
|
291
|
+
// exec'ing, so the old walk down from process.cwd() started below the
|
|
292
|
+
// repo root: a test name `cele2e list` printed came back "No test
|
|
293
|
+
// files matched" from `cele2e run <name>`.
|
|
294
|
+
const target = resolvePatternTarget(resolveSuiteRoot(), positionals[0]);
|
|
295
|
+
if (target?.kind === 'top-level') {
|
|
296
|
+
runScript('e2e-run', args, {
|
|
297
|
+
E2E_TEST_DIR: target.dir,
|
|
298
|
+
E2E_TESTS_PATH: target.testsPath,
|
|
299
|
+
});
|
|
300
|
+
} else if (target?.kind === 'module') {
|
|
307
301
|
runScript('e2e-run', args, {
|
|
308
302
|
E2E_TEST_DIR: stateDir,
|
|
309
|
-
E2E_MODULE_DIR:
|
|
303
|
+
E2E_MODULE_DIR: target.moduleDir,
|
|
310
304
|
});
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
console.error(` ${basename(m.moduleDir)}: ${m.matchedTests.join(', ')}`);
|
|
305
|
+
} else if (target?.kind === 'ambiguous') {
|
|
306
|
+
console.error(`Multiple tests match "${positionals[0]}":`);
|
|
307
|
+
for (const m of target.matches) {
|
|
308
|
+
console.error(` ${m.group}: ${m.name}`);
|
|
316
309
|
}
|
|
317
|
-
console.error(
|
|
318
|
-
'\nDisambiguate by running from inside the module directory or passing its path.',
|
|
319
|
-
);
|
|
310
|
+
console.error('\nDisambiguate with a longer pattern (an exact name always wins).');
|
|
320
311
|
process.exit(1);
|
|
321
312
|
}
|
|
322
313
|
} else if (positionals.length === 0) {
|
|
@@ -327,6 +318,33 @@ switch (command) {
|
|
|
327
318
|
E2E_MODULE_DIRS: allModules.join('\n'),
|
|
328
319
|
});
|
|
329
320
|
}
|
|
321
|
+
} else {
|
|
322
|
+
// Several names: resolve each independently against the full suite
|
|
323
|
+
// and feed the runner's multi-module env (E2E_MODULE_DIRS /
|
|
324
|
+
// E2E_TOP_LEVEL_TESTS). The old fall-through ran top-level discovery
|
|
325
|
+
// only, so module suites reported "No test files matched" — which
|
|
326
|
+
// reads as a typo, not a limitation.
|
|
327
|
+
const resolved = resolveRunTargets(resolveSuiteRoot(), positionals);
|
|
328
|
+
if (resolved.kind === 'no-match') {
|
|
329
|
+
console.error(`No suite matches "${resolved.pattern}". List them: cele2e list`);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
if (resolved.kind === 'ambiguous') {
|
|
333
|
+
console.error(`Multiple tests match "${resolved.pattern}":`);
|
|
334
|
+
for (const m of resolved.matches) {
|
|
335
|
+
console.error(` ${m.group}: ${m.name}`);
|
|
336
|
+
}
|
|
337
|
+
console.error('\nDisambiguate with a longer pattern (an exact name always wins).');
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
const env: Record<string, string> = { E2E_TEST_DIR: stateDir };
|
|
341
|
+
if (resolved.modules.length > 0) {
|
|
342
|
+
env.E2E_MODULE_DIRS = resolved.modules.join('\n');
|
|
343
|
+
}
|
|
344
|
+
if (resolved.topLevel) {
|
|
345
|
+
env.E2E_TOP_LEVEL_TESTS = join(resolveSuiteRoot(), 'e2e', 'tests');
|
|
346
|
+
}
|
|
347
|
+
runScript('e2e-run', args, env);
|
|
330
348
|
}
|
|
331
349
|
}
|
|
332
350
|
|
|
@@ -467,6 +485,15 @@ switch (command) {
|
|
|
467
485
|
break;
|
|
468
486
|
}
|
|
469
487
|
|
|
488
|
+
case 'host': {
|
|
489
|
+
// Deliberately outside the run-lock: `status` is read-only, and `up`/`down`/
|
|
490
|
+
// `reset` take the lock's VERDICT (see host.ts lockBlocks) rather than the
|
|
491
|
+
// lock itself — taking it would make `cele2e host down` unable to report
|
|
492
|
+
// that somebody else's run is what stopped it.
|
|
493
|
+
process.exit(runHost(args));
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
|
|
470
497
|
case 'last': {
|
|
471
498
|
// Which results directory was MINE? Inferring it from `ls -t results` returns
|
|
472
499
|
// the newest run, which after a refused start is somebody else's — a suite
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
14
|
-
import { join, resolve } from 'node:path';
|
|
14
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
15
15
|
import { parse as parseYaml } from 'yaml';
|
|
16
16
|
|
|
17
17
|
const MAX_DEPTH = 2;
|
|
@@ -75,22 +75,117 @@ function walkModules(cwd: string, visit: (moduleDir: string, testsPath: string)
|
|
|
75
75
|
scan(cwd, 1);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
export
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
export const TOP_LEVEL_GROUP = '(top-level)';
|
|
79
|
+
|
|
80
|
+
export interface SuiteTest {
|
|
81
|
+
name: string;
|
|
82
|
+
file: string;
|
|
83
|
+
group: string;
|
|
84
|
+
/** Owning directory: the module dir, or the suite's `e2e/` dir for top-level entries. */
|
|
85
|
+
dir: string;
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
89
|
+
* Enumerate every discoverable Docker e2e test (all module suites + the repo's
|
|
90
|
+
* top-level e2e/tests/) sorted, with module tests first.
|
|
86
91
|
*/
|
|
87
|
-
export function
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
export function discoverSuiteTests(suiteRoot: string): SuiteTest[] {
|
|
93
|
+
const out: SuiteTest[] = [];
|
|
94
|
+
for (const moduleDir of findAllModules(suiteRoot)) {
|
|
95
|
+
const testsDir = existsSync(join(moduleDir, 'manifest.yml'))
|
|
96
|
+
? resolve(moduleDir, readManifestTestsDir(join(moduleDir, 'manifest.yml')) ?? 'e2e')
|
|
97
|
+
: join(moduleDir, 'e2e');
|
|
98
|
+
if (!existsSync(testsDir)) continue;
|
|
99
|
+
for (const f of readdirSync(testsDir)
|
|
100
|
+
.filter((f) => f.endsWith('.test.ts'))
|
|
101
|
+
.sort()) {
|
|
102
|
+
out.push({
|
|
103
|
+
name: f.replace(/\.test\.ts$/, ''),
|
|
104
|
+
file: join(testsDir, f),
|
|
105
|
+
group: basename(moduleDir),
|
|
106
|
+
dir: moduleDir,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const topLevel = join(suiteRoot, 'e2e', 'tests');
|
|
111
|
+
if (existsSync(topLevel)) {
|
|
112
|
+
for (const f of readdirSync(topLevel)
|
|
113
|
+
.filter((f) => f.endsWith('.test.ts'))
|
|
114
|
+
.sort()) {
|
|
115
|
+
out.push({
|
|
116
|
+
name: f.replace(/\.test\.ts$/, ''),
|
|
117
|
+
file: join(topLevel, f),
|
|
118
|
+
group: TOP_LEVEL_GROUP,
|
|
119
|
+
dir: dirname(topLevel),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type PatternTarget =
|
|
127
|
+
| { kind: 'module'; moduleDir: string }
|
|
128
|
+
| { kind: 'top-level'; dir: string; testsPath: string }
|
|
129
|
+
| { kind: 'ambiguous'; matches: SuiteTest[] };
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Resolve a `cele2e run <pattern>` positional against the full discovered
|
|
133
|
+
* suite. Pattern semantics match the runner's filter: substring against the
|
|
134
|
+
* test name, with one exception — a pattern that exactly equals a test name
|
|
135
|
+
* wins outright, so a suite whose name prefixes another (`aspect-fanout` vs
|
|
136
|
+
* `aspect-fanout-new-systems`) stays reachable from the repo root instead of
|
|
137
|
+
* reading as permanently ambiguous. Returns null when nothing matches, so the
|
|
138
|
+
* caller can fall through to its default tests dir.
|
|
139
|
+
*/
|
|
140
|
+
export function resolvePatternTarget(suiteRoot: string, pattern: string): PatternTarget | null {
|
|
141
|
+
const tests = discoverSuiteTests(suiteRoot);
|
|
142
|
+
const exact = tests.filter((t) => t.name === pattern);
|
|
143
|
+
if (exact.length === 1) {
|
|
144
|
+
return toPatternTarget(exact[0]);
|
|
145
|
+
}
|
|
146
|
+
if (exact.length > 1) {
|
|
147
|
+
return { kind: 'ambiguous', matches: exact };
|
|
148
|
+
}
|
|
149
|
+
const matches = tests.filter((t) => t.name.includes(pattern));
|
|
150
|
+
if (matches.length === 0) return null;
|
|
151
|
+
if (matches.length > 1) return { kind: 'ambiguous', matches };
|
|
152
|
+
return toPatternTarget(matches[0]);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function toPatternTarget(match: SuiteTest): PatternTarget {
|
|
156
|
+
if (match.group === TOP_LEVEL_GROUP) {
|
|
157
|
+
return { kind: 'top-level', dir: match.dir, testsPath: join(match.dir, 'tests') };
|
|
158
|
+
}
|
|
159
|
+
return { kind: 'module', moduleDir: match.dir };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export type RunTargets =
|
|
163
|
+
| { kind: 'ok'; modules: string[]; topLevel: boolean }
|
|
164
|
+
| { kind: 'no-match'; pattern: string }
|
|
165
|
+
| { kind: 'ambiguous'; pattern: string; matches: SuiteTest[] };
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Resolve every `cele2e run <name>...` positional independently and aggregate
|
|
169
|
+
* the targets into the env-shaped set the runner consumes: module dirs (for
|
|
170
|
+
* E2E_MODULE_DIRS) plus a top-level flag (for E2E_TOP_LEVEL_TESTS). Reports
|
|
171
|
+
* the FIRST unresolvable name rather than silently dropping it, so a typo or
|
|
172
|
+
* an un-followable ambiguity errors instead of falling through to a
|
|
173
|
+
* top-level-only run that prints "No test files matched".
|
|
174
|
+
*/
|
|
175
|
+
export function resolveRunTargets(suiteRoot: string, patterns: readonly string[]): RunTargets {
|
|
176
|
+
const modules = new Set<string>();
|
|
177
|
+
let topLevel = false;
|
|
178
|
+
for (const pattern of patterns) {
|
|
179
|
+
const target = resolvePatternTarget(suiteRoot, pattern);
|
|
180
|
+
if (!target) return { kind: 'no-match', pattern };
|
|
181
|
+
if (target.kind === 'ambiguous') return { kind: 'ambiguous', pattern, matches: target.matches };
|
|
182
|
+
if (target.kind === 'module') {
|
|
183
|
+
modules.add(target.moduleDir);
|
|
184
|
+
} else {
|
|
185
|
+
topLevel = true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return { kind: 'ok', modules: Array.from(modules).sort(), topLevel };
|
|
94
189
|
}
|
|
95
190
|
|
|
96
191
|
/**
|
package/src/cli/scaffold.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
6
|
+
|
|
6
7
|
import { join } from 'node:path';
|
|
7
8
|
|
|
8
9
|
const green = '\x1b[32m';
|
|
@@ -32,28 +33,23 @@ function generateTestTemplate(name: string): string {
|
|
|
32
33
|
* Run (reuse): cele2e run --reuse ${name}
|
|
33
34
|
*/
|
|
34
35
|
|
|
35
|
-
import { afterAll, describe, expect
|
|
36
|
-
import { network, progress, reconnectNetwork } from '@celilo/e2e';
|
|
36
|
+
import { afterAll, describe, expect } from 'bun:test';
|
|
37
|
+
import { createStages, network, progress, reconnectNetwork } from '@celilo/e2e';
|
|
37
38
|
import type { NetworkHandle } from '@celilo/e2e/types';
|
|
38
39
|
|
|
39
40
|
const REUSE = process.argv.includes('--reuse');
|
|
40
41
|
const PROJECT_NAME = 'celilo-e2e-${name}';
|
|
41
42
|
|
|
42
43
|
describe('${name}', () => {
|
|
43
|
-
|
|
44
|
-
let stageError: string | null = null;
|
|
44
|
+
const { stage } = createStages();
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
if (stageError) {
|
|
48
|
-
throw new Error(\`Skipped: \${stage} — \${stageError}\`);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
46
|
+
let net: NetworkHandle;
|
|
51
47
|
|
|
52
48
|
afterAll(async () => {
|
|
53
49
|
await net?.stop();
|
|
54
50
|
});
|
|
55
51
|
|
|
56
|
-
|
|
52
|
+
stage('start network', async () => {
|
|
57
53
|
if (REUSE) {
|
|
58
54
|
net = await reconnectNetwork(PROJECT_NAME);
|
|
59
55
|
return;
|
|
@@ -65,24 +61,20 @@ describe('${name}', () => {
|
|
|
65
61
|
.start();
|
|
66
62
|
}, 120_000);
|
|
67
63
|
|
|
68
|
-
test
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
stageError = String(error);
|
|
80
|
-
throw error;
|
|
81
|
-
}
|
|
64
|
+
// Each stage runs as its own bun test with its own timeout. If a stage fails
|
|
65
|
+
// or times out, the later stages skip with a 'Skipped: ...' reason naming
|
|
66
|
+
// the cause, instead of running against a half-built fixture.
|
|
67
|
+
stage('deploy', async () => {
|
|
68
|
+
progress('importing module', 'module imported');
|
|
69
|
+
await net.celilo('module import /path/to/module.netapp');
|
|
70
|
+
progress.done();
|
|
71
|
+
|
|
72
|
+
progress('deploying', 'deployed');
|
|
73
|
+
await net.celilo('module deploy my-module');
|
|
74
|
+
progress.done();
|
|
82
75
|
}, 300_000);
|
|
83
76
|
|
|
84
|
-
|
|
85
|
-
requireStage('verify');
|
|
77
|
+
stage('verify', async () => {
|
|
86
78
|
// TODO: add verification steps
|
|
87
79
|
// const result = await net.exec('machine-1', 'curl -s http://localhost');
|
|
88
80
|
// expect(result.exitCode).toBe(0);
|