Skip to content

Commit

Permalink
Build and execute tf test file runs in a graph
Browse files Browse the repository at this point in the history
  • Loading branch information
dsa0x committed Jan 8, 2025
1 parent 3a5a21e commit 169d18e
Show file tree
Hide file tree
Showing 7 changed files with 1,099 additions and 7 deletions.
75 changes: 70 additions & 5 deletions internal/backend/local/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/hashicorp/terraform/internal/backend/backendrun"
"github.com/hashicorp/terraform/internal/command/views"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/dag"
"github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/lang/langrefs"
"github.com/hashicorp/terraform/internal/logging"
Expand Down Expand Up @@ -295,8 +296,66 @@ func (runner *TestFileRunner) Test(file *moduletest.File) {
file.Status = file.Status.Merge(moduletest.Pass)
}

// Now execute the runs.
for _, run := range file.Runs {
// Build the graph for the file. Currently, we build this serially to maintain
// the existing sequential functionality of test runs. In the future, we could
// optimize this by parallelizing runs that do not depend on each other.
b := terraform.TestGraphBuilder{File: file}
graph, diags := b.Build(addrs.RootModuleInstance)
file.Diagnostics = file.Diagnostics.Append(diags)

// walk and execute the graph
diags = runner.walkGraph(graph)
file.Diagnostics = file.Diagnostics.Append(diags)
}

// walkGraph goes through the graph and execute each run it finds.
func (runner *TestFileRunner) walkGraph(g *terraform.Graph) tfdiags.Diagnostics {
par := 1 // defaults to 1 for now, so that run are executed sequentially
sem := terraform.NewSemaphore(par)

// Walk the graph.
walkFn := func(v dag.Vertex) (diags tfdiags.Diagnostics) {
// the walkFn is called asynchronously, and needs to be recovered
// separately in the case of a panic.
defer logging.PanicHandler()

log.Printf("[TRACE] vertex %q: starting visit (%T)", dag.VertexName(v), v)

defer func() {
if r := recover(); r != nil {
// If the walkFn panics, we get confusing logs about how the
// visit was complete. To stop this, we'll catch the panic log
// that the vertex panicked without finishing and re-panic.
log.Printf("[ERROR] vertex %q panicked", dag.VertexName(v))
panic(r) // re-panic
}

if diags.HasErrors() {
for _, diag := range diags {
if diag.Severity() == tfdiags.Error {
desc := diag.Description()
log.Printf("[ERROR] vertex %q error: %s", dag.VertexName(v), desc.Summary)
}
}
log.Printf("[TRACE] vertex %q: visit complete, with errors", dag.VertexName(v))
} else {
log.Printf("[TRACE] vertex %q: visit complete", dag.VertexName(v))
}
}()

runNode, ok := v.(*terraform.NodeTestRun)
if !ok {
// If the vertex isn't a test run, we'll just skip it.
return
}

// Acquire a lock on the semaphore
sem.Acquire()
defer sem.Release()

file := runNode.File()
run := runNode.Run()

if runner.Suite.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
Expand All @@ -312,7 +371,8 @@ func (runner *TestFileRunner) Test(file *moduletest.File) {
// following test as skipped, print the status, and move on.
run.Status = moduletest.Skip
runner.Suite.View.Run(run, file, moduletest.Complete, 0)
continue
// continue
return
}

if file.Status == moduletest.Error {
Expand All @@ -321,7 +381,8 @@ func (runner *TestFileRunner) Test(file *moduletest.File) {
// skipped, print the status, and move on.
run.Status = moduletest.Skip
runner.Suite.View.Run(run, file, moduletest.Complete, 0)
continue
// continue
return
}

key := MainStateIdentifier
Expand All @@ -344,7 +405,8 @@ func (runner *TestFileRunner) Test(file *moduletest.File) {

run.Status = moduletest.Error
file.Status = moduletest.Error
continue // Abort!
// continue // Abort!
return
}

if _, exists := runner.RelevantStates[key]; !exists {
Expand Down Expand Up @@ -374,7 +436,10 @@ func (runner *TestFileRunner) Test(file *moduletest.File) {
}
runner.Suite.View.Run(run, file, moduletest.Complete, 0)
file.Status = file.Status.Merge(run.Status)
return
}

return g.AcyclicGraph.Walk(walkFn)
}

func (runner *TestFileRunner) run(run *moduletest.Run, file *moduletest.File, state *states.State, config *configs.Config) (*states.State, bool) {
Expand Down
4 changes: 4 additions & 0 deletions internal/command/test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,10 @@ func TestTest_Runs(t *testing.T) {
expectedErr: []string{"Ephemeral resource instance has expired", "Ephemeral resources cannot be asserted"},
code: 1,
},
"simple_testdata": {
expectedOut: []string{" passed, 0 failed."},
code: 0,
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
Expand Down
15 changes: 15 additions & 0 deletions internal/command/testdata/test/simple_testdata/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

variable "input" {
type = string
}


resource "test_resource" "a" {
value = var.input
}

resource "test_resource" "c" {}

output "name" {
value = test_resource.a.value
}
Loading

0 comments on commit 169d18e

Please sign in to comment.