Skip to content

Commit e58d249

Browse files
authored
test: add missing validation and coverage tests (#286)
* test: add missing validation and coverage tests - Add tests for budget cap validation (maxTotalCpuIncrease, maxTotalMemoryIncrease) - Add tests for safetyObservationPeriod validation (negative, below 1m) - Add tests for canary.observationPeriod validation (negative, below 1m) - Add comprehensive SLO guardrails validation tests (10 cases covering empty name, duplicates, empty query/threshold, NaN/Inf, invalid comparison, evaluationWindow bounds) - Add SupportsThrottle test covering both inner implementations - Add StatefulSet/DaemonSet nil selector coverage for PodSelectorLabels Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca> * fix: use go-version-file in setup-e2e-cluster composite action PR #285 changed CI to pass go-version-file instead of go-version, but the composite action input was not updated. The unrecognized input was silently dropped, causing setup-go to skip installation and fall back to the runner default Go 1.24.13, which cannot satisfy go.mod's go >= 1.26.4 requirement. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca> --------- Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
1 parent 3805728 commit e58d249

4 files changed

Lines changed: 220 additions & 3 deletions

File tree

.github/actions/setup-e2e-cluster/action.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ inputs:
88
kubeconfig-path:
99
description: Path to write the kubeconfig file
1010
required: true
11-
go-version:
12-
description: Go version to install
11+
go-version-file:
12+
description: Path to go.mod for Go version resolution
1313
required: true
1414
k3d-version:
1515
description: k3d version
@@ -47,7 +47,7 @@ runs:
4747
4848
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
4949
with:
50-
go-version: ${{ inputs.go-version }}
50+
go-version-file: ${{ inputs.go-version-file }}
5151
cache: true
5252

5353
- name: Cache E2E container images

internal/controller/workload_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,8 @@ func TestWorkload_AdapterPodSelectorLabels(t *testing.T) {
897897
},
898898
},
899899
}, false, []string{"app"}},
900+
{"StatefulSet without selector returns nil", &appsv1.StatefulSet{}, true, nil},
901+
{"DaemonSet without selector returns nil", &appsv1.DaemonSet{}, true, nil},
900902
}
901903

902904
for _, tt := range tests {

internal/metrics/ratelimit_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,20 @@ func (m *mockThrottleCollector) GetThrottleRatio(_ context.Context, _, _, _ stri
133133
return m.throttleRatio, nil
134134
}
135135

136+
func TestRateLimitedCollector_SupportsThrottle(t *testing.T) {
137+
t.Run("inner implements ThrottleChecker", func(t *testing.T) {
138+
inner := &mockThrottleCollector{}
139+
rl := NewRateLimitedCollector(inner, 10, 20)
140+
assert.True(t, rl.SupportsThrottle())
141+
})
142+
143+
t.Run("inner does not implement ThrottleChecker", func(t *testing.T) {
144+
inner := &mockCollector{}
145+
rl := NewRateLimitedCollector(inner, 10, 20)
146+
assert.False(t, rl.SupportsThrottle())
147+
})
148+
}
149+
136150
func TestRateLimitedCollector_GetThrottleRatio_Delegates(t *testing.T) {
137151
inner := &mockThrottleCollector{throttleRatio: 0.75}
138152
rl := NewRateLimitedCollector(inner, 10, 20)

internal/webhook/defaults_validation_test.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,207 @@ func TestDefaultsValidate_CooldownInvalid(t *testing.T) {
656656
}
657657
}
658658

659+
func TestDefaultsValidate_BudgetCapInvalid(t *testing.T) {
660+
tests := []struct {
661+
name string
662+
spec attunev1alpha1.AttuneDefaultsSpec
663+
wantErr string
664+
}{
665+
{
666+
name: "negative maxTotalCpuIncrease",
667+
spec: attunev1alpha1.AttuneDefaultsSpec{
668+
UpdateStrategy: &attunev1alpha1.UpdateStrategy{
669+
MaxTotalCPUIncrease: resourcePtr("-500m"),
670+
},
671+
},
672+
wantErr: "maxTotalCpuIncrease must be non-negative",
673+
},
674+
{
675+
name: "negative maxTotalMemoryIncrease",
676+
spec: attunev1alpha1.AttuneDefaultsSpec{
677+
UpdateStrategy: &attunev1alpha1.UpdateStrategy{
678+
MaxTotalMemoryIncrease: resourcePtr("-1Gi"),
679+
},
680+
},
681+
wantErr: "maxTotalMemoryIncrease must be non-negative",
682+
},
683+
}
684+
for _, tc := range tests {
685+
t.Run(tc.name, func(t *testing.T) {
686+
v := &AttuneDefaultsValidator{}
687+
defaults := &attunev1alpha1.AttuneDefaults{
688+
ObjectMeta: metav1.ObjectMeta{Name: "default"},
689+
Spec: tc.spec,
690+
}
691+
692+
_, err := v.ValidateCreate(context.Background(), defaults)
693+
require.Error(t, err)
694+
assert.Contains(t, err.Error(), tc.wantErr)
695+
})
696+
}
697+
}
698+
699+
func resourcePtr(s string) *resource.Quantity {
700+
q := resource.MustParse(s)
701+
return &q
702+
}
703+
704+
func TestDefaultsValidate_SafetyObservationPeriodInvalid(t *testing.T) {
705+
tests := []struct {
706+
name string
707+
period time.Duration
708+
wantErr string
709+
}{
710+
{"negative", -time.Minute, "safetyObservationPeriod must be non-negative"},
711+
{"below minimum", 30 * time.Second, "safetyObservationPeriod must be at least 1m"},
712+
}
713+
for _, tc := range tests {
714+
t.Run(tc.name, func(t *testing.T) {
715+
v := &AttuneDefaultsValidator{}
716+
defaults := &attunev1alpha1.AttuneDefaults{
717+
ObjectMeta: metav1.ObjectMeta{Name: "default"},
718+
Spec: attunev1alpha1.AttuneDefaultsSpec{
719+
UpdateStrategy: &attunev1alpha1.UpdateStrategy{
720+
SafetyObservationPeriod: &metav1.Duration{Duration: tc.period},
721+
},
722+
},
723+
}
724+
725+
_, err := v.ValidateCreate(context.Background(), defaults)
726+
require.Error(t, err)
727+
assert.Contains(t, err.Error(), tc.wantErr)
728+
})
729+
}
730+
}
731+
732+
func TestDefaultsValidate_CanaryObservationPeriodInvalid(t *testing.T) {
733+
tests := []struct {
734+
name string
735+
period time.Duration
736+
wantErr string
737+
}{
738+
{"negative", -time.Minute, "canary.observationPeriod must be non-negative"},
739+
{"below minimum", 30 * time.Second, "canary.observationPeriod must be at least 1m"},
740+
}
741+
for _, tc := range tests {
742+
t.Run(tc.name, func(t *testing.T) {
743+
v := &AttuneDefaultsValidator{}
744+
defaults := &attunev1alpha1.AttuneDefaults{
745+
ObjectMeta: metav1.ObjectMeta{Name: "default"},
746+
Spec: attunev1alpha1.AttuneDefaultsSpec{
747+
UpdateStrategy: &attunev1alpha1.UpdateStrategy{
748+
Canary: &attunev1alpha1.CanaryConfig{
749+
Percentage: 10,
750+
ObservationPeriod: metav1.Duration{Duration: tc.period},
751+
},
752+
},
753+
},
754+
}
755+
756+
_, err := v.ValidateCreate(context.Background(), defaults)
757+
require.Error(t, err)
758+
assert.Contains(t, err.Error(), tc.wantErr)
759+
})
760+
}
761+
}
762+
763+
func TestDefaultsValidate_SLOGuardrailsInvalid(t *testing.T) {
764+
tests := []struct {
765+
name string
766+
guardrails []attunev1alpha1.SLOGuardrail
767+
wantErr string
768+
}{
769+
{
770+
name: "empty name",
771+
guardrails: []attunev1alpha1.SLOGuardrail{
772+
{Name: "", Query: "up", Threshold: "1"},
773+
},
774+
wantErr: "name is required",
775+
},
776+
{
777+
name: "duplicate name",
778+
guardrails: []attunev1alpha1.SLOGuardrail{
779+
{Name: "slo1", Query: "up", Threshold: "1"},
780+
{Name: "slo1", Query: "down", Threshold: "0"},
781+
},
782+
wantErr: "is duplicated",
783+
},
784+
{
785+
name: "empty query",
786+
guardrails: []attunev1alpha1.SLOGuardrail{
787+
{Name: "slo1", Query: "", Threshold: "1"},
788+
},
789+
wantErr: "query is required",
790+
},
791+
{
792+
name: "empty threshold",
793+
guardrails: []attunev1alpha1.SLOGuardrail{
794+
{Name: "slo1", Query: "up", Threshold: ""},
795+
},
796+
wantErr: "threshold is required",
797+
},
798+
{
799+
name: "non-numeric threshold",
800+
guardrails: []attunev1alpha1.SLOGuardrail{
801+
{Name: "slo1", Query: "up", Threshold: "abc"},
802+
},
803+
wantErr: "not a valid number",
804+
},
805+
{
806+
name: "NaN threshold",
807+
guardrails: []attunev1alpha1.SLOGuardrail{
808+
{Name: "slo1", Query: "up", Threshold: "NaN"},
809+
},
810+
wantErr: "must be a finite number",
811+
},
812+
{
813+
name: "Inf threshold",
814+
guardrails: []attunev1alpha1.SLOGuardrail{
815+
{Name: "slo1", Query: "up", Threshold: "Inf"},
816+
},
817+
wantErr: "must be a finite number",
818+
},
819+
{
820+
name: "invalid comparison",
821+
guardrails: []attunev1alpha1.SLOGuardrail{
822+
{Name: "slo1", Query: "up", Threshold: "1", Comparison: "equals"},
823+
},
824+
wantErr: "must be \"above\" or \"below\"",
825+
},
826+
{
827+
name: "negative evaluationWindow",
828+
guardrails: []attunev1alpha1.SLOGuardrail{
829+
{Name: "slo1", Query: "up", Threshold: "1", EvaluationWindow: &metav1.Duration{Duration: -time.Minute}},
830+
},
831+
wantErr: "evaluationWindow must be non-negative",
832+
},
833+
{
834+
name: "evaluationWindow below minimum",
835+
guardrails: []attunev1alpha1.SLOGuardrail{
836+
{Name: "slo1", Query: "up", Threshold: "1", EvaluationWindow: &metav1.Duration{Duration: 30 * time.Second}},
837+
},
838+
wantErr: "evaluationWindow must be at least 1m",
839+
},
840+
}
841+
for _, tc := range tests {
842+
t.Run(tc.name, func(t *testing.T) {
843+
v := &AttuneDefaultsValidator{}
844+
defaults := &attunev1alpha1.AttuneDefaults{
845+
ObjectMeta: metav1.ObjectMeta{Name: "default"},
846+
Spec: attunev1alpha1.AttuneDefaultsSpec{
847+
UpdateStrategy: &attunev1alpha1.UpdateStrategy{
848+
SLOGuardrails: tc.guardrails,
849+
},
850+
},
851+
}
852+
853+
_, err := v.ValidateCreate(context.Background(), defaults)
854+
require.Error(t, err)
855+
assert.Contains(t, err.Error(), tc.wantErr)
856+
})
857+
}
858+
}
859+
659860
func TestDefaultsValidate_HistoryWindowInvalid(t *testing.T) {
660861
tests := []struct {
661862
name string

0 commit comments

Comments
 (0)