package huya import ( "context" "errors" "testing" "time" ) // TestServerTimestampFromRealPingSignalPayload 用 2026-10-13 的真实报文夹具验证: // signal_ap_* 是 Unix 毫秒时间戳,能被解析成正确的墙钟时间。 func TestServerTimestampFromRealPingSignalPayload(t *testing.T) { var resp PingSignalResp readStructPayload(t, mustDecodeHex(t, realPingSignalRspHex), &resp) at, ok := resp.ServerTimestamp() if !ok { t.Fatalf("ServerTimestamp() 未识别真实报文里的时间戳: %v", resp.Values) } want := time.UnixMilli(1791891743288).UTC() if !at.Equal(want) { t.Fatalf("ServerTimestamp()=%s, want %s", at, want) } // 夹具来自 2026-10-13 的抓包,时间戳必须落在那天(北京时间为 19:42)。 if year, month, day := at.In(time.FixedZone("CST", 8*3600)).Date(); year != 2026 || month != time.October || day != 13 { t.Fatalf("服务端时间日期=%d-%02d-%02d, want 2026-10-13", year, month, day) } } func TestServerTimestampRejectsImplausibleValues(t *testing.T) { tests := []struct { name string values map[string]int64 wantOK bool }{ {name: "empty", values: map[string]int64{}, wantOK: false}, {name: "nil", values: nil, wantOK: false}, // 5 位数字明显不是 Unix 毫秒(会被解析成 1970 年),必须拒绝, // 否则会按错误偏移把触发时刻整体挪走。 {name: "tiny value", values: map[string]int64{PingSignalStartKey: 12345}, wantOK: false}, {name: "seconds instead of ms", values: map[string]int64{PingSignalStartKey: 1791891743}, wantOK: false}, {name: "start wins", values: map[string]int64{ PingSignalStartKey: 1791891743288, PingSignalEndKey: 1791891743301, }, wantOK: true}, {name: "end fallback", values: map[string]int64{PingSignalEndKey: 1791891743288}, wantOK: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resp := PingSignalResp{Values: tt.values} at, ok := resp.ServerTimestamp() if ok != tt.wantOK { t.Fatalf("ServerTimestamp() ok=%v, want %v (values=%v)", ok, tt.wantOK, tt.values) } if ok && at.Equal(time.Time{}) { t.Fatalf("ok=true 但时间为零值") } }) } } func TestTimeSampleOffsetUsesHalfRTT(t *testing.T) { base := time.Date(2026, 10, 16, 19, 0, 0, 0, time.UTC) // 本机比服务端慢 300ms:服务端在「本机发送后 40ms」打的时间戳 = base+300ms+40ms。 sample := TimeSample{ ServerAt: base.Add(300*time.Millisecond + 40*time.Millisecond), SentAt: base, RecvAt: base.Add(80 * time.Millisecond), } if got := sample.RTT(); got != 80*time.Millisecond { t.Fatalf("RTT()=%s, want 80ms", got) } if got := sample.ErrorBound(); got != 40*time.Millisecond { t.Fatalf("ErrorBound()=%s, want 40ms", got) } if got := sample.Offset(); got != 300*time.Millisecond { t.Fatalf("Offset()=%s, want 300ms", got) } } func TestRunTimeCalibrationPicksLowestErrorSample(t *testing.T) { base := time.Date(2026, 10, 16, 19, 0, 0, 0, time.UTC) rtts := []time.Duration{200 * time.Millisecond, 40 * time.Millisecond, 120 * time.Millisecond} index := 0 sampler := func(ctx context.Context) (TimeSample, error) { rtt := rtts[index] index++ return TimeSample{ ServerAt: base.Add(500*time.Millisecond + rtt/2), SentAt: base, RecvAt: base.Add(rtt), }, nil } var history OffsetHistory calibration, err := RunTimeCalibration(context.Background(), 3, &history, sampler) if err != nil { t.Fatal(err) } if calibration.Samples != 3 { t.Fatalf("Samples=%d, want 3", calibration.Samples) } if calibration.RTT != 40*time.Millisecond || calibration.ErrorBound != 20*time.Millisecond { t.Fatalf("应取误差最小的样本: RTT=%s ErrorBound=%s", calibration.RTT, calibration.ErrorBound) } if calibration.Offset != 500*time.Millisecond { t.Fatalf("Offset=%s, want 500ms", calibration.Offset) } if calibration.Smoothed { t.Fatalf("首轮只有一个样本时不应标记为已平滑") } } // TestRunTimeCalibrationSmoothsOutlierWithMedian 验证中位数平滑能过滤单轮跳变。 func TestRunTimeCalibrationSmoothsOutlierWithMedian(t *testing.T) { base := time.Date(2026, 10, 16, 19, 0, 0, 0, time.UTC) offsets := []time.Duration{100 * time.Millisecond, 900 * time.Millisecond, 100 * time.Millisecond} var history OffsetHistory var calibration TimeCalibration for _, offset := range offsets { offset := offset sampler := func(ctx context.Context) (TimeSample, error) { return TimeSample{ ServerAt: base.Add(offset), SentAt: base, RecvAt: base, // RTT=0,Offset 就等于偏移本身 }, nil } var err error calibration, err = RunTimeCalibration(context.Background(), 1, &history, sampler) if err != nil { t.Fatal(err) } } if !calibration.Smoothed { t.Fatalf("多轮采样后应标记为已平滑") } if calibration.Offset != 100*time.Millisecond { t.Fatalf("Offset=%s, want 100ms(900ms 的跳变应被中位数滤掉)", calibration.Offset) } } func TestRunTimeCalibrationFallsBackWhenAllSamplesFail(t *testing.T) { failure := errors.New("pingSignal 不可用") sampler := func(ctx context.Context) (TimeSample, error) { return TimeSample{}, failure } var history OffsetHistory calibration, err := RunTimeCalibration(context.Background(), 2, &history, sampler) if !errors.Is(err, failure) { t.Fatalf("err=%v, want %v", err, failure) } if !calibration.At.IsZero() || calibration.Samples != 0 { t.Fatalf("失败时不应返回可用校准: %+v", calibration) } } func TestRunTimeCalibrationToleratesPartialFailure(t *testing.T) { base := time.Date(2026, 10, 16, 19, 0, 0, 0, time.UTC) round := 0 sampler := func(ctx context.Context) (TimeSample, error) { round++ if round != 2 { return TimeSample{}, errors.New("偶发失败") } return TimeSample{ServerAt: base.Add(150 * time.Millisecond), SentAt: base, RecvAt: base}, nil } var history OffsetHistory calibration, err := RunTimeCalibration(context.Background(), 3, &history, sampler) if err != nil { t.Fatal(err) } if calibration.Samples != 1 { t.Fatalf("Samples=%d, want 1", calibration.Samples) } if calibration.Offset != 150*time.Millisecond { t.Fatalf("Offset=%s, want 150ms", calibration.Offset) } } func TestAlignedTriggerTimeCompensatesClockSkew(t *testing.T) { target := time.Date(2026, 10, 16, 20, 0, 0, 0, time.Local) tests := []struct { name string lead time.Duration offset time.Duration want time.Time }{ {name: "no skew", lead: 2500 * time.Millisecond, offset: 0, want: target.Add(-2500 * time.Millisecond)}, // 本机比服务端慢 300ms:服务端的 20:00:00 发生在本机 19:59:59.700,再提前 lead。 {name: "local behind", lead: 2500 * time.Millisecond, offset: 300 * time.Millisecond, want: target.Add(-2800 * time.Millisecond)}, // 本机比服务端快 300ms:本机 20:00:00.300 才是服务端的 20:00:00。 {name: "local ahead", lead: 2500 * time.Millisecond, offset: -300 * time.Millisecond, want: target.Add(-2200 * time.Millisecond)}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := AlignedTriggerTime(target, tt.lead, tt.offset); !got.Equal(tt.want) { t.Fatalf("AlignedTriggerTime()=%s, want %s", got, tt.want) } }) } } func TestTimeCalibrationServerNowAppliesOffset(t *testing.T) { calibration := TimeCalibration{Offset: 2 * time.Second, At: time.Now()} before := time.Now().Add(2 * time.Second).UTC() got := calibration.ServerNow() if got.Before(before.Add(-100*time.Millisecond)) || got.After(before.Add(100*time.Millisecond)) { t.Fatalf("ServerNow()=%s, 偏离预期 %s 太多", got, before) } var zero TimeCalibration if zero.ServerNow().IsZero() { t.Fatalf("未校准时应退化为本机时间") } }