Testing
April 19, 2026 · View on GitHub
cargo test
That's the whole thing — no fixtures, no test database, no env vars required. The tests run on Linux and macOS; the live ones skip themselves when the underlying tool isn't available.
What's covered
125 unit tests across 9 modules — 115 fixture tests plus 10 property-based cases — weighted toward parsing and state-machine logic. The shape of the suite:
| Module | Tests | What they actually exercise |
|---|---|---|
traffic.rs | 4 | First-call zero rates; history accumulation; 60-sample window cap; eviction of dropped interfaces |
connections.rs | 19 | parse_ss_output (+hidden users, non-ESTAB states, short rows), parse_nettop_output (+zero-RTT skip, multi-ms tokens), parse_lsof_output (UDP, IPv6 brackets, multi-socket processes), RTT merge, top_connections ranking |
process_bandwidth.rs | 5 | Proportional split, ranking by combined rate, max truncation, empty-input guards |
network_intel.rs | 21 | Each detector; DNS analytics + latency bucketing; bandwidth clear-ratio recovery; per-interface threshold isolation; alert-history cap at 100; port-scan isolation per source IP; split_host_port |
health.rs | 10 | parse_loss (zero / partial / full / empty), parse_avg_rtt (Linux + macOS), RttHistory window cap + None gap preservation, live loopback smoke test |
system.rs | 25 | parse_proc_loadavg, parse_proc_meminfo (with/without MemAvailable), parse_proc_swap, parse_vm_stat, parse_macos_swapusage, parse_proc_cpuinfo_model, plus Phase 3 parse_proc_stat_aggregate / _per_core and cpu_pct_from_samples (busy, idle, reversed, rounding) |
config.rs | 11 | parse_default_gateway_ip_route, parse_default_gateway_netstat (Linux + macOS formats), parse_first_nameserver (comments, indent, keyword-prefix safety) |
disk.rs | 13 | parse_proc_mounts (real devices, virtual FS skip, snap/loop filtering), parse_macos_mount (root, firmlink skip), parse_proc_diskstats (sector summing, loop/ram/dm-* skip) |
platform/linux.rs | 5 | collect_interface_stats_from driven against a tempfile-populated sysfs tree: counters + operstate, loopback-skip, missing-file defaults, operstate variants (up/down/unknown), missing-root error |
Property-based tests
In addition to the fixture suite, parse_ss_output, parse_nettop_output, parse_lsof_output, parse_proc_meminfo, parse_proc_loadavg, parse_proc_stat_aggregate, parse_vm_stat, and cpu_pct_from_samples each have a proptest block in their module. The properties assert invariants — never panics on arbitrary bytes, emitted fields always populated, used_bytes ≤ total_bytes, cpu_pct always in [0, 100] — and each case runs against several hundred random inputs per run.
These already paid for themselves:
parse_lsof_outputwas panicking on lines whose first byte was the start of a multi-byte UTF-8 sequence (e.g.é…) —&line[1..]would land inside a char.cpu_pct_from_sampleswas underflowing au64subtraction whenidle_diff > total_diff(counter-reset edge case).
Both bugs were invisible to the fixture suite.
Test conventions in this repo
-
Pure parsers are public so they can be tested directly without spawning a process.
parse_ss_output,parse_nettop_output, andparse_lsofall live aspub fns at module scope. -
Live tests are gated on tool availability so the suite stays green on minimal CI images:
#[test] fn live_ss_returns_something() { if std::process::Command::new("ss").arg("--version").output().is_err() { return; // ss not installed; skip } let conns = collect_connections(); // …assert structural properties, not exact contents… } -
Detector tests build a collector and feed it events — they don't mock time, they just push enough events into a fresh
NetworkIntelCollectorto cross the threshold. Look atnetwork_intel.rstests for the pattern when adding a new detector. -
No async runtime assumptions. All tests are synchronous
#[test]. If you add a stateful collector that needs a runtime, gate the test behind a feature.
Running a subset
cargo test --test '' # all unit tests
cargo test parse_ss # by name pattern
cargo test --package netwatch-sdk -- --nocapture # see println! output
Coverage gaps worth knowing
system::measure_cpu_usageandmeasure_cpu_per_corearen't tested because they sleep and read live/proc/stat. Reaching them needs the FsReader seam (Phase 3 indocs/plans/test-coverage.md).libc::statvfspaths indisk::stat_mountaren't tested — same class of problem as the CPU sampling. Unsafe syscall, no mock today.- Live
connections::collect_connectionsrunsss/lsof/nettopagainst the real machine. The test asserts only "no panic; results are well-formed", because exact contents depend on the host. - Cross-platform behaviour is verified by running CI on both Linux and macOS runners, not by mocking each platform from the other.
Measuring coverage
CI runs cargo-llvm-cov on every push and uploads an lcov.info artifact. The job fails if line coverage drops below 83 % (current baseline is ~86 %, so there's modest headroom).
To reproduce locally:
cargo install cargo-llvm-cov # one-time
rustup component add llvm-tools-preview # one-time
cargo llvm-cov --lib --summary-only
Per-file baseline at the time of writing:
| File | Lines |
|---|---|
collectors/process_bandwidth.rs | 99 % |
platform/linux.rs | 97 % |
collectors/traffic.rs | 94 % |
collectors/connections.rs | 89 % |
collectors/network_intel.rs | 85 % |
collectors/config.rs | 85 % |
collectors/disk.rs | 84 % |
collectors/health.rs | 79 % |
collectors/system.rs | 79 % |
| Total | 86 % |
platform/linux.rs is at 0 % because nothing in the test suite calls collect_interface_stats() directly — traffic tests construct an InterfaceStats map by hand. That's by design: platform shims are exercised by the integration suite in the agent, not by SDK unit tests.
The remaining ~16 % on disk.rs is the unsafe libc::statvfs path inside stat_mount. Reaching it from a unit test would require an FsReader-style seam — see docs/plans/test-coverage.md Phase 3.
Once coverage trends up, raise --fail-under-lines in .github/workflows/ci.yml so the floor moves with you.
Pre-commit hook (recommended)
Same pattern as the netwatch / netscan repos:
cat > .git/hooks/pre-commit <<'SH'
#!/bin/sh
set -e
if ! cargo fmt --check; then
echo "rustfmt would rewrite files above. Run: cargo fmt && git add -u"
exit 1
fi
SH
chmod +x .git/hooks/pre-commit
If you also want clippy and tests to run before each commit, append them — but be aware they slow commits noticeably and CI catches them anyway.