Files
-rpi-sendspin/third_party/sendspin-go/pkg/sync/clock_test.go

199 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ABOUTME: Tests for Kalman-filter-based clock synchronization
// ABOUTME: Tests RTT calculation, time conversion, quality tracking
package sync
import (
"testing"
"time"
)
func TestRTTCalculation(t *testing.T) {
t1 := int64(1000000)
t2 := int64(2000)
t3 := int64(2500)
t4 := int64(1005000)
cs := NewClockSync()
cs.ProcessSyncResponse(t1, t2, t3, t4)
// RTT = (t4-t1) - (t3-t2) = 5000 - 500 = 4500µs
rtt, _ := cs.GetStats()
if rtt != 4500 {
t.Errorf("expected RTT 4500µs, got %dµs", rtt)
}
}
func TestSyncEstablishment(t *testing.T) {
cs := NewClockSync()
if cs.filter.Synced() {
t.Error("expected not synced initially")
}
// One low-noise sample is enough to mark the filter Synced.
cs.ProcessSyncResponse(1_000_000, 500_000, 500_100, 1_000_200)
if !cs.filter.Synced() {
t.Error("expected synced after first response")
}
// Drive enough low-noise samples to converge to QualityGood.
for i := 1; i < 60; i++ {
t1 := int64(1_000_000 + i*100_000)
cs.ProcessSyncResponse(t1, t1+50, t1+150, t1+200) // ~200µs RTT
}
_, quality := cs.GetStats()
if quality != QualityGood {
t.Errorf("expected QualityGood after convergence, got %v", quality)
}
}
func TestServerToLocalTimeConversion(t *testing.T) {
cs := NewClockSync()
clientNow := time.Now().UnixMicro()
serverTime := int64(5000000) // 5s into server loop
// Feed several samples to let the filter converge
for i := 0; i < 10; i++ {
ct := clientNow + int64(i*100000) // 100ms apart
st := serverTime + int64(i*100000)
cs.ProcessSyncResponse(ct-1000, st, st+50, ct)
}
// Convert a server time 100ms in the future
futureServer := serverTime + 10*100000 + 100000
localTime := cs.ServerToLocalTime(futureServer)
expectedLocal := time.UnixMicro(clientNow + 10*100000 + 100000)
diff := localTime.Sub(expectedLocal).Microseconds()
if diff < -50000 || diff > 50000 {
t.Errorf("time conversion off by %dµs", diff)
}
}
func TestQualityTracking(t *testing.T) {
cs := NewClockSync()
// Single noisy sample → high σ → not yet QualityGood.
cs.ProcessSyncResponse(1000000, 1000, 1100, 1025000)
_, quality := cs.GetStats()
if quality == QualityGood {
t.Errorf("expected non-Good quality on first sample, got %v", quality)
}
// Drive enough low-noise samples to converge below the QualityGood threshold.
for i := 1; i < 60; i++ {
t1 := int64(1_000_000 + i*100_000)
cs.ProcessSyncResponse(t1, t1+50, t1+150, t1+200) // ~200µs RTT
}
_, quality = cs.GetStats()
if quality != QualityGood {
t.Errorf("expected QualityGood after convergence, got %v (filter err=%d)",
quality, cs.filter.GetError())
}
}
func TestQualityDegradation(t *testing.T) {
cs := NewClockSync()
// Drive enough low-noise samples to reach QualityGood.
for i := 0; i < 60; i++ {
t1 := int64(1_000_000 + i*100_000)
cs.ProcessSyncResponse(t1, t1+50, t1+150, t1+200) // ~200µs RTT
}
quality := cs.CheckQuality()
if quality != QualityGood {
t.Errorf("expected QualityGood initially, got %v", quality)
}
cs.mu.Lock()
cs.lastSync = time.Now().Add(-6 * time.Second)
cs.mu.Unlock()
quality = cs.CheckQuality()
if quality != QualityLost {
t.Errorf("expected QualityLost after 6s, got %v", quality)
}
}
func TestClockSync_ServerMicrosNow(t *testing.T) {
cs := NewClockSync()
// Before sync, should return roughly current Unix micros
now1 := cs.ServerMicrosNow()
unixNow := time.Now().UnixMicro()
if abs64(now1-unixNow) > 1000000 {
t.Errorf("before sync: expected ~%d, got %d", unixNow, now1)
}
// After sync, should return server-frame time
cs.ProcessSyncResponse(1000, 500000, 500100, 1200)
now2 := cs.ServerMicrosNow()
if now2 == 0 {
t.Error("after sync: got zero")
}
}
func TestNewClockSyncWithConfig(t *testing.T) {
cfg := DefaultTimeFilterConfig()
cfg.MaxErrorScale = 0.25
csDefault := NewClockSync()
csScaled := NewClockSyncWithConfig(cfg)
const samples = 30
for i := 0; i < samples; i++ {
t1 := int64(1_000_000 + i*100_000)
t2 := int64(500_000 + i*100_000)
t3 := t2 + 100
t4 := t1 + 1000 // ~1ms RTT
csDefault.ProcessSyncResponse(t1, t2, t3, t4)
csScaled.ProcessSyncResponse(t1, t2, t3, t4)
}
errDefault := csDefault.filter.GetError()
errScaled := csScaled.filter.GetError()
if !(errScaled < errDefault) {
t.Errorf("expected scaled (0.25) error < default (0.5); got scaled=%d default=%d",
errScaled, errDefault)
}
}
func TestConcurrentAccess(t *testing.T) {
cs := NewClockSync()
cs.ProcessSyncResponse(1000000, 1000, 1100, 1025000)
done := make(chan bool, 10)
for i := 0; i < 10; i++ {
go func() {
for j := 0; j < 100; j++ {
cs.GetStats()
cs.CheckQuality()
cs.ServerMicrosNow()
cs.ServerToLocalTime(int64(j * 1000))
cs.ProcessSyncResponse(
int64(1000000+j), int64(1000+j),
int64(1100+j), int64(1025000+j),
)
}
done <- true
}()
}
for i := 0; i < 10; i++ {
<-done
}
rtt, quality := cs.GetStats()
if rtt <= 0 {
t.Error("invalid RTT after concurrent access")
}
if quality == QualityLost {
t.Error("unexpected QualityLost after concurrent access")
}
}