Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/v1.13/ENHANCEMENTS-20250527-095755.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: ENHANCEMENTS
body: ' TF Test: Allow parallel execution of teardown operations'
time: 2025-05-27T09:57:55.267277+02:00
custom:
Issue: "37169"
6 changes: 6 additions & 0 deletions internal/backend/local/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"maps"

"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend/backendrun"
"github.com/hashicorp/terraform/internal/command/junit"
"github.com/hashicorp/terraform/internal/command/views"
Expand All @@ -23,6 +24,7 @@ import (
"github.com/hashicorp/terraform/internal/moduletest"
"github.com/hashicorp/terraform/internal/moduletest/graph"
hcltest "github.com/hashicorp/terraform/internal/moduletest/hcl"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
)
Expand Down Expand Up @@ -66,6 +68,9 @@ type TestSuiteRunner struct {

Concurrency int
semaphore terraform.Semaphore

// ExternalProviders is a map of pre-configured external providers
ExternalProviders map[addrs.RootProviderConfig]providers.Interface
}

func (runner *TestSuiteRunner) Stop() {
Expand Down Expand Up @@ -259,6 +264,7 @@ func (runner *TestFileRunner) Test(file *moduletest.File) {
File: file,
GlobalVars: runner.EvalContext.VariableCaches.GlobalVariables,
ContextOpts: runner.Suite.Opts,
Providers: runner.Suite.ExternalProviders,
}
g, diags := b.Build()
file.Diagnostics = file.Diagnostics.Append(diags)
Expand Down
3 changes: 3 additions & 0 deletions internal/command/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ type Meta struct {
// Override certain behavior for tests within this package
testingOverrides *testingOverrides

// Only used in tests for now
externalProviders map[addrs.RootProviderConfig]providers.Interface

//----------------------------------------------------------
// Private: do not set these
//----------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions internal/command/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ func (c *TestCommand) Run(rawArgs []string) int {
CancelledCtx: cancelCtx,
Filter: args.Filter,
Verbose: args.Verbose,
ExternalProviders: c.externalProviders,
}

// JUnit output is only compatible with local test execution
Expand Down
99 changes: 99 additions & 0 deletions internal/command/test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"runtime"
"strings"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/cli"
Expand Down Expand Up @@ -813,6 +814,104 @@ func TestTest_Parallel(t *testing.T) {
}
}

func TestTest_ParallelJSON(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "parallel")), td)
defer testChdir(t, td)()

provider := testing_command.NewProvider(&testing_command.ResourceStore{Data: make(map[string]cty.Value)})
providerSource, close := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
})
defer close()

configuredProv := func() providers.Interface {
prov := testing_command.NewProvider(nil)
req := providers.ConfigureProviderRequest{Config: cty.DynamicVal}
prov.ConfigureProvider(req)
prov.Provider.ConfigureProvider(req)
return prov.Provider
}

streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)

meta := Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
externalProviders: map[addrs.RootProviderConfig]providers.Interface{
{Provider: addrs.MustParseProviderSourceString("test")}: configuredProv(),
{Provider: addrs.MustParseProviderSourceString("test"), Alias: "start"}: configuredProv(),
{Provider: addrs.MustParseProviderSourceString("test"), Alias: "state_foo"}: configuredProv(),
{Provider: addrs.MustParseProviderSourceString("test"), Alias: "state_bar"}: configuredProv(),
{Provider: addrs.MustParseProviderSourceString("test"), Alias: "state_baz"}: configuredProv(),
{Provider: addrs.MustParseProviderSourceString("test"), Alias: "state_qux"}: configuredProv(),
},
}

init := &InitCommand{Meta: meta}
if code := init.Run(nil); code != 0 {
output := done(t)
t.Fatalf("expected status code %d but got %d: %s", 0, code, output.All())
}

c := &TestCommand{Meta: meta}
c.Run([]string{"-json", "-no-color"})
output := done(t).All()

if !strings.Contains(output, "40 passed, 0 failed") {
t.Errorf("output didn't produce the right output:\n\n%s", output)
}

// Split the log into lines
lines := strings.Split(output, "\n")

// Find the start of the teardown and complete timestamps
// The difference is the approximate duration of the test teardown operation.
// This test is running in parallel, so we expect the teardown to also run in parallel.
// We sleep for 3 seconds in the test teardown to simulate a long-running destroy.
// There are 6 unique state keys in the parallel test, so we expect the teardown to take less than 3*6 (18) seconds.
var startTimestamp, completeTimestamp string
for _, line := range lines {
if strings.Contains(line, `{"path":"parallel.tftest.hcl","progress":"teardown"`) {
var obj map[string]interface{}
if err := json.Unmarshal([]byte(line), &obj); err == nil {
if ts, ok := obj["@timestamp"].(string); ok {
startTimestamp = ts
}
}
} else if strings.Contains(line, `{"path":"parallel.tftest.hcl","progress":"complete"`) {
var obj map[string]interface{}
if err := json.Unmarshal([]byte(line), &obj); err == nil {
if ts, ok := obj["@timestamp"].(string); ok {
completeTimestamp = ts
}
}
}
}

if startTimestamp == "" || completeTimestamp == "" {
t.Fatalf("could not find start or complete timestamp in log output")
}

startTime, err := time.Parse(time.RFC3339Nano, startTimestamp)
if err != nil {
t.Fatalf("failed to parse start timestamp: %v", err)
}
completeTime, err := time.Parse(time.RFC3339Nano, completeTimestamp)
if err != nil {
t.Fatalf("failed to parse complete timestamp: %v", err)
}
dur := completeTime.Sub(startTime)
if dur > 10*time.Second {
t.Fatalf("parallel.tftest.hcl duration took too long: %0.2f seconds", dur.Seconds())
}
}

func TestTest_InterruptSkipsRemaining(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "with_interrupt_and_additional_file")), td)
Expand Down
1 change: 1 addition & 0 deletions internal/command/testdata/test/parallel/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ variable "input" {

resource "test_resource" "foo" {
value = var.input
destroy_wait_seconds = 3
}

output "value" {
Expand Down
Loading