Files
actions-runner-controller/controllers/actions.github.com/ephemeralrunner_lifecycle_test.go
T

304 lines
11 KiB
Go

package actionsgithubcom
import (
"testing"
v1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestAggregateEphemeralRunnerLifecycle(t *testing.T) {
now := metav1.Now()
tests := []struct {
name string
runners []v1alpha1.EphemeralRunner
expected EphemeralRunnerLifecycleBuckets
}{
{
name: "empty list",
runners: []v1alpha1.EphemeralRunner{},
expected: EphemeralRunnerLifecycleBuckets{
Pending: 0, Running: 0, Succeeded: 0, Failed: 0, Outdated: 0, Deleting: 0,
},
},
{
name: "all phases without deletion timestamp",
runners: []v1alpha1.EphemeralRunner{
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhasePending}},
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseRunning}},
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseSucceeded}},
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseFailed}},
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseOutdated}},
},
expected: EphemeralRunnerLifecycleBuckets{
Pending: 1, Running: 1, Succeeded: 1, Failed: 1, Outdated: 1, Deleting: 0,
},
},
{
name: "empty phase defaults to pending",
runners: []v1alpha1.EphemeralRunner{
{Status: v1alpha1.EphemeralRunnerStatus{Phase: ""}},
{Status: v1alpha1.EphemeralRunnerStatus{}},
},
expected: EphemeralRunnerLifecycleBuckets{
Pending: 2, Running: 0, Succeeded: 0, Failed: 0, Outdated: 0, Deleting: 0,
},
},
{
name: "unrecognized phase defaults to pending",
runners: []v1alpha1.EphemeralRunner{
{Status: v1alpha1.EphemeralRunnerStatus{Phase: "UnknownPhase"}},
},
expected: EphemeralRunnerLifecycleBuckets{
Pending: 1, Running: 0, Succeeded: 0, Failed: 0, Outdated: 0, Deleting: 0,
},
},
{
name: "deletion timestamp takes precedence over all phases",
runners: []v1alpha1.EphemeralRunner{
{
ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now},
Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhasePending},
},
{
ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now},
Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseRunning},
},
{
ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now},
Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseSucceeded},
},
{
ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now},
Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseFailed},
},
{
ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now},
Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseOutdated},
},
},
expected: EphemeralRunnerLifecycleBuckets{
Pending: 0, Running: 0, Succeeded: 0, Failed: 0, Outdated: 0, Deleting: 5,
},
},
{
name: "edge case: running phase with deletion timestamp goes to deleting",
runners: []v1alpha1.EphemeralRunner{
{
ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now},
Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseRunning},
},
},
expected: EphemeralRunnerLifecycleBuckets{
Pending: 0, Running: 0, Succeeded: 0, Failed: 0, Outdated: 0, Deleting: 1,
},
},
{
name: "mixed runners with and without deletion timestamp",
runners: []v1alpha1.EphemeralRunner{
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseRunning}},
{
ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now},
Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseRunning},
},
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseFailed}},
{
ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now},
Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseFailed},
},
},
expected: EphemeralRunnerLifecycleBuckets{
Pending: 0, Running: 1, Succeeded: 0, Failed: 1, Outdated: 0, Deleting: 2,
},
},
{
name: "multiple runners in same phase",
runners: []v1alpha1.EphemeralRunner{
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseRunning}},
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseRunning}},
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseRunning}},
},
expected: EphemeralRunnerLifecycleBuckets{
Pending: 0, Running: 3, Succeeded: 0, Failed: 0, Outdated: 0, Deleting: 0,
},
},
{
name: "deletion timestamp with empty phase goes to deleting",
runners: []v1alpha1.EphemeralRunner{
{
ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now},
Status: v1alpha1.EphemeralRunnerStatus{Phase: ""},
},
},
expected: EphemeralRunnerLifecycleBuckets{
Pending: 0, Running: 0, Succeeded: 0, Failed: 0, Outdated: 0, Deleting: 1,
},
},
{
name: "comprehensive mixed scenario",
runners: []v1alpha1.EphemeralRunner{
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhasePending}},
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhasePending}},
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseRunning}},
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseRunning}},
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseRunning}},
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseSucceeded}},
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseFailed}},
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseFailed}},
{Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseOutdated}},
{
ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now},
Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseRunning},
},
{
ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now},
Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseSucceeded},
},
},
expected: EphemeralRunnerLifecycleBuckets{
Pending: 2, Running: 3, Succeeded: 1, Failed: 2, Outdated: 1, Deleting: 2,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := AggregateEphemeralRunnerLifecycle(tt.runners)
if result.Pending != tt.expected.Pending {
t.Errorf("Pending count mismatch: got %d, want %d", result.Pending, tt.expected.Pending)
}
if result.Running != tt.expected.Running {
t.Errorf("Running count mismatch: got %d, want %d", result.Running, tt.expected.Running)
}
if result.Succeeded != tt.expected.Succeeded {
t.Errorf("Succeeded count mismatch: got %d, want %d", result.Succeeded, tt.expected.Succeeded)
}
if result.Failed != tt.expected.Failed {
t.Errorf("Failed count mismatch: got %d, want %d", result.Failed, tt.expected.Failed)
}
if result.Outdated != tt.expected.Outdated {
t.Errorf("Outdated count mismatch: got %d, want %d", result.Outdated, tt.expected.Outdated)
}
if result.Deleting != tt.expected.Deleting {
t.Errorf("Deleting count mismatch: got %d, want %d", result.Deleting, tt.expected.Deleting)
}
total := result.Pending + result.Running + result.Succeeded + result.Failed + result.Outdated + result.Deleting
if total != len(tt.runners) {
t.Errorf("Total count mismatch: counted %d runners but input had %d runners", total, len(tt.runners))
}
})
}
}
func TestAggregateEphemeralRunnerLifecycle_EdgePrecedence(t *testing.T) {
now := metav1.Now()
tests := []struct {
name string
phase v1alpha1.EphemeralRunnerPhase
hasDeletionTime bool
expectedBucket string
}{
{
name: "Running without deletion timestamp",
phase: v1alpha1.EphemeralRunnerPhaseRunning,
hasDeletionTime: false,
expectedBucket: "running",
},
{
name: "Running with deletion timestamp goes to deleting",
phase: v1alpha1.EphemeralRunnerPhaseRunning,
hasDeletionTime: true,
expectedBucket: "deleting",
},
{
name: "Succeeded without deletion timestamp",
phase: v1alpha1.EphemeralRunnerPhaseSucceeded,
hasDeletionTime: false,
expectedBucket: "succeeded",
},
{
name: "Succeeded with deletion timestamp goes to deleting",
phase: v1alpha1.EphemeralRunnerPhaseSucceeded,
hasDeletionTime: true,
expectedBucket: "deleting",
},
{
name: "Failed without deletion timestamp",
phase: v1alpha1.EphemeralRunnerPhaseFailed,
hasDeletionTime: false,
expectedBucket: "failed",
},
{
name: "Failed with deletion timestamp goes to deleting",
phase: v1alpha1.EphemeralRunnerPhaseFailed,
hasDeletionTime: true,
expectedBucket: "deleting",
},
{
name: "Outdated without deletion timestamp",
phase: v1alpha1.EphemeralRunnerPhaseOutdated,
hasDeletionTime: false,
expectedBucket: "outdated",
},
{
name: "Outdated with deletion timestamp goes to deleting",
phase: v1alpha1.EphemeralRunnerPhaseOutdated,
hasDeletionTime: true,
expectedBucket: "deleting",
},
{
name: "Pending without deletion timestamp",
phase: v1alpha1.EphemeralRunnerPhasePending,
hasDeletionTime: false,
expectedBucket: "pending",
},
{
name: "Pending with deletion timestamp goes to deleting",
phase: v1alpha1.EphemeralRunnerPhasePending,
hasDeletionTime: true,
expectedBucket: "deleting",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
runner := v1alpha1.EphemeralRunner{
Status: v1alpha1.EphemeralRunnerStatus{Phase: tt.phase},
}
if tt.hasDeletionTime {
runner.DeletionTimestamp = &now
}
result := AggregateEphemeralRunnerLifecycle([]v1alpha1.EphemeralRunner{runner})
var foundBucket string
if result.Pending == 1 {
foundBucket = "pending"
} else if result.Running == 1 {
foundBucket = "running"
} else if result.Succeeded == 1 {
foundBucket = "succeeded"
} else if result.Failed == 1 {
foundBucket = "failed"
} else if result.Outdated == 1 {
foundBucket = "outdated"
} else if result.Deleting == 1 {
foundBucket = "deleting"
}
if foundBucket != tt.expectedBucket {
t.Errorf("Expected runner to be in %s bucket, but found in %s bucket", tt.expectedBucket, foundBucket)
}
total := result.Pending + result.Running + result.Succeeded + result.Failed + result.Outdated + result.Deleting
if total != 1 {
t.Errorf("Expected exactly 1 runner to be counted, but got %d", total)
}
})
}
}