github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/command/command_test.go (about) 1 package command 2 3 import ( 4 "bytes" 5 "crypto/md5" 6 "encoding/base64" 7 "encoding/json" 8 "flag" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "log" 13 "net/http" 14 "net/http/httptest" 15 "os" 16 "os/exec" 17 "path/filepath" 18 "strings" 19 "syscall" 20 "testing" 21 22 "github.com/hashicorp/terraform/internal/initwd" 23 "github.com/hashicorp/terraform/registry" 24 25 "github.com/hashicorp/terraform/addrs" 26 "github.com/hashicorp/terraform/configs" 27 "github.com/hashicorp/terraform/configs/configload" 28 "github.com/hashicorp/terraform/configs/configschema" 29 "github.com/hashicorp/terraform/helper/logging" 30 "github.com/hashicorp/terraform/plans" 31 "github.com/hashicorp/terraform/plans/planfile" 32 "github.com/hashicorp/terraform/providers" 33 "github.com/hashicorp/terraform/provisioners" 34 "github.com/hashicorp/terraform/states" 35 "github.com/hashicorp/terraform/states/statefile" 36 "github.com/hashicorp/terraform/states/statemgr" 37 "github.com/hashicorp/terraform/terraform" 38 "github.com/hashicorp/terraform/version" 39 "github.com/zclconf/go-cty/cty" 40 41 backendInit "github.com/hashicorp/terraform/backend/init" 42 ) 43 44 // These are the directories for our test data and fixtures. 45 var ( 46 fixtureDir = "./testdata" 47 testDataDir = "./testdata" 48 ) 49 50 // a top level temp directory which will be cleaned after all tests 51 var testingDir string 52 53 func init() { 54 test = true 55 56 // Initialize the backends 57 backendInit.Init(nil) 58 59 // Expand the data and fixture dirs on init because 60 // we change the working directory in some tests. 61 var err error 62 fixtureDir, err = filepath.Abs(fixtureDir) 63 if err != nil { 64 panic(err) 65 } 66 67 testDataDir, err = filepath.Abs(testDataDir) 68 if err != nil { 69 panic(err) 70 } 71 72 testingDir, err = ioutil.TempDir(testingDir, "tf") 73 if err != nil { 74 panic(err) 75 } 76 } 77 78 func TestMain(m *testing.M) { 79 defer os.RemoveAll(testingDir) 80 81 flag.Parse() 82 if testing.Verbose() { 83 // if we're verbose, use the logging requested by TF_LOG 84 logging.SetOutput() 85 } else { 86 // otherwise silence all logs 87 log.SetOutput(ioutil.Discard) 88 } 89 90 // Make sure backend init is initialized, since our tests tend to assume it. 91 backendInit.Init(nil) 92 93 os.Exit(m.Run()) 94 } 95 96 func tempDir(t *testing.T) string { 97 t.Helper() 98 99 dir, err := ioutil.TempDir(testingDir, "tf") 100 if err != nil { 101 t.Fatalf("err: %s", err) 102 } 103 104 dir, err = filepath.EvalSymlinks(dir) 105 if err != nil { 106 t.Fatal(err) 107 } 108 109 if err := os.RemoveAll(dir); err != nil { 110 t.Fatalf("err: %s", err) 111 } 112 113 return dir 114 } 115 116 func testFixturePath(name string) string { 117 return filepath.Join(fixtureDir, name) 118 } 119 120 func metaOverridesForProvider(p providers.Interface) *testingOverrides { 121 return &testingOverrides{ 122 ProviderResolver: providers.ResolverFixed( 123 map[addrs.Provider]providers.Factory{ 124 addrs.NewLegacyProvider("test"): providers.FactoryFixed(p), 125 }, 126 ), 127 } 128 } 129 130 func metaOverridesForProviderAndProvisioner(p providers.Interface, pr provisioners.Interface) *testingOverrides { 131 return &testingOverrides{ 132 ProviderResolver: providers.ResolverFixed( 133 map[addrs.Provider]providers.Factory{ 134 addrs.NewLegacyProvider("test"): providers.FactoryFixed(p), 135 }, 136 ), 137 Provisioners: map[string]provisioners.Factory{ 138 "shell": provisioners.FactoryFixed(pr), 139 }, 140 } 141 } 142 143 func testModule(t *testing.T, name string) *configs.Config { 144 t.Helper() 145 c, _ := testModuleWithSnapshot(t, name) 146 return c 147 } 148 149 func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *configload.Snapshot) { 150 t.Helper() 151 152 dir := filepath.Join(fixtureDir, name) 153 // FIXME: We're not dealing with the cleanup function here because 154 // this testModule function is used all over and so we don't want to 155 // change its interface at this late stage. 156 loader, _ := configload.NewLoaderForTests(t) 157 158 // Test modules usually do not refer to remote sources, and for local 159 // sources only this ultimately just records all of the module paths 160 // in a JSON file so that we can load them below. 161 inst := initwd.NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) 162 _, instDiags := inst.InstallModules(dir, true, initwd.ModuleInstallHooksImpl{}) 163 if instDiags.HasErrors() { 164 t.Fatal(instDiags.Err()) 165 } 166 167 config, snap, diags := loader.LoadConfigWithSnapshot(dir) 168 if diags.HasErrors() { 169 t.Fatal(diags.Error()) 170 } 171 172 return config, snap 173 } 174 175 // testPlan returns a non-nil noop plan. 176 func testPlan(t *testing.T) *plans.Plan { 177 t.Helper() 178 179 // This is what an empty configuration block would look like after being 180 // decoded with the schema of the "local" backend. 181 backendConfig := cty.ObjectVal(map[string]cty.Value{ 182 "path": cty.NullVal(cty.String), 183 "workspace_dir": cty.NullVal(cty.String), 184 }) 185 backendConfigRaw, err := plans.NewDynamicValue(backendConfig, backendConfig.Type()) 186 if err != nil { 187 t.Fatal(err) 188 } 189 190 return &plans.Plan{ 191 Backend: plans.Backend{ 192 // This is just a placeholder so that the plan file can be written 193 // out. Caller may wish to override it to something more "real" 194 // where the plan will actually be subsequently applied. 195 Type: "local", 196 Config: backendConfigRaw, 197 }, 198 Changes: plans.NewChanges(), 199 } 200 } 201 202 func testPlanFile(t *testing.T, configSnap *configload.Snapshot, state *states.State, plan *plans.Plan) string { 203 t.Helper() 204 205 stateFile := &statefile.File{ 206 Lineage: "", 207 State: state, 208 TerraformVersion: version.SemVer, 209 } 210 211 path := testTempFile(t) 212 err := planfile.Create(path, configSnap, stateFile, plan) 213 if err != nil { 214 t.Fatalf("failed to create temporary plan file: %s", err) 215 } 216 217 return path 218 } 219 220 // testPlanFileNoop is a shortcut function that creates a plan file that 221 // represents no changes and returns its path. This is useful when a test 222 // just needs any plan file, and it doesn't matter what is inside it. 223 func testPlanFileNoop(t *testing.T) string { 224 snap := &configload.Snapshot{ 225 Modules: map[string]*configload.SnapshotModule{ 226 "": { 227 Dir: ".", 228 Files: map[string][]byte{ 229 "main.tf": nil, 230 }, 231 }, 232 }, 233 } 234 state := states.NewState() 235 plan := testPlan(t) 236 return testPlanFile(t, snap, state, plan) 237 } 238 239 func testReadPlan(t *testing.T, path string) *plans.Plan { 240 t.Helper() 241 242 f, err := planfile.Open(path) 243 if err != nil { 244 t.Fatalf("error opening plan file %q: %s", path, err) 245 } 246 defer f.Close() 247 248 p, err := f.ReadPlan() 249 if err != nil { 250 t.Fatalf("error reading plan from plan file %q: %s", path, err) 251 } 252 253 return p 254 } 255 256 // testState returns a test State structure that we use for a lot of tests. 257 func testState() *states.State { 258 return states.BuildState(func(s *states.SyncState) { 259 s.SetResourceInstanceCurrent( 260 addrs.Resource{ 261 Mode: addrs.ManagedResourceMode, 262 Type: "test_instance", 263 Name: "foo", 264 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 265 &states.ResourceInstanceObjectSrc{ 266 // The weird whitespace here is reflective of how this would 267 // get written out in a real state file, due to the indentation 268 // of all of the containing wrapping objects and arrays. 269 AttrsJSON: []byte("{\n \"id\": \"bar\"\n }"), 270 Status: states.ObjectReady, 271 Dependencies: []addrs.AbsResource{}, 272 DependsOn: []addrs.Referenceable{}, 273 }, 274 addrs.ProviderConfig{ 275 Type: addrs.NewLegacyProvider("test"), 276 }.Absolute(addrs.RootModuleInstance), 277 ) 278 // DeepCopy is used here to ensure our synthetic state matches exactly 279 // with a state that will have been copied during the command 280 // operation, and all fields have been copied correctly. 281 }).DeepCopy() 282 } 283 284 // writeStateForTesting is a helper that writes the given naked state to the 285 // given writer, generating a stub *statefile.File wrapper which is then 286 // immediately discarded. 287 func writeStateForTesting(state *states.State, w io.Writer) error { 288 sf := &statefile.File{ 289 Serial: 0, 290 Lineage: "fake-for-testing", 291 State: state, 292 } 293 return statefile.Write(sf, w) 294 } 295 296 // testStateMgrCurrentLineage returns the current lineage for the given state 297 // manager, or the empty string if it does not use lineage. This is primarily 298 // for testing against the local backend, which always supports lineage. 299 func testStateMgrCurrentLineage(mgr statemgr.Persistent) string { 300 if pm, ok := mgr.(statemgr.PersistentMeta); ok { 301 m := pm.StateSnapshotMeta() 302 return m.Lineage 303 } 304 return "" 305 } 306 307 // markStateForMatching is a helper that writes a specific marker value to 308 // a state so that it can be recognized later with getStateMatchingMarker. 309 // 310 // Internally this just sets a root module output value called "testing_mark" 311 // to the given string value. If the state is being checked in other ways, 312 // the test code may need to compensate for the addition or overwriting of this 313 // special output value name. 314 // 315 // The given mark string is returned verbatim, to allow the following pattern 316 // in tests: 317 // 318 // mark := markStateForMatching(state, "foo") 319 // // (do stuff to the state) 320 // assertStateHasMarker(state, mark) 321 func markStateForMatching(state *states.State, mark string) string { 322 state.RootModule().SetOutputValue("testing_mark", cty.StringVal(mark), false) 323 return mark 324 } 325 326 // getStateMatchingMarker is used with markStateForMatching to retrieve the 327 // mark string previously added to the given state. If no such mark is present, 328 // the result is an empty string. 329 func getStateMatchingMarker(state *states.State) string { 330 os := state.RootModule().OutputValues["testing_mark"] 331 if os == nil { 332 return "" 333 } 334 v := os.Value 335 if v.Type() == cty.String && v.IsKnown() && !v.IsNull() { 336 return v.AsString() 337 } 338 return "" 339 } 340 341 // stateHasMarker is a helper around getStateMatchingMarker that also includes 342 // the equality test, for more convenient use in test assertion branches. 343 func stateHasMarker(state *states.State, want string) bool { 344 return getStateMatchingMarker(state) == want 345 } 346 347 // assertStateHasMarker wraps stateHasMarker to automatically generate a 348 // fatal test result (i.e. t.Fatal) if the marker doesn't match. 349 func assertStateHasMarker(t *testing.T, state *states.State, want string) { 350 if !stateHasMarker(state, want) { 351 t.Fatalf("wrong state marker\ngot: %q\nwant: %q", getStateMatchingMarker(state), want) 352 } 353 } 354 355 func testStateFile(t *testing.T, s *states.State) string { 356 t.Helper() 357 358 path := testTempFile(t) 359 360 f, err := os.Create(path) 361 if err != nil { 362 t.Fatalf("failed to create temporary state file %s: %s", path, err) 363 } 364 defer f.Close() 365 366 err = writeStateForTesting(s, f) 367 if err != nil { 368 t.Fatalf("failed to write state to temporary file %s: %s", path, err) 369 } 370 371 return path 372 } 373 374 // testStateFileDefault writes the state out to the default statefile 375 // in the cwd. Use `testCwd` to change into a temp cwd. 376 func testStateFileDefault(t *testing.T, s *terraform.State) string { 377 t.Helper() 378 379 f, err := os.Create(DefaultStateFilename) 380 if err != nil { 381 t.Fatalf("err: %s", err) 382 } 383 defer f.Close() 384 385 if err := terraform.WriteState(s, f); err != nil { 386 t.Fatalf("err: %s", err) 387 } 388 389 return DefaultStateFilename 390 } 391 392 // testStateFileRemote writes the state out to the remote statefile 393 // in the cwd. Use `testCwd` to change into a temp cwd. 394 func testStateFileRemote(t *testing.T, s *terraform.State) string { 395 t.Helper() 396 397 path := filepath.Join(DefaultDataDir, DefaultStateFilename) 398 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { 399 t.Fatalf("err: %s", err) 400 } 401 402 f, err := os.Create(path) 403 if err != nil { 404 t.Fatalf("err: %s", err) 405 } 406 defer f.Close() 407 408 if err := terraform.WriteState(s, f); err != nil { 409 t.Fatalf("err: %s", err) 410 } 411 412 return path 413 } 414 415 // testStateRead reads the state from a file 416 func testStateRead(t *testing.T, path string) *states.State { 417 t.Helper() 418 419 f, err := os.Open(path) 420 if err != nil { 421 t.Fatalf("err: %s", err) 422 } 423 defer f.Close() 424 425 sf, err := statefile.Read(f) 426 if err != nil { 427 t.Fatalf("err: %s", err) 428 } 429 430 return sf.State 431 } 432 433 // testDataStateRead reads a "data state", which is a file format resembling 434 // our state format v3 that is used only to track current backend settings. 435 // 436 // This old format still uses *terraform.State, but should be replaced with 437 // a more specialized type in a later release. 438 func testDataStateRead(t *testing.T, path string) *terraform.State { 439 t.Helper() 440 441 f, err := os.Open(path) 442 if err != nil { 443 t.Fatalf("err: %s", err) 444 } 445 defer f.Close() 446 447 s, err := terraform.ReadState(f) 448 if err != nil { 449 t.Fatalf("err: %s", err) 450 } 451 452 return s 453 } 454 455 // testStateOutput tests that the state at the given path contains 456 // the expected state string. 457 func testStateOutput(t *testing.T, path string, expected string) { 458 t.Helper() 459 460 newState := testStateRead(t, path) 461 actual := strings.TrimSpace(newState.String()) 462 expected = strings.TrimSpace(expected) 463 if actual != expected { 464 t.Fatalf("expected:\n%s\nactual:\n%s", expected, actual) 465 } 466 } 467 468 func testProvider() *terraform.MockProvider { 469 p := new(terraform.MockProvider) 470 p.PlanResourceChangeResponse = providers.PlanResourceChangeResponse{ 471 PlannedState: cty.EmptyObjectVal, 472 } 473 p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { 474 return providers.ReadResourceResponse{ 475 NewState: req.PriorState, 476 } 477 } 478 return p 479 } 480 481 func testTempFile(t *testing.T) string { 482 t.Helper() 483 484 return filepath.Join(testTempDir(t), "state.tfstate") 485 } 486 487 func testTempDir(t *testing.T) string { 488 t.Helper() 489 490 d, err := ioutil.TempDir(testingDir, "tf") 491 if err != nil { 492 t.Fatalf("err: %s", err) 493 } 494 495 d, err = filepath.EvalSymlinks(d) 496 if err != nil { 497 t.Fatal(err) 498 } 499 500 return d 501 } 502 503 // testRename renames the path to new and returns a function to defer to 504 // revert the rename. 505 func testRename(t *testing.T, base, path, new string) func() { 506 t.Helper() 507 508 if base != "" { 509 path = filepath.Join(base, path) 510 new = filepath.Join(base, new) 511 } 512 513 if err := os.Rename(path, new); err != nil { 514 t.Fatalf("err: %s", err) 515 } 516 517 return func() { 518 // Just re-rename and ignore the return value 519 testRename(t, "", new, path) 520 } 521 } 522 523 // testChdir changes the directory and returns a function to defer to 524 // revert the old cwd. 525 func testChdir(t *testing.T, new string) func() { 526 t.Helper() 527 528 old, err := os.Getwd() 529 if err != nil { 530 t.Fatalf("err: %s", err) 531 } 532 533 if err := os.Chdir(new); err != nil { 534 t.Fatalf("err: %v", err) 535 } 536 537 return func() { 538 // Re-run the function ignoring the defer result 539 testChdir(t, old) 540 } 541 } 542 543 // testCwd is used to change the current working directory 544 // into a test directory that should be remoted after 545 func testCwd(t *testing.T) (string, string) { 546 t.Helper() 547 548 tmp, err := ioutil.TempDir(testingDir, "tf") 549 if err != nil { 550 t.Fatalf("err: %v", err) 551 } 552 553 cwd, err := os.Getwd() 554 if err != nil { 555 t.Fatalf("err: %v", err) 556 } 557 558 if err := os.Chdir(tmp); err != nil { 559 t.Fatalf("err: %v", err) 560 } 561 562 return tmp, cwd 563 } 564 565 // testFixCwd is used to as a defer to testDir 566 func testFixCwd(t *testing.T, tmp, cwd string) { 567 t.Helper() 568 569 if err := os.Chdir(cwd); err != nil { 570 t.Fatalf("err: %v", err) 571 } 572 573 if err := os.RemoveAll(tmp); err != nil { 574 t.Fatalf("err: %v", err) 575 } 576 } 577 578 // testStdinPipe changes os.Stdin to be a pipe that sends the data from 579 // the reader before closing the pipe. 580 // 581 // The returned function should be deferred to properly clean up and restore 582 // the original stdin. 583 func testStdinPipe(t *testing.T, src io.Reader) func() { 584 t.Helper() 585 586 r, w, err := os.Pipe() 587 if err != nil { 588 t.Fatalf("err: %s", err) 589 } 590 591 // Modify stdin to point to our new pipe 592 old := os.Stdin 593 os.Stdin = r 594 595 // Copy the data from the reader to the pipe 596 go func() { 597 defer w.Close() 598 io.Copy(w, src) 599 }() 600 601 return func() { 602 // Close our read end 603 r.Close() 604 605 // Reset stdin 606 os.Stdin = old 607 } 608 } 609 610 // Modify os.Stdout to write to the given buffer. Note that this is generally 611 // not useful since the commands are configured to write to a cli.Ui, not 612 // Stdout directly. Commands like `console` though use the raw stdout. 613 func testStdoutCapture(t *testing.T, dst io.Writer) func() { 614 t.Helper() 615 616 r, w, err := os.Pipe() 617 if err != nil { 618 t.Fatalf("err: %s", err) 619 } 620 621 // Modify stdout 622 old := os.Stdout 623 os.Stdout = w 624 625 // Copy 626 doneCh := make(chan struct{}) 627 go func() { 628 defer close(doneCh) 629 defer r.Close() 630 io.Copy(dst, r) 631 }() 632 633 return func() { 634 // Close the writer end of the pipe 635 w.Sync() 636 w.Close() 637 638 // Reset stdout 639 os.Stdout = old 640 641 // Wait for the data copy to complete to avoid a race reading data 642 <-doneCh 643 } 644 } 645 646 // testInteractiveInput configures tests so that the answers given are sent 647 // in order to interactive prompts. The returned function must be called 648 // in a defer to clean up. 649 func testInteractiveInput(t *testing.T, answers []string) func() { 650 t.Helper() 651 652 // Disable test mode so input is called 653 test = false 654 655 // Setup reader/writers 656 testInputResponse = answers 657 defaultInputReader = bytes.NewBufferString("") 658 defaultInputWriter = new(bytes.Buffer) 659 660 // Return the cleanup 661 return func() { 662 test = true 663 testInputResponse = nil 664 } 665 } 666 667 // testInputMap configures tests so that the given answers are returned 668 // for calls to Input when the right question is asked. The key is the 669 // question "Id" that is used. 670 func testInputMap(t *testing.T, answers map[string]string) func() { 671 t.Helper() 672 673 // Disable test mode so input is called 674 test = false 675 676 // Setup reader/writers 677 defaultInputReader = bytes.NewBufferString("") 678 defaultInputWriter = new(bytes.Buffer) 679 680 // Setup answers 681 testInputResponse = nil 682 testInputResponseMap = answers 683 684 // Return the cleanup 685 return func() { 686 test = true 687 testInputResponseMap = nil 688 } 689 } 690 691 // testBackendState is used to make a test HTTP server to test a configured 692 // backend. This returns the complete state that can be saved. Use 693 // `testStateFileRemote` to write the returned state. 694 // 695 // When using this function, the configuration fixture for the test must 696 // include an empty configuration block for the HTTP backend, like this: 697 // 698 // terraform { 699 // backend "http" { 700 // } 701 // } 702 // 703 // If such a block isn't present, or if it isn't empty, then an error will 704 // be returned about the backend configuration having changed and that 705 // "terraform init" must be run, since the test backend config cache created 706 // by this function contains the hash for an empty configuration. 707 func testBackendState(t *testing.T, s *terraform.State, c int) (*terraform.State, *httptest.Server) { 708 t.Helper() 709 710 var b64md5 string 711 buf := bytes.NewBuffer(nil) 712 713 cb := func(resp http.ResponseWriter, req *http.Request) { 714 if req.Method == "PUT" { 715 resp.WriteHeader(c) 716 return 717 } 718 if s == nil { 719 resp.WriteHeader(404) 720 return 721 } 722 723 resp.Header().Set("Content-MD5", b64md5) 724 resp.Write(buf.Bytes()) 725 } 726 727 // If a state was given, make sure we calculate the proper b64md5 728 if s != nil { 729 enc := json.NewEncoder(buf) 730 if err := enc.Encode(s); err != nil { 731 t.Fatalf("err: %v", err) 732 } 733 md5 := md5.Sum(buf.Bytes()) 734 b64md5 = base64.StdEncoding.EncodeToString(md5[:16]) 735 } 736 737 srv := httptest.NewServer(http.HandlerFunc(cb)) 738 739 backendConfig := &configs.Backend{ 740 Type: "http", 741 Config: configs.SynthBody("<testBackendState>", map[string]cty.Value{}), 742 } 743 b := backendInit.Backend("http")() 744 configSchema := b.ConfigSchema() 745 hash := backendConfig.Hash(configSchema) 746 747 state := terraform.NewState() 748 state.Backend = &terraform.BackendState{ 749 Type: "http", 750 ConfigRaw: json.RawMessage(fmt.Sprintf(`{"address":%q}`, srv.URL)), 751 Hash: uint64(hash), 752 } 753 754 return state, srv 755 } 756 757 // testRemoteState is used to make a test HTTP server to return a given 758 // state file that can be used for testing legacy remote state. 759 // 760 // The return values are a *terraform.State instance that should be written 761 // as the "data state" (really: backend state) and the server that the 762 // returned data state refers to. 763 func testRemoteState(t *testing.T, s *states.State, c int) (*terraform.State, *httptest.Server) { 764 t.Helper() 765 766 var b64md5 string 767 buf := bytes.NewBuffer(nil) 768 769 cb := func(resp http.ResponseWriter, req *http.Request) { 770 if req.Method == "PUT" { 771 resp.WriteHeader(c) 772 return 773 } 774 if s == nil { 775 resp.WriteHeader(404) 776 return 777 } 778 779 resp.Header().Set("Content-MD5", b64md5) 780 resp.Write(buf.Bytes()) 781 } 782 783 retState := terraform.NewState() 784 785 srv := httptest.NewServer(http.HandlerFunc(cb)) 786 b := &terraform.BackendState{ 787 Type: "http", 788 } 789 b.SetConfig(cty.ObjectVal(map[string]cty.Value{ 790 "address": cty.StringVal(srv.URL), 791 }), &configschema.Block{ 792 Attributes: map[string]*configschema.Attribute{ 793 "address": { 794 Type: cty.String, 795 Required: true, 796 }, 797 }, 798 }) 799 retState.Backend = b 800 801 if s != nil { 802 err := statefile.Write(&statefile.File{State: s}, buf) 803 if err != nil { 804 t.Fatalf("failed to write initial state: %v", err) 805 } 806 } 807 808 return retState, srv 809 } 810 811 // testlockState calls a separate process to the lock the state file at path. 812 // deferFunc should be called in the caller to properly unlock the file. 813 // Since many tests change the working directory, the sourcedir argument must be 814 // supplied to locate the statelocker.go source. 815 func testLockState(sourceDir, path string) (func(), error) { 816 // build and run the binary ourselves so we can quickly terminate it for cleanup 817 buildDir, err := ioutil.TempDir(testingDir, "locker") 818 if err != nil { 819 return nil, err 820 } 821 cleanFunc := func() { 822 os.RemoveAll(buildDir) 823 } 824 825 source := filepath.Join(sourceDir, "statelocker.go") 826 lockBin := filepath.Join(buildDir, "statelocker") 827 828 cmd := exec.Command("go", "build", "-mod=vendor", "-o", lockBin, source) 829 cmd.Dir = filepath.Dir(sourceDir) 830 831 out, err := cmd.CombinedOutput() 832 if err != nil { 833 cleanFunc() 834 return nil, fmt.Errorf("%s %s", err, out) 835 } 836 837 locker := exec.Command(lockBin, path) 838 pr, pw, err := os.Pipe() 839 if err != nil { 840 cleanFunc() 841 return nil, err 842 } 843 defer pr.Close() 844 defer pw.Close() 845 locker.Stderr = pw 846 locker.Stdout = pw 847 848 if err := locker.Start(); err != nil { 849 return nil, err 850 } 851 deferFunc := func() { 852 cleanFunc() 853 locker.Process.Signal(syscall.SIGTERM) 854 locker.Wait() 855 } 856 857 // wait for the process to lock 858 buf := make([]byte, 1024) 859 n, err := pr.Read(buf) 860 if err != nil { 861 return deferFunc, fmt.Errorf("read from statelocker returned: %s", err) 862 } 863 864 output := string(buf[:n]) 865 if !strings.HasPrefix(output, "LOCKID") { 866 return deferFunc, fmt.Errorf("statelocker wrote: %s", string(buf[:n])) 867 } 868 return deferFunc, nil 869 } 870 871 // normalizeJSON removes all insignificant whitespace from the given JSON buffer 872 // and returns it as a string for easier comparison. 873 func normalizeJSON(t *testing.T, src []byte) string { 874 t.Helper() 875 var buf bytes.Buffer 876 err := json.Compact(&buf, src) 877 if err != nil { 878 t.Fatalf("error normalizing JSON: %s", err) 879 } 880 return buf.String() 881 } 882 883 func mustResourceAddr(s string) addrs.AbsResource { 884 addr, diags := addrs.ParseAbsResourceStr(s) 885 if diags.HasErrors() { 886 panic(diags.Err()) 887 } 888 return addr 889 }