| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package stackruntime | |
| import ( | |
| "context" | |
| "sort" | |
| "sync" | |
| "testing" | |
| "github.com/google/go-cmp/cmp" | |
| "github.com/hashicorp/terraform/internal/collections" | |
| "github.com/hashicorp/terraform/internal/stacks/stackaddrs" | |
| "github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks" | |
| ) | |
| type ExpectedHooks struct { | |
| ComponentExpanded []*hooks.ComponentInstances | |
| RemovedComponentExpanded []*hooks.RemovedComponentInstances | |
| PendingComponentInstancePlan collections.Set[stackaddrs.AbsComponentInstance] | |
| BeginComponentInstancePlan collections.Set[stackaddrs.AbsComponentInstance] | |
| EndComponentInstancePlan collections.Set[stackaddrs.AbsComponentInstance] | |
| ErrorComponentInstancePlan collections.Set[stackaddrs.AbsComponentInstance] | |
| DeferComponentInstancePlan collections.Set[stackaddrs.AbsComponentInstance] | |
| PendingComponentInstanceApply collections.Set[stackaddrs.AbsComponentInstance] | |
| BeginComponentInstanceApply collections.Set[stackaddrs.AbsComponentInstance] | |
| EndComponentInstanceApply collections.Set[stackaddrs.AbsComponentInstance] | |
| ErrorComponentInstanceApply collections.Set[stackaddrs.AbsComponentInstance] | |
| ReportResourceInstanceStatus []*hooks.ResourceInstanceStatusHookData | |
| ReportResourceInstanceProvisionerStatus []*hooks.ResourceInstanceProvisionerHookData | |
| ReportResourceInstanceDrift []*hooks.ResourceInstanceChange | |
| ReportResourceInstancePlanned []*hooks.ResourceInstanceChange | |
| ReportResourceInstanceDeferred []*hooks.DeferredResourceInstanceChange | |
| ReportActionInvocationPlanned []*hooks.ActionInvocation | |
| ReportActionInvocationStatus []*hooks.ActionInvocationStatusHookData | |
| ReportActionInvocationProgress []*hooks.ActionInvocationProgressHookData | |
| ReportComponentInstancePlanned []*hooks.ComponentInstanceChange | |
| ReportComponentInstanceApplied []*hooks.ComponentInstanceChange | |
| } | |
| func (eh *ExpectedHooks) Validate(t *testing.T, expectedHooks *ExpectedHooks) { | |
| sort.SliceStable(expectedHooks.ComponentExpanded, func(i, j int) bool { | |
| return expectedHooks.ComponentExpanded[i].ComponentAddr.String() < expectedHooks.ComponentExpanded[j].ComponentAddr.String() | |
| }) | |
| sort.SliceStable(expectedHooks.RemovedComponentExpanded, func(i, j int) bool { | |
| return expectedHooks.RemovedComponentExpanded[i].Source.String() < expectedHooks.RemovedComponentExpanded[j].Source.String() | |
| }) | |
| sort.SliceStable(expectedHooks.ReportResourceInstanceStatus, func(i, j int) bool { | |
| return expectedHooks.ReportResourceInstanceStatus[i].Addr.String() < expectedHooks.ReportResourceInstanceStatus[j].Addr.String() | |
| }) | |
| sort.SliceStable(expectedHooks.ReportResourceInstanceProvisionerStatus, func(i, j int) bool { | |
| return expectedHooks.ReportResourceInstanceProvisionerStatus[i].Addr.String() < expectedHooks.ReportResourceInstanceProvisionerStatus[j].Addr.String() | |
| }) | |
| sort.SliceStable(expectedHooks.ReportResourceInstanceDrift, func(i, j int) bool { | |
| return expectedHooks.ReportResourceInstanceDrift[i].Addr.String() < expectedHooks.ReportResourceInstanceDrift[j].Addr.String() | |
| }) | |
| sort.SliceStable(expectedHooks.ReportResourceInstancePlanned, func(i, j int) bool { | |
| return expectedHooks.ReportResourceInstancePlanned[i].Addr.String() < expectedHooks.ReportResourceInstancePlanned[j].Addr.String() | |
| }) | |
| sort.SliceStable(expectedHooks.ReportResourceInstanceDeferred, func(i, j int) bool { | |
| return expectedHooks.ReportResourceInstanceDeferred[i].Change.Addr.String() < expectedHooks.ReportResourceInstanceDeferred[j].Change.Addr.String() | |
| }) | |
| sort.SliceStable(expectedHooks.ReportActionInvocationPlanned, func(i, j int) bool { | |
| return expectedHooks.ReportActionInvocationPlanned[i].Addr.String() < expectedHooks.ReportActionInvocationPlanned[j].Addr.String() | |
| }) | |
| sort.SliceStable(expectedHooks.ReportActionInvocationStatus, func(i, j int) bool { | |
| return expectedHooks.ReportActionInvocationStatus[i].Addr.String() < expectedHooks.ReportActionInvocationStatus[j].Addr.String() | |
| }) | |
| sort.SliceStable(expectedHooks.ReportActionInvocationProgress, func(i, j int) bool { | |
| return expectedHooks.ReportActionInvocationProgress[i].Addr.String() < expectedHooks.ReportActionInvocationProgress[j].Addr.String() | |
| }) | |
| sort.SliceStable(expectedHooks.ReportComponentInstancePlanned, func(i, j int) bool { | |
| return expectedHooks.ReportComponentInstancePlanned[i].Addr.String() < expectedHooks.ReportComponentInstancePlanned[j].Addr.String() | |
| }) | |
| sort.SliceStable(expectedHooks.ReportComponentInstanceApplied, func(i, j int) bool { | |
| return expectedHooks.ReportComponentInstanceApplied[i].Addr.String() < expectedHooks.ReportComponentInstanceApplied[j].Addr.String() | |
| }) | |
| if diff := cmp.Diff(expectedHooks.ComponentExpanded, eh.ComponentExpanded); len(diff) > 0 { | |
| t.Errorf("wrong ComponentExpanded hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.RemovedComponentExpanded, eh.RemovedComponentExpanded); len(diff) > 0 { | |
| t.Errorf("wrong RemovedComponentExpanded hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.PendingComponentInstancePlan, eh.PendingComponentInstancePlan, collections.CmpOptions); len(diff) > 0 { | |
| t.Errorf("wrong PendingComponentInstancePlan hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.BeginComponentInstancePlan, eh.BeginComponentInstancePlan, collections.CmpOptions); len(diff) > 0 { | |
| t.Errorf("wrong BeginComponentInstancePlan hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.EndComponentInstancePlan, eh.EndComponentInstancePlan, collections.CmpOptions); len(diff) > 0 { | |
| t.Errorf("wrong EndComponentInstancePlan hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.ErrorComponentInstancePlan, eh.ErrorComponentInstancePlan, collections.CmpOptions); len(diff) > 0 { | |
| t.Errorf("wrong ErrorComponentInstancePlan hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.DeferComponentInstancePlan, eh.DeferComponentInstancePlan, collections.CmpOptions); len(diff) > 0 { | |
| t.Errorf("wrong DeferComponentInstancePlan hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.PendingComponentInstanceApply, eh.PendingComponentInstanceApply, collections.CmpOptions); len(diff) > 0 { | |
| t.Errorf("wrong PendingComponentInstanceApply hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.BeginComponentInstanceApply, eh.BeginComponentInstanceApply, collections.CmpOptions); len(diff) > 0 { | |
| t.Errorf("wrong BeginComponentInstanceApply hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.EndComponentInstanceApply, eh.EndComponentInstanceApply, collections.CmpOptions); len(diff) > 0 { | |
| t.Errorf("wrong EndComponentInstanceApply hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.ErrorComponentInstanceApply, eh.ErrorComponentInstanceApply, collections.CmpOptions); len(diff) > 0 { | |
| t.Errorf("wrong ErrorComponentInstanceApply hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.ReportResourceInstanceStatus, eh.ReportResourceInstanceStatus); len(diff) > 0 { | |
| t.Errorf("wrong ReportResourceInstanceStatus hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.ReportResourceInstanceProvisionerStatus, eh.ReportResourceInstanceProvisionerStatus); len(diff) > 0 { | |
| t.Errorf("wrong ReportResourceInstanceProvisionerStatus hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.ReportResourceInstanceDrift, eh.ReportResourceInstanceDrift); len(diff) > 0 { | |
| t.Errorf("wrong ReportResourceInstanceDrift hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.ReportResourceInstancePlanned, eh.ReportResourceInstancePlanned); len(diff) > 0 { | |
| t.Errorf("wrong ReportResourceInstancePlanned hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.ReportResourceInstanceDeferred, eh.ReportResourceInstanceDeferred); len(diff) > 0 { | |
| t.Errorf("wrong ReportResourceInstanceDeferred hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.ReportActionInvocationPlanned, eh.ReportActionInvocationPlanned); len(diff) > 0 { | |
| t.Errorf("wrong ReportActionInvocationPlanned hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.ReportActionInvocationStatus, eh.ReportActionInvocationStatus); len(diff) > 0 { | |
| t.Errorf("wrong ReportActionInvocationStatus hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.ReportActionInvocationProgress, eh.ReportActionInvocationProgress); len(diff) > 0 { | |
| t.Errorf("wrong ReportActionInvocationProgress hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.ReportComponentInstancePlanned, eh.ReportComponentInstancePlanned); len(diff) > 0 { | |
| t.Errorf("wrong ReportComponentInstancePlanned hooks: %s", diff) | |
| } | |
| if diff := cmp.Diff(expectedHooks.ReportComponentInstanceApplied, eh.ReportComponentInstanceApplied); len(diff) > 0 { | |
| t.Errorf("wrong ReportComponentInstanceApplied hooks: %s", diff) | |
| } | |
| } | |
| type CapturedHooks struct { | |
| ExpectedHooks | |
| sync.Mutex | |
| Planning bool | |
| } | |
| func NewCapturedHooks(planning bool) *CapturedHooks { | |
| return &CapturedHooks{ | |
| Planning: planning, | |
| ExpectedHooks: ExpectedHooks{ | |
| PendingComponentInstancePlan: collections.NewSet[stackaddrs.AbsComponentInstance](), | |
| BeginComponentInstancePlan: collections.NewSet[stackaddrs.AbsComponentInstance](), | |
| EndComponentInstancePlan: collections.NewSet[stackaddrs.AbsComponentInstance](), | |
| ErrorComponentInstancePlan: collections.NewSet[stackaddrs.AbsComponentInstance](), | |
| DeferComponentInstancePlan: collections.NewSet[stackaddrs.AbsComponentInstance](), | |
| PendingComponentInstanceApply: collections.NewSet[stackaddrs.AbsComponentInstance](), | |
| BeginComponentInstanceApply: collections.NewSet[stackaddrs.AbsComponentInstance](), | |
| EndComponentInstanceApply: collections.NewSet[stackaddrs.AbsComponentInstance](), | |
| ErrorComponentInstanceApply: collections.NewSet[stackaddrs.AbsComponentInstance](), | |
| }, | |
| } | |
| } | |
| func (ch *CapturedHooks) ComponentInstancePending(addr stackaddrs.AbsComponentInstance) bool { | |
| if ch.Planning { | |
| return ch.PendingComponentInstancePlan.Has(addr) | |
| } | |
| return ch.PendingComponentInstanceApply.Has(addr) | |
| } | |
| func (ch *CapturedHooks) ComponentInstanceBegun(addr stackaddrs.AbsComponentInstance) bool { | |
| if ch.Planning { | |
| return ch.BeginComponentInstancePlan.Has(addr) | |
| } | |
| return ch.BeginComponentInstanceApply.Has(addr) | |
| } | |
| func (ch *CapturedHooks) ComponentInstanceFinished(addr stackaddrs.AbsComponentInstance) bool { | |
| if ch.Planning { | |
| return ch.EndComponentInstancePlan.Has(addr) || ch.ErrorComponentInstancePlan.Has(addr) || ch.DeferComponentInstancePlan.Has(addr) | |
| } | |
| return ch.EndComponentInstanceApply.Has(addr) || ch.ErrorComponentInstanceApply.Has(addr) | |
| } | |
| func (ch *CapturedHooks) captureHooks() *Hooks { | |
| return &Hooks{ | |
| ComponentExpanded: func(ctx context.Context, instances *hooks.ComponentInstances) { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| ch.ComponentExpanded = append(ch.ComponentExpanded, instances) | |
| }, | |
| RemovedComponentExpanded: func(ctx context.Context, instances *hooks.RemovedComponentInstances) { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| ch.RemovedComponentExpanded = append(ch.RemovedComponentExpanded, instances) | |
| }, | |
| PendingComponentInstancePlan: func(ctx context.Context, instance stackaddrs.AbsComponentInstance) { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if ch.ComponentInstancePending(instance) { | |
| panic("tried to add pending component instance plan twice") | |
| } | |
| ch.PendingComponentInstancePlan.Add(instance) | |
| }, | |
| BeginComponentInstancePlan: func(ctx context.Context, instance stackaddrs.AbsComponentInstance) any { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if !ch.ComponentInstancePending(instance) { | |
| panic("tried to begin component instance plan before ending") | |
| } | |
| if ch.ComponentInstanceBegun(instance) { | |
| panic("tried to add begin component instance plan twice") | |
| } | |
| ch.BeginComponentInstancePlan.Add(instance) | |
| return nil | |
| }, | |
| EndComponentInstancePlan: func(ctx context.Context, a any, instance stackaddrs.AbsComponentInstance) any { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if !ch.BeginComponentInstancePlan.Has(instance) { | |
| panic("tried to end component instance plan before beginning") | |
| } | |
| if ch.EndComponentInstancePlan.Has(instance) || ch.ErrorComponentInstancePlan.Has(instance) || ch.DeferComponentInstancePlan.Has(instance) { | |
| panic("tried to add end component instance plan twice") | |
| } | |
| ch.EndComponentInstancePlan.Add(instance) | |
| return a | |
| }, | |
| ErrorComponentInstancePlan: func(ctx context.Context, a any, instance stackaddrs.AbsComponentInstance) any { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if !ch.ComponentInstanceBegun(instance) { | |
| panic("tried to end component instance plan before beginning") | |
| } | |
| if ch.ComponentInstanceFinished(instance) { | |
| panic("tried to add end component instance plan twice") | |
| } | |
| ch.ErrorComponentInstancePlan.Add(instance) | |
| return a | |
| }, | |
| DeferComponentInstancePlan: func(ctx context.Context, a any, instance stackaddrs.AbsComponentInstance) any { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if !ch.ComponentInstanceBegun(instance) { | |
| panic("tried to end component instance plan before beginning") | |
| } | |
| if ch.ComponentInstanceFinished(instance) { | |
| panic("tried to add end component instance plan twice") | |
| } | |
| ch.DeferComponentInstancePlan.Add(instance) | |
| return a | |
| }, | |
| PendingComponentInstanceApply: func(ctx context.Context, instance stackaddrs.AbsComponentInstance) { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if ch.ComponentInstancePending(instance) { | |
| panic("tried to add pending component instance apply twice") | |
| } | |
| ch.PendingComponentInstanceApply.Add(instance) | |
| }, | |
| BeginComponentInstanceApply: func(ctx context.Context, instance stackaddrs.AbsComponentInstance) any { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if !ch.ComponentInstancePending(instance) { | |
| panic("tried to begin component before pending") | |
| } | |
| if ch.ComponentInstanceBegun(instance) { | |
| panic("tried to add begin component instance apply twice") | |
| } | |
| ch.BeginComponentInstanceApply.Add(instance) | |
| return nil | |
| }, | |
| EndComponentInstanceApply: func(ctx context.Context, a any, instance stackaddrs.AbsComponentInstance) any { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if !ch.ComponentInstanceBegun(instance) { | |
| panic("tried to end component before beginning") | |
| } | |
| if ch.ComponentInstanceFinished(instance) { | |
| panic("tried to add end component instance apply twice") | |
| } | |
| ch.EndComponentInstanceApply.Add(instance) | |
| return a | |
| }, | |
| ErrorComponentInstanceApply: func(ctx context.Context, a any, instance stackaddrs.AbsComponentInstance) any { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if !ch.ComponentInstanceBegun(instance) { | |
| panic("tried to end component before beginning") | |
| } | |
| if ch.ComponentInstanceFinished(instance) { | |
| panic("tried to add error component instance apply twice") | |
| } | |
| ch.ErrorComponentInstanceApply.Add(instance) | |
| return a | |
| }, | |
| ReportResourceInstanceStatus: func(ctx context.Context, a any, data *hooks.ResourceInstanceStatusHookData) any { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if !ch.ComponentInstanceBegun(data.Addr.Component) { | |
| panic("tried to report resource instance status before component") | |
| } | |
| if ch.ComponentInstanceFinished(data.Addr.Component) { | |
| panic("tried to report resource instance status after component") | |
| } | |
| ch.ReportResourceInstanceStatus = append(ch.ReportResourceInstanceStatus, data) | |
| return a | |
| }, | |
| ReportResourceInstanceProvisionerStatus: func(ctx context.Context, a any, data *hooks.ResourceInstanceProvisionerHookData) any { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if !ch.ComponentInstanceBegun(data.Addr.Component) { | |
| panic("tried to report resource instance provisioner status before component") | |
| } | |
| if ch.ComponentInstanceFinished(data.Addr.Component) { | |
| panic("tried to report resource instance provisioner status after component") | |
| } | |
| ch.ReportResourceInstanceProvisionerStatus = append(ch.ReportResourceInstanceProvisionerStatus, data) | |
| return a | |
| }, | |
| ReportResourceInstanceDrift: func(ctx context.Context, a any, change *hooks.ResourceInstanceChange) any { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if !ch.ComponentInstanceBegun(change.Addr.Component) { | |
| panic("tried to report resource instance drift before component") | |
| } | |
| if ch.ComponentInstanceFinished(change.Addr.Component) { | |
| panic("tried to report resource instance drift after component") | |
| } | |
| ch.ReportResourceInstanceDrift = append(ch.ReportResourceInstanceDrift, change) | |
| return a | |
| }, | |
| ReportResourceInstancePlanned: func(ctx context.Context, a any, change *hooks.ResourceInstanceChange) any { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if !ch.ComponentInstanceBegun(change.Addr.Component) { | |
| panic("tried to report resource instance planned before component") | |
| } | |
| if ch.ComponentInstanceFinished(change.Addr.Component) { | |
| panic("tried to report resource instance planned after component") | |
| } | |
| ch.ReportResourceInstancePlanned = append(ch.ReportResourceInstancePlanned, change) | |
| return a | |
| }, | |
| ReportResourceInstanceDeferred: func(ctx context.Context, a any, change *hooks.DeferredResourceInstanceChange) any { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if !ch.ComponentInstanceBegun(change.Change.Addr.Component) { | |
| panic("tried to report resource instance deferred before component") | |
| } | |
| if ch.ComponentInstanceFinished(change.Change.Addr.Component) { | |
| panic("tried to report resource instance deferred after component") | |
| } | |
| ch.ReportResourceInstanceDeferred = append(ch.ReportResourceInstanceDeferred, change) | |
| return a | |
| }, | |
| ReportActionInvocationPlanned: func(ctx context.Context, a any, ai *hooks.ActionInvocation) any { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if !ch.ComponentInstanceBegun(ai.Addr.Component) { | |
| panic("tried to report action invocation planned before component") | |
| } | |
| if ch.ComponentInstanceFinished(ai.Addr.Component) { | |
| panic("tried to report action invocation planned after component") | |
| } | |
| ch.ReportActionInvocationPlanned = append(ch.ReportActionInvocationPlanned, ai) | |
| return a | |
| }, | |
| ReportActionInvocationStatus: func(ctx context.Context, a any, status *hooks.ActionInvocationStatusHookData) any { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if !ch.ComponentInstanceBegun(status.Addr.Component) { | |
| panic("tried to report action invocation status before component") | |
| } | |
| if ch.ComponentInstanceFinished(status.Addr.Component) { | |
| panic("tried to report action invocation status after component") | |
| } | |
| ch.ReportActionInvocationStatus = append(ch.ReportActionInvocationStatus, status) | |
| return a | |
| }, | |
| ReportActionInvocationProgress: func(ctx context.Context, a any, progress *hooks.ActionInvocationProgressHookData) any { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if !ch.ComponentInstanceBegun(progress.Addr.Component) { | |
| panic("tried to report action invocation progress before component") | |
| } | |
| if ch.ComponentInstanceFinished(progress.Addr.Component) { | |
| panic("tried to report action invocation progress after component") | |
| } | |
| ch.ReportActionInvocationProgress = append(ch.ReportActionInvocationProgress, progress) | |
| return a | |
| }, | |
| ReportComponentInstancePlanned: func(ctx context.Context, a any, change *hooks.ComponentInstanceChange) any { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if !ch.ComponentInstanceBegun(change.Addr) { | |
| panic("tried to report component instance planned before component") | |
| } | |
| if ch.ComponentInstanceFinished(change.Addr) { | |
| panic("tried to report component instance planned after component") | |
| } | |
| ch.ReportComponentInstancePlanned = append(ch.ReportComponentInstancePlanned, change) | |
| return a | |
| }, | |
| ReportComponentInstanceApplied: func(ctx context.Context, a any, change *hooks.ComponentInstanceChange) any { | |
| ch.Lock() | |
| defer ch.Unlock() | |
| if !ch.ComponentInstanceBegun(change.Addr) { | |
| panic("tried to report component instance planned before component") | |
| } | |
| if ch.ComponentInstanceFinished(change.Addr) { | |
| panic("tried to report component instance planned after component") | |
| } | |
| ch.ReportComponentInstanceApplied = append(ch.ReportComponentInstanceApplied, change) | |
| return a | |
| }, | |
| } | |
| } |