// Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2023 HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package views import ( "os" "path/filepath" "runtime" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/zclconf/go-cty/cty" "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/command/arguments" "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/configs/configschema" "github.com/opentofu/opentofu/internal/moduletest" "github.com/opentofu/opentofu/internal/plans" "github.com/opentofu/opentofu/internal/providers" "github.com/opentofu/opentofu/internal/states" "github.com/opentofu/opentofu/internal/terminal" "github.com/opentofu/opentofu/internal/tfdiags" ) func TestTestHuman_Conclusion(t *testing.T) { tcs := map[string]struct { Suite *moduletest.Suite Expected string }{ "no tests": { Suite: &moduletest.Suite{}, Expected: "\nExecuted 0 tests.\n", }, "only skipped tests": { Suite: &moduletest.Suite{ Status: moduletest.Skip, Files: map[string]*moduletest.File{ "descriptive_test_name.tftest.hcl": { Name: "descriptive_test_name.tftest.hcl", Status: moduletest.Skip, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Skip, }, { Name: "test_two", Status: moduletest.Skip, }, { Name: "test_three", Status: moduletest.Skip, }, }, }, "other_descriptive_test_name.tftest.hcl": { Name: "other_descriptive_test_name.tftest.hcl", Status: moduletest.Skip, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Skip, }, { Name: "test_two", Status: moduletest.Skip, }, { Name: "test_three", Status: moduletest.Skip, }, }, }, }, }, Expected: "\nExecuted 0 tests, 6 skipped.\n", }, "only passed tests": { Suite: &moduletest.Suite{ Status: moduletest.Pass, Files: map[string]*moduletest.File{ "descriptive_test_name.tftest.hcl": { Name: "descriptive_test_name.tftest.hcl", Status: moduletest.Pass, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Pass, }, { Name: "test_two", Status: moduletest.Pass, }, { Name: "test_three", Status: moduletest.Pass, }, }, }, "other_descriptive_test_name.tftest.hcl": { Name: "other_descriptive_test_name.tftest.hcl", Status: moduletest.Pass, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Pass, }, { Name: "test_two", Status: moduletest.Pass, }, { Name: "test_three", Status: moduletest.Pass, }, }, }, }, }, Expected: "\nSuccess! 6 passed, 0 failed.\n", }, "passed and skipped tests": { Suite: &moduletest.Suite{ Status: moduletest.Pass, Files: map[string]*moduletest.File{ "descriptive_test_name.tftest.hcl": { Name: "descriptive_test_name.tftest.hcl", Status: moduletest.Pass, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Pass, }, { Name: "test_two", Status: moduletest.Skip, }, { Name: "test_three", Status: moduletest.Pass, }, }, }, "other_descriptive_test_name.tftest.hcl": { Name: "other_descriptive_test_name.tftest.hcl", Status: moduletest.Pass, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Skip, }, { Name: "test_two", Status: moduletest.Pass, }, { Name: "test_three", Status: moduletest.Pass, }, }, }, }, }, Expected: "\nSuccess! 4 passed, 0 failed, 2 skipped.\n", }, "only failed tests": { Suite: &moduletest.Suite{ Status: moduletest.Fail, Files: map[string]*moduletest.File{ "descriptive_test_name.tftest.hcl": { Name: "descriptive_test_name.tftest.hcl", Status: moduletest.Fail, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Fail, }, { Name: "test_two", Status: moduletest.Fail, }, { Name: "test_three", Status: moduletest.Fail, }, }, }, "other_descriptive_test_name.tftest.hcl": { Name: "other_descriptive_test_name.tftest.hcl", Status: moduletest.Fail, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Fail, }, { Name: "test_two", Status: moduletest.Fail, }, { Name: "test_three", Status: moduletest.Fail, }, }, }, }, }, Expected: "\nFailure! 0 passed, 6 failed.\n", }, "failed and skipped tests": { Suite: &moduletest.Suite{ Status: moduletest.Fail, Files: map[string]*moduletest.File{ "descriptive_test_name.tftest.hcl": { Name: "descriptive_test_name.tftest.hcl", Status: moduletest.Fail, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Fail, }, { Name: "test_two", Status: moduletest.Skip, }, { Name: "test_three", Status: moduletest.Fail, }, }, }, "other_descriptive_test_name.tftest.hcl": { Name: "other_descriptive_test_name.tftest.hcl", Status: moduletest.Fail, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Fail, }, { Name: "test_two", Status: moduletest.Fail, }, { Name: "test_three", Status: moduletest.Skip, }, }, }, }, }, Expected: "\nFailure! 0 passed, 4 failed, 2 skipped.\n", }, "failed, passed and skipped tests": { Suite: &moduletest.Suite{ Status: moduletest.Fail, Files: map[string]*moduletest.File{ "descriptive_test_name.tftest.hcl": { Name: "descriptive_test_name.tftest.hcl", Status: moduletest.Fail, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Fail, }, { Name: "test_two", Status: moduletest.Pass, }, { Name: "test_three", Status: moduletest.Skip, }, }, }, "other_descriptive_test_name.tftest.hcl": { Name: "other_descriptive_test_name.tftest.hcl", Status: moduletest.Fail, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Skip, }, { Name: "test_two", Status: moduletest.Fail, }, { Name: "test_three", Status: moduletest.Pass, }, }, }, }, }, Expected: "\nFailure! 2 passed, 2 failed, 2 skipped.\n", }, "failed and errored tests": { Suite: &moduletest.Suite{ Status: moduletest.Error, Files: map[string]*moduletest.File{ "descriptive_test_name.tftest.hcl": { Name: "descriptive_test_name.tftest.hcl", Status: moduletest.Error, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Fail, }, { Name: "test_two", Status: moduletest.Error, }, { Name: "test_three", Status: moduletest.Fail, }, }, }, "other_descriptive_test_name.tftest.hcl": { Name: "other_descriptive_test_name.tftest.hcl", Status: moduletest.Error, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Fail, }, { Name: "test_two", Status: moduletest.Error, }, { Name: "test_three", Status: moduletest.Error, }, }, }, }, }, Expected: "\nFailure! 0 passed, 6 failed.\n", }, "failed, errored, passed, and skipped tests": { Suite: &moduletest.Suite{ Status: moduletest.Error, Files: map[string]*moduletest.File{ "descriptive_test_name.tftest.hcl": { Name: "descriptive_test_name.tftest.hcl", Status: moduletest.Fail, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Pass, }, { Name: "test_two", Status: moduletest.Pass, }, { Name: "test_three", Status: moduletest.Fail, }, }, }, "other_descriptive_test_name.tftest.hcl": { Name: "other_descriptive_test_name.tftest.hcl", Status: moduletest.Error, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Error, }, { Name: "test_two", Status: moduletest.Skip, }, { Name: "test_three", Status: moduletest.Skip, }, }, }, }, }, Expected: "\nFailure! 2 passed, 2 failed, 2 skipped.\n", }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewTest(arguments.ViewHuman, NewView(streams)) view.Conclusion(tc.Suite) actual := done(t).Stdout() expected := tc.Expected if diff := cmp.Diff(expected, actual); len(diff) > 0 { t.Fatalf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) } }) } } func TestTestHuman_File(t *testing.T) { tcs := map[string]struct { File *moduletest.File Expected string }{ "pass": { File: &moduletest.File{Name: "main.tf", Status: moduletest.Pass}, Expected: "main.tf... pass\n", }, "pending": { File: &moduletest.File{Name: "main.tf", Status: moduletest.Pending}, Expected: "main.tf... pending\n", }, "skip": { File: &moduletest.File{Name: "main.tf", Status: moduletest.Skip}, Expected: "main.tf... skip\n", }, "fail": { File: &moduletest.File{Name: "main.tf", Status: moduletest.Fail}, Expected: "main.tf... fail\n", }, "error": { File: &moduletest.File{Name: "main.tf", Status: moduletest.Error}, Expected: "main.tf... fail\n", }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewTest(arguments.ViewHuman, NewView(streams)) view.File(tc.File) actual := done(t).Stdout() expected := tc.Expected if diff := cmp.Diff(expected, actual); len(diff) > 0 { t.Fatalf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) } }) } } func TestTestHuman_Run(t *testing.T) { tcs := map[string]struct { Run *moduletest.Run StdOut string StdErr string }{ "pass": { Run: &moduletest.Run{Name: "run_block", Status: moduletest.Pass}, StdOut: " run \"run_block\"... pass\n", }, "pass_with_diags": { Run: &moduletest.Run{ Name: "run_block", Status: moduletest.Pass, Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Warning, "a warning occurred", "some warning happened during this test")}, }, StdOut: ` run "run_block"... pass Warning: a warning occurred some warning happened during this test `, }, "pending": { Run: &moduletest.Run{Name: "run_block", Status: moduletest.Pending}, StdOut: " run \"run_block\"... pending\n", }, "skip": { Run: &moduletest.Run{Name: "run_block", Status: moduletest.Skip}, StdOut: " run \"run_block\"... skip\n", }, "fail": { Run: &moduletest.Run{Name: "run_block", Status: moduletest.Fail}, StdOut: " run \"run_block\"... fail\n", }, "fail_with_diags": { Run: &moduletest.Run{ Name: "run_block", Status: moduletest.Fail, Diagnostics: tfdiags.Diagnostics{ tfdiags.Sourceless(tfdiags.Error, "a comparison failed", "details details details"), tfdiags.Sourceless(tfdiags.Error, "a second comparison failed", "other details"), }, }, StdOut: " run \"run_block\"... fail\n", StdErr: ` Error: a comparison failed details details details Error: a second comparison failed other details `, }, "error": { Run: &moduletest.Run{Name: "run_block", Status: moduletest.Error}, StdOut: " run \"run_block\"... fail\n", }, "error_with_diags": { Run: &moduletest.Run{ Name: "run_block", Status: moduletest.Error, Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "an error occurred", "something bad happened during this test")}, }, StdOut: " run \"run_block\"... fail\n", StdErr: ` Error: an error occurred something bad happened during this test `, }, "verbose_plan": { Run: &moduletest.Run{ Name: "run_block", Status: moduletest.Pass, Config: &configs.TestRun{ Command: configs.PlanTestCommand, }, Verbose: &moduletest.Verbose{ Plan: &plans.Plan{ Changes: &plans.Changes{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "creating", }, }, }, PrevRunAddr: addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "creating", }, }, }, ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.Provider{ Hostname: addrs.DefaultProviderRegistryHost, Namespace: "hashicorp", Type: "test", }, }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, After: dynamicValue( t, cty.ObjectVal(map[string]cty.Value{ "value": cty.StringVal("Hello, world!"), }), cty.Object(map[string]cty.Type{ "value": cty.String, })), }, }, }, }, }, State: states.NewState(), // empty state Config: &configs.Config{}, Providers: map[addrs.Provider]providers.ProviderSchema{ addrs.Provider{ Hostname: addrs.DefaultProviderRegistryHost, Namespace: "hashicorp", Type: "test", }: { ResourceTypes: map[string]providers.Schema{ "test_resource": { Block: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "value": { Type: cty.String, }, }, }, }, }, }, }, }, }, StdOut: ` run "run_block"... pass OpenTofu used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create OpenTofu will perform the following actions: # test_resource.creating will be created + resource "test_resource" "creating" { + value = "Hello, world!" } Plan: 1 to add, 0 to change, 0 to destroy. `, }, "verbose_apply": { Run: &moduletest.Run{ Name: "run_block", Status: moduletest.Pass, Config: &configs.TestRun{ Command: configs.ApplyTestCommand, }, Verbose: &moduletest.Verbose{ Plan: &plans.Plan{}, // empty plan State: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "creating", }, }, }, &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"value":"foobar"}`), }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.Provider{ Hostname: addrs.DefaultProviderRegistryHost, Namespace: "hashicorp", Type: "test", }, }, addrs.NoKey) }), Config: &configs.Config{}, Providers: map[addrs.Provider]providers.ProviderSchema{ addrs.Provider{ Hostname: addrs.DefaultProviderRegistryHost, Namespace: "hashicorp", Type: "test", }: { ResourceTypes: map[string]providers.Schema{ "test_resource": { Block: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "value": { Type: cty.String, }, }, }, }, }, }, }, }, }, StdOut: ` run "run_block"... pass # test_resource.creating: resource "test_resource" "creating" { value = "foobar" } `, }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { file := &moduletest.File{ Name: "main.tftest.hcl", } streams, done := terminal.StreamsForTesting(t) view := NewTest(arguments.ViewHuman, NewView(streams)) view.Run(tc.Run, file) output := done(t) actual, expected := output.Stdout(), tc.StdOut if diff := cmp.Diff(expected, actual); len(diff) > 0 { t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) } actual, expected = output.Stderr(), tc.StdErr if diff := cmp.Diff(expected, actual); len(diff) > 0 { t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) } }) } } func TestTestHuman_DestroySummary(t *testing.T) { tcs := map[string]struct { diags tfdiags.Diagnostics run *moduletest.Run file *moduletest.File state *states.State stdout string stderr string }{ "empty": { diags: nil, file: &moduletest.File{Name: "main.tftest.hcl"}, state: states.NewState(), }, "empty_state_only_warnings": { diags: tfdiags.Diagnostics{ tfdiags.Sourceless(tfdiags.Warning, "first warning", "some thing not very bad happened"), tfdiags.Sourceless(tfdiags.Warning, "second warning", "some thing not very bad happened again"), }, file: &moduletest.File{Name: "main.tftest.hcl"}, state: states.NewState(), stdout: ` Warning: first warning some thing not very bad happened Warning: second warning some thing not very bad happened again `, }, "empty_state_with_errors": { diags: tfdiags.Diagnostics{ tfdiags.Sourceless(tfdiags.Warning, "first warning", "some thing not very bad happened"), tfdiags.Sourceless(tfdiags.Warning, "second warning", "some thing not very bad happened again"), tfdiags.Sourceless(tfdiags.Error, "first error", "this time it is very bad"), }, file: &moduletest.File{Name: "main.tftest.hcl"}, state: states.NewState(), stdout: ` Warning: first warning some thing not very bad happened Warning: second warning some thing not very bad happened again `, stderr: `OpenTofu encountered an error destroying resources created while executing main.tftest.hcl. Error: first error this time it is very bad `, }, "error_from_run": { diags: tfdiags.Diagnostics{ tfdiags.Sourceless(tfdiags.Error, "first error", "this time it is very bad"), }, run: &moduletest.Run{Name: "run_block"}, file: &moduletest.File{Name: "main.tftest.hcl"}, state: states.NewState(), stderr: `OpenTofu encountered an error destroying resources created while executing main.tftest.hcl/run_block. Error: first error this time it is very bad `, }, "state_only_warnings": { diags: tfdiags.Diagnostics{ tfdiags.Sourceless(tfdiags.Warning, "first warning", "some thing not very bad happened"), tfdiags.Sourceless(tfdiags.Warning, "second warning", "some thing not very bad happened again"), }, file: &moduletest.File{Name: "main.tftest.hcl"}, state: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "bar", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) state.SetResourceInstanceDeposed( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "bar", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), "0fcb640a", &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) }), stdout: ` Warning: first warning some thing not very bad happened Warning: second warning some thing not very bad happened again `, stderr: ` OpenTofu left the following resources in state after executing main.tftest.hcl, these left-over resources can be viewed by reading the statefile written to disk(errored_test.tfstate) and they need to be cleaned up manually: - test.bar - test.bar (0fcb640a) - test.foo `, }, "state_with_errors": { diags: tfdiags.Diagnostics{ tfdiags.Sourceless(tfdiags.Warning, "first warning", "some thing not very bad happened"), tfdiags.Sourceless(tfdiags.Warning, "second warning", "some thing not very bad happened again"), tfdiags.Sourceless(tfdiags.Error, "first error", "this time it is very bad"), }, file: &moduletest.File{Name: "main.tftest.hcl"}, state: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "bar", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) state.SetResourceInstanceDeposed( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "bar", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), "0fcb640a", &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) }), stdout: ` Warning: first warning some thing not very bad happened Warning: second warning some thing not very bad happened again `, stderr: `OpenTofu encountered an error destroying resources created while executing main.tftest.hcl. Error: first error this time it is very bad OpenTofu left the following resources in state after executing main.tftest.hcl, these left-over resources can be viewed by reading the statefile written to disk(errored_test.tfstate) and they need to be cleaned up manually: - test.bar - test.bar (0fcb640a) - test.foo `, }, "state_null_resource_with_errors": { diags: tfdiags.Diagnostics{ tfdiags.Sourceless(tfdiags.Warning, "first warning", "some thing not very bad happened"), tfdiags.Sourceless(tfdiags.Warning, "second warning", "some thing not very bad happened again"), tfdiags.Sourceless(tfdiags.Error, "first error", "this time it is very bad"), }, file: &moduletest.File{Name: "main.tftest.hcl"}, state: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "null_resource", Name: "failing_will_depend_on_me", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("null"), }, addrs.NoKey) state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "null_resource", Name: "failing", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, Dependencies: []addrs.ConfigResource{ { Module: []string{}, Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "null_resource", Name: "failing_will_depend_on_me", }, }, }, CreateBeforeDestroy: false, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("null"), }, addrs.NoKey) }), stdout: ` Warning: first warning some thing not very bad happened Warning: second warning some thing not very bad happened again `, stderr: `OpenTofu encountered an error destroying resources created while executing main.tftest.hcl. Error: first error this time it is very bad OpenTofu left the following resources in state after executing main.tftest.hcl, these left-over resources can be viewed by reading the statefile written to disk(errored_test.tfstate) and they need to be cleaned up manually: - null_resource.failing - null_resource.failing_will_depend_on_me `, }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewTest(arguments.ViewHuman, NewView(streams)) view.DestroySummary(tc.diags, tc.run, tc.file, tc.state) output := done(t) actual, expected := output.Stdout(), tc.stdout if diff := cmp.Diff(expected, actual); len(diff) > 0 { t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) } actual, expected = output.Stderr(), tc.stderr if diff := cmp.Diff(expected, actual); len(diff) > 0 { t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) } }) } } func TestTestHuman_FatalInterruptSummary(t *testing.T) { tcs := map[string]struct { states map[*moduletest.Run]*states.State run *moduletest.Run created []*plans.ResourceInstanceChangeSrc want string }{ "no_state_only_plan": { states: make(map[*moduletest.Run]*states.State), run: &moduletest.Run{ Config: &configs.TestRun{}, Name: "run_block", }, created: []*plans.ResourceInstanceChangeSrc{ { Addr: addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "one", }, }, }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, }, }, { Addr: addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "two", }, }, }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, }, }, }, want: ` OpenTofu was interrupted while executing main.tftest.hcl, and may not have performed the expected cleanup operations. OpenTofu was in the process of creating the following resources for "run_block" from the module under test, and they may not have been destroyed: - test_instance.one - test_instance.two `, }, "file_state_no_plan": { states: map[*moduletest.Run]*states.State{ nil: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "one", }, }, }, &states.ResourceInstanceObjectSrc{}, addrs.AbsProviderConfig{}, addrs.NoKey) state.SetResourceInstanceCurrent( addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "two", }, }, }, &states.ResourceInstanceObjectSrc{}, addrs.AbsProviderConfig{}, addrs.NoKey) }), }, created: nil, want: ` OpenTofu was interrupted while executing main.tftest.hcl, and may not have performed the expected cleanup operations. OpenTofu has already created the following resources from the module under test: - test_instance.one - test_instance.two `, }, "run_states_no_plan": { states: map[*moduletest.Run]*states.State{ &moduletest.Run{ Name: "setup_block", Config: &configs.TestRun{ Module: &configs.TestRunModuleCall{ Source: addrs.ModuleSourceLocal("../setup"), }, }, }: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "one", }, }, }, &states.ResourceInstanceObjectSrc{}, addrs.AbsProviderConfig{}, addrs.NoKey) state.SetResourceInstanceCurrent( addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "two", }, }, }, &states.ResourceInstanceObjectSrc{}, addrs.AbsProviderConfig{}, addrs.NoKey) }), }, created: nil, want: ` OpenTofu was interrupted while executing main.tftest.hcl, and may not have performed the expected cleanup operations. OpenTofu has already created the following resources for "setup_block" from "../setup": - test_instance.one - test_instance.two `, }, "all_states_with_plan": { states: map[*moduletest.Run]*states.State{ &moduletest.Run{ Name: "setup_block", Config: &configs.TestRun{ Module: &configs.TestRunModuleCall{ Source: addrs.ModuleSourceLocal("../setup"), }, }, }: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "setup_one", }, }, }, &states.ResourceInstanceObjectSrc{}, addrs.AbsProviderConfig{}, addrs.NoKey) state.SetResourceInstanceCurrent( addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "setup_two", }, }, }, &states.ResourceInstanceObjectSrc{}, addrs.AbsProviderConfig{}, addrs.NoKey) }), nil: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "one", }, }, }, &states.ResourceInstanceObjectSrc{}, addrs.AbsProviderConfig{}, addrs.NoKey) state.SetResourceInstanceCurrent( addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "two", }, }, }, &states.ResourceInstanceObjectSrc{}, addrs.AbsProviderConfig{}, addrs.NoKey) }), }, created: []*plans.ResourceInstanceChangeSrc{ { Addr: addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "new_one", }, }, }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, }, }, { Addr: addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "new_two", }, }, }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, }, }, }, run: &moduletest.Run{ Config: &configs.TestRun{}, Name: "run_block", }, want: ` OpenTofu was interrupted while executing main.tftest.hcl, and may not have performed the expected cleanup operations. OpenTofu has already created the following resources from the module under test: - test_instance.one - test_instance.two OpenTofu has already created the following resources for "setup_block" from "../setup": - test_instance.setup_one - test_instance.setup_two OpenTofu was in the process of creating the following resources for "run_block" from the module under test, and they may not have been destroyed: - test_instance.new_one - test_instance.new_two `, }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewTest(arguments.ViewHuman, NewView(streams)) file := &moduletest.File{ Name: "main.tftest.hcl", Runs: func() []*moduletest.Run { var runs []*moduletest.Run for run := range tc.states { if run != nil { runs = append(runs, run) } } return runs }(), } view.FatalInterruptSummary(tc.run, file, tc.states, tc.created) actual, expected := done(t).Stderr(), tc.want if diff := cmp.Diff(expected, actual); len(diff) > 0 { t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) } }) } } func TestTestJSON_Abstract(t *testing.T) { tcs := map[string]struct { suite *moduletest.Suite want []map[string]interface{} }{ "single": { suite: &moduletest.Suite{ Files: map[string]*moduletest.File{ "main.tftest.hcl": { Runs: []*moduletest.Run{ { Name: "setup", }, }, }, }, }, want: []map[string]interface{}{ { "@level": "info", "@message": "Found 1 file and 1 run block", "@module": "tofu.ui", "test_abstract": map[string]interface{}{ "main.tftest.hcl": []interface{}{ "setup", }, }, "type": "test_abstract", }, }, }, "plural": { suite: &moduletest.Suite{ Files: map[string]*moduletest.File{ "main.tftest.hcl": { Runs: []*moduletest.Run{ { Name: "setup", }, { Name: "test", }, }, }, "other.tftest.hcl": { Runs: []*moduletest.Run{ { Name: "test", }, }, }, }, }, want: []map[string]interface{}{ { "@level": "info", "@message": "Found 2 files and 3 run blocks", "@module": "tofu.ui", "test_abstract": map[string]interface{}{ "main.tftest.hcl": []interface{}{ "setup", "test", }, "other.tftest.hcl": []interface{}{ "test", }, }, "type": "test_abstract", }, }, }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewTest(arguments.ViewJSON, NewView(streams)) view.Abstract(tc.suite) testJSONViewOutputEquals(t, done(t).All(), tc.want) }) } } func TestTestJSON_Conclusion(t *testing.T) { tcs := map[string]struct { suite *moduletest.Suite want []map[string]interface{} }{ "no tests": { suite: &moduletest.Suite{}, want: []map[string]interface{}{ { "@level": "info", "@message": "Executed 0 tests.", "@module": "tofu.ui", "test_summary": map[string]interface{}{ "status": "pending", "errored": 0.0, "failed": 0.0, "passed": 0.0, "skipped": 0.0, }, "type": "test_summary", }, }, }, "only skipped tests": { suite: &moduletest.Suite{ Status: moduletest.Skip, Files: map[string]*moduletest.File{ "descriptive_test_name.tftest.hcl": { Name: "descriptive_test_name.tftest.hcl", Status: moduletest.Skip, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Skip, }, { Name: "test_two", Status: moduletest.Skip, }, { Name: "test_three", Status: moduletest.Skip, }, }, }, "other_descriptive_test_name.tftest.hcl": { Name: "other_descriptive_test_name.tftest.hcl", Status: moduletest.Skip, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Skip, }, { Name: "test_two", Status: moduletest.Skip, }, { Name: "test_three", Status: moduletest.Skip, }, }, }, }, }, want: []map[string]interface{}{ { "@level": "info", "@message": "Executed 0 tests, 6 skipped.", "@module": "tofu.ui", "test_summary": map[string]interface{}{ "status": "skip", "errored": 0.0, "failed": 0.0, "passed": 0.0, "skipped": 6.0, }, "type": "test_summary", }, }, }, "only passed tests": { suite: &moduletest.Suite{ Status: moduletest.Pass, Files: map[string]*moduletest.File{ "descriptive_test_name.tftest.hcl": { Name: "descriptive_test_name.tftest.hcl", Status: moduletest.Pass, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Pass, }, { Name: "test_two", Status: moduletest.Pass, }, { Name: "test_three", Status: moduletest.Pass, }, }, }, "other_descriptive_test_name.tftest.hcl": { Name: "other_descriptive_test_name.tftest.hcl", Status: moduletest.Pass, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Pass, }, { Name: "test_two", Status: moduletest.Pass, }, { Name: "test_three", Status: moduletest.Pass, }, }, }, }, }, want: []map[string]interface{}{ { "@level": "info", "@message": "Success! 6 passed, 0 failed.", "@module": "tofu.ui", "test_summary": map[string]interface{}{ "status": "pass", "errored": 0.0, "failed": 0.0, "passed": 6.0, "skipped": 0.0, }, "type": "test_summary", }, }, }, "passed and skipped tests": { suite: &moduletest.Suite{ Status: moduletest.Pass, Files: map[string]*moduletest.File{ "descriptive_test_name.tftest.hcl": { Name: "descriptive_test_name.tftest.hcl", Status: moduletest.Pass, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Pass, }, { Name: "test_two", Status: moduletest.Skip, }, { Name: "test_three", Status: moduletest.Pass, }, }, }, "other_descriptive_test_name.tftest.hcl": { Name: "other_descriptive_test_name.tftest.hcl", Status: moduletest.Pass, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Skip, }, { Name: "test_two", Status: moduletest.Pass, }, { Name: "test_three", Status: moduletest.Pass, }, }, }, }, }, want: []map[string]interface{}{ { "@level": "info", "@message": "Success! 4 passed, 0 failed, 2 skipped.", "@module": "tofu.ui", "test_summary": map[string]interface{}{ "status": "pass", "errored": 0.0, "failed": 0.0, "passed": 4.0, "skipped": 2.0, }, "type": "test_summary", }, }, }, "only failed tests": { suite: &moduletest.Suite{ Status: moduletest.Fail, Files: map[string]*moduletest.File{ "descriptive_test_name.tftest.hcl": { Name: "descriptive_test_name.tftest.hcl", Status: moduletest.Fail, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Fail, }, { Name: "test_two", Status: moduletest.Fail, }, { Name: "test_three", Status: moduletest.Fail, }, }, }, "other_descriptive_test_name.tftest.hcl": { Name: "other_descriptive_test_name.tftest.hcl", Status: moduletest.Fail, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Fail, }, { Name: "test_two", Status: moduletest.Fail, }, { Name: "test_three", Status: moduletest.Fail, }, }, }, }, }, want: []map[string]interface{}{ { "@level": "info", "@message": "Failure! 0 passed, 6 failed.", "@module": "tofu.ui", "test_summary": map[string]interface{}{ "status": "fail", "errored": 0.0, "failed": 6.0, "passed": 0.0, "skipped": 0.0, }, "type": "test_summary", }, }, }, "failed and skipped tests": { suite: &moduletest.Suite{ Status: moduletest.Fail, Files: map[string]*moduletest.File{ "descriptive_test_name.tftest.hcl": { Name: "descriptive_test_name.tftest.hcl", Status: moduletest.Fail, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Fail, }, { Name: "test_two", Status: moduletest.Skip, }, { Name: "test_three", Status: moduletest.Fail, }, }, }, "other_descriptive_test_name.tftest.hcl": { Name: "other_descriptive_test_name.tftest.hcl", Status: moduletest.Fail, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Fail, }, { Name: "test_two", Status: moduletest.Fail, }, { Name: "test_three", Status: moduletest.Skip, }, }, }, }, }, want: []map[string]interface{}{ { "@level": "info", "@message": "Failure! 0 passed, 4 failed, 2 skipped.", "@module": "tofu.ui", "test_summary": map[string]interface{}{ "status": "fail", "errored": 0.0, "failed": 4.0, "passed": 0.0, "skipped": 2.0, }, "type": "test_summary", }, }, }, "failed, passed and skipped tests": { suite: &moduletest.Suite{ Status: moduletest.Fail, Files: map[string]*moduletest.File{ "descriptive_test_name.tftest.hcl": { Name: "descriptive_test_name.tftest.hcl", Status: moduletest.Fail, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Fail, }, { Name: "test_two", Status: moduletest.Pass, }, { Name: "test_three", Status: moduletest.Skip, }, }, }, "other_descriptive_test_name.tftest.hcl": { Name: "other_descriptive_test_name.tftest.hcl", Status: moduletest.Fail, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Skip, }, { Name: "test_two", Status: moduletest.Fail, }, { Name: "test_three", Status: moduletest.Pass, }, }, }, }, }, want: []map[string]interface{}{ { "@level": "info", "@message": "Failure! 2 passed, 2 failed, 2 skipped.", "@module": "tofu.ui", "test_summary": map[string]interface{}{ "status": "fail", "errored": 0.0, "failed": 2.0, "passed": 2.0, "skipped": 2.0, }, "type": "test_summary", }, }, }, "failed and errored tests": { suite: &moduletest.Suite{ Status: moduletest.Error, Files: map[string]*moduletest.File{ "descriptive_test_name.tftest.hcl": { Name: "descriptive_test_name.tftest.hcl", Status: moduletest.Error, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Fail, }, { Name: "test_two", Status: moduletest.Error, }, { Name: "test_three", Status: moduletest.Fail, }, }, }, "other_descriptive_test_name.tftest.hcl": { Name: "other_descriptive_test_name.tftest.hcl", Status: moduletest.Error, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Fail, }, { Name: "test_two", Status: moduletest.Error, }, { Name: "test_three", Status: moduletest.Error, }, }, }, }, }, want: []map[string]interface{}{ { "@level": "info", "@message": "Failure! 0 passed, 6 failed.", "@module": "tofu.ui", "test_summary": map[string]interface{}{ "status": "error", "errored": 3.0, "failed": 3.0, "passed": 0.0, "skipped": 0.0, }, "type": "test_summary", }, }, }, "failed, errored, passed, and skipped tests": { suite: &moduletest.Suite{ Status: moduletest.Error, Files: map[string]*moduletest.File{ "descriptive_test_name.tftest.hcl": { Name: "descriptive_test_name.tftest.hcl", Status: moduletest.Fail, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Pass, }, { Name: "test_two", Status: moduletest.Pass, }, { Name: "test_three", Status: moduletest.Fail, }, }, }, "other_descriptive_test_name.tftest.hcl": { Name: "other_descriptive_test_name.tftest.hcl", Status: moduletest.Error, Runs: []*moduletest.Run{ { Name: "test_one", Status: moduletest.Error, }, { Name: "test_two", Status: moduletest.Skip, }, { Name: "test_three", Status: moduletest.Skip, }, }, }, }, }, want: []map[string]interface{}{ { "@level": "info", "@message": "Failure! 2 passed, 2 failed, 2 skipped.", "@module": "tofu.ui", "test_summary": map[string]interface{}{ "status": "error", "errored": 1.0, "failed": 1.0, "passed": 2.0, "skipped": 2.0, }, "type": "test_summary", }, }, }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewTest(arguments.ViewJSON, NewView(streams)) view.Conclusion(tc.suite) testJSONViewOutputEquals(t, done(t).All(), tc.want) }) } } func TestTestJSON_DestroySummary(t *testing.T) { tcs := map[string]struct { file *moduletest.File run *moduletest.Run state *states.State diags tfdiags.Diagnostics want []map[string]interface{} }{ "empty_state_only_warnings": { diags: tfdiags.Diagnostics{ tfdiags.Sourceless(tfdiags.Warning, "first warning", "something not very bad happened"), tfdiags.Sourceless(tfdiags.Warning, "second warning", "something not very bad happened again"), }, file: &moduletest.File{Name: "main.tftest.hcl"}, state: states.NewState(), want: []map[string]interface{}{ { "@level": "warn", "@message": "Warning: first warning", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "diagnostic": map[string]interface{}{ "detail": "something not very bad happened", "severity": "warning", "summary": "first warning", }, "type": "diagnostic", }, { "@level": "warn", "@message": "Warning: second warning", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "diagnostic": map[string]interface{}{ "detail": "something not very bad happened again", "severity": "warning", "summary": "second warning", }, "type": "diagnostic", }, }, }, "empty_state_with_errors": { diags: tfdiags.Diagnostics{ tfdiags.Sourceless(tfdiags.Warning, "first warning", "something not very bad happened"), tfdiags.Sourceless(tfdiags.Warning, "second warning", "something not very bad happened again"), tfdiags.Sourceless(tfdiags.Error, "first error", "this time it is very bad"), }, file: &moduletest.File{Name: "main.tftest.hcl"}, state: states.NewState(), want: []map[string]interface{}{ { "@level": "warn", "@message": "Warning: first warning", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "diagnostic": map[string]interface{}{ "detail": "something not very bad happened", "severity": "warning", "summary": "first warning", }, "type": "diagnostic", }, { "@level": "warn", "@message": "Warning: second warning", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "diagnostic": map[string]interface{}{ "detail": "something not very bad happened again", "severity": "warning", "summary": "second warning", }, "type": "diagnostic", }, { "@level": "error", "@message": "Error: first error", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "diagnostic": map[string]interface{}{ "detail": "this time it is very bad", "severity": "error", "summary": "first error", }, "type": "diagnostic", }, }, }, "state_from_run": { file: &moduletest.File{Name: "main.tftest.hcl"}, run: &moduletest.Run{Name: "run_block"}, state: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) }), want: []map[string]interface{}{ { "@level": "error", "@message": "OpenTofu left some resources in state after executing main.tftest.hcl/run_block, these left-over resources can be viewed by reading the statefile written to disk(errored_test.tfstate) and they need to be cleaned up manually:", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "@testrun": "run_block", "test_cleanup": map[string]interface{}{ "failed_resources": []interface{}{ map[string]interface{}{ "instance": "test.foo", }, }, }, "type": "test_cleanup", }, }, }, "state_only_warnings": { diags: tfdiags.Diagnostics{ tfdiags.Sourceless(tfdiags.Warning, "first warning", "something not very bad happened"), tfdiags.Sourceless(tfdiags.Warning, "second warning", "something not very bad happened again"), }, file: &moduletest.File{Name: "main.tftest.hcl"}, state: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "bar", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) state.SetResourceInstanceDeposed( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "bar", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), "0fcb640a", &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) }), want: []map[string]interface{}{ { "@level": "error", "@message": "OpenTofu left some resources in state after executing main.tftest.hcl, these left-over resources can be viewed by reading the statefile written to disk(errored_test.tfstate) and they need to be cleaned up manually:", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "test_cleanup": map[string]interface{}{ "failed_resources": []interface{}{ map[string]interface{}{ "instance": "test.bar", }, map[string]interface{}{ "instance": "test.bar", "deposed_key": "0fcb640a", }, map[string]interface{}{ "instance": "test.foo", }, }, }, "type": "test_cleanup", }, { "@level": "warn", "@message": "Warning: first warning", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "diagnostic": map[string]interface{}{ "detail": "something not very bad happened", "severity": "warning", "summary": "first warning", }, "type": "diagnostic", }, { "@level": "warn", "@message": "Warning: second warning", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "diagnostic": map[string]interface{}{ "detail": "something not very bad happened again", "severity": "warning", "summary": "second warning", }, "type": "diagnostic", }, }, }, "state_with_errors": { diags: tfdiags.Diagnostics{ tfdiags.Sourceless(tfdiags.Warning, "first warning", "something not very bad happened"), tfdiags.Sourceless(tfdiags.Warning, "second warning", "something not very bad happened again"), tfdiags.Sourceless(tfdiags.Error, "first error", "this time it is very bad"), }, file: &moduletest.File{Name: "main.tftest.hcl"}, state: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "bar", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) state.SetResourceInstanceDeposed( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "bar", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), "0fcb640a", &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) }), want: []map[string]interface{}{ { "@level": "error", "@message": "OpenTofu left some resources in state after executing main.tftest.hcl, these left-over resources can be viewed by reading the statefile written to disk(errored_test.tfstate) and they need to be cleaned up manually:", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "test_cleanup": map[string]interface{}{ "failed_resources": []interface{}{ map[string]interface{}{ "instance": "test.bar", }, map[string]interface{}{ "instance": "test.bar", "deposed_key": "0fcb640a", }, map[string]interface{}{ "instance": "test.foo", }, }, }, "type": "test_cleanup", }, { "@level": "warn", "@message": "Warning: first warning", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "diagnostic": map[string]interface{}{ "detail": "something not very bad happened", "severity": "warning", "summary": "first warning", }, "type": "diagnostic", }, { "@level": "warn", "@message": "Warning: second warning", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "diagnostic": map[string]interface{}{ "detail": "something not very bad happened again", "severity": "warning", "summary": "second warning", }, "type": "diagnostic", }, { "@level": "error", "@message": "Error: first error", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "diagnostic": map[string]interface{}{ "detail": "this time it is very bad", "severity": "error", "summary": "first error", }, "type": "diagnostic", }, }, }, "state_null_resource_with_errors": { diags: tfdiags.Diagnostics{ tfdiags.Sourceless(tfdiags.Warning, "first warning", "something not very bad happened"), tfdiags.Sourceless(tfdiags.Warning, "second warning", "something not very bad happened again"), tfdiags.Sourceless(tfdiags.Error, "first error", "this time it is very bad"), }, file: &moduletest.File{Name: "main.tftest.hcl"}, state: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "null_resource", Name: "failing_will_depend_on_me", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("null"), }, addrs.NoKey) state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "null_resource", Name: "failing", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, Dependencies: []addrs.ConfigResource{ { Module: []string{}, Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "null_resource", Name: "failing_will_depend_on_me", }, }, }, CreateBeforeDestroy: false, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("null"), }, addrs.NoKey) }), want: []map[string]interface{}{ { "@level": "error", "@message": "OpenTofu left some resources in state after executing main.tftest.hcl, these left-over resources can be viewed by reading the statefile written to disk(errored_test.tfstate) and they need to be cleaned up manually:", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "test_cleanup": map[string]interface{}{ "failed_resources": []interface{}{ map[string]interface{}{ "instance": "null_resource.failing", }, map[string]interface{}{ "instance": "null_resource.failing_will_depend_on_me", }, }, }, "type": "test_cleanup", }, { "@level": "warn", "@message": "Warning: first warning", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "diagnostic": map[string]interface{}{ "detail": "something not very bad happened", "severity": "warning", "summary": "first warning", }, "type": "diagnostic", }, { "@level": "warn", "@message": "Warning: second warning", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "diagnostic": map[string]interface{}{ "detail": "something not very bad happened again", "severity": "warning", "summary": "second warning", }, "type": "diagnostic", }, { "@level": "error", "@message": "Error: first error", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "diagnostic": map[string]interface{}{ "detail": "this time it is very bad", "severity": "error", "summary": "first error", }, "type": "diagnostic", }, }, }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewTest(arguments.ViewJSON, NewView(streams)) view.DestroySummary(tc.diags, tc.run, tc.file, tc.state) testJSONViewOutputEquals(t, done(t).All(), tc.want) }) } } func TestTestJSON_File(t *testing.T) { tcs := map[string]struct { file *moduletest.File want []map[string]interface{} }{ "pass": { file: &moduletest.File{Name: "main.tf", Status: moduletest.Pass}, want: []map[string]interface{}{ { "@level": "info", "@message": "main.tf... pass", "@module": "tofu.ui", "@testfile": "main.tf", "test_file": map[string]interface{}{ "path": "main.tf", "status": "pass", }, "type": "test_file", }, }, }, "pending": { file: &moduletest.File{Name: "main.tf", Status: moduletest.Pending}, want: []map[string]interface{}{ { "@level": "info", "@message": "main.tf... pending", "@module": "tofu.ui", "@testfile": "main.tf", "test_file": map[string]interface{}{ "path": "main.tf", "status": "pending", }, "type": "test_file", }, }, }, "skip": { file: &moduletest.File{Name: "main.tf", Status: moduletest.Skip}, want: []map[string]interface{}{ { "@level": "info", "@message": "main.tf... skip", "@module": "tofu.ui", "@testfile": "main.tf", "test_file": map[string]interface{}{ "path": "main.tf", "status": "skip", }, "type": "test_file", }, }, }, "fail": { file: &moduletest.File{Name: "main.tf", Status: moduletest.Fail}, want: []map[string]interface{}{ { "@level": "info", "@message": "main.tf... fail", "@module": "tofu.ui", "@testfile": "main.tf", "test_file": map[string]interface{}{ "path": "main.tf", "status": "fail", }, "type": "test_file", }, }, }, "error": { file: &moduletest.File{Name: "main.tf", Status: moduletest.Error}, want: []map[string]interface{}{ { "@level": "info", "@message": "main.tf... fail", "@module": "tofu.ui", "@testfile": "main.tf", "test_file": map[string]interface{}{ "path": "main.tf", "status": "error", }, "type": "test_file", }, }, }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewTest(arguments.ViewJSON, NewView(streams)) view.File(tc.file) testJSONViewOutputEquals(t, done(t).All(), tc.want) }) } } func TestTestJSON_Run(t *testing.T) { tcs := map[string]struct { run *moduletest.Run want []map[string]interface{} }{ "pass": { run: &moduletest.Run{Name: "run_block", Status: moduletest.Pass}, want: []map[string]interface{}{ { "@level": "info", "@message": " \"run_block\"... pass", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "@testrun": "run_block", "test_run": map[string]interface{}{ "path": "main.tftest.hcl", "run": "run_block", "status": "pass", }, "type": "test_run", }, }, }, "pass_with_diags": { run: &moduletest.Run{ Name: "run_block", Status: moduletest.Pass, Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Warning, "a warning occurred", "some warning happened during this test")}, }, want: []map[string]interface{}{ { "@level": "info", "@message": " \"run_block\"... pass", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "@testrun": "run_block", "test_run": map[string]interface{}{ "path": "main.tftest.hcl", "run": "run_block", "status": "pass", }, "type": "test_run", }, { "@level": "warn", "@message": "Warning: a warning occurred", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "@testrun": "run_block", "diagnostic": map[string]interface{}{ "detail": "some warning happened during this test", "severity": "warning", "summary": "a warning occurred", }, "type": "diagnostic", }, }, }, "pending": { run: &moduletest.Run{Name: "run_block", Status: moduletest.Pending}, want: []map[string]interface{}{ { "@level": "info", "@message": " \"run_block\"... pending", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "@testrun": "run_block", "test_run": map[string]interface{}{ "path": "main.tftest.hcl", "run": "run_block", "status": "pending", }, "type": "test_run", }, }, }, "skip": { run: &moduletest.Run{Name: "run_block", Status: moduletest.Skip}, want: []map[string]interface{}{ { "@level": "info", "@message": " \"run_block\"... skip", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "@testrun": "run_block", "test_run": map[string]interface{}{ "path": "main.tftest.hcl", "run": "run_block", "status": "skip", }, "type": "test_run", }, }, }, "fail": { run: &moduletest.Run{Name: "run_block", Status: moduletest.Fail}, want: []map[string]interface{}{ { "@level": "info", "@message": " \"run_block\"... fail", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "@testrun": "run_block", "test_run": map[string]interface{}{ "path": "main.tftest.hcl", "run": "run_block", "status": "fail", }, "type": "test_run", }, }, }, "fail_with_diags": { run: &moduletest.Run{ Name: "run_block", Status: moduletest.Fail, Diagnostics: tfdiags.Diagnostics{ tfdiags.Sourceless(tfdiags.Error, "a comparison failed", "details details details"), tfdiags.Sourceless(tfdiags.Error, "a second comparison failed", "other details"), }, }, want: []map[string]interface{}{ { "@level": "info", "@message": " \"run_block\"... fail", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "@testrun": "run_block", "test_run": map[string]interface{}{ "path": "main.tftest.hcl", "run": "run_block", "status": "fail", }, "type": "test_run", }, { "@level": "error", "@message": "Error: a comparison failed", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "@testrun": "run_block", "diagnostic": map[string]interface{}{ "detail": "details details details", "severity": "error", "summary": "a comparison failed", }, "type": "diagnostic", }, { "@level": "error", "@message": "Error: a second comparison failed", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "@testrun": "run_block", "diagnostic": map[string]interface{}{ "detail": "other details", "severity": "error", "summary": "a second comparison failed", }, "type": "diagnostic", }, }, }, "error": { run: &moduletest.Run{Name: "run_block", Status: moduletest.Error}, want: []map[string]interface{}{ { "@level": "info", "@message": " \"run_block\"... fail", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "@testrun": "run_block", "test_run": map[string]interface{}{ "path": "main.tftest.hcl", "run": "run_block", "status": "error", }, "type": "test_run", }, }, }, "error_with_diags": { run: &moduletest.Run{ Name: "run_block", Status: moduletest.Error, Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "an error occurred", "something bad happened during this test")}, }, want: []map[string]interface{}{ { "@level": "info", "@message": " \"run_block\"... fail", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "@testrun": "run_block", "test_run": map[string]interface{}{ "path": "main.tftest.hcl", "run": "run_block", "status": "error", }, "type": "test_run", }, { "@level": "error", "@message": "Error: an error occurred", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "@testrun": "run_block", "diagnostic": map[string]interface{}{ "detail": "something bad happened during this test", "severity": "error", "summary": "an error occurred", }, "type": "diagnostic", }, }, }, "verbose_plan": { run: &moduletest.Run{ Name: "run_block", Status: moduletest.Pass, Config: &configs.TestRun{ Command: configs.PlanTestCommand, }, Verbose: &moduletest.Verbose{ Plan: &plans.Plan{ Changes: &plans.Changes{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "creating", }, }, }, PrevRunAddr: addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "creating", }, }, }, ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.Provider{ Hostname: addrs.DefaultProviderRegistryHost, Namespace: "hashicorp", Type: "test", }, }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, After: dynamicValue( t, cty.ObjectVal(map[string]cty.Value{ "value": cty.StringVal("foobar"), }), cty.Object(map[string]cty.Type{ "value": cty.String, })), }, }, }, }, }, State: states.NewState(), // empty state Config: &configs.Config{ Module: &configs.Module{ ProviderRequirements: &configs.RequiredProviders{}, }, }, Providers: map[addrs.Provider]providers.ProviderSchema{ addrs.Provider{ Hostname: addrs.DefaultProviderRegistryHost, Namespace: "hashicorp", Type: "test", }: { ResourceTypes: map[string]providers.Schema{ "test_resource": { Block: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "value": { Type: cty.String, }, }, }, }, }, }, }, }, }, want: []map[string]interface{}{ { "@level": "info", "@message": " \"run_block\"... pass", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "@testrun": "run_block", "test_run": map[string]interface{}{ "path": "main.tftest.hcl", "run": "run_block", "status": "pass", }, "type": "test_run", }, { "@level": "info", "@message": "-verbose flag enabled, printing plan", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "@testrun": "run_block", "test_plan": map[string]interface{}{ "configuration": map[string]interface{}{ "root_module": map[string]interface{}{}, }, "errored": false, "planned_values": map[string]interface{}{ "root_module": map[string]interface{}{ "resources": []interface{}{ map[string]interface{}{ "address": "test_resource.creating", "mode": "managed", "name": "creating", "provider_name": "registry.opentofu.org/hashicorp/test", "schema_version": 0.0, "sensitive_values": map[string]interface{}{}, "type": "test_resource", "values": map[string]interface{}{ "value": "foobar", }, }, }, }, }, "resource_changes": []interface{}{ map[string]interface{}{ "address": "test_resource.creating", "change": map[string]interface{}{ "actions": []interface{}{"create"}, "after": map[string]interface{}{ "value": "foobar", }, "after_sensitive": map[string]interface{}{}, "after_unknown": map[string]interface{}{}, "before": nil, "before_sensitive": false, }, "mode": "managed", "name": "creating", "provider_name": "registry.opentofu.org/hashicorp/test", "type": "test_resource", }, }, }, "type": "test_plan", }, }, }, "verbose_apply": { run: &moduletest.Run{ Name: "run_block", Status: moduletest.Pass, Config: &configs.TestRun{ Command: configs.ApplyTestCommand, }, Verbose: &moduletest.Verbose{ Plan: &plans.Plan{}, // empty plan State: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "creating", }, }, }, &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"value":"foobar"}`), }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.Provider{ Hostname: addrs.DefaultProviderRegistryHost, Namespace: "hashicorp", Type: "test", }, }, addrs.NoKey) }), Config: &configs.Config{ Module: &configs.Module{}, }, Providers: map[addrs.Provider]providers.ProviderSchema{ addrs.Provider{ Hostname: addrs.DefaultProviderRegistryHost, Namespace: "hashicorp", Type: "test", }: { ResourceTypes: map[string]providers.Schema{ "test_resource": { Block: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "value": { Type: cty.String, }, }, }, }, }, }, }, }, }, want: []map[string]interface{}{ { "@level": "info", "@message": " \"run_block\"... pass", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "@testrun": "run_block", "test_run": map[string]interface{}{ "path": "main.tftest.hcl", "run": "run_block", "status": "pass", }, "type": "test_run", }, { "@level": "info", "@message": "-verbose flag enabled, printing state", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "@testrun": "run_block", "test_state": map[string]interface{}{ "values": map[string]interface{}{ "root_module": map[string]interface{}{ "resources": []interface{}{ map[string]interface{}{ "address": "test_resource.creating", "mode": "managed", "name": "creating", "provider_name": "registry.opentofu.org/hashicorp/test", "schema_version": 0.0, "sensitive_values": map[string]interface{}{}, "type": "test_resource", "values": map[string]interface{}{ "value": "foobar", }, }, }, }, }, }, "type": "test_state", }, }, }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewTest(arguments.ViewJSON, NewView(streams)) file := &moduletest.File{Name: "main.tftest.hcl"} view.Run(tc.run, file) testJSONViewOutputEquals(t, done(t).All(), tc.want, cmp.FilterPath(func(path cmp.Path) bool { return strings.Contains(path.Last().String(), "version") || strings.Contains(path.Last().String(), "timestamp") }, cmp.Ignore())) }) } } func TestTestJSON_FatalInterruptSummary(t *testing.T) { tcs := map[string]struct { states map[*moduletest.Run]*states.State changes []*plans.ResourceInstanceChangeSrc want []map[string]interface{} }{ "no_state_only_plan": { states: make(map[*moduletest.Run]*states.State), changes: []*plans.ResourceInstanceChangeSrc{ { Addr: addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "one", }, }, }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, }, }, { Addr: addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "two", }, }, }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, }, }, }, want: []map[string]interface{}{ { "@level": "error", "@message": "OpenTofu was interrupted during test execution, and may not have performed the expected cleanup operations.", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "test_interrupt": map[string]interface{}{ "planned": []interface{}{ "test_instance.one", "test_instance.two", }, }, "type": "test_interrupt", }, }, }, "file_state_no_plan": { states: map[*moduletest.Run]*states.State{ nil: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "one", }, }, }, &states.ResourceInstanceObjectSrc{}, addrs.AbsProviderConfig{}, addrs.NoKey) state.SetResourceInstanceCurrent( addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "two", }, }, }, &states.ResourceInstanceObjectSrc{}, addrs.AbsProviderConfig{}, addrs.NoKey) }), }, changes: nil, want: []map[string]interface{}{ { "@level": "error", "@message": "OpenTofu was interrupted during test execution, and may not have performed the expected cleanup operations.", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "test_interrupt": map[string]interface{}{ "state": []interface{}{ map[string]interface{}{ "instance": "test_instance.one", }, map[string]interface{}{ "instance": "test_instance.two", }, }, }, "type": "test_interrupt", }, }, }, "run_states_no_plan": { states: map[*moduletest.Run]*states.State{ &moduletest.Run{Name: "setup_block"}: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "one", }, }, }, &states.ResourceInstanceObjectSrc{}, addrs.AbsProviderConfig{}, addrs.NoKey) state.SetResourceInstanceCurrent( addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "two", }, }, }, &states.ResourceInstanceObjectSrc{}, addrs.AbsProviderConfig{}, addrs.NoKey) }), }, changes: nil, want: []map[string]interface{}{ { "@level": "error", "@message": "OpenTofu was interrupted during test execution, and may not have performed the expected cleanup operations.", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "test_interrupt": map[string]interface{}{ "states": map[string]interface{}{ "setup_block": []interface{}{ map[string]interface{}{ "instance": "test_instance.one", }, map[string]interface{}{ "instance": "test_instance.two", }, }, }, }, "type": "test_interrupt", }, }, }, "all_states_with_plan": { states: map[*moduletest.Run]*states.State{ &moduletest.Run{Name: "setup_block"}: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "setup_one", }, }, }, &states.ResourceInstanceObjectSrc{}, addrs.AbsProviderConfig{}, addrs.NoKey) state.SetResourceInstanceCurrent( addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "setup_two", }, }, }, &states.ResourceInstanceObjectSrc{}, addrs.AbsProviderConfig{}, addrs.NoKey) }), nil: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "one", }, }, }, &states.ResourceInstanceObjectSrc{}, addrs.AbsProviderConfig{}, addrs.NoKey) state.SetResourceInstanceCurrent( addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "two", }, }, }, &states.ResourceInstanceObjectSrc{}, addrs.AbsProviderConfig{}, addrs.NoKey) }), }, changes: []*plans.ResourceInstanceChangeSrc{ { Addr: addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "new_one", }, }, }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, }, }, { Addr: addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "new_two", }, }, }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, }, }, }, want: []map[string]interface{}{ { "@level": "error", "@message": "OpenTofu was interrupted during test execution, and may not have performed the expected cleanup operations.", "@module": "tofu.ui", "@testfile": "main.tftest.hcl", "test_interrupt": map[string]interface{}{ "state": []interface{}{ map[string]interface{}{ "instance": "test_instance.one", }, map[string]interface{}{ "instance": "test_instance.two", }, }, "states": map[string]interface{}{ "setup_block": []interface{}{ map[string]interface{}{ "instance": "test_instance.setup_one", }, map[string]interface{}{ "instance": "test_instance.setup_two", }, }, }, "planned": []interface{}{ "test_instance.new_one", "test_instance.new_two", }, }, "type": "test_interrupt", }, }, }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewTest(arguments.ViewJSON, NewView(streams)) file := &moduletest.File{Name: "main.tftest.hcl"} run := &moduletest.Run{Name: "run_block"} view.FatalInterruptSummary(run, file, tc.states, tc.changes) testJSONViewOutputEquals(t, done(t).All(), tc.want) }) } } func TestSaveErroredStateFile(t *testing.T) { tcsHuman := map[string]struct { state *states.State run *moduletest.Run file *moduletest.File stderr string want interface{} }{ "state_foo_bar_human": { file: &moduletest.File{Name: "main.tftest.hcl"}, state: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "bar", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) state.SetResourceInstanceDeposed( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "bar", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), "0fcb640a", &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) }), stderr: ` Writing state to file: errored_test.tfstate `, want: nil, }, "state_null_resource_human": { file: &moduletest.File{Name: "main.tftest.hcl"}, state: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "null_resource", Name: "failing_will_depend_on_me", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("null"), }, addrs.NoKey) state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "null_resource", Name: "failing", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, Dependencies: []addrs.ConfigResource{ { Module: []string{}, Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "null_resource", Name: "failing_will_depend_on_me", }, }, }, CreateBeforeDestroy: false, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("null"), }, addrs.NoKey) }), stderr: ` Writing state to file: errored_test.tfstate `, want: nil, }, } tcsJson := map[string]struct { state *states.State run *moduletest.Run file *moduletest.File stderr string want interface{} }{ "state_with_run_json": { file: &moduletest.File{Name: "main.tftest.hcl"}, run: &moduletest.Run{Name: "run_block"}, state: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) }), stderr: "", want: []map[string]interface{}{ { "@level": "info", "@message": "Writing state to file: errored_test.tfstate", "@module": string("tofu.ui"), }, }, }, "state_foo_bar_json": { file: &moduletest.File{Name: "main.tftest.hcl"}, state: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "bar", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) state.SetResourceInstanceDeposed( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "bar", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), "0fcb640a", &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, addrs.NoKey) }), stderr: "", want: []map[string]interface{}{ { "@level": "info", "@message": "Writing state to file: errored_test.tfstate", "@module": "tofu.ui", }, }, }, "state_null_resource_with_errors": { file: &moduletest.File{Name: "main.tftest.hcl"}, state: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "null_resource", Name: "failing_will_depend_on_me", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("null"), }, addrs.NoKey) state.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "null_resource", Name: "failing", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, Dependencies: []addrs.ConfigResource{ { Module: []string{}, Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "null_resource", Name: "failing_will_depend_on_me", }, }, }, CreateBeforeDestroy: false, }, addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("null"), }, addrs.NoKey) }), stderr: "", want: []map[string]interface{}{ { "@level": "info", "@message": "Writing state to file: errored_test.tfstate", "@module": "tofu.ui", }, }, }, } // Run tests for Human view runTestSaveErroredStateFile(t, tcsHuman, arguments.ViewHuman) // Run tests for JSON view runTestSaveErroredStateFile(t, tcsJson, arguments.ViewJSON) } func runTestSaveErroredStateFile(t *testing.T, tc map[string]struct { state *states.State run *moduletest.Run file *moduletest.File stderr string want interface{} }, viewType arguments.ViewType) { for name, data := range tc { t.Run(name, func(t *testing.T) { // Create a temporary directory tempDir := t.TempDir() // Modify the state file path to use the temporary directory tempStateFilePath := filepath.Clean(filepath.Join(tempDir, "errored_test.tfstate")) // Change the working directory to the temporary directory t.Chdir(tempDir) streams, done := terminal.StreamsForTesting(t) switch viewType { case arguments.ViewHuman: view := NewTest(arguments.ViewHuman, NewView(streams)) SaveErroredTestStateFile(data.state, data.run, data.file, view) output := done(t) actual, expected := output.Stderr(), data.stderr if diff := cmp.Diff(expected, actual); len(diff) > 0 { t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) } case arguments.ViewJSON: view := NewTest(arguments.ViewJSON, NewView(streams)) SaveErroredTestStateFile(data.state, data.run, data.file, view) want, ok := data.want.([]map[string]interface{}) if !ok { t.Fatalf("Failed to assert want as []map[string]interface{}") } testJSONViewOutputEquals(t, done(t).All(), want) default: t.Fatalf("Unsupported view type: %v", viewType) } // Check if the state file exists if _, err := os.Stat(tempStateFilePath); os.IsNotExist(err) { // File does not exist t.Errorf("Expected state file 'errored_test.tfstate' to exist in: %s, but it does not.", tempDir) } // Trigger garbage collection to ensure that all open file handles are closed. // This prevents TempDir RemoveAll cleanup errors on Windows. if runtime.GOOS == "windows" { runtime.GC() } }) } } func dynamicValue(t *testing.T, value cty.Value, typ cty.Type) plans.DynamicValue { d, err := plans.NewDynamicValue(value, typ) if err != nil { t.Fatalf("failed to create dynamic value: %s", err) } return d }