github.com/sacloud/iaas-api-go@v1.12.0/state_test.go (about)

     1  // Copyright 2022-2023 The sacloud/iaas-api-go 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 iaas
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"net/http"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/sacloud/iaas-api-go/types"
    26  	"github.com/stretchr/testify/require"
    27  )
    28  
    29  type dummyState struct {
    30  	state interface{}
    31  	err   error
    32  }
    33  
    34  func testStateCheckFunc(target interface{}) (bool, error) {
    35  	state, ok := target.(*dummyState)
    36  	if !ok {
    37  		return false, fmt.Errorf("got invalid state type: %+v", target)
    38  	}
    39  	return state.state != nil, state.err
    40  }
    41  
    42  func TestStatePollingWaiter_withStateCheckFunc(t *testing.T) {
    43  	t.Run("timeout", func(t *testing.T) {
    44  		waiter := &StatePollingWaiter{
    45  			ReadFunc: func() (interface{}, error) {
    46  				return &dummyState{}, nil
    47  			},
    48  			StateCheckFunc: testStateCheckFunc,
    49  			Timeout:        5 * time.Millisecond,
    50  			Interval:       1 * time.Millisecond,
    51  		}
    52  		ctx := context.Background()
    53  		_, err := waiter.WaitForState(ctx)
    54  		require.Error(t, err)
    55  		require.EqualError(t, err, "context deadline exceeded")
    56  	})
    57  
    58  	t.Run("parent context was canceled", func(t *testing.T) {
    59  		waiter := &StatePollingWaiter{
    60  			ReadFunc: func() (interface{}, error) {
    61  				return &dummyState{}, nil
    62  			},
    63  			StateCheckFunc: testStateCheckFunc,
    64  			Timeout:        100 * time.Millisecond,
    65  			Interval:       1 * time.Millisecond,
    66  		}
    67  		ctx, cancel := context.WithCancel(context.Background())
    68  		defer cancel()
    69  
    70  		_, err := waiter.WaitForState(ctx)
    71  		go func() {
    72  			time.Sleep(5 * time.Millisecond)
    73  			cancel()
    74  		}()
    75  
    76  		require.Error(t, err)
    77  		require.EqualError(t, err, "context deadline exceeded")
    78  	})
    79  
    80  	t.Run("ReadFunc got 404", func(t *testing.T) {
    81  		retry := 5
    82  		read := 0
    83  		waiter := &StatePollingWaiter{
    84  			NotFoundRetry: 10,
    85  			ReadFunc: func() (interface{}, error) {
    86  				read++
    87  				if read < retry {
    88  					return nil, &apiError{responseCode: http.StatusNotFound}
    89  				}
    90  				return &dummyState{state: "done"}, nil
    91  			},
    92  			StateCheckFunc: testStateCheckFunc,
    93  			Timeout:        100 * time.Millisecond,
    94  			Interval:       1 * time.Millisecond,
    95  		}
    96  		ctx := context.Background()
    97  		_, err := waiter.WaitForState(ctx)
    98  
    99  		require.NoError(t, err)
   100  		require.Equal(t, retry, read)
   101  	})
   102  
   103  	t.Run("404 errors exceeded maximum", func(t *testing.T) {
   104  		retry := 5
   105  		read := 0
   106  		waiter := &StatePollingWaiter{
   107  			NotFoundRetry: 2,
   108  			ReadFunc: func() (interface{}, error) {
   109  				read++
   110  				if read < retry {
   111  					return nil, &apiError{responseCode: http.StatusNotFound}
   112  				}
   113  				return &dummyState{state: "done"}, nil
   114  			},
   115  			StateCheckFunc: testStateCheckFunc,
   116  			Timeout:        100 * time.Millisecond,
   117  			Interval:       1 * time.Millisecond,
   118  		}
   119  		ctx := context.Background()
   120  		_, err := waiter.WaitForState(ctx)
   121  
   122  		require.Error(t, err)
   123  		require.True(t, IsNotFoundError(err))
   124  		require.Equal(t, waiter.NotFoundRetry+1, read)
   125  	})
   126  
   127  	t.Run("ReadFunc got unexpected error", func(t *testing.T) {
   128  		waiter := &StatePollingWaiter{
   129  			ReadFunc: func() (interface{}, error) {
   130  				return &dummyState{}, errors.New("dummy")
   131  			},
   132  			StateCheckFunc: testStateCheckFunc,
   133  			Timeout:        100 * time.Millisecond,
   134  			Interval:       1 * time.Millisecond,
   135  		}
   136  		ctx := context.Background()
   137  		_, err := waiter.WaitForState(ctx)
   138  
   139  		require.Error(t, err)
   140  		require.EqualError(t, err, "dummy")
   141  	})
   142  }
   143  
   144  type dummyInstanceStatus struct {
   145  	status       string
   146  	availability string
   147  }
   148  
   149  func (d *dummyInstanceStatus) GetInstanceStatus() types.EServerInstanceStatus {
   150  	return types.EServerInstanceStatus(d.status)
   151  }
   152  
   153  func (d *dummyInstanceStatus) SetInstanceStatus(s types.EServerInstanceStatus) {
   154  	d.status = string(s)
   155  }
   156  func (d *dummyInstanceStatus) GetAvailability() types.EAvailability {
   157  	return types.EAvailability(d.availability)
   158  }
   159  func (d *dummyInstanceStatus) SetAvailability(a types.EAvailability) {
   160  	d.availability = string(a)
   161  }
   162  
   163  func TestStatePollingWaiter_withTargetStates(t *testing.T) {
   164  	t.Run("ReadFunc got unknown state with RaiseErrorWithUnknownState=false", func(t *testing.T) {
   165  		counter := 0
   166  		max := 3
   167  		waiter := &StatePollingWaiter{
   168  			ReadFunc: func() (interface{}, error) {
   169  				counter++
   170  				status := ""
   171  				if counter > max {
   172  					status = string(types.ServerInstanceStatuses.Up)
   173  				}
   174  				return &dummyInstanceStatus{status: status}, nil
   175  			},
   176  			TargetInstanceStatus: []types.EServerInstanceStatus{types.ServerInstanceStatuses.Up},
   177  			Timeout:              100 * time.Millisecond,
   178  			Interval:             1 * time.Millisecond,
   179  		}
   180  		ctx := context.Background()
   181  		_, err := waiter.WaitForState(ctx)
   182  
   183  		require.NoError(t, err)
   184  	})
   185  	t.Run("ReadFunc got unknown state with RaiseErrorWithUnknownState=true", func(t *testing.T) {
   186  		waiter := &StatePollingWaiter{
   187  			ReadFunc: func() (interface{}, error) {
   188  				return &dummyInstanceStatus{status: "unknown-instance-status"}, nil
   189  			},
   190  			TargetInstanceStatus:       []types.EServerInstanceStatus{types.ServerInstanceStatuses.Up},
   191  			Timeout:                    100 * time.Millisecond,
   192  			Interval:                   1 * time.Millisecond,
   193  			RaiseErrorWithUnknownState: true,
   194  		}
   195  		ctx := context.Background()
   196  		_, err := waiter.WaitForState(ctx)
   197  
   198  		require.Error(t, err)
   199  		require.EqualError(t, err, `got unexpected value of InstanceState: got "unknown-instance-status"`)
   200  	})
   201  
   202  	t.Run("ReadFunc got unknown availability with RaiseErrorWithUnknownState=false", func(t *testing.T) {
   203  		counter := 0
   204  		max := 3
   205  		waiter := &StatePollingWaiter{
   206  			ReadFunc: func() (interface{}, error) {
   207  				counter++
   208  				availability := ""
   209  				if counter > max {
   210  					availability = string(types.Availabilities.Available)
   211  				}
   212  				return &dummyInstanceStatus{availability: availability}, nil
   213  			},
   214  			TargetAvailability: []types.EAvailability{types.Availabilities.Available},
   215  			Timeout:            100 * time.Millisecond,
   216  			Interval:           1 * time.Millisecond,
   217  		}
   218  		ctx := context.Background()
   219  		_, err := waiter.WaitForState(ctx)
   220  
   221  		require.NoError(t, err)
   222  	})
   223  
   224  	t.Run("ReadFunc got unknown availability with RaiseErrorWithUnknownState=true", func(t *testing.T) {
   225  		waiter := &StatePollingWaiter{
   226  			ReadFunc: func() (interface{}, error) {
   227  				return &dummyInstanceStatus{availability: "unknown-availability"}, nil
   228  			},
   229  			TargetAvailability:         []types.EAvailability{types.Availabilities.Available},
   230  			Timeout:                    100 * time.Millisecond,
   231  			Interval:                   1 * time.Millisecond,
   232  			RaiseErrorWithUnknownState: true,
   233  		}
   234  		ctx := context.Background()
   235  		_, err := waiter.WaitForState(ctx)
   236  
   237  		require.Error(t, err)
   238  		require.EqualError(t, err, `got unexpected value of Availability: got "unknown-availability"`)
   239  	})
   240  }