test(dns): close #158 — end-to-end execution tests for enable_dns_mode - #161
Conversation
flyhigher139
left a comment
There was a problem hiding this comment.
Self-review (issue #158 fix)
Looking at the diff fresh — what holds up, what should change before merge.
Verdict: ✅ Approve with 1 should-fix + 3 nice-to-haves
The PR closes #158's remaining gap (execution tests, not just structural). 5 new tests all pass locally + cargo fmt --check + cargo clippy --all-targets -- -D warnings. Net +5 tests in mhost-dns (104 → 109). Resource cleanup is mostly handled by RAII (EnvRestore, TempDir) and explicit kill_proxy_from_pid_file. Test isolation is enforced by the existing serial_runtime_dir_test() lock.
Should-fix (1)
1. test_enable_script_transactional_cleanup_on_networksetup_failure masks the regression it's testing.
Lines 2548-2566: we unsafe { libc::kill(proxy_pid, libc::SIGKILL); } before the assertion that the trap killed the proxy. If a future regression breaks the EXIT-trap kill, this unconditional SIGKILL still satisfies kill -0 == -1, and the assertion passes — masking the bug. The "defensive cleanup" intent is good, but it must run after the assertion, not before.
Suggested order:
// 1. assert exit code != 0
// 2. assert proxy is dead (this is the regression we're guarding)
// 3. only THEN, defensive SIGKILL (silent ignore if already dead)Same pattern applies to test 1 (happy path): the kill_proxy_from_pid_file call is at the bottom, after the alive-check assertion, which is already correct — but worth re-confirming in review.
Nice-to-have (3)
2. kill_proxy_from_pid_file has no SIGKILL fallback.
SIGTERM is honored by sleep and most processes, but if a future test uses a fake proxy that traps SIGTERM, we leak a zombie. Cheap fix:
unsafe { libc::kill(pid, libc::SIGTERM); }
std::thread::sleep(Duration::from_millis(150));
// fallback if still alive
if unsafe { libc::kill(pid, 0) } == 0 {
unsafe { libc::kill(pid, libc::SIGKILL); }
}3. test_old_buggy_script_silently_succeeds_with_dead_pid is a documentation test, not a regression guard.
The hand-built buggy script demonstrates #155's silent failure concretely. But the existing structural tests (test_pid_file_content_format, test_enable_script_contains_safety_layers) already fail if the production builder regresses to set -e + cmd & echo "$! ..." — they assert [ -x ], kill -0, trap cleanup EXIT, and the PROXY_PID named variable, all absent from the buggy form. So test 5's value is pedagogical ("this is what we're protecting against") rather than load-bearing.
Recommend: keep it, but trim the suggestion in the doc-comment that it would catch a regression (line 2706-2707). Just say it's the silent-failure demo.
4. The script-write logic is duplicated between exec_production_enable_script and test_old_buggy_script_silently_succeeds_with_dead_pid.
Both write a 0o700 temp script, run /bin/sh, and delete. Old-buggy test inlines it because the script content comes from a format!, not the production builder. Could extract a write_and_exec_script(name: &str, body: &str) -> Output helper. 6 lines saved, but more importantly removes the inconsistency where old-buggy omits sync_all() (lines 2772-2780) while exec_production_enable_script has it.
Non-issues (verified)
- Test reliability: the
sleep 1in the production script +kill -0is well-trodden ground (issue #140); no flakiness on macOS. Verified across 3 sequential runs locally, all green in ~8.3s. - PID-reuse race in happy-path test: between script exit and our
kill -0check, the window is microseconds. PID reuse in that window is theoretical, not practical. split_whitespace()on PID file content: brittle iffake_proxypath has spaces (caught during local dev, fixed by removing spaces fromsetup_fake_bin_env's prefix). The production code'sread_proxy_pid()has the same fragility but production paths don't have spaces — acceptable.- RAII for fake proxy cleanup: would be cleaner than relying on tests reaching the kill line, but the leaked process is just
sleep 30(no port conflict, dies naturally). Not worth the abstraction.
One nit (cosmetic)
The new test names mix snake_case Chinese prefixes with English (test_enable_script_*). Matches existing convention. No action.
…ns_mode Adds a fake-binary test harness plus 5 execution tests that consume the production build_enable_script() output and run it through /bin/sh. setup_fake_bin_env writes fake mhost-dns-proxy + fake networksetup to a tempdir and prepends it to PATH, so production scripts can be exercised without root / without touching the real system. exec_production_enable_script writes the builder output to a 0o700 temp file and shells out to /bin/sh. kill_proxy_from_pid_file cleans up the long-running fake proxy left by happy-path tests. EnvRestore RAII guard restores MHOST_RUNTIME_DIR + PATH so a panic mid-test does not pollute sibling tests. The 5 new tests: 1. test_enable_script_happy_path_with_fakes — long-running proxy and succeeding networksetup → script exits 0, PID file written with PID FULL_PROXY_PATH (#81 safe format), networksetup received the right args, and EXIT trap was disarmed (proxy PID still alive). 2. test_enable_script_kill_zero_catches_immediate_exit — fake proxy exit 1 immediately → script exits non-zero with exited within 1s plus log dump on stderr; PID file cleaned by EXIT trap. 3. test_enable_script_transactional_cleanup_on_networksetup_failure — long proxy + failing networksetup → EXIT trap kills the proxy, removes PID file, and propagates the non-zero exit code. 4. test_enable_script_end_to_end_with_spaces_in_paths — full happy path with spaces in every path component (runtime dir, fake bin dir, log, interface) → script exits 0, PID file created at the path with spaces, fake networksetup received the full interface. 5. test_old_buggy_script_silently_succeeds_with_dead_pid — hand-builds the pre-#155 buggy script (set -e plus cmd & echo without [ -x ] / kill -0 / trap) and demonstrates it exits 0 with a PID file pointing at a dead process. Acts as a regression proof: if the production builder ever regresses to this form, the structural tests would also catch it but this test makes the failure mode concrete. CI: cargo fmt, cargo clippy --all-targets --all-features -D warnings, and cargo test --all-features --workspace all green. mhost-dns goes from 104 to 109 tests. Refs: #158
9e80a7e to
b414882
Compare
flyhigher139
left a comment
There was a problem hiding this comment.
Self-review fixes applied (commit b414882)
Should-fix (1) ✅ Fixed
test_enable_script_transactional_cleanup_on_networksetup_failure 断言顺序已调换:
- 所有断言(exit code / proxy killed / PID file cleaned / ns_log present)先跑
- unconditional
SIGKILL兜底 cleanup 放到断言之后 - 这样未来如果 EXIT trap 行为回归(不再 kill proxy),断言会立刻 fail,SIGKILL 不会再遮盖 bug
Nice-to-have #1 ✅ Fixed
kill_proxy_from_pid_file 加了 SIGKILL fallback:
- 先 SIGTERM → 等 150ms → kill -0 检查还活着吗
- 还活着就升级到 SIGKILL(防止 fake proxy trap 了 SIGTERM 时泄漏 zombie)
- 两次都 silent ignore(PID 已被 reap 返回 ESRCH)
Nice-to-have #3 ✅ Fixed
抽出 write_and_exec_script(name_prefix, body) -> Output helper,统一两个 caller:
exec_production_enable_script内部调build_enable_script(...)然后传给 helpertest_old_buggy_script_silently_succeeds_with_dead_pid调format!拼 buggy 脚本然后传给 helper- 删掉了 inline 的 OpenOptions / sync_all 重复(也顺手统一了 old-buggy 测试之前漏掉的
sync_all())
Nice-to-have #2 ✅ Fixed
test_old_buggy_script_silently_succeeds_with_dead_pid 的 doc-comment 重写:
- 标题从「旧 buggy 脚本回归证明」改为「#155 silent-failure 文档测试」
- 显式说明不是 regression guard —— 已有
test_pid_file_content_format/test_enable_script_contains_safety_layers这两个结构测试 catch 回归 - 明确本测试的价值是 pedagogical demo
Verification
cargo fmt --all -- --check✅cargo clippy --all-targets --all-features -- -D warnings✅cargo test --all-features --workspace✅ (429 tests pass)
PR ready to merge.
Closes #158.
Context
PR #156 review (commit
5e0b109) already addressed F1/F2 —build_enable_script,validate_proxy_binary, andEnableScriptInputsare already extracted aspub(crate)and the existing 3 tests already consume the production builder. The remaining gap from this issue was end-to-end execution coverage: existing tests only exercised the Layer-1 fail path (binary missing → exit 127).What this PR adds
A fake-binary test harness plus 5 execution tests that consume the production
build_enable_script()output and run it through/bin/sh:Harness
EnvRestore(RAII guard) — restoresMHOST_RUNTIME_DIR+PATHon test exit so a panic mid-test doesn't pollute sibling tests.setup_fake_bin_env(proxy_contents, networksetup_contents)— writes fakemhost-dns-proxy+ fakenetworksetupto a tempdir and prepends it toPATH. Production scripts can be exercised without root / without touching the real system.exec_production_enable_script(inputs)— writes the builder output to a 0o700 temp file and shells out to/bin/sh.kill_proxy_from_pid_file(path)— cleans up the long-running fake proxy left by happy-path tests.5 new tests
test_enable_script_happy_path_with_fakesPID FULL_PROXY_PATH(#81 safe format), networksetup received the right args, and EXIT trap was disarmed (proxy PID still alive).test_enable_script_kill_zero_catches_immediate_exitexit 1immediately → script exits non-zero withexited within 1s+ log dump on stderr; PID file cleaned by EXIT trap.test_enable_script_transactional_cleanup_on_networksetup_failuretest_enable_script_end_to_end_with_spaces_in_pathstest_old_buggy_script_silently_succeeds_with_dead_pidset -e + cmd & echo "$! ..."without[ -x ]/kill -0/ trap) and demonstrates it exits 0 with a PID file pointing at a dead process. Acts as a regression proof for the production fix.Verification
CI matrix locally:
cargo fmt --all -- --check✅cargo clippy --all-targets --all-features -- -D warnings✅cargo test --all-features --workspace✅mhost-dnstests go from 104 → 109 (+5 new tests, all in theenable_dns_modeexecution surface).🤖 Generated with Claude Code