testing framework: refactor interrupt logic for immediate exits (#33532)

* testing framework: refactor interrupt logic

* fix formatting
This commit is contained in:
Liam Cervante 2023-07-19 10:31:32 +02:00 committed by GitHub
parent 6882dd9530
commit 6c7db16566
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 817 additions and 98 deletions

View File

@ -6,6 +6,7 @@ import (
"path"
"sort"
"strings"
"time"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend"
@ -173,6 +174,14 @@ func (c *TestCommand) Run(rawArgs []string) int {
return 1
}
// We have two levels of interrupt here. A 'stop' and a 'cancel'. A 'stop'
// is a soft request to stop. We'll finish the current test, do the tidy up,
// but then skip all remaining tests and run blocks. A 'cancel' is a hard
// request to stop now. We'll cancel the current operation immediately
// even if it's a delete operation, and we won't clean up any infrastructure
// if we're halfway through a test. We'll print details explaining what was
// stopped so the user can do their best to recover from it.
runningCtx, done := context.WithCancel(context.Background())
stopCtx, stop := context.WithCancel(runningCtx)
cancelCtx, cancel := context.WithCancel(context.Background())
@ -199,7 +208,7 @@ func (c *TestCommand) Run(rawArgs []string) int {
go func() {
defer logging.PanicHandler()
defer done() // We completed successfully.
defer done()
defer stop()
defer cancel()
@ -224,10 +233,12 @@ func (c *TestCommand) Run(rawArgs []string) int {
runner.Cancelled = true
cancel()
// TODO(liamcervante): Should we add a timer here? That would mean
// after 5 seconds we just give up and don't even print out the
// lists of resources left behind?
<-runningCtx.Done() // Nothing left to do now but wait.
// We'll wait 5 seconds for this operation to finish now, regardless
// of whether it finishes successfully or not.
select {
case <-runningCtx.Done():
case <-time.After(5 * time.Second):
}
case <-runningCtx.Done():
// The application finished nicely after the request was stopped.
@ -331,13 +342,13 @@ func (runner *TestRunner) ExecuteTestFile(file *moduletest.File, globals map[str
if run.Config.ConfigUnderTest != nil {
// Then we want to execute a different module under a kind of
// sandbox.
state := runner.ExecuteTestRun(run, file, states.NewState(), run.Config.ConfigUnderTest, globals)
state := runner.ExecuteTestRun(mgr, run, file, states.NewState(), run.Config.ConfigUnderTest, globals)
mgr.States = append(mgr.States, &TestModuleState{
State: state,
Run: run,
})
} else {
mgr.State = runner.ExecuteTestRun(run, file, mgr.State, runner.Config, globals)
mgr.State = runner.ExecuteTestRun(mgr, run, file, mgr.State, runner.Config, globals)
}
file.Status = file.Status.Merge(run.Status)
}
@ -348,7 +359,7 @@ func (runner *TestRunner) ExecuteTestFile(file *moduletest.File, globals map[str
}
}
func (runner *TestRunner) ExecuteTestRun(run *moduletest.Run, file *moduletest.File, state *states.State, config *configs.Config, globals map[string]backend.UnparsedVariableValue) *states.State {
func (runner *TestRunner) ExecuteTestRun(mgr *TestStateManager, run *moduletest.Run, file *moduletest.File, state *states.State, config *configs.Config, globals map[string]backend.UnparsedVariableValue) *states.State {
if runner.Cancelled {
// Don't do anything, just give up and return immediately.
// The surrounding functions should stop this even being called, but in
@ -376,7 +387,7 @@ func (runner *TestRunner) ExecuteTestRun(run *moduletest.Run, file *moduletest.F
return state
}
ctx, plan, state, diags := runner.execute(run, file, config, state, &terraform.PlanOpts{
ctx, plan, state, diags := runner.execute(mgr, run, file, config, state, &terraform.PlanOpts{
Mode: func() plans.Mode {
switch run.Config.Options.Mode {
case configs.RefreshOnlyTestMode:
@ -458,17 +469,12 @@ func (runner *TestRunner) ExecuteTestRun(run *moduletest.Run, file *moduletest.F
//
// The command argument decides whether it executes only a plan or also applies
// the plan it creates during the planning.
func (runner *TestRunner) execute(run *moduletest.Run, file *moduletest.File, config *configs.Config, state *states.State, opts *terraform.PlanOpts, command configs.TestCommand, globals map[string]backend.UnparsedVariableValue) (*terraform.Context, *plans.Plan, *states.State, tfdiags.Diagnostics) {
func (runner *TestRunner) execute(mgr *TestStateManager, run *moduletest.Run, file *moduletest.File, config *configs.Config, state *states.State, opts *terraform.PlanOpts, command configs.TestCommand, globals map[string]backend.UnparsedVariableValue) (*terraform.Context, *plans.Plan, *states.State, tfdiags.Diagnostics) {
if opts.Mode == plans.DestroyMode && state.Empty() {
// Nothing to do!
return nil, nil, state, nil
}
identifier := file.Name
if run != nil {
identifier = fmt.Sprintf("%s/%s", identifier, run.Name)
}
// First, transform the config for the given test run and test file.
var diags tfdiags.Diagnostics
@ -517,7 +523,7 @@ func (runner *TestRunner) execute(run *moduletest.Run, file *moduletest.File, co
defer done()
plan, planDiags = tfCtx.Plan(config, state, opts)
}()
waitDiags, cancelled := runner.wait(tfCtx, runningCtx, opts, identifier)
waitDiags, cancelled := runner.wait(tfCtx, runningCtx, mgr, run, file, nil)
planDiags = planDiags.Append(waitDiags)
diags = diags.Append(planDiags)
@ -556,6 +562,25 @@ func (runner *TestRunner) execute(run *moduletest.Run, file *moduletest.File, co
runningCtx, done = context.WithCancel(context.Background())
// If things get cancelled while we are executing the apply operation below
// we want to print out all the objects that we were creating so the user
// can verify we managed to tidy everything up possibly.
//
// Unfortunately, this creates a race condition as the apply operation can
// edit the plan (by removing changes once they are applied) while at the
// same time our cancellation process will try to read the plan.
//
// We take a quick copy of the changes we care about here, which will then
// be used in place of the plan when we print out the objects to be created
// as part of the cancellation process.
var created []*plans.ResourceInstanceChangeSrc
for _, change := range plan.Changes.Resources {
if change.Action != plans.Create {
continue
}
created = append(created, change)
}
var updated *states.State
var applyDiags tfdiags.Diagnostics
@ -564,77 +589,58 @@ func (runner *TestRunner) execute(run *moduletest.Run, file *moduletest.File, co
defer done()
updated, applyDiags = tfCtx.Apply(plan, config)
}()
waitDiags, _ = runner.wait(tfCtx, runningCtx, opts, identifier)
waitDiags, _ = runner.wait(tfCtx, runningCtx, mgr, run, file, created)
applyDiags = applyDiags.Append(waitDiags)
diags = diags.Append(applyDiags)
return tfCtx, plan, updated, diags
}
func (runner *TestRunner) wait(ctx *terraform.Context, runningCtx context.Context, opts *terraform.PlanOpts, identifier string) (diags tfdiags.Diagnostics, cancelled bool) {
select {
case <-runner.StoppedCtx.Done():
func (runner *TestRunner) wait(ctx *terraform.Context, runningCtx context.Context, mgr *TestStateManager, run *moduletest.Run, file *moduletest.File, created []*plans.ResourceInstanceChangeSrc) (diags tfdiags.Diagnostics, cancelled bool) {
if opts.Mode != plans.DestroyMode {
// It takes more impetus from the user to cancel the cleanup
// operations, so we only do this during the actual tests.
cancelled = true
go ctx.Stop()
// This function handles what happens when the user presses the second
// interrupt. This is a "hard cancel", we are going to stop doing whatever
// it is we're doing. This means even if we're halfway through creating or
// destroying infrastructure we just give up.
handleCancelled := func() {
states := make(map[*moduletest.Run]*states.State)
states[nil] = mgr.State
for _, module := range mgr.States {
states[module.Run] = module.State
}
runner.View.FatalInterruptSummary(run, file, states, created)
select {
case <-runner.CancelledCtx.Done():
// If the user still really wants to cancel, then we'll oblige
// even during the destroy mode at this point.
if opts.Mode == plans.DestroyMode {
cancelled = true
go ctx.Stop()
}
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Terraform Test Interrupted",
fmt.Sprintf("Terraform test was interrupted while executing %s. This means resources that were created during the test may have been left active, please monitor the rest of the output closely as any dangling resources will be listed.", identifier)))
// It is actually quite disastrous if we exist early at this
// point as it means we'll have created resources that we
// haven't tracked at all. So for now, we won't ever actually
// forcibly terminate the test. When cancelled, we make the
// clean up faster by not performing it but we should still
// always manage it give an accurate list of resources left
// alive.
// TODO(liamcervante): Consider adding a timer here, so that we
// exit early even if that means some resources are just lost
// forever.
<-runningCtx.Done() // Just wait for things to finish now.
case <-runningCtx.Done():
// The operation exited nicely when asked!
}
case <-runner.CancelledCtx.Done():
// This shouldn't really happen, as we'd expect to see the StoppedCtx
// being triggered first. But, just in case.
cancelled = true
go ctx.Stop()
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Terraform Test Interrupted",
fmt.Sprintf("Terraform test was interrupted while executing %s. This means resources that were created during the test may have been left active, please monitor the rest of the output closely as any dangling resources will be listed.", identifier)))
// Just wait for things to finish now, the overall test execution will
// exit early if this takes too long.
<-runningCtx.Done()
}
// It is actually quite disastrous if we exist early at this
// point as it means we'll have created resources that we
// haven't tracked at all. So for now, we won't ever actually
// forcibly terminate the test. When cancelled, we make the
// clean up faster by not performing it but we should still
// always manage it give an accurate list of resources left
// alive.
// TODO(liamcervante): Consider adding a timer here, so that we
// exit early even if that means some resources are just lost
// forever.
<-runningCtx.Done() // Just wait for things to finish now.
// This function handles what happens when the user presses the first
// interrupt. This is essentially a "soft cancel", we're not going to do
// anything but just wait for things to finish safely. But, we do listen
// for the crucial second interrupt which will prompt a hard stop / cancel.
handleStopped := func() {
select {
case <-runner.CancelledCtx.Done():
// We've been asked again. This time we stop whatever we're doing
// and abandon all attempts to do anything reasonable.
handleCancelled()
case <-runningCtx.Done():
// Do nothing, we finished safely and skipping the remaining tests
// will be handled elsewhere.
}
}
select {
case <-runner.StoppedCtx.Done():
handleStopped()
case <-runner.CancelledCtx.Done():
handleCancelled()
case <-runningCtx.Done():
// The operation exited normally.
}
@ -675,27 +681,21 @@ type TestModuleState struct {
func (manager *TestStateManager) cleanupStates(file *moduletest.File, globals map[string]backend.UnparsedVariableValue) {
if manager.runner.Cancelled {
// We are still going to print out the resources that we have left
// even though the user asked for an immediate exit.
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Test cleanup skipped due to immediate exit", "Terraform could not clean up the state left behind due to immediate interrupt."))
manager.runner.View.DestroySummary(diags, nil, file, manager.State)
for _, module := range manager.States {
manager.runner.View.DestroySummary(diags, module.Run, file, module.State)
}
// Don't try and clean anything up if the execution has been cancelled.
return
}
// First, we'll clean up the main state.
_, _, state, diags := manager.runner.execute(nil, file, manager.runner.Config, manager.State, &terraform.PlanOpts{
_, _, state, diags := manager.runner.execute(manager, nil, file, manager.runner.Config, manager.State, &terraform.PlanOpts{
Mode: plans.DestroyMode,
}, configs.ApplyTestCommand, globals)
manager.runner.View.DestroySummary(diags, nil, file, state)
if manager.runner.Cancelled {
// In case things were cancelled during the last execution.
return
}
// Then we'll clean up the additional states for custom modules in reverse
// order.
for ix := len(manager.States); ix > 0; ix-- {
@ -704,11 +704,10 @@ func (manager *TestStateManager) cleanupStates(file *moduletest.File, globals ma
if manager.runner.Cancelled {
// In case the cancellation came while a previous state was being
// destroyed.
manager.runner.View.DestroySummary(diags, module.Run, file, module.State)
continue
return
}
_, _, state, diags := manager.runner.execute(module.Run, file, module.Run.Config.ConfigUnderTest, module.State, &terraform.PlanOpts{
_, _, state, diags := manager.runner.execute(manager, module.Run, file, module.Run.Config.ConfigUnderTest, module.State, &terraform.PlanOpts{
Mode: plans.DestroyMode,
}, configs.ApplyTestCommand, globals)
manager.runner.View.DestroySummary(diags, module.Run, file, state)

View File

@ -211,6 +211,19 @@ func TestTest_DoubleInterrupt(t *testing.T) {
t.Errorf("output didn't produce the right output:\n\n%s", output)
}
cleanupMessage := `Terraform was interrupted while executing main.tftest, and may not have performed the expected cleanup operations.
Terraform has already created the following resources from the module under test:
- test_resource.primary
- test_resource.secondary
- test_resource.tertiary`
// It's really important that the above message is printed, so we're testing
// for it specifically and making sure it contains all the resources.
if !strings.Contains(output, cleanupMessage) {
t.Errorf("output didn't produce the right output:\n\n%s", output)
}
// This time the test command shouldn't have cleaned up the resource because
// of the hard interrupt.
if provider.ResourceCount() != 3 {

View File

@ -4,6 +4,7 @@ import (
"fmt"
"path"
"strings"
"time"
"github.com/hashicorp/go-uuid"
"github.com/zclconf/go-cty/cty"
@ -217,6 +218,11 @@ func (provider *TestProvider) ApplyResourceChange(request providers.ApplyResourc
for ix := 0; ix < int(count); ix++ {
provider.Interrupt <- struct{}{}
}
// Wait for a second to make sure the interrupts are processed by
// Terraform before the provider finishes. This is an attempt to ensure
// the output of any tests that rely on this behaviour is deterministic.
time.Sleep(time.Second)
}
provider.Store.Put(provider.GetResourceKey(id.AsString()), resource)

View File

@ -30,11 +30,12 @@ const (
MessageRefreshComplete MessageType = "refresh_complete"
// Test messages
MessageTestAbstract MessageType = "test_abstract"
MessageTestFile MessageType = "test_file"
MessageTestRun MessageType = "test_run"
MessageTestPlan MessageType = "test_plan"
MessageTestState MessageType = "test_state"
MessageTestSummary MessageType = "test_summary"
MessageTestCleanup MessageType = "test_cleanup"
MessageTestAbstract MessageType = "test_abstract"
MessageTestFile MessageType = "test_file"
MessageTestRun MessageType = "test_run"
MessageTestPlan MessageType = "test_plan"
MessageTestState MessageType = "test_state"
MessageTestSummary MessageType = "test_summary"
MessageTestCleanup MessageType = "test_cleanup"
MessageTestInterrupt MessageType = "test_interrupt"
)

View File

@ -38,6 +38,12 @@ type TestFailedResource struct {
DeposedKey string `json:"deposed_key,omitempty"`
}
type TestFatalInterrupt struct {
State []TestFailedResource `json:"state,omitempty"`
States map[string][]TestFailedResource `json:"states,omitempty"`
Planned []string `json:"planned,omitempty"`
}
func ToTestStatus(status moduletest.Status) TestStatus {
return TestStatus(strings.ToLower(status.String()))
}

View File

@ -14,6 +14,7 @@ import (
"github.com/hashicorp/terraform/internal/command/views/json"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/moduletest"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/states/statefile"
"github.com/hashicorp/terraform/internal/terraform"
@ -53,6 +54,16 @@ type Test interface {
// FatalInterrupt prints out a message stating that a hard interrupt has
// been received and testing will stop and cleanup will be skipped.
FatalInterrupt()
// FatalInterruptSummary prints out the resources that were held in state
// and were being created at the time the FatalInterrupt was received.
//
// This will typically be called in place of DestroySummary, as there is no
// guarantee that this function will be called during a FatalInterrupt. In
// addition, this function prints additional details about the current
// operation alongside the current state as the state will be missing newly
// created resources that also need to be handled manually.
FatalInterruptSummary(run *moduletest.Run, file *moduletest.File, states map[*moduletest.Run]*states.State, created []*plans.ResourceInstanceChangeSrc)
}
func NewTest(vt arguments.ViewType, view *View) Test {
@ -221,11 +232,65 @@ func (t *TestHuman) Diagnostics(_ *moduletest.Run, _ *moduletest.File, diags tfd
}
func (t *TestHuman) Interrupted() {
t.view.streams.Print(interrupted)
t.view.streams.Eprint(interrupted)
}
func (t *TestHuman) FatalInterrupt() {
t.view.streams.Print(fatalInterrupt)
t.view.streams.Eprint(fatalInterrupt)
}
func (t *TestHuman) FatalInterruptSummary(run *moduletest.Run, file *moduletest.File, existingStates map[*moduletest.Run]*states.State, created []*plans.ResourceInstanceChangeSrc) {
t.view.streams.Eprintf("\nTerraform was interrupted while executing %s, and may not have performed the expected cleanup operations.\n", file.Name)
for run, state := range existingStates {
if state.Empty() {
// Then it's fine, don't worry about it.
continue
}
if run == nil {
// Then this is just the main state for the whole file.
t.view.streams.Eprintln("\nTerraform has already created the following resources from the module under test:")
for _, resource := range state.AllResourceInstanceObjectAddrs() {
if resource.DeposedKey != states.NotDeposed {
t.view.streams.Eprintf(" - %s (%s)\n", resource.Instance, resource.DeposedKey)
continue
}
t.view.streams.Eprintf(" - %s\n", resource.Instance)
}
} else {
t.view.streams.Eprintf("\nTerraform has already created the following resources for %s from %s:\n", run.Name, run.Config.Module.Source)
for _, resource := range state.AllResourceInstanceObjectAddrs() {
if resource.DeposedKey != states.NotDeposed {
t.view.streams.Eprintf(" - %s (%s)\n", resource.Instance, resource.DeposedKey)
continue
}
t.view.streams.Eprintf(" - %s\n", resource.Instance)
}
}
}
if len(created) == 0 {
// No planned changes, so we won't print anything.
return
}
var resources []string
for _, change := range created {
resources = append(resources, change.Addr.String())
}
if len(resources) > 0 {
module := "the module under test"
if run.Config.ConfigUnderTest != nil {
module = run.Config.Module.Source.String()
}
t.view.streams.Eprintf("\nTerraform was in the process of creating the following resources for %s from %s, and they may not have been destroyed:\n", run.Name, module)
for _, resource := range resources {
t.view.streams.Eprintf(" - %s\n", resource)
}
}
}
type TestJSON struct {
@ -422,6 +487,50 @@ func (t *TestJSON) FatalInterrupt() {
t.view.Log(fatalInterrupt)
}
func (t *TestJSON) FatalInterruptSummary(run *moduletest.Run, file *moduletest.File, existingStates map[*moduletest.Run]*states.State, created []*plans.ResourceInstanceChangeSrc) {
message := json.TestFatalInterrupt{
States: make(map[string][]json.TestFailedResource),
}
for run, state := range existingStates {
if state.Empty() {
continue
}
var resources []json.TestFailedResource
for _, resource := range state.AllResourceInstanceObjectAddrs() {
resources = append(resources, json.TestFailedResource{
Instance: resource.Instance.String(),
DeposedKey: resource.DeposedKey.String(),
})
}
if run == nil {
message.State = resources
} else {
message.States[run.Name] = resources
}
}
if len(created) > 0 {
for _, change := range created {
message.Planned = append(message.Planned, change.Addr.String())
}
}
if len(message.States) == 0 && len(message.State) == 0 && len(message.Planned) == 0 {
// Then we don't have any information to share with the user.
return
}
t.view.log.Error(
"Terraform was interrupted during test execution, and may not have performed the expected cleanup operations.",
"type", json.MessageTestInterrupt,
json.MessageTestInterrupt, message,
"@testfile", file.Name)
}
func colorizeTestStatus(status moduletest.Status, color *colorstring.Colorize) string {
switch status {
case moduletest.Error, moduletest.Fail:

View File

@ -939,6 +939,285 @@ Terraform left the following resources in state after executing main.tftest, the
}
}
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: `
Terraform was interrupted while executing main.tftest, and may not have performed the expected cleanup operations.
Terraform 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{})
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{})
}),
},
created: nil,
want: `
Terraform was interrupted while executing main.tftest, and may not have performed the expected cleanup operations.
Terraform 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{})
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{})
}),
},
created: nil,
want: `
Terraform was interrupted while executing main.tftest, and may not have performed the expected cleanup operations.
Terraform 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{})
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{})
}),
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{})
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{})
}),
},
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: `
Terraform was interrupted while executing main.tftest, and may not have performed the expected cleanup operations.
Terraform has already created the following resources for setup_block from ../setup:
- test_instance.setup_one
- test_instance.setup_two
Terraform has already created the following resources from the module under test:
- test_instance.one
- test_instance.two
Terraform 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"}
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
@ -2462,6 +2741,312 @@ func TestTestJSON_Run(t *testing.T) {
}
}
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": "Terraform was interrupted during test execution, and may not have performed the expected cleanup operations.",
"@module": "terraform.ui",
"@testfile": "main.tftest",
"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{})
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{})
}),
},
changes: nil,
want: []map[string]interface{}{
{
"@level": "error",
"@message": "Terraform was interrupted during test execution, and may not have performed the expected cleanup operations.",
"@module": "terraform.ui",
"@testfile": "main.tftest",
"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{})
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{})
}),
},
changes: nil,
want: []map[string]interface{}{
{
"@level": "error",
"@message": "Terraform was interrupted during test execution, and may not have performed the expected cleanup operations.",
"@module": "terraform.ui",
"@testfile": "main.tftest",
"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{})
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{})
}),
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{})
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{})
}),
},
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": "Terraform was interrupted during test execution, and may not have performed the expected cleanup operations.",
"@module": "terraform.ui",
"@testfile": "main.tftest",
"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"}
run := &moduletest.Run{Name: "run_block"}
view.FatalInterruptSummary(run, file, tc.states, tc.changes)
testJSONViewOutputEquals(t, done(t).All(), tc.want)
})
}
}
func dynamicValue(t *testing.T, value cty.Value, typ cty.Type) plans.DynamicValue {
d, err := plans.NewDynamicValue(value, typ)
if err != nil {