Skip to content
Open
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.15/BUG FIXES-20260309-084347.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: BUG FIXES
body: 'test: fix dependency ordering in parallel cleanup'
time: 2026-03-09T08:43:47.688216+01:00
custom:
Issue: "38247"
132 changes: 129 additions & 3 deletions internal/command/test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/hashicorp/terraform/internal/addrs"
testing_command "github.com/hashicorp/terraform/internal/command/testing"
"github.com/hashicorp/terraform/internal/command/views"
viewsJson "github.com/hashicorp/terraform/internal/command/views/json"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configload"
"github.com/hashicorp/terraform/internal/getproviders"
Expand All @@ -40,6 +41,11 @@ import (
"github.com/hashicorp/terraform/internal/tfdiags"
)

type jsonLine struct {
Type string `json:"type"`
TestRun *viewsJson.TestRunStatus `json:"test_run,omitempty"`
}

func TestTest_Runs(t *testing.T) {
tcs := map[string]struct {
override string
Expand Down Expand Up @@ -1515,7 +1521,7 @@ func TestTest_ParallelTeardown(t *testing.T) {
value = test_resource.foo.value
}
`,
// c2 => a1, b1 => a1, a2 => b1, b2 => c1
// c2 => a2, b1 => a1, a2 => b1, b2 => c2
"parallel.tftest.hcl": `
test {
parallel = true
Expand Down Expand Up @@ -1558,7 +1564,7 @@ func TestTest_ParallelTeardown(t *testing.T) {
run "a2" {
state_key = "a"
variables {
foo = run.b1.value
foo = run.b2.value
}

providers = {
Expand Down Expand Up @@ -1588,7 +1594,7 @@ func TestTest_ParallelTeardown(t *testing.T) {
run "c2" {
state_key = "c"
variables {
foo = run.a1.value
foo = run.a2.value
}
}
`,
Expand Down Expand Up @@ -5674,6 +5680,126 @@ func TestTest_TeardownOrder(t *testing.T) {
}
}

func TestTest_ParallelDeps(t *testing.T) {
// This tests that parallel dependencies are handled correctly during teardown.
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "parallel_deps")), td)
t.Chdir(td)

provider := testing_command.NewProvider(nil)
providerSource, close := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
})
defer close()

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,
AllowExperimentalFeatures: true,
}

init := &InitCommand{
Meta: meta,
}

output := done(t)

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

// Reset the streams for the next command.
streams, done = terminal.StreamsForTesting(t)
meta.Streams = streams
meta.View = views.NewView(streams)

c := &TestCommand{
Meta: meta,
}

code := c.Run([]string{"-no-color", "-json"})
output = done(t)

if code != 0 {
t.Errorf("expected status code 0 but got %d", code)
}

actual := output.All()

var teardownOrder []string
lines, err := parseJSONLines(t, actual)
if err != nil {
t.Fatal(err)
}
for _, parsed := range lines {
if parsed.Type != "test_run" || parsed.TestRun == nil {
continue
}
if parsed.TestRun.Progress != "teardown" {
continue
}

// We only care about teardowns with elapsed time of 0, indicating the start
// of the teardown phase.
if parsed.TestRun.Elapsed == nil || *parsed.TestRun.Elapsed != 0 {
continue
}
teardownOrder = append(teardownOrder, parsed.TestRun.Run)
}

// test_two depends on test_three (via run.test_three.id), so during
// teardown the dependency order should be reversed, i.e test_two must
// be torn down before test_three.
testThreeIdx := -1
testTwoIdx := -1
for i, name := range teardownOrder {
switch name {
case "test_three":
testThreeIdx = i
case "test_two":
testTwoIdx = i
}
}

if testThreeIdx == -1 {
t.Fatalf("expected test_three teardown (elapsed=0) in output but did not find it.\nteardown order: %v\nfull output:\n%s", teardownOrder, actual)
}
if testTwoIdx == -1 {
t.Fatalf("expected test_two teardown (elapsed=0) in output but did not find it.\nteardown order: %v\nfull output:\n%s", teardownOrder, actual)
}
if testThreeIdx <= testTwoIdx {
t.Errorf("expected test_two teardown to come before test_three teardown, but got test_three at index %d and test_two at index %d.\nteardown order: %v\nfull output:\n%s",
testThreeIdx, testTwoIdx, teardownOrder, actual)
}

if provider.ResourceCount() != 0 {
t.Errorf("should have deleted all resources")
}
}

func parseJSONLines(t *testing.T, actual string) ([]jsonLine, error) {
t.Helper()
var lines []jsonLine
for line := range strings.SplitSeq(strings.TrimSpace(actual), "\n") {
if line == "" {
continue
}
var parsed jsonLine
if err := json.Unmarshal([]byte(line), &parsed); err != nil {
return nil, err
}
lines = append(lines, parsed)
}
return lines, nil
}

// testModuleInline takes a map of path -> config strings and yields a config
// structure with those files loaded from disk
func testModuleInline(t *testing.T, sources map[string]string) (*configs.Config, string, func()) {
Expand Down
2 changes: 1 addition & 1 deletion internal/command/testdata/test/cleanup/main.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ run "test_four" {
variables {
id = "test_four"
}
}
}
20 changes: 20 additions & 0 deletions internal/command/testdata/test/parallel_deps/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
variable "id" {
type = string
}

variable "unused" {
type = string
default = "unused"
}

resource "test_resource" "resource" {
value = var.id
}

output "id" {
value = test_resource.resource.id
}

output "unused" {
value = var.unused
}
30 changes: 30 additions & 0 deletions internal/command/testdata/test/parallel_deps/main.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
test {
parallel = true
}

run "test" {
variables {
id = "test"
unused = "unused"
}
}

run "test_two" {
state_key = "state2"
variables {
// This dependency is a later run, but that should be fine because we are in parallel mode.
id = run.test_three.id

// The output state data for this dependency will also be left behind, but the actual
// resource will have been destroyed by the cleanup step of test_three.
unused = run.test.unused
}
}

run "test_three" {
state_key = "state3"
variables {
id = "test_three"
unused = run.test.unused
}
}
2 changes: 1 addition & 1 deletion internal/moduletest/graph/test_graph_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func (b *TestGraphBuilder) Steps() []terraform.GraphTransformer {
&TestVariablesTransformer{File: b.File},
terraform.DynamicTransformer(validateRunConfigs),
terraform.DynamicTransformer(func(g *terraform.Graph) error {
cleanup := &TeardownSubgraph{opts: opts, parent: g, mode: b.CommandMode}
cleanup := &TeardownSubgraph{opts: opts, runGraph: g, mode: b.CommandMode}
g.Add(cleanup)

// ensure that the teardown node runs after all the run nodes
Expand Down
Loading