|
| 1 | +#!/usr/bin/env bash |
| 2 | +# |
| 3 | +# Build the livekit-ffi Rust crate, install it into the livekit-rtc Python |
| 4 | +# package, prepare the .test-venv, spin up a local livekit-server (dev mode), |
| 5 | +# and run E2E tests against it. Server is killed on exit. |
| 6 | +# |
| 7 | +# Usage: |
| 8 | +# ./e2e_local_test.sh # run ./tests and livekit-rtc/tests |
| 9 | +# ./e2e_local_test.sh tests/test_connection.py # run a specific file |
| 10 | +# ./e2e_local_test.sh tests/test_connection.py::test_simulate_server_leave |
| 11 | +# # pass any pytest args through |
| 12 | +# |
| 13 | +# Optional env vars: |
| 14 | +# SKIP_BUILD=1 skip the cargo build + dylib copy step |
| 15 | +# SKIP_VENV=1 skip creating/refreshing the .test-venv (use existing one) |
| 16 | +# CARGO_PROFILE cargo profile to build (default: release) |
| 17 | +# VENV_DIR venv directory (default: .test-venv at repo root) |
| 18 | +# LIVEKIT_SERVER_BIN path to livekit-server (default: livekit-server on PATH) |
| 19 | +# LIVEKIT_BIND bind address (default: 127.0.0.1) |
| 20 | +# LIVEKIT_PORT signal port (default: 7880) |
| 21 | +# SERVER_LOG server log path (default: /tmp/livekit-server.log) |
| 22 | +# SERVER_READY_TIMEOUT seconds to wait for server to listen (default: 15) |
| 23 | + |
| 24 | +set -euo pipefail |
| 25 | + |
| 26 | +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 27 | +RTC_DIR="${REPO_ROOT}/livekit-rtc" |
| 28 | +RUST_DIR="${RTC_DIR}/rust-sdks" |
| 29 | +RESOURCES_DIR="${RTC_DIR}/livekit/rtc/resources" |
| 30 | + |
| 31 | +CARGO_PROFILE="${CARGO_PROFILE:-release}" |
| 32 | +VENV_DIR="${VENV_DIR:-${REPO_ROOT}/.test-venv}" |
| 33 | + |
| 34 | +LIVEKIT_SERVER_BIN="${LIVEKIT_SERVER_BIN:-livekit-server}" |
| 35 | +LIVEKIT_BIND="${LIVEKIT_BIND:-127.0.0.1}" |
| 36 | +LIVEKIT_PORT="${LIVEKIT_PORT:-7880}" |
| 37 | +SERVER_LOG="${SERVER_LOG:-/tmp/livekit-server.log}" |
| 38 | +SERVER_READY_TIMEOUT="${SERVER_READY_TIMEOUT:-15}" |
| 39 | + |
| 40 | +# Dev-mode placeholder credentials baked into livekit-server. |
| 41 | +DEV_API_KEY="devkey" |
| 42 | +DEV_API_SECRET="secret" |
| 43 | + |
| 44 | +# Pick the platform-specific FFI artifact name. |
| 45 | +case "$(uname -s)" in |
| 46 | + Darwin) FFI_LIB_NAME="liblivekit_ffi.dylib" ;; |
| 47 | + Linux) FFI_LIB_NAME="liblivekit_ffi.so" ;; |
| 48 | + MINGW*|MSYS*|CYGWIN*) FFI_LIB_NAME="livekit_ffi.dll" ;; |
| 49 | + *) echo "[e2e_local] unsupported platform: $(uname -s)" >&2; exit 1 ;; |
| 50 | +esac |
| 51 | + |
| 52 | +if ! command -v "${LIVEKIT_SERVER_BIN}" >/dev/null 2>&1; then |
| 53 | + echo "[e2e_local] '${LIVEKIT_SERVER_BIN}' not found in PATH." >&2 |
| 54 | + echo "[e2e_local] Install with: brew install livekit (or see https://docs.livekit.io/home/self-hosting/local/)" >&2 |
| 55 | + exit 1 |
| 56 | +fi |
| 57 | + |
| 58 | +if [[ "${SKIP_BUILD:-0}" != "1" ]]; then |
| 59 | + echo "[e2e_local] building livekit-ffi (${CARGO_PROFILE}) ..." |
| 60 | + ( |
| 61 | + cd "${RUST_DIR}" |
| 62 | + if [[ "${CARGO_PROFILE}" == "release" ]]; then |
| 63 | + cargo build --release -p livekit-ffi |
| 64 | + else |
| 65 | + cargo build --profile "${CARGO_PROFILE}" -p livekit-ffi |
| 66 | + fi |
| 67 | + ) |
| 68 | + |
| 69 | + SRC_LIB="${RUST_DIR}/target/${CARGO_PROFILE}/${FFI_LIB_NAME}" |
| 70 | + DST_LIB="${RESOURCES_DIR}/${FFI_LIB_NAME}" |
| 71 | + |
| 72 | + if [[ ! -f "${SRC_LIB}" ]]; then |
| 73 | + echo "[e2e_local] expected ${SRC_LIB} to exist after build" >&2 |
| 74 | + exit 1 |
| 75 | + fi |
| 76 | + |
| 77 | + echo "[e2e_local] installing ${FFI_LIB_NAME} -> ${DST_LIB}" |
| 78 | + mkdir -p "${RESOURCES_DIR}" |
| 79 | + cp "${SRC_LIB}" "${DST_LIB}" |
| 80 | +else |
| 81 | + echo "[e2e_local] SKIP_BUILD=1, using existing ${RESOURCES_DIR}/${FFI_LIB_NAME}" |
| 82 | +fi |
| 83 | + |
| 84 | +if [[ "${SKIP_VENV:-0}" != "1" ]]; then |
| 85 | + if ! command -v uv >/dev/null 2>&1; then |
| 86 | + echo "[e2e_local] 'uv' not found in PATH; install from https://docs.astral.sh/uv/ or set SKIP_VENV=1" >&2 |
| 87 | + exit 1 |
| 88 | + fi |
| 89 | + |
| 90 | + if [[ ! -d "${VENV_DIR}" ]]; then |
| 91 | + echo "[e2e_local] creating venv at ${VENV_DIR}" |
| 92 | + uv venv "${VENV_DIR}" |
| 93 | + fi |
| 94 | + |
| 95 | + # Reinstall livekit-rtc from local source so the venv tracks the freshly |
| 96 | + # built FFI dylib and any local proto / room.py edits. |
| 97 | + echo "[e2e_local] installing livekit-rtc (and siblings) into ${VENV_DIR}" |
| 98 | + uv pip install --python "${VENV_DIR}" --reinstall \ |
| 99 | + "${RTC_DIR}" \ |
| 100 | + "${REPO_ROOT}/livekit-api" \ |
| 101 | + "${REPO_ROOT}/livekit-protocol" |
| 102 | + uv pip install --python "${VENV_DIR}" \ |
| 103 | + pytest pytest-asyncio numpy matplotlib |
| 104 | +fi |
| 105 | + |
| 106 | +if [[ ! -x "${VENV_DIR}/bin/python" ]]; then |
| 107 | + echo "[e2e_local] venv not found at ${VENV_DIR}; re-run without SKIP_VENV=1." >&2 |
| 108 | + exit 1 |
| 109 | +fi |
| 110 | + |
| 111 | +if lsof -nP -iTCP:"${LIVEKIT_PORT}" -sTCP:LISTEN >/dev/null 2>&1; then |
| 112 | + echo "[e2e_local] port ${LIVEKIT_PORT} is already in use; refusing to start another server." >&2 |
| 113 | + exit 1 |
| 114 | +fi |
| 115 | + |
| 116 | +SERVER_PID="" |
| 117 | +cleanup() { |
| 118 | + if [[ -n "${SERVER_PID}" ]] && kill -0 "${SERVER_PID}" 2>/dev/null; then |
| 119 | + echo "[e2e_local] stopping livekit-server (pid ${SERVER_PID})" |
| 120 | + kill "${SERVER_PID}" 2>/dev/null || true |
| 121 | + wait "${SERVER_PID}" 2>/dev/null || true |
| 122 | + fi |
| 123 | +} |
| 124 | +trap cleanup EXIT INT TERM |
| 125 | + |
| 126 | +echo "[e2e_local] starting ${LIVEKIT_SERVER_BIN} --dev --bind ${LIVEKIT_BIND} (log: ${SERVER_LOG})" |
| 127 | +"${LIVEKIT_SERVER_BIN}" --dev --bind "${LIVEKIT_BIND}" >"${SERVER_LOG}" 2>&1 & |
| 128 | +SERVER_PID=$! |
| 129 | + |
| 130 | +deadline=$(( $(date +%s) + SERVER_READY_TIMEOUT )) |
| 131 | +until lsof -nP -iTCP:"${LIVEKIT_PORT}" -sTCP:LISTEN >/dev/null 2>&1; do |
| 132 | + if ! kill -0 "${SERVER_PID}" 2>/dev/null; then |
| 133 | + echo "[e2e_local] livekit-server exited before becoming ready; see ${SERVER_LOG}" >&2 |
| 134 | + exit 1 |
| 135 | + fi |
| 136 | + if (( $(date +%s) >= deadline )); then |
| 137 | + echo "[e2e_local] livekit-server did not listen on ${LIVEKIT_PORT} within ${SERVER_READY_TIMEOUT}s; see ${SERVER_LOG}" >&2 |
| 138 | + exit 1 |
| 139 | + fi |
| 140 | + sleep 0.3 |
| 141 | +done |
| 142 | +echo "[e2e_local] livekit-server ready on ws://${LIVEKIT_BIND}:${LIVEKIT_PORT}" |
| 143 | + |
| 144 | +# Prepend the default test roots when no path-like argument was given, so |
| 145 | +# things like `./e2e_local_test.sh -k foo` still target the right directories. |
| 146 | +# A path-like arg is one that exists on disk, or looks like a pytest node id |
| 147 | +# (contains '::', or starts with 'tests/' / 'livekit-' / '/' / './'). |
| 148 | +has_path=0 |
| 149 | +for arg in "$@"; do |
| 150 | + if [[ -e "${arg%%::*}" \ |
| 151 | + || "${arg}" == *::* \ |
| 152 | + || "${arg}" == tests/* \ |
| 153 | + || "${arg}" == livekit-* \ |
| 154 | + || "${arg}" == /* \ |
| 155 | + || "${arg}" == ./* ]]; then |
| 156 | + has_path=1 |
| 157 | + break |
| 158 | + fi |
| 159 | +done |
| 160 | +if [[ "${has_path}" -eq 0 ]]; then |
| 161 | + set -- "${REPO_ROOT}/tests" "${RTC_DIR}/tests" "$@" |
| 162 | +fi |
| 163 | + |
| 164 | +echo "[e2e_local] running pytest: $*" |
| 165 | +cd "${RTC_DIR}" |
| 166 | +LIVEKIT_URL="ws://${LIVEKIT_BIND}:${LIVEKIT_PORT}" \ |
| 167 | +LIVEKIT_API_KEY="${DEV_API_KEY}" \ |
| 168 | +LIVEKIT_API_SECRET="${DEV_API_SECRET}" \ |
| 169 | + "${VENV_DIR}/bin/python" -m pytest -v "$@" |
0 commit comments