@celilo/e2e 0.5.1 → 0.6.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 +2 -1
- package/bin/cele2e +14 -0
- package/bin/e2e-cache-images +57 -0
- package/bin/e2e-shell +36 -13
- package/bin/e2e-status +8 -2
- package/bin/e2e-up +5 -5
- package/completions/install-zsh-completion.sh +78 -0
- package/config/dns/celilo.computer.zone +16 -0
- package/config/dns/iamtheinternet.org.zone +7 -6
- package/config/dns/knot-namecheap.conf +8 -0
- package/config/dns/knot-tld.conf +4 -0
- package/config/dns/lunacycle.net.zone +16 -0
- package/config/dns/net.zone +11 -0
- package/config/dns/org.zone +5 -0
- package/config/dns/root.zone +2 -0
- package/config/dns/templates/iamtheinternet.org.zone +16 -0
- package/config/dns/templates/lunacycle.net.zone +16 -0
- package/config/resolver/unbound.conf +10 -0
- package/config/routing/docker-image-preload.service +14 -0
- package/config/routing/docker-image-preload.sh +24 -0
- package/config/routing/management-routes.sh +24 -2
- package/config/routing/resolver-internal-routes.sh +59 -0
- package/docker/Dockerfile.internal-resolver +16 -0
- package/docker/Dockerfile.isitup +37 -0
- package/docker/Dockerfile.management +2 -0
- package/docker/Dockerfile.registry +34 -0
- package/docker/Dockerfile.target-machine +20 -0
- package/docker/Dockerfile.target-machine-docker +11 -19
- package/docker/Dockerfile.target-technitium +97 -0
- package/netapps/authentik.netapp +0 -0
- package/netapps/caddy.netapp +0 -0
- package/netapps/greenwave.netapp +0 -0
- package/netapps/homebridge.netapp +0 -0
- package/netapps/iptables.netapp +0 -0
- package/netapps/knot-unbound-internal.netapp +0 -0
- package/netapps/namecheap.netapp +0 -0
- package/netapps/technitium.netapp +0 -0
- package/netapps/test-ansible-output.netapp +0 -0
- package/package.json +8 -1
- package/simulators/greenwave/state.ts +7 -12
- package/simulators/isitup/Caddyfile +11 -0
- package/simulators/isitup/entrypoint.sh +47 -0
- package/simulators/isitup/server.ts +112 -0
- package/simulators/namecheap-ddns/server.ts +31 -2
- package/src/cli/build.ts +214 -0
- package/src/cli/command-registry.ts +100 -0
- package/src/cli/command-tree-parser.ts +52 -0
- package/src/cli/completion.ts +195 -0
- package/src/cli/index.ts +273 -0
- package/src/cli/module-discovery.ts +120 -0
- package/src/cli/scaffold.ts +108 -0
- package/src/container-manager.ts +373 -76
- package/src/docker-compose-generator.ts +127 -23
- package/src/network-builder.ts +21 -0
- package/src/parse-line.ts +147 -0
- package/src/runner.ts +197 -247
- package/src/simulator-ips.ts +42 -0
- package/src/types.ts +28 -0
- package/bin/cache-docker-images +0 -46
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* cele2e — E2E test CLI for Celilo-deployed applications.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* cele2e run [pattern] [--keep] [--reuse] [--live] [--published]
|
|
7
|
+
* cele2e up [--caddy|--full-stack|--infrastructure|--custom '<json>']
|
|
8
|
+
* cele2e down [--keep] [--all]
|
|
9
|
+
* cele2e build-infra [module-dir...] [--save]
|
|
10
|
+
* cele2e clear-timing
|
|
11
|
+
* cele2e version
|
|
12
|
+
* cele2e shell [container]
|
|
13
|
+
* cele2e status
|
|
14
|
+
* cele2e load
|
|
15
|
+
* cele2e scaffold <test-name>
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
20
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
21
|
+
import { parse as parseYaml } from 'yaml';
|
|
22
|
+
import { runBuild } from './build';
|
|
23
|
+
import { runScaffold } from './scaffold';
|
|
24
|
+
import { generateBashCompletion, generateZshCompletion, getCompletions } from './completion';
|
|
25
|
+
import { findAllModules, findModulesWithMatchingTest } from './module-discovery';
|
|
26
|
+
|
|
27
|
+
const PKG_DIR = resolve(import.meta.dir, '../..');
|
|
28
|
+
const BIN_DIR = join(PKG_DIR, 'bin');
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
function readManifestTestsDir(manifestPath: string): string | undefined {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = parseYaml(readFileSync(manifestPath, 'utf-8')) as { e2e?: { tests_dir?: string } };
|
|
34
|
+
return parsed?.e2e?.tests_dir;
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function detectTestDir(): { stateDir: string; testsPath: string } {
|
|
41
|
+
if (process.env.E2E_TEST_DIR) {
|
|
42
|
+
const stateDir = process.env.E2E_TEST_DIR;
|
|
43
|
+
return { stateDir, testsPath: join(stateDir, 'tests') };
|
|
44
|
+
}
|
|
45
|
+
const cwd = process.cwd();
|
|
46
|
+
const manifestPath = join(cwd, 'manifest.yml');
|
|
47
|
+
if (existsSync(manifestPath)) {
|
|
48
|
+
const testsDir = readManifestTestsDir(manifestPath);
|
|
49
|
+
if (testsDir) {
|
|
50
|
+
const testsPath = resolve(cwd, testsDir);
|
|
51
|
+
return { stateDir: dirname(testsPath), testsPath };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (existsSync(join(cwd, 'tests'))) return { stateDir: cwd, testsPath: join(cwd, 'tests') };
|
|
55
|
+
return { stateDir: cwd, testsPath: join(cwd, 'tests') };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function runScript(script: string, args: string[], env?: Record<string, string>): never {
|
|
59
|
+
const result = spawnSync(join(BIN_DIR, script), args, {
|
|
60
|
+
stdio: 'inherit',
|
|
61
|
+
env: { ...process.env, ...env },
|
|
62
|
+
});
|
|
63
|
+
process.exit(result.status ?? 1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function printHelp(): void {
|
|
67
|
+
console.log(`
|
|
68
|
+
cele2e — E2E test CLI for Celilo-deployed applications
|
|
69
|
+
|
|
70
|
+
Usage:
|
|
71
|
+
cele2e <command> [options]
|
|
72
|
+
|
|
73
|
+
Commands:
|
|
74
|
+
run [pattern] Run e2e tests (pattern filters by test file name)
|
|
75
|
+
up [preset] Start interactive test network
|
|
76
|
+
down Tear down running network
|
|
77
|
+
build-infra [dirs...] Rebuild Docker images and package all modules as .netapps
|
|
78
|
+
clear-timing Clear saved timing history used for test ETAs
|
|
79
|
+
version Show cele2e version
|
|
80
|
+
shell [container] Shell into a running container (default: management)
|
|
81
|
+
status Show network status and DNS health
|
|
82
|
+
load Load cached Docker images from tarball
|
|
83
|
+
scaffold <name> Generate a new test file from template
|
|
84
|
+
completion <shell> Generate shell completion script (zsh|bash)
|
|
85
|
+
|
|
86
|
+
Options for \`run\`:
|
|
87
|
+
--keep Keep network running after tests
|
|
88
|
+
--reuse Reuse existing network if running
|
|
89
|
+
--live Use live (non-simulated) internet
|
|
90
|
+
--published Use published .netapp packages
|
|
91
|
+
|
|
92
|
+
Options for \`up\`:
|
|
93
|
+
--caddy Start with caddy machine in DMZ
|
|
94
|
+
--full-stack Start with caddy, IDP, and DB machines
|
|
95
|
+
--custom <json> Custom machine spec JSON
|
|
96
|
+
|
|
97
|
+
Options for \`down\`:
|
|
98
|
+
--keep Stop containers but preserve volumes
|
|
99
|
+
--all Also stop competing module containers
|
|
100
|
+
|
|
101
|
+
Options for \`build-infra\`:
|
|
102
|
+
--save Save Docker images to tarball after build
|
|
103
|
+
--skip-modules Skip .netapp packaging (only rebuild Docker images)
|
|
104
|
+
|
|
105
|
+
Examples:
|
|
106
|
+
cele2e run # in a module: run its tests; outside: run all modules' tests
|
|
107
|
+
cele2e run deploy # run tests matching "deploy"
|
|
108
|
+
cele2e run caddy-direct-internet # from modules/, finds the matching module automatically
|
|
109
|
+
cele2e run --reuse deploy # reuse existing network
|
|
110
|
+
cele2e up --full-stack # interactive full-stack environment
|
|
111
|
+
cele2e shell caddy # debug running caddy container
|
|
112
|
+
cele2e build-infra # rebuild Docker images + repackage all modules
|
|
113
|
+
cele2e build-infra --skip-modules # rebuild Docker images only (faster)
|
|
114
|
+
cele2e scaffold lunacycle-auth # create tests/lunacycle-auth.test.ts
|
|
115
|
+
cele2e completion zsh # print zsh completion script
|
|
116
|
+
`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const [, , command, ...args] = process.argv;
|
|
120
|
+
const { stateDir, testsPath: detectedTestsPath } = detectTestDir();
|
|
121
|
+
|
|
122
|
+
// Handle --get-completions before routing (used by shell completion scripts)
|
|
123
|
+
if (command === '--get-completions') {
|
|
124
|
+
const words = args.slice(0, -1);
|
|
125
|
+
const current = Number.parseInt(args[args.length - 1] || '0', 10);
|
|
126
|
+
const suggestions = getCompletions(words, current);
|
|
127
|
+
process.stdout.write(`${suggestions.join('\n')}\n`);
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
switch (command) {
|
|
132
|
+
case 'run': {
|
|
133
|
+
// Check if any non-flag arg is a module path (has manifest.yml)
|
|
134
|
+
const modulePath = args
|
|
135
|
+
.filter((a) => !a.startsWith('--'))
|
|
136
|
+
.find((a) => existsSync(join(resolve(a), 'manifest.yml')));
|
|
137
|
+
|
|
138
|
+
if (modulePath) {
|
|
139
|
+
const absModulePath = resolve(modulePath);
|
|
140
|
+
runScript('e2e-run', args.filter((a) => a !== modulePath), {
|
|
141
|
+
E2E_TEST_DIR: stateDir,
|
|
142
|
+
E2E_MODULE_DIR: absModulePath,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// If cwd is not a module, walk sibling modules so the user can invoke
|
|
147
|
+
// `cele2e run` from infra/modules/ (or the repo root) without cd-ing in.
|
|
148
|
+
const cwdHasManifest = existsSync(join(process.cwd(), 'manifest.yml'));
|
|
149
|
+
const positionals = args.filter((a) => !a.startsWith('--'));
|
|
150
|
+
|
|
151
|
+
if (!cwdHasManifest) {
|
|
152
|
+
if (positionals.length === 1) {
|
|
153
|
+
const matches = findModulesWithMatchingTest(process.cwd(), positionals[0]);
|
|
154
|
+
if (matches.length === 1) {
|
|
155
|
+
runScript('e2e-run', args, {
|
|
156
|
+
E2E_TEST_DIR: stateDir,
|
|
157
|
+
E2E_MODULE_DIR: matches[0].moduleDir,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (matches.length > 1) {
|
|
161
|
+
console.error(`Multiple modules contain tests matching "${positionals[0]}":`);
|
|
162
|
+
for (const m of matches) {
|
|
163
|
+
console.error(` ${basename(m.moduleDir)}: ${m.matchedTests.join(', ')}`);
|
|
164
|
+
}
|
|
165
|
+
console.error('\nDisambiguate by running from inside the module directory or passing its path.');
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
} else if (positionals.length === 0) {
|
|
169
|
+
const allModules = findAllModules(process.cwd());
|
|
170
|
+
if (allModules.length > 0) {
|
|
171
|
+
runScript('e2e-run', args, {
|
|
172
|
+
E2E_TEST_DIR: stateDir,
|
|
173
|
+
E2E_MODULE_DIRS: allModules.join('\n'),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
runScript('e2e-run', args, { E2E_TEST_DIR: stateDir, E2E_TESTS_PATH: detectedTestsPath });
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
case 'up':
|
|
184
|
+
runScript('e2e-up', args);
|
|
185
|
+
|
|
186
|
+
case 'down':
|
|
187
|
+
runScript('e2e-down', args, { E2E_TEST_DIR: stateDir });
|
|
188
|
+
|
|
189
|
+
case 'shell':
|
|
190
|
+
runScript('e2e-shell', args);
|
|
191
|
+
|
|
192
|
+
case 'status': {
|
|
193
|
+
const persistentFile = join(stateDir, '.e2e-persistent.json');
|
|
194
|
+
let projectName: string | undefined;
|
|
195
|
+
if (existsSync(persistentFile)) {
|
|
196
|
+
try {
|
|
197
|
+
const data = JSON.parse(readFileSync(persistentFile, 'utf-8'));
|
|
198
|
+
projectName = data.projectName;
|
|
199
|
+
console.log('=== Persistent Test Network ===');
|
|
200
|
+
console.log(` Project: ${data.projectName}`);
|
|
201
|
+
console.log(` Test: ${data.testFile}`);
|
|
202
|
+
console.log(` Created: ${data.createdAt}`);
|
|
203
|
+
console.log(` Reuse: cele2e run --reuse`);
|
|
204
|
+
console.log(` Tear down: cele2e down`);
|
|
205
|
+
console.log('');
|
|
206
|
+
} catch { /* ignore parse errors */ }
|
|
207
|
+
}
|
|
208
|
+
runScript('e2e-status', args, projectName ? { CELILO_E2E_PROJECT: projectName } : undefined);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
case 'load':
|
|
212
|
+
runScript('e2e-load', args);
|
|
213
|
+
|
|
214
|
+
case 'build-infra': {
|
|
215
|
+
const save = args.includes('--save');
|
|
216
|
+
const skipModules = args.includes('--skip-modules');
|
|
217
|
+
const moduleDirs = args.filter((a) => !a.startsWith('--'));
|
|
218
|
+
runBuild({ pkgDir: PKG_DIR, moduleDirs, save, skipModules });
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
case 'clear-timing': {
|
|
223
|
+
const timingFile = join(stateDir, '.e2e-timing.json');
|
|
224
|
+
if (existsSync(timingFile)) {
|
|
225
|
+
rmSync(timingFile);
|
|
226
|
+
console.log(`Cleared timing data: ${timingFile}`);
|
|
227
|
+
} else {
|
|
228
|
+
console.log(`No timing data found at ${timingFile}`);
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
case 'scaffold': {
|
|
234
|
+
const name = args[0];
|
|
235
|
+
if (!name) {
|
|
236
|
+
console.error('Error: scaffold requires a test name\n cele2e scaffold <name>');
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
runScaffold({ testsPath: detectedTestsPath, name });
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
case 'version': {
|
|
244
|
+
const pkg = await import('../../package.json', { with: { type: 'json' } });
|
|
245
|
+
console.log(`cele2e v${pkg.default.version}`);
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case 'completion': {
|
|
250
|
+
const shell = args[0];
|
|
251
|
+
if (shell === 'zsh') {
|
|
252
|
+
process.stdout.write(generateZshCompletion());
|
|
253
|
+
} else if (shell === 'bash') {
|
|
254
|
+
process.stdout.write(generateBashCompletion());
|
|
255
|
+
} else {
|
|
256
|
+
console.error('Usage: cele2e completion <zsh|bash>');
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
case undefined:
|
|
263
|
+
case '--help':
|
|
264
|
+
case '-h':
|
|
265
|
+
case 'help':
|
|
266
|
+
printHelp();
|
|
267
|
+
break;
|
|
268
|
+
|
|
269
|
+
default:
|
|
270
|
+
console.error(`Unknown command: ${command}\n`);
|
|
271
|
+
printHelp();
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Walks the cwd tree to find Celilo modules and their e2e tests.
|
|
3
|
+
*
|
|
4
|
+
* Used so that `cele2e run <test-name>` works from `infra/modules/` (or the
|
|
5
|
+
* repo root) without needing to cd into the module first, and so tab
|
|
6
|
+
* completion can suggest test names discovered across all modules.
|
|
7
|
+
*
|
|
8
|
+
* Scans direct subdirs and one level deeper — covers `modules/` (where each
|
|
9
|
+
* subdir is a module) and `infra/` (where `modules/<name>/` is one level
|
|
10
|
+
* deeper).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
14
|
+
import { join, resolve } from 'node:path';
|
|
15
|
+
import { parse as parseYaml } from 'yaml';
|
|
16
|
+
|
|
17
|
+
const MAX_DEPTH = 2;
|
|
18
|
+
|
|
19
|
+
function readManifestTestsDir(manifestPath: string): string | undefined {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = parseYaml(readFileSync(manifestPath, 'utf-8')) as { e2e?: { tests_dir?: string } };
|
|
22
|
+
return parsed?.e2e?.tests_dir;
|
|
23
|
+
} catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function listTestNames(testsPath: string): string[] {
|
|
29
|
+
if (!existsSync(testsPath)) return [];
|
|
30
|
+
try {
|
|
31
|
+
return readdirSync(testsPath)
|
|
32
|
+
.filter((f) => f.endsWith('.test.ts'))
|
|
33
|
+
.map((f) => f.replace(/\.test\.ts$/, ''));
|
|
34
|
+
} catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isModule(dir: string): { manifestPath: string; testsPath: string } | null {
|
|
40
|
+
const manifestPath = join(dir, 'manifest.yml');
|
|
41
|
+
if (!existsSync(manifestPath)) return null;
|
|
42
|
+
// Match the runner's resolution: prefer e2e.tests_dir from the manifest,
|
|
43
|
+
// fall back to a conventional e2e/ directory at the module root.
|
|
44
|
+
const testsDir = readManifestTestsDir(manifestPath) ?? 'e2e';
|
|
45
|
+
return { manifestPath, testsPath: resolve(dir, testsDir) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function walkModules(cwd: string, visit: (moduleDir: string, testsPath: string) => void): void {
|
|
49
|
+
function scan(dir: string, depth: number): void {
|
|
50
|
+
if (depth > MAX_DEPTH) return;
|
|
51
|
+
let entries: string[];
|
|
52
|
+
try {
|
|
53
|
+
entries = readdirSync(dir);
|
|
54
|
+
} catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (entry.startsWith('.') || entry === 'node_modules') continue;
|
|
59
|
+
const subdir = join(dir, entry);
|
|
60
|
+
try {
|
|
61
|
+
if (!statSync(subdir).isDirectory()) continue;
|
|
62
|
+
} catch {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const mod = isModule(subdir);
|
|
66
|
+
if (mod) {
|
|
67
|
+
visit(subdir, mod.testsPath);
|
|
68
|
+
} else if (depth < MAX_DEPTH) {
|
|
69
|
+
scan(subdir, depth + 1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
scan(cwd, 1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ModuleTestMatch {
|
|
77
|
+
moduleDir: string;
|
|
78
|
+
matchedTests: string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Find modules under `cwd` whose e2e dir contains a test name including `pattern`.
|
|
83
|
+
* Used by `cele2e run <pattern>` when invoked outside any module dir.
|
|
84
|
+
*/
|
|
85
|
+
export function findModulesWithMatchingTest(cwd: string, pattern: string): ModuleTestMatch[] {
|
|
86
|
+
const results: ModuleTestMatch[] = [];
|
|
87
|
+
walkModules(cwd, (moduleDir, testsPath) => {
|
|
88
|
+
const matched = listTestNames(testsPath).filter((t) => t.includes(pattern));
|
|
89
|
+
if (matched.length > 0) results.push({ moduleDir, matchedTests: matched });
|
|
90
|
+
});
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Collect all test names reachable from `cwd`. Includes tests in cwd itself
|
|
96
|
+
* (when cwd is a module) and tests in any module discovered under cwd.
|
|
97
|
+
*/
|
|
98
|
+
export function findAllTestNames(cwd: string): string[] {
|
|
99
|
+
const names = new Set<string>();
|
|
100
|
+
const cwdMod = isModule(cwd);
|
|
101
|
+
if (cwdMod) {
|
|
102
|
+
for (const n of listTestNames(cwdMod.testsPath)) names.add(n);
|
|
103
|
+
}
|
|
104
|
+
walkModules(cwd, (_moduleDir, testsPath) => {
|
|
105
|
+
for (const n of listTestNames(testsPath)) names.add(n);
|
|
106
|
+
});
|
|
107
|
+
return Array.from(names).sort();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Return the absolute paths of all modules discoverable under `cwd` that have
|
|
112
|
+
* an e2e tests directory containing at least one `*.test.ts` file.
|
|
113
|
+
*/
|
|
114
|
+
export function findAllModules(cwd: string): string[] {
|
|
115
|
+
const modules: string[] = [];
|
|
116
|
+
walkModules(cwd, (moduleDir, testsPath) => {
|
|
117
|
+
if (listTestNames(testsPath).length > 0) modules.push(moduleDir);
|
|
118
|
+
});
|
|
119
|
+
return modules.sort();
|
|
120
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cele2e scaffold — generate a new e2e test file from template.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
const green = '\x1b[32m';
|
|
9
|
+
const dim = '\x1b[2m';
|
|
10
|
+
const reset = '\x1b[0m';
|
|
11
|
+
|
|
12
|
+
interface ScaffoldOptions {
|
|
13
|
+
testsPath: string;
|
|
14
|
+
name: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toTitleCase(kebab: string): string {
|
|
18
|
+
return kebab
|
|
19
|
+
.split('-')
|
|
20
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
21
|
+
.join(' ');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function generateTestTemplate(name: string): string {
|
|
25
|
+
const title = toTitleCase(name);
|
|
26
|
+
return `/**
|
|
27
|
+
* ${title} E2E Test
|
|
28
|
+
*
|
|
29
|
+
* Tests that ${title} deploys and operates correctly through Celilo's infrastructure.
|
|
30
|
+
*
|
|
31
|
+
* Run: cele2e run ${name}
|
|
32
|
+
* Run (reuse): cele2e run --reuse ${name}
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { afterAll, describe, expect, test } from 'bun:test';
|
|
36
|
+
import { network, progress, reconnectNetwork } from '@celilo/e2e';
|
|
37
|
+
import type { NetworkHandle } from '@celilo/e2e/types';
|
|
38
|
+
|
|
39
|
+
const REUSE = process.argv.includes('--reuse');
|
|
40
|
+
const PROJECT_NAME = 'celilo-e2e-${name}';
|
|
41
|
+
|
|
42
|
+
describe('${name}', () => {
|
|
43
|
+
let net: NetworkHandle;
|
|
44
|
+
let stageError: string | null = null;
|
|
45
|
+
|
|
46
|
+
function requireStage(stage: string) {
|
|
47
|
+
if (stageError) {
|
|
48
|
+
throw new Error(\`Skipped: \${stage} — \${stageError}\`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
afterAll(async () => {
|
|
53
|
+
await net?.stop();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('start network', async () => {
|
|
57
|
+
if (REUSE) {
|
|
58
|
+
net = await reconnectNetwork(PROJECT_NAME);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
net = await network({ projectName: PROJECT_NAME })
|
|
63
|
+
.dmz({ 'machine-1': '10.0.10.10' })
|
|
64
|
+
// .app({ 'app-1': '10.0.20.10' })
|
|
65
|
+
.start();
|
|
66
|
+
}, 120_000);
|
|
67
|
+
|
|
68
|
+
test('deploy', async () => {
|
|
69
|
+
requireStage('deploy');
|
|
70
|
+
try {
|
|
71
|
+
progress('importing module', 'module imported');
|
|
72
|
+
await net.celilo('module import /path/to/module.netapp');
|
|
73
|
+
progress.done();
|
|
74
|
+
|
|
75
|
+
progress('deploying', 'deployed');
|
|
76
|
+
await net.celilo('module deploy my-module');
|
|
77
|
+
progress.done();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
stageError = String(error);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}, 300_000);
|
|
83
|
+
|
|
84
|
+
test('verify', async () => {
|
|
85
|
+
requireStage('verify');
|
|
86
|
+
// TODO: add verification steps
|
|
87
|
+
// const result = await net.exec('machine-1', 'curl -s http://localhost');
|
|
88
|
+
// expect(result.exitCode).toBe(0);
|
|
89
|
+
}, 60_000);
|
|
90
|
+
});
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function runScaffold(options: ScaffoldOptions): void {
|
|
95
|
+
const { testsPath, name } = options;
|
|
96
|
+
|
|
97
|
+
mkdirSync(testsPath, { recursive: true });
|
|
98
|
+
|
|
99
|
+
const outPath = join(testsPath, `${name}.test.ts`);
|
|
100
|
+
if (existsSync(outPath)) {
|
|
101
|
+
console.error(`Error: ${outPath} already exists`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
writeFileSync(outPath, generateTestTemplate(name));
|
|
106
|
+
console.log(`${green}✔${reset} Created ${outPath}`);
|
|
107
|
+
console.log(`\n${dim}Run it with:\n cele2e run ${name}${reset}`);
|
|
108
|
+
}
|