// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package checks_test import ( "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" tftesting "github.com/hashicorp/terraform/internal/terraform/testing" ) func TestChecksHappyPath(t *testing.T) { const fixtureDir = "testdata/happypath" cfg, _, configCleanup := tftesting.MustLoadConfigForTests(t, fixtureDir, "tests") t.Cleanup(configCleanup) resourceA := addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "null_resource", Name: "a", }.InModule(addrs.RootModule) resourceNoChecks := addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "null_resource", Name: "no_checks", }.InModule(addrs.RootModule) resourceNonExist := addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "null_resource", Name: "nonexist", }.InModule(addrs.RootModule) rootOutput := addrs.OutputValue{ Name: "a", }.InModule(addrs.RootModule) moduleChild := addrs.RootModule.Child("child") resourceB := addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "null_resource", Name: "b", }.InModule(moduleChild) resourceC := addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "null_resource", Name: "c", }.InModule(moduleChild) childOutput := addrs.OutputValue{ Name: "b", }.InModule(moduleChild) checkBlock := addrs.Check{ Name: "check", }.InModule(addrs.RootModule) // First some consistency checks to make sure our configuration is the // shape we are relying on it to be. if addr := resourceA; cfg.Module.ResourceByAddr(addr.Resource) == nil { t.Fatalf("configuration does not include %s", addr) } if addr := resourceB; cfg.Children["child"].Module.ResourceByAddr(addr.Resource) == nil { t.Fatalf("configuration does not include %s", addr) } if addr := resourceNoChecks; cfg.Module.ResourceByAddr(addr.Resource) == nil { t.Fatalf("configuration does not include %s", addr) } if addr := resourceNonExist; cfg.Module.ResourceByAddr(addr.Resource) != nil { t.Fatalf("configuration includes %s, which is not supposed to exist", addr) } if addr := checkBlock; cfg.Module.Checks[addr.Check.Name] == nil { t.Fatalf("configuration does not include %s", addr) } ///////////////////////////////////////////////////////////////////////// state := checks.NewState(cfg) missing := 0 if addr := resourceA; !state.ConfigHasChecks(addr) { t.Errorf("checks not detected for %s", addr) missing++ } if addr := resourceB; !state.ConfigHasChecks(addr) { t.Errorf("checks not detected for %s", addr) missing++ } if addr := resourceC; !state.ConfigHasChecks(addr) { t.Errorf("checks not detected for %s", addr) missing++ } if addr := rootOutput; !state.ConfigHasChecks(addr) { t.Errorf("checks not detected for %s", addr) missing++ } if addr := childOutput; !state.ConfigHasChecks(addr) { t.Errorf("checks not detected for %s", addr) missing++ } if addr := resourceNoChecks; state.ConfigHasChecks(addr) { t.Errorf("checks detected for %s, even though it has none", addr) } if addr := resourceNonExist; state.ConfigHasChecks(addr) { t.Errorf("checks detected for %s, even though it doesn't exist", addr) } if addr := checkBlock; !state.ConfigHasChecks(addr) { t.Errorf("checks not detected for %s", addr) missing++ } if missing > 0 { t.Fatalf("missing some configuration objects we'd need for subsequent testing") } ///////////////////////////////////////////////////////////////////////// // Everything should start with status unknown. { wantConfigAddrs := addrs.MakeSet[addrs.ConfigCheckable]( resourceA, resourceB, resourceC, rootOutput, childOutput, checkBlock, ) gotConfigAddrs := state.AllConfigAddrs() if diff := cmp.Diff(wantConfigAddrs, gotConfigAddrs); diff != "" { t.Errorf("wrong detected config addresses\n%s", diff) } for _, configAddr := range gotConfigAddrs { if got, want := state.AggregateCheckStatus(configAddr), checks.StatusUnknown; got != want { t.Errorf("incorrect initial aggregate check status for %s: %s, but want %s", configAddr, got, want) } } } ///////////////////////////////////////////////////////////////////////// // The following are steps that would normally be done by Terraform Core // as part of visiting checkable objects during the graph walk. We're // simulating a likely sequence of calls here for testing purposes, but // Terraform Core won't necessarily visit all of these in exactly the // same order every time and so this is just one possible valid ordering // of calls. resourceInstA := resourceA.Resource.Absolute(addrs.RootModuleInstance).Instance(addrs.NoKey) rootOutputInst := rootOutput.OutputValue.Absolute(addrs.RootModuleInstance) moduleChildInst := addrs.RootModuleInstance.Child("child", addrs.NoKey) resourceInstB := resourceB.Resource.Absolute(moduleChildInst).Instance(addrs.NoKey) resourceInstC0 := resourceC.Resource.Absolute(moduleChildInst).Instance(addrs.IntKey(0)) resourceInstC1 := resourceC.Resource.Absolute(moduleChildInst).Instance(addrs.IntKey(1)) childOutputInst := childOutput.OutputValue.Absolute(moduleChildInst) checkBlockInst := checkBlock.Check.Absolute(addrs.RootModuleInstance) state.ReportCheckableObjects(resourceA, addrs.MakeSet[addrs.Checkable](resourceInstA)) state.ReportCheckResult(resourceInstA, addrs.ResourcePrecondition, 0, checks.StatusPass) state.ReportCheckResult(resourceInstA, addrs.ResourcePrecondition, 1, checks.StatusPass) state.ReportCheckResult(resourceInstA, addrs.ResourcePostcondition, 0, checks.StatusPass) state.ReportCheckableObjects(resourceB, addrs.MakeSet[addrs.Checkable](resourceInstB)) state.ReportCheckResult(resourceInstB, addrs.ResourcePrecondition, 0, checks.StatusPass) state.ReportCheckableObjects(resourceC, addrs.MakeSet[addrs.Checkable](resourceInstC0, resourceInstC1)) state.ReportCheckResult(resourceInstC0, addrs.ResourcePostcondition, 0, checks.StatusPass) state.ReportCheckResult(resourceInstC1, addrs.ResourcePostcondition, 0, checks.StatusPass) state.ReportCheckableObjects(childOutput, addrs.MakeSet[addrs.Checkable](childOutputInst)) state.ReportCheckResult(childOutputInst, addrs.OutputPrecondition, 0, checks.StatusPass) state.ReportCheckableObjects(rootOutput, addrs.MakeSet[addrs.Checkable](rootOutputInst)) state.ReportCheckResult(rootOutputInst, addrs.OutputPrecondition, 0, checks.StatusPass) state.ReportCheckableObjects(checkBlock, addrs.MakeSet[addrs.Checkable](checkBlockInst)) state.ReportCheckResult(checkBlockInst, addrs.CheckAssertion, 0, checks.StatusPass) ///////////////////////////////////////////////////////////////////////// // This "section" is simulating what we might do to report the results // of the checks after a run completes. { configCount := 0 for _, configAddr := range state.AllConfigAddrs() { configCount++ if got, want := state.AggregateCheckStatus(configAddr), checks.StatusPass; got != want { t.Errorf("incorrect final aggregate check status for %s: %s, but want %s", configAddr, got, want) } } if got, want := configCount, 6; got != want { t.Errorf("incorrect number of known config addresses %d; want %d", got, want) } } { objAddrs := addrs.MakeSet[addrs.Checkable]( resourceInstA, rootOutputInst, resourceInstB, resourceInstC0, resourceInstC1, childOutputInst, checkBlockInst, ) for _, addr := range objAddrs { if got, want := state.ObjectCheckStatus(addr), checks.StatusPass; got != want { t.Errorf("incorrect final check status for object %s: %s, but want %s", addr, got, want) } } } }