mirror of
https://github.com/Horizontal42/netsleuth.git
synced 2026-11-10 15:42:10 +03:00
78 lines
3.3 KiB
Python
78 lines
3.3 KiB
Python
from __future__ import annotations
|
|
|
|
import time
|
|
from unittest.mock import patch
|
|
|
|
from netsleuth.probes.icmp_win import IP_REQ_TIMED_OUT, IcmpReply, IcmpRequest, trace_hops_win
|
|
|
|
|
|
def _fake_gethostbyname(host: str) -> str:
|
|
return "203.0.113.1"
|
|
|
|
|
|
def test_trace_hops_win_total_elapsed_is_bounded_by_the_total_budget_not_hops_times_timeout():
|
|
# Every hop times out; each fake call sleeps for its per-hop timeout to
|
|
# simulate a genuinely unresponsive router. Old (buggy) code handed the
|
|
# *entire* 100ms total budget to every one of the 30 hops as its own
|
|
# per-hop timeout, i.e. ~3s worst case for this scenario. The fix must
|
|
# divide that budget across hops instead, keeping total wall time close
|
|
# to the 100ms total budget.
|
|
call_timeouts: list[int] = []
|
|
|
|
def fake_echo_once(req: IcmpRequest) -> IcmpReply:
|
|
call_timeouts.append(req.timeout_ms)
|
|
time.sleep(req.timeout_ms / 1000.0)
|
|
return IcmpReply(address=None, status=IP_REQ_TIMED_OUT, rtt_ms=None, ttl=0)
|
|
|
|
with (
|
|
patch("netsleuth.probes.icmp_win.socket.gethostbyname", side_effect=_fake_gethostbyname),
|
|
patch("netsleuth.probes.icmp_win.echo_once", side_effect=fake_echo_once),
|
|
):
|
|
started = time.monotonic()
|
|
trace_hops_win("example.invalid", max_hops=30, timeout_ms=100)
|
|
elapsed = time.monotonic() - started
|
|
|
|
# Comfortably under the ~3s the old per-hop-gets-the-whole-budget bug would
|
|
# have produced here, while still generous about scheduling jitter.
|
|
assert elapsed < 1.0
|
|
# Every individual hop got a fraction of the total budget, not the whole thing.
|
|
assert call_timeouts
|
|
assert all(t == 100 // 30 for t in call_timeouts)
|
|
|
|
|
|
def test_trace_hops_win_per_hop_timeout_is_capped_even_for_a_huge_total_budget():
|
|
call_timeouts: list[int] = []
|
|
|
|
def fake_echo_once(req: IcmpRequest) -> IcmpReply:
|
|
call_timeouts.append(req.timeout_ms)
|
|
return IcmpReply(address=None, status=IP_REQ_TIMED_OUT, rtt_ms=None, ttl=0)
|
|
|
|
with (
|
|
patch("netsleuth.probes.icmp_win.socket.gethostbyname", side_effect=_fake_gethostbyname),
|
|
patch("netsleuth.probes.icmp_win.echo_once", side_effect=fake_echo_once),
|
|
):
|
|
# A single-hop trace with a huge total budget (e.g. subprocess_seconds=60s
|
|
# divided across a small max_hops) must still not hand the whole budget
|
|
# to one hop - it's capped at _MAX_HOP_TIMEOUT_MS (4s).
|
|
trace_hops_win("example.invalid", max_hops=1, timeout_ms=60_000)
|
|
|
|
assert call_timeouts == [4000]
|
|
|
|
|
|
def test_trace_hops_win_happy_path_hop_count_and_ok_break_are_unchanged():
|
|
from netsleuth.probes.icmp_win import IP_SUCCESS
|
|
|
|
def fake_echo_once(req: IcmpRequest) -> IcmpReply:
|
|
if req.ttl < 3:
|
|
return IcmpReply(address=f"10.0.0.{req.ttl}", status=IP_REQ_TIMED_OUT, rtt_ms=None, ttl=0)
|
|
return IcmpReply(address="203.0.113.1", status=IP_SUCCESS, rtt_ms=5.0, ttl=64)
|
|
|
|
with (
|
|
patch("netsleuth.probes.icmp_win.socket.gethostbyname", side_effect=_fake_gethostbyname),
|
|
patch("netsleuth.probes.icmp_win.echo_once", side_effect=fake_echo_once),
|
|
):
|
|
hops = trace_hops_win("example.invalid", max_hops=30, timeout_ms=60_000)
|
|
|
|
# Fast-responding hops still stop the loop at the first success, same as before.
|
|
assert [ttl for ttl, _ in hops] == [1, 2, 3]
|
|
assert hops[-1][1].status == IP_SUCCESS
|