|
|
|
|
@ -9,194 +9,207 @@ import (
|
|
|
|
|
"github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// TestActionInvocationHooksValidation demonstrates how to validate that
|
|
|
|
|
// action invocation status hooks are being called during apply operations.
|
|
|
|
|
//
|
|
|
|
|
// This test shows all three levels of validation:
|
|
|
|
|
// 1. Hooks are captured via CapturedHooks helper
|
|
|
|
|
// 2. Multiple hooks fire for a single action (state transitions)
|
|
|
|
|
// 3. Hook data contains all required fields
|
|
|
|
|
// TestActionInvocationHooksValidation validates that action invocation status
|
|
|
|
|
// hooks work correctly, including enum values, hook data structure, and lifecycle ordering.
|
|
|
|
|
func TestActionInvocationHooksValidation(t *testing.T) {
|
|
|
|
|
t.Run("validate_hook_capture_mechanism", func(t *testing.T) {
|
|
|
|
|
// Level 1: Verify CapturedHooks mechanism works
|
|
|
|
|
capturedHooks := NewCapturedHooks(false) // false = apply phase, true = planning phase
|
|
|
|
|
t.Run("hook_capture_mechanism", func(t *testing.T) {
|
|
|
|
|
// Verify CapturedHooks mechanism initializes correctly
|
|
|
|
|
capturedHooks := NewCapturedHooks(false) // false = apply phase
|
|
|
|
|
|
|
|
|
|
if capturedHooks == nil {
|
|
|
|
|
t.Fatal("CapturedHooks should not be nil")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify the hooks object exists and has expected fields
|
|
|
|
|
// Verify the hooks slice starts empty (nil or zero length)
|
|
|
|
|
if len(capturedHooks.ReportActionInvocationStatus) != 0 {
|
|
|
|
|
t.Fatalf("expected empty initial hook list, got %d", len(capturedHooks.ReportActionInvocationStatus))
|
|
|
|
|
t.Errorf("expected empty initial hook list, got %d", len(capturedHooks.ReportActionInvocationStatus))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.Log("✓ CapturedHooks mechanism is properly set up")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("validate_hook_structure", func(t *testing.T) {
|
|
|
|
|
// Level 3: Validate ActionInvocationStatusHookData structure
|
|
|
|
|
// Verify we can append to it
|
|
|
|
|
capturedHooks.ReportActionInvocationStatus = append(
|
|
|
|
|
capturedHooks.ReportActionInvocationStatus,
|
|
|
|
|
&hooks.ActionInvocationStatusHookData{
|
|
|
|
|
Addr: mustAbsActionInvocationInstance("component.test.action.example.run"),
|
|
|
|
|
ProviderAddr: mustDefaultRootProvider("testing").Provider,
|
|
|
|
|
Status: hooks.ActionInvocationRunning,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// This should be the structure of each hook:
|
|
|
|
|
_ = &hooks.ActionInvocationStatusHookData{
|
|
|
|
|
// Addr: stackaddrs.AbsActionInvocationInstance - the action address
|
|
|
|
|
// ProviderAddr: addrs.Provider - the provider address
|
|
|
|
|
// Status: ActionInvocationStatus - status value (Pending, Running, Completed, Errored)
|
|
|
|
|
if len(capturedHooks.ReportActionInvocationStatus) != 1 {
|
|
|
|
|
t.Errorf("after append, expected 1 hook, got %d", len(capturedHooks.ReportActionInvocationStatus))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.Log("✓ ActionInvocationStatusHookData structure is properly defined")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("validate_action_invocation_status_enum", func(t *testing.T) {
|
|
|
|
|
// Verify that ActionInvocationStatus enum values exist
|
|
|
|
|
validStatuses := map[string]bool{
|
|
|
|
|
// These are the valid status values an action can have
|
|
|
|
|
"Invalid": true, // ActionInvocationInvalid (0)
|
|
|
|
|
"Pending": true, // ActionInvocationPending (1)
|
|
|
|
|
"Running": true, // ActionInvocationRunning (2)
|
|
|
|
|
"Completed": true, // ActionInvocationCompleted (3)
|
|
|
|
|
"Errored": true, // ActionInvocationErrored (4)
|
|
|
|
|
t.Run("action_invocation_status_enum", func(t *testing.T) {
|
|
|
|
|
// Test that all enum constants are defined and have valid string representations
|
|
|
|
|
statuses := []hooks.ActionInvocationStatus{
|
|
|
|
|
hooks.ActionInvocationStatusInvalid,
|
|
|
|
|
hooks.ActionInvocationPending,
|
|
|
|
|
hooks.ActionInvocationRunning,
|
|
|
|
|
hooks.ActionInvocationCompleted,
|
|
|
|
|
hooks.ActionInvocationErrored,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(validStatuses) != 5 {
|
|
|
|
|
t.Fatalf("expected 5 status values, got %d", len(validStatuses))
|
|
|
|
|
expectedStrings := map[hooks.ActionInvocationStatus]string{
|
|
|
|
|
hooks.ActionInvocationStatusInvalid: "ActionInvocationStatusInvalid",
|
|
|
|
|
hooks.ActionInvocationPending: "ActionInvocationPending",
|
|
|
|
|
hooks.ActionInvocationRunning: "ActionInvocationRunning",
|
|
|
|
|
hooks.ActionInvocationCompleted: "ActionInvocationCompleted",
|
|
|
|
|
hooks.ActionInvocationErrored: "ActionInvocationErrored",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.Logf("✓ Action invocation status enum has %d valid values: %v",
|
|
|
|
|
len(validStatuses), validStatuses)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("validate_hook_firing_pattern", func(t *testing.T) {
|
|
|
|
|
// Level 2: Demonstrate expected hook firing pattern
|
|
|
|
|
// For a successful action invocation, we expect:
|
|
|
|
|
// 1. StartAction() fires with Running status
|
|
|
|
|
// 2. ProgressAction() optionally fires with intermediate status
|
|
|
|
|
// 3. CompleteAction() fires with Completed or Errored status
|
|
|
|
|
|
|
|
|
|
expectedSequence := []string{
|
|
|
|
|
"Running", // StartAction called
|
|
|
|
|
"Completed", // CompleteAction called successfully
|
|
|
|
|
// Verify String() returns expected values
|
|
|
|
|
for _, status := range statuses {
|
|
|
|
|
str := status.String()
|
|
|
|
|
expected, ok := expectedStrings[status]
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Errorf("unexpected status constant: %v", status)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if str != expected {
|
|
|
|
|
t.Errorf("status %v: expected String() = %q, got %q", status, expected, str)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
alternativeSequence := []string{
|
|
|
|
|
"Running", // StartAction called
|
|
|
|
|
"Errored", // CompleteAction called with error
|
|
|
|
|
// Verify ForProtobuf() returns valid values (non-negative)
|
|
|
|
|
for _, status := range statuses {
|
|
|
|
|
proto := status.ForProtobuf()
|
|
|
|
|
if proto < 0 {
|
|
|
|
|
t.Errorf("status %v has invalid protobuf value: %v", status, proto)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.Logf("Expected hook sequence 1 (success): %v", expectedSequence)
|
|
|
|
|
t.Logf("Expected hook sequence 2 (error): %v", alternativeSequence)
|
|
|
|
|
|
|
|
|
|
t.Log("✓ Hook firing pattern documented")
|
|
|
|
|
// Verify we have exactly 5 status values
|
|
|
|
|
if len(statuses) != 5 {
|
|
|
|
|
t.Errorf("expected 5 status constants, got %d", len(statuses))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("logging_points_exist", func(t *testing.T) {
|
|
|
|
|
// This test documents where logging has been added for validation
|
|
|
|
|
t.Run("hook_data_structure", func(t *testing.T) {
|
|
|
|
|
// Validate ActionInvocationStatusHookData structure and methods
|
|
|
|
|
hookData := &hooks.ActionInvocationStatusHookData{
|
|
|
|
|
Addr: mustAbsActionInvocationInstance("component.test.action.example.run"),
|
|
|
|
|
ProviderAddr: mustDefaultRootProvider("testing").Provider,
|
|
|
|
|
Status: hooks.ActionInvocationRunning,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loggingLocations := map[string]string{
|
|
|
|
|
"terraform_hook.go:StartAction": "Logs action address and Running status",
|
|
|
|
|
"terraform_hook.go:ProgressAction": "Logs progress mapping and status transition",
|
|
|
|
|
"terraform_hook.go:CompleteAction": "Logs completion with Completed/Errored status",
|
|
|
|
|
"stacks.go:ReportActionInvocationStatus": "Logs at gRPC boundary with proto status value",
|
|
|
|
|
// Verify fields are set
|
|
|
|
|
if hookData.Addr.String() == "" {
|
|
|
|
|
t.Error("Addr should not be empty")
|
|
|
|
|
}
|
|
|
|
|
if hookData.ProviderAddr.String() == "" {
|
|
|
|
|
t.Error("ProviderAddr should not be empty")
|
|
|
|
|
}
|
|
|
|
|
if hookData.Status == hooks.ActionInvocationStatusInvalid {
|
|
|
|
|
t.Error("Status should not be Invalid when explicitly set to Running")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for location, purpose := range loggingLocations {
|
|
|
|
|
t.Logf(" %s: %s", location, purpose)
|
|
|
|
|
// Verify String() method
|
|
|
|
|
str := hookData.String()
|
|
|
|
|
if str == "" || str == "<nil>" {
|
|
|
|
|
t.Errorf("String() should return valid representation, got: %q", str)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.Logf("✓ %d logging points have been added for debugging", len(loggingLocations))
|
|
|
|
|
// Verify String() contains address
|
|
|
|
|
if !contains(str, "component.test") {
|
|
|
|
|
t.Errorf("String() should contain address, got: %q", str)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify nil handling
|
|
|
|
|
var nilHook *hooks.ActionInvocationStatusHookData
|
|
|
|
|
if nilHook.String() != "<nil>" {
|
|
|
|
|
t.Errorf("nil hook String() should return <nil>, got: %q", nilHook.String())
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("validation_checklist", func(t *testing.T) {
|
|
|
|
|
// Use this checklist to verify the complete setup
|
|
|
|
|
checklist := []struct {
|
|
|
|
|
name string
|
|
|
|
|
validate func() bool
|
|
|
|
|
t.Run("hook_status_lifecycle_ordering", func(t *testing.T) {
|
|
|
|
|
// Test expected hook status sequences for different scenarios
|
|
|
|
|
testCases := []struct {
|
|
|
|
|
name string
|
|
|
|
|
capturedStatuses []hooks.ActionInvocationStatus
|
|
|
|
|
wantValid bool
|
|
|
|
|
description string
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "Logging imports added to terraform_hook.go",
|
|
|
|
|
validate: func() bool {
|
|
|
|
|
// Check: log.Printf should be called in hook methods
|
|
|
|
|
return true
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "Logging imports added to stacks.go",
|
|
|
|
|
validate: func() bool {
|
|
|
|
|
// Check: log.Printf should be called in ReportActionInvocationStatus
|
|
|
|
|
return true
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "Binary rebuilt with logging",
|
|
|
|
|
validate: func() bool {
|
|
|
|
|
// Check: Run `make install` after logging additions
|
|
|
|
|
return true
|
|
|
|
|
name: "successful_action",
|
|
|
|
|
capturedStatuses: []hooks.ActionInvocationStatus{
|
|
|
|
|
hooks.ActionInvocationRunning,
|
|
|
|
|
hooks.ActionInvocationCompleted,
|
|
|
|
|
},
|
|
|
|
|
wantValid: true,
|
|
|
|
|
description: "Action starts running and completes successfully",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "Log contains hook method entries",
|
|
|
|
|
validate: func() bool {
|
|
|
|
|
// Check: grep "terraform_hook.*Action\|ReportActionInvocationStatus" terraform.log
|
|
|
|
|
return true
|
|
|
|
|
name: "failed_action",
|
|
|
|
|
capturedStatuses: []hooks.ActionInvocationStatus{
|
|
|
|
|
hooks.ActionInvocationRunning,
|
|
|
|
|
hooks.ActionInvocationErrored,
|
|
|
|
|
},
|
|
|
|
|
wantValid: true,
|
|
|
|
|
description: "Action starts running but encounters an error",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "Unit tests capture hooks via CapturedHooks",
|
|
|
|
|
validate: func() bool {
|
|
|
|
|
// Check: Test uses NewCapturedHooks() and captureHooks()
|
|
|
|
|
return true
|
|
|
|
|
name: "pending_then_running_then_completed",
|
|
|
|
|
capturedStatuses: []hooks.ActionInvocationStatus{
|
|
|
|
|
hooks.ActionInvocationPending,
|
|
|
|
|
hooks.ActionInvocationRunning,
|
|
|
|
|
hooks.ActionInvocationCompleted,
|
|
|
|
|
},
|
|
|
|
|
wantValid: true,
|
|
|
|
|
description: "Action goes through all states including pending",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "Hook status values match enum",
|
|
|
|
|
validate: func() bool {
|
|
|
|
|
// Check: Running, Completed, Errored are valid values
|
|
|
|
|
return true
|
|
|
|
|
name: "invalid_only_completed",
|
|
|
|
|
capturedStatuses: []hooks.ActionInvocationStatus{
|
|
|
|
|
hooks.ActionInvocationCompleted,
|
|
|
|
|
},
|
|
|
|
|
wantValid: false,
|
|
|
|
|
description: "Invalid: completed without running",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.Logf("Validation Checklist (%d items):", len(checklist))
|
|
|
|
|
for i, item := range checklist {
|
|
|
|
|
t.Logf(" %d. %s", i+1, item.name)
|
|
|
|
|
for _, tc := range testCases {
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
|
// Verify we captured the expected number of statuses
|
|
|
|
|
if len(tc.capturedStatuses) == 0 {
|
|
|
|
|
t.Error("test case should have at least one status")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For valid sequences, verify terminal state is at the end
|
|
|
|
|
if tc.wantValid && len(tc.capturedStatuses) > 0 {
|
|
|
|
|
lastStatus := tc.capturedStatuses[len(tc.capturedStatuses)-1]
|
|
|
|
|
isTerminal := lastStatus == hooks.ActionInvocationCompleted ||
|
|
|
|
|
lastStatus == hooks.ActionInvocationErrored
|
|
|
|
|
|
|
|
|
|
if !isTerminal {
|
|
|
|
|
t.Errorf("valid sequence should end in terminal state (Completed/Errored), got %v", lastStatus)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For invalid sequences starting with Completed, verify it's actually invalid
|
|
|
|
|
if !tc.wantValid && len(tc.capturedStatuses) > 0 {
|
|
|
|
|
firstStatus := tc.capturedStatuses[0]
|
|
|
|
|
if firstStatus == hooks.ActionInvocationCompleted && len(tc.capturedStatuses) == 1 {
|
|
|
|
|
// This is indeed invalid - can't complete without running
|
|
|
|
|
t.Logf("correctly identified invalid sequence: %v", tc.capturedStatuses)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestActionInvocationHooksLoggingOutput demonstrates what the logging output
|
|
|
|
|
// should look like when action invocation hooks are fired during apply.
|
|
|
|
|
//
|
|
|
|
|
// Expected log output pattern:
|
|
|
|
|
//
|
|
|
|
|
// [DEBUG] terraform_hook.StartAction called for action: component.nulls.action.bufo_print.success
|
|
|
|
|
// [DEBUG] Reporting action invocation status for action: component.nulls.action.bufo_print.success (Running)
|
|
|
|
|
// [DEBUG] ReportActionInvocationStatus called: Action=component.nulls.action.bufo_print.success, Status=Running, Provider=registry.terraform.io/austinvalle/bufo
|
|
|
|
|
// [DEBUG] Sending ActionInvocationStatus to gRPC client: Addr=component.nulls.action.bufo_print.success, Status=2 (proto)
|
|
|
|
|
// [DEBUG] ActionInvocationStatus event successfully sent to client
|
|
|
|
|
// [DEBUG] terraform_hook.CompleteAction called for action: component.nulls.action.bufo_print.success, error=<nil>
|
|
|
|
|
// [DEBUG] Action completed successfully - reporting Completed status
|
|
|
|
|
// [DEBUG] Reporting action invocation status for action: component.nulls.action.bufo_print.success (Completed)
|
|
|
|
|
// [DEBUG] ReportActionInvocationStatus called: Action=component.nulls.action.bufo_print.success, Status=Completed, Provider=registry.terraform.io/austinvalle/bufo
|
|
|
|
|
// [DEBUG] Sending ActionInvocationStatus to gRPC client: Addr=component.nulls.action.bufo_print.success, Status=3 (proto)
|
|
|
|
|
// [DEBUG] ActionInvocationStatus event successfully sent to client
|
|
|
|
|
func TestActionInvocationHooksLoggingOutput(t *testing.T) {
|
|
|
|
|
t.Run("logging_documentation", func(t *testing.T) {
|
|
|
|
|
expectedLogPatterns := []string{
|
|
|
|
|
"terraform_hook.StartAction called for action",
|
|
|
|
|
"ReportActionInvocationStatus called",
|
|
|
|
|
"Sending ActionInvocationStatus to gRPC client",
|
|
|
|
|
"ActionInvocationStatus event successfully sent to client",
|
|
|
|
|
"terraform_hook.CompleteAction called for action",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.Logf("When action invocation hooks fire, you should see these log patterns:")
|
|
|
|
|
for i, pattern := range expectedLogPatterns {
|
|
|
|
|
t.Logf(" %d. [DEBUG] %s", i+1, pattern)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.Log("\nStatus enum values in logs:")
|
|
|
|
|
t.Log(" Status=1 (proto) = Pending")
|
|
|
|
|
t.Log(" Status=2 (proto) = Running")
|
|
|
|
|
t.Log(" Status=3 (proto) = Completed")
|
|
|
|
|
t.Log(" Status=4 (proto) = Errored")
|
|
|
|
|
})
|
|
|
|
|
// contains checks if a string contains a substring
|
|
|
|
|
func contains(s, substr string) bool {
|
|
|
|
|
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && containsHelper(s, substr))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func containsHelper(s, substr string) bool {
|
|
|
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
|
|
|
if s[i:i+len(substr)] == substr {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|