-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlocal-test.sh
More file actions
executable file
·445 lines (388 loc) · 12 KB
/
Copy pathlocal-test.sh
File metadata and controls
executable file
·445 lines (388 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
#!/usr/bin/env bash
# Local test runner — mirrors .github/workflows/ci.yml on your machine.
# Usage: ./local-test [command] [--no-cov]
# (default) all — Postgres + migrations + Python + web + .NET (Data, Bff)
# python — DB + pytest + CLI smoke only
# web — build, typecheck, lint, vitest (no Postgres)
# dotnet — dotnet test services/WebsiteProfiling.slnx + Bff OpenAPI drift gate
# quick — pytest --no-cov + web + dotnet (DB must already be running)
# help — show commands
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
PG_CONTAINER="${WP_PG_CONTAINER:-wp-pg}"
PG_IMAGE="${WP_PG_IMAGE:-postgres:16-alpine}"
PG_PORT="${WP_PG_PORT:-5432}"
PG_USER="${WP_PG_USER:-postgres}"
PG_PASSWORD="${WP_PG_PASSWORD:-dev}"
PG_DB="${WP_PG_DB:-website_profiling}"
export DATABASE_URL="${DATABASE_URL:-postgres://${PG_USER}:${PG_PASSWORD}@127.0.0.1:${PG_PORT}/${PG_DB}}"
export DATA_DIR="${DATA_DIR:-$ROOT/data}"
export WEBSITE_PROFILING_ROOT="$ROOT"
export PYTHONPATH="${PYTHONPATH:+$PYTHONPATH:}$ROOT/src"
VENV="$ROOT/.venv"
WEB="$ROOT/web"
PYTEST_NO_COV=0
STEP_PASS=()
STEP_FAIL=() # entries: "name|detail"
STEP_SKIP=() # entries: "name|reason"
log() { printf '\033[1;36m→\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m✓\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m!\033[0m %s\n' "$*" >&2; }
fail_msg() { printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; }
die() { fail_msg "$*"; exit 1; }
reset_steps() {
STEP_PASS=()
STEP_FAIL=()
STEP_SKIP=()
}
run_step() {
local name="$1"
shift
log "$name"
local ec=0
"$@" || ec=$?
if [[ "$ec" -eq 0 ]]; then
STEP_PASS+=("$name")
else
STEP_FAIL+=("$name|exit code $ec")
fi
}
skip_step() {
local name="$1"
local reason="${2:-skipped}"
warn "$name — $reason"
STEP_SKIP+=("$name|$reason")
}
print_summary() {
local total_pass=${#STEP_PASS[@]}
local total_fail=${#STEP_FAIL[@]}
local total_skip=${#STEP_SKIP[@]}
local entry name detail
printf '\n'
printf '\033[1m═══════════════════════════════════════════════════════════════\033[0m\n'
printf '\033[1m Test summary\033[0m\n'
printf '\033[1m═══════════════════════════════════════════════════════════════\033[0m\n'
if [[ "$total_pass" -gt 0 ]]; then
printf '\n\033[1;32mPASSED (%d)\033[0m\n' "$total_pass"
for name in "${STEP_PASS[@]}"; do
printf ' \033[1;32m✓\033[0m %s\n' "$name"
done
fi
if [[ "$total_fail" -gt 0 ]]; then
printf '\n\033[1;31mFAILED (%d)\033[0m\n' "$total_fail"
for entry in "${STEP_FAIL[@]}"; do
name="${entry%%|*}"
detail="${entry#*|}"
printf ' \033[1;31m✗\033[0m %s (%s)\n' "$name" "$detail"
done
fi
if [[ "$total_skip" -gt 0 ]]; then
printf '\n\033[1;33mSKIPPED (%d)\033[0m\n' "$total_skip"
for entry in "${STEP_SKIP[@]}"; do
name="${entry%%|*}"
detail="${entry#*|}"
printf ' \033[1;33m-\033[0m %s (%s)\n' "$name" "$detail"
done
fi
printf '\n\033[1m───────────────────────────────────────────────────────────────\033[0m\n'
if [[ "$total_fail" -eq 0 ]]; then
ok "All steps passed ($total_pass passed, $total_skip skipped)"
else
fail_msg "$total_fail failed, $total_pass passed, $total_skip skipped"
fi
printf '\n'
}
finish() {
print_summary
[[ ${#STEP_FAIL[@]} -eq 0 ]]
}
need_cmd() {
command -v "$1" >/dev/null 2>&1
}
start_postgres() {
need_cmd docker || { warn "docker not found"; return 1; }
if ! docker info >/dev/null 2>&1; then
warn "Docker is not running"
return 1
fi
if docker ps -a --format '{{.Names}}' | grep -qx "$PG_CONTAINER"; then
if docker ps --format '{{.Names}}' | grep -qx "$PG_CONTAINER"; then
log "Postgres already running ($PG_CONTAINER)"
else
log "Starting existing container $PG_CONTAINER"
docker start "$PG_CONTAINER" >/dev/null || return 1
fi
else
log "Creating Postgres container $PG_CONTAINER on port $PG_PORT"
docker run -d --name "$PG_CONTAINER" \
-e "POSTGRES_PASSWORD=$PG_PASSWORD" \
-e "POSTGRES_DB=$PG_DB" \
-p "${PG_PORT}:5432" \
"$PG_IMAGE" >/dev/null || return 1
fi
local i
for i in $(seq 1 30); do
if docker exec "$PG_CONTAINER" pg_isready -U "$PG_USER" -d "$PG_DB" >/dev/null 2>&1; then
log "DATABASE_URL=$DATABASE_URL"
return 0
fi
sleep 1
done
warn "Postgres did not become ready in time (container: $PG_CONTAINER)"
return 1
}
ensure_venv() {
need_cmd python3 || { warn "python3 not found"; return 1; }
if [[ ! -x "$VENV/bin/python" ]]; then
log "Creating Python venv at .venv"
python3 -m venv "$VENV" || return 1
fi
if [[ ! -x "$VENV/bin/pytest" ]]; then
log "Installing Python dependencies"
"$VENV/bin/pip" install -q -r "$ROOT/requirements.txt" || return 1
fi
return 0
}
run_migrate() {
need_cmd dotnet || { warn "dotnet not found"; return 1; }
dotnet run --project "$ROOT/services/Schema/src/Schema.Migrator" --no-launch-profile
}
ensure_web_deps() {
need_cmd npm || { warn "npm not found"; return 1; }
if [[ ! -d "$WEB/node_modules" ]]; then
log "Installing web dependencies (npm ci)"
(cd "$WEB" && npm ci) || return 1
fi
return 0
}
run_pytest_core() {
if [[ "$PYTEST_NO_COV" -eq 1 ]]; then
"$VENV/bin/pytest" tests/ -q -m "not browser" --no-cov
else
"$VENV/bin/pytest" tests/ -q -m "not browser"
fi
}
run_pytest_reporting() {
[[ "$PYTEST_NO_COV" -eq 1 ]] && return 0
"$VENV/bin/pytest" \
tests/reporting/ \
--cov=website_profiling.reporting \
--cov-config=.coveragerc.reporting \
--cov-report=term-missing \
--cov-fail-under=100 \
-q \
-o addopts=
}
run_pytest_tools() {
[[ "$PYTEST_NO_COV" -eq 1 ]] && return 0
"$VENV/bin/pytest" \
tests/tools/ \
tests/clients/ \
--cov=website_profiling.tools \
--cov-config=.coveragerc.tools \
--cov-report=term-missing \
--cov-fail-under=100 \
-q \
-o addopts=
}
run_browser_pytest() {
if "$VENV/bin/python" -c "from website_profiling.crawl.fetchers import browser_status; import sys; sys.exit(0 if browser_status().get('ok') else 1)" 2>/dev/null; then
"$VENV/bin/pytest" tests/test_crawl_fetchers.py tests/test_crawler_browser_e2e.py -m browser -q --no-cov
else
return 2
fi
}
run_cli_smoke() {
"$VENV/bin/python" -m src --help >/dev/null
}
run_web_build() { (cd "$WEB" && npm run build); }
run_web_typecheck() { (cd "$WEB" && npm run typecheck); }
run_web_lint() { (cd "$WEB" && npm run lint); }
run_web_test() { (cd "$WEB" && npm test); }
dotnet_test_sln() {
(cd "$ROOT/services" && dotnet test WebsiteProfiling.slnx -m:1)
}
run_bff_openapi_drift_gate() {
if ! need_cmd dotnet; then
return 0
fi
if ! need_cmd nswag; then
if dotnet tool list -g 2>/dev/null | grep -q NSwag.ConsoleCore; then
export PATH="$PATH:$HOME/.dotnet/tools"
else
log "Installing NSwag.ConsoleCore (Bff OpenAPI drift gate)"
dotnet tool install -g NSwag.ConsoleCore || return 1
export PATH="$PATH:$HOME/.dotnet/tools"
fi
fi
if ! need_cmd nswag; then
return 2
fi
(cd "$ROOT/services/Bff" && nswag run nswag.json) || return 1
git diff --exit-code services/Bff/src/Bff.Application/Generated/FastApiClient.g.cs
}
run_step_or_skip_browser() {
local name="Browser pytest (tests/test_crawl_fetchers.py, tests/test_crawler_browser_e2e.py)"
log "$name"
local ec=0
run_browser_pytest || ec=$?
if [[ "$ec" -eq 0 ]]; then
STEP_PASS+=("$name")
elif [[ "$ec" -eq 2 ]]; then
skip_step "$name" "Chromium unavailable"
else
STEP_FAIL+=("$name|exit code $ec")
fi
}
run_step_or_skip_openapi() {
local name="Bff OpenAPI drift gate (FastApiClient.g.cs)"
log "$name"
local ec=0
run_bff_openapi_drift_gate || ec=$?
if [[ "$ec" -eq 0 ]]; then
STEP_PASS+=("$name")
elif [[ "$ec" -eq 2 ]]; then
skip_step "$name" "nswag not on PATH"
else
STEP_FAIL+=("$name|exit code $ec — run services/Bff/generate-client.sh and commit")
fi
}
steps_postgres() {
run_step "Postgres ($PG_CONTAINER)" start_postgres
}
steps_venv() {
run_step "Python venv + dependencies" ensure_venv
}
steps_migrate() {
run_step "Database migrations (EF Core: Schema.Migrator)" run_migrate
}
steps_pytest() {
if [[ "$PYTEST_NO_COV" -eq 1 ]]; then
run_step "Pytest core (tests/ — no coverage)" run_pytest_core
skip_step "Pytest reporting coverage gate" "--no-cov"
skip_step "Pytest tools coverage gate" "--no-cov"
else
run_step "Pytest core (tests/ — 100% coverage gate)" run_pytest_core
run_step "Pytest reporting coverage gate (tests/reporting/)" run_pytest_reporting
run_step "Pytest tools coverage gate (tests/tools/, tests/clients/)" run_pytest_tools
fi
}
steps_browser() {
run_step_or_skip_browser
}
steps_cli_smoke() {
run_step "CLI smoke (python -m src --help)" run_cli_smoke
}
steps_web_deps() {
run_step "Web dependencies (npm ci if needed)" ensure_web_deps
}
steps_web() {
steps_web_deps
run_step "Web build (web/)" run_web_build
run_step "Web typecheck (web/)" run_web_typecheck
run_step "Web lint (web/)" run_web_lint
run_step "Web tests / vitest (web/)" run_web_test
}
steps_dotnet() {
if ! need_cmd dotnet; then
skip_step ".NET tests (WebsiteProfiling.slnx)" "dotnet not found"
return 0
fi
run_step "dotnet test (WebsiteProfiling.slnx)" dotnet_test_sln
run_step_or_skip_openapi
}
steps_python() {
steps_postgres
steps_venv
steps_migrate
steps_pytest
steps_browser
steps_cli_smoke
}
cmd_python() {
reset_steps
steps_python
finish
}
cmd_browser() {
reset_steps
run_step "Python venv + dependencies" ensure_venv
run_step_or_skip_browser
finish
}
cmd_web() {
reset_steps
steps_web
finish
}
cmd_dotnet() {
reset_steps
steps_dotnet
finish
}
cmd_all() {
reset_steps
steps_python
steps_web
steps_dotnet
finish
}
cmd_quick() {
if [[ -z "${DATABASE_URL:-}" ]]; then
die "DATABASE_URL is not set. Export it or run ./local-test all."
fi
reset_steps
warn "quick: assuming Postgres is up and migrated (./local-run db && ./local-run migrate)"
PYTEST_NO_COV=1
steps_venv
steps_pytest
steps_cli_smoke
steps_web
steps_dotnet
finish
}
cmd_help() {
cat <<EOF
Local test runner — mirrors .github/workflows/ci.yml
./local-test Same as: all
./local-test all Postgres + migrations + pytest + web + .NET (Data, Bff)
./local-test python DB + pytest (core + reporting + tools) + browser pytest + CLI smoke
./local-test browser Browser integration pytest only (skips if no Chromium)
./local-test web build, typecheck, lint, vitest (no Docker)
./local-test dotnet dotnet test services/WebsiteProfiling.slnx + Bff OpenAPI drift gate
./local-test quick pytest --no-cov + web + dotnet (DB must be ready)
./local-test all --no-cov skip pytest coverage gates (faster)
./local-test quick uses --no-cov for pytest by default
Failed steps do not stop the run — a pass/fail summary is printed at the end.
Also: ./local-run test (alias for ./local-test all)
Environment (same as ./local-run):
DATABASE_URL, DATA_DIR, WP_PG_CONTAINER, WP_PG_PORT, ...
One-time dev setup: ./local-run setup
EOF
}
main() {
local raw_cmd="${1:-all}"
shift || true
while [[ $# -gt 0 ]]; do
case "$1" in
--no-cov) PYTEST_NO_COV=1 ;;
-h|--help) cmd_help; exit 0 ;;
*) die "Unknown argument: $1 (try: ./local-test help)" ;;
esac
shift
done
case "$raw_cmd" in
all|"") cmd_all ;;
python) cmd_python ;;
browser) cmd_browser ;;
web) cmd_web ;;
dotnet) cmd_dotnet ;;
quick) cmd_quick ;;
help|-h|--help) cmd_help ;;
*)
die "Unknown command: $raw_cmd (try: ./local-test help)"
;;
esac
}
main "$@"