#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT_DIR" RUN_ID="${PARRHESIA_NODE_SYNC_E2E_RUN_ID:-local-$(date +%s)}" RESOURCE="${PARRHESIA_NODE_SYNC_E2E_RESOURCE:-tribes.accounts.user}" RUNNER_MIX_ENV="${PARRHESIA_NODE_SYNC_E2E_RUNNER_MIX_ENV:-test}" TMP_DIR="${PARRHESIA_NODE_SYNC_E2E_TMP_DIR:-$(mktemp -d "${TMPDIR:-/tmp}/parrhesia-node-sync-e2e.XXXXXX")}" STATE_FILE="$TMP_DIR/state.json" LOG_DIR="$TMP_DIR/logs" mkdir -p "$LOG_DIR" SUFFIX="$(basename "$TMP_DIR" | tr -c 'a-zA-Z0-9' '_')" DB_NAME_A="${PARRHESIA_NODE_SYNC_E2E_DB_A:-parrhesia_node_sync_a_${SUFFIX}}" DB_NAME_B="${PARRHESIA_NODE_SYNC_E2E_DB_B:-parrhesia_node_sync_b_${SUFFIX}}" port_in_use() { local port="$1" if command -v ss >/dev/null 2>&1; then ss -ltn "( sport = :${port} )" | tail -n +2 | grep -q . return fi if command -v lsof >/dev/null 2>&1; then lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1 return fi echo "Neither ss nor lsof is available for checking port usage." >&2 exit 1 } pick_port() { local port while true; do port="$(( (RANDOM % 10000) + 40000 ))" if ! port_in_use "$port"; then printf '%s\n' "$port" return fi done } NODE_A_PORT="${PARRHESIA_NODE_A_PORT:-$(pick_port)}" NODE_B_PORT="${PARRHESIA_NODE_B_PORT:-$(pick_port)}" if [[ "$NODE_A_PORT" == "$NODE_B_PORT" ]]; then echo "Node A and Node B ports must differ." >&2 exit 1 fi database_url_for() { local database_name="$1" local pg_user="${PGUSER:-${USER:-agent}}" local pg_host="${PGHOST:-localhost}" local pg_port="${PGPORT:-5432}" if [[ "$pg_host" == /* ]]; then if [[ -n "${PGPASSWORD:-}" ]]; then printf 'ecto://%s:%s@localhost/%s?socket_dir=%s&port=%s\n' \ "$pg_user" "$PGPASSWORD" "$database_name" "$pg_host" "$pg_port" else printf 'ecto://%s@localhost/%s?socket_dir=%s&port=%s\n' \ "$pg_user" "$database_name" "$pg_host" "$pg_port" fi else if [[ -n "${PGPASSWORD:-}" ]]; then printf 'ecto://%s:%s@%s:%s/%s\n' \ "$pg_user" "$PGPASSWORD" "$pg_host" "$pg_port" "$database_name" else printf 'ecto://%s@%s:%s/%s\n' \ "$pg_user" "$pg_host" "$pg_port" "$database_name" fi fi } DATABASE_URL_A="$(database_url_for "$DB_NAME_A")" DATABASE_URL_B="$(database_url_for "$DB_NAME_B")" printf -v PROTECTED_FILTERS_JSON '[{"kinds":[5000],"#r":["%s"]}]' "$RESOURCE" cleanup() { if [[ -n "${NODE_A_PID:-}" ]] && kill -0 "$NODE_A_PID" 2>/dev/null; then kill "$NODE_A_PID" 2>/dev/null || true wait "$NODE_A_PID" 2>/dev/null || true fi if [[ -n "${NODE_B_PID:-}" ]] && kill -0 "$NODE_B_PID" 2>/dev/null; then kill "$NODE_B_PID" 2>/dev/null || true wait "$NODE_B_PID" 2>/dev/null || true fi if [[ "${PARRHESIA_NODE_SYNC_E2E_DROP_DB_ON_EXIT:-1}" == "1" ]]; then DATABASE_URL="$DATABASE_URL_A" MIX_ENV=prod mix ecto.drop --quiet --force || true DATABASE_URL="$DATABASE_URL_B" MIX_ENV=prod mix ecto.drop --quiet --force || true fi if [[ "${PARRHESIA_NODE_SYNC_E2E_KEEP_TMP:-0}" != "1" ]]; then rm -rf "$TMP_DIR" fi } trap cleanup EXIT INT TERM wait_for_health() { local port="$1" local label="$2" for _ in {1..150}; do if curl -fsS "http://127.0.0.1:${port}/health" >/dev/null 2>&1; then return fi sleep 0.1 done echo "${label} did not become healthy on port ${port}" >&2 exit 1 } setup_database() { local database_url="$1" DATABASE_URL="$database_url" MIX_ENV=prod mix ecto.drop --quiet --force || true DATABASE_URL="$database_url" MIX_ENV=prod mix ecto.create --quiet DATABASE_URL="$database_url" MIX_ENV=prod mix ecto.migrate --quiet } start_node() { local node_name="$1" local port="$2" local database_url="$3" local relay_url="$4" local identity_path="$5" local sync_path="$6" local log_path="$7" DATABASE_URL="$database_url" \ PORT="$port" \ PARRHESIA_RELAY_URL="$relay_url" \ PARRHESIA_ACL_PROTECTED_FILTERS="$PROTECTED_FILTERS_JSON" \ PARRHESIA_IDENTITY_PATH="$identity_path" \ PARRHESIA_SYNC_PATH="$sync_path" \ MIX_ENV=prod \ mix run --no-halt >"$log_path" 2>&1 & if [[ "$node_name" == "a" ]]; then NODE_A_PID=$! else NODE_B_PID=$! fi } run_runner() { ERL_LIBS="_build/${RUNNER_MIX_ENV}/lib" \ elixir scripts/node_sync_e2e.exs "$@" --state-file "$STATE_FILE" } export DATABASE_URL="$DATABASE_URL_A" MIX_ENV=prod mix compile MIX_ENV="$RUNNER_MIX_ENV" mix compile >/dev/null setup_database "$DATABASE_URL_A" setup_database "$DATABASE_URL_B" NODE_A_HTTP_URL="http://127.0.0.1:${NODE_A_PORT}" NODE_B_HTTP_URL="http://127.0.0.1:${NODE_B_PORT}" NODE_A_WS_URL="ws://127.0.0.1:${NODE_A_PORT}/relay" NODE_B_WS_URL="ws://127.0.0.1:${NODE_B_PORT}/relay" start_node \ a \ "$NODE_A_PORT" \ "$DATABASE_URL_A" \ "$NODE_A_WS_URL" \ "$TMP_DIR/node-a-identity.json" \ "$TMP_DIR/node-a-sync.json" \ "$LOG_DIR/node-a.log" start_node \ b \ "$NODE_B_PORT" \ "$DATABASE_URL_B" \ "$NODE_B_WS_URL" \ "$TMP_DIR/node-b-identity.json" \ "$TMP_DIR/node-b-sync.json" \ "$LOG_DIR/node-b.log" wait_for_health "$NODE_A_PORT" "Node A" wait_for_health "$NODE_B_PORT" "Node B" export PARRHESIA_NODE_SYNC_E2E_RUN_ID="$RUN_ID" export PARRHESIA_NODE_SYNC_E2E_RESOURCE="$RESOURCE" export PARRHESIA_NODE_A_HTTP_URL="$NODE_A_HTTP_URL" export PARRHESIA_NODE_B_HTTP_URL="$NODE_B_HTTP_URL" export PARRHESIA_NODE_A_WS_URL="$NODE_A_WS_URL" export PARRHESIA_NODE_B_WS_URL="$NODE_B_WS_URL" export PARRHESIA_NODE_A_RELAY_AUTH_URL="$NODE_A_WS_URL" export PARRHESIA_NODE_B_RELAY_AUTH_URL="$NODE_B_WS_URL" export PARRHESIA_NODE_A_SYNC_URL="$NODE_A_WS_URL" export PARRHESIA_NODE_B_SYNC_URL="$NODE_B_WS_URL" run_runner bootstrap kill "$NODE_B_PID" wait "$NODE_B_PID" 2>/dev/null || true unset NODE_B_PID run_runner publish-resume start_node \ b \ "$NODE_B_PORT" \ "$DATABASE_URL_B" \ "$NODE_B_WS_URL" \ "$TMP_DIR/node-b-identity.json" \ "$TMP_DIR/node-b-sync.json" \ "$LOG_DIR/node-b.log" wait_for_health "$NODE_B_PORT" "Node B" run_runner verify-resume printf 'node-sync-e2e local run completed\nlogs: %s\n' "$LOG_DIR"