Files
gomag-vending/test.sh
Claude Agent 419464a62c feat: add CI/CD testing infrastructure with test.sh orchestrator
Complete testing system: pyproject.toml (pytest markers), test.sh
orchestrator with auto app start/stop and colorful summary,
pre-push hook, Gitea Actions workflow.

New QA tests: API health (7 endpoints), responsive (3 viewports),
log monitoring (ERROR/ORA-/Traceback detection), real GoMag sync,
PL/SQL package validation, smoke prod (read-only).

Converted test_app_basic.py and test_integration.py to pytest.
Added pytestmark to all existing tests (unit/e2e/oracle).
E2E conftest upgraded: console error collector, screenshot on
failure, auto-detect live app on :5003.

Usage: ./test.sh ci (30s) | ./test.sh full (2-3min)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 10:40:25 +00:00

263 lines
9.2 KiB
Bash
Executable File

#!/bin/bash
# Test orchestrator for GoMag Vending
# Usage: ./test.sh [ci|full|unit|e2e|oracle|sync|plsql|qa|smoke-prod|logs|--dry-run]
set -uo pipefail
cd "$(dirname "$0")"
# ─── Colors ───────────────────────────────────────────────────────────────────
GREEN='\033[32m'
RED='\033[31m'
YELLOW='\033[33m'
RESET='\033[0m'
# ─── Stage tracking ───────────────────────────────────────────────────────────
declare -a STAGE_NAMES=()
declare -a STAGE_RESULTS=() # 0=pass, 1=fail, 2=skip
EXIT_CODE=0
record() {
local name="$1"
local code="$2"
STAGE_NAMES+=("$name")
if [ "$code" -eq 0 ]; then
STAGE_RESULTS+=(0)
else
STAGE_RESULTS+=(1)
EXIT_CODE=1
fi
}
skip_stage() {
STAGE_NAMES+=("$1")
STAGE_RESULTS+=(2)
}
# ─── Environment setup ────────────────────────────────────────────────────────
setup_env() {
# Activate venv
if [ ! -d "venv" ]; then
echo -e "${RED}ERROR: venv not found. Run ./start.sh first.${RESET}"
exit 1
fi
source venv/bin/activate
# Oracle env
export TNS_ADMIN="$(pwd)/api"
INSTANTCLIENT_PATH=""
if [ -f "api/.env" ]; then
INSTANTCLIENT_PATH=$(grep -E "^INSTANTCLIENTPATH=" api/.env 2>/dev/null | cut -d'=' -f2- | tr -d ' ' || true)
fi
if [ -z "$INSTANTCLIENT_PATH" ]; then
INSTANTCLIENT_PATH="/opt/oracle/instantclient_21_15"
fi
if [ -d "$INSTANTCLIENT_PATH" ]; then
export LD_LIBRARY_PATH="${INSTANTCLIENT_PATH}:${LD_LIBRARY_PATH:-}"
fi
}
# ─── App lifecycle (for tests that need a running app) ───────────────────────
APP_PID=""
APP_PORT=5003
app_is_running() {
curl -sf "http://localhost:${APP_PORT}/health" >/dev/null 2>&1
}
start_app() {
if app_is_running; then
echo -e "${GREEN}App already running on :${APP_PORT}${RESET}"
return
fi
echo -e "${YELLOW}Starting app on :${APP_PORT}...${RESET}"
cd api
python -m uvicorn app.main:app --host 0.0.0.0 --port "$APP_PORT" &>/dev/null &
APP_PID=$!
cd ..
# Wait up to 15 seconds
for i in $(seq 1 30); do
if app_is_running; then
echo -e "${GREEN}App started (PID=${APP_PID})${RESET}"
return
fi
sleep 0.5
done
echo -e "${RED}App failed to start within 15s${RESET}"
[ -n "$APP_PID" ] && kill "$APP_PID" 2>/dev/null || true
APP_PID=""
}
stop_app() {
if [ -n "$APP_PID" ]; then
echo -e "${YELLOW}Stopping app (PID=${APP_PID})...${RESET}"
kill "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
APP_PID=""
fi
}
# ─── Dry-run checks ───────────────────────────────────────────────────────────
dry_run() {
echo -e "${YELLOW}=== Dry-run: checking prerequisites ===${RESET}"
local ok=0
if [ -d "venv" ]; then
echo -e "${GREEN}✅ venv exists${RESET}"
else
echo -e "${RED}❌ venv missing — run ./start.sh first${RESET}"
ok=1
fi
source venv/bin/activate 2>/dev/null || true
if python -m pytest --version &>/dev/null; then
echo -e "${GREEN}✅ pytest installed${RESET}"
else
echo -e "${RED}❌ pytest not found${RESET}"
ok=1
fi
if python -c "import playwright" 2>/dev/null; then
echo -e "${GREEN}✅ playwright installed${RESET}"
else
echo -e "${YELLOW}⚠️ playwright not found (needed for e2e/qa)${RESET}"
fi
if [ -n "${ORACLE_USER:-}" ] && [ -n "${ORACLE_PASSWORD:-}" ] && [ -n "${ORACLE_DSN:-}" ]; then
echo -e "${GREEN}✅ Oracle env vars set${RESET}"
else
echo -e "${YELLOW}⚠️ Oracle env vars not set (needed for oracle/sync/full)${RESET}"
fi
exit $ok
}
# ─── Run helpers ──────────────────────────────────────────────────────────────
run_stage() {
local label="$1"
shift
echo ""
echo -e "${YELLOW}=== $label ===${RESET}"
set +e
"$@"
local code=$?
set -e
record "$label" $code
# Don't return $code — let execution continue to next stage
}
# ─── Summary box ──────────────────────────────────────────────────────────────
print_summary() {
echo ""
echo -e "${YELLOW}╔══════════════════════════════════════════╗${RESET}"
echo -e "${YELLOW}║ TEST RESULTS SUMMARY ║${RESET}"
echo -e "${YELLOW}╠══════════════════════════════════════════╣${RESET}"
for i in "${!STAGE_NAMES[@]}"; do
local name="${STAGE_NAMES[$i]}"
local result="${STAGE_RESULTS[$i]}"
# Pad name to 26 chars
local padded
padded=$(printf "%-26s" "$name")
if [ "$result" -eq 0 ]; then
echo -e "${YELLOW}${RESET} ${GREEN}${RESET} ${padded} ${GREEN}passed${RESET} ${YELLOW}${RESET}"
elif [ "$result" -eq 1 ]; then
echo -e "${YELLOW}${RESET} ${RED}${RESET} ${padded} ${RED}FAILED${RESET} ${YELLOW}${RESET}"
else
echo -e "${YELLOW}${RESET} ${YELLOW}⏭️ ${RESET} ${padded} ${YELLOW}skipped${RESET} ${YELLOW}${RESET}"
fi
done
echo -e "${YELLOW}╠══════════════════════════════════════════╣${RESET}"
if [ "$EXIT_CODE" -eq 0 ]; then
echo -e "${YELLOW}${RESET} ${GREEN}All stages passed!${RESET} ${YELLOW}${RESET}"
else
echo -e "${YELLOW}${RESET} ${RED}Some stages FAILED — check output above${RESET} ${YELLOW}${RESET}"
fi
echo -e "${YELLOW}║ Health Score: see qa-reports/ ║${RESET}"
echo -e "${YELLOW}╚══════════════════════════════════════════╝${RESET}"
}
# ─── Cleanup trap ────────────────────────────────────────────────────────────
trap 'stop_app' EXIT
# ─── Main ─────────────────────────────────────────────────────────────────────
MODE="${1:-ci}"
if [ "$MODE" = "--dry-run" ]; then
setup_env
dry_run
fi
setup_env
case "$MODE" in
ci)
run_stage "Unit tests" python -m pytest -m unit -v
run_stage "E2E browser" python -m pytest api/tests/e2e/ \
--ignore=api/tests/e2e/test_dashboard_live.py -v
;;
full)
run_stage "Unit tests" python -m pytest -m unit -v
run_stage "E2E browser" python -m pytest api/tests/e2e/ \
--ignore=api/tests/e2e/test_dashboard_live.py -v
run_stage "Oracle integration" python -m pytest -m oracle -v
# Start app for stages that need HTTP access
start_app
run_stage "Sync tests" python -m pytest -m sync -v --base-url "http://localhost:${APP_PORT}"
run_stage "PL/SQL QA" python -m pytest api/tests/qa/test_qa_plsql.py -v
run_stage "QA suite" python -m pytest -m qa -v --base-url "http://localhost:${APP_PORT}"
stop_app
;;
unit)
run_stage "Unit tests" python -m pytest -m unit -v
;;
e2e)
run_stage "E2E browser" python -m pytest api/tests/e2e/ \
--ignore=api/tests/e2e/test_dashboard_live.py -v
;;
oracle)
run_stage "Oracle integration" python -m pytest -m oracle -v
;;
sync)
start_app
run_stage "Sync tests" python -m pytest -m sync -v --base-url "http://localhost:${APP_PORT}"
stop_app
;;
plsql)
run_stage "PL/SQL QA" python -m pytest api/tests/qa/test_qa_plsql.py -v
;;
qa)
start_app
run_stage "QA suite" python -m pytest -m qa -v --base-url "http://localhost:${APP_PORT}"
stop_app
;;
smoke-prod)
shift || true
run_stage "Smoke prod" python -m pytest api/tests/qa/test_qa_smoke_prod.py "$@"
;;
logs)
run_stage "Logs monitor" python -m pytest api/tests/qa/test_qa_logs_monitor.py -v
;;
*)
echo -e "${RED}Unknown mode: $MODE${RESET}"
echo "Usage: $0 [ci|full|unit|e2e|oracle|sync|plsql|qa|smoke-prod|logs|--dry-run]"
exit 1
;;
esac
print_summary
exit $EXIT_CODE