...

Source file src/edge-infra.dev/pkg/sds/etcd/manager/cluster/cluster_test.go

Documentation: edge-infra.dev/pkg/sds/etcd/manager/cluster

     1  package cluster
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"os"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/go-logr/logr/testr"
    11  	"github.com/golang/mock/gomock"
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  	clientv3 "go.etcd.io/etcd/client/v3"
    15  	ctrl "sigs.k8s.io/controller-runtime"
    16  
    17  	"edge-infra.dev/pkg/sds/lib/etcd/client/retry"
    18  	"edge-infra.dev/pkg/sds/lib/etcd/client/retry/mocks"
    19  )
    20  
    21  func TestMain(m *testing.M) {
    22  	os.Exit(m.Run())
    23  }
    24  
    25  func setupTestCtx(t *testing.T) context.Context {
    26  	logOptions := testr.Options{
    27  		LogTimestamp: true,
    28  		Verbosity:    -1,
    29  	}
    30  
    31  	return ctrl.LoggerInto(context.Background(), testr.NewWithOptions(t, logOptions))
    32  }
    33  
    34  func TestUpdateStatus(t *testing.T) {
    35  	mockCtrl := gomock.NewController(t)
    36  	defer mockCtrl.Finish()
    37  
    38  	tests := map[string]struct {
    39  		client               retry.Retrier
    40  		initialLastHealthy   time.Time
    41  		initialLastUnhealthy time.Time
    42  		wantHealthy          bool
    43  	}{
    44  		"Healthy": {
    45  			client: getSafeStatusClient(mockCtrl, func(context.Context, string) (*clientv3.StatusResponse, error) {
    46  				return &clientv3.StatusResponse{}, nil
    47  			}),
    48  			initialLastHealthy:   nowOffset(-1),
    49  			initialLastUnhealthy: nowOffset(0),
    50  			wantHealthy:          true,
    51  		},
    52  		"Unhealthy_NoResponse": {
    53  			client: getSafeStatusClient(mockCtrl, func(context.Context, string) (*clientv3.StatusResponse, error) {
    54  				return nil, errors.New("No response")
    55  			}),
    56  			initialLastHealthy:   nowOffset(0),
    57  			initialLastUnhealthy: nowOffset(-1),
    58  			wantHealthy:          false,
    59  		},
    60  		"Unhealthy_ErrorResponse": {
    61  			client: getSafeStatusClient(mockCtrl, func(context.Context, string) (*clientv3.StatusResponse, error) {
    62  				return &clientv3.StatusResponse{
    63  					Errors: []string{"Not healthy"},
    64  				}, nil
    65  			}),
    66  			initialLastHealthy:   nowOffset(0),
    67  			initialLastUnhealthy: nowOffset(-1),
    68  			wantHealthy:          false,
    69  		},
    70  	}
    71  
    72  	for name, tc := range tests {
    73  		t.Run(name, func(t *testing.T) {
    74  			status := Status{
    75  				lastHealthy:   tc.initialLastHealthy,
    76  				lastUnhealthy: tc.initialLastUnhealthy,
    77  			}
    78  			cluster := New("test-endpoint", 10*time.Minute, status)
    79  
    80  			require.Equal(t, !tc.wantHealthy, cluster.IsHealthy())
    81  			cluster.UpdateStatus(setupTestCtx(t), tc.client)
    82  			assert.Equal(t, tc.wantHealthy, cluster.IsHealthy())
    83  		})
    84  	}
    85  }
    86  
    87  func TestIsHealthy(t *testing.T) {
    88  	healthyStatus := Status{
    89  		lastHealthy:   time.Now(),
    90  		lastUnhealthy: nowOffset(-1),
    91  	}
    92  	healthyCluster := New("", 10*time.Minute, healthyStatus)
    93  
    94  	unhealthyStatus := Status{
    95  		lastHealthy:   nowOffset(-1),
    96  		lastUnhealthy: time.Now(),
    97  	}
    98  	unhealthyCluster := New("", 10*time.Minute, unhealthyStatus)
    99  
   100  	tests := map[string]struct {
   101  		cluster Cluster
   102  		want    bool
   103  	}{
   104  		"Healthy": {
   105  			cluster: healthyCluster,
   106  			want:    true,
   107  		},
   108  		"Unhealthy": {
   109  			cluster: unhealthyCluster,
   110  			want:    false,
   111  		},
   112  	}
   113  
   114  	for name, tc := range tests {
   115  		t.Run(name, func(t *testing.T) {
   116  			healthy := tc.cluster.IsHealthy()
   117  			assert.Equal(t, tc.want, healthy)
   118  		})
   119  	}
   120  }
   121  
   122  func TestIsRecoveryRequired(t *testing.T) {
   123  	healthyStatus := Status{
   124  		lastHealthy:   time.Now(),
   125  		lastUnhealthy: nowOffset(-1),
   126  	}
   127  	healthyCluster := New("", 10*time.Minute, healthyStatus)
   128  
   129  	unhealthyStatus := Status{
   130  		lastHealthy:   nowOffset(-1), // offset of -1 is unhealthy but shouldn't trigger recovery
   131  		lastUnhealthy: time.Now(),
   132  	}
   133  	unhealthyCluster := New("", 10*time.Minute, unhealthyStatus)
   134  
   135  	unhealthyForDurationStatus := Status{
   136  		lastHealthy:   nowOffset(-10), // offset of -10 is unhealthy and should trigger recovery
   137  		lastUnhealthy: time.Now(),
   138  	}
   139  	unhealthyForDurationCluster := New("", 10*time.Minute, unhealthyForDurationStatus)
   140  
   141  	tests := map[string]struct {
   142  		cluster Cluster
   143  		want    bool
   144  	}{
   145  		"ResetNotRequired_Healthy": {
   146  			cluster: healthyCluster,
   147  			want:    false,
   148  		},
   149  		"ResetNotRequired_Unhealthy": {
   150  			cluster: unhealthyCluster,
   151  			want:    false,
   152  		},
   153  		"ResetRequired_UnhealthyForDuration": {
   154  			cluster: unhealthyForDurationCluster,
   155  			want:    true,
   156  		},
   157  	}
   158  
   159  	for name, tc := range tests {
   160  		t.Run(name, func(t *testing.T) {
   161  			assert.Equal(t, tc.want, tc.cluster.IsResetRequired())
   162  		})
   163  	}
   164  }
   165  
   166  func TestResetTimer(t *testing.T) {
   167  	status := Status{
   168  		lastHealthy:   nowOffset(-10),
   169  		lastUnhealthy: nowOffset(-5),
   170  	}
   171  	cluster := New("", 10*time.Minute, status)
   172  
   173  	assert.NotEqual(t, cluster.lastHealthy, cluster.lastUnhealthy)
   174  
   175  	cluster.ResetTimer()
   176  
   177  	assert.WithinRange(t, cluster.lastHealthy, nowOffset(-1), time.Now())
   178  	assert.WithinRange(t, cluster.lastUnhealthy, nowOffset(-1), time.Now())
   179  	assert.Equal(t, cluster.lastHealthy, cluster.lastUnhealthy)
   180  }
   181  
   182  func getSafeStatusClient(mockCtrl *gomock.Controller, retFn func(context.Context, string) (*clientv3.StatusResponse, error)) retry.Retrier {
   183  	mockRetrier := mocks.NewMockRetrier(mockCtrl)
   184  	mockRetrier.EXPECT().SafeStatus(gomock.Any(), gomock.Any()).DoAndReturn(retFn)
   185  
   186  	return mockRetrier
   187  }
   188  
   189  func nowOffset(mins time.Duration) time.Time {
   190  	return time.Now().Add(mins * time.Minute)
   191  }
   192  

View as plain text