-
Notifications
You must be signed in to change notification settings - Fork 158
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
chore: add tests for kustomize's sortOptions configuration #1615
Conversation
This probably can supersede #1600 |
so the plan is to ask component teams to set fifo in their kustomization as the solution for ordering? |
|
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1615 +/- ##
==========================================
+ Coverage 20.28% 20.35% +0.07%
==========================================
Files 163 163
Lines 11137 11155 +18
==========================================
+ Hits 2259 2271 +12
- Misses 8638 8644 +6
Partials 240 240 ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we can optimize it like this:
package jq_test
import (
"encoding/json"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/utils/test/matchers/jq"
. "github.com/onsi/gomega"
)
// TestMatcher verifies the jq.Match function against various JSON strings.
// It ensures correct behavior for simple key-value matches, array value checks,
// handling of null values, and nested object conditions.
func TestMatcher(t *testing.T) {
t.Parallel()
g := NewWithT(t)
// Define multiple test cases for jq matching
testCases := []struct {
input string
jqQuery string
expected bool
}{
{
// Test matching a simple value
input: `{"a":1}`,
jqQuery: `.a == 1`,
expected: true,
},
{
// Test when the value doesn't match
input: `{"a":1}`,
jqQuery: `.a == 2`,
expected: false,
},
{
// Test array with values matching
input: `{"Values":[ "foo" ]}`,
jqQuery: `.Values | if . then any(. == "foo") else false end`,
expected: true,
},
{
// Test array with non-matching value
input: `{"Values":[ "foo" ]}`,
jqQuery: `.Values | if . then any(. == "bar") else false end`,
expected: false,
},
{
// Test when the value is null
input: `{"Values": null}`,
jqQuery: `.Values | if . then any(. == "foo") else false end`,
expected: false,
},
{
// Test multiple matching conditions
input: `{ "status": { "foo": { "bar": "fr", "baz": "fb" } } }`,
jqQuery: `(.status.foo.bar == "fr") and (.status.foo.baz == "fb")`
expected: true,
},
}
// Run the test cases
for _, tc := range testCases {
t.Run(tc.jqQuery, func(t *testing.T) {
// Assert based on the expected result
if tc.expected {
g.Expect(tc.input).Should(jq.Match(tc.jqQuery))
} else {
g.Expect(tc.input).ShouldNot(jq.Match(tc.jqQuery))
}
})
}
}
// TestMatcherWithType validates jq.Match against different Go data types such as maps and structs.
// It ensures that jq.Match works correctly after serializing Go types to JSON.
func TestMatcherWithType(t *testing.T) {
t.Parallel()
g := NewWithT(t)
// Define multiple test cases for jq matching
testCases := []struct {
name string
input interface{}
jqQuery string
useTransform bool
}{
{
name: "Simple map match",
input: map[string]any{"a": 1},
jqQuery: `.a == 1`,
useTransform: false,
},
{
name: "Map match with JSON transformation",
input: map[string]any{"a": 1},
jqQuery: `.a == 1`,
useTransform: true,
},
{
name: "Nested map match with JSON transformation",
input: map[string]any{
"status": map[string]any{
"foo": map[string]any{
"bar": "fr",
"baz": "fb",
},
},
},
jqQuery: `(.status.foo.bar == "fr") and (.status.foo.baz == "fb"`),
useTransform: true,
},
{
name: "Struct match with JSON transformation",
input: struct { A int `json:"a"` }{A: 1},
jqQuery: `.a == 1`,
useTransform: true,
},
}
// Run the test cases
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
marshaled, _ := json.Marshal(tc.input)
// If `useTransform` is true, we first transform the input to JSON before running jq.Match
// If `useTransform` is false, we directly pass the input to jq.Match.
if tc.useTransform {
g.Expect(tc.input).Should(WithTransform(json.Marshal, jq.Match(tc.jqQuery)))
} else {
g.Expect(tc.input).Should(jq.Match(tc.jqQuery))
}
})
}
}
func TestUnstructuredSliceMatcher(t *testing.T) {
t.Parallel()
g := NewWithT(t)
u := []unstructured.Unstructured{{
Object: map[string]interface{}{
"a": 1,
}},
}
g.Expect(u).Should(
jq.Match(`.[0] | .a == 1`))
g.Expect(unstructured.UnstructuredList{Items: u}).Should(
jq.Match(`.[0] | .a == 1`))
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we can optimize it like this:
package jq
import (
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
"strings"
"github.com/onsi/gomega/format"
"github.com/onsi/gomega/gbytes"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func formattedMessage(comparisonMessage string, failurePath []interface{}) string {
diffMessage := ""
if len(failurePath) != 0 {
diffMessage = "\n\nfirst mismatched key: " + formattedFailurePath(failurePath)
}
return comparisonMessage + diffMessage
}
func formattedFailurePath(failurePath []interface{}) string {
formattedPaths := make([]string, 0)
for i := len(failurePath) - 1; i >= 0; i-- {
switch p := failurePath[i].(type) {
case int:
val := fmt.Sprintf(`[%d]`, p)
formattedPaths = append(formattedPaths, val)
default:
if i != len(failurePath)-1 {
formattedPaths = append(formattedPaths, ".")
}
val := fmt.Sprintf(`"%s"`, p)
formattedPaths = append(formattedPaths, val)
}
}
return strings.Join(formattedPaths, "")
}
//nolint:cyclop
func toType(in any) (any, error) {
switch v := in.(type) {
case string, []byte, json.RawMessage, *gbytes.Buffer:
// Handle string, []byte, json.RawMessage, and *gbytes.Buffer by converting them into the appropriate type.
return convertToJSON(v)
case io.Reader:
// Handle io.Reader by reading the content and converting it into the appropriate type.
data, err := io.ReadAll(v)
if err != nil {
return nil, fmt.Errorf("failed to read from reader: %w", err)
}
return convertToJSON(data)
case unstructured.UnstructuredList:
// Handle UnstructuredList by extracting the objects from the list.
return convertUnstructuredItemsToType(v.Items)
case []unstructured.Unstructured:
// Handle slice of Unstructured items.
return convertUnstructuredItemsToType(v)
case unstructured.Unstructured, *unstructured.Unstructured:
// Handle single Unstructured item or pointer to Unstructured item.
// Both types can be handled the same way by accessing the `Object` field.
return v.(unstructured.Unstructured).Object, nil
case []*unstructured.Unstructured:
// Handle slice of pointers to Unstructured items.
return convertUnstructuredItemsToType(v)
}
// Fallback case for maps and slices.
switch reflect.TypeOf(in).Kind() {
case reflect.Map:
return in, nil
case reflect.Slice:
return in, nil
default:
return nil, fmt.Errorf("unsuported type:\n%s", format.Object(in, 1))
}
}
// convertToJSON converts various types into a structured JSON object or slice.
func convertToJSON(v any) (any, error) {
var data []byte
switch value := v.(type) {
case string:
data = []byte(value)
case []byte:
data = value
case json.RawMessage:
data = []byte(value)
case *gbytes.Buffer:
data = value.Contents()
}
switch data[0] {
case '{':
var obj map[string]any
if err := json.Unmarshal(data, &obj); err != nil {
return nil, fmt.Errorf("unable to unmarshal JSON object, %w", err)
}
return obj, nil
case '[':
var arr []any
if err := json.Unmarshal(data, &arr); err != nil {
return nil, fmt.Errorf("unable to unmarshal JSON array, %w", err)
}
return arr, nil
default:
return nil, errors.New("JSON must be an array or object")
}
}
// convertUnstructuredItemsToType converts Unstructured items into their map representation.
func convertUnstructuredItemsToType(items []unstructured.Unstructured) ([]any, error) {
res := make([]any, 0, len(items))
for i := range items {
res = append(res, items[i].Object)
}
return res, nil
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
feel free to send a pr :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do!
The Kustomize rendering engine embedded in the operator, does not enforce any particular order, but relies on the order of the manifests provided, however, Kustomize has options to change the strategy used to sort resources at the end of the Kustomize build see: - https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/sortoptions This commit adds some test to ensure the sortOrder configuration is honored by the operator's own Kustomize engine.
I will have a look shortly :) |
/hold |
/unhold |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me, nice!
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: MarianMacik, ugiordan, zdtsw The full list of commands accepted by this bot can be found here. The pull request process is described here
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
The Kustomize rendering engine embedded in the operator, does not
enforce any particular order, but relies on the order of the manifests
provided, however, Kustomize has options to change the strategy used
to sort resources at the end of the Kustomize build see:
This commit adds some test to ensure the
sortOrder
configuration ishonored by the operator's own Kustomize engine.
Description
https://issues.redhat.com/browse/RHOAIENG-19091
How Has This Been Tested?
Screenshot or short clip
Merge criteria