Skip to content

test(dns): close #158 — end-to-end execution tests for enable_dns_mode - #161

Merged
flyhigher139 merged 1 commit into
masterfrom
fix/dns-mode-test-coverage-158
Aug 26, 2026
Merged

test(dns): close #158 — end-to-end execution tests for enable_dns_mode#161
flyhigher139 merged 1 commit into
masterfrom
fix/dns-mode-test-coverage-158

Conversation

@flyhigher139

Copy link
Copy Markdown
Contributor

Closes #158.

Context

PR #156 review (commit 5e0b109) already addressed F1/F2 — build_enable_script, validate_proxy_binary, and EnableScriptInputs are already extracted as pub(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) — restores MHOST_RUNTIME_DIR + PATH on test exit so a panic mid-test doesn't pollute sibling tests.
  • setup_fake_bin_env(proxy_contents, networksetup_contents) — writes fake mhost-dns-proxy + fake networksetup to a tempdir and prepends it to PATH. 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 Scenario
test_enable_script_happy_path_with_fakes long-running proxy + 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).
test_enable_script_kill_zero_catches_immediate_exit fake proxy exit 1 immediately → script exits non-zero with exited within 1s + log dump on stderr; PID file cleaned by EXIT trap.
test_enable_script_transactional_cleanup_on_networksetup_failure long proxy + failing networksetup → EXIT trap kills the proxy + removes PID file + propagates the non-zero exit code.
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 name.
test_old_buggy_script_silently_succeeds_with_dead_pid hand-builds the pre-#155 buggy script (set -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-dns tests go from 104 → 109 (+5 new tests, all in the enable_dns_mode execution surface).

🤖 Generated with Claude Code

@flyhigher139 flyhigher139 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 1 in the production script + kill -0 is 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 -0 check, the window is microseconds. PID reuse in that window is theoretical, not practical.
  • split_whitespace() on PID file content: brittle if fake_proxy path has spaces (caught during local dev, fixed by removing spaces from setup_fake_bin_env's prefix). The production code's read_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.

Ready to merge after fix #1. #2-#4 can follow-up or skip.

…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
@flyhigher139
flyhigher139 force-pushed the fix/dns-mode-test-coverage-158 branch from 9e80a7e to b414882 Compare August 26, 2026 09:22

@flyhigher139 flyhigher139 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(...) 然后传给 helper
  • test_old_buggy_script_silently_succeeds_with_dead_pidformat! 拼 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.

@flyhigher139
flyhigher139 merged commit fa71010 into master Aug 26, 2026
4 checks passed
@flyhigher139
flyhigher139 deleted the fix/dns-mode-test-coverage-158 branch August 26, 2026 12:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Tech-debt] Tests for enable_dns_mode script don't exercise production code (#156 F1)

1 participant