github.com/sacloud/iaas-api-go@v1.12.0/testutil/crud.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 testutil
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"log"
    21  	"net/http"
    22  	"runtime/debug"
    23  	"sync"
    24  	"time"
    25  
    26  	"github.com/sacloud/iaas-api-go"
    27  	"github.com/sacloud/iaas-api-go/accessor"
    28  	"github.com/sacloud/iaas-api-go/types"
    29  )
    30  
    31  // CRUDTestCase CRUD操作テストケース
    32  type CRUDTestCase struct {
    33  	// PreCheck テスト実行 or スキップを判定するためのFunc
    34  	PreCheck func(TestT)
    35  
    36  	// APICallerのセットアップ用Func、テストケースごとに1回呼ばれる
    37  	SetupAPICallerFunc func() iaas.APICaller
    38  
    39  	// Setup テスト前の準備(依存リソースの作成など)を行うためのFunc(省略可)
    40  	Setup func(*CRUDTestContext, iaas.APICaller) error
    41  
    42  	// Create Create操作のテスト用Func(省略可)
    43  	Create *CRUDTestFunc
    44  
    45  	// Read Read操作のテスト用Func(必須)
    46  	Read *CRUDTestFunc
    47  
    48  	// Updates Update操作のテスト用Func(省略可)
    49  	Updates []*CRUDTestFunc
    50  
    51  	// Shutdown Delete操作の前のシャットダウン(省略可)
    52  	Shutdown func(*CRUDTestContext, iaas.APICaller) error
    53  
    54  	// Delete Delete操作のテスト用Func(省略可)
    55  	Delete *CRUDTestDeleteFunc
    56  
    57  	// Cleanup APIで作成/変更したリソースなどのクリーンアップ用Func(省略化)
    58  	Cleanup func(*CRUDTestContext, iaas.APICaller) error
    59  
    60  	// Parallel t.Parallelを呼ぶかのフラグ
    61  	Parallel bool
    62  
    63  	// IgnoreStartupWait リソース作成後の起動待ちを行わない
    64  	IgnoreStartupWait bool
    65  }
    66  
    67  // CRUDTestContext CRUD操作テストでのコンテキスト、一連のテスト中に共有される
    68  type CRUDTestContext struct {
    69  	// ID CRUDテスト対象リソースのID
    70  	//
    71  	// Create/Read/Updateの戻り値がidAccessorの場合に各操作の後で設定される
    72  	ID types.ID
    73  
    74  	// Values 一連のテスト中に共有したい値
    75  	//
    76  	// 依存リソースのIDの保持などで利用する
    77  	Values map[string]interface{}
    78  
    79  	// LastValue 最後の操作での戻り値
    80  	LastValue interface{}
    81  
    82  	ctx  context.Context
    83  	once sync.Once
    84  }
    85  
    86  func (c *CRUDTestContext) initInnerContext() {
    87  	c.once.Do(func() {
    88  		c.ctx = context.TODO()
    89  	})
    90  }
    91  
    92  // Deadline context.Context実装
    93  func (c *CRUDTestContext) Deadline() (deadline time.Time, ok bool) {
    94  	c.initInnerContext()
    95  	return c.ctx.Deadline()
    96  }
    97  
    98  // Done context.Context実装
    99  func (c *CRUDTestContext) Done() <-chan struct{} {
   100  	c.initInnerContext()
   101  	return c.ctx.Done()
   102  }
   103  
   104  // Err context.Context実装
   105  func (c *CRUDTestContext) Err() error {
   106  	c.initInnerContext()
   107  	return c.ctx.Err()
   108  }
   109  
   110  // Value context.Context実装
   111  func (c *CRUDTestContext) Value(key interface{}) interface{} {
   112  	c.initInnerContext()
   113  	return c.ctx.Value(key)
   114  }
   115  
   116  // CRUDTestFunc CRUD操作(DELETE以外)テストでのテスト用Func
   117  type CRUDTestFunc struct {
   118  	// Func API操作を行うFunc
   119  	Func func(*CRUDTestContext, iaas.APICaller) (interface{}, error)
   120  
   121  	// CheckFunc 任意のチェックを行うためのFunc、省略可能。
   122  	CheckFunc func(TestT, *CRUDTestContext, interface{}) error
   123  
   124  	// SkipExtractID Trueの場合Funcの戻り値からのID抽出を行わない
   125  	SkipExtractID bool
   126  }
   127  
   128  // CRUDTestDeleteFunc CRUD操作テストのDeleteテスト用Func
   129  type CRUDTestDeleteFunc struct {
   130  	// Func API操作を行うFunc
   131  	Func func(*CRUDTestContext, iaas.APICaller) error
   132  }
   133  
   134  // CRUDTestExpect CRUD操作(DELETE以外)テストでの期待値
   135  type CRUDTestExpect struct {
   136  	// ExpectValue CRUD操作実行後の期待値
   137  	ExpectValue interface{}
   138  
   139  	// IgnoreFields比較時に無視する項目
   140  	IgnoreFields []string
   141  }
   142  
   143  // Prepare テスト対象値を受け取り、比較可能な状態に加工した対象値と期待値を返す
   144  func (c *CRUDTestExpect) Prepare(actual interface{}) (interface{}, interface{}) {
   145  	toMap := func(v interface{}) map[string]interface{} {
   146  		data, err := json.Marshal(v)
   147  		if err != nil {
   148  			log.Fatalf("prepare is failed: json.Marshal returned error: %s", err)
   149  		}
   150  		var m map[string]interface{}
   151  		if err := json.Unmarshal(data, &m); err != nil {
   152  			log.Fatalf("prepare is failed: json.Unmarshal returned error: %s", err)
   153  		}
   154  		for _, key := range c.IgnoreFields {
   155  			delete(m, key)
   156  		}
   157  		return m
   158  	}
   159  
   160  	return toMap(c.ExpectValue), toMap(actual)
   161  }
   162  
   163  // RunCRUD 任意の条件でCRUD操作をテストする
   164  func RunCRUD(t TestT, testCase *CRUDTestCase) {
   165  	if testCase.SetupAPICallerFunc == nil {
   166  		t.Fatal("CRUDTestCase.SetupAPICallerFunc is required")
   167  	}
   168  
   169  	if testCase.Parallel {
   170  		t.Parallel()
   171  	}
   172  
   173  	if testCase.PreCheck != nil {
   174  		testCase.PreCheck(t)
   175  	}
   176  
   177  	testContext := &CRUDTestContext{
   178  		Values: make(map[string]interface{}),
   179  	}
   180  	defer func() {
   181  		// Cleanup
   182  		if testCase.Cleanup != nil {
   183  			if err := testCase.Cleanup(testContext, testCase.SetupAPICallerFunc()); err != nil {
   184  				t.Logf("Cleanup is failed: ", err)
   185  			}
   186  		}
   187  		if err := recover(); err != nil {
   188  			t.Logf("Unexpected error is occurred: %v, Trace: %s", err, string(debug.Stack()))
   189  		}
   190  	}()
   191  
   192  	if testCase.Setup != nil {
   193  		if err := testCase.Setup(testContext, testCase.SetupAPICallerFunc()); err != nil {
   194  			t.Error("Setup is failed: ", err)
   195  			return
   196  		}
   197  	}
   198  
   199  	testFunc := func(f *CRUDTestFunc) error {
   200  		actual, err := f.Func(testContext, testCase.SetupAPICallerFunc())
   201  		if err != nil {
   202  			return err
   203  		}
   204  		testContext.LastValue = actual
   205  
   206  		if actual != nil && f.CheckFunc != nil {
   207  			if err := f.CheckFunc(t, testContext, actual); err != nil {
   208  				return err
   209  			}
   210  		}
   211  
   212  		// extract ID from result of f.Func()
   213  		if actual != nil && !f.SkipExtractID {
   214  			if idHolder, ok := actual.(accessor.ID); ok {
   215  				testContext.ID = idHolder.GetID()
   216  			}
   217  		}
   218  
   219  		return nil
   220  	}
   221  
   222  	// Create
   223  	if testCase.Create != nil {
   224  		if err := testFunc(testCase.Create); err != nil {
   225  			t.Error("Create is failed: ", err)
   226  			return
   227  		}
   228  
   229  		if !testCase.IgnoreStartupWait && testCase.Read != nil && testContext.LastValue != nil {
   230  			waiter := iaas.WaiterForApplianceUp(func() (interface{}, error) {
   231  				return testCase.Read.Func(testContext, testCase.SetupAPICallerFunc())
   232  			}, 100)
   233  			if _, err := waiter.WaitForState(context.TODO()); err != nil {
   234  				t.Error("WaitForUp is failed: ", err)
   235  				return
   236  			}
   237  		}
   238  	}
   239  
   240  	// Read
   241  	if testCase.Read != nil {
   242  		if err := testFunc(testCase.Read); err != nil {
   243  			t.Fatal("Read is failed: ", err)
   244  		}
   245  	}
   246  
   247  	// Updates
   248  	for _, updFunc := range testCase.Updates {
   249  		if err := testFunc(updFunc); err != nil {
   250  			t.Error("Update is failed: ", err)
   251  			return
   252  		}
   253  	}
   254  
   255  	// Shutdown
   256  	if testCase.Shutdown != nil {
   257  		if testCase.Read == nil {
   258  			t.Log("CRUDTestCase.Shutdown is set, but CRUDTestCase.Read is nil. Shutdown is skipped")
   259  		} else {
   260  			v, err := testCase.Read.Func(testContext, testCase.SetupAPICallerFunc())
   261  			if err != nil {
   262  				t.Error("Shutdown is failed: ", err)
   263  				return
   264  			}
   265  			if v, ok := v.(accessor.InstanceStatus); ok && v.GetInstanceStatus().IsUp() {
   266  				if err := testCase.Shutdown(testContext, testCase.SetupAPICallerFunc()); err != nil {
   267  					t.Error("Shutdown is failed: ", err)
   268  					return
   269  				}
   270  
   271  				waiter := iaas.WaiterForDown(func() (interface{}, error) {
   272  					return testCase.Read.Func(testContext, testCase.SetupAPICallerFunc())
   273  				})
   274  				if _, err := waiter.WaitForState(context.TODO()); err != nil {
   275  					t.Error("WaitForDown is failed: ", err)
   276  					return
   277  				}
   278  			}
   279  		}
   280  	}
   281  
   282  	// Delete
   283  	if testCase.Delete != nil {
   284  		if err := testCase.Delete.Func(testContext, testCase.SetupAPICallerFunc()); err != nil {
   285  			t.Error("Delete is failed: ", err)
   286  			return
   287  		}
   288  		if testCase.Read != nil {
   289  			// check not exists
   290  			_, err := testCase.Read.Func(testContext, testCase.SetupAPICallerFunc())
   291  			if err == nil {
   292  				t.Error("Resource still exists: ", testContext.ID)
   293  				return
   294  			}
   295  			if e, ok := err.(iaas.APIError); ok {
   296  				if e.ResponseCode() != http.StatusNotFound {
   297  					t.Error("Reading after delete is failed: ", e)
   298  					return
   299  				}
   300  			}
   301  		}
   302  	}
   303  }