Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[test] Improve test coverage and performance in apimachinery_status_conditions #172

Merged
11 changes: 3 additions & 8 deletions pkg/common/apimachinery_status_conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,14 @@ import (

// CopyConditions copies the set of conditions
func CopyConditions(conditions []metav1.Condition) []metav1.Condition {
newConditions := make([]metav1.Condition, 0)
for idx := range conditions {
// copy
newCondition := conditions[idx]
newConditions = append(newConditions, newCondition)
}
newConditions := make([]metav1.Condition, len(conditions))
copy(newConditions, conditions)
return newConditions
}

// ConditionMarshal marshals the set of conditions as a JSON array, sorted by condition type.
func ConditionMarshal(conditions []metav1.Condition) ([]byte, error) {
var condCopy []metav1.Condition
condCopy = append(condCopy, conditions...)
condCopy := CopyConditions(conditions)
sort.Slice(condCopy, func(a, b int) bool {
return condCopy[a].Type < condCopy[b].Type
})
Expand Down
222 changes: 222 additions & 0 deletions pkg/common/apimachinery_status_conditions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
//go:build unit

package common

import (
"testing"
"time"

goCmp "github.com/google/go-cmp/cmp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestCopyConditions(t *testing.T) {
now := metav1.Now()
testCases := []struct {
name string
expectedConditions []metav1.Condition
}{
{
name: "when multiple conditions then return copy",
expectedConditions: []metav1.Condition{
{
Type: "Installed",
Status: metav1.ConditionTrue,
ObservedGeneration: 1,
LastTransitionTime: now,
Reason: "InstallSuccessful",
Message: "Object successfully installed",
},
{
Type: "Reconciled",
Status: metav1.ConditionFalse,
ObservedGeneration: 0,
LastTransitionTime: now,
Reason: "ReconcileError",
Message: "Object failed to reconcile due to an error",
},
{
Type: "Ready",
Status: metav1.ConditionFalse,
ObservedGeneration: 2,
LastTransitionTime: now,
Reason: "ComponentsNotReady",
Message: "Object components are not ready",
},
{
Type: "Validated",
Status: metav1.ConditionUnknown,
ObservedGeneration: 0,
LastTransitionTime: now,
Reason: "ValidationError",
Message: "Object validation failed",
},
},
},
{
name: "when one condition then return copy",
expectedConditions: []metav1.Condition{
{
Type: "Validated",
Status: metav1.ConditionUnknown,
ObservedGeneration: 0,
LastTransitionTime: now,
Reason: "ValidationError",
Message: "Object validation failed",
},
},
},
{
name: "when empty conditions slice return empty slice",
expectedConditions: []metav1.Condition{},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(subT *testing.T) {
actualConditions := CopyConditions(tc.expectedConditions)
if diff := goCmp.Diff(tc.expectedConditions, actualConditions); diff != "" {
subT.Errorf("Conditions mismatch (-want +got):\n%s", diff)
}
})
}
}

func TestCopyConditions_OriginalConditionsNotModified(t *testing.T) {
now := metav1.Now()
expectedConditions := []metav1.Condition{
{
Type: "Installed",
Status: metav1.ConditionTrue,
ObservedGeneration: 1,
LastTransitionTime: now,
Reason: "InstallSuccessful",
Message: "Object successfully installed",
},
{
Type: "Reconciled",
Status: metav1.ConditionFalse,
ObservedGeneration: 0,
LastTransitionTime: now,
Reason: "ReconcileError",
Message: "Object failed to reconcile due to an error",
},
}

originalConditions := append([]metav1.Condition{}, expectedConditions...)
actualConditions := CopyConditions(expectedConditions)

// Check that the copied conditions are equal to the original conditions
if diff := goCmp.Diff(expectedConditions, actualConditions); diff != "" {
t.Errorf("Conditions mismatch (-want +got):\n%s", diff)
}

// Check that the original conditions were not modified
if diff := goCmp.Diff(originalConditions, expectedConditions); diff != "" {
t.Errorf("Original conditions were modified (-want +got):\n%s", diff)
}
}

func TestConditionMarshal(t *testing.T) {
now := metav1.Now()
nowUTC := now.UTC().Format(time.RFC3339)

testCases := []struct {
name string
conditions []metav1.Condition
expectedJSONOutput string
expectedError error
}{
{
name: "when empty conditions slice then return empty slice",
conditions: []metav1.Condition{},
expectedJSONOutput: "[]",
expectedError: nil,
},
{
name: "when single condition then return JSON",
conditions: []metav1.Condition{
{
Type: "Installed",
Status: metav1.ConditionTrue,
ObservedGeneration: 1,
LastTransitionTime: now,
Reason: "InstallSuccessful",
Message: "Object successfully installed",
},
},
expectedJSONOutput: `[{"type":"Installed","status":"True","observedGeneration":1,"lastTransitionTime":"` + nowUTC + `","reason":"InstallSuccessful","message":"Object successfully installed"}]`,
expectedError: nil,
},
{
name: "when multiple conditions then return JSON",
conditions: []metav1.Condition{
{
Type: "Ready",
Status: metav1.ConditionFalse,
ObservedGeneration: 2,
LastTransitionTime: now,
Reason: "ComponentsNotReady",
Message: "Object components are not ready",
},
{
Type: "Installed",
Status: metav1.ConditionTrue,
ObservedGeneration: 1,
LastTransitionTime: now,
Reason: "InstallSuccessful",
Message: "Object successfully installed",
},
{
Type: "Validated",
Status: metav1.ConditionUnknown,
ObservedGeneration: 3,
LastTransitionTime: now,
Reason: "ValidationError",
Message: "Object validation failed",
},
},
expectedJSONOutput: `[{"type":"Installed","status":"True","observedGeneration":1,"lastTransitionTime":"` + nowUTC + `","reason":"InstallSuccessful","message":"Object successfully installed"},{"type":"Ready","status":"False","observedGeneration":2,"lastTransitionTime":"` + nowUTC + `","reason":"ComponentsNotReady","message":"Object components are not ready"},{"type":"Validated","status":"Unknown","observedGeneration":3,"lastTransitionTime":"` + nowUTC + `","reason":"ValidationError","message":"Object validation failed"}]`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what? Order does not keep consistent?? First Ready, then Installed and lastly Validated. And for the serialized string: Installed, Ready, Validated. Weird.

HAve you run the test multiple times? if order is not maintained, on multiple runs, some of them could fail

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the concern! Conditions are sorted based on their Condition.Type field in the ConditionMarshal function before they're serialized. So, the order is consistent and not random. To answer your question, running the test multiple times should not result in any failures due to the consistent ordering provided by the sorting mechanism. Heer is ConditionMarhsal implementation, pay attention to the sorting part:

func ConditionMarshal(conditions []metav1.Condition) ([]byte, error) {
	condCopy := CopyConditions(conditions)
	sort.Slice(condCopy, func(a, b int) bool {
		return condCopy[a].Type < condCopy[b].Type
	})
	return json.Marshal(condCopy)
}

Let me know if you have any other questions! 👼

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦 Sorry for that. Once again I took some assumptions that are wrong.

Looking good

expectedError: nil,
},
{
name: "when empty ObservedGeneration field (EQ 0) then it is omitted in JSON",
conditions: []metav1.Condition{
{
Type: "Installed",
Status: metav1.ConditionTrue,
ObservedGeneration: 0,
LastTransitionTime: now,
Reason: "InstallSuccessful",
Message: "Object successfully installed",
},
{
Type: "Validated",
Status: metav1.ConditionUnknown,
ObservedGeneration: 0,
LastTransitionTime: now,
Reason: "ValidationError",
Message: "Object validation failed",
},
},
expectedJSONOutput: `[{"type":"Installed","status":"True","lastTransitionTime":"` + nowUTC + `","reason":"InstallSuccessful","message":"Object successfully installed"},{"type":"Validated","status":"Unknown","lastTransitionTime":"` + nowUTC + `","reason":"ValidationError","message":"Object validation failed"}]`,
expectedError: nil,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(subT *testing.T) {
actualJSONOutput, actualError := ConditionMarshal(tc.conditions)

diff := goCmp.Diff(tc.expectedJSONOutput, string(actualJSONOutput))
if diff != "" {
subT.Errorf("Unexpected JSON output (-want +got):\n%s", diff)
}

diff = goCmp.Diff(tc.expectedError, actualError)
if diff != "" {
subT.Errorf("Unexpected error (-want +got):\n%s", diff)
}
})
}
}