package main import ( "bytes" "strings" "testing" "time" ) func TestParseConfigDefaults(t *testing.T) { cfg, err := parseConfig([]string{"-to", "peer-b"}, ioDiscard{}) if err != nil { t.Fatalf("parseConfig() error = %v", err) } if cfg.id != defaultPeerID { t.Fatalf("id = %q, want %q", cfg.id, defaultPeerID) } if cfg.server != defaultServer { t.Fatalf("server = %q, want %q", cfg.server, defaultServer) } if cfg.count != defaultCount { t.Fatalf("count = %d, want %d", cfg.count, defaultCount) } if cfg.interval != defaultInterval { t.Fatalf("interval = %s, want %s", cfg.interval, defaultInterval) } if cfg.size != defaultSize { t.Fatalf("size = %d, want %d", cfg.size, defaultSize) } if cfg.timeout != defaultTimeout { t.Fatalf("timeout = %s, want %s", cfg.timeout, defaultTimeout) } } func TestParseConfigRequiresTargetInPingMode(t *testing.T) { _, err := parseConfig([]string{"-echo=false"}, ioDiscard{}) if err == nil || !strings.Contains(err.Error(), "flag -to is required") { t.Fatalf("parseConfig() error = %v, want missing -to error", err) } } func TestParseConfigAllowsEchoWithoutTarget(t *testing.T) { cfg, err := parseConfig([]string{"-echo"}, ioDiscard{}) if err != nil { t.Fatalf("parseConfig() error = %v", err) } if !cfg.echo { t.Fatal("echo = false, want true") } } func TestParseConfigRejectsBindDeviceFlag(t *testing.T) { _, err := parseConfig([]string{"-to", "peer-b", "-bind-device", "wwan0"}, ioDiscard{}) if err == nil || !strings.Contains(err.Error(), "flag provided but not defined") { t.Fatalf("parseConfig() error = %v, want unknown flag error", err) } } func TestBuildPingPayloadUsesExactSize(t *testing.T) { body, err := buildPingPayload(7, 123456789, 96) if err != nil { t.Fatalf("buildPingPayload() error = %v", err) } if len(body) != 96 { t.Fatalf("len(body) = %d, want 96", len(body)) } payload, err := parsePingPayload(body) if err != nil { t.Fatalf("parsePingPayload() error = %v", err) } if payload.Seq != 7 { t.Fatalf("seq = %d, want 7", payload.Seq) } if payload.TSUnixNano != 123456789 { t.Fatalf("ts_ns = %d, want 123456789", payload.TSUnixNano) } } func TestBuildPingPayloadRejectsTooSmallSize(t *testing.T) { _, err := buildPingPayload(1, 123456789, 8) if err == nil || !strings.Contains(err.Error(), "too small") { t.Fatalf("buildPingPayload() error = %v, want size too small error", err) } } func TestParsePingPayloadRejectsInvalidJSON(t *testing.T) { _, err := parsePingPayload([]byte("not-json")) if err == nil || !strings.Contains(err.Error(), "decode ping payload") { t.Fatalf("parsePingPayload() error = %v, want decode error", err) } } func TestPingTrackerHandlesMatchedDuplicateAndTimeout(t *testing.T) { tracker := newPingTracker(50 * time.Millisecond) sentAt := time.Unix(0, 100) tracker.markSent(1, sentAt) match := tracker.observeReply(pingPayload{Seq: 1, TSUnixNano: sentAt.UnixNano()}, sentAt.Add(12*time.Millisecond)) if match.disposition != replyMatched { t.Fatalf("first disposition = %v, want matched", match.disposition) } if match.rtt != 12*time.Millisecond { t.Fatalf("first rtt = %s, want 12ms", match.rtt) } duplicate := tracker.observeReply(pingPayload{Seq: 1, TSUnixNano: sentAt.UnixNano()}, sentAt.Add(20*time.Millisecond)) if duplicate.disposition != replyDuplicate { t.Fatalf("second disposition = %v, want duplicate", duplicate.disposition) } tracker.markSent(2, sentAt) expired := tracker.expire(sentAt.Add(60 * time.Millisecond)) if len(expired) != 1 || expired[0] != 2 { t.Fatalf("expired = %v, want [2]", expired) } late := tracker.observeReply(pingPayload{Seq: 2, TSUnixNano: sentAt.UnixNano()}, sentAt.Add(70*time.Millisecond)) if late.disposition != replyDuplicate { t.Fatalf("late disposition = %v, want duplicate", late.disposition) } unexpected := tracker.observeReply(pingPayload{Seq: 99, TSUnixNano: sentAt.UnixNano()}, sentAt.Add(80*time.Millisecond)) if unexpected.disposition != replyUnexpected { t.Fatalf("unexpected disposition = %v, want unexpected", unexpected.disposition) } } func TestCalculateRTTSummary(t *testing.T) { summary := calculateRTTSummary( []time.Duration{ 10 * time.Millisecond, 20 * time.Millisecond, 30 * time.Millisecond, 40 * time.Millisecond, 50 * time.Millisecond, }, 6, 2, ) if summary.Sent != 6 { t.Fatalf("Sent = %d, want 6", summary.Sent) } if summary.Received != 5 { t.Fatalf("Received = %d, want 5", summary.Received) } if summary.Duplicates != 2 { t.Fatalf("Duplicates = %d, want 2", summary.Duplicates) } if summary.LossPct != (float64(1) * 100 / 6) { t.Fatalf("LossPct = %f, want %f", summary.LossPct, float64(1)*100/6) } if summary.Min != 10*time.Millisecond { t.Fatalf("Min = %s, want 10ms", summary.Min) } if summary.Avg != 30*time.Millisecond { t.Fatalf("Avg = %s, want 30ms", summary.Avg) } if summary.Max != 50*time.Millisecond { t.Fatalf("Max = %s, want 50ms", summary.Max) } if summary.P50 != 30*time.Millisecond { t.Fatalf("P50 = %s, want 30ms", summary.P50) } if summary.P95 != 50*time.Millisecond { t.Fatalf("P95 = %s, want 50ms", summary.P95) } if summary.P99 != 50*time.Millisecond { t.Fatalf("P99 = %s, want 50ms", summary.P99) } if summary.StdDev == 0 { t.Fatal("StdDev = 0, want non-zero") } } func TestWriteSummaryUsesNAWithoutSamples(t *testing.T) { var buf bytes.Buffer err := writeSummary(&buf, "host", rttSummary{ Sent: 3, Received: 0, Duplicates: 1, LossPct: 100, }) if err != nil { t.Fatalf("writeSummary() error = %v", err) } out := buf.String() if !strings.Contains(out, "3 packets transmitted, 0 received, 1 duplicates, 100.00% packet loss") { t.Fatalf("summary output missing counters: %q", out) } if !strings.Contains(out, "n/a/n/a/n/a/n/a/n/a/n/a") { t.Fatalf("summary output missing n/a metrics: %q", out) } } type ioDiscard struct{} func (ioDiscard) Write(p []byte) (int, error) { return len(p), nil }