github.com/opentofu/opentofu@v1.7.1/internal/command/views/test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package views
     7  
     8  import (
     9  	"bytes"
    10  	"fmt"
    11  
    12  	"github.com/mitchellh/colorstring"
    13  
    14  	"github.com/opentofu/opentofu/internal/command/arguments"
    15  	"github.com/opentofu/opentofu/internal/command/format"
    16  	"github.com/opentofu/opentofu/internal/command/jsonformat"
    17  	"github.com/opentofu/opentofu/internal/command/jsonplan"
    18  	"github.com/opentofu/opentofu/internal/command/jsonprovider"
    19  	"github.com/opentofu/opentofu/internal/command/jsonstate"
    20  	"github.com/opentofu/opentofu/internal/command/views/json"
    21  	"github.com/opentofu/opentofu/internal/configs"
    22  	"github.com/opentofu/opentofu/internal/encryption"
    23  	"github.com/opentofu/opentofu/internal/moduletest"
    24  	"github.com/opentofu/opentofu/internal/plans"
    25  	"github.com/opentofu/opentofu/internal/states"
    26  	"github.com/opentofu/opentofu/internal/states/statefile"
    27  	"github.com/opentofu/opentofu/internal/states/statemgr"
    28  	"github.com/opentofu/opentofu/internal/tfdiags"
    29  	"github.com/opentofu/opentofu/internal/tofu"
    30  )
    31  
    32  // Test renders outputs for test executions.
    33  type Test interface {
    34  	// Abstract should print an early summary of the tests that will be
    35  	// executed. This will be called before the tests have been executed so
    36  	// the status for everything within suite will be test.Pending.
    37  	//
    38  	// This should be used to state what is going to be tested.
    39  	Abstract(suite *moduletest.Suite)
    40  
    41  	// Conclusion should print out a summary of the tests including their
    42  	// completed status.
    43  	Conclusion(suite *moduletest.Suite)
    44  
    45  	// File prints out the summary for an entire test file.
    46  	File(file *moduletest.File)
    47  
    48  	// Run prints out the summary for a single test run block.
    49  	Run(run *moduletest.Run, file *moduletest.File)
    50  
    51  	// DestroySummary prints out the summary of the destroy step of each test
    52  	// file. If everything goes well, this should be empty.
    53  	DestroySummary(diags tfdiags.Diagnostics, run *moduletest.Run, file *moduletest.File, state *states.State)
    54  
    55  	// Diagnostics prints out the provided diagnostics.
    56  	Diagnostics(run *moduletest.Run, file *moduletest.File, diags tfdiags.Diagnostics)
    57  
    58  	// Interrupted prints out a message stating that an interrupt has been
    59  	// received and testing will stop.
    60  	Interrupted()
    61  
    62  	// FatalInterrupt prints out a message stating that a hard interrupt has
    63  	// been received and testing will stop and cleanup will be skipped.
    64  	FatalInterrupt()
    65  
    66  	// FatalInterruptSummary prints out the resources that were held in state
    67  	// and were being created at the time the FatalInterrupt was received.
    68  	//
    69  	// This will typically be called in place of DestroySummary, as there is no
    70  	// guarantee that this function will be called during a FatalInterrupt. In
    71  	// addition, this function prints additional details about the current
    72  	// operation alongside the current state as the state will be missing newly
    73  	// created resources that also need to be handled manually.
    74  	FatalInterruptSummary(run *moduletest.Run, file *moduletest.File, states map[*moduletest.Run]*states.State, created []*plans.ResourceInstanceChangeSrc)
    75  }
    76  
    77  func NewTest(vt arguments.ViewType, view *View) Test {
    78  	switch vt {
    79  	case arguments.ViewJSON:
    80  		return &TestJSON{
    81  			view: NewJSONView(view),
    82  		}
    83  	case arguments.ViewHuman:
    84  		return &TestHuman{
    85  			view: view,
    86  		}
    87  	default:
    88  		panic(fmt.Sprintf("unknown view type %v", vt))
    89  	}
    90  }
    91  
    92  type TestHuman struct {
    93  	view *View
    94  }
    95  
    96  var _ Test = (*TestHuman)(nil)
    97  
    98  func (t *TestHuman) Abstract(_ *moduletest.Suite) {
    99  	// Do nothing, we don't print an abstract for the human view.
   100  }
   101  
   102  func (t *TestHuman) Conclusion(suite *moduletest.Suite) {
   103  	t.view.streams.Println()
   104  
   105  	counts := make(map[moduletest.Status]int)
   106  	for _, file := range suite.Files {
   107  		for _, run := range file.Runs {
   108  			count := counts[run.Status]
   109  			counts[run.Status] = count + 1
   110  		}
   111  	}
   112  
   113  	if suite.Status <= moduletest.Skip {
   114  		// Then no tests.
   115  		t.view.streams.Print("Executed 0 tests")
   116  		if counts[moduletest.Skip] > 0 {
   117  			t.view.streams.Printf(", %d skipped.\n", counts[moduletest.Skip])
   118  		} else {
   119  			t.view.streams.Println(".")
   120  		}
   121  		return
   122  	}
   123  
   124  	if suite.Status == moduletest.Pass {
   125  		t.view.streams.Print(t.view.colorize.Color("[green]Success![reset]"))
   126  	} else {
   127  		t.view.streams.Print(t.view.colorize.Color("[red]Failure![reset]"))
   128  	}
   129  
   130  	t.view.streams.Printf(" %d passed, %d failed", counts[moduletest.Pass], counts[moduletest.Fail]+counts[moduletest.Error])
   131  	if counts[moduletest.Skip] > 0 {
   132  		t.view.streams.Printf(", %d skipped.\n", counts[moduletest.Skip])
   133  	} else {
   134  		t.view.streams.Println(".")
   135  	}
   136  }
   137  
   138  func (t *TestHuman) File(file *moduletest.File) {
   139  	t.view.streams.Printf("%s... %s\n", file.Name, colorizeTestStatus(file.Status, t.view.colorize))
   140  	t.Diagnostics(nil, file, file.Diagnostics)
   141  }
   142  
   143  func (t *TestHuman) Run(run *moduletest.Run, file *moduletest.File) {
   144  	t.view.streams.Printf("  run %q... %s\n", run.Name, colorizeTestStatus(run.Status, t.view.colorize))
   145  
   146  	if run.Verbose != nil {
   147  		// We're going to be more verbose about what we print, here's the plan
   148  		// or the state depending on the type of run we did.
   149  
   150  		schemas := &tofu.Schemas{
   151  			Providers:    run.Verbose.Providers,
   152  			Provisioners: run.Verbose.Provisioners,
   153  		}
   154  
   155  		renderer := jsonformat.Renderer{
   156  			Streams:             t.view.streams,
   157  			Colorize:            t.view.colorize,
   158  			RunningInAutomation: t.view.runningInAutomation,
   159  		}
   160  
   161  		if run.Config.Command == configs.ApplyTestCommand {
   162  			// Then we'll print the state.
   163  			root, outputs, err := jsonstate.MarshalForRenderer(statefile.New(run.Verbose.State, file.Name, uint64(run.Index)), schemas)
   164  			if err != nil {
   165  				run.Diagnostics = run.Diagnostics.Append(tfdiags.Sourceless(
   166  					tfdiags.Warning,
   167  					"Failed to render test state",
   168  					fmt.Sprintf("OpenTofu could not marshal the state for display: %v", err)))
   169  			} else {
   170  				state := jsonformat.State{
   171  					StateFormatVersion:    jsonstate.FormatVersion,
   172  					ProviderFormatVersion: jsonprovider.FormatVersion,
   173  					RootModule:            root,
   174  					RootModuleOutputs:     outputs,
   175  					ProviderSchemas:       jsonprovider.MarshalForRenderer(schemas),
   176  				}
   177  
   178  				renderer.RenderHumanState(state)
   179  			}
   180  		} else {
   181  			// We'll print the plan.
   182  			outputs, changed, drift, attrs, err := jsonplan.MarshalForRenderer(run.Verbose.Plan, schemas)
   183  			if err != nil {
   184  				run.Diagnostics = run.Diagnostics.Append(tfdiags.Sourceless(
   185  					tfdiags.Warning,
   186  					"Failed to render test plan",
   187  					fmt.Sprintf("OpenTofu could not marshal the plan for display: %v", err)))
   188  			} else {
   189  				plan := jsonformat.Plan{
   190  					PlanFormatVersion:     jsonplan.FormatVersion,
   191  					ProviderFormatVersion: jsonprovider.FormatVersion,
   192  					OutputChanges:         outputs,
   193  					ResourceChanges:       changed,
   194  					ResourceDrift:         drift,
   195  					ProviderSchemas:       jsonprovider.MarshalForRenderer(schemas),
   196  					RelevantAttributes:    attrs,
   197  				}
   198  
   199  				var opts []plans.Quality
   200  				if !run.Verbose.Plan.CanApply() {
   201  					opts = append(opts, plans.NoChanges)
   202  				}
   203  				if run.Verbose.Plan.Errored {
   204  					opts = append(opts, plans.Errored)
   205  				}
   206  
   207  				renderer.RenderHumanPlan(plan, run.Verbose.Plan.UIMode, opts...)
   208  			}
   209  		}
   210  	}
   211  
   212  	// Finally we'll print out a summary of the diagnostics from the run.
   213  	t.Diagnostics(run, file, run.Diagnostics)
   214  }
   215  
   216  func (t *TestHuman) DestroySummary(diags tfdiags.Diagnostics, run *moduletest.Run, file *moduletest.File, state *states.State) {
   217  	identifier := file.Name
   218  	if run != nil {
   219  		identifier = fmt.Sprintf("%s/%s", identifier, run.Name)
   220  	}
   221  
   222  	if diags.HasErrors() {
   223  		t.view.streams.Eprint(format.WordWrap(fmt.Sprintf("OpenTofu encountered an error destroying resources created while executing %s.\n", identifier), t.view.errorColumns()))
   224  	}
   225  	t.Diagnostics(run, file, diags)
   226  
   227  	if state.HasManagedResourceInstanceObjects() {
   228  		t.view.streams.Eprint(format.WordWrap(fmt.Sprintf("\nOpenTofu left the following resources in state after executing %s, 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:\n", identifier), t.view.errorColumns()))
   229  		for _, resource := range state.AllResourceInstanceObjectAddrs() {
   230  			if resource.DeposedKey != states.NotDeposed {
   231  				t.view.streams.Eprintf("  - %s (%s)\n", resource.Instance, resource.DeposedKey)
   232  				continue
   233  			}
   234  			t.view.streams.Eprintf("  - %s\n", resource.Instance)
   235  		}
   236  	}
   237  }
   238  
   239  func (t *TestHuman) Diagnostics(_ *moduletest.Run, _ *moduletest.File, diags tfdiags.Diagnostics) {
   240  	t.view.Diagnostics(diags)
   241  }
   242  
   243  func (t *TestHuman) Interrupted() {
   244  	t.view.streams.Eprintln(format.WordWrap(interrupted, t.view.errorColumns()))
   245  }
   246  
   247  func (t *TestHuman) FatalInterrupt() {
   248  	t.view.streams.Eprintln(format.WordWrap(fatalInterrupt, t.view.errorColumns()))
   249  }
   250  
   251  func (t *TestHuman) FatalInterruptSummary(run *moduletest.Run, file *moduletest.File, existingStates map[*moduletest.Run]*states.State, created []*plans.ResourceInstanceChangeSrc) {
   252  	t.view.streams.Eprint(format.WordWrap(fmt.Sprintf("\nOpenTofu was interrupted while executing %s, and may not have performed the expected cleanup operations.\n", file.Name), t.view.errorColumns()))
   253  
   254  	// Print out the main state first, this is the state that isn't associated
   255  	// with a run block.
   256  	if state, exists := existingStates[nil]; exists && !state.Empty() {
   257  		t.view.streams.Eprint(format.WordWrap("\nOpenTofu has already created the following resources from the module under test:\n", t.view.errorColumns()))
   258  		for _, resource := range state.AllResourceInstanceObjectAddrs() {
   259  			if resource.DeposedKey != states.NotDeposed {
   260  				t.view.streams.Eprintf("  - %s (%s)\n", resource.Instance, resource.DeposedKey)
   261  				continue
   262  			}
   263  			t.view.streams.Eprintf("  - %s\n", resource.Instance)
   264  		}
   265  	}
   266  
   267  	// Then print out the other states in order.
   268  	for _, run := range file.Runs {
   269  		state, exists := existingStates[run]
   270  		if !exists || state.Empty() {
   271  			continue
   272  		}
   273  
   274  		t.view.streams.Eprint(format.WordWrap(fmt.Sprintf("\nOpenTofu has already created the following resources for %q from %q:\n", run.Name, run.Config.Module.Source), t.view.errorColumns()))
   275  		for _, resource := range state.AllResourceInstanceObjectAddrs() {
   276  			if resource.DeposedKey != states.NotDeposed {
   277  				t.view.streams.Eprintf("  - %s (%s)\n", resource.Instance, resource.DeposedKey)
   278  				continue
   279  			}
   280  			t.view.streams.Eprintf("  - %s\n", resource.Instance)
   281  		}
   282  	}
   283  
   284  	if len(created) == 0 {
   285  		// No planned changes, so we won't print anything.
   286  		return
   287  	}
   288  
   289  	var resources []string
   290  	for _, change := range created {
   291  		resources = append(resources, change.Addr.String())
   292  	}
   293  
   294  	if len(resources) > 0 {
   295  		module := "the module under test"
   296  		if run.Config.ConfigUnderTest != nil {
   297  			module = fmt.Sprintf("%q", run.Config.Module.Source.String())
   298  		}
   299  
   300  		t.view.streams.Eprint(format.WordWrap(fmt.Sprintf("\nOpenTofu 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()))
   301  		for _, resource := range resources {
   302  			t.view.streams.Eprintf("  - %s\n", resource)
   303  		}
   304  	}
   305  }
   306  
   307  type TestJSON struct {
   308  	view *JSONView
   309  }
   310  
   311  var _ Test = (*TestJSON)(nil)
   312  
   313  func (t *TestJSON) Abstract(suite *moduletest.Suite) {
   314  	var fileCount, runCount int
   315  
   316  	abstract := json.TestSuiteAbstract{}
   317  	for name, file := range suite.Files {
   318  		fileCount++
   319  		var runs []string
   320  		for _, run := range file.Runs {
   321  			runCount++
   322  			runs = append(runs, run.Name)
   323  		}
   324  		abstract[name] = runs
   325  	}
   326  
   327  	files := "files"
   328  	runs := "run blocks"
   329  
   330  	if fileCount == 1 {
   331  		files = "file"
   332  	}
   333  
   334  	if runCount == 1 {
   335  		runs = "run block"
   336  	}
   337  
   338  	t.view.log.Info(
   339  		fmt.Sprintf("Found %d %s and %d %s", fileCount, files, runCount, runs),
   340  		"type", json.MessageTestAbstract,
   341  		json.MessageTestAbstract, abstract)
   342  }
   343  
   344  func (t *TestJSON) Conclusion(suite *moduletest.Suite) {
   345  	summary := json.TestSuiteSummary{
   346  		Status: json.ToTestStatus(suite.Status),
   347  	}
   348  	for _, file := range suite.Files {
   349  		for _, run := range file.Runs {
   350  			switch run.Status {
   351  			case moduletest.Skip:
   352  				summary.Skipped++
   353  			case moduletest.Pass:
   354  				summary.Passed++
   355  			case moduletest.Error:
   356  				summary.Errored++
   357  			case moduletest.Fail:
   358  				summary.Failed++
   359  			}
   360  		}
   361  	}
   362  
   363  	var message bytes.Buffer
   364  	if suite.Status <= moduletest.Skip {
   365  		// Then no tests.
   366  		message.WriteString("Executed 0 tests")
   367  		if summary.Skipped > 0 {
   368  			message.WriteString(fmt.Sprintf(", %d skipped.", summary.Skipped))
   369  		} else {
   370  			message.WriteString(".")
   371  		}
   372  	} else {
   373  		if suite.Status == moduletest.Pass {
   374  			message.WriteString("Success!")
   375  		} else {
   376  			message.WriteString("Failure!")
   377  		}
   378  
   379  		message.WriteString(fmt.Sprintf(" %d passed, %d failed", summary.Passed, summary.Failed+summary.Errored))
   380  		if summary.Skipped > 0 {
   381  			message.WriteString(fmt.Sprintf(", %d skipped.", summary.Skipped))
   382  		} else {
   383  			message.WriteString(".")
   384  		}
   385  	}
   386  
   387  	t.view.log.Info(
   388  		message.String(),
   389  		"type", json.MessageTestSummary,
   390  		json.MessageTestSummary, summary)
   391  }
   392  
   393  func (t *TestJSON) File(file *moduletest.File) {
   394  	t.view.log.Info(
   395  		fmt.Sprintf("%s... %s", file.Name, testStatus(file.Status)),
   396  		"type", json.MessageTestFile,
   397  		json.MessageTestFile, json.TestFileStatus{file.Name, json.ToTestStatus(file.Status)},
   398  		"@testfile", file.Name)
   399  	t.Diagnostics(nil, file, file.Diagnostics)
   400  }
   401  
   402  func (t *TestJSON) Run(run *moduletest.Run, file *moduletest.File) {
   403  	t.view.log.Info(
   404  		fmt.Sprintf("  %q... %s", run.Name, testStatus(run.Status)),
   405  		"type", json.MessageTestRun,
   406  		json.MessageTestRun, json.TestRunStatus{file.Name, run.Name, json.ToTestStatus(run.Status)},
   407  		"@testfile", file.Name,
   408  		"@testrun", run.Name)
   409  
   410  	if run.Verbose != nil {
   411  
   412  		schemas := &tofu.Schemas{
   413  			Providers:    run.Verbose.Providers,
   414  			Provisioners: run.Verbose.Provisioners,
   415  		}
   416  
   417  		if run.Config.Command == configs.ApplyTestCommand {
   418  			state, err := jsonstate.MarshalForLog(statefile.New(run.Verbose.State, file.Name, uint64(run.Index)), schemas)
   419  			if err != nil {
   420  				run.Diagnostics = run.Diagnostics.Append(tfdiags.Sourceless(
   421  					tfdiags.Warning,
   422  					"Failed to render test state",
   423  					fmt.Sprintf("OpenTofu could not marshal the state for display: %v", err)))
   424  			} else {
   425  				t.view.log.Info(
   426  					"-verbose flag enabled, printing state",
   427  					"type", json.MessageTestState,
   428  					json.MessageTestState, state,
   429  					"@testfile", file.Name,
   430  					"@testrun", run.Name)
   431  			}
   432  		} else {
   433  			plan, err := jsonplan.MarshalForLog(run.Verbose.Config, run.Verbose.Plan, nil, schemas)
   434  			if err != nil {
   435  				run.Diagnostics = run.Diagnostics.Append(tfdiags.Sourceless(
   436  					tfdiags.Warning,
   437  					"Failed to render test plan",
   438  					fmt.Sprintf("OpenTofu could not marshal the plan for display: %v", err)))
   439  			} else {
   440  				t.view.log.Info(
   441  					"-verbose flag enabled, printing plan",
   442  					"type", json.MessageTestPlan,
   443  					json.MessageTestPlan, plan,
   444  					"@testfile", file.Name,
   445  					"@testrun", run.Name)
   446  			}
   447  		}
   448  	}
   449  
   450  	t.Diagnostics(run, file, run.Diagnostics)
   451  }
   452  
   453  func (t *TestJSON) DestroySummary(diags tfdiags.Diagnostics, run *moduletest.Run, file *moduletest.File, state *states.State) {
   454  	if state.HasManagedResourceInstanceObjects() {
   455  		cleanup := json.TestFileCleanup{}
   456  		for _, resource := range state.AllResourceInstanceObjectAddrs() {
   457  			cleanup.FailedResources = append(cleanup.FailedResources, json.TestFailedResource{
   458  				Instance:   resource.Instance.String(),
   459  				DeposedKey: resource.DeposedKey.String(),
   460  			})
   461  		}
   462  
   463  		if run != nil {
   464  			t.view.log.Error(
   465  				fmt.Sprintf("OpenTofu left some resources in state after executing %s/%s, 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:", file.Name, run.Name),
   466  				"type", json.MessageTestCleanup,
   467  				json.MessageTestCleanup, cleanup,
   468  				"@testfile", file.Name,
   469  				"@testrun", run.Name)
   470  		} else {
   471  			t.view.log.Error(
   472  				fmt.Sprintf("OpenTofu left some resources in state after executing %s, 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:", file.Name),
   473  				"type", json.MessageTestCleanup,
   474  				json.MessageTestCleanup, cleanup,
   475  				"@testfile", file.Name)
   476  		}
   477  	}
   478  	t.Diagnostics(run, file, diags)
   479  }
   480  
   481  func (t *TestJSON) Diagnostics(run *moduletest.Run, file *moduletest.File, diags tfdiags.Diagnostics) {
   482  	var metadata []interface{}
   483  	if file != nil {
   484  		metadata = append(metadata, "@testfile", file.Name)
   485  	}
   486  	if run != nil {
   487  		metadata = append(metadata, "@testrun", run.Name)
   488  	}
   489  	t.view.Diagnostics(diags, metadata...)
   490  }
   491  
   492  func (t *TestJSON) Interrupted() {
   493  	t.view.Log(interrupted)
   494  }
   495  
   496  func (t *TestJSON) FatalInterrupt() {
   497  	t.view.Log(fatalInterrupt)
   498  }
   499  
   500  func (t *TestJSON) FatalInterruptSummary(run *moduletest.Run, file *moduletest.File, existingStates map[*moduletest.Run]*states.State, created []*plans.ResourceInstanceChangeSrc) {
   501  
   502  	message := json.TestFatalInterrupt{
   503  		States: make(map[string][]json.TestFailedResource),
   504  	}
   505  
   506  	for run, state := range existingStates {
   507  		if state.Empty() {
   508  			continue
   509  		}
   510  
   511  		var resources []json.TestFailedResource
   512  		for _, resource := range state.AllResourceInstanceObjectAddrs() {
   513  			resources = append(resources, json.TestFailedResource{
   514  				Instance:   resource.Instance.String(),
   515  				DeposedKey: resource.DeposedKey.String(),
   516  			})
   517  		}
   518  
   519  		if run == nil {
   520  			message.State = resources
   521  		} else {
   522  			message.States[run.Name] = resources
   523  		}
   524  	}
   525  
   526  	if len(created) > 0 {
   527  		for _, change := range created {
   528  			message.Planned = append(message.Planned, change.Addr.String())
   529  		}
   530  	}
   531  
   532  	if len(message.States) == 0 && len(message.State) == 0 && len(message.Planned) == 0 {
   533  		// Then we don't have any information to share with the user.
   534  		return
   535  	}
   536  
   537  	t.view.log.Error(
   538  		"OpenTofu was interrupted during test execution, and may not have performed the expected cleanup operations.",
   539  		"type", json.MessageTestInterrupt,
   540  		json.MessageTestInterrupt, message,
   541  		"@testfile", file.Name)
   542  }
   543  
   544  func colorizeTestStatus(status moduletest.Status, color *colorstring.Colorize) string {
   545  	switch status {
   546  	case moduletest.Error, moduletest.Fail:
   547  		return color.Color("[red]fail[reset]")
   548  	case moduletest.Pass:
   549  		return color.Color("[green]pass[reset]")
   550  	case moduletest.Skip:
   551  		return color.Color("[light_gray]skip[reset]")
   552  	case moduletest.Pending:
   553  		return color.Color("[light_gray]pending[reset]")
   554  	default:
   555  		panic("unrecognized status: " + status.String())
   556  	}
   557  }
   558  
   559  func testStatus(status moduletest.Status) string {
   560  	switch status {
   561  	case moduletest.Error, moduletest.Fail:
   562  		return "fail"
   563  	case moduletest.Pass:
   564  		return "pass"
   565  	case moduletest.Skip:
   566  		return "skip"
   567  	case moduletest.Pending:
   568  		return "pending"
   569  	default:
   570  		panic("unrecognized status: " + status.String())
   571  	}
   572  }
   573  
   574  // SaveErroredTestStateFile is a helper function to invoked in DestorySummary
   575  // to store the state to errored_test.tfstate and handle associated diagnostics and errors with this operation
   576  func SaveErroredTestStateFile(state *states.State, run *moduletest.Run, file *moduletest.File, view Test) {
   577  	var diags tfdiags.Diagnostics
   578  	localFileSystem := statemgr.NewFilesystem("errored_test.tfstate", encryption.StateEncryptionDisabled())
   579  	stateFile := statemgr.NewStateFile()
   580  	stateFile.State = state
   581  
   582  	//creating an operation to invoke EmergencyDumpState()
   583  	var op Operation
   584  	switch v := view.(type) {
   585  	case *TestHuman:
   586  		op = NewOperation(arguments.ViewHuman, false, v.view)
   587  		v.view.streams.Eprint(format.WordWrap("\nWriting state to file: errored_test.tfstate\n", v.view.errorColumns()))
   588  	case *TestJSON:
   589  		op = &OperationJSON{
   590  			view: v.view,
   591  		}
   592  		v.view.log.Info("Writing state to file: errored_test.tfstate")
   593  	default:
   594  	}
   595  
   596  	writeErr := localFileSystem.WriteStateForMigration(stateFile, true)
   597  	if writeErr != nil {
   598  		// if the write operation to errored_test.tfstate executed by WriteStateForMigration fails, as a final attempt to
   599  		// prevent leaving the user with no state file at all, the JSON state is printed onto the terminal by EmergencyDumpState()
   600  
   601  		if dumpErr := op.EmergencyDumpState(stateFile, encryption.StateEncryptionDisabled()); dumpErr != nil {
   602  			diags = diags.Append(tfdiags.Sourceless(
   603  				tfdiags.Error,
   604  				"Failed to serialize state",
   605  				fmt.Sprintf(stateWriteFatalErrorFmt, dumpErr),
   606  			))
   607  		}
   608  		diags = diags.Append(tfdiags.Sourceless(
   609  			tfdiags.Error,
   610  			"Failed to persist state",
   611  			stateWriteConsoleFallbackError,
   612  		))
   613  	}
   614  	view.Diagnostics(run, file, diags)
   615  }
   616  
   617  const stateWriteFatalErrorFmt = `Failed to save state after an errored test run.
   618  
   619  Error serializing state: %s
   620  
   621  A catastrophic error has prevented OpenTofu from persisting the state during an errored test run. 
   622  
   623  This is a serious bug in OpenTofu and should be reported.
   624  `
   625  
   626  const stateWriteConsoleFallbackError = `The errors shown above prevented OpenTofu from writing the state to
   627  the errored_test.tfstate. As a fallback, the raw state data is printed above as a JSON object.
   628  
   629  To retry writing this state, copy the state data (from the first { to the last } inclusive) and save it into a local file named "errored_test.tfstate".
   630  `