Running the full device test suite on the real Android target found a
genuine test-harness gap: every extern-network test in
test_network_join_scenarios.cpp failed with "RTNETLINK answers: File
exists" adding its own uplink route. Root cause: each test runs under its
own ScratchXdgDirs, so its own persistent.yaml starts empty every time --
allocate_ipv4_subnet()'s own auto-allocation always picks the very first
slot (10.168.0.0/24) with no way to see whatever real, non-test networks
already exist on the host. The device happens to have a real, long-lived
"extern" network already occupying exactly that subnet from prior manual
testing, and since extern's uplink adds a route back into the (necessarily
shared, host-root) routing table -- unlike intern, whose routing lives
entirely inside its own isolated per-network namespace -- every extern
test collided with it. Not a production bug: a real end user only ever has
one persistent.yaml where allocation correctly sees every existing entry.
Fixed by giving each of the 10 test networks in
test_network_join_scenarios.cpp its own fixed, explicit subnet (--subnet)
in 10.169.0.0/16 -- a different /16 than production's own default
10.168.0.0/16 range, so a test run can't collide with a real network
regardless of how many the host already has.
Also renamed every network/hostname/container-name string literal used
across the test suite (test_network_join_scenarios.cpp,
test_root_networking.cpp, test_rootless_run.cpp,
test_session_cleanup.cpp) from "selftest*" to "test-*", to further reduce
the chance of colliding with anything a real invocation might already be
using. Low-level interface device literals (slkselftest0/thselftest0/
ethselftest in test_root_networking.cpp) are left as-is -- they're
internal identifiers for a throwaway unit test, not network or container
names.
Verified: clean rebuild, meson test, and 3 consecutive
[integration][root] suite runs (61 assertions, 14 test cases) with no
failures.
persistent-netns, tap-relay, and dns-resolver (formerly hand-rolled
bool-returning functions in self_test.cpp, called unconditionally by the
old ad hoc run_self_tests()) move to tests/integration/test_root_networking.cpp
as TEST_CASEs tagged [integration][root][net] -- SKIP() (not a whole-suite
skip) when not root, or when dnsmasq isn't installed for the DNS one, so
e.g. -t -- "[unit]" on a rootless machine is unaffected. Every assertion
uses CHECK, not REQUIRE: these tests manage real host-side namespaces,
bridges, and tap devices that must not leak just because an earlier
assertion failed, so execution always falls through to the same
unconditional cleanup at the end (guarded only by simple pid/bool checks
to skip meaningless dependent steps).
Real bug found running the tap-relay test under Catch2, not assumed:
Catch2 installs its own fatal-signal handler around a running TEST_CASE,
which create_tap_relay()'s own forked relay child inherits -- so the
relay's ordinary shutdown SIGTERM (sent by stop_tap_relay()) got caught by
that *inherited* handler in the child instead of terminating it via the
default disposition the relay's own design relies on, producing a
spurious "FAILED ... due to a fatal error condition: SIGTERM" report
interleaved into the real output (confirmed cosmetic only -- exit code
and assertion count were correct either way, just confusing). Fixed by
resetting SIGTERM to SIG_DFL for the narrow window around the
create_tap_relay() call and restoring it right after -- only the
disposition at fork time is inherited, so nothing about how long the
relay then keeps running matters. No production code changed for this;
it's purely an artifact of forking network primitives from within a
Catch2-instrumented process.
meson.build: the new tests/integration/*.cpp sources are only added to
slocker-lite's own source list when enable_tests is true (mirroring
config.h's ENABLE_TESTS runtime guard, added last commit), and src/ is
added to the target's own include_directories so test sources under
tests/ can #include project headers the same way src/*.cpp already does.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz