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  }