#!/usr/bin/env bash # test-sources.sh — run source integration tests inside the compose network. # # Usage: # ./scripts/test-sources.sh # run all source packages # ./scripts/test-sources.sh ./sources/en/... # run specific packages # ./scripts/test-sources.sh -short # metadata-only, no network calls # # Environment: # PARALLELISM number of packages to test in parallel (default: 4) # PKG_TIMEOUT per-package timeout (default: 120s) set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" NETWORK="goyomi_goyomi_dev" FLARESOLVERR_SERVICE="goyomi-flaresolverr-1" COMPOSE_FILE="$REPO_ROOT/compose.yml" GO_IMAGE="golang:1.26" PARALLELISM="${PARALLELISM:-4}" PKG_TIMEOUT="${PKG_TIMEOUT:-120s}" LOGS_DIR="$REPO_ROOT/internal/sourcetest/logs" RUN_TIMESTAMP="$(date '+%Y-%m-%dT%H-%M-%S')" SUMMARY_LOG="$LOGS_DIR/$RUN_TIMESTAMP.log" # --- helpers ----------------------------------------------------------------- info() { printf '\033[1;34m[info]\033[0m %s\n' "$*"; } ok() { printf '\033[1;32m[ok]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; } die() { printf '\033[1;31m[error]\033[0m %s\n' "$*" >&2; exit 1; } # --- write Python helpers to temp files -------------------------------------- PRINTER_PY="$(mktemp /tmp/goyomi-printer-XXXXXX.py)" SUMMARY_PY="$(mktemp /tmp/goyomi-summary-XXXXXX.py)" JSON_LOG="$(mktemp /tmp/goyomi-test-XXXXXX.json)" trap 'rm -f "$PRINTER_PY" "$SUMMARY_PY" "$JSON_LOG"' EXIT # Live streaming printer: reads JSON events from stdin, pretty-prints to terminal, # and saves all events to the log file for the summary step. cat > "$PRINTER_PY" << 'PYEOF' import sys, json log_path = sys.argv[1] RESET = '\033[0m' GREEN = '\033[1;32m' RED = '\033[1;31m' YELLOW = '\033[1;33m' GRAY = '\033[0;37m' with open(log_path, 'w') as log_file: for raw in sys.stdin: raw = raw.rstrip() log_file.write(raw + '\n') log_file.flush() try: ev = json.loads(raw) except json.JSONDecodeError: print(raw, flush=True) continue action = ev.get('Action', '') pkg = ev.get('Package', '') test = ev.get('Test', '') output = ev.get('Output', '').rstrip() short = pkg.split('/')[-1] if pkg else '' if action == 'output': skip_prefixes = ('=== RUN', '=== PAUSE', '=== CONT', '--- PASS', '--- FAIL', 'ok ') skip_exact = {'PASS', 'FAIL', ''} if output and output not in skip_exact and not any(output.startswith(p) for p in skip_prefixes): print(f'{GRAY}{output}{RESET}', flush=True) elif action == 'pass' and not test: elapsed = ev.get('Elapsed', 0) print(f'{GREEN}PASS{RESET} {short:<45} ({elapsed:.2f}s)', flush=True) elif action == 'fail' and not test: elapsed = ev.get('Elapsed', 0) print(f'{RED}FAIL{RESET} {short:<45} ({elapsed:.2f}s)', flush=True) elif action == 'skip' and not test: print(f'{YELLOW}SKIP{RESET} {short}', flush=True) PYEOF # Summary parser: reads the saved JSON log, prints pass/fail counts to terminal, # and writes a plain-text copy to the summary log file. # argv: cat > "$SUMMARY_PY" << 'PYEOF' import sys, json, re json_path = sys.argv[1] summary_path = sys.argv[2] RESET = '\033[0m' GREEN = '\033[1;32m' RED = '\033[1;31m' BOLD = '\033[1m' passed = [] failed = {} cur_sub = {} pkg_errs = {} with open(json_path) as f: for raw in f: try: ev = json.loads(raw) except: continue action = ev.get('Action', '') pkg = ev.get('Package', '') test = ev.get('Test', '') output = ev.get('Output', '').rstrip() if action == 'run' and test and '/' in test: cur_sub[pkg] = test.split('/', 1)[1] elif action == 'output' and test: m = re.match(r'^\s+\S+\.go:\d+:\s+(.*)', output) if m: subtest = cur_sub.get(pkg, test.split('/', 1)[1] if '/' in test else test) pkg_errs.setdefault(pkg, {}).setdefault(subtest, []).append(m.group(1).strip()) elif action == 'pass' and not test: passed.append(pkg) elif action == 'fail' and not test: failed[pkg] = pkg_errs.get(pkg, {}) total = len(passed) + len(failed) # Build plain-text lines (no ANSI codes) for the log file plain_lines = [] plain_lines.append('') plain_lines.append('=' * 60) plain_lines.append(' TEST SUMMARY') plain_lines.append('=' * 60) plain_lines.append(f' Packages tested : {total}') plain_lines.append(f' Passed : {len(passed)}') plain_lines.append(f' Failed : {len(failed)}') if failed: plain_lines.append('') plain_lines.append('Failed packages:') for pkg in sorted(failed): short = pkg.split('/')[-1] subtests = failed[pkg] if subtests: for subtest, errors in subtests.items(): reason = '; '.join(errors) if errors else '(no message captured)' plain_lines.append(f' ✗ {short} [{subtest}] {reason}') else: plain_lines.append(f' ✗ {short} (build error or panic)') plain_lines.append('') if not failed: plain_lines.append(f'All {total} packages passed.') else: plain_lines.append(f'{len(failed)} package(s) failed.') plain_lines.append('') # Print to terminal with colours colour = { 'Passed': GREEN, 'Failed': RED, 'Failed packages:': RED, 'All': GREEN, } for line in plain_lines: if line.startswith(' Passed'): print(f' {GREEN}Passed{RESET}' + line[9:], flush=True) elif line.startswith(' Failed :'): print(f' {RED}Failed{RESET} :' + line[19:], flush=True) elif line.startswith('Failed packages:'): print(f'{RED}Failed packages:{RESET}', flush=True) elif line.startswith(' ✗'): print(f' {RED}✗{RESET}' + line[3:], flush=True) elif line.startswith('All') and 'passed' in line: print(f'{GREEN}{line}{RESET}', flush=True) elif line.startswith('='): print(BOLD + line + RESET, flush=True) elif line.strip() == 'TEST SUMMARY': print(BOLD + line + RESET, flush=True) else: print(line, flush=True) # Write plain text to summary log file with open(summary_path, 'w') as f: f.write('\n'.join(plain_lines) + '\n') if failed: f.write('\nPassed packages:\n') for pkg in sorted(passed): f.write(f' ✓ {pkg.split("/")[-1]}\n') PYEOF # --- check docker daemon ----------------------------------------------------- if ! docker info > /dev/null 2>&1; then die "Docker daemon is not running. Start OrbStack (or Docker Desktop) first." fi ok "Docker daemon is running" # --- ensure flaresolverr is up ----------------------------------------------- if docker inspect "$FLARESOLVERR_SERVICE" --format '{{.State.Running}}' 2>/dev/null | grep -q true; then ok "FlareSolverr container is already running" else info "Starting FlareSolverr via docker compose..." docker compose -f "$COMPOSE_FILE" up -d flaresolverr for i in $(seq 1 20); do if docker inspect "$FLARESOLVERR_SERVICE" --format '{{.State.Running}}' 2>/dev/null | grep -q true; then ok "FlareSolverr is up"; break fi sleep 2 if [ "$i" -eq 20 ]; then die "FlareSolverr did not start in time" fi done fi # --- ensure network exists --------------------------------------------------- if ! docker network inspect "$NETWORK" > /dev/null 2>&1; then die "Docker network '$NETWORK' not found. Run 'docker compose up -d' first." fi ok "Network $NETWORK exists" # --- resolve packages to test ------------------------------------------------ SHORT_FLAG="" if [ $# -eq 0 ]; then PACKAGES="./sources/en/... ./sources/all/..." elif [ "$1" = "-short" ]; then PACKAGES="./sources/en/... ./sources/all/..." SHORT_FLAG="-short" shift else PACKAGES="$*" fi info "Packages : $PACKAGES" info "Parallel : $PARALLELISM" info "Timeout : $PKG_TIMEOUT" [ -n "$SHORT_FLAG" ] && info "Mode : short (metadata only)" || info "Mode : full (live network)" # --- run tests --------------------------------------------------------------- info "Running tests..." echo set +e docker run --rm \ --network "$NETWORK" \ -e FLARESOLVERR_URL=http://flaresolverr:8191 \ -v "$REPO_ROOT":/workspace \ -v goyomi_go_mod_cache:/go/pkg/mod \ -v goyomi_go_build_cache:/root/.cache/go-build \ -w /workspace \ "$GO_IMAGE" \ go test -json -count=1 -p "$PARALLELISM" -timeout "$PKG_TIMEOUT" $SHORT_FLAG $PACKAGES \ | python3 -u "$PRINTER_PY" "$JSON_LOG" EXIT_CODE=$? set -e # --- print summary ----------------------------------------------------------- mkdir -p "$LOGS_DIR" python3 "$SUMMARY_PY" "$JSON_LOG" "$SUMMARY_LOG" info "Summary log written to internal/sourcetest/logs/$RUN_TIMESTAMP.log" exit $EXIT_CODE