2023-06-28 02:37:42 -05:00
package command
import (
2023-07-10 08:53:13 -05:00
"context"
"fmt"
2023-07-19 03:07:46 -05:00
"path"
2023-06-28 02:37:42 -05:00
"sort"
"strings"
2023-07-19 03:31:32 -05:00
"time"
2023-06-28 02:37:42 -05:00
2023-07-19 02:44:40 -05:00
"github.com/hashicorp/terraform/internal/addrs"
2023-06-28 02:37:42 -05:00
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/views"
"github.com/hashicorp/terraform/internal/configs"
2023-07-10 08:53:13 -05:00
"github.com/hashicorp/terraform/internal/logging"
2023-06-28 02:37:42 -05:00
"github.com/hashicorp/terraform/internal/moduletest"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
)
type TestCommand struct {
Meta
}
func ( c * TestCommand ) Help ( ) string {
helpText := `
Usage : terraform [ global options ] test [ options ]
Executes automated integration tests against the current Terraform
configuration .
2023-07-20 09:57:05 -05:00
Terraform will search for . tftest . hcl files within the current configuration
and testing directories . Terraform will then execute the testing run blocks
within any testing files in order , and verify conditional checks and
assertions against the created infrastructure .
2023-06-28 02:37:42 -05:00
This command creates real infrastructure and will attempt to clean up the
testing infrastructure on completion . Monitor the output carefully to ensure
this cleanup process is successful .
Options :
2023-07-19 03:07:46 -05:00
- filter = testfile If specified , Terraform will only execute the test files
specified by this flag . You can use this option multiple
times to execute more than one test file .
- json If specified , machine readable output will be printed in
JSON format
- test - directory = path Set the Terraform test directory , defaults to "tests" .
- var ' foo = bar ' Set a value for one of the input variables in the root
module of the configuration . Use this option more than
once to set more than one variable .
- var - file = filename Load variable values from the given file , in addition
to the default files terraform . tfvars and * . auto . tfvars .
Use this option more than once to include more than one
variables file .
- verbose Print the plan or state for each test run block as it
executes .
2023-06-28 02:37:42 -05:00
`
return strings . TrimSpace ( helpText )
}
func ( c * TestCommand ) Synopsis ( ) string {
return "Execute integration tests for Terraform modules"
}
func ( c * TestCommand ) Run ( rawArgs [ ] string ) int {
var diags tfdiags . Diagnostics
2023-07-19 03:07:46 -05:00
common , rawArgs := arguments . ParseView ( rawArgs )
2023-06-28 02:37:42 -05:00
c . View . Configure ( common )
2023-07-19 03:07:46 -05:00
args , diags := arguments . ParseTest ( rawArgs )
if diags . HasErrors ( ) {
c . View . Diagnostics ( diags )
c . View . HelpPrompt ( "test" )
return 1
}
2023-06-28 02:37:42 -05:00
2023-07-19 03:07:46 -05:00
view := views . NewTest ( args . ViewType , c . View )
config , configDiags := c . loadConfigWithTests ( "." , args . TestDirectory )
2023-06-28 02:37:42 -05:00
diags = diags . Append ( configDiags )
if configDiags . HasErrors ( ) {
2023-07-06 08:53:18 -05:00
view . Diagnostics ( nil , nil , diags )
2023-06-28 02:37:42 -05:00
return 1
}
2023-07-19 03:07:46 -05:00
var fileDiags tfdiags . Diagnostics
2023-06-28 02:37:42 -05:00
suite := moduletest . Suite {
Files : func ( ) map [ string ] * moduletest . File {
files := make ( map [ string ] * moduletest . File )
2023-07-19 03:07:46 -05:00
if len ( args . Filter ) > 0 {
for _ , name := range args . Filter {
file , ok := config . Module . Tests [ name ]
if ! ok {
// If the filter is invalid, we'll simply skip this
// entry and print a warning. But we could still execute
// any other tests within the filter.
fileDiags . Append ( tfdiags . Sourceless (
tfdiags . Warning ,
"Unknown test file" ,
fmt . Sprintf ( "The specified test file, %s, could not be found." , name ) ) )
continue
}
var runs [ ] * moduletest . Run
for ix , run := range file . Runs {
runs = append ( runs , & moduletest . Run {
Config : run ,
Index : ix ,
Name : run . Name ,
} )
}
files [ name ] = & moduletest . File {
Config : file ,
Name : name ,
Runs : runs ,
}
}
return files
}
// Otherwise, we'll just do all the tests in the directory!
2023-06-28 02:37:42 -05:00
for name , file := range config . Module . Tests {
var runs [ ] * moduletest . Run
2023-07-19 03:07:46 -05:00
for ix , run := range file . Runs {
2023-06-28 02:37:42 -05:00
runs = append ( runs , & moduletest . Run {
Config : run ,
2023-07-19 03:07:46 -05:00
Index : ix ,
2023-06-28 02:37:42 -05:00
Name : run . Name ,
} )
}
files [ name ] = & moduletest . File {
Config : file ,
Name : name ,
Runs : runs ,
}
}
return files
} ( ) ,
}
2023-07-19 03:07:46 -05:00
diags = diags . Append ( fileDiags )
if fileDiags . HasErrors ( ) {
view . Diagnostics ( nil , nil , diags )
return 1
}
// Users can also specify variables via the command line, so we'll parse
// all that here.
var items [ ] rawFlag
for _ , variable := range args . Vars . All ( ) {
items = append ( items , rawFlag {
Name : variable . Name ,
Value : variable . Value ,
} )
}
c . variableArgs = rawFlags { items : & items }
variables , variableDiags := c . collectVariableValues ( )
diags = diags . Append ( variableDiags )
if variableDiags . HasErrors ( ) {
view . Diagnostics ( nil , nil , diags )
return 1
}
2023-07-19 03:31:32 -05:00
// 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.
2023-07-10 08:53:13 -05:00
runningCtx , done := context . WithCancel ( context . Background ( ) )
stopCtx , stop := context . WithCancel ( runningCtx )
cancelCtx , cancel := context . WithCancel ( context . Background ( ) )
runner := & TestRunner {
command : c ,
Suite : & suite ,
Config : config ,
View : view ,
CancelledCtx : cancelCtx ,
StoppedCtx : stopCtx ,
// Just to be explicit, we'll set the following fields even though they
// default to these values.
Cancelled : false ,
Stopped : false ,
2023-07-19 03:07:46 -05:00
Verbose : args . Verbose ,
2023-07-10 08:53:13 -05:00
}
2023-06-28 02:37:42 -05:00
view . Abstract ( & suite )
2023-07-10 08:53:13 -05:00
go func ( ) {
defer logging . PanicHandler ( )
2023-07-19 03:31:32 -05:00
defer done ( )
2023-07-10 08:53:13 -05:00
defer stop ( )
defer cancel ( )
2023-07-19 03:07:46 -05:00
runner . Start ( variables )
2023-07-10 08:53:13 -05:00
} ( )
// Wait for the operation to complete, or for an interrupt to occur.
select {
case <- c . ShutdownCh :
// Nice request to be cancelled.
view . Interrupted ( )
runner . Stopped = true
stop ( )
select {
case <- c . ShutdownCh :
// The user pressed it again, now we have to get it to stop as
// fast as possible.
view . FatalInterrupt ( )
runner . Cancelled = true
cancel ( )
2023-07-19 03:31:32 -05:00
// 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 ) :
}
2023-07-10 08:53:13 -05:00
case <- runningCtx . Done ( ) :
// The application finished nicely after the request was stopped.
}
case <- runningCtx . Done ( ) :
// tests finished normally with no interrupts.
}
if runner . Cancelled {
// Don't print out the conclusion if the test was cancelled.
return 1
}
2023-06-28 02:37:42 -05:00
view . Conclusion ( & suite )
if suite . Status != moduletest . Pass {
return 1
}
return 0
}
2023-07-10 08:53:13 -05:00
// test runner
2023-06-28 02:37:42 -05:00
2023-07-10 08:53:13 -05:00
type TestRunner struct {
command * TestCommand
2023-06-28 02:37:42 -05:00
2023-07-10 08:53:13 -05:00
Suite * moduletest . Suite
Config * configs . Config
View views . Test
// Stopped and Cancelled track whether the user requested the testing
// process to be interrupted. Stopped is a nice graceful exit, we'll still
// tidy up any state that was created and mark the tests with relevant
// `skipped` status updates. Cancelled is a hard stop right now exit, we
// won't attempt to clean up any state left hanging, and tests will just
// be left showing `pending` as the status. We will still print out the
// destroy summary diagnostics that tell the user what state has been left
// behind and needs manual clean up.
Stopped bool
Cancelled bool
2023-06-28 02:37:42 -05:00
2023-07-10 08:53:13 -05:00
// StoppedCtx and CancelledCtx allow in progress Terraform operations to
// respond to external calls from the test command.
StoppedCtx context . Context
CancelledCtx context . Context
2023-07-19 03:07:46 -05:00
// Verbose tells the runner to print out plan files during each test run.
Verbose bool
2023-07-10 08:53:13 -05:00
}
2023-07-19 03:07:46 -05:00
func ( runner * TestRunner ) Start ( globals map [ string ] backend . UnparsedVariableValue ) {
2023-06-28 02:37:42 -05:00
var files [ ] string
2023-07-10 08:53:13 -05:00
for name := range runner . Suite . Files {
2023-06-28 02:37:42 -05:00
files = append ( files , name )
}
sort . Strings ( files ) // execute the files in alphabetical order
2023-07-10 08:53:13 -05:00
runner . Suite . Status = moduletest . Pass
2023-06-28 02:37:42 -05:00
for _ , name := range files {
2023-07-10 08:53:13 -05:00
if runner . Cancelled {
return
}
2023-06-28 02:37:42 -05:00
2023-07-10 08:53:13 -05:00
file := runner . Suite . Files [ name ]
2023-07-19 03:07:46 -05:00
runner . ExecuteTestFile ( file , globals )
2023-07-10 08:53:13 -05:00
runner . Suite . Status = runner . Suite . Status . Merge ( file . Status )
2023-06-28 02:37:42 -05:00
}
}
2023-07-19 03:07:46 -05:00
func ( runner * TestRunner ) ExecuteTestFile ( file * moduletest . File , globals map [ string ] backend . UnparsedVariableValue ) {
2023-07-10 05:42:05 -05:00
mgr := new ( TestStateManager )
2023-07-10 08:53:13 -05:00
mgr . runner = runner
2023-07-10 05:42:05 -05:00
mgr . State = states . NewState ( )
2023-07-19 03:07:46 -05:00
defer mgr . cleanupStates ( file , globals )
2023-06-28 02:37:42 -05:00
file . Status = file . Status . Merge ( moduletest . Pass )
for _ , run := range file . Runs {
2023-07-10 08:53:13 -05:00
if runner . Cancelled {
// This means a hard stop has been requested, in this case we don't
// even stop to mark future tests as having been skipped. They'll
// just show up as pending in the printed summary.
return
}
if runner . Stopped {
// Then the test was requested to be stopped, so we just mark each
// following test as skipped and move on.
run . Status = moduletest . Skip
continue
}
2023-06-28 02:37:42 -05:00
if file . Status == moduletest . Error {
2023-07-10 05:42:05 -05:00
// If the overall test file has errored, we don't keep trying to
// execute tests. Instead, we mark all remaining run blocks as
// skipped.
2023-06-28 02:37:42 -05:00
run . Status = moduletest . Skip
continue
}
2023-07-10 05:42:05 -05:00
if run . Config . ConfigUnderTest != nil {
// Then we want to execute a different module under a kind of
// sandbox.
2023-07-19 03:31:32 -05:00
state := runner . ExecuteTestRun ( mgr , run , file , states . NewState ( ) , run . Config . ConfigUnderTest , globals )
2023-07-10 05:42:05 -05:00
mgr . States = append ( mgr . States , & TestModuleState {
State : state ,
Run : run ,
} )
} else {
2023-07-19 03:31:32 -05:00
mgr . State = runner . ExecuteTestRun ( mgr , run , file , mgr . State , runner . Config , globals )
2023-07-10 05:42:05 -05:00
}
2023-06-28 02:37:42 -05:00
file . Status = file . Status . Merge ( run . Status )
}
2023-07-10 08:53:13 -05:00
runner . View . File ( file )
2023-06-28 02:37:42 -05:00
for _ , run := range file . Runs {
2023-07-10 08:53:13 -05:00
runner . View . Run ( run , file )
2023-06-28 02:37:42 -05:00
}
}
2023-07-19 03:31:32 -05:00
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 {
2023-07-10 08:53:13 -05:00
if runner . Cancelled {
// Don't do anything, just give up and return immediately.
// The surrounding functions should stop this even being called, but in
// case of race conditions or something we can still verify this.
2023-07-10 08:33:15 -05:00
return state
}
2023-07-10 08:53:13 -05:00
if runner . Stopped {
// Basically the same as above, except we'll be a bit nicer.
run . Status = moduletest . Skip
return state
2023-06-28 02:37:42 -05:00
}
2023-07-10 08:53:13 -05:00
targets , diags := run . GetTargets ( )
run . Diagnostics = run . Diagnostics . Append ( diags )
2023-06-28 02:37:42 -05:00
2023-07-10 08:53:13 -05:00
replaces , diags := run . GetReplaces ( )
run . Diagnostics = run . Diagnostics . Append ( diags )
2023-06-28 02:37:42 -05:00
2023-07-10 08:53:13 -05:00
references , diags := run . GetReferences ( )
2023-06-28 02:37:42 -05:00
run . Diagnostics = run . Diagnostics . Append ( diags )
if run . Diagnostics . HasErrors ( ) {
run . Status = moduletest . Error
return state
}
2023-07-19 03:31:32 -05:00
ctx , plan , state , diags := runner . execute ( mgr , run , file , config , state , & terraform . PlanOpts {
2023-06-28 02:37:42 -05:00
Mode : func ( ) plans . Mode {
switch run . Config . Options . Mode {
case configs . RefreshOnlyTestMode :
return plans . RefreshOnlyMode
default :
return plans . NormalMode
}
} ( ) ,
Targets : targets ,
ForceReplace : replaces ,
SkipRefresh : ! run . Config . Options . Refresh ,
ExternalReferences : references ,
2023-07-19 03:07:46 -05:00
} , run . Config . Command , globals )
2023-07-19 02:44:40 -05:00
diags = run . ValidateExpectedFailures ( diags )
2023-06-28 02:37:42 -05:00
run . Diagnostics = run . Diagnostics . Append ( diags )
2023-07-10 08:53:13 -05:00
if runner . Cancelled {
// Print out the diagnostics from the run now, since it was cancelled
// the normal set of diagnostics will not be printed otherwise.
runner . View . Diagnostics ( run , file , run . Diagnostics )
run . Status = moduletest . Error
return state
}
2023-06-28 02:37:42 -05:00
if diags . HasErrors ( ) {
run . Status = moduletest . Error
return state
}
2023-07-10 08:53:13 -05:00
if runner . Stopped {
run . Status = moduletest . Skip
return state
}
2023-06-28 02:37:42 -05:00
2023-07-19 03:07:46 -05:00
// If the user wants to render the plans as part of the test output, we
// track that here.
if runner . Verbose {
schemas , diags := ctx . Schemas ( config , state )
// If we're going to fail to render the plan, let's not fail the overall
// test. It can still have succeeded. So we'll add the diagnostics, but
// still report the test status as a success.
if diags . HasErrors ( ) {
// This is very unlikely.
diags = diags . Append ( tfdiags . Sourceless (
tfdiags . Warning ,
"Failed to print verbose output" ,
fmt . Sprintf ( "Terraform failed to print the verbose output for %s, other diagnostics will contain more details as to why." , path . Join ( file . Name , run . Name ) ) ) )
} else {
run . Verbose = & moduletest . Verbose {
Plan : plan ,
State : state ,
Config : config ,
Providers : schemas . Providers ,
Provisioners : schemas . Provisioners ,
}
}
run . Diagnostics = run . Diagnostics . Append ( diags )
}
variables , diags := buildInputVariablesForAssertions ( run , file , config , globals )
2023-07-10 08:53:13 -05:00
run . Diagnostics = run . Diagnostics . Append ( diags )
if diags . HasErrors ( ) {
run . Status = moduletest . Error
return state
}
if run . Config . Command == configs . ApplyTestCommand {
2023-06-28 02:37:42 -05:00
ctx . TestContext ( config , state , plan , variables ) . EvaluateAgainstState ( run )
return state
}
ctx . TestContext ( config , plan . PlannedState , plan , variables ) . EvaluateAgainstPlan ( run )
return state
}
2023-07-10 08:53:13 -05:00
// execute executes Terraform plan and apply operations for the given arguments.
//
// The command argument decides whether it executes only a plan or also applies
// the plan it creates during the planning.
2023-07-19 03:31:32 -05:00
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 ) {
2023-07-10 08:53:13 -05:00
if opts . Mode == plans . DestroyMode && state . Empty ( ) {
// Nothing to do!
return nil , nil , state , nil
}
2023-07-10 05:42:05 -05:00
2023-07-10 08:53:13 -05:00
// First, transform the config for the given test run and test file.
var diags tfdiags . Diagnostics
if run == nil {
reset , cfgDiags := config . TransformForTest ( nil , file . Config )
defer reset ( )
diags = diags . Append ( cfgDiags )
} else {
reset , cfgDiags := config . TransformForTest ( run . Config , file . Config )
defer reset ( )
diags = diags . Append ( cfgDiags )
}
if diags . HasErrors ( ) {
return nil , nil , state , diags
2023-07-10 05:42:05 -05:00
}
2023-07-10 08:53:13 -05:00
// Second, gather any variables and give them to the plan options.
2023-07-19 03:07:46 -05:00
variables , variableDiags := buildInputVariablesForTest ( run , file , config , globals )
2023-07-10 08:53:13 -05:00
diags = diags . Append ( variableDiags )
if variableDiags . HasErrors ( ) {
return nil , nil , state , diags
2023-06-28 02:37:42 -05:00
}
2023-07-10 08:53:13 -05:00
opts . SetVariables = variables
2023-06-28 02:37:42 -05:00
2023-07-10 08:53:13 -05:00
// Third, execute planning stage.
tfCtxOpts , err := runner . command . contextOpts ( )
diags = diags . Append ( err )
if err != nil {
return nil , nil , state , diags
2023-06-28 02:37:42 -05:00
}
2023-07-10 08:53:13 -05:00
tfCtx , ctxDiags := terraform . NewContext ( tfCtxOpts )
diags = diags . Append ( ctxDiags )
if ctxDiags . HasErrors ( ) {
return nil , nil , state , diags
2023-07-10 05:42:05 -05:00
}
2023-07-10 08:53:13 -05:00
runningCtx , done := context . WithCancel ( context . Background ( ) )
var plan * plans . Plan
var planDiags tfdiags . Diagnostics
go func ( ) {
2023-07-19 02:57:09 -05:00
defer logging . PanicHandler ( )
2023-07-10 08:53:13 -05:00
defer done ( )
plan , planDiags = tfCtx . Plan ( config , state , opts )
} ( )
2023-07-19 03:31:32 -05:00
waitDiags , cancelled := runner . wait ( tfCtx , runningCtx , mgr , run , file , nil )
2023-07-10 08:53:13 -05:00
planDiags = planDiags . Append ( waitDiags )
diags = diags . Append ( planDiags )
if planDiags . HasErrors ( ) || command == configs . PlanTestCommand {
// Either the plan errored, or we only wanted to see the plan. Either
// way, just return what we have: The plan and diagnostics from making
// it and the unchanged state.
return tfCtx , plan , state , diags
2023-07-10 08:33:15 -05:00
}
2023-07-10 08:53:13 -05:00
if cancelled {
// If the execution was cancelled during the plan, we'll exit here to
// stop the plan being applied and using more time.
return tfCtx , plan , state , diags
2023-07-10 08:33:15 -05:00
}
2023-06-28 02:37:42 -05:00
2023-07-19 02:44:40 -05:00
// We're also going to strip out any warnings from check blocks, as we do
// for normal executions. Since we're going to go ahead and execute the
// plan immediately, any warnings from the check block are just not relevant
// any more.
var filteredDiags tfdiags . Diagnostics
for _ , diag := range diags {
if rule , ok := addrs . DiagnosticOriginatesFromCheckRule ( diag ) ; ok && rule . Container . CheckableKind ( ) == addrs . CheckableCheck {
continue
}
filteredDiags = filteredDiags . Append ( diag )
}
diags = filteredDiags
2023-07-10 08:53:13 -05:00
// Fourth, execute apply stage.
tfCtx , ctxDiags = terraform . NewContext ( tfCtxOpts )
diags = diags . Append ( ctxDiags )
if ctxDiags . HasErrors ( ) {
return nil , nil , state , diags
2023-07-10 05:42:05 -05:00
}
2023-06-28 02:37:42 -05:00
2023-07-10 08:53:13 -05:00
runningCtx , done = context . WithCancel ( context . Background ( ) )
2023-07-19 03:31:32 -05:00
// 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 )
}
2023-07-10 08:53:13 -05:00
var updated * states . State
var applyDiags tfdiags . Diagnostics
go func ( ) {
2023-07-19 02:57:09 -05:00
defer logging . PanicHandler ( )
2023-07-10 08:53:13 -05:00
defer done ( )
updated , applyDiags = tfCtx . Apply ( plan , config )
} ( )
2023-07-19 03:31:32 -05:00
waitDiags , _ = runner . wait ( tfCtx , runningCtx , mgr , run , file , created )
2023-07-10 08:53:13 -05:00
applyDiags = applyDiags . Append ( waitDiags )
diags = diags . Append ( applyDiags )
return tfCtx , plan , updated , diags
}
2023-07-19 03:31:32 -05:00
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 ) {
2023-07-10 08:53:13 -05:00
2023-07-19 03:31:32 -05:00
// 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 ( ) {
2023-07-10 08:53:13 -05:00
2023-07-19 03:31:32 -05:00
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 )
2023-07-10 08:53:13 -05:00
2023-07-19 03:31:32 -05:00
cancelled = true
go ctx . Stop ( )
2023-07-10 08:53:13 -05:00
2023-07-19 03:31:32 -05:00
// Just wait for things to finish now, the overall test execution will
// exit early if this takes too long.
<- runningCtx . Done ( )
}
2023-07-10 08:53:13 -05:00
2023-07-19 03:31:32 -05:00
// 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 ( )
2023-07-10 08:53:13 -05:00
case <- runningCtx . Done ( ) :
2023-07-19 03:31:32 -05:00
// Do nothing, we finished safely and skipping the remaining tests
// will be handled elsewhere.
2023-07-10 08:53:13 -05:00
}
2023-07-19 03:31:32 -05:00
}
2023-07-10 08:53:13 -05:00
2023-07-19 03:31:32 -05:00
select {
case <- runner . StoppedCtx . Done ( ) :
handleStopped ( )
case <- runner . CancelledCtx . Done ( ) :
handleCancelled ( )
2023-07-10 08:53:13 -05:00
case <- runningCtx . Done ( ) :
// The operation exited normally.
2023-06-28 02:37:42 -05:00
}
2023-07-10 08:53:13 -05:00
return diags , cancelled
2023-07-10 05:42:05 -05:00
}
2023-07-10 08:53:13 -05:00
// state management
2023-07-10 05:42:05 -05:00
// TestStateManager is a helper struct to maintain the various state objects
// that a test file has to keep track of.
type TestStateManager struct {
2023-07-10 08:53:13 -05:00
runner * TestRunner
2023-07-10 05:42:05 -05:00
// State is the main state of the module under test during a single test
// file execution. This state will be updated by every run block without
// a modifier module block within the test file. At the end of the test
// file's execution everything in this state should be executed.
State * states . State
// States contains the states of every run block within a test file that
// executed using an alternative module. Any resources created by these
// run blocks also need to be tidied up, but only after the main state file
// has been handled.
States [ ] * TestModuleState
}
// TestModuleState holds the config and the state for a given run block that
// executed with a custom module.
type TestModuleState struct {
// State is the state after the module executed.
State * states . State
// Run is the config for the given run block, that contains the config
// under test and the variable values.
Run * moduletest . Run
}
2023-07-19 03:07:46 -05:00
func ( manager * TestStateManager ) cleanupStates ( file * moduletest . File , globals map [ string ] backend . UnparsedVariableValue ) {
2023-07-10 08:53:13 -05:00
if manager . runner . Cancelled {
2023-07-19 03:31:32 -05:00
// Don't try and clean anything up if the execution has been cancelled.
2023-07-10 08:53:13 -05:00
return
}
2023-07-10 05:42:05 -05:00
// First, we'll clean up the main state.
2023-07-19 03:31:32 -05:00
_ , _ , state , diags := manager . runner . execute ( manager , nil , file , manager . runner . Config , manager . State , & terraform . PlanOpts {
2023-07-10 08:53:13 -05:00
Mode : plans . DestroyMode ,
2023-07-19 03:07:46 -05:00
} , configs . ApplyTestCommand , globals )
2023-07-10 08:53:13 -05:00
manager . runner . View . DestroySummary ( diags , nil , file , state )
2023-07-10 05:42:05 -05:00
2023-07-19 03:31:32 -05:00
if manager . runner . Cancelled {
// In case things were cancelled during the last execution.
return
}
2023-07-10 05:42:05 -05:00
// Then we'll clean up the additional states for custom modules in reverse
// order.
for ix := len ( manager . States ) ; ix > 0 ; ix -- {
2023-07-10 08:53:13 -05:00
module := manager . States [ ix - 1 ]
if manager . runner . Cancelled {
// In case the cancellation came while a previous state was being
// destroyed.
2023-07-19 03:31:32 -05:00
return
2023-07-10 08:53:13 -05:00
}
2023-07-19 03:31:32 -05:00
_ , _ , state , diags := manager . runner . execute ( manager , module . Run , file , module . Run . Config . ConfigUnderTest , module . State , & terraform . PlanOpts {
2023-07-10 08:53:13 -05:00
Mode : plans . DestroyMode ,
2023-07-19 03:07:46 -05:00
} , configs . ApplyTestCommand , globals )
2023-07-10 08:53:13 -05:00
manager . runner . View . DestroySummary ( diags , module . Run , file , state )
}
}
// helper functions
// buildInputVariablesForTest creates a terraform.InputValues mapping for
// variable values that are relevant to the config being tested.
//
// Crucially, it differs from buildInputVariablesForAssertions in that it only
// includes variables that are reference by the config and not everything that
// is defined within the test run block and test file.
2023-07-19 03:07:46 -05:00
func buildInputVariablesForTest ( run * moduletest . Run , file * moduletest . File , config * configs . Config , globals map [ string ] backend . UnparsedVariableValue ) ( terraform . InputValues , tfdiags . Diagnostics ) {
variables := make ( map [ string ] backend . UnparsedVariableValue )
2023-07-10 08:53:13 -05:00
for name := range config . Module . Variables {
if run != nil {
if expr , exists := run . Config . Variables [ name ] ; exists {
// Local variables take precedence.
2023-07-19 03:07:46 -05:00
variables [ name ] = unparsedVariableValueExpression {
expr : expr ,
sourceType : terraform . ValueFromConfig ,
}
2023-07-10 08:53:13 -05:00
continue
}
}
if file != nil {
if expr , exists := file . Config . Variables [ name ] ; exists {
2023-07-19 03:07:46 -05:00
// If it's not set locally, it maybe set for the entire file.
variables [ name ] = unparsedVariableValueExpression {
expr : expr ,
sourceType : terraform . ValueFromConfig ,
}
2023-07-10 08:53:13 -05:00
continue
}
}
2023-07-19 03:07:46 -05:00
if globals != nil {
// If it's not set locally or at the file level, maybe it was
// defined globally.
if variable , exists := globals [ name ] ; exists {
variables [ name ] = variable
}
}
2023-07-10 08:53:13 -05:00
// If it's not set at all that might be okay if the variable is optional
// so we'll just not add anything to the map.
2023-06-28 02:37:42 -05:00
}
2023-07-10 08:53:13 -05:00
2023-07-19 03:07:46 -05:00
return backend . ParseVariableValues ( variables , config . Module . Variables )
2023-07-10 08:53:13 -05:00
}
// buildInputVariablesForAssertions creates a terraform.InputValues mapping that
// contains all the variables defined for a given run and file, alongside any
// unset variables that have defaults within the provided config.
//
// Crucially, it differs from buildInputVariablesForTest in that the returned
// input values include all variables available even if they are not defined
// within the config.
//
// This does mean the returned diags might contain warnings about variables not
// defined within the config. We might want to remove these warnings in the
// future, since it is actually okay for test files to have variables defined
// outside the configuration.
2023-07-19 03:07:46 -05:00
func buildInputVariablesForAssertions ( run * moduletest . Run , file * moduletest . File , config * configs . Config , globals map [ string ] backend . UnparsedVariableValue ) ( terraform . InputValues , tfdiags . Diagnostics ) {
variables := make ( map [ string ] backend . UnparsedVariableValue )
2023-07-10 08:53:13 -05:00
if run != nil {
for name , expr := range run . Config . Variables {
2023-07-19 03:07:46 -05:00
variables [ name ] = unparsedVariableValueExpression {
expr : expr ,
sourceType : terraform . ValueFromConfig ,
}
2023-07-10 08:53:13 -05:00
}
}
if file != nil {
for name , expr := range file . Config . Variables {
2023-07-19 03:07:46 -05:00
if _ , exists := variables [ name ] ; exists {
2023-07-10 08:53:13 -05:00
// Then this variable was defined at the run level and we want
// that value to take precedence.
continue
}
2023-07-19 03:07:46 -05:00
variables [ name ] = unparsedVariableValueExpression {
expr : expr ,
sourceType : terraform . ValueFromConfig ,
}
2023-07-10 08:53:13 -05:00
}
}
2023-07-19 03:07:46 -05:00
for name , variable := range globals {
if _ , exists := variables [ name ] ; exists {
// Then this value was already defined at either the run level
// or the file level, and we want those values to take
// precedence.
continue
2023-07-10 08:53:13 -05:00
}
2023-07-19 03:07:46 -05:00
variables [ name ] = variable
2023-07-10 08:53:13 -05:00
}
2023-07-19 03:07:46 -05:00
return backend . ParseVariableValues ( variables , config . Module . Variables )
2023-06-28 02:37:42 -05:00
}