diff --git a/bin/run-e2e-tests b/bin/run-e2e-tests index 8d6e4517..ebf5b73e 100755 --- a/bin/run-e2e-tests +++ b/bin/run-e2e-tests @@ -10,23 +10,268 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" BACKUP_DIR="" -SERVICES_STARTED=0 +BACKUP_EXPECTED=0 +TEST_DATA_INSTALLED=0 +SERVICES_MAY_BE_RUNNING=0 +MANAGED_APPLICATION_PORTS=(5173 8080 8081 8083 9099 9199) cd "$PROJECT_ROOT" +report_recovery_state() { + local backup_path="" + + if [ -n "$BACKUP_DIR" ]; then + backup_path="$BACKUP_DIR/emulator-data" + fi + + echo "E2E cleanup could not safely restore emulator data." >&2 + if [ -n "$backup_path" ] && [ -e "$backup_path" ]; then + echo "Original emulator data backup: $backup_path" >&2 + else + echo "Original emulator data backup is not present." >&2 + fi + + if [ -e "$PROJECT_ROOT/emulator-data" ] || [ -L "$PROJECT_ROOT/emulator-data" ]; then + echo "Current root emulator data remains at: $PROJECT_ROOT/emulator-data" >&2 + else + echo "Current root emulator-data path is absent." >&2 + fi + + echo "Do not rerun the E2E runner until this recovery state is resolved." >&2 +} + +port_has_listener() { + local port="$1" + + if [[ ! "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then + echo "Invalid local port: $port" >&2 + return 2 + fi + + python3 - "$port" <<'PY' +import errno +import socket +import sys + +port = int(sys.argv[1]) + +try: + with socket.create_connection(("127.0.0.1", port), timeout=0.5): + pass +except ConnectionRefusedError: + raise SystemExit(1) +except socket.timeout as exc: + print( + f"Timed out while checking local port {port}: {exc}", + file=sys.stderr, + ) + raise SystemExit(2) +except OSError as exc: + if exc.errno == errno.ECONNREFUSED: + raise SystemExit(1) + print( + f"Unable to determine whether local port {port} is available: " + f"{type(exc).__name__}: {exc}", + file=sys.stderr, + ) + raise SystemExit(2) + +raise SystemExit(0) +PY +} + +get_optional_process_compose_port() { + local port + + if ! port="$(devbox services pcport 2>/dev/null)"; then + return 1 + fi + + if [[ ! "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then + echo "Devbox returned an invalid process-compose port: $port" >&2 + return 2 + fi + + printf '%s\n' "$port" +} + +wait_for_ports_to_close() { + local max_attempts=20 + local attempt=1 + local port + local port_status + local -a open_ports + + while [ "$attempt" -le "$max_attempts" ]; do + open_ports=() + + for port in "$@"; do + if port_has_listener "$port"; then + open_ports+=("$port") + else + port_status=$? + if [ "$port_status" -ne 1 ]; then + echo "Unable to confirm that local port $port is closed." >&2 + return 2 + fi + fi + done + + if [ "${#open_ports[@]}" -eq 0 ]; then + return 0 + fi + + if [ "$attempt" -eq "$max_attempts" ]; then + echo "Local ports still listening after shutdown: ${open_ports[*]}" >&2 + return 1 + fi + + sleep 0.25 + attempt=$((attempt + 1)) + done +} + cleanup() { local exit_code=$? + local cleanup_code=0 + local data_transaction_active=0 + local restoration_required=0 + local safe_to_modify_data=1 + local process_compose_port="" + local process_compose_status + local port_status + + trap - EXIT + set +e - if [ "$SERVICES_STARTED" -eq 1 ]; then - devbox services stop || true + if [ "$BACKUP_EXPECTED" -eq 1 ]; then + restoration_required=1 + elif [ -n "$BACKUP_DIR" ] && [ -e "$BACKUP_DIR/emulator-data" ]; then + restoration_required=1 fi - rm -rf emulator-data - if [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR/emulator-data" ]; then - mv "$BACKUP_DIR/emulator-data" emulator-data + if [ "$TEST_DATA_INSTALLED" -eq 1 ]; then + data_transaction_active=1 + elif [ "$restoration_required" -eq 1 ]; then + data_transaction_active=1 + fi + + if [ "$SERVICES_MAY_BE_RUNNING" -eq 1 ]; then + if process_compose_port="$(get_optional_process_compose_port)"; then + if port_has_listener "$process_compose_port"; then + if ! devbox services stop; then + echo "Failed to stop the E2E service graph during cleanup." >&2 + cleanup_code=1 + safe_to_modify_data=0 + elif ! wait_for_ports_to_close "$process_compose_port" "${MANAGED_APPLICATION_PORTS[@]}"; then + echo "Could not confirm complete E2E service graph shutdown during cleanup." >&2 + cleanup_code=1 + safe_to_modify_data=0 + fi + else + port_status=$? + if [ "$port_status" -eq 1 ]; then + if ! wait_for_ports_to_close "${MANAGED_APPLICATION_PORTS[@]}"; then + echo "Could not confirm that all E2E application services are stopped during cleanup." >&2 + cleanup_code=1 + safe_to_modify_data=0 + fi + else + cleanup_code=1 + safe_to_modify_data=0 + fi + fi + else + process_compose_status=$? + if [ "$process_compose_status" -eq 1 ]; then + if ! wait_for_ports_to_close "${MANAGED_APPLICATION_PORTS[@]}"; then + echo "Could not confirm that all E2E application services are stopped during cleanup." >&2 + cleanup_code=1 + safe_to_modify_data=0 + fi + else + cleanup_code=1 + safe_to_modify_data=0 + fi + fi + elif [ "$data_transaction_active" -eq 1 ]; then + if process_compose_port="$(get_optional_process_compose_port)"; then + if ! wait_for_ports_to_close "$process_compose_port" "${MANAGED_APPLICATION_PORTS[@]}"; then + echo "Could not confirm that all local service ports are closed before cleanup." >&2 + cleanup_code=1 + safe_to_modify_data=0 + fi + else + process_compose_status=$? + if [ "$process_compose_status" -eq 1 ]; then + if ! wait_for_ports_to_close "${MANAGED_APPLICATION_PORTS[@]}"; then + echo "Could not confirm that all local service ports are closed before cleanup." >&2 + cleanup_code=1 + safe_to_modify_data=0 + fi + else + cleanup_code=1 + safe_to_modify_data=0 + fi + fi + fi + + if [ "$safe_to_modify_data" -eq 1 ] && [ "$restoration_required" -eq 1 ]; then + if [ -z "$BACKUP_DIR" ] || [ ! -e "$BACKUP_DIR/emulator-data" ]; then + echo "Expected original emulator data backup is missing." >&2 + cleanup_code=1 + safe_to_modify_data=0 + fi + fi + + if [ "$safe_to_modify_data" -eq 1 ] && [ "$TEST_DATA_INSTALLED" -eq 1 ]; then + if ! rm -rf -- "$PROJECT_ROOT/emulator-data"; then + echo "Failed to remove E2E emulator data during cleanup." >&2 + cleanup_code=1 + safe_to_modify_data=0 + elif [ -e "$PROJECT_ROOT/emulator-data" ] || [ -L "$PROJECT_ROOT/emulator-data" ]; then + echo "E2E emulator data still exists after cleanup removal." >&2 + cleanup_code=1 + safe_to_modify_data=0 + else + TEST_DATA_INSTALLED=0 + fi + fi + + if [ "$safe_to_modify_data" -eq 1 ] && [ "$restoration_required" -eq 1 ]; then + if [ -z "$BACKUP_DIR" ] || [ ! -e "$BACKUP_DIR/emulator-data" ]; then + echo "Expected original emulator data backup is missing." >&2 + cleanup_code=1 + safe_to_modify_data=0 + elif [ -e "$PROJECT_ROOT/emulator-data" ] || [ -L "$PROJECT_ROOT/emulator-data" ]; then + echo "Refusing to restore emulator data because the destination already exists." >&2 + cleanup_code=1 + safe_to_modify_data=0 + elif ! mv "$BACKUP_DIR/emulator-data" "$PROJECT_ROOT/emulator-data"; then + echo "Failed to restore the original emulator data from $BACKUP_DIR." >&2 + cleanup_code=1 + safe_to_modify_data=0 + elif [ ! -e "$PROJECT_ROOT/emulator-data" ] || [ -e "$BACKUP_DIR/emulator-data" ]; then + echo "Emulator data restoration could not be verified." >&2 + cleanup_code=1 + safe_to_modify_data=0 + else + BACKUP_EXPECTED=0 + fi + fi + + if [ "$safe_to_modify_data" -eq 0 ]; then + report_recovery_state + fi + + if [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then rmdir "$BACKUP_DIR" 2>/dev/null || true fi + if [ "$exit_code" -eq 0 ] && [ "$cleanup_code" -ne 0 ]; then + exit_code=$cleanup_code + fi + exit "$exit_code" } @@ -45,25 +290,77 @@ wait_for_url() { local name="$1" local url="$2" local max_attempts="${3:-90}" - local attempt=1 - echo "Waiting for $name at $url" - while [ "$attempt" -le "$max_attempts" ]; do - if curl --silent --show-error --output /dev/null --max-time 2 "$url"; then - echo "$name is ready" - return 0 - fi + python3 - "$name" "$url" "$max_attempts" <<'PY' +import sys +import time +import urllib.error +import urllib.request - sleep 1 - attempt=$((attempt + 1)) - done +name = sys.argv[1] +url = sys.argv[2] +max_attempts = int(sys.argv[3]) +last_error = None + +print(f"Waiting for {name} at {url}") + +for attempt in range(1, max_attempts + 1): + try: + with urllib.request.urlopen(url, timeout=2): + pass + print(f"{name} is ready") + raise SystemExit(0) + except urllib.error.HTTPError: + print(f"{name} is ready") + raise SystemExit(0) + except Exception as exc: + last_error = exc - echo "Timed out waiting for $name at $url" >&2 - return 1 + if attempt < max_attempts: + time.sleep(1) + +message = f"Timed out waiting for {name} at {url}" +if last_error is not None: + message += f": {type(last_error).__name__}: {last_error}" + +print(message, file=sys.stderr) +raise SystemExit(1) +PY } -if ! command -v devbox >/dev/null 2>&1; then - echo "devbox is required. Install it with bin/install-devbox first." >&2 +for required_command in devbox python3 node npm bru; do + if ! command -v "$required_command" >/dev/null 2>&1; then + echo "$required_command is required. Run this script from the Devbox environment." >&2 + exit 1 + fi +done + +if [ ! -d "$PROJECT_ROOT/e2e/e2e-emulator-data" ]; then + echo "E2E emulator fixture directory not found: $PROJECT_ROOT/e2e/e2e-emulator-data" >&2 + exit 1 +fi + +if [ -L "$PROJECT_ROOT/emulator-data" ]; then + echo "Refusing to run because $PROJECT_ROOT/emulator-data is a symbolic link." >&2 + echo "Replace it with a directory or remove it before rerunning." >&2 + exit 1 +elif [ -e "$PROJECT_ROOT/emulator-data" ] && [ ! -d "$PROJECT_ROOT/emulator-data" ]; then + if [ -f "$PROJECT_ROOT/emulator-data" ]; then + emulator_data_type="regular file" + elif [ -p "$PROJECT_ROOT/emulator-data" ]; then + emulator_data_type="named pipe" + elif [ -S "$PROJECT_ROOT/emulator-data" ]; then + emulator_data_type="socket" + elif [ -b "$PROJECT_ROOT/emulator-data" ]; then + emulator_data_type="block device" + elif [ -c "$PROJECT_ROOT/emulator-data" ]; then + emulator_data_type="character device" + else + emulator_data_type="non-directory path" + fi + + echo "Refusing to run because $PROJECT_ROOT/emulator-data is a $emulator_data_type." >&2 + echo "Replace it with a directory or remove it before rerunning." >&2 exit 1 fi @@ -72,29 +369,127 @@ ensure_env_file "builder-frontend/.env" "builder-frontend/.env.example" ensure_env_file "builder-api/.env" "builder-api/.env.example" ensure_env_file "library-api/.env" "library-api/.env.example" -devbox run install-builder-frontend-ci +if [ "${DEVBOX_SHELL_ENABLED:-}" = "1" ]; then + ( + cd builder-frontend + npm ci + ) +else + devbox run install-builder-frontend-ci +fi + +( + cd e2e + npm ci + + if [ ! -x ./node_modules/.bin/playwright ]; then + echo "The repository-local Playwright executable was not installed by npm ci." >&2 + exit 1 + fi + + ./node_modules/.bin/playwright install --with-deps chromium +) + +open_managed_ports=() +for port in "${MANAGED_APPLICATION_PORTS[@]}"; do + if port_has_listener "$port"; then + open_managed_ports+=("$port") + else + port_status=$? + if [ "$port_status" -ne 1 ]; then + echo "Unable to confirm that required local port $port is available." >&2 + exit "$port_status" + fi + fi +done + +process_compose_port="" +process_compose_running=0 +if process_compose_port="$(get_optional_process_compose_port)"; then + if port_has_listener "$process_compose_port"; then + process_compose_running=1 + else + port_status=$? + if [ "$port_status" -ne 1 ]; then + exit "$port_status" + fi + fi +else + process_compose_status=$? + if [ "$process_compose_status" -ne 1 ]; then + exit "$process_compose_status" + fi +fi -devbox services stop || true +if [ "$process_compose_running" -eq 1 ]; then + devbox services stop -if [ -d emulator-data ]; then - BACKUP_DIR="$(mktemp -d)" - mv emulator-data "$BACKUP_DIR/emulator-data" + if wait_for_ports_to_close "$process_compose_port" "${MANAGED_APPLICATION_PORTS[@]}"; then + : + else + port_status=$? + echo "Refusing to touch emulator data because not all required local ports are closed." >&2 + exit "$port_status" + fi +elif [ "${#open_managed_ports[@]}" -ne 0 ]; then + echo "Refusing to touch emulator data because required local ports are already in use: ${open_managed_ports[*]}" >&2 + echo "Stop the processes using those ports before rerunning the E2E runner." >&2 + exit 1 +elif [ -n "$process_compose_port" ]; then + if wait_for_ports_to_close "$process_compose_port" "${MANAGED_APPLICATION_PORTS[@]}"; then + : + else + port_status=$? + echo "Refusing to touch emulator data because not all required local ports are closed." >&2 + exit "$port_status" + fi +elif wait_for_ports_to_close "${MANAGED_APPLICATION_PORTS[@]}"; then + : +else + port_status=$? + echo "Refusing to touch emulator data because not all required local ports are closed." >&2 + exit "$port_status" fi -cp -R e2e/e2e-emulator-data emulator-data +if [ -d "$PROJECT_ROOT/emulator-data" ]; then + mkdir -p "$PROJECT_ROOT/logs" + BACKUP_DIR="$(mktemp -d "$PROJECT_ROOT/logs/e2e-emulator-data.XXXXXX")" + echo "Backing up original emulator data to $BACKUP_DIR/emulator-data" + mv "$PROJECT_ROOT/emulator-data" "$BACKUP_DIR/emulator-data" + if [ ! -d "$BACKUP_DIR/emulator-data" ] || [ -e "$PROJECT_ROOT/emulator-data" ]; then + echo "Original emulator data backup could not be verified." >&2 + report_recovery_state + exit 1 + fi + + BACKUP_EXPECTED=1 +fi + +if [ -e "$PROJECT_ROOT/emulator-data" ] || [ -L "$PROJECT_ROOT/emulator-data" ]; then + echo "Refusing to install E2E fixtures because $PROJECT_ROOT/emulator-data reappeared after backup." >&2 + exit 1 +fi + +TEST_DATA_INSTALLED=1 +cp -R "$PROJECT_ROOT/e2e/e2e-emulator-data" "$PROJECT_ROOT/emulator-data" + +SERVICES_MAY_BE_RUNNING=1 devbox services up -b -SERVICES_STARTED=1 wait_for_url "Firestore emulator" "http://localhost:8080/" wait_for_url "Storage emulator" "http://localhost:9199/" wait_for_url "Auth emulator" "http://localhost:9099/" wait_for_url "Library API" "http://localhost:8083/" + +( + cd library-api/test/bdt + bru run +) + wait_for_url "builder frontend" "http://localhost:5173/" ( cd e2e - npm ci - npx playwright install - npx playwright test "$@" + ./node_modules/.bin/playwright test "$@" ) diff --git a/bin/setup b/bin/setup index e42eaa0a..ae53c995 100755 --- a/bin/setup +++ b/bin/setup @@ -6,14 +6,70 @@ set -e # Exit on any error SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +USING_DEVBOX=0 source "$SCRIPT_DIR/functions" +port_has_listener() { + local port="$1" + + if [[ ! "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then + print_error "Invalid local port: $port" + return 2 + fi + + python3 - "$port" <<'PY' +import errno +import socket +import sys + +port = int(sys.argv[1]) + +try: + with socket.create_connection(("127.0.0.1", port), timeout=0.5): + pass +except ConnectionRefusedError: + raise SystemExit(1) +except socket.timeout as exc: + print( + f"Timed out while checking local port {port}: {exc}", + file=sys.stderr, + ) + raise SystemExit(2) +except OSError as exc: + if exc.errno == errno.ECONNREFUSED: + raise SystemExit(1) + print( + f"Unable to determine whether local port {port} is available: " + f"{type(exc).__name__}: {exc}", + file=sys.stderr, + ) + raise SystemExit(2) + +raise SystemExit(0) +PY +} + +get_optional_process_compose_port() { + local port + + if ! port="$(devbox services pcport 2>/dev/null)"; then + return 1 + fi + + if [[ ! "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then + echo "Devbox returned an invalid process-compose port: $port" >&2 + return 2 + fi + + printf '%s\n' "$port" +} + print_status "๐Ÿš€ Starting Benefit Decision Toolkit Developer Setup..." echo "" -if [[ "$DEVBOX_SHELL_ENABLED" != "1" ]]; then +if [[ "${DEVBOX_SHELL_ENABLED:-}" != "1" ]]; then if ask_user "This project is configured to use devbox (https://www.jetify.com/devbox) to manage system dependencies. Do you want to use devbox? (default=yes)" "y"; then echo "" echo "Devbox instructions:" @@ -29,6 +85,62 @@ if [[ "$DEVBOX_SHELL_ENABLED" != "1" ]]; then print_warning "Setup won't use devbox; system dependencies are the developer's responsibility. See devbox.json for the list of dependencies needed." sleep 2 fi +else + USING_DEVBOX=1 +fi + +if [ "$USING_DEVBOX" -eq 1 ]; then + if ! command_exists python3; then + print_error "python3 not found. Devbox setup requires Python to check the local service state." + exit 1 + fi + + MANAGED_APPLICATION_PORTS=(5173 8080 8081 8083 9099 9199) + for port in "${MANAGED_APPLICATION_PORTS[@]}"; do + if port_has_listener "$port"; then + print_error "Setup cannot run because required local port $port is already in use." + echo "" + echo "Stop the process using port $port, then rerun:" + echo "" + echo " devbox run setup" + echo "" + echo "Setup verification requires exclusive ownership of the local service ports and emulator data." + exit 1 + else + port_status=$? + if [ "$port_status" -ne 1 ]; then + print_error "Unable to confirm that local port $port is available." + exit "$port_status" + fi + fi + done + + process_compose_port="" + if process_compose_port="$(get_optional_process_compose_port)"; then + if port_has_listener "$process_compose_port"; then + print_error "Setup cannot run while this project's local service graph is active on port $process_compose_port." + echo "" + echo "Stop the local service graph, then rerun setup:" + echo "" + echo " devbox services stop" + echo " devbox run setup" + echo "" + echo "Setup verification requires exclusive ownership of the local service ports and emulator data." + exit 1 + else + port_status=$? + if [ "$port_status" -ne 1 ]; then + print_error "Unable to confirm that the process-compose port is available." + exit "$port_status" + fi + fi + else + process_compose_status=$? + if [ "$process_compose_status" -ne 1 ]; then + print_error "Unable to determine a valid process-compose port for this project." + exit "$process_compose_status" + fi + fi fi print_status "๐Ÿ“ฆ Installing frontend dependencies..." @@ -66,8 +178,7 @@ cd .. print_status "Installing builder-api dependencies..." cd builder-api -# TODO: are the tests important to run here? (one of them was failing for me) -mvn clean package -DskipTests +mvn clean package print_success "builder-api dependencies installed" cd .. @@ -94,6 +205,16 @@ done echo "" +if [ "$USING_DEVBOX" -eq 1 ]; then + print_status "๐Ÿงช Verifying the complete local development environment..." + bin/run-e2e-tests + print_success "Complete local development environment verified" +else + print_warning "Skipping full-stack verification because it requires Devbox." +fi + +echo "" + print_success "๐ŸŽ‰ Developer setup completed successfully!" echo "" echo "๐Ÿ“‹ Next Steps:"