test: end-to-end FHS path verification + simplify cleanup
Two related changes from a /simplify pass and a follow-up testing
finalization:
1. lib.rs cleanup (no behavior change):
- Drop FHS_LINUX_DATA_DIR and LEGACY_LINUX_DATA_DIR consts. Both
were used in only 4 places total and the unit tests already
bypassed them with string literals, so they were over-engineering.
Inline the strings in daemon_data_dir() and resolve_linux_data_dir().
- Trim narrating doc/comments on the helper and the test bodies.
Keep only the non-obvious WHY (the macOS Homebrew note and the
migration-keeps-legacy rationale).
2. tests/docker/smoke-arch.sh:
- Cherry-picked the previously-uncommitted Arch compatibility smoke
test from feat/smoke-arch.
- Removed the [server] data_dir = "/tmp/numa-smoke" override from
the test config so the script now exercises the DEFAULT data dir
code path — which is exactly what the FHS fix touches.
- Added a path assertion after the dig succeeds: verify that
/var/lib/numa/ca.pem exists (FHS) and /usr/local/var/numa is
absent (no accidental dual-creation on a fresh install).
Verified end-to-end on archlinux:latest (Apple Silicon, Rosetta):
── building + running numa on archlinux:latest ──
── cargo build --release --locked ──
Finished `release` profile [optimized] target(s) in 24.02s
── dig @127.0.0.1 -p 5354 google.com A ──
142.251.38.206
── FHS path check ──
✓ CA cert at /var/lib/numa/ca.pem (FHS path)
✓ legacy path /usr/local/var/numa absent (fresh install used FHS)
── smoke-arch passed ──
This closes the testing gap where the unit tests covered the
path-decision LOGIC in isolation but nothing exercised the live
wiring on a real Linux filesystem.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
27
src/lib.rs
27
src/lib.rs
@@ -98,8 +98,8 @@ fn daemon_data_dir() -> std::path::PathBuf {
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
std::path::PathBuf::from(resolve_linux_data_dir(
|
std::path::PathBuf::from(resolve_linux_data_dir(
|
||||||
std::path::Path::new(LEGACY_LINUX_DATA_DIR).exists(),
|
std::path::Path::new("/usr/local/var/numa").exists(),
|
||||||
std::path::Path::new(FHS_LINUX_DATA_DIR).exists(),
|
std::path::Path::new("/var/lib/numa").exists(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@@ -109,22 +109,14 @@ fn daemon_data_dir() -> std::path::PathBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", test))]
|
/// Extracted as a pure function so the migration logic is unit-testable
|
||||||
const FHS_LINUX_DATA_DIR: &str = "/var/lib/numa";
|
/// without touching the real filesystem.
|
||||||
#[cfg(any(target_os = "linux", test))]
|
|
||||||
const LEGACY_LINUX_DATA_DIR: &str = "/usr/local/var/numa";
|
|
||||||
|
|
||||||
/// Pure path-decision logic for Linux. Returns the FHS-compliant default
|
|
||||||
/// for fresh installs, or the legacy pre-v0.10.1 path if data already
|
|
||||||
/// lives there (so users don't lose their CA cert on upgrade). Extracted
|
|
||||||
/// as a pure function so the migration logic is unit-testable without
|
|
||||||
/// touching the real filesystem.
|
|
||||||
#[cfg(any(target_os = "linux", test))]
|
#[cfg(any(target_os = "linux", test))]
|
||||||
fn resolve_linux_data_dir(legacy_exists: bool, fhs_exists: bool) -> &'static str {
|
fn resolve_linux_data_dir(legacy_exists: bool, fhs_exists: bool) -> &'static str {
|
||||||
if legacy_exists && !fhs_exists {
|
if legacy_exists && !fhs_exists {
|
||||||
LEGACY_LINUX_DATA_DIR
|
"/usr/local/var/numa"
|
||||||
} else {
|
} else {
|
||||||
FHS_LINUX_DATA_DIR
|
"/var/lib/numa"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,27 +126,22 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn linux_data_dir_fresh_install_uses_fhs() {
|
fn linux_data_dir_fresh_install_uses_fhs() {
|
||||||
// No data anywhere → fresh install gets the FHS path.
|
|
||||||
assert_eq!(resolve_linux_data_dir(false, false), "/var/lib/numa");
|
assert_eq!(resolve_linux_data_dir(false, false), "/var/lib/numa");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn linux_data_dir_upgrading_install_keeps_legacy() {
|
fn linux_data_dir_upgrading_install_keeps_legacy() {
|
||||||
// Pre-v0.10.1 install: legacy path has data, FHS path doesn't yet.
|
// Migration must keep legacy so the user doesn't lose their CA on upgrade.
|
||||||
// Migration must keep using legacy so the user doesn't lose their CA.
|
|
||||||
assert_eq!(resolve_linux_data_dir(true, false), "/usr/local/var/numa");
|
assert_eq!(resolve_linux_data_dir(true, false), "/usr/local/var/numa");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn linux_data_dir_after_migration_uses_fhs() {
|
fn linux_data_dir_after_migration_uses_fhs() {
|
||||||
// Both paths exist (e.g., user manually copied data to FHS path).
|
|
||||||
// Prefer FHS since the legacy path is no longer the canonical home.
|
|
||||||
assert_eq!(resolve_linux_data_dir(true, true), "/var/lib/numa");
|
assert_eq!(resolve_linux_data_dir(true, true), "/var/lib/numa");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn linux_data_dir_only_fhs_uses_fhs() {
|
fn linux_data_dir_only_fhs_uses_fhs() {
|
||||||
// Only FHS path has data — straightforward fresh-FHS case.
|
|
||||||
assert_eq!(resolve_linux_data_dir(false, true), "/var/lib/numa");
|
assert_eq!(resolve_linux_data_dir(false, true), "/var/lib/numa");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
147
tests/docker/smoke-arch.sh
Executable file
147
tests/docker/smoke-arch.sh
Executable file
@@ -0,0 +1,147 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Arch Linux compatibility smoke test.
|
||||||
|
#
|
||||||
|
# Builds numa from source inside an archlinux:latest container, runs it
|
||||||
|
# in forward mode on port 5354, and verifies a single DNS query returns
|
||||||
|
# an A record. Validates the "Arch compatible" claim end-to-end before
|
||||||
|
# release announcements.
|
||||||
|
#
|
||||||
|
# Dogfooding: the test numa forwards to the host's running numa via
|
||||||
|
# host.docker.internal (Docker Desktop's host gateway). This avoids the
|
||||||
|
# Docker NAT/UDP issues with public resolvers and exercises the realistic
|
||||||
|
# numa-on-numa shape. Requires the host to be running numa on port 53.
|
||||||
|
#
|
||||||
|
# First run is slow (~8-12 min): image pull + pacman + cold cargo build.
|
||||||
|
# No caching across runs.
|
||||||
|
#
|
||||||
|
# Requirements: docker, host running numa on 0.0.0.0:53
|
||||||
|
# Usage: ./tests/docker/smoke-arch.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
|
GREEN="\033[32m"; RED="\033[31m"; RESET="\033[0m"
|
||||||
|
|
||||||
|
# Precondition: the test numa-on-arch forwards to the host numa as its
|
||||||
|
# upstream (dogfood pattern). Fail fast with a clear error if there is
|
||||||
|
# no working DNS on the host, rather than letting the dig inside the
|
||||||
|
# container time out with "deadline has elapsed".
|
||||||
|
if ! dig @127.0.0.1 google.com A +short +time=1 +tries=1 >/dev/null 2>&1; then
|
||||||
|
printf "${RED}error:${RESET} host numa is not answering on 127.0.0.1:53\n" >&2
|
||||||
|
echo " This test forwards to the host numa via host.docker.internal." >&2
|
||||||
|
echo " Start numa on the host first (sudo numa install), then rerun." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "── building + running numa on archlinux:latest ──"
|
||||||
|
echo " (first run is slow: image pull + pacman + cold cargo build, ~8-12 min)"
|
||||||
|
echo
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--security-opt seccomp=unconfined \
|
||||||
|
-v "$PWD:/src:ro" \
|
||||||
|
-v numa-arch-cargo:/root/.cargo \
|
||||||
|
-v numa-arch-target:/work/target \
|
||||||
|
archlinux:latest bash -c '
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# pacman 7+ filters syscalls in its own sandbox; disable for Rosetta/qemu
|
||||||
|
sed -i "s/^#DisableSandboxSyscalls/DisableSandboxSyscalls/" /etc/pacman.conf
|
||||||
|
|
||||||
|
echo "── pacman: installing build + runtime deps ──"
|
||||||
|
pacman -Sy --noconfirm --needed rust gcc pkgconf cmake make perl bind 2>&1 | tail -3
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Copy source to a writable workdir, skipping target/ + .git so we
|
||||||
|
# do not pull in the host (macOS) build artifacts.
|
||||||
|
mkdir -p /work
|
||||||
|
tar -C /src --exclude=./target --exclude=./.git -cf - . | tar -C /work -xf -
|
||||||
|
cd /work
|
||||||
|
|
||||||
|
echo "── cargo build --release --locked ──"
|
||||||
|
cargo build --release --locked 2>&1 | tail -5
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Dogfood: forward to the host numa via host.docker.internal.
|
||||||
|
# numa parses upstream.address as a literal SocketAddr, so we resolve
|
||||||
|
# the hostname to an IPv4 address first (force v4 — getent hosts may
|
||||||
|
# return IPv6 first, and IPv6 addresses need bracketed addr:port form).
|
||||||
|
HOST_IP=$(getent ahostsv4 host.docker.internal | awk "/STREAM/ {print \$1; exit}")
|
||||||
|
if [ -z "$HOST_IP" ]; then
|
||||||
|
echo " ✗ could not resolve host.docker.internal to IPv4 (not on Docker Desktop?)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "── starting numa on :5354 (forward to host numa at $HOST_IP:53) ──"
|
||||||
|
# Intentionally NOT setting [server] data_dir — we want to exercise the
|
||||||
|
# default code path (data_dir() → daemon_data_dir() → /var/lib/numa) so
|
||||||
|
# the FHS-path assertion below verifies the live wiring, not just the
|
||||||
|
# unit-tested helper.
|
||||||
|
cat > /tmp/numa.toml <<EOF
|
||||||
|
[server]
|
||||||
|
bind_addr = "127.0.0.1:5354"
|
||||||
|
api_port = 5381
|
||||||
|
|
||||||
|
[upstream]
|
||||||
|
mode = "forward"
|
||||||
|
address = "$HOST_IP"
|
||||||
|
port = 53
|
||||||
|
EOF
|
||||||
|
|
||||||
|
./target/release/numa /tmp/numa.toml > /tmp/numa.log 2>&1 &
|
||||||
|
NUMA_PID=$!
|
||||||
|
|
||||||
|
# Poll for readiness — numa is ready when it answers a query
|
||||||
|
READY=0
|
||||||
|
for i in 1 2 3 4 5 6 7 8; do
|
||||||
|
sleep 1
|
||||||
|
if dig @127.0.0.1 -p 5354 google.com A +short +time=1 +tries=1 2>/dev/null \
|
||||||
|
| grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then
|
||||||
|
READY=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$READY" -ne 1 ]; then
|
||||||
|
echo " ✗ numa did not return an A record after 8s"
|
||||||
|
echo " numa log:"
|
||||||
|
cat /tmp/numa.log
|
||||||
|
kill $NUMA_PID 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "── dig @127.0.0.1 -p 5354 google.com A ──"
|
||||||
|
ANSWER=$(dig @127.0.0.1 -p 5354 google.com A +short +time=2 +tries=1)
|
||||||
|
echo "$ANSWER" | sed "s/^/ /"
|
||||||
|
|
||||||
|
kill $NUMA_PID 2>/dev/null || true
|
||||||
|
|
||||||
|
# FHS path assertion: the default data dir on Linux must be /var/lib/numa
|
||||||
|
# (not the legacy /usr/local/var/numa). The CA cert generated at startup
|
||||||
|
# is the canonical proof that numa wrote to the right place.
|
||||||
|
echo
|
||||||
|
echo "── FHS path check ──"
|
||||||
|
if [ -f /var/lib/numa/ca.pem ]; then
|
||||||
|
echo " ✓ CA cert at /var/lib/numa/ca.pem (FHS path)"
|
||||||
|
else
|
||||||
|
echo " ✗ CA cert NOT at /var/lib/numa/ca.pem"
|
||||||
|
echo " ls /var/lib/numa/:"
|
||||||
|
ls -la /var/lib/numa/ 2>&1 | sed "s/^/ /"
|
||||||
|
echo " ls /usr/local/var/numa/:"
|
||||||
|
ls -la /usr/local/var/numa/ 2>&1 | sed "s/^/ /"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -e /usr/local/var/numa ]; then
|
||||||
|
echo " ✗ legacy path /usr/local/var/numa unexpectedly exists on a fresh container"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " ✓ legacy path /usr/local/var/numa absent (fresh install used FHS)"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " ✓ numa built, ran, answered a forward query, and used the FHS data dir on Arch"
|
||||||
|
'
|
||||||
|
|
||||||
|
echo
|
||||||
|
printf "${GREEN}── smoke-arch passed ──${RESET}\n"
|
||||||
Reference in New Issue
Block a user