#!/usr/bin/env bash
# Integration test for the development environment.
#
# It drives `frogue-dev`, the same entry point `nix run .#dev` runs, with the
# same config and the same OTLP environment; drives a request through the
# server; and then asserts that the trace actually reached Jaeger. Nothing here
# re-implements the environment, so it covers the topology *and* the way the app
# starts process-compose -- an app that binds something fixed fails here rather
# than only on whoever's machine happens to have that port taken.
#
# Run it with `nix run .#dev-test`. It needs podman, it really starts the
# container, and it uses the development ports, so nothing else may be holding
# 6565, 4318 or 16686 while it runs.
set -euo pipefail
if ! command -v frogue-dev >/dev/null; then
echo "dev-test: frogue-dev is not on PATH" >&2
echo "dev-test: run me through \`nix run .#dev-test\`" >&2
exit 1
fi
root=${FROGUE_DEV_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}
cd "$root"
# A connection attempt is the cheapest protocol-agnostic liveness probe, and
# unlike curl it is not fooled by a listener that speaks something other than
# HTTP -- which is exactly what the OTLP port is.
port_open() {
(exec 3<>"/dev/tcp/127.0.0.1/$1") 2>/dev/null
}
busy=""
for port in 6565 4318 16686; do
if port_open "$port"; then
busy="$busy $port"
fi
done
if [ -n "$busy" ]; then
echo "dev-test: something is already listening on$busy; stop it first" >&2
exit 1
fi
log_dir=${TMPDIR:-/tmp}/frogue-dev-test
mkdir -p "$log_dir"
log=$log_dir/process-compose.log
# Kept apart from the stack's console output: pointing both at one file would
# leave two writers truncating each other.
console=$log_dir/console.log
echo "dev-test: starting the development stack (logs: $log_dir)"
frogue-dev --tui=false --log-file "$log" >"$console" 2>&1 &
pc_pid=$!
cleanup() {
kill "$pc_pid" 2>/dev/null || true
wait "$pc_pid" 2>/dev/null || true
# process-compose stops what it started, but if it was killed outright the
# container can outlive it and hold the ports.
podman rm --force frogue-dev-jaeger >/dev/null 2>&1 || true
}
trap cleanup EXIT
wait_for_http() {
local what=$1 url=$2 timeout=$3
local deadline=$((SECONDS + timeout))
while [ "$SECONDS" -lt "$deadline" ]; do
if curl --silent --fail --output /dev/null "$url"; then
echo "dev-test: $what is up"
return 0
fi
sleep 1
done
echo "dev-test: $what did not answer at $url within ${timeout}s" >&2
echo "dev-test: last lines of $console:" >&2
tail --lines=20 "$console" >&2 || true
return 1
}
# Jaeger 2 dropped the v1 query routes: /api/services is a 404 there, and
# probing it would report a healthy collector as dead.
wait_for_http jaeger "http://127.0.0.1:16686/api/v3/services" 60
# Generous: the watcher compiles the whole workspace before it can serve
# anything, and that is minutes from cold.
wait_for_http frogue "http://127.0.0.1:6565/" 900
echo "dev-test: driving a request through the server"
curl --silent --output /dev/null "http://127.0.0.1:6565/"
# The export is batched, with a five second schedule delay, so this is expected
# to take a few seconds rather than being immediate.
deadline=$((SECONDS + 60))
services=""
while [ "$SECONDS" -lt "$deadline" ]; do
services=$(curl --silent "http://127.0.0.1:16686/api/v3/services" || true)
if printf '%s' "$services" | jq --exit-status '.services | index("frogue")' >/dev/null 2>&1; then
break
fi
sleep 1
done
if ! printf '%s' "$services" | jq --exit-status '.services | index("frogue")' >/dev/null 2>&1; then
echo "dev-test: frogue never reached jaeger; services were: $services" >&2
echo "dev-test: last lines of $console:" >&2
tail --lines=20 "$console" >&2 || true
exit 1
fi
echo "dev-test: jaeger knows about frogue"
# Auto-reload is the point of the watcher, so it is asserted rather than
# assumed: a change under either watched directory has to end in the server
# being restarted. The files are only touched, never modified -- only the mtime
# matters, so this leaves no trace in the working copy -- and the restart is
# seen through cargo, which prints its build line on every run. Watching that
# rather than the server's own startup line keeps the assertion independent of
# RUST_LOG, which the environment deliberately leaves to the caller.
expect_restart() {
local path=$1
# The dots stand in for the backticks cargo puts around `dev`: spelling
# them out would trip shellcheck's "expressions don't expand in single
# quotes" warning.
local pattern='Finished .dev. profile'
local before
before=$(grep --extended-regexp --count "$pattern" "$console" || true)
touch "$path"
local deadline=$((SECONDS + 300))
while [ "$SECONDS" -lt "$deadline" ]; do
if [ "$(grep --extended-regexp --count "$pattern" "$console" || true)" -gt "$before" ]; then
echo "dev-test: touching $path restarted the server"
return 0
fi
sleep 1
done
echo "dev-test: touching $path did not restart the server" >&2
echo "dev-test: last lines of $console:" >&2
tail --lines=20 "$console" >&2 || true
return 1
}
expect_restart frogue/src/main.rs
expect_restart frogue/src/views.rs
wait_for_http "frogue after reloading" "http://127.0.0.1:6565/" 120
cleanup
trap - EXIT
# A leak here would be invisible in the happy path but would break the next
# run, so it is part of the test.
for _ in $(seq 1 20); do
busy=""
for port in 6565 4318 16686; do
if port_open "$port"; then
busy="$busy $port"
fi
done
[ -z "$busy" ] && break
sleep 0.5
done
if [ -n "$busy" ]; then
echo "dev-test: ports still bound after shutdown:$busy" >&2
exit 1
fi
echo "dev-test: OK, traces flow to jaeger and the stack shuts down cleanly"