github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/local/testing.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package local 5 6 import ( 7 "path/filepath" 8 "testing" 9 10 "github.com/zclconf/go-cty/cty" 11 12 "github.com/terramate-io/tf/addrs" 13 "github.com/terramate-io/tf/backend" 14 "github.com/terramate-io/tf/configs/configschema" 15 "github.com/terramate-io/tf/providers" 16 "github.com/terramate-io/tf/states" 17 "github.com/terramate-io/tf/states/statemgr" 18 "github.com/terramate-io/tf/terraform" 19 ) 20 21 // TestLocal returns a configured Local struct with temporary paths and 22 // in-memory ContextOpts. 23 // 24 // No operations will be called on the returned value, so you can still set 25 // public fields without any locks. 26 func TestLocal(t *testing.T) *Local { 27 t.Helper() 28 tempDir, err := filepath.EvalSymlinks(t.TempDir()) 29 if err != nil { 30 t.Fatal(err) 31 } 32 33 local := New() 34 local.StatePath = filepath.Join(tempDir, "state.tfstate") 35 local.StateOutPath = filepath.Join(tempDir, "state.tfstate") 36 local.StateBackupPath = filepath.Join(tempDir, "state.tfstate.bak") 37 local.StateWorkspaceDir = filepath.Join(tempDir, "state.tfstate.d") 38 local.ContextOpts = &terraform.ContextOpts{} 39 40 return local 41 } 42 43 // TestLocalProvider modifies the ContextOpts of the *Local parameter to 44 // have a provider with the given name. 45 func TestLocalProvider(t *testing.T, b *Local, name string, schema providers.ProviderSchema) *terraform.MockProvider { 46 // Build a mock resource provider for in-memory operations 47 p := new(terraform.MockProvider) 48 49 p.GetProviderSchemaResponse = &schema 50 51 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 52 // this is a destroy plan, 53 if req.ProposedNewState.IsNull() { 54 resp.PlannedState = req.ProposedNewState 55 resp.PlannedPrivate = req.PriorPrivate 56 return resp 57 } 58 59 rSchema, _ := schema.SchemaForResourceType(addrs.ManagedResourceMode, req.TypeName) 60 if rSchema == nil { 61 rSchema = &configschema.Block{} // default schema is empty 62 } 63 plannedVals := map[string]cty.Value{} 64 for name, attrS := range rSchema.Attributes { 65 val := req.ProposedNewState.GetAttr(name) 66 if attrS.Computed && val.IsNull() { 67 val = cty.UnknownVal(attrS.Type) 68 } 69 plannedVals[name] = val 70 } 71 for name := range rSchema.BlockTypes { 72 // For simplicity's sake we just copy the block attributes over 73 // verbatim, since this package's mock providers are all relatively 74 // simple -- we're testing the backend, not esoteric provider features. 75 plannedVals[name] = req.ProposedNewState.GetAttr(name) 76 } 77 78 return providers.PlanResourceChangeResponse{ 79 PlannedState: cty.ObjectVal(plannedVals), 80 PlannedPrivate: req.PriorPrivate, 81 } 82 } 83 p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { 84 return providers.ReadResourceResponse{NewState: req.PriorState} 85 } 86 p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { 87 return providers.ReadDataSourceResponse{State: req.Config} 88 } 89 90 // Initialize the opts 91 if b.ContextOpts == nil { 92 b.ContextOpts = &terraform.ContextOpts{} 93 } 94 95 // Set up our provider 96 b.ContextOpts.Providers = map[addrs.Provider]providers.Factory{ 97 addrs.NewDefaultProvider(name): providers.FactoryFixed(p), 98 } 99 100 return p 101 102 } 103 104 // TestLocalSingleState is a backend implementation that wraps Local 105 // and modifies it to only support single states (returns 106 // ErrWorkspacesNotSupported for multi-state operations). 107 // 108 // This isn't an actual use case, this is exported just to provide a 109 // easy way to test that behavior. 110 type TestLocalSingleState struct { 111 *Local 112 } 113 114 // TestNewLocalSingle is a factory for creating a TestLocalSingleState. 115 // This function matches the signature required for backend/init. 116 func TestNewLocalSingle() backend.Backend { 117 return &TestLocalSingleState{Local: New()} 118 } 119 120 func (b *TestLocalSingleState) Workspaces() ([]string, error) { 121 return nil, backend.ErrWorkspacesNotSupported 122 } 123 124 func (b *TestLocalSingleState) DeleteWorkspace(string, bool) error { 125 return backend.ErrWorkspacesNotSupported 126 } 127 128 func (b *TestLocalSingleState) StateMgr(name string) (statemgr.Full, error) { 129 if name != backend.DefaultStateName { 130 return nil, backend.ErrWorkspacesNotSupported 131 } 132 133 return b.Local.StateMgr(name) 134 } 135 136 // TestLocalNoDefaultState is a backend implementation that wraps 137 // Local and modifies it to support named states, but not the 138 // default state. It returns ErrDefaultWorkspaceNotSupported when 139 // the DefaultStateName is used. 140 type TestLocalNoDefaultState struct { 141 *Local 142 } 143 144 // TestNewLocalNoDefault is a factory for creating a TestLocalNoDefaultState. 145 // This function matches the signature required for backend/init. 146 func TestNewLocalNoDefault() backend.Backend { 147 return &TestLocalNoDefaultState{Local: New()} 148 } 149 150 func (b *TestLocalNoDefaultState) Workspaces() ([]string, error) { 151 workspaces, err := b.Local.Workspaces() 152 if err != nil { 153 return nil, err 154 } 155 156 filtered := workspaces[:0] 157 for _, name := range workspaces { 158 if name != backend.DefaultStateName { 159 filtered = append(filtered, name) 160 } 161 } 162 163 return filtered, nil 164 } 165 166 func (b *TestLocalNoDefaultState) DeleteWorkspace(name string, force bool) error { 167 if name == backend.DefaultStateName { 168 return backend.ErrDefaultWorkspaceNotSupported 169 } 170 return b.Local.DeleteWorkspace(name, force) 171 } 172 173 func (b *TestLocalNoDefaultState) StateMgr(name string) (statemgr.Full, error) { 174 if name == backend.DefaultStateName { 175 return nil, backend.ErrDefaultWorkspaceNotSupported 176 } 177 return b.Local.StateMgr(name) 178 } 179 180 func testStateFile(t *testing.T, path string, s *states.State) { 181 stateFile := statemgr.NewFilesystem(path) 182 stateFile.WriteState(s) 183 } 184 185 func mustProviderConfig(s string) addrs.AbsProviderConfig { 186 p, diags := addrs.ParseAbsProviderConfigStr(s) 187 if diags.HasErrors() { 188 panic(diags.Err()) 189 } 190 return p 191 } 192 193 func mustResourceInstanceAddr(s string) addrs.AbsResourceInstance { 194 addr, diags := addrs.ParseAbsResourceInstanceStr(s) 195 if diags.HasErrors() { 196 panic(diags.Err()) 197 } 198 return addr 199 } 200 201 // assertBackendStateUnlocked attempts to lock the backend state. Failure 202 // indicates that the state was indeed locked and therefore this function will 203 // return true. 204 func assertBackendStateUnlocked(t *testing.T, b *Local) bool { 205 t.Helper() 206 stateMgr, _ := b.StateMgr(backend.DefaultStateName) 207 if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { 208 t.Errorf("state is already locked: %s", err.Error()) 209 return false 210 } 211 return true 212 } 213 214 // assertBackendStateLocked attempts to lock the backend state. Failure 215 // indicates that the state was already locked and therefore this function will 216 // return false. 217 func assertBackendStateLocked(t *testing.T, b *Local) bool { 218 t.Helper() 219 stateMgr, _ := b.StateMgr(backend.DefaultStateName) 220 if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { 221 return true 222 } 223 t.Error("unexpected success locking state") 224 return true 225 }