Skip to content
This repository has been archived by the owner on Feb 16, 2024. It is now read-only.

Commit

Permalink
Merge pull request #39 from shubham-cmyk/factory
Browse files Browse the repository at this point in the history
introduce strategy factory
  • Loading branch information
eddycharly authored Sep 27, 2023
2 parents 837c074 + 4b8a232 commit 69b0cb8
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 31 deletions.
26 changes: 24 additions & 2 deletions pkg/apis/testharness/v1beta1/test_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ const (
MatchWildcard MatchType = "Wildcard"
)

type Strategy string

const (
StrategyAnywhere Strategy = "Anywhere"
StrategyExact Strategy = "Exact"
)

// Create embedded struct to implement custom DeepCopyInto method
type RestConfig struct {
RC *rest.Config
Expand Down Expand Up @@ -154,7 +161,20 @@ type TestStep struct {

type Assert struct {
// File specifies the relative or full path to the YAML containing the expected content.
File string `json:"file"`
File string `json:"file"`
Options *Options `json:"options,omitempty"`
}

type Options struct {
AssertArray []AssertArray `json:"arrays,omitempty"`
}

// AssertArray specifies conditions for verifying content within a YAML against a Kubernetes resource.
type AssertArray struct {
// Path indicates the location within the YAML file to extract data for verification.
Path string `json:"path"`
// Strategy defines how the extracted data should be compared against the Kubernetes resource.
Strategy Strategy `json:"strategy"`
}

// UnmarshalJSON implements the json.Unmarshaller interface.
Expand All @@ -163,12 +183,14 @@ func (assert *Assert) UnmarshalJSON(value []byte) error {
return json.Unmarshal(value, &assert.File)
}
data := struct {
File string `json:"file,omitempty"`
File string `json:"file,omitempty"`
Options *Options `json:"options,omitempty"`
}{}
if err := json.Unmarshal(value, &data); err != nil {
return err
}
assert.File = data.File
assert.Options = data.Options
return nil
}

Expand Down
46 changes: 45 additions & 1 deletion pkg/apis/testharness/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pkg/test/assert.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func Assert(namespace string, timeout int, assertFiles ...string) error {
// start fresh
testErrors = []error{}
for _, expected := range objects {
testErrors = append(testErrors, s.CheckResource(expected, namespace)...)
testErrors = append(testErrors, s.CheckResource(expected, namespace, nil)...)
}

if len(testErrors) == 0 {
Expand Down
35 changes: 30 additions & 5 deletions pkg/test/step.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ type apply struct {
}

type asserts struct {
object client.Object
object client.Object
options *harness.Options
}

// A Step contains the name of the test step, its index in the test,
Expand Down Expand Up @@ -287,7 +288,7 @@ func list(cl client.Client, gvk schema.GroupVersionKind, namespace string) ([]un
}

// CheckResource checks if the expected resource's state in Kubernetes is correct.
func (s *Step) CheckResource(expected runtime.Object, namespace string) []error {
func (s *Step) CheckResource(expected runtime.Object, namespace string, strategyFactory testutils.ArrayComparisonStrategyFactory) []error {
cl, err := s.Client(false)
if err != nil {
return []error{err}
Expand Down Expand Up @@ -339,7 +340,7 @@ func (s *Step) CheckResource(expected runtime.Object, namespace string) []error

tmpTestErrors := []error{}

if err := testutils.IsSubset(expectedObj, actual.UnstructuredContent()); err != nil {
if err := testutils.IsSubset(expectedObj, actual.UnstructuredContent(), "/", strategyFactory); err != nil {
diff, diffErr := testutils.PrettyDiff(expected, &actual)
if diffErr == nil {
tmpTestErrors = append(tmpTestErrors, fmt.Errorf(diff))
Expand Down Expand Up @@ -411,7 +412,7 @@ func (s *Step) CheckResourceAbsent(expected runtime.Object, namespace string) er

var unexpectedObjects []unstructured.Unstructured
for _, actual := range actuals {
if err := testutils.IsSubset(expectedObj, actual.UnstructuredContent()); err == nil {
if err := testutils.IsSubset(expectedObj, actual.UnstructuredContent(), "/", nil); err == nil {
unexpectedObjects = append(unexpectedObjects, actual)
}
}
Expand All @@ -425,6 +426,29 @@ func (s *Step) CheckResourceAbsent(expected runtime.Object, namespace string) er
return fmt.Errorf("resource %s %s (and %d other resources) matched error assertion", unexpectedObjects[0].GroupVersionKind(), unexpectedObjects[0].GetName(), len(unexpectedObjects)-1)
}

// Build StrategyFactory for IsSubset
func NewStrategyFactory(a asserts) func(path string) testutils.ArrayComparisonStrategy {
var strategyFactory func(path string) testutils.ArrayComparisonStrategy
recursiveStrategyFactory := func(path string) testutils.ArrayComparisonStrategy {
if a.options != nil && len(a.options.AssertArray) > 0 {
for _, assertArr := range a.options.AssertArray {
if assertArr.Path == path {
switch assertArr.Strategy {
case harness.StrategyExact:
return testutils.StrategyExact(path, strategyFactory)
case harness.StrategyAnywhere:
return testutils.StrategyAnywhere(path, strategyFactory)
}
}
}
}
// Default strategy if no match is found
return testutils.StrategyExact(path, strategyFactory)
}
strategyFactory = recursiveStrategyFactory
return strategyFactory
}

// CheckAssertCommands Runs the commands provided in `commands` and check if have been run successfully.
// the errors returned can be a a failure of executing the command or the failure of the command executed.
func (s *Step) CheckAssertCommands(ctx context.Context, namespace string, commands []harness.TestAssertCommand, timeout int) []error {
Expand All @@ -440,7 +464,8 @@ func (s *Step) Check(namespace string, timeout int) []error {
testErrors := []error{}

for _, expected := range s.Asserts {
testErrors = append(testErrors, s.CheckResource(expected.object, namespace)...)
strategyFactory := NewStrategyFactory(expected)
testErrors = append(testErrors, s.CheckResource(expected.object, namespace, strategyFactory)...)
}

if s.Assert != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/step_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ func TestCheckResource(t *testing.T) {
DiscoveryClient: func() (discovery.DiscoveryInterface, error) { return fakeDiscovery, nil },
}

errors := step.CheckResource(test.expected, namespace)
errors := step.CheckResource(test.expected, namespace, nil)

if test.shouldError {
assert.NotEqual(t, []error{}, errors)
Expand Down
76 changes: 65 additions & 11 deletions pkg/test/utils/subset.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ import (
"reflect"
)

type ArrayComparisonStrategyFactory = func(path string) ArrayComparisonStrategy
type ArrayComparisonStrategy = func(expectedData, actualData interface{}) error

func defaultStrategyFactory() ArrayComparisonStrategyFactory {
return alwaysExact
}

func alwaysExact(path string) ArrayComparisonStrategy {
return StrategyExact(path, defaultStrategyFactory())
}

// SubsetError is an error type used by IsSubset for tracking the path in the struct.
type SubsetError struct {
path []string
Expand Down Expand Up @@ -36,7 +47,7 @@ func (e *SubsetError) Error() string {

// IsSubset checks to see if `expected` is a subset of `actual`. A "subset" is an object that is equivalent to
// the other object, but where map keys found in actual that are not defined in expected are ignored.
func IsSubset(expected, actual interface{}) error {
func IsSubset(expected, actual interface{}, currentPath string, strategyFactory ArrayComparisonStrategyFactory) error {
if reflect.TypeOf(expected) != reflect.TypeOf(actual) {
return &SubsetError{
message: fmt.Sprintf("type mismatch: %v != %v", reflect.TypeOf(expected), reflect.TypeOf(actual)),
Expand All @@ -49,17 +60,13 @@ func IsSubset(expected, actual interface{}) error {

switch reflect.TypeOf(expected).Kind() {
case reflect.Slice:
if reflect.ValueOf(expected).Len() != reflect.ValueOf(actual).Len() {
return &SubsetError{
message: fmt.Sprintf("slice length mismatch: %d != %d", reflect.ValueOf(expected).Len(), reflect.ValueOf(actual).Len()),
}
if strategyFactory == nil {
strategyFactory = defaultStrategyFactory()
}
strategy := strategyFactory(currentPath)

return strategy(expected, actual)

for i := 0; i < reflect.ValueOf(expected).Len(); i++ {
if err := IsSubset(reflect.ValueOf(expected).Index(i).Interface(), reflect.ValueOf(actual).Index(i).Interface()); err != nil {
return err
}
}
case reflect.Map:
iter := reflect.ValueOf(expected).MapRange()

Expand All @@ -73,7 +80,8 @@ func IsSubset(expected, actual interface{}) error {
}
}

if err := IsSubset(iter.Value().Interface(), actualValue.Interface()); err != nil {
newPath := currentPath + "/" + iter.Key().String()
if err := IsSubset(iter.Value().Interface(), actualValue.Interface(), newPath, strategyFactory); err != nil {
subsetErr, ok := err.(*SubsetError)
if ok {
subsetErr.AppendPath(iter.Key().String())
Expand All @@ -90,3 +98,49 @@ func IsSubset(expected, actual interface{}) error {

return nil
}

func StrategyAnywhere(path string, strategyFactory ArrayComparisonStrategyFactory) ArrayComparisonStrategy {
return func(expected, actual interface{}) error {
expectedData := toSlice(expected)
actualData := toSlice(actual)

for i, expectedItem := range expectedData {
matched := false
for _, actualItem := range actualData {
newPath := path + fmt.Sprintf("[%d]", i)
if err := IsSubset(expectedItem, actualItem, newPath, strategyFactory); err == nil {
matched = true
break
}
}
if !matched {
return &SubsetError{message: fmt.Sprintf("expected item %v not found in actual slice at path %s", expectedItem, path)}
}
}
return nil
}
}

func StrategyExact(path string, strategyFactory ArrayComparisonStrategyFactory) ArrayComparisonStrategy {
return func(expected, actual interface{}) error {
if reflect.ValueOf(expected).Len() != reflect.ValueOf(actual).Len() {
return &SubsetError{message: fmt.Sprintf("slice length mismatch at path %s: %d != %d", path, reflect.ValueOf(expected).Len(), reflect.ValueOf(actual).Len())}
}
for i := 0; i < reflect.ValueOf(expected).Len(); i++ {
newPath := path + fmt.Sprintf("[%d]", i)
if err := IsSubset(reflect.ValueOf(expected).Index(i).Interface(), reflect.ValueOf(actual).Index(i).Interface(), newPath, strategyFactory); err != nil {
return err
}
}
return nil
}
}

func toSlice(v interface{}) []interface{} {
value := reflect.ValueOf(v)
slice := make([]interface{}, value.Len())
for i := 0; i < value.Len(); i++ {
slice[i] = value.Index(i).Interface()
}
return slice
}
Loading

0 comments on commit 69b0cb8

Please sign in to comment.