| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package views | |
| import ( | |
| "bytes" | |
| "fmt" | |
| "net/http" | |
| "time" | |
| "github.com/hashicorp/go-tfe" | |
| "github.com/mitchellh/colorstring" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/command/arguments" | |
| "github.com/hashicorp/terraform/internal/command/format" | |
| "github.com/hashicorp/terraform/internal/command/jsonformat" | |
| "github.com/hashicorp/terraform/internal/command/jsonplan" | |
| "github.com/hashicorp/terraform/internal/command/jsonprovider" | |
| "github.com/hashicorp/terraform/internal/command/jsonstate" | |
| "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" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| ) | |
| // Test renders outputs for test executions. | |
| type Test interface { | |
| // Abstract should print an early summary of the tests that will be | |
| // executed. This will be called before the tests have been executed so | |
| // the status for everything within suite will be test.Pending. | |
| // | |
| // This should be used to state what is going to be tested. | |
| Abstract(suite *moduletest.Suite) | |
| // Conclusion should print out a summary of the tests including their | |
| // completed status. | |
| Conclusion(suite *moduletest.Suite) | |
| // File prints out the summary for an entire test file. | |
| File(file *moduletest.File, progress moduletest.Progress) | |
| // Run prints out the summary for a single test run block. | |
| Run(run *moduletest.Run, file *moduletest.File, progress moduletest.Progress, elapsed int64) | |
| // DestroySummary prints out the summary of the destroy step of each test | |
| // file. If everything goes well, this should be empty. | |
| DestroySummary(diags tfdiags.Diagnostics, run *moduletest.Run, file *moduletest.File, state *states.State) | |
| // Diagnostics prints out the provided diagnostics. | |
| Diagnostics(run *moduletest.Run, file *moduletest.File, diags tfdiags.Diagnostics) | |
| // Interrupted prints out a message stating that an interrupt has been | |
| // received and testing will stop. | |
| Interrupted() | |
| // 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[string]*states.State, created []*plans.ResourceInstanceChangeSrc) | |
| // TFCStatusUpdate prints a reassuring update, letting users know the latest | |
| // status of their ongoing remote test run. | |
| TFCStatusUpdate(status tfe.TestRunStatus, elapsed time.Duration) | |
| // TFCRetryHook prints an update if a request failed and is being retried. | |
| TFCRetryHook(attemptNum int, resp *http.Response) | |
| } | |
| func NewTest(vt arguments.ViewType, view *View) Test { | |
| switch vt { | |
| case arguments.ViewJSON: | |
| return &TestJSON{ | |
| view: NewJSONView(view), | |
| } | |
| case arguments.ViewHuman: | |
| return &TestHuman{ | |
| view: view, | |
| } | |
| default: | |
| panic(fmt.Sprintf("unknown view type %v", vt)) | |
| } | |
| } | |
| type TestHuman struct { | |
| CloudHooks | |
| view *View | |
| } | |
| var _ Test = (*TestHuman)(nil) | |
| func (t *TestHuman) Abstract(_ *moduletest.Suite) { | |
| // Do nothing, we don't print an abstract for the human view. | |
| } | |
| func (t *TestHuman) Conclusion(suite *moduletest.Suite) { | |
| t.view.streams.Println() | |
| counts := make(map[moduletest.Status]int) | |
| for _, file := range suite.Files { | |
| for _, run := range file.Runs { | |
| count := counts[run.Status] | |
| counts[run.Status] = count + 1 | |
| } | |
| } | |
| if suite.Status <= moduletest.Skip { | |
| // Then no tests. | |
| t.view.streams.Print("Executed 0 tests") | |
| if counts[moduletest.Skip] > 0 { | |
| t.view.streams.Printf(", %d skipped.\n", counts[moduletest.Skip]) | |
| } else { | |
| t.view.streams.Println(".") | |
| } | |
| return | |
| } | |
| if suite.Status == moduletest.Pass { | |
| t.view.streams.Print(t.view.colorize.Color("[green]Success![reset]")) | |
| } else { | |
| t.view.streams.Print(t.view.colorize.Color("[red]Failure![reset]")) | |
| } | |
| if suite.CommandMode != moduletest.CleanupMode { | |
| t.view.streams.Printf(" %d passed, %d failed", counts[moduletest.Pass], counts[moduletest.Fail]+counts[moduletest.Error]) | |
| if counts[moduletest.Skip] > 0 { | |
| t.view.streams.Printf(", %d skipped.\n", counts[moduletest.Skip]) | |
| } else { | |
| t.view.streams.Println(".") | |
| } | |
| } else { | |
| t.view.streams.Println() | |
| } | |
| } | |
| func (t *TestHuman) File(file *moduletest.File, progress moduletest.Progress) { | |
| switch progress { | |
| case moduletest.Starting, moduletest.Running: | |
| t.view.streams.Printf(t.view.colorize.Color("%s... [light_gray]in progress[reset]\n"), file.Name) | |
| case moduletest.TearDown: | |
| t.view.streams.Printf(t.view.colorize.Color("%s... [light_gray]tearing down[reset]\n"), file.Name) | |
| case moduletest.Complete: | |
| t.view.streams.Printf("%s... %s\n", file.Name, colorizeTestStatus(file.Status, t.view.colorize)) | |
| t.Diagnostics(nil, file, file.Diagnostics) | |
| default: | |
| panic("unrecognized test progress: " + progress.String()) | |
| } | |
| } | |
| func (t *TestHuman) Run(run *moduletest.Run, file *moduletest.File, progress moduletest.Progress, _ int64) { | |
| switch progress { | |
| case moduletest.Starting, moduletest.Running, moduletest.TearDown: | |
| return // We don't print progress updates in human mode | |
| case moduletest.Complete: | |
| // Do nothing, the rest of the function handles this. | |
| default: | |
| panic("unrecognized test progress: " + progress.String()) | |
| } | |
| t.view.streams.Printf(" run %q... %s\n", run.Name, colorizeTestStatus(run.Status, t.view.colorize)) | |
| if run.Verbose != nil { | |
| // We're going to be more verbose about what we print, here's the plan | |
| // or the state depending on the type of run we did. | |
| schemas := &terraform.Schemas{ | |
| Providers: run.Verbose.Providers, | |
| Provisioners: run.Verbose.Provisioners, | |
| } | |
| renderer := jsonformat.Renderer{ | |
| Streams: t.view.streams, | |
| Colorize: t.view.colorize, | |
| RunningInAutomation: t.view.runningInAutomation, | |
| } | |
| if run.Config.Command == configs.ApplyTestCommand { | |
| // Then we'll print the state. | |
| root, outputs, err := jsonstate.MarshalForRenderer(statefile.New(run.Verbose.State, file.Name, uint64(run.Index)), schemas) | |
| if err != nil { | |
| run.Diagnostics = run.Diagnostics.Append(tfdiags.Sourceless( | |
| tfdiags.Warning, | |
| "Failed to render test state", | |
| fmt.Sprintf("Terraform could not marshal the state for display: %v", err))) | |
| } else { | |
| state := jsonformat.State{ | |
| StateFormatVersion: jsonstate.FormatVersion, | |
| ProviderFormatVersion: jsonprovider.FormatVersion, | |
| RootModule: root, | |
| RootModuleOutputs: outputs, | |
| ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), | |
| } | |
| t.view.streams.Println() // Separate the state from any previous statements. | |
| renderer.RenderHumanState(state) | |
| t.view.streams.Println() // Separate the state from any future statements. | |
| } | |
| } else { | |
| // We'll print the plan. | |
| outputs, changed, drift, attrs, actions, err := jsonplan.MarshalForRenderer(run.Verbose.Plan, schemas) | |
| if err != nil { | |
| run.Diagnostics = run.Diagnostics.Append(tfdiags.Sourceless( | |
| tfdiags.Warning, | |
| "Failed to render test plan", | |
| fmt.Sprintf("Terraform could not marshal the plan for display: %v", err))) | |
| } else { | |
| plan := jsonformat.Plan{ | |
| PlanFormatVersion: jsonplan.FormatVersion, | |
| ProviderFormatVersion: jsonprovider.FormatVersion, | |
| OutputChanges: outputs, | |
| ResourceChanges: changed, | |
| ResourceDrift: drift, | |
| ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), | |
| RelevantAttributes: attrs, | |
| ActionInvocations: actions, | |
| } | |
| var opts []plans.Quality | |
| if run.Verbose.Plan.Errored { | |
| opts = append(opts, plans.Errored) | |
| } else if !run.Verbose.Plan.Applyable { | |
| opts = append(opts, plans.NoChanges) | |
| } | |
| renderer.RenderHumanPlan(plan, run.Verbose.Plan.UIMode, opts...) | |
| t.view.streams.Println() // Separate the plan from any future statements. | |
| } | |
| } | |
| } | |
| // Finally we'll print out a summary of the diagnostics from the run. | |
| t.Diagnostics(run, file, run.Diagnostics) | |
| var warnings bool | |
| for _, diag := range run.Diagnostics { | |
| switch diag.Severity() { | |
| case tfdiags.Error: | |
| // do nothing | |
| case tfdiags.Warning: | |
| warnings = true | |
| } | |
| if warnings { | |
| // We only care about checking if we printed any warnings in the | |
| // previous output. | |
| break | |
| } | |
| } | |
| if warnings { | |
| // warnings are printed to stdout, so we'll put a new line into stdout | |
| // to separate any future statements info statements. | |
| t.view.streams.Println() | |
| } | |
| } | |
| func (t *TestHuman) DestroySummary(diags tfdiags.Diagnostics, run *moduletest.Run, file *moduletest.File, state *states.State) { | |
| identifier := file.Name | |
| if run != nil { | |
| identifier = fmt.Sprintf("%s/%s", identifier, run.Name) | |
| } | |
| if diags.HasErrors() { | |
| t.view.streams.Eprint(format.WordWrap(fmt.Sprintf("Terraform encountered an error destroying resources created while executing %s.\n", identifier), t.view.errorColumns())) | |
| } | |
| t.Diagnostics(run, file, diags) | |
| skipCleanup := run != nil && run.Config.SkipCleanup | |
| if state.HasManagedResourceInstanceObjects() { | |
| if skipCleanup { | |
| t.view.streams.Print(format.WordWrap(fmt.Sprintf("\nTerraform left the following resources in state after executing %s because the skip_cleanup attribute was set:\n", identifier), t.view.outputColumns())) | |
| for _, resource := range addrs.SetSortedNatural(state.AllManagedResourceInstanceObjectAddrs()) { | |
| if resource.DeposedKey != states.NotDeposed { | |
| t.view.streams.Printf(" - %s (%s)\n", resource.ResourceInstance, resource.DeposedKey) | |
| continue | |
| } | |
| t.view.streams.Printf(" - %s\n", resource.ResourceInstance) | |
| } | |
| } else { | |
| // FIXME: This message says "resources" but this is actually a list | |
| // of resource instance objects. | |
| t.view.streams.Eprint(format.WordWrap(fmt.Sprintf("\nTerraform left the following resources in state after executing %s, and they need to be cleaned up manually:\n", identifier), t.view.errorColumns())) | |
| for _, resource := range addrs.SetSortedNatural(state.AllManagedResourceInstanceObjectAddrs()) { | |
| if resource.DeposedKey != states.NotDeposed { | |
| t.view.streams.Eprintf(" - %s (%s)\n", resource.ResourceInstance, resource.DeposedKey) | |
| continue | |
| } | |
| t.view.streams.Eprintf(" - %s\n", resource.ResourceInstance) | |
| } | |
| } | |
| } | |
| } | |
| func (t *TestHuman) Diagnostics(_ *moduletest.Run, _ *moduletest.File, diags tfdiags.Diagnostics) { | |
| t.view.Diagnostics(diags) | |
| } | |
| func (t *TestHuman) Interrupted() { | |
| t.view.streams.Eprintln(format.WordWrap(interrupted, t.view.errorColumns())) | |
| } | |
| func (t *TestHuman) FatalInterrupt() { | |
| t.view.streams.Eprintln(format.WordWrap(fatalInterrupt, t.view.errorColumns())) | |
| } | |
| func (t *TestHuman) FatalInterruptSummary(run *moduletest.Run, file *moduletest.File, existingStates map[string]*states.State, created []*plans.ResourceInstanceChangeSrc) { | |
| t.view.streams.Eprint(format.WordWrap(fmt.Sprintf("\nTerraform was interrupted while executing %s, and may not have performed the expected cleanup operations.\n", file.Name), t.view.errorColumns())) | |
| // Print out the main state first, this is the state that isn't associated | |
| // with a run block. | |
| if state, exists := existingStates[configs.TestMainStateIdentifier]; exists && !state.Empty() { | |
| t.view.streams.Eprint(format.WordWrap("\nTerraform has already created the following resources from the module under test:\n", t.view.errorColumns())) | |
| for _, resource := range addrs.SetSortedNatural(state.AllManagedResourceInstanceObjectAddrs()) { | |
| if resource.DeposedKey != states.NotDeposed { | |
| t.view.streams.Eprintf(" - %s (%s)\n", resource.ResourceInstance, resource.DeposedKey) | |
| continue | |
| } | |
| t.view.streams.Eprintf(" - %s\n", resource.ResourceInstance) | |
| } | |
| } | |
| for key, state := range existingStates { | |
| if key == configs.TestMainStateIdentifier || state.Empty() { | |
| continue | |
| } | |
| t.view.streams.Eprint(format.WordWrap(fmt.Sprintf("\nTerraform has already created the following resources for %q:\n", key), t.view.errorColumns())) | |
| for _, resource := range addrs.SetSortedNatural(state.AllManagedResourceInstanceObjectAddrs()) { | |
| if resource.DeposedKey != states.NotDeposed { | |
| t.view.streams.Eprintf(" - %s (%s)\n", resource.ResourceInstance, resource.DeposedKey) | |
| continue | |
| } | |
| t.view.streams.Eprintf(" - %s\n", resource.ResourceInstance) | |
| } | |
| } | |
| 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 = fmt.Sprintf("%q", run.Config.Module.Source.String()) | |
| } | |
| t.view.streams.Eprint(format.WordWrap(fmt.Sprintf("\nTerraform was in the process of creating the following resources for %q from %s, and they may not have been destroyed:\n", run.Name, module), t.view.errorColumns())) | |
| for _, resource := range resources { | |
| t.view.streams.Eprintf(" - %s\n", resource) | |
| } | |
| } | |
| } | |
| func (t *TestHuman) TFCStatusUpdate(status tfe.TestRunStatus, elapsed time.Duration) { | |
| switch status { | |
| case tfe.TestRunQueued: | |
| t.view.streams.Printf("Waiting for the tests to start... (%s elapsed)\n", elapsed.Truncate(30*time.Second)) | |
| case tfe.TestRunRunning: | |
| t.view.streams.Printf("Waiting for the tests to complete... (%s elapsed)\n", elapsed.Truncate(30*time.Second)) | |
| } | |
| } | |
| func (t *TestHuman) TFCRetryHook(attemptNum int, resp *http.Response) { | |
| t.view.streams.Println(t.view.colorize.Color(t.RetryLogHook(attemptNum, resp, true))) | |
| } | |
| type TestJSON struct { | |
| CloudHooks | |
| view *JSONView | |
| } | |
| var _ Test = (*TestJSON)(nil) | |
| func (t *TestJSON) Abstract(suite *moduletest.Suite) { | |
| var fileCount, runCount int | |
| abstract := json.TestSuiteAbstract{} | |
| for name, file := range suite.Files { | |
| fileCount++ | |
| var runs []string | |
| for _, run := range file.Runs { | |
| runCount++ | |
| runs = append(runs, run.Name) | |
| } | |
| abstract[name] = runs | |
| } | |
| files := "files" | |
| runs := "run blocks" | |
| if fileCount == 1 { | |
| files = "file" | |
| } | |
| if runCount == 1 { | |
| runs = "run block" | |
| } | |
| t.view.log.Info( | |
| fmt.Sprintf("Found %d %s and %d %s", fileCount, files, runCount, runs), | |
| "type", json.MessageTestAbstract, | |
| json.MessageTestAbstract, abstract) | |
| } | |
| func (t *TestJSON) Conclusion(suite *moduletest.Suite) { | |
| summary := json.TestSuiteSummary{ | |
| Status: json.ToTestStatus(suite.Status), | |
| } | |
| for _, file := range suite.Files { | |
| for _, run := range file.Runs { | |
| switch run.Status { | |
| case moduletest.Skip: | |
| summary.Skipped++ | |
| case moduletest.Pass: | |
| summary.Passed++ | |
| case moduletest.Error: | |
| summary.Errored++ | |
| case moduletest.Fail: | |
| summary.Failed++ | |
| } | |
| } | |
| } | |
| var message bytes.Buffer | |
| if suite.Status <= moduletest.Skip { | |
| // Then no tests. | |
| message.WriteString("Executed 0 tests") | |
| if summary.Skipped > 0 { | |
| message.WriteString(fmt.Sprintf(", %d skipped.", summary.Skipped)) | |
| } else { | |
| message.WriteString(".") | |
| } | |
| } else { | |
| if suite.Status == moduletest.Pass { | |
| message.WriteString("Success!") | |
| } else { | |
| message.WriteString("Failure!") | |
| } | |
| if suite.CommandMode != moduletest.CleanupMode { | |
| // don't print test summaries during cleanup mode. | |
| message.WriteString(fmt.Sprintf(" %d passed, %d failed", summary.Passed, summary.Failed+summary.Errored)) | |
| if summary.Skipped > 0 { | |
| message.WriteString(fmt.Sprintf(", %d skipped.", summary.Skipped)) | |
| } else { | |
| message.WriteString(".") | |
| } | |
| } | |
| } | |
| t.view.log.Info( | |
| message.String(), | |
| "type", json.MessageTestSummary, | |
| json.MessageTestSummary, summary) | |
| } | |
| func (t *TestJSON) File(file *moduletest.File, progress moduletest.Progress) { | |
| switch progress { | |
| case moduletest.Starting, moduletest.Running: | |
| t.view.log.Info( | |
| fmt.Sprintf("%s... in progress", file.Name), | |
| "type", json.MessageTestFile, | |
| json.MessageTestFile, json.TestFileStatus{ | |
| Path: file.Name, | |
| Progress: json.ToTestProgress(moduletest.Starting), | |
| }, | |
| "@testfile", file.Name) | |
| case moduletest.TearDown: | |
| t.view.log.Info( | |
| fmt.Sprintf("%s... tearing down", file.Name), | |
| "type", json.MessageTestFile, | |
| json.MessageTestFile, json.TestFileStatus{ | |
| Path: file.Name, | |
| Progress: json.ToTestProgress(moduletest.TearDown), | |
| }, | |
| "@testfile", file.Name) | |
| case moduletest.Complete: | |
| t.view.log.Info( | |
| fmt.Sprintf("%s... %s", file.Name, testStatus(file.Status)), | |
| "type", json.MessageTestFile, | |
| json.MessageTestFile, json.TestFileStatus{ | |
| Path: file.Name, | |
| Progress: json.ToTestProgress(moduletest.Complete), | |
| Status: json.ToTestStatus(file.Status), | |
| }, | |
| "@testfile", file.Name) | |
| t.Diagnostics(nil, file, file.Diagnostics) | |
| default: | |
| panic("unrecognized test progress: " + progress.String()) | |
| } | |
| } | |
| func (t *TestJSON) Run(run *moduletest.Run, file *moduletest.File, progress moduletest.Progress, elapsed int64) { | |
| switch progress { | |
| case moduletest.Starting, moduletest.Running: | |
| t.view.log.Info( | |
| fmt.Sprintf(" %q... in progress", run.Name), | |
| "type", json.MessageTestRun, | |
| json.MessageTestRun, json.TestRunStatus{ | |
| Path: file.Name, | |
| Run: run.Name, | |
| Progress: json.ToTestProgress(progress), | |
| Elapsed: &elapsed, | |
| }, | |
| "@testfile", file.Name, | |
| "@testrun", run.Name) | |
| return | |
| case moduletest.TearDown: | |
| t.view.log.Info( | |
| fmt.Sprintf(" %q... tearing down", run.Name), | |
| "type", json.MessageTestRun, | |
| json.MessageTestRun, json.TestRunStatus{ | |
| Path: file.Name, | |
| Run: run.Name, | |
| Progress: json.ToTestProgress(progress), | |
| Elapsed: &elapsed, | |
| }, | |
| "@testfile", file.Name, | |
| "@testrun", run.Name) | |
| return | |
| case moduletest.Complete: | |
| // Do nothing, the rest of the function handles this case. | |
| default: | |
| panic("unrecognized test progress: " + progress.String()) | |
| } | |
| t.view.log.Info( | |
| fmt.Sprintf(" %q... %s", run.Name, testStatus(run.Status)), | |
| "type", json.MessageTestRun, | |
| json.MessageTestRun, json.TestRunStatus{ | |
| Path: file.Name, | |
| Run: run.Name, | |
| Progress: json.ToTestProgress(progress), | |
| Status: json.ToTestStatus(run.Status)}, | |
| "@testfile", file.Name, | |
| "@testrun", run.Name) | |
| if run.Verbose != nil { | |
| schemas := &terraform.Schemas{ | |
| Providers: run.Verbose.Providers, | |
| Provisioners: run.Verbose.Provisioners, | |
| } | |
| if run.Config.Command == configs.ApplyTestCommand { | |
| // Then we'll print the state. | |
| root, outputs, err := jsonstate.MarshalForRenderer(statefile.New(run.Verbose.State, file.Name, uint64(run.Index)), schemas) | |
| if err != nil { | |
| run.Diagnostics = run.Diagnostics.Append(tfdiags.Sourceless( | |
| tfdiags.Warning, | |
| "Failed to render test state", | |
| fmt.Sprintf("Terraform could not marshal the state for display: %v", err))) | |
| } else { | |
| state := jsonformat.State{ | |
| StateFormatVersion: jsonstate.FormatVersion, | |
| ProviderFormatVersion: jsonprovider.FormatVersion, | |
| RootModule: root, | |
| RootModuleOutputs: outputs, | |
| ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), | |
| } | |
| t.view.log.Info( | |
| "-verbose flag enabled, printing state", | |
| "type", json.MessageTestState, | |
| json.MessageTestState, state, | |
| "@testfile", file.Name, | |
| "@testrun", run.Name) | |
| } | |
| } else { | |
| outputs, changed, drift, attrs, actions, err := jsonplan.MarshalForRenderer(run.Verbose.Plan, schemas) | |
| if err != nil { | |
| run.Diagnostics = run.Diagnostics.Append(tfdiags.Sourceless( | |
| tfdiags.Warning, | |
| "Failed to render test plan", | |
| fmt.Sprintf("Terraform could not marshal the plan for display: %v", err))) | |
| } else { | |
| plan := jsonformat.Plan{ | |
| PlanFormatVersion: jsonplan.FormatVersion, | |
| ProviderFormatVersion: jsonprovider.FormatVersion, | |
| OutputChanges: outputs, | |
| ResourceChanges: changed, | |
| ResourceDrift: drift, | |
| ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), | |
| RelevantAttributes: attrs, | |
| ActionInvocations: actions, | |
| } | |
| t.view.log.Info( | |
| "-verbose flag enabled, printing plan", | |
| "type", json.MessageTestPlan, | |
| json.MessageTestPlan, plan, | |
| "@testfile", file.Name, | |
| "@testrun", run.Name) | |
| } | |
| } | |
| } | |
| t.Diagnostics(run, file, run.Diagnostics) | |
| } | |
| func (t *TestJSON) DestroySummary(diags tfdiags.Diagnostics, run *moduletest.Run, file *moduletest.File, state *states.State) { | |
| skipCleanup := run != nil && run.Config.SkipCleanup | |
| if state.HasManagedResourceInstanceObjects() { | |
| if skipCleanup { | |
| cleanup := json.TestFileCleanupSkipped{} | |
| for _, resource := range addrs.SetSortedNatural(state.AllManagedResourceInstanceObjectAddrs()) { | |
| cleanup.SkippedResources = append(cleanup.SkippedResources, json.TestFailedResource{ | |
| Instance: resource.ResourceInstance.String(), | |
| DeposedKey: resource.DeposedKey.String(), | |
| }) | |
| } | |
| if run != nil { | |
| t.view.log.Info( | |
| fmt.Sprintf("Terraform left some resources in state after executing %s/%s because the skip_cleanup attribute was set.", file.Name, run.Name), | |
| "type", json.MessageTestCleanup, | |
| json.MessageTestCleanup, cleanup, | |
| "@testfile", file.Name, | |
| "@testrun", run.Name) | |
| } else { | |
| t.view.log.Info( | |
| fmt.Sprintf("Terraform left some resources in state after executing %s because the skip_cleanup attribute was set.", file.Name), | |
| "type", json.MessageTestCleanup, | |
| json.MessageTestCleanup, cleanup, | |
| "@testfile", file.Name) | |
| } | |
| } else { | |
| cleanup := json.TestFileCleanup{} | |
| for _, resource := range addrs.SetSortedNatural(state.AllManagedResourceInstanceObjectAddrs()) { | |
| cleanup.FailedResources = append(cleanup.FailedResources, json.TestFailedResource{ | |
| Instance: resource.ResourceInstance.String(), | |
| DeposedKey: resource.DeposedKey.String(), | |
| }) | |
| } | |
| if run != nil { | |
| t.view.log.Error( | |
| fmt.Sprintf("Terraform left some resources in state after executing %s/%s, they need to be cleaned up manually.", file.Name, run.Name), | |
| "type", json.MessageTestCleanup, | |
| json.MessageTestCleanup, cleanup, | |
| "@testfile", file.Name, | |
| "@testrun", run.Name) | |
| } else { | |
| t.view.log.Error( | |
| fmt.Sprintf("Terraform left some resources in state after executing %s, they need to be cleaned up manually.", file.Name), | |
| "type", json.MessageTestCleanup, | |
| json.MessageTestCleanup, cleanup, | |
| "@testfile", file.Name) | |
| } | |
| } | |
| } | |
| t.Diagnostics(run, file, diags) | |
| } | |
| func (t *TestJSON) Diagnostics(run *moduletest.Run, file *moduletest.File, diags tfdiags.Diagnostics) { | |
| var metadata []interface{} | |
| if file != nil { | |
| metadata = append(metadata, "@testfile", file.Name) | |
| } | |
| if run != nil { | |
| metadata = append(metadata, "@testrun", run.Name) | |
| } | |
| t.view.Diagnostics(diags, metadata...) | |
| } | |
| func (t *TestJSON) Interrupted() { | |
| t.view.Log(interrupted) | |
| } | |
| func (t *TestJSON) FatalInterrupt() { | |
| t.view.Log(fatalInterrupt) | |
| } | |
| func (t *TestJSON) FatalInterruptSummary(run *moduletest.Run, file *moduletest.File, existingStates map[string]*states.State, created []*plans.ResourceInstanceChangeSrc) { | |
| message := json.TestFatalInterrupt{ | |
| States: make(map[string][]json.TestFailedResource), | |
| } | |
| for key, state := range existingStates { | |
| if state.Empty() { | |
| continue | |
| } | |
| var resources []json.TestFailedResource | |
| for _, resource := range addrs.SetSortedNatural(state.AllManagedResourceInstanceObjectAddrs()) { | |
| resources = append(resources, json.TestFailedResource{ | |
| Instance: resource.ResourceInstance.String(), | |
| DeposedKey: resource.DeposedKey.String(), | |
| }) | |
| } | |
| if key == configs.TestMainStateIdentifier { | |
| message.State = resources | |
| } else { | |
| message.States[key] = 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 | |
| } | |
| if run != nil { | |
| 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, | |
| "@testrun", run.Name) | |
| } else { | |
| 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 (t *TestJSON) TFCStatusUpdate(status tfe.TestRunStatus, elapsed time.Duration) { | |
| var message string | |
| switch status { | |
| case tfe.TestRunQueued: | |
| message = fmt.Sprintf("Waiting for the tests to start... (%s elapsed)\n", elapsed.Truncate(30*time.Second)) | |
| case tfe.TestRunRunning: | |
| message = fmt.Sprintf("Waiting for the tests to complete... (%s elapsed)\n", elapsed.Truncate(30*time.Second)) | |
| default: | |
| // Don't care about updates for other statuses. | |
| return | |
| } | |
| t.view.log.Info( | |
| message, | |
| "type", json.MessageTestStatus, | |
| json.MessageTestStatus, json.TestStatusUpdate{ | |
| Status: string(status), | |
| Duration: elapsed.Seconds(), | |
| }) | |
| } | |
| func (t *TestJSON) TFCRetryHook(attemptNum int, resp *http.Response) { | |
| t.view.log.Error( | |
| t.RetryLogHook(attemptNum, resp, false), | |
| "type", json.MessageTestRetry, | |
| ) | |
| } | |
| func colorizeTestStatus(status moduletest.Status, color *colorstring.Colorize) string { | |
| switch status { | |
| case moduletest.Error, moduletest.Fail: | |
| return color.Color("[red]fail[reset]") | |
| case moduletest.Pass: | |
| return color.Color("[green]pass[reset]") | |
| case moduletest.Skip: | |
| return color.Color("[light_gray]skip[reset]") | |
| case moduletest.Pending: | |
| return color.Color("[light_gray]pending[reset]") | |
| default: | |
| panic("unrecognized status: " + status.String()) | |
| } | |
| } | |
| func testStatus(status moduletest.Status) string { | |
| switch status { | |
| case moduletest.Error, moduletest.Fail: | |
| return "fail" | |
| case moduletest.Pass: | |
| return "pass" | |
| case moduletest.Skip: | |
| return "skip" | |
| case moduletest.Pending: | |
| return "pending" | |
| default: | |
| panic("unrecognized status: " + status.String()) | |
| } | |
| } |