github.com/sacloud/libsacloud/v2@v2.32.3/helper/setup/retryable_setup_test.go (about)

     1  // Copyright 2016-2022 The Libsacloud Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package setup
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"testing"
    21  	"time"
    22  
    23  	"github.com/sacloud/libsacloud/v2/sacloud/accessor"
    24  	"github.com/sacloud/libsacloud/v2/sacloud/types"
    25  	"github.com/stretchr/testify/require"
    26  )
    27  
    28  type dummyIDAccessor struct {
    29  	id types.ID
    30  }
    31  
    32  func (d *dummyIDAccessor) GetID() types.ID {
    33  	return d.id
    34  }
    35  
    36  func (d *dummyIDAccessor) SetID(id types.ID) {
    37  	d.id = id
    38  }
    39  
    40  type dummyAvailabilityAccessor struct {
    41  	available types.EAvailability
    42  }
    43  
    44  func (d *dummyAvailabilityAccessor) GetAvailability() types.EAvailability {
    45  	return d.available
    46  }
    47  
    48  func (d *dummyAvailabilityAccessor) SetAvailability(a types.EAvailability) {
    49  	d.available = a
    50  }
    51  
    52  func TestRetryableSetup_Setup(t *testing.T) {
    53  	ctx := context.Background()
    54  	zone := "tk1v"
    55  
    56  	t.Run("create", func(t *testing.T) {
    57  		t.Run("no error", func(t *testing.T) {
    58  			retryable := &RetryableSetup{
    59  				Create: func(context.Context, string) (id accessor.ID, e error) {
    60  					return &dummyIDAccessor{id: 1}, nil
    61  				},
    62  				Read: func(ctx context.Context, zone string, id types.ID) (interface{}, error) {
    63  					return &dummyIDAccessor{id: 1}, nil
    64  				},
    65  				ProvisioningRetryInterval: time.Millisecond,
    66  				DeleteRetryInterval:       time.Millisecond,
    67  				PollingInterval:           time.Millisecond,
    68  			}
    69  			res, err := retryable.Setup(ctx, zone)
    70  
    71  			require.NotNil(t, res)
    72  			require.NoError(t, err)
    73  			_, ok := res.(accessor.ID)
    74  			require.True(t, ok)
    75  		})
    76  
    77  		t.Run("error", func(t *testing.T) {
    78  			retryable := &RetryableSetup{
    79  				Create: func(context.Context, string) (accessor.ID, error) {
    80  					return nil, fmt.Errorf("error")
    81  				},
    82  				Read: func(ctx context.Context, zone string, id types.ID) (interface{}, error) {
    83  					return &dummyIDAccessor{id: 1}, nil
    84  				},
    85  				ProvisioningRetryInterval: time.Millisecond,
    86  				DeleteRetryInterval:       time.Millisecond,
    87  				PollingInterval:           time.Millisecond,
    88  			}
    89  			res, err := retryable.Setup(ctx, zone)
    90  
    91  			require.Nil(t, res)
    92  			require.Error(t, err)
    93  		})
    94  	})
    95  	//
    96  	t.Run("Retry", func(t *testing.T) {
    97  		t.Run("retry under max count", func(t *testing.T) {
    98  			// リトライ3回、ReadFuncは3回呼ばれるまではFailedが返る(4回目以降はAvailable)
    99  			retryable := &RetryableSetup{
   100  				Create: func(context.Context, string) (id accessor.ID, e error) {
   101  					return &dummyIDAccessor{id: 1}, nil
   102  				},
   103  				IsWaitForCopy: true,
   104  				Delete: func(context.Context, string, types.ID) error {
   105  					return nil
   106  				},
   107  				RetryCount: 3,
   108  				Read: withErrorReadFunc(func(ctx context.Context, zone string, id types.ID) (interface{}, error) {
   109  					return &dummyIDAccessor{id: 1}, nil
   110  				}, 3),
   111  				ProvisioningRetryInterval: time.Millisecond,
   112  				DeleteRetryInterval:       time.Millisecond,
   113  				PollingInterval:           time.Millisecond,
   114  			}
   115  
   116  			res, err := retryable.Setup(ctx, zone)
   117  
   118  			require.NotNil(t, res)
   119  			require.NoError(t, err)
   120  		})
   121  
   122  		t.Run("max retry count exceeded", func(t *testing.T) {
   123  			retryable := &RetryableSetup{
   124  				Create: func(context.Context, string) (id accessor.ID, e error) {
   125  					return &dummyIDAccessor{id: 1}, nil
   126  				},
   127  				IsWaitForCopy: true,
   128  				Delete: func(context.Context, string, types.ID) error {
   129  					return nil
   130  				},
   131  				RetryCount: 3,
   132  				Read: withErrorReadFunc(func(ctx context.Context, zone string, id types.ID) (interface{}, error) {
   133  					return &dummyIDAccessor{id: 1}, nil
   134  				}, 5),
   135  				ProvisioningRetryInterval: time.Millisecond,
   136  				DeleteRetryInterval:       time.Millisecond,
   137  				PollingInterval:           time.Millisecond,
   138  			}
   139  
   140  			res, err := retryable.Setup(ctx, zone)
   141  
   142  			require.Nil(t, res)
   143  			require.Error(t, err)
   144  			_, ok := err.(MaxRetryCountExceededError)
   145  			require.True(t, ok)
   146  		})
   147  	})
   148  }
   149  
   150  func withErrorReadFunc(readFunc ReadFunc, errCount int) ReadFunc {
   151  	maxErr := errCount
   152  	return func(ctx context.Context, zone string, id types.ID) (interface{}, error) {
   153  		maxErr--
   154  		if maxErr <= 0 {
   155  			return &dummyAvailabilityAccessor{available: types.Availabilities.Available}, nil
   156  		}
   157  		return &dummyAvailabilityAccessor{available: types.Availabilities.Failed}, nil
   158  	}
   159  }