@celilo/e2e 0.1.0 → 0.2.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/bin/e2e-infra +2 -2
- package/bin/e2e-run +2 -351
- package/config/dns/iamtheinternet.org.zone +2 -2
- package/package.json +1 -1
- package/src/container-manager.ts +67 -16
- package/src/docker-compose-generator.ts +34 -27
- package/src/index.ts +4 -1
- package/src/progress.ts +39 -0
- package/src/runner.ts +637 -0
- package/src/shared-infra.ts +20 -9
- package/src/types.ts +26 -1
package/bin/e2e-infra
CHANGED
|
@@ -39,7 +39,7 @@ case "${1:-status}" in
|
|
|
39
39
|
|
|
40
40
|
echo -e "${DIM}Waiting for DNS convergence...${NC}"
|
|
41
41
|
for i in $(seq 1 30); do
|
|
42
|
-
DNS=$(docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" exec -T
|
|
42
|
+
DNS=$(docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" exec -T namecheap-dns dig @127.0.0.1 iamtheinternet.org SOA +short +timeout=2 2>/dev/null || true)
|
|
43
43
|
if [ -n "$DNS" ]; then
|
|
44
44
|
echo -e "${GREEN}Shared infrastructure ready${NC}"
|
|
45
45
|
echo ""
|
|
@@ -72,7 +72,7 @@ case "${1:-status}" in
|
|
|
72
72
|
docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" ps --format " {{.Name}}\t{{.Status}}"
|
|
73
73
|
|
|
74
74
|
# Health check
|
|
75
|
-
DNS=$(docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" exec -T
|
|
75
|
+
DNS=$(docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" exec -T namecheap-dns dig @127.0.0.1 iamtheinternet.org SOA +short +timeout=2 2>/dev/null || true)
|
|
76
76
|
echo ""
|
|
77
77
|
if [ -n "$DNS" ]; then
|
|
78
78
|
echo -e " DNS: ${GREEN}healthy${NC}"
|
package/bin/e2e-run
CHANGED
|
@@ -1,354 +1,5 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
# E2E Test Runner with Live Progress Reporting
|
|
4
|
-
#
|
|
5
|
-
# Runs each test file sequentially with real-time progress display.
|
|
6
|
-
# Shows sub-steps as they happen (building, deploying, configuring).
|
|
7
|
-
# Captures full output per test in e2e/results/<timestamp>/.
|
|
8
|
-
#
|
|
9
|
-
# Shared infrastructure (DNS, Pebble, apt-cache) is started once
|
|
10
|
-
# before the first test and torn down after the last test. Per-test
|
|
11
|
-
# containers connect to the shared networks.
|
|
12
|
-
#
|
|
13
|
-
# Usage:
|
|
14
|
-
# ./e2e/bin/e2e-run # run all tests
|
|
15
|
-
# ./e2e/bin/e2e-run smoke caddy # run tests matching these patterns
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
set -e
|
|
19
|
-
|
|
2
|
+
# E2E test runner — delegates to the TypeScript runner
|
|
20
3
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
-
# Package root (where docker/, config/ live)
|
|
22
4
|
PKG_DIR="$(dirname "$SCRIPT_DIR")"
|
|
23
|
-
|
|
24
|
-
# Default: look for tests/ relative to CWD, or fall back to package dir.
|
|
25
|
-
E2E_TEST_DIR="${E2E_TEST_DIR:-$(pwd)}"
|
|
26
|
-
cd "$PKG_DIR"
|
|
27
|
-
|
|
28
|
-
SHARED_PROJECT="celilo-e2e-shared"
|
|
29
|
-
SHARED_COMPOSE="docker-compose.shared.yml"
|
|
30
|
-
|
|
31
|
-
# Handle Ctrl+C: kill running test, clean up, and exit immediately.
|
|
32
|
-
INTERRUPTED=false
|
|
33
|
-
cleanup_and_exit() {
|
|
34
|
-
INTERRUPTED=true
|
|
35
|
-
echo ""
|
|
36
|
-
echo -e "${RED}Interrupted. Cleaning up...${NC}"
|
|
37
|
-
# Kill any running test process
|
|
38
|
-
[ -n "$TEST_PID" ] && kill $TEST_PID 2>/dev/null
|
|
39
|
-
# Remove named pipe
|
|
40
|
-
[ -n "$PIPE" ] && rm -f "$PIPE"
|
|
41
|
-
# Tear down shared infra
|
|
42
|
-
echo -e "${DIM} Stopping shared infrastructure...${NC}"
|
|
43
|
-
docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" down --volumes --remove-orphans > /dev/null 2>&1 || true
|
|
44
|
-
# Clean up Docker
|
|
45
|
-
docker network prune -f > /dev/null 2>&1 || true
|
|
46
|
-
echo -e "${RED}Aborted.${NC}"
|
|
47
|
-
exit 130
|
|
48
|
-
}
|
|
49
|
-
trap cleanup_and_exit INT TERM
|
|
50
|
-
|
|
51
|
-
# Colors
|
|
52
|
-
GREEN='\033[0;32m'
|
|
53
|
-
RED='\033[0;31m'
|
|
54
|
-
DIM='\033[2m'
|
|
55
|
-
BOLD='\033[1m'
|
|
56
|
-
CYAN='\033[0;36m'
|
|
57
|
-
NC='\033[0m'
|
|
58
|
-
|
|
59
|
-
# Create results directory (in the test project, not the package)
|
|
60
|
-
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
61
|
-
RESULTS_DIR="$E2E_TEST_DIR/results/$TIMESTAMP"
|
|
62
|
-
mkdir -p "$RESULTS_DIR"
|
|
63
|
-
|
|
64
|
-
# Discover test files from E2E_TEST_DIR
|
|
65
|
-
TESTS_PATH="$E2E_TEST_DIR/tests"
|
|
66
|
-
if [ ! -d "$TESTS_PATH" ]; then
|
|
67
|
-
echo -e "${RED}No tests/ directory found at $TESTS_PATH${NC}"
|
|
68
|
-
echo "Set E2E_TEST_DIR to the directory containing your tests/ folder."
|
|
69
|
-
exit 1
|
|
70
|
-
fi
|
|
71
|
-
|
|
72
|
-
if [ $# -gt 0 ]; then
|
|
73
|
-
TEST_FILES=()
|
|
74
|
-
for pattern in "$@"; do
|
|
75
|
-
for f in "$TESTS_PATH"/*${pattern}*.test.ts; do
|
|
76
|
-
[ -f "$f" ] && TEST_FILES+=("$f")
|
|
77
|
-
done
|
|
78
|
-
done
|
|
79
|
-
else
|
|
80
|
-
TEST_FILES=("$TESTS_PATH"/*.test.ts)
|
|
81
|
-
fi
|
|
82
|
-
|
|
83
|
-
TOTAL=${#TEST_FILES[@]}
|
|
84
|
-
PASSED=0
|
|
85
|
-
FAILED=0
|
|
86
|
-
RESULTS=()
|
|
87
|
-
|
|
88
|
-
echo ""
|
|
89
|
-
echo -e "${BOLD}╔══════════════════════════════════════════════════════════════╗${NC}"
|
|
90
|
-
echo -e "${BOLD}║ Celilo E2E Tests ║${NC}"
|
|
91
|
-
printf "${BOLD}║ %d test(s) queued — %s ║${NC}\n" "$TOTAL" "$(date +%H:%M:%S)"
|
|
92
|
-
echo -e "${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}"
|
|
93
|
-
echo ""
|
|
94
|
-
|
|
95
|
-
SUITE_START=$(date +%s)
|
|
96
|
-
|
|
97
|
-
# ─── Shared Infrastructure Startup ────────────────────────────────────
|
|
98
|
-
# The first test will call ensureSharedInfra() which starts the shared
|
|
99
|
-
# project. But we also pre-check here so the runner can show progress.
|
|
100
|
-
echo -e "${BOLD}Shared Infrastructure${NC}"
|
|
101
|
-
|
|
102
|
-
# Check if shared infra is already running
|
|
103
|
-
SHARED_RUNNING=false
|
|
104
|
-
SHARED_CONTAINERS=$(docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" ps -q 2>/dev/null || true)
|
|
105
|
-
if [ -n "$SHARED_CONTAINERS" ]; then
|
|
106
|
-
# Verify DNS is healthy
|
|
107
|
-
DNS_CHECK=$(docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" exec -T comcast-resolver dig @127.0.0.1 iamtheinternet.org NS +short +timeout=2 2>/dev/null || true)
|
|
108
|
-
if [ -n "$DNS_CHECK" ]; then
|
|
109
|
-
SHARED_RUNNING=true
|
|
110
|
-
echo -e " ${GREEN}✔ already running (DNS healthy)${NC}"
|
|
111
|
-
else
|
|
112
|
-
echo -e " ${DIM}DNS unhealthy, restarting...${NC}"
|
|
113
|
-
docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" down --volumes --remove-orphans > /dev/null 2>&1 || true
|
|
114
|
-
fi
|
|
115
|
-
fi
|
|
116
|
-
|
|
117
|
-
if [ "$SHARED_RUNNING" = false ]; then
|
|
118
|
-
echo -e " ${DIM}Starting DNS hierarchy, Pebble ACME, apt-cache...${NC}"
|
|
119
|
-
# The actual startup happens inside the first test's ensureSharedInfra()
|
|
120
|
-
# call. We just report it here for visibility.
|
|
121
|
-
fi
|
|
122
|
-
|
|
123
|
-
SHARED_END=$(date +%s)
|
|
124
|
-
echo ""
|
|
125
|
-
|
|
126
|
-
# Track step count per test for percentage
|
|
127
|
-
STEP_COUNT=0
|
|
128
|
-
ESTIMATED_STEPS=15
|
|
129
|
-
|
|
130
|
-
# Parse a line of test output and display as a progress step.
|
|
131
|
-
show_progress() {
|
|
132
|
-
local line="$1"
|
|
133
|
-
local test_start="$2"
|
|
134
|
-
local step=""
|
|
135
|
-
|
|
136
|
-
# Shared infra progress (from ensureSharedInfra)
|
|
137
|
-
if [[ "$line" == *"[e2e] Shared infrastructure ready"* ]]; then
|
|
138
|
-
step="✔ shared infrastructure ready"
|
|
139
|
-
elif [[ "$line" == *"[e2e] Building shared"* ]]; then
|
|
140
|
-
step="building shared infrastructure images"
|
|
141
|
-
elif [[ "$line" == *"[e2e] Starting shared"* ]]; then
|
|
142
|
-
step="starting shared infrastructure"
|
|
143
|
-
elif [[ "$line" == *"[e2e] Cleaning up stale shared"* ]]; then
|
|
144
|
-
step="cleaning up stale shared infra"
|
|
145
|
-
|
|
146
|
-
# Per-test container manager progress
|
|
147
|
-
elif [[ "$line" == *"[e2e] Building images"* ]]; then
|
|
148
|
-
step="building test images"
|
|
149
|
-
elif [[ "$line" == *"[e2e] Starting containers"* ]]; then
|
|
150
|
-
step="starting test containers"
|
|
151
|
-
elif [[ "$line" == *"[e2e] Cleaning up"* ]]; then
|
|
152
|
-
step="cleaning up stale test resources"
|
|
153
|
-
elif [[ "$line" == *"[e2e] Waiting for DNS"* ]]; then
|
|
154
|
-
step="waiting for DNS convergence"
|
|
155
|
-
elif [[ "$line" == *"[e2e] Verifying routing"* ]]; then
|
|
156
|
-
step="verifying routing"
|
|
157
|
-
elif [[ "$line" == *"[e2e] Waiting for target"* ]]; then
|
|
158
|
-
step="waiting for target machines"
|
|
159
|
-
elif [[ "$line" == *"[e2e] Waiting for DHCP"* ]]; then
|
|
160
|
-
step="waiting for DHCP"
|
|
161
|
-
elif [[ "$line" == *"[e2e] Initializing celilo"* ]]; then
|
|
162
|
-
step="initializing celilo"
|
|
163
|
-
elif [[ "$line" == *"[e2e] Network ready"* ]]; then
|
|
164
|
-
step="network ready"
|
|
165
|
-
elif [[ "$line" == *"[e2e] Stopping network"* ]]; then
|
|
166
|
-
step="stopping network"
|
|
167
|
-
|
|
168
|
-
# Test-level progress (console.log from tests)
|
|
169
|
-
elif [[ "$line" == *"Adding machines"* ]]; then
|
|
170
|
-
step="adding machines"
|
|
171
|
-
elif [[ "$line" == *"Importing modules"* ]]; then
|
|
172
|
-
step="importing modules"
|
|
173
|
-
elif [[ "$line" == *"Configuring modules"* ]]; then
|
|
174
|
-
step="configuring modules"
|
|
175
|
-
elif [[ "$line" == *"Prerequisites ready"* ]]; then
|
|
176
|
-
step="prerequisites ready"
|
|
177
|
-
|
|
178
|
-
# Celilo CLI: "Deploying module: namecheap"
|
|
179
|
-
elif [[ "$line" == *"Deploying module:"* ]]; then
|
|
180
|
-
local module=$(echo "$line" | sed -n 's/.*Deploying module: \([^ ]*\).*/\1/p')
|
|
181
|
-
if [ -n "$module" ]; then
|
|
182
|
-
step="deploying $module"
|
|
183
|
-
else
|
|
184
|
-
step="deploying module"
|
|
185
|
-
fi
|
|
186
|
-
# Celilo CLI: "Deploying software..."
|
|
187
|
-
elif [[ "$line" == *"Deploying software"* ]]; then
|
|
188
|
-
step="running Ansible"
|
|
189
|
-
|
|
190
|
-
# Test console.log: "[45s] Deploying namecheap..."
|
|
191
|
-
elif [[ "$line" =~ Deploying\ ([a-z_-]+) ]]; then
|
|
192
|
-
step="deploying ${BASH_REMATCH[1]}"
|
|
193
|
-
|
|
194
|
-
# Celilo CLI: "✓ Module 'namecheap' deployed successfully"
|
|
195
|
-
elif [[ "$line" == *"deployed successfully"* ]]; then
|
|
196
|
-
local module=$(echo "$line" | sed -n "s/.*Module '\([^']*\)' deployed.*/\1/p")
|
|
197
|
-
step="✔ ${module:-module} deployed"
|
|
198
|
-
|
|
199
|
-
# Celilo CLI: "Successfully imported module: namecheap"
|
|
200
|
-
elif [[ "$line" == *"Successfully imported module:"* ]]; then
|
|
201
|
-
local module=$(echo "$line" | sed -n 's/.*imported module: \([^ ]*\).*/\1/p')
|
|
202
|
-
step="✔ ${module:-module} imported"
|
|
203
|
-
|
|
204
|
-
# Celilo CLI: "Infrastructure selected: machine"
|
|
205
|
-
elif [[ "$line" == *"Infrastructure selected"* ]]; then
|
|
206
|
-
step="infrastructure selected"
|
|
207
|
-
# Celilo CLI: "Terraform"
|
|
208
|
-
elif [[ "$line" == *"Terraform skipped"* ]]; then
|
|
209
|
-
step="terraform skipped (existing machine)"
|
|
210
|
-
elif [[ "$line" == *"terraform init"* ]] || [[ "$line" == *"Terraform init"* ]]; then
|
|
211
|
-
step="terraform init"
|
|
212
|
-
elif [[ "$line" == *"terraform apply"* ]] || [[ "$line" == *"Terraform apply"* ]]; then
|
|
213
|
-
step="terraform apply"
|
|
214
|
-
|
|
215
|
-
# Ansible health retries
|
|
216
|
-
elif [[ "$line" == *"RETRYING"* ]] && [[ "$line" == *"healthy"* ]]; then
|
|
217
|
-
local retries=$(echo "$line" | sed -n 's/.*(\([0-9]*\) retries left).*/\1/p')
|
|
218
|
-
step="waiting for health check ($retries retries left)"
|
|
219
|
-
|
|
220
|
-
# Skip noise
|
|
221
|
-
elif [[ "$line" == *"Set config for"* ]]; then return
|
|
222
|
-
elif [[ "$line" == *"Set secret for"* ]]; then return
|
|
223
|
-
elif [[ "$line" == *"Auto-generated"* ]]; then return
|
|
224
|
-
elif [[ "$line" == *"Warning: Permanently added"* ]]; then return
|
|
225
|
-
|
|
226
|
-
# Error detection
|
|
227
|
-
elif [[ "$line" == *"Error:"* ]] && [[ "$line" != *"vitest"* ]] && [[ "$line" != *"bun test"* ]] && [[ "$line" != *"ipv4: Address already"* ]]; then
|
|
228
|
-
local err=$(echo "$line" | sed 's/.*Error: //' | cut -c1-70)
|
|
229
|
-
step="✗ $err"
|
|
230
|
-
fi
|
|
231
|
-
|
|
232
|
-
if [ -n "$step" ]; then
|
|
233
|
-
STEP_COUNT=$((STEP_COUNT + 1))
|
|
234
|
-
# Update estimate upward if we're exceeding it
|
|
235
|
-
if [ $STEP_COUNT -gt $ESTIMATED_STEPS ]; then
|
|
236
|
-
ESTIMATED_STEPS=$((STEP_COUNT + 5))
|
|
237
|
-
fi
|
|
238
|
-
local pct=$(( (STEP_COUNT * 100) / ESTIMATED_STEPS ))
|
|
239
|
-
[ $pct -gt 99 ] && pct=99 # Don't show 100% until actually done
|
|
240
|
-
local now=$(date +%s)
|
|
241
|
-
local elapsed=$((now - test_start))
|
|
242
|
-
printf " ${DIM}%3ds${NC} ${CYAN}%2d%%${NC} %s\n" "$elapsed" "$pct" "$step"
|
|
243
|
-
fi
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
for ((i=0; i<TOTAL; i++)); do
|
|
247
|
-
FILE="${TEST_FILES[$i]}"
|
|
248
|
-
NAME=$(basename "$FILE" .test.ts)
|
|
249
|
-
TEST_DIR="$RESULTS_DIR/$NAME"
|
|
250
|
-
mkdir -p "$TEST_DIR"
|
|
251
|
-
|
|
252
|
-
# Percentage complete (based on test count, not time)
|
|
253
|
-
PCT=$(( (i * 100) / TOTAL ))
|
|
254
|
-
ELAPSED_TOTAL=$(($(date +%s) - SUITE_START))
|
|
255
|
-
if [ $i -gt 0 ]; then
|
|
256
|
-
AVG_PER_TEST=$((ELAPSED_TOTAL / i))
|
|
257
|
-
REMAINING=$(( (TOTAL - i) * AVG_PER_TEST ))
|
|
258
|
-
ETA_MIN=$((REMAINING / 60))
|
|
259
|
-
ETA_SEC=$((REMAINING % 60))
|
|
260
|
-
ETA_STR=" — ~${ETA_MIN}m ${ETA_SEC}s remaining"
|
|
261
|
-
else
|
|
262
|
-
ETA_STR=""
|
|
263
|
-
fi
|
|
264
|
-
|
|
265
|
-
echo -e "${BOLD}[${i}/${TOTAL}] ${CYAN}${PCT}%${NC}${BOLD} ${NAME}${NC}${DIM}${ETA_STR}${NC}"
|
|
266
|
-
|
|
267
|
-
STEP_COUNT=0
|
|
268
|
-
TEST_START=$(date +%s)
|
|
269
|
-
PIPE=$(mktemp -u)
|
|
270
|
-
mkfifo "$PIPE"
|
|
271
|
-
|
|
272
|
-
# Background: run bun test, merge stdout+stderr, tee to log + pipe
|
|
273
|
-
(bun test --timeout 300000 "$FILE" 2>&1 | tee "$TEST_DIR/output.log" > "$PIPE") &
|
|
274
|
-
TEST_PID=$!
|
|
275
|
-
|
|
276
|
-
# Foreground: read pipe line-by-line and show progress
|
|
277
|
-
while IFS= read -r line; do
|
|
278
|
-
show_progress "$line" "$TEST_START"
|
|
279
|
-
done < "$PIPE"
|
|
280
|
-
|
|
281
|
-
# Wait for test to finish and get exit code
|
|
282
|
-
wait $TEST_PID 2>/dev/null
|
|
283
|
-
EXIT_CODE=$?
|
|
284
|
-
rm -f "$PIPE"
|
|
285
|
-
|
|
286
|
-
TEST_END=$(date +%s)
|
|
287
|
-
DURATION=$((TEST_END - TEST_START))
|
|
288
|
-
|
|
289
|
-
if [ $EXIT_CODE -eq 0 ]; then
|
|
290
|
-
echo -e " ${GREEN}✔ passed (${DURATION}s)${NC}"
|
|
291
|
-
PASSED=$((PASSED + 1))
|
|
292
|
-
RESULTS+=("✓|$NAME|${DURATION}s")
|
|
293
|
-
echo '{"status":"pass","duration":'$DURATION'}' > "$TEST_DIR/result.json"
|
|
294
|
-
else
|
|
295
|
-
echo -e " ${RED}✗ failed (${DURATION}s)${NC}"
|
|
296
|
-
FAILED=$((FAILED + 1))
|
|
297
|
-
ERROR=$(grep -E "Error:|expected.*to be|Timeout" "$TEST_DIR/output.log" 2>/dev/null | grep -v "vitest\|bun test" | head -1 | sed 's/.*Error: //' | cut -c1-80)
|
|
298
|
-
RESULTS+=("✗|$NAME|${DURATION}s|$ERROR")
|
|
299
|
-
echo '{"status":"fail","duration":'$DURATION',"error":"'"${ERROR//\"/\\\"}"'"}' > "$TEST_DIR/result.json"
|
|
300
|
-
echo -e " ${RED} └─ ${ERROR}${NC}"
|
|
301
|
-
fi
|
|
302
|
-
echo ""
|
|
303
|
-
|
|
304
|
-
# Clean up per-test Docker resources between tests (not shared infra)
|
|
305
|
-
docker network prune -f > /dev/null 2>&1 || true
|
|
306
|
-
done
|
|
307
|
-
|
|
308
|
-
# ─── Shared Infrastructure Teardown ───────────────────────────────────
|
|
309
|
-
echo -e "${DIM}Stopping shared infrastructure...${NC}"
|
|
310
|
-
docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" down --volumes --remove-orphans > /dev/null 2>&1 || true
|
|
311
|
-
docker network prune -f > /dev/null 2>&1 || true
|
|
312
|
-
|
|
313
|
-
SUITE_END=$(date +%s)
|
|
314
|
-
SUITE_DURATION=$((SUITE_END - SUITE_START))
|
|
315
|
-
SUITE_MINUTES=$((SUITE_DURATION / 60))
|
|
316
|
-
SUITE_SECONDS=$((SUITE_DURATION % 60))
|
|
317
|
-
|
|
318
|
-
echo ""
|
|
319
|
-
echo -e "${BOLD}╔══════════════════════════════════════════════════════════════╗${NC}"
|
|
320
|
-
printf "${BOLD}║ Results: ${GREEN}%d passed${NC}${BOLD}, ${RED}%d failed${NC}${BOLD} — %dm %ds total ║${NC}\n" "$PASSED" "$FAILED" "$SUITE_MINUTES" "$SUITE_SECONDS"
|
|
321
|
-
echo -e "${BOLD}╠══════════════════════════════════════════════════════════════╣${NC}"
|
|
322
|
-
|
|
323
|
-
for result in "${RESULTS[@]}"; do
|
|
324
|
-
IFS='|' read -r icon name duration error <<< "$result"
|
|
325
|
-
if [ "$icon" = "✓" ]; then
|
|
326
|
-
printf "${BOLD}║${NC} ${GREEN}%s${NC} %-28s ${DIM}%6s${NC}\n" "$icon" "$name" "$duration"
|
|
327
|
-
else
|
|
328
|
-
printf "${BOLD}║${NC} ${RED}%s${NC} %-28s ${DIM}%6s${NC}\n" "$icon" "$name" "$duration"
|
|
329
|
-
if [ -n "$error" ]; then
|
|
330
|
-
printf "${BOLD}║${NC} ${RED}└─ %s${NC}\n" "$error"
|
|
331
|
-
fi
|
|
332
|
-
fi
|
|
333
|
-
done
|
|
334
|
-
|
|
335
|
-
echo -e "${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}"
|
|
336
|
-
echo ""
|
|
337
|
-
echo -e "Full logs: ${DIM}$RESULTS_DIR${NC}"
|
|
338
|
-
|
|
339
|
-
# Write summary
|
|
340
|
-
cat > "$RESULTS_DIR/summary.json" <<EOF
|
|
341
|
-
{
|
|
342
|
-
"timestamp": "$TIMESTAMP",
|
|
343
|
-
"total": $TOTAL,
|
|
344
|
-
"passed": $PASSED,
|
|
345
|
-
"failed": $FAILED,
|
|
346
|
-
"duration": $SUITE_DURATION
|
|
347
|
-
}
|
|
348
|
-
EOF
|
|
349
|
-
|
|
350
|
-
# Add results dir to gitignore if not already there
|
|
351
|
-
grep -q "^results/" "$E2E_TEST_DIR/.gitignore" 2>/dev/null || echo "results/" >> "$E2E_TEST_DIR/.gitignore"
|
|
352
|
-
|
|
353
|
-
# Exit with failure if any tests failed
|
|
354
|
-
[ $FAILED -eq 0 ]
|
|
5
|
+
exec bun "$PKG_DIR/src/runner.ts" "$@"
|
|
@@ -2,14 +2,14 @@ $ORIGIN iamtheinternet.org.
|
|
|
2
2
|
$TTL 300
|
|
3
3
|
|
|
4
4
|
@ IN SOA ns1.iamtheinternet.org. admin.iamtheinternet.org. (
|
|
5
|
-
|
|
5
|
+
2024010764 300 60 604800 300
|
|
6
6
|
)
|
|
7
7
|
|
|
8
8
|
@ IN NS ns1.iamtheinternet.org.
|
|
9
9
|
ns1 IN A 100.64.0.55
|
|
10
10
|
@ IN A 0.0.0.0
|
|
11
11
|
caddy IN A 100.100.0.100
|
|
12
|
-
www IN A
|
|
12
|
+
www IN A 10.0.10.10
|
|
13
13
|
deathstar IN A 100.100.0.100
|
|
14
14
|
test IN A 0.0.0.0
|
|
15
15
|
auth IN A 100.100.0.100
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celilo/e2e",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "E2E test infrastructure for Celilo-deployed applications. Provides a simulated internet with DNS hierarchy, ACME server, firewalls, and target machines in Docker.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
package/src/container-manager.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { execSync, spawn, type ExecSyncOptions } from 'node:child_process';
|
|
1
|
+
import { execSync, spawn, spawnSync, type ExecSyncOptions } from 'node:child_process';
|
|
2
2
|
import { existsSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import type { ExecResult, NetworkConfig, NetworkHandle } from './types';
|
|
5
|
-
import {
|
|
5
|
+
import { CeliloCommandError } from './types';
|
|
6
|
+
import { generateTestComposeYaml, SHARED_PROJECT_NAME } from './docker-compose-generator';
|
|
6
7
|
import { ensureSharedInfra } from './shared-infra';
|
|
7
8
|
|
|
8
9
|
/** Package root — where docker/, config/, simulators/ live */
|
|
@@ -68,11 +69,22 @@ function run(cmd: string, opts?: ExecSyncOptions): string {
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
const COMPOSE_FILE = 'docker-compose.test.yml';
|
|
72
|
+
const SHARED_COMPOSE_FILE = 'docker-compose.shared.yml';
|
|
73
|
+
|
|
74
|
+
/** Containers that live in the shared infra project, not the per-test project */
|
|
75
|
+
const SHARED_CONTAINERS = new Set([
|
|
76
|
+
'root-dns', 'tld-dns', 'namecheap-dns', 'letsencrypt', 'apt-cache',
|
|
77
|
+
]);
|
|
71
78
|
|
|
72
79
|
function dockerExec(projectName: string, composeDir: string, container: string, cmd: string, timeoutMs = 60_000): ExecResult {
|
|
80
|
+
// Use the shared infra project for shared containers
|
|
81
|
+
const isShared = SHARED_CONTAINERS.has(container);
|
|
82
|
+
const project = isShared ? SHARED_PROJECT_NAME : projectName;
|
|
83
|
+
const composeFile = isShared ? SHARED_COMPOSE_FILE : COMPOSE_FILE;
|
|
84
|
+
|
|
73
85
|
try {
|
|
74
86
|
const stdout = run(
|
|
75
|
-
`docker compose -f ${
|
|
87
|
+
`docker compose -f ${composeFile} -p ${project} exec -T ${container} bash -c ${JSON.stringify(cmd)}`,
|
|
76
88
|
{ cwd: composeDir, timeout: timeoutMs },
|
|
77
89
|
);
|
|
78
90
|
return { stdout, stderr: '', exitCode: 0 };
|
|
@@ -167,7 +179,7 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
167
179
|
await ensureSharedInfra();
|
|
168
180
|
|
|
169
181
|
// Clean up stale per-test containers (self-healing from prior crashes)
|
|
170
|
-
console.log('[
|
|
182
|
+
console.log('[progress:start] cleaning up stale test resources | cleanup complete');
|
|
171
183
|
try {
|
|
172
184
|
// Force-kill per-test containers (not shared infra)
|
|
173
185
|
try {
|
|
@@ -205,18 +217,27 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
205
217
|
writeFileSync(join(composeDir, COMPOSE_FILE), yaml);
|
|
206
218
|
|
|
207
219
|
// Build and start per-test containers only
|
|
208
|
-
console.log('[
|
|
220
|
+
console.log('[progress:start] building Docker images | images built');
|
|
209
221
|
run(`docker compose -f ${COMPOSE_FILE} -p ${projectName} build`, { cwd: composeDir, timeout: 300_000 });
|
|
210
222
|
|
|
211
|
-
console.log('[
|
|
223
|
+
console.log('[progress:start] starting containers | containers running');
|
|
212
224
|
run(`docker compose -f ${COMPOSE_FILE} -p ${projectName} up -d`, { cwd: composeDir, timeout: 120_000 });
|
|
213
225
|
|
|
214
|
-
// DNS convergence is
|
|
215
|
-
|
|
226
|
+
// Wait for DNS convergence (comcast-resolver is per-test, needs to reach shared DNS)
|
|
227
|
+
console.log('[progress:start] waiting for DNS convergence | DNS converged');
|
|
228
|
+
await waitFor(
|
|
229
|
+
async () => {
|
|
230
|
+
const result = dockerExec(projectName, composeDir, 'comcast-resolver',
|
|
231
|
+
'dig @127.0.0.1 iamtheinternet.org NS +short +timeout=2');
|
|
232
|
+
return result.exitCode === 0 && result.stdout.trim().length > 0;
|
|
233
|
+
},
|
|
234
|
+
60_000,
|
|
235
|
+
'DNS convergence',
|
|
236
|
+
);
|
|
216
237
|
|
|
217
238
|
// Optional routing verification
|
|
218
239
|
if (config.verifyRouting) {
|
|
219
|
-
console.log('[
|
|
240
|
+
console.log('[progress:start] verifying routing | routing verified');
|
|
220
241
|
await waitFor(
|
|
221
242
|
async () => {
|
|
222
243
|
const result = dockerExec(projectName, composeDir, 'management',
|
|
@@ -231,7 +252,7 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
231
252
|
// Wait for all target machines to complete setup (systemd boot + routing)
|
|
232
253
|
const allMachines = [...config.dmzMachines, ...config.appMachines, ...config.secureMachines, ...config.internalMachines];
|
|
233
254
|
if (allMachines.length > 0) {
|
|
234
|
-
console.log('[
|
|
255
|
+
console.log('[progress:start] waiting for target machines | target machines ready');
|
|
235
256
|
}
|
|
236
257
|
for (const machine of allMachines) {
|
|
237
258
|
await waitFor(
|
|
@@ -247,7 +268,7 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
247
268
|
|
|
248
269
|
// Wait for DHCP client lease
|
|
249
270
|
if (config.dhcpClient) {
|
|
250
|
-
console.log('[
|
|
271
|
+
console.log('[progress:start] waiting for DHCP client lease | DHCP lease acquired');
|
|
251
272
|
await waitFor(
|
|
252
273
|
async () => {
|
|
253
274
|
const result = dockerExec(projectName, composeDir, 'dhcp-client',
|
|
@@ -273,7 +294,7 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
273
294
|
}
|
|
274
295
|
|
|
275
296
|
// Initialize celilo
|
|
276
|
-
console.log('[
|
|
297
|
+
console.log('[progress:start] initializing celilo | celilo initialized');
|
|
277
298
|
const initResult = dockerExec(projectName, composeDir, 'management',
|
|
278
299
|
`cd /celilo/apps/celilo && bun run src/cli/index.ts system init --accept-defaults \
|
|
279
300
|
network.dmz.subnet=10.0.10.0/24 \
|
|
@@ -286,14 +307,22 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
286
307
|
throw new Error(`Celilo init failed: ${initResult.stderr}`);
|
|
287
308
|
}
|
|
288
309
|
|
|
289
|
-
console.log(
|
|
310
|
+
console.log('[progress:done] network ready');
|
|
290
311
|
|
|
291
312
|
const handle: NetworkHandle = {
|
|
292
313
|
projectName,
|
|
293
314
|
|
|
294
|
-
celilo(cmd: string, timeoutMs
|
|
295
|
-
|
|
315
|
+
async celilo(cmd: string, optsOrTimeout?: number | { check?: boolean; timeoutMs?: number }): Promise<ExecResult> {
|
|
316
|
+
const check = typeof optsOrTimeout === 'object' ? (optsOrTimeout.check ?? true) : true;
|
|
317
|
+
const timeoutMs = typeof optsOrTimeout === 'number' ? optsOrTimeout : (optsOrTimeout?.timeoutMs ?? 120_000);
|
|
318
|
+
|
|
319
|
+
const result = await dockerExecAsync(projectName, composeDir, 'management',
|
|
296
320
|
`cd /celilo/apps/celilo && bun run src/cli/index.ts ${cmd}`, timeoutMs);
|
|
321
|
+
|
|
322
|
+
if (check && result.exitCode !== 0) {
|
|
323
|
+
throw new CeliloCommandError(cmd, result);
|
|
324
|
+
}
|
|
325
|
+
return result;
|
|
297
326
|
},
|
|
298
327
|
|
|
299
328
|
exec(container: string, cmd: string, timeoutMs = 60_000): Promise<ExecResult> {
|
|
@@ -306,10 +335,32 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
306
335
|
return result.stdout.trim();
|
|
307
336
|
},
|
|
308
337
|
|
|
338
|
+
async debug(container = 'management'): Promise<void> {
|
|
339
|
+
// Signal the runner to hand us the terminal
|
|
340
|
+
console.log(`[debug:pause] ${projectName} ${container}`);
|
|
341
|
+
|
|
342
|
+
// The runner intercepts [debug:pause] and spawns the interactive
|
|
343
|
+
// shell itself (since it owns the terminal). It writes a signal
|
|
344
|
+
// file when the user exits the shell.
|
|
345
|
+
const signalFile = join(composeDir, `.debug-resume-${process.pid}`);
|
|
346
|
+
|
|
347
|
+
// Wait for the runner to create the signal file
|
|
348
|
+
const start = Date.now();
|
|
349
|
+
const timeout = 86_400_000; // 24 hour max debug session
|
|
350
|
+
while (!existsSync(signalFile) && Date.now() - start < timeout) {
|
|
351
|
+
await new Promise(r => setTimeout(r, 500));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Clean up
|
|
355
|
+
try { require('node:fs').unlinkSync(signalFile); } catch {}
|
|
356
|
+
|
|
357
|
+
console.log('[debug:resumed]');
|
|
358
|
+
},
|
|
359
|
+
|
|
309
360
|
waitFor,
|
|
310
361
|
|
|
311
362
|
async stop(): Promise<void> {
|
|
312
|
-
console.log(
|
|
363
|
+
console.log('[progress:start] stopping network | network stopped');
|
|
313
364
|
try {
|
|
314
365
|
run(`docker compose -f ${COMPOSE_FILE} -p ${projectName} down --volumes --remove-orphans`,
|
|
315
366
|
{ cwd: composeDir, timeout: 60_000 });
|
|
@@ -65,7 +65,6 @@ export const SHARED_PROJECT_NAME = 'celilo-e2e-shared';
|
|
|
65
65
|
|
|
66
66
|
/** Docker network names as created by the shared infrastructure project */
|
|
67
67
|
export const SHARED_NETWORKS = {
|
|
68
|
-
'isp-external': `${SHARED_PROJECT_NAME}_isp-external`,
|
|
69
68
|
'internet-external': `${SHARED_PROJECT_NAME}_internet-external`,
|
|
70
69
|
'real-internet': `${SHARED_PROJECT_NAME}_real-internet`,
|
|
71
70
|
} as const;
|
|
@@ -140,12 +139,14 @@ const TOPOLOGY_SERVICES: Record<TopologyPreset, () => Record<string, unknown>> =
|
|
|
140
139
|
|
|
141
140
|
/**
|
|
142
141
|
* Generate the compose YAML for shared infrastructure that runs once
|
|
143
|
-
* per test suite. Contains:
|
|
144
|
-
* Owns the
|
|
142
|
+
* per test suite. Contains: DNS hierarchy, Pebble ACME, apt-cache.
|
|
143
|
+
* Owns the internet-external and real-internet networks.
|
|
144
|
+
*
|
|
145
|
+
* fw-ext and comcast-resolver are per-test because they route to
|
|
146
|
+
* per-test containers (fw-isp/fw-main) and need those to exist first.
|
|
145
147
|
*/
|
|
146
148
|
export function generateSharedInfraYaml(): string {
|
|
147
149
|
const networks: Record<string, unknown> = {
|
|
148
|
-
'isp-external': networkDef('100.100.0.0/24', '100.100.0.250'),
|
|
149
150
|
'internet-external': networkDef('100.64.0.0/24', '100.64.0.250'),
|
|
150
151
|
'real-internet': {
|
|
151
152
|
driver: 'bridge',
|
|
@@ -159,26 +160,6 @@ export function generateSharedInfraYaml(): string {
|
|
|
159
160
|
|
|
160
161
|
const services: Record<string, unknown> = {};
|
|
161
162
|
|
|
162
|
-
services['fw-ext'] = baseService({
|
|
163
|
-
build: { context: '.', dockerfile: 'docker/Dockerfile.router' },
|
|
164
|
-
networks: {
|
|
165
|
-
'isp-external': { ipv4_address: '100.100.0.101' },
|
|
166
|
-
'internet-external': { ipv4_address: '100.64.0.1' },
|
|
167
|
-
'real-internet': { ipv4_address: '172.30.0.5' },
|
|
168
|
-
},
|
|
169
|
-
cap_add: ['NET_ADMIN', 'SYS_ADMIN'],
|
|
170
|
-
sysctls: ROUTER_SYSCTLS,
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
services['comcast-resolver'] = baseService({
|
|
174
|
-
build: { context: '.', dockerfile: 'docker/Dockerfile.resolver' },
|
|
175
|
-
networks: {
|
|
176
|
-
'isp-external': { ipv4_address: '100.100.0.1' },
|
|
177
|
-
'real-internet': { ipv4_address: '172.30.0.4' },
|
|
178
|
-
},
|
|
179
|
-
cap_add: ['NET_ADMIN'],
|
|
180
|
-
});
|
|
181
|
-
|
|
182
163
|
services['root-dns'] = baseService({
|
|
183
164
|
build: { context: '.', dockerfile: 'docker/Dockerfile.authoritative-dns' },
|
|
184
165
|
networks: { 'internet-external': { ipv4_address: '100.64.0.53' } },
|
|
@@ -252,10 +233,15 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot = '..'
|
|
|
252
233
|
dmz: networkDef('10.0.10.0/24', '10.0.10.250'),
|
|
253
234
|
app: networkDef('10.0.20.0/24', '10.0.20.250'),
|
|
254
235
|
secure: networkDef('10.0.30.0/24', '10.0.30.250'),
|
|
255
|
-
|
|
256
|
-
|
|
236
|
+
'isp-external': networkDef('100.100.0.0/24', '100.100.0.250'),
|
|
237
|
+
// internet-external and real-internet are owned by shared infra
|
|
238
|
+
'internet-external': {
|
|
257
239
|
external: true,
|
|
258
|
-
name: SHARED_NETWORKS['
|
|
240
|
+
name: SHARED_NETWORKS['internet-external'],
|
|
241
|
+
},
|
|
242
|
+
'real-internet': {
|
|
243
|
+
external: true,
|
|
244
|
+
name: SHARED_NETWORKS['real-internet'],
|
|
259
245
|
},
|
|
260
246
|
};
|
|
261
247
|
|
|
@@ -287,6 +273,27 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot = '..'
|
|
|
287
273
|
const topologyFn = TOPOLOGY_SERVICES[config.topology] || TOPOLOGY_SERVICES.default;
|
|
288
274
|
Object.assign(services, topologyFn());
|
|
289
275
|
|
|
276
|
+
// --- Routing infrastructure (bridges per-test to shared infra) ---
|
|
277
|
+
services['fw-ext'] = baseService({
|
|
278
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.router' },
|
|
279
|
+
networks: {
|
|
280
|
+
'isp-external': { ipv4_address: '100.100.0.101' },
|
|
281
|
+
'internet-external': { ipv4_address: '100.64.0.1' },
|
|
282
|
+
'real-internet': { ipv4_address: '172.30.0.5' },
|
|
283
|
+
},
|
|
284
|
+
cap_add: ['NET_ADMIN', 'SYS_ADMIN'],
|
|
285
|
+
sysctls: ROUTER_SYSCTLS,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
services['comcast-resolver'] = baseService({
|
|
289
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.resolver' },
|
|
290
|
+
networks: {
|
|
291
|
+
'isp-external': { ipv4_address: '100.100.0.1' },
|
|
292
|
+
'real-internet': { ipv4_address: '172.30.0.4' },
|
|
293
|
+
},
|
|
294
|
+
cap_add: ['NET_ADMIN'],
|
|
295
|
+
});
|
|
296
|
+
|
|
290
297
|
// --- Dynamic test machines ---
|
|
291
298
|
const allMachines = [
|
|
292
299
|
...config.dmzMachines,
|