mirror of
https://github.com/merbanan/rtl_433.git
synced 2026-06-11 02:25:24 -04:00
The /cmd getters serialized into fixed stack buffers (get_stats 20480, get_meta 2048, get_protocols 102400). When a report exceeded its buffer the output was silently corrupted into invalid JSON, not cleanly truncated: the abuf string builder drops an oversized chunk but keeps appending the smaller chunks that follow, so e.g. a get_stats reply with ~230 enabled decoders ended like `...,"name":,:8256}`. Clients then fail to parse the response and the corresponding data never updates. Make the serializer report truncation and add data_print_jsons_dup(), which grows a heap buffer until the whole document fits, and use it for the three JSON-payload getters: - abuf gains an `overflow` flag, set by abuf_cat/abuf_printf and the hand-rolled string formatter whenever a write is dropped or truncated. - data_print_jsons_dup() retries with a doubled buffer until no overflow (64 MiB sanity cap), so the report is never truncated. - get_stats/get_meta/get_protocols use it and free the buffer after responding; out-of-memory yields an error reply instead of a partial one. Tests: - data-test asserts a large report serializes to a complete object via data_print_jsons_dup() while a fixed undersized buffer does not. - the HTTP integration test now validates that get_protocols/get_stats/ get_meta return parseable JSON, not just HTTP 200. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
204 lines
8.0 KiB
Bash
Executable File
204 lines
8.0 KiB
Bash
Executable File
#!/bin/sh
|
|
#
|
|
# Black-box integration test for the rtl_433 HTTP/WS API server.
|
|
#
|
|
# Starts rtl_433 in "manual" device mode (-D manual), which runs the HTTP
|
|
# server event loop without any SDR hardware or input file, exercises each
|
|
# endpoint over the loopback interface, then shuts the server down with
|
|
# SIGTERM so it exits cleanly (this is what lets gcov flush .gcda files when
|
|
# the binary was built with -DENABLE_COVERAGE=ON).
|
|
#
|
|
# Usage: http-integration-test.sh [path-to-rtl_433-binary]
|
|
# Defaults to ../src/rtl_433 relative to this script.
|
|
# Override the port with PORT=NNNN, otherwise a free port is chosen.
|
|
|
|
set -u
|
|
|
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
RTL_433=${1:-"$SCRIPT_DIR/../src/rtl_433"}
|
|
HOST=127.0.0.1
|
|
|
|
if [ ! -x "$RTL_433" ]; then
|
|
echo "ERROR: rtl_433 binary not found or not executable: $RTL_433" >&2
|
|
exit 99
|
|
fi
|
|
|
|
# Pick a free TCP port unless one was given, so parallel ctest runs don't collide.
|
|
if [ -z "${PORT:-}" ]; then
|
|
PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()' 2>/dev/null)
|
|
fi
|
|
PORT=${PORT:-18433}
|
|
BASE="http://$HOST:$PORT"
|
|
|
|
SERVER_LOG=$(mktemp 2>/dev/null || echo /tmp/rtl_433_http_test.$$.log)
|
|
SERVER_PID=""
|
|
FAILED=0
|
|
PASSED=0
|
|
|
|
cleanup() {
|
|
if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
# SIGTERM -> clean shutdown -> atexit/gcov flush. Do NOT use SIGKILL.
|
|
kill -TERM "$SERVER_PID" 2>/dev/null
|
|
# give it a moment to drain and write coverage data
|
|
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
|
kill -0 "$SERVER_PID" 2>/dev/null || break
|
|
sleep 0.3
|
|
done
|
|
kill -0 "$SERVER_PID" 2>/dev/null && kill -KILL "$SERVER_PID" 2>/dev/null
|
|
wait "$SERVER_PID" 2>/dev/null
|
|
fi
|
|
rm -f "$SERVER_LOG"
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
fail() { echo " NOT OK: $1" >&2; FAILED=$((FAILED + 1)); }
|
|
pass() { echo " ok: $1"; PASSED=$((PASSED + 1)); }
|
|
|
|
# expect_status DESC EXPECTED_CODE curl-args...
|
|
expect_status() {
|
|
desc=$1; want=$2; shift 2
|
|
got=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$@")
|
|
[ "$got" = "$want" ] && pass "$desc (HTTP $got)" || fail "$desc: expected HTTP $want, got $got"
|
|
}
|
|
|
|
# expect_body_contains DESC SUBSTRING curl-args...
|
|
expect_body_contains() {
|
|
desc=$1; needle=$2; shift 2
|
|
body=$(curl -s --max-time 10 "$@")
|
|
case "$body" in
|
|
*"$needle"*) pass "$desc (body contains '$needle')" ;;
|
|
*) fail "$desc: body did not contain '$needle'"; printf ' got: %.200s\n' "$body" >&2 ;;
|
|
esac
|
|
}
|
|
|
|
# expect_valid_json DESC curl-args... -- body must parse as JSON. Guards against
|
|
# the getters truncating an oversized report into invalid JSON (see get_stats /
|
|
# get_protocols, which now grow their output buffer to fit). Skipped when
|
|
# python3 is unavailable to validate.
|
|
HAVE_PY=0
|
|
command -v python3 >/dev/null 2>&1 && HAVE_PY=1
|
|
expect_valid_json() {
|
|
desc=$1; shift
|
|
if [ "$HAVE_PY" != 1 ]; then
|
|
echo " skip: $desc (python3 not found)"
|
|
return
|
|
fi
|
|
body=$(curl -s --max-time 10 "$@")
|
|
if printf '%s' "$body" | python3 -c 'import sys,json; json.load(sys.stdin)' 2>/dev/null; then
|
|
pass "$desc (valid JSON)"
|
|
else
|
|
fail "$desc: response is not valid JSON"; printf ' got: %.200s\n' "$body" >&2
|
|
fi
|
|
}
|
|
|
|
# expect_stream DESC URL -- streaming endpoints stay open; we only check the
|
|
# response headers, tolerating the curl timeout (exit 28) that follows.
|
|
expect_stream() {
|
|
desc=$1; url=$2
|
|
hdr=$(curl -s -D - -o /dev/null --max-time 2 "$url" 2>/dev/null)
|
|
case "$hdr" in
|
|
*"200 OK"*) pass "$desc (200, stream open)" ;;
|
|
*) fail "$desc: no '200 OK' in response headers"; printf ' got: %.200s\n' "$hdr" >&2 ;;
|
|
esac
|
|
}
|
|
|
|
echo "Starting rtl_433 HTTP server: $RTL_433 -D manual -F $BASE"
|
|
"$RTL_433" -D manual -F "http://$HOST:$PORT" >"$SERVER_LOG" 2>&1 &
|
|
SERVER_PID=$!
|
|
|
|
# Wait for readiness (server prints/serves once the manual loop is up).
|
|
ready=0
|
|
for _ in $(seq 1 50); do
|
|
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
echo "ERROR: server exited during startup" >&2
|
|
cat "$SERVER_LOG" >&2
|
|
exit 98
|
|
fi
|
|
if curl -s -o /dev/null --max-time 1 "$BASE/"; then
|
|
ready=1
|
|
break
|
|
fi
|
|
sleep 0.2
|
|
done
|
|
if [ "$ready" != 1 ]; then
|
|
echo "ERROR: server did not become ready on $BASE" >&2
|
|
cat "$SERVER_LOG" >&2
|
|
exit 97
|
|
fi
|
|
|
|
echo "Server ready on port $PORT (pid $SERVER_PID). Running checks:"
|
|
|
|
# --- static / UI ---
|
|
expect_status "GET / returns 200" 200 "$BASE/"
|
|
expect_body_contains "GET / serves index html" "DOCTYPE html" "$BASE/"
|
|
expect_status "OPTIONS / CORS preflight" 204 -X OPTIONS "$BASE/"
|
|
expect_status "GET /ui redirects" 307 "$BASE/ui"
|
|
|
|
# --- OpenMetrics / Prometheus ---
|
|
expect_status "GET /metrics returns 200" 200 "$BASE/metrics"
|
|
expect_body_contains "GET /metrics is OpenMetrics" "# TYPE uptime_seconds counter" "$BASE/metrics"
|
|
# Non-GET is rejected; send an explicit (empty) body so mongoose frames the
|
|
# request rather than waiting for a body that curl's bare -X POST never sends.
|
|
expect_status "POST /metrics is rejected" 405 -d '' "$BASE/metrics"
|
|
|
|
# --- /cmd RPC (GET query string and POST form) ---
|
|
expect_body_contains "GET /cmd?cmd=get_meta" "\"samp_rate\"" "$BASE/cmd?cmd=get_meta"
|
|
expect_status "GET /cmd?cmd=get_sample_rate" 200 "$BASE/cmd?cmd=get_sample_rate"
|
|
expect_body_contains "POST /cmd form get_meta" "\"samp_rate\"" -d "cmd=get_meta" "$BASE/cmd"
|
|
expect_status "GET /cmd with no cmd" 200 "$BASE/cmd"
|
|
|
|
# --- more RPC methods, to exercise the rpc_exec getter/setter branches ---
|
|
expect_body_contains "GET /cmd get_protocols" "Nice" "$BASE/cmd?cmd=get_protocols"
|
|
expect_status "GET /cmd get_stats" 200 "$BASE/cmd?cmd=get_stats"
|
|
# These JSON-payload getters build large reports; verify the whole document is
|
|
# well-formed (get_protocols is ~100k, well past the buffer's initial size).
|
|
expect_valid_json "GET /cmd get_protocols is valid JSON" "$BASE/cmd?cmd=get_protocols"
|
|
expect_valid_json "GET /cmd get_stats is valid JSON" "$BASE/cmd?cmd=get_stats"
|
|
expect_valid_json "GET /cmd get_meta is valid JSON" "$BASE/cmd?cmd=get_meta"
|
|
expect_status "GET /cmd get_center_frequency" 200 "$BASE/cmd?cmd=get_center_frequency"
|
|
expect_status "GET /cmd setter center_frequency" 200 "$BASE/cmd?cmd=center_frequency&val=433920000"
|
|
expect_body_contains "GET /cmd unknown method is rejected" "Unknown method" "$BASE/cmd?cmd=no_such_method"
|
|
|
|
# --- JSON-RPC ---
|
|
expect_status "POST /jsonrpc valid method" 200 \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"jsonrpc":"2.0","id":1,"method":"get_sample_rate"}' "$BASE/jsonrpc"
|
|
expect_body_contains "POST /jsonrpc method with params" "result" \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"jsonrpc":"2.0","id":2,"method":"sample_rate","params":[1024000]}' "$BASE/jsonrpc"
|
|
expect_status "POST /jsonrpc malformed body does not crash" 200 \
|
|
-H 'Content-Type: application/json' -d 'not json at all' "$BASE/jsonrpc"
|
|
|
|
# --- streaming endpoints ---
|
|
expect_stream "GET /stream opens" "$BASE/stream"
|
|
expect_stream "GET /events opens" "$BASE/events"
|
|
|
|
# --- WebSocket: handshake, meta push, and an RPC round-trip ---
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
if ws_out=$(python3 "$SCRIPT_DIR/ws-probe.py" "$HOST" "$PORT" 10 2>&1); then
|
|
pass "websocket handshake + meta + rpc round-trip"
|
|
else
|
|
fail "websocket probe failed"
|
|
printf '%s\n' "$ws_out" | sed 's/^/ /' >&2
|
|
fi
|
|
else
|
|
echo " skip: websocket probe (python3 not found)"
|
|
fi
|
|
|
|
# --- robustness: unknown path must not crash the server ---
|
|
curl -s -o /dev/null --max-time 2 "$BASE/no/such/path" 2>/dev/null
|
|
if kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
pass "server still alive after unknown path"
|
|
else
|
|
fail "server died after request to unknown path"
|
|
fi
|
|
|
|
echo
|
|
echo "Results: $PASSED passed, $FAILED failed"
|
|
if [ "$FAILED" -ne 0 ]; then
|
|
echo "--- server log ---" >&2
|
|
cat "$SERVER_LOG" >&2
|
|
exit 1
|
|
fi
|
|
exit 0
|