Skip to content

Commit 5000f2b

Browse files
committed
chore: Add E2E Test for Docker container.
1 parent 7209a4c commit 5000f2b

3 files changed

Lines changed: 327 additions & 0 deletions

File tree

Dockerfile

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Run the e2e_local_test.sh suite inside a container.
2+
#
3+
# Build:
4+
# docker build -t livekit-py-e2e .
5+
# Run:
6+
# docker run --rm livekit-py-e2e # full run (build FFI + tests)
7+
# docker run --rm livekit-py-e2e -k server_leave # pass args through to pytest
8+
#
9+
# The image bundles livekit-server, the Rust toolchain, uv, and the system
10+
# libraries needed to build livekit-ffi. The repo is copied to /workspace and
11+
# e2e_local_test.sh is the entrypoint.
12+
13+
FROM python:3.11-slim-bookworm
14+
15+
ENV DEBIAN_FRONTEND=noninteractive \
16+
PATH=/root/.cargo/bin:/root/.local/bin:${PATH} \
17+
CARGO_HOME=/root/.cargo \
18+
RUSTUP_HOME=/root/.rustup
19+
20+
# System deps: build toolchain for livekit-ffi, lsof for port probing in the
21+
# script, curl/ca-certificates/git for fetching toolchains and sources.
22+
RUN apt-get update && apt-get install -y --no-install-recommends \
23+
build-essential \
24+
ca-certificates \
25+
clang \
26+
cmake \
27+
curl \
28+
git \
29+
libasound2-dev \
30+
libclang-dev \
31+
libdbus-1-dev \
32+
libgbm-dev \
33+
libgl1-mesa-dev \
34+
libglib2.0-dev \
35+
libpulse-dev \
36+
libssl-dev \
37+
libudev-dev \
38+
libx11-dev \
39+
libxcomposite-dev \
40+
libxcursor-dev \
41+
libxdamage-dev \
42+
libxext-dev \
43+
libxfixes-dev \
44+
libxi-dev \
45+
libxinerama-dev \
46+
libxrandr-dev \
47+
libxrender-dev \
48+
libxss-dev \
49+
libxtst-dev \
50+
lld \
51+
lsof \
52+
nasm \
53+
ninja-build \
54+
pkg-config \
55+
procps \
56+
protobuf-compiler \
57+
python3-dev \
58+
unzip \
59+
&& rm -rf /var/lib/apt/lists/*
60+
61+
# Rust toolchain (livekit-ffi build).
62+
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
63+
| sh -s -- -y --default-toolchain stable --profile minimal
64+
65+
# uv (used by the script to create / populate .test-venv).
66+
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
67+
68+
# livekit-server (dev-mode binary; arch-aware). The release filename includes
69+
# the version, so we look up the latest tag via the GitHub API first.
70+
RUN set -eux; \
71+
arch="$(dpkg --print-architecture)"; \
72+
case "${arch}" in \
73+
amd64|arm64|armv7) ;; \
74+
*) echo "unsupported arch: ${arch}" >&2; exit 1 ;; \
75+
esac; \
76+
version="$(curl -fsSL https://api.github.com/repos/livekit/livekit/releases/latest \
77+
| sed -n 's/.*\"tag_name\": *\"v\([^\"]*\)\".*/\1/p')"; \
78+
test -n "${version}"; \
79+
url="https://github.com/livekit/livekit/releases/download/v${version}/livekit_${version}_linux_${arch}.tar.gz"; \
80+
echo "fetching ${url}"; \
81+
curl -fsSL "${url}" -o /tmp/livekit.tgz; \
82+
tar -xzf /tmp/livekit.tgz -C /usr/local/bin livekit-server; \
83+
rm /tmp/livekit.tgz; \
84+
livekit-server --version
85+
86+
WORKDIR /workspace
87+
88+
# Copy the repo. .dockerignore should keep .test-venv, target/, node_modules
89+
# etc. out of the build context (see the file next to this Dockerfile).
90+
COPY . /workspace
91+
92+
RUN chmod +x /workspace/e2e_local_test.sh
93+
94+
ENTRYPOINT ["/workspace/e2e_local_test.sh"]
95+
CMD []

e2e_local_test.sh

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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 "$@"

run_docker_test.sh

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Build the livekit-py-e2e Docker image (if needed) and run e2e_local_test.sh
4+
# inside it. Any args passed are forwarded to pytest via the container's
5+
# entrypoint.
6+
#
7+
# Usage:
8+
# ./run_docker_test.sh # default: ./tests + livekit-rtc/tests
9+
# ./run_docker_test.sh -k server_leave # filter
10+
# ./run_docker_test.sh tests/test_connection.py::test_simulate_server_leave
11+
#
12+
# Optional env vars:
13+
# IMAGE image tag (default: livekit-py-e2e:latest)
14+
# DOCKERFILE path to Dockerfile (default: Dockerfile)
15+
# FORCE_REBUILD=1 rebuild the image even if it already exists
16+
# SKIP_BUILD=1 never rebuild; fail if image is missing
17+
# DOCKER_ARGS extra args appended to `docker run` (e.g. "--cpus 4")
18+
# BUILD_LOG file to tee the docker build output (default: /tmp/docker-build.log)
19+
# RUN_LOG file to tee the docker run output (default: /tmp/docker-run.log)
20+
21+
set -euo pipefail
22+
23+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24+
IMAGE="${IMAGE:-livekit-py-e2e:latest}"
25+
DOCKERFILE="${DOCKERFILE:-${REPO_ROOT}/Dockerfile}"
26+
BUILD_LOG="${BUILD_LOG:-/tmp/docker-build.log}"
27+
RUN_LOG="${RUN_LOG:-/tmp/docker-run.log}"
28+
29+
if ! command -v docker >/dev/null 2>&1; then
30+
echo "[docker_test] 'docker' not found in PATH." >&2
31+
exit 1
32+
fi
33+
34+
if ! docker info >/dev/null 2>&1; then
35+
echo "[docker_test] docker daemon is not reachable. Start Docker Desktop / OrbStack / Colima first." >&2
36+
exit 1
37+
fi
38+
39+
image_exists() {
40+
docker image inspect "${IMAGE}" >/dev/null 2>&1
41+
}
42+
43+
need_build=0
44+
if [[ "${FORCE_REBUILD:-0}" == "1" ]]; then
45+
need_build=1
46+
elif ! image_exists; then
47+
need_build=1
48+
fi
49+
50+
if [[ "${need_build}" -eq 1 ]]; then
51+
if [[ "${SKIP_BUILD:-0}" == "1" ]]; then
52+
echo "[docker_test] SKIP_BUILD=1 but image '${IMAGE}' is missing." >&2
53+
exit 1
54+
fi
55+
echo "[docker_test] building ${IMAGE} (log: ${BUILD_LOG})"
56+
docker build -t "${IMAGE}" -f "${DOCKERFILE}" "${REPO_ROOT}" 2>&1 | tee "${BUILD_LOG}"
57+
else
58+
echo "[docker_test] reusing existing image ${IMAGE} (set FORCE_REBUILD=1 to rebuild)"
59+
fi
60+
61+
echo "[docker_test] running container (log: ${RUN_LOG})"
62+
# shellcheck disable=SC2086 # DOCKER_ARGS is intentionally word-split.
63+
docker run --rm ${DOCKER_ARGS:-} "${IMAGE}" "$@" 2>&1 | tee "${RUN_LOG}"

0 commit comments

Comments
 (0)