backtrace/bk/route_report_test.go
2026-08-07 10:19:13 +08:00

183 lines
7.7 KiB
Go

package backtrace
import (
"context"
"net"
"regexp"
"strings"
"testing"
"time"
"github.com/oneclickvirt/backtrace/model"
)
func testHop(distance int, ip string, rtt time.Duration) *Hop {
return &Hop{Distance: distance, Nodes: []*Node{{IP: net.ParseIP(ip), RTT: []time.Duration{rtt}}}}
}
func TestRunRouteReportOfflineFixture(t *testing.T) {
report := RunRouteReport(context.Background(), RouteReportConfig{
Attempts: 2,
Timeout: time.Second,
Targets: []RouteTarget{{Name: "测试电信v4", Address: "192.0.2.1", IPVersion: "v4", Carrier: "CT"}},
Trace: func(_ context.Context, _ net.IP) ([]*Hop, error) {
return []*Hop{
testHop(1, "59.43.1.1", 10*time.Millisecond),
testHop(2, "59.43.2.2", 20*time.Millisecond),
testHop(3, "202.97.1.1", 30*time.Millisecond),
}, nil
},
AlternativeTarget: func(RouteTarget) []string { return nil },
})
if report.SchemaVersion != RouteReportSchema || len(report.Targets) != 1 {
t.Fatalf("unexpected report: %+v", report)
}
target := report.Targets[0]
if target.Status != RouteProbeAvailable || target.SuccessfulAttempts != 2 || target.ValidHops != 3 {
t.Fatalf("unexpected target status: %+v", target)
}
if target.Classification.Code != "ct_cn2_gia" || target.Latency.Samples != 6 || target.Latency.P95MS != 30 {
t.Fatalf("unexpected route classification or latency: %+v", target)
}
rendered := RenderRouteReport(report)
if strings.Contains(rendered, "P95") || !strings.Contains(rendered, "电信CN2GT") || !strings.Contains(rendered, "电信163") {
t.Fatalf("legacy rendering is not compact: %q", RenderRouteReport(report))
}
}
func TestRunRouteReportUsesAlternativeWithoutExposingItsAddress(t *testing.T) {
report := RunRouteReport(context.Background(), RouteReportConfig{
Attempts: 1,
Timeout: time.Second,
Targets: []RouteTarget{{Name: "测试联通v4", Address: "192.0.2.1", IPVersion: "v4", Carrier: "CU"}},
Trace: func(_ context.Context, ip net.IP) ([]*Hop, error) {
if ip.Equal(net.ParseIP("198.51.100.2")) {
return []*Hop{testHop(1, "202.77.1.1", 12*time.Millisecond)}, nil
}
return nil, nil
},
AlternativeTarget: func(RouteTarget) []string { return []string{"198.51.100.2"} },
})
target := report.Targets[0]
if !target.Fallback || target.Classification.Code != "cu_cug" {
t.Fatalf("alternative result = %+v", target)
}
if strings.Contains(RenderRouteReport(report), "198.51.100.2") {
t.Fatalf("legacy output exposed fallback target: %q", RenderRouteReport(report))
}
}
func TestRenderRouteReportPreservesLegacyAddressWidths(t *testing.T) {
report := RouteReport{Targets: []RouteTargetReport{
{
Target: RouteTarget{Name: "北京电信v4", Address: "219.141.140.10", IPVersion: "v4", Carrier: "CT"},
Status: RouteProbeAvailable,
Classification: RouteClassification{Label: "电信163 [普通线路]", Confidence: routeConfidenceConfirmed, Rank: 2},
},
{
Target: RouteTarget{Name: "北京电信v6", Address: "2400:89c0:1053:3::69", IPVersion: "v6", Carrier: "CT"},
Status: RouteProbeAvailable,
Classification: RouteClassification{Label: "电信163 [普通线路]", Confidence: routeConfidenceConfirmed, Rank: 2},
},
}}
rendered := regexp.MustCompile(`\x1b\[[0-9;]*m`).ReplaceAllString(RenderRouteReport(report), "")
if !strings.Contains(rendered, "北京电信v4 219.141.140.10 电信163") {
t.Fatalf("IPv4 legacy spacing changed: %q", rendered)
}
if !strings.Contains(rendered, "北京电信v6 2400:89c0:1053:3::69 电信163") {
t.Fatalf("IPv6 legacy spacing changed: %q", rendered)
}
}
func TestRenderRouteReportUsesClassicASNLabels(t *testing.T) {
report := RouteReport{Targets: []RouteTargetReport{
{
Target: RouteTarget{Name: "北京电信v4", Address: "219.141.140.10", IPVersion: "v4", Carrier: "CT"},
Status: RouteProbeAvailable,
ObservedASNs: []string{"AS6453", "AS4134"},
Classification: inconclusiveClassification("ct_destination_only", "仅见电信目的网", "only one AS4134 hop"),
},
{
Target: RouteTarget{Name: "上海电信v4", Address: "202.96.209.133", IPVersion: "v4", Carrier: "CT"},
Status: RouteProbeAvailable,
ObservedASNs: []string{"AS4809", "AS4134"},
Classification: RouteClassification{Code: "ct_cn2_mixed", Label: "电信CN2混合 [优质线路]", Confidence: routeConfidenceMixed, Rank: 3},
},
{
Target: RouteTarget{Name: "广州联通v4", Address: "210.21.196.6", IPVersion: "v4", Carrier: "CU"},
Status: RouteProbeAvailable,
ObservedASNs: []string{"AS4837"},
Classification: inconclusiveClassification("cu_destination_only", "仅见联通目的网", "only one AS4837 hop"),
},
}}
rendered := regexp.MustCompile(`\x1b\[[0-9;]*m`).ReplaceAllString(RenderRouteReport(report), "")
for _, want := range []string{
"北京电信v4 219.141.140.10 电信163 [普通线路]",
"上海电信v4 202.96.209.133 电信CN2GT [优质线路] 电信163 [普通线路]",
"广州联通v4 210.21.196.6 联通4837 [普通线路]",
} {
if !strings.Contains(rendered, want) {
t.Fatalf("classic route label missing %q from %q", want, rendered)
}
}
if strings.Contains(rendered, "仅见") || strings.Contains(rendered, "未见") {
t.Fatalf("structured fallback label leaked into classic output: %q", rendered)
}
}
func TestRenderRouteReportUsesClassicUnknownMessage(t *testing.T) {
report := RouteReport{Targets: []RouteTargetReport{{
Target: RouteTarget{Name: "北京电信v4", Address: "219.141.140.10", IPVersion: "v4", Carrier: "CT"},
Status: RouteProbeAvailable,
ObservedASNs: []string{"AS6453"},
Classification: inconclusiveClassification("ct_unknown", "未见电信骨干", "known carrier ASNs are absent"),
}}}
rendered := regexp.MustCompile(`\x1b\[[0-9;]*m`).ReplaceAllString(RenderRouteReport(report), "")
if !strings.Contains(rendered, "检测不到已知线路的ASN") || strings.Contains(rendered, "未见电信骨干") {
t.Fatalf("unexpected classic unknown route output: %q", rendered)
}
}
func TestRunRouteReportHonorsContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
report := RunRouteReport(ctx, RouteReportConfig{
Attempts: 1,
Targets: []RouteTarget{{Name: "测试移动v4", Address: "192.0.2.1", IPVersion: "v4", Carrier: "CM"}},
Trace: func(ctx context.Context, _ net.IP) ([]*Hop, error) { return nil, ctx.Err() },
AlternativeTarget: func(RouteTarget) []string { return nil },
})
if report.Targets[0].Status != RouteProbeCanceled {
t.Fatalf("canceled report = %+v", report.Targets[0])
}
}
func TestRefreshAlternativeTargetsHonorsCanceledContextAndKeepsValidCache(t *testing.T) {
alternativeRefreshMu.Lock()
oldData := model.CachedIcmpData
oldTargets := model.ParsedIcmpTargets
oldFetchedAt := model.CachedIcmpDataFetchTime
model.CachedIcmpData = `[{"province":"北京","isp":"电信","ip_version":"v4","ips":"192.0.2.10"}]`
model.ParsedIcmpTargets = []model.IcmpTarget{{Province: "北京", ISP: "电信", IPVersion: "v4", IPs: "192.0.2.10"}}
model.CachedIcmpDataFetchTime = time.Now().Add(-2 * time.Hour)
alternativeRefreshMu.Unlock()
t.Cleanup(func() {
alternativeRefreshMu.Lock()
defer alternativeRefreshMu.Unlock()
model.CachedIcmpData = oldData
model.ParsedIcmpTargets = oldTargets
model.CachedIcmpDataFetchTime = oldFetchedAt
})
ctx, cancel := context.WithCancel(context.Background())
cancel()
started := time.Now()
refreshAlternativeTargets(ctx)
if elapsed := time.Since(started); elapsed > 250*time.Millisecond {
t.Fatalf("canceled refresh took %s", elapsed)
}
if model.CachedIcmpData == "" || len(model.ParsedIcmpTargets) != 1 {
t.Fatalf("failed refresh replaced valid cache: data=%q targets=%v", model.CachedIcmpData, model.ParsedIcmpTargets)
}
}