Skip to content

Commit

Permalink
fix(json_compare): solve unorderd slice deep equal (#1300)
Browse files Browse the repository at this point in the history
* fix(json_compare): sort slice

* fix(json_compare): improve

* fix(json_compare): remove duplicated tests

---------

Co-authored-by: Evans Mungai <[email protected]>
  • Loading branch information
DexterYan and banjoh authored Aug 24, 2023
1 parent 57b988b commit 04f69b3
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 1 deletion.
69 changes: 68 additions & 1 deletion pkg/analyze/json_compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package analyzer
import (
"bytes"
"encoding/json"
"fmt"
"path/filepath"
"reflect"
"sort"
"strconv"

"github.com/pkg/errors"
Expand Down Expand Up @@ -97,7 +99,8 @@ func (a *AnalyzeJsonCompare) analyzeJsonCompare(analyzer *troubleshootv1beta2.Js
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
}

equal := reflect.DeepEqual(actual, expected)
// due to jsp.Execute may return a slice of results unsorted, we need to sort the slice before comparing
equal := deepEqualWithSlicesSorted(actual, expected)

for _, outcome := range analyzer.Outcomes {
if outcome.Fail != nil {
Expand Down Expand Up @@ -159,3 +162,67 @@ func (a *AnalyzeJsonCompare) analyzeJsonCompare(analyzer *troubleshootv1beta2.Js
Message: "Invalid analyzer",
}, nil
}

// deepEqualWithSlicesSorted compares two interfaces and returns true if they contain the same values
// If the interfaces are slices, they are sorted before comparison to ensure order does not matter
// If the interfaces are not slices, reflect.DeepEqual is used
func deepEqualWithSlicesSorted(actual, expected interface{}) bool {
ra, re := reflect.ValueOf(actual), reflect.ValueOf(expected)

// If types are different, they're not equal
if ra.Kind() != re.Kind() {
return false
}

// If types are slices, compare sorted slices
if ra.Kind() == reflect.Slice {
return compareSortedSlices(ra.Interface().([]interface{}), re.Interface().([]interface{}))
}

// Otherwise, compare values (reflect.DeepEqual)
return reflect.DeepEqual(actual, expected)
}

// compareSortedSlices compares two sorted slices of interfaces and returns true if they contain the same values
func compareSortedSlices(actual, expected []interface{}) bool {
if len(actual) != len(expected) {
return false
}

// Sort slices
sortSliceOfInterfaces(actual)
sortSliceOfInterfaces(expected)

// Compare slices (reflect.DeepEqual)
return reflect.DeepEqual(actual, expected)
}

func sortSliceOfInterfaces(slice []interface{}) {
sort.Slice(slice, func(i, j int) bool {
return order(slice[i], slice[j])
})
}

// order function determines the order of two interface{} values
func order(a, b interface{}) bool {
switch va := a.(type) {
case int:
if vb, ok := b.(int); ok {
return va < vb
}
case float64:
if vb, ok := b.(float64); ok {
return va < vb
}
case string:
if vb, ok := b.(string); ok {
return va < vb
}
case bool:
if vb, ok := b.(bool); ok {
return !va && vb // false < true
}
}
// use string representation for comparison
return fmt.Sprintf("%v", a) < fmt.Sprintf("%v", b)
}
77 changes: 77 additions & 0 deletions pkg/analyze/json_compare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,86 @@ import (
"testing"

troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_compareSortedSlices(t *testing.T) {
type args struct {
actual []interface{}
expected []interface{}
}

tests := []struct {
name string
args args
equal bool
}{
{
name: "empty slices",
args: args{
actual: []interface{}{},
expected: []interface{}{},
},
equal: true,
},
{
name: "same order slices",
args: args{
actual: []interface{}{"a", "b", "c"},
expected: []interface{}{"a", "b", "c"},
},
equal: true,
},
{
name: "unordered slices",
args: args{
actual: []interface{}{"a", "b", "c"},
expected: []interface{}{"b", "a", "c"},
},
equal: true,
},
{
name: "different type and unordered slices",
args: args{
actual: []interface{}{1, "a", "c"},
expected: []interface{}{"a", 1, "c"},
},
equal: true,
},
{
name: "unordered slices with map",
args: args{
actual: []interface{}{map[string]int{"a": 1}, "a", "c"},
expected: []interface{}{"a", map[string]int{"a": 1}, "c"},
},
equal: true,
},
{
name: "unequal slices with duplicates",
args: args{
actual: []interface{}{"a", "a", "a", "c"},
expected: []interface{}{"a", "a", "c", "c"},
},
equal: false,
},
{
name: "unordered slices with boolean and strings",
args: args{
actual: []interface{}{true, "a", false, true},
expected: []interface{}{"a", true, false, true},
},
equal: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := compareSortedSlices(tt.args.actual, tt.args.expected)
assert.Equalf(t, tt.equal, got, "compareSlices() = %v, want %v", got, tt.equal)
})
}
}

func Test_jsonCompare(t *testing.T) {
tests := []struct {
name string
Expand Down

0 comments on commit 04f69b3

Please sign in to comment.