From 6ebc9ac6953c3fee470395adac6d8e6f28f472e9 Mon Sep 17 00:00:00 2001 From: Andrew Pimenov Date: Sat, 8 Aug 2026 22:04:34 +0300 Subject: [PATCH] traceparse: os dispatcher and trace completion detection --- src/netcheck/traceparse.py | 32 ++++++++++++++++++- tests/test_traceparse.py | 63 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/netcheck/traceparse.py b/src/netcheck/traceparse.py index 3b27059..8cd0223 100644 --- a/src/netcheck/traceparse.py +++ b/src/netcheck/traceparse.py @@ -2,7 +2,7 @@ from __future__ import annotations import re -from netcheck.models import TraceHop +from netcheck.models import TraceHop, TraceResult from netcheck.stats import rtt_stats IPV4_RE = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b") @@ -147,3 +147,33 @@ def parse_darwin(text: str) -> list[TraceHop]: if token not in hop.annotations: hop.annotations.append(token) return [finalize_hop(hop) for hop in hops] + + +def parse_traceroute(text: str, os_name: str) -> list[TraceHop]: + if os_name == "Windows": + return parse_windows(text) + if os_name == "Darwin": + return parse_darwin(text) + return parse_linux(text) + + +def build_trace_result( + text: str, + os_name: str, + target: str, + resolved_ip: str | None, + backend: str = "system_traceroute", + max_hops: int = 30, +) -> TraceResult: + hops = parse_traceroute(text, os_name) + completed = bool(hops and resolved_ip and hops[-1].ip == resolved_ip) + max_hops_reached = bool(hops) and not completed and hops[-1].ttl >= max_hops + return TraceResult( + target=target, + resolved_ip=resolved_ip, + backend=backend, + hops=hops, + cycles=1, + completed=completed, + max_hops_reached=max_hops_reached, + ) diff --git a/tests/test_traceparse.py b/tests/test_traceparse.py index bcb9e61..d94713d 100644 --- a/tests/test_traceparse.py +++ b/tests/test_traceparse.py @@ -183,3 +183,66 @@ def test_darwin_partial_timeout_hop_keeps_positions(trace_fixture): def test_darwin_parser_tolerates_empty_input(): assert parse_darwin("") == [] + + +from netcheck.traceparse import build_trace_result, parse_traceroute + + +def test_dispatcher_routes_by_os_name(trace_fixture): + win = parse_traceroute(trace_fixture("windows", "tracert_en.txt"), "Windows") + lin = parse_traceroute(trace_fixture("linux", "gnu_basic.txt"), "Linux") + mac = parse_traceroute(trace_fixture("darwin", "bsd_basic.txt"), "Darwin") + assert win[0].probes[1] == 0.5 + assert lin[0].probes == [1.234, 1.102, 1.045] + assert mac[0].probes == [2.145, 1.902, 1.87] + + +def test_dispatcher_defaults_unknown_os_to_the_gnu_parser(trace_fixture): + hops = parse_traceroute(trace_fixture("linux", "gnu_basic.txt"), "SunOS") + assert [h.ttl for h in hops] == [1, 2, 3, 4, 5, 6] + + +def test_build_trace_result_marks_completion_when_the_target_is_reached(trace_fixture): + result = build_trace_result( + trace_fixture("linux", "gnu_basic.txt"), + "Linux", + target="1.1.1.1", + resolved_ip="1.1.1.1", + max_hops=30, + ) + assert result.completed is True + assert result.max_hops_reached is False + assert result.backend == "system_traceroute" + assert result.cycles == 1 + assert len(result.hops) == 6 + + +def test_build_trace_result_flags_a_run_that_died_at_max_hops(trace_fixture): + result = build_trace_result( + trace_fixture("linux", "gnu_maxhops.txt"), + "Linux", + target="203.0.113.200", + resolved_ip="203.0.113.200", + max_hops=5, + ) + assert result.completed is False + assert result.max_hops_reached is True + + +def test_build_trace_result_is_not_complete_when_the_last_hop_is_a_different_host(trace_fixture): + result = build_trace_result( + trace_fixture("linux", "gnu_unreachable.txt"), + "Linux", + target="203.0.113.9", + resolved_ip="203.0.113.9", + max_hops=30, + ) + assert result.completed is False + assert result.max_hops_reached is False + + +def test_build_trace_result_on_empty_output_is_a_well_formed_empty_result(): + result = build_trace_result("", "Linux", target="1.1.1.1", resolved_ip=None) + assert result.hops == [] + assert result.completed is False + assert result.max_hops_reached is False