github.com/status-im/status-go@v1.1.0/timesource/timesource_test.go (about) 1 package timesource 2 3 import ( 4 "errors" 5 "sync" 6 "testing" 7 "time" 8 9 "github.com/beevik/ntp" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 ) 13 14 const ( 15 // clockCompareDelta declares time required between multiple calls to time.Now 16 clockCompareDelta = 100 * time.Microsecond 17 ) 18 19 // we don't user real servers for tests, but logic depends on 20 // actual number of involved NTP servers. 21 var mockedServers = []string{"ntp1", "ntp2", "ntp3", "ntp4"} 22 23 type testCase struct { 24 description string 25 servers []string 26 allowedFailures int 27 responses []queryResponse 28 expected time.Duration 29 expectError bool 30 31 // actual attempts are mutable 32 mu sync.Mutex 33 actualAttempts int 34 } 35 36 func (tc *testCase) query(string, ntp.QueryOptions) (*ntp.Response, error) { 37 tc.mu.Lock() 38 defer func() { 39 tc.actualAttempts++ 40 tc.mu.Unlock() 41 }() 42 response := &ntp.Response{ 43 ClockOffset: tc.responses[tc.actualAttempts].Offset, 44 Stratum: 1, 45 } 46 return response, tc.responses[tc.actualAttempts].Error 47 } 48 49 func newTestCases() []*testCase { 50 return []*testCase{ 51 { 52 description: "SameResponse", 53 servers: mockedServers, 54 responses: []queryResponse{ 55 {Offset: 10 * time.Second}, 56 {Offset: 10 * time.Second}, 57 {Offset: 10 * time.Second}, 58 {Offset: 10 * time.Second}, 59 }, 60 expected: 10 * time.Second, 61 }, 62 { 63 description: "Median", 64 servers: mockedServers, 65 responses: []queryResponse{ 66 {Offset: 10 * time.Second}, 67 {Offset: 20 * time.Second}, 68 {Offset: 20 * time.Second}, 69 {Offset: 30 * time.Second}, 70 }, 71 expected: 20 * time.Second, 72 }, 73 { 74 description: "EvenMedian", 75 servers: mockedServers[:2], 76 responses: []queryResponse{ 77 {Offset: 10 * time.Second}, 78 {Offset: 20 * time.Second}, 79 }, 80 expected: 15 * time.Second, 81 }, 82 { 83 description: "Error", 84 servers: mockedServers, 85 responses: []queryResponse{ 86 {Offset: 10 * time.Second}, 87 {Error: errors.New("test")}, 88 {Offset: 30 * time.Second}, 89 {Offset: 30 * time.Second}, 90 }, 91 expected: time.Duration(0), 92 expectError: true, 93 }, 94 { 95 description: "MultiError", 96 servers: mockedServers, 97 responses: []queryResponse{ 98 {Error: errors.New("test 1")}, 99 {Error: errors.New("test 2")}, 100 {Error: errors.New("test 3")}, 101 {Error: errors.New("test 3")}, 102 }, 103 expected: time.Duration(0), 104 expectError: true, 105 }, 106 { 107 description: "TolerableError", 108 servers: mockedServers, 109 allowedFailures: 1, 110 responses: []queryResponse{ 111 {Offset: 10 * time.Second}, 112 {Error: errors.New("test")}, 113 {Offset: 20 * time.Second}, 114 {Offset: 30 * time.Second}, 115 }, 116 expected: 20 * time.Second, 117 }, 118 { 119 description: "NonTolerableError", 120 servers: mockedServers, 121 allowedFailures: 1, 122 responses: []queryResponse{ 123 {Offset: 10 * time.Second}, 124 {Error: errors.New("test")}, 125 {Error: errors.New("test")}, 126 {Error: errors.New("test")}, 127 }, 128 expected: time.Duration(0), 129 expectError: true, 130 }, 131 { 132 description: "AllFailed", 133 servers: mockedServers, 134 allowedFailures: 4, 135 responses: []queryResponse{ 136 {Error: errors.New("test")}, 137 {Error: errors.New("test")}, 138 {Error: errors.New("test")}, 139 {Error: errors.New("test")}, 140 }, 141 expected: time.Duration(0), 142 expectError: true, 143 }, 144 { 145 description: "HalfTolerable", 146 servers: mockedServers, 147 allowedFailures: 2, 148 responses: []queryResponse{ 149 {Offset: 10 * time.Second}, 150 {Offset: 20 * time.Second}, 151 {Error: errors.New("test")}, 152 {Error: errors.New("test")}, 153 }, 154 expected: 15 * time.Second, 155 }, 156 } 157 } 158 159 func TestComputeOffset(t *testing.T) { 160 for _, tc := range newTestCases() { 161 t.Run(tc.description, func(t *testing.T) { 162 offset, err := computeOffset(tc.query, tc.servers, tc.allowedFailures) 163 if tc.expectError { 164 assert.Error(t, err) 165 } else { 166 assert.NoError(t, err) 167 } 168 assert.Equal(t, tc.expected, offset) 169 }) 170 } 171 } 172 173 func TestNTPTimeSource(t *testing.T) { 174 for _, tc := range newTestCases() { 175 t.Run(tc.description, func(t *testing.T) { 176 source := &NTPTimeSource{ 177 servers: tc.servers, 178 allowedFailures: tc.allowedFailures, 179 timeQuery: tc.query, 180 now: time.Now, 181 } 182 assert.WithinDuration(t, time.Now(), source.Now(), clockCompareDelta) 183 err := source.updateOffset() 184 if tc.expectError { 185 assert.Equal(t, errUpdateOffset, err) 186 } else { 187 assert.NoError(t, err) 188 } 189 assert.WithinDuration(t, time.Now().Add(tc.expected), source.Now(), clockCompareDelta) 190 }) 191 } 192 } 193 194 func TestRunningPeriodically(t *testing.T) { 195 var hits int 196 var mu sync.RWMutex 197 periods := make([]time.Duration, 0) 198 199 tc := newTestCases()[0] 200 fastHits := 3 201 slowHits := 1 202 203 t.Run(tc.description, func(t *testing.T) { 204 source := &NTPTimeSource{ 205 servers: tc.servers, 206 allowedFailures: tc.allowedFailures, 207 timeQuery: tc.query, 208 fastNTPSyncPeriod: time.Duration(fastHits*10) * time.Millisecond, 209 slowNTPSyncPeriod: time.Duration(slowHits*10) * time.Millisecond, 210 now: time.Now, 211 } 212 lastCall := time.Now() 213 // we're simulating a calls to updateOffset, testing ntp calls happens 214 // on NTPTimeSource specified periods (fastNTPSyncPeriod & slowNTPSyncPeriod) 215 wg := sync.WaitGroup{} 216 wg.Add(1) 217 source.runPeriodically(func() error { 218 mu.Lock() 219 periods = append(periods, time.Since(lastCall)) 220 mu.Unlock() 221 hits++ 222 if hits < 3 { 223 return errUpdateOffset 224 } 225 if hits == 6 { 226 wg.Done() 227 } 228 return nil 229 }, false) 230 231 wg.Wait() 232 233 mu.Lock() 234 require.Len(t, periods, 6) 235 defer mu.Unlock() 236 prev := 0 237 for _, period := range periods[1:3] { 238 p := int(period.Seconds() * 100) 239 require.True(t, fastHits <= (p-prev)) 240 prev = p 241 } 242 243 for _, period := range periods[3:] { 244 p := int(period.Seconds() * 100) 245 require.True(t, slowHits <= (p-prev)) 246 prev = p 247 } 248 }) 249 } 250 251 func TestGetCurrentTimeInMillis(t *testing.T) { 252 invokeTimes := 3 253 numResponses := len(mockedServers) * invokeTimes 254 responseOffset := 10 * time.Second 255 tc := &testCase{ 256 servers: mockedServers, 257 responses: make([]queryResponse, numResponses), 258 expected: responseOffset, 259 } 260 for i := range tc.responses { 261 tc.responses[i] = queryResponse{Offset: responseOffset} 262 } 263 264 ts := NTPTimeSource{ 265 servers: tc.servers, 266 allowedFailures: tc.allowedFailures, 267 timeQuery: tc.query, 268 slowNTPSyncPeriod: SlowNTPSyncPeriod, 269 now: func() time.Time { 270 return time.Unix(1, 0) 271 }, 272 } 273 274 expectedTime := uint64(11000) 275 n := ts.GetCurrentTimeInMillis() 276 require.Equal(t, expectedTime, n) 277 // test repeat invoke GetCurrentTimeInMillis 278 n = ts.GetCurrentTimeInMillis() 279 require.Equal(t, expectedTime, n) 280 e := ts.Stop() 281 require.NoError(t, e) 282 283 // test invoke after stop 284 n = ts.GetCurrentTimeInMillis() 285 require.Equal(t, expectedTime, n) 286 e = ts.Stop() 287 require.NoError(t, e) 288 } 289 290 func TestGetCurrentTimeOffline(t *testing.T) { 291 // covers https://github.com/status-im/status-desktop/issues/12691 292 ts := &NTPTimeSource{ 293 servers: defaultServers, 294 allowedFailures: DefaultMaxAllowedFailures, 295 fastNTPSyncPeriod: 1 * time.Millisecond, 296 slowNTPSyncPeriod: 1 * time.Second, 297 timeQuery: func(string, ntp.QueryOptions) (*ntp.Response, error) { 298 return nil, errors.New("offline") 299 }, 300 now: time.Now, 301 } 302 303 // ensure there is no "panic: sync: negative WaitGroup counter" 304 // when GetCurrentTime() is invoked more than once when offline 305 _ = ts.GetCurrentTime() 306 _ = ts.GetCurrentTime() 307 }