Skip to content

Commit 04f69b3

Browse files
DexterYanbanjoh
andauthored
fix(json_compare): solve unorderd slice deep equal (#1300)
* fix(json_compare): sort slice * fix(json_compare): improve * fix(json_compare): remove duplicated tests --------- Co-authored-by: Evans Mungai <[email protected]>
1 parent 57b988b commit 04f69b3

File tree

2 files changed

+145
-1
lines changed

2 files changed

+145
-1
lines changed

pkg/analyze/json_compare.go

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package analyzer
33
import (
44
"bytes"
55
"encoding/json"
6+
"fmt"
67
"path/filepath"
78
"reflect"
9+
"sort"
810
"strconv"
911

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

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

102105
for _, outcome := range analyzer.Outcomes {
103106
if outcome.Fail != nil {
@@ -159,3 +162,67 @@ func (a *AnalyzeJsonCompare) analyzeJsonCompare(analyzer *troubleshootv1beta2.Js
159162
Message: "Invalid analyzer",
160163
}, nil
161164
}
165+
166+
// deepEqualWithSlicesSorted compares two interfaces and returns true if they contain the same values
167+
// If the interfaces are slices, they are sorted before comparison to ensure order does not matter
168+
// If the interfaces are not slices, reflect.DeepEqual is used
169+
func deepEqualWithSlicesSorted(actual, expected interface{}) bool {
170+
ra, re := reflect.ValueOf(actual), reflect.ValueOf(expected)
171+
172+
// If types are different, they're not equal
173+
if ra.Kind() != re.Kind() {
174+
return false
175+
}
176+
177+
// If types are slices, compare sorted slices
178+
if ra.Kind() == reflect.Slice {
179+
return compareSortedSlices(ra.Interface().([]interface{}), re.Interface().([]interface{}))
180+
}
181+
182+
// Otherwise, compare values (reflect.DeepEqual)
183+
return reflect.DeepEqual(actual, expected)
184+
}
185+
186+
// compareSortedSlices compares two sorted slices of interfaces and returns true if they contain the same values
187+
func compareSortedSlices(actual, expected []interface{}) bool {
188+
if len(actual) != len(expected) {
189+
return false
190+
}
191+
192+
// Sort slices
193+
sortSliceOfInterfaces(actual)
194+
sortSliceOfInterfaces(expected)
195+
196+
// Compare slices (reflect.DeepEqual)
197+
return reflect.DeepEqual(actual, expected)
198+
}
199+
200+
func sortSliceOfInterfaces(slice []interface{}) {
201+
sort.Slice(slice, func(i, j int) bool {
202+
return order(slice[i], slice[j])
203+
})
204+
}
205+
206+
// order function determines the order of two interface{} values
207+
func order(a, b interface{}) bool {
208+
switch va := a.(type) {
209+
case int:
210+
if vb, ok := b.(int); ok {
211+
return va < vb
212+
}
213+
case float64:
214+
if vb, ok := b.(float64); ok {
215+
return va < vb
216+
}
217+
case string:
218+
if vb, ok := b.(string); ok {
219+
return va < vb
220+
}
221+
case bool:
222+
if vb, ok := b.(bool); ok {
223+
return !va && vb // false < true
224+
}
225+
}
226+
// use string representation for comparison
227+
return fmt.Sprintf("%v", a) < fmt.Sprintf("%v", b)
228+
}

pkg/analyze/json_compare_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,86 @@ import (
44
"testing"
55

66
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
7+
"github.com/stretchr/testify/assert"
78
"github.com/stretchr/testify/require"
89
)
910

11+
func Test_compareSortedSlices(t *testing.T) {
12+
type args struct {
13+
actual []interface{}
14+
expected []interface{}
15+
}
16+
17+
tests := []struct {
18+
name string
19+
args args
20+
equal bool
21+
}{
22+
{
23+
name: "empty slices",
24+
args: args{
25+
actual: []interface{}{},
26+
expected: []interface{}{},
27+
},
28+
equal: true,
29+
},
30+
{
31+
name: "same order slices",
32+
args: args{
33+
actual: []interface{}{"a", "b", "c"},
34+
expected: []interface{}{"a", "b", "c"},
35+
},
36+
equal: true,
37+
},
38+
{
39+
name: "unordered slices",
40+
args: args{
41+
actual: []interface{}{"a", "b", "c"},
42+
expected: []interface{}{"b", "a", "c"},
43+
},
44+
equal: true,
45+
},
46+
{
47+
name: "different type and unordered slices",
48+
args: args{
49+
actual: []interface{}{1, "a", "c"},
50+
expected: []interface{}{"a", 1, "c"},
51+
},
52+
equal: true,
53+
},
54+
{
55+
name: "unordered slices with map",
56+
args: args{
57+
actual: []interface{}{map[string]int{"a": 1}, "a", "c"},
58+
expected: []interface{}{"a", map[string]int{"a": 1}, "c"},
59+
},
60+
equal: true,
61+
},
62+
{
63+
name: "unequal slices with duplicates",
64+
args: args{
65+
actual: []interface{}{"a", "a", "a", "c"},
66+
expected: []interface{}{"a", "a", "c", "c"},
67+
},
68+
equal: false,
69+
},
70+
{
71+
name: "unordered slices with boolean and strings",
72+
args: args{
73+
actual: []interface{}{true, "a", false, true},
74+
expected: []interface{}{"a", true, false, true},
75+
},
76+
equal: true,
77+
},
78+
}
79+
for _, tt := range tests {
80+
t.Run(tt.name, func(t *testing.T) {
81+
got := compareSortedSlices(tt.args.actual, tt.args.expected)
82+
assert.Equalf(t, tt.equal, got, "compareSlices() = %v, want %v", got, tt.equal)
83+
})
84+
}
85+
}
86+
1087
func Test_jsonCompare(t *testing.T) {
1188
tests := []struct {
1289
name string

0 commit comments

Comments
 (0)