Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions assert/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -1015,12 +1015,12 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok

listKind := reflect.TypeOf(list).Kind()
if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map {
return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...)
return Fail(t, fmt.Sprintf("%#v has an unsupported type %s", list, listKind), msgAndArgs...)
}

subsetKind := reflect.TypeOf(subset).Kind()
if subsetKind != reflect.Array && subsetKind != reflect.Slice && subsetKind != reflect.Map {
return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...)
return Fail(t, fmt.Sprintf("%#v has an unsupported type %s", subset, subsetKind), msgAndArgs...)
}

if subsetKind == reflect.Map && listKind == reflect.Map {
Expand Down Expand Up @@ -1083,12 +1083,12 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{})

listKind := reflect.TypeOf(list).Kind()
if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map {
return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...)
return Fail(t, fmt.Sprintf("%#v has an unsupported type %s", list, listKind), msgAndArgs...)
}

subsetKind := reflect.TypeOf(subset).Kind()
if subsetKind != reflect.Array && subsetKind != reflect.Slice && subsetKind != reflect.Map {
return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...)
return Fail(t, fmt.Sprintf("%#v has an unsupported type %s", subset, subsetKind), msgAndArgs...)
}

if subsetKind == reflect.Map && listKind == reflect.Map {
Expand All @@ -1107,7 +1107,7 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{})
}
}

return Fail(t, fmt.Sprintf("%s is a subset of %s", truncatingFormat("%q", subset), truncatingFormat("%q", list)), msgAndArgs...)
return Fail(t, fmt.Sprintf("%s is a subset of %s", truncatingFormat("%#v", subset), truncatingFormat("%#v", list)), msgAndArgs...)
}

subsetList := reflect.ValueOf(subset)
Expand All @@ -1122,14 +1122,14 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{})
element := subsetList.Index(i).Interface()
ok, found := containsElement(list, element)
if !ok {
return Fail(t, fmt.Sprintf("%q could not be applied builtin len()", list), msgAndArgs...)
return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", list), msgAndArgs...)
}
if !found {
return true
}
}

return Fail(t, fmt.Sprintf("%s is a subset of %s", truncatingFormat("%q", subset), truncatingFormat("%q", list)), msgAndArgs...)
return Fail(t, fmt.Sprintf("%s is a subset of %s", truncatingFormat("%#v", subset), truncatingFormat("%#v", list)), msgAndArgs...)
}

// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified
Expand Down Expand Up @@ -1162,7 +1162,7 @@ func ElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...interface
func isList(t TestingT, list interface{}, msgAndArgs ...interface{}) (ok bool) {
kind := reflect.TypeOf(list).Kind()
if kind != reflect.Array && kind != reflect.Slice {
return Fail(t, fmt.Sprintf("%q has an unsupported type %s, expecting array or slice", list, kind),
return Fail(t, fmt.Sprintf("%#v has an unsupported type %s, expecting array or slice", list, kind),
msgAndArgs...)
}
return true
Expand Down
212 changes: 202 additions & 10 deletions assert/assertions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1202,19 +1202,19 @@ func TestSubsetNotSubset(t *testing.T) {
}{
// cases that are expected to contain
{[]int{1, 2, 3}, nil, true, `nil is the empty set which is a subset of every set`},
{[]int{1, 2, 3}, []int{}, true, `[] is a subset of ['\x01' '\x02' '\x03']`},
{[]int{1, 2, 3}, []int{1, 2}, true, `['\x01' '\x02'] is a subset of ['\x01' '\x02' '\x03']`},
{[]int{1, 2, 3}, []int{1, 2, 3}, true, `['\x01' '\x02' '\x03'] is a subset of ['\x01' '\x02' '\x03']`},
{[]string{"hello", "world"}, []string{"hello"}, true, `["hello"] is a subset of ["hello" "world"]`},
{[]int{1, 2, 3}, []int{}, true, `[]int{} is a subset of []int{1, 2, 3}`},
{[]int{1, 2, 3}, []int{1, 2}, true, `[]int{1, 2} is a subset of []int{1, 2, 3}`},
{[]int{1, 2, 3}, []int{1, 2, 3}, true, `[]int{1, 2, 3} is a subset of []int{1, 2, 3}`},
{[]string{"hello", "world"}, []string{"hello"}, true, `[]string{"hello"} is a subset of []string{"hello", "world"}`},
{map[string]string{
"a": "x",
"c": "z",
"b": "y",
}, map[string]string{
"a": "x",
"b": "y",
}, true, `map["a":"x" "b":"y"] is a subset of map["a":"x" "b":"y" "c":"z"]`},
{[]string{"a", "b", "c"}, map[string]int{"a": 1, "c": 3}, true, `map["a":'\x01' "c":'\x03'] is a subset of ["a" "b" "c"]`},
}, true, `map[string]string{"a":"x", "b":"y"} is a subset of map[string]string{"a":"x", "b":"y", "c":"z"}`},
{[]string{"a", "b", "c"}, map[string]int{"a": 1, "c": 3}, true, `map[string]int{"a":1, "c":3} is a subset of []string{"a", "b", "c"}`},

// cases that are expected not to contain
{[]string{"hello", "world"}, []string{"hello", "testify"}, false, `[]string{"hello", "world"} does not contain "testify"`},
Expand Down Expand Up @@ -1288,6 +1288,198 @@ func TestNotSubsetNil(t *testing.T) {
}
}

// TestSubsetNotSubsetErrorMessages verifies that Subset and NotSubset produce
// readable error messages using Go-syntax formatting (%#v) for various data types.
// This catches regressions where %q was used, which produces broken output like
// %!q(bool=true) for non-string types.
Comment on lines +1291 to +1294
Copy link
Collaborator

Choose a reason for hiding this comment

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

You should mention the related issues too

func TestSubsetNotSubsetErrorMessages(t *testing.T) {
t.Parallel()

t.Run("NotSubset with bool slices", func(t *testing.T) {
mockT := new(mockTestingT)
NotSubset(mockT, []bool{true, false}, []bool{true})
msg := mockT.errorString()
Contains(t, msg, "[]bool{true} is a subset of []bool{true, false}")
// Ensure no broken %q formatting like %!q(bool=true)
NotContains(t, msg, "%!q")
Comment on lines +1302 to +1304
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this is enough

Suggested change
Contains(t, msg, "[]bool{true} is a subset of []bool{true, false}")
// Ensure no broken %q formatting like %!q(bool=true)
NotContains(t, msg, "%!q")
Contains(t, msg, "[]bool{true} is a subset of []bool{true, false}")

The code may divert and this refers to something you remove, and that no longer exists once this PR will be merged.

For me checking, Contains is enough.

This comment apply to everything you added to this PR

Copy link
Collaborator

Choose a reason for hiding this comment

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

If we want to add a detection of bad formatting looking for "%!" would be OK if we consider it will detect:

  • %!(MISSING)
  • %!(EXTRA string=foo) and variation
  • %!d(string=hello), %!q(bool=true)

But then I feel it should be part of testify test suite, and not limited to this PR, and the refactoring would be way larger than the scope of this PR and related issue

})

t.Run("NotSubset with float64 slices", func(t *testing.T) {
mockT := new(mockTestingT)
NotSubset(mockT, []float64{1.1, 2.2, 3.3}, []float64{1.1, 2.2})
msg := mockT.errorString()
Contains(t, msg, "[]float64{1.1, 2.2} is a subset of []float64{1.1, 2.2, 3.3}")
NotContains(t, msg, "%!q")
})

t.Run("NotSubset with uint slices", func(t *testing.T) {
mockT := new(mockTestingT)
NotSubset(mockT, []uint{10, 20, 30}, []uint{10, 20})
msg := mockT.errorString()
Contains(t, msg, "[]uint{0xa, 0x14} is a subset of []uint{0xa, 0x14, 0x1e}")
NotContains(t, msg, "%!q")
})

t.Run("NotSubset with map of int to bool", func(t *testing.T) {
mockT := new(mockTestingT)
NotSubset(mockT,
map[int]bool{1: true, 2: false, 3: true},
map[int]bool{1: true, 2: false},
)
msg := mockT.errorString()
Contains(t, msg, "is a subset of")
// Verify %#v formatting is used (type info present, no %!q artifacts)
Contains(t, msg, "map[int]bool{")
NotContains(t, msg, "%!q")
})

t.Run("Subset failure with bool slices", func(t *testing.T) {
mockT := new(mockTestingT)
Subset(mockT, []bool{true}, []bool{true, false})
msg := mockT.errorString()
Contains(t, msg, "[]bool{true} does not contain false")
NotContains(t, msg, "%!q")
})

t.Run("Subset failure with float64 slices", func(t *testing.T) {
mockT := new(mockTestingT)
Subset(mockT, []float64{1.1, 2.2}, []float64{1.1, 3.3})
msg := mockT.errorString()
Contains(t, msg, "does not contain 3.3")
NotContains(t, msg, "%!q")
})

t.Run("NotSubset with byte slices", func(t *testing.T) {
mockT := new(mockTestingT)
NotSubset(mockT, []byte{0x01, 0x02, 0x03}, []byte{0x01, 0x02})
msg := mockT.errorString()
Contains(t, msg, "is a subset of")
// %#v for byte slices uses hex notation like []byte{0x1, 0x2}
Contains(t, msg, "[]byte{")
NotContains(t, msg, "%!q")
})

t.Run("NotSubset with struct slices", func(t *testing.T) {
type item struct {
Name string
Val int
}
mockT := new(mockTestingT)
list := []item{{"a", 1}, {"b", 2}, {"c", 3}}
subset := []item{{"a", 1}, {"b", 2}}
NotSubset(mockT, list, subset)
msg := mockT.errorString()
Contains(t, msg, "is a subset of")
Contains(t, msg, "assert.item{")
NotContains(t, msg, "%!q")
})

t.Run("NotSubset with empty bool slice", func(t *testing.T) {
mockT := new(mockTestingT)
NotSubset(mockT, []bool{true, false}, []bool{})
msg := mockT.errorString()
Contains(t, msg, "[]bool{} is a subset of []bool{true, false}")
})

t.Run("NotSubset with string slices still readable", func(t *testing.T) {
// Strings were the one type %q handled well; verify %#v still produces
// clear output (quoted strings with type info).
mockT := new(mockTestingT)
NotSubset(mockT, []string{"hello", "world"}, []string{"hello"})
msg := mockT.errorString()
Contains(t, msg, `[]string{"hello"} is a subset of []string{"hello", "world"}`)
NotContains(t, msg, "%!q")
})

t.Run("NotSubset map-to-map with int keys", func(t *testing.T) {
mockT := new(mockTestingT)
NotSubset(mockT, map[int]string{1: "a", 2: "b"}, map[int]string{1: "a"})
msg := mockT.errorString()
Contains(t, msg, "is a subset of")
Contains(t, msg, "map[int]string{")
NotContains(t, msg, "%!q")
})

t.Run("Subset failure with map of int keys", func(t *testing.T) {
mockT := new(mockTestingT)
Subset(mockT, map[int]string{1: "a"}, map[int]string{1: "a", 2: "b"})
msg := mockT.errorString()
Contains(t, msg, "does not contain")
Contains(t, msg, "map[int]string{")
NotContains(t, msg, "%!q")
})

t.Run("Subset failure with uint slices", func(t *testing.T) {
mockT := new(mockTestingT)
Subset(mockT, []uint{10, 20}, []uint{10, 30})
msg := mockT.errorString()
Contains(t, msg, "does not contain")
NotContains(t, msg, "%!q")
})

t.Run("NotSubset with int32 slices", func(t *testing.T) {
mockT := new(mockTestingT)
NotSubset(mockT, []int32{1, 2, 3}, []int32{1, 2})
msg := mockT.errorString()
Contains(t, msg, "is a subset of")
Contains(t, msg, "[]int32{")
NotContains(t, msg, "%!q")
})
}

// TestSubsetNotSubsetUnsupportedTypes verifies that Subset and NotSubset
// produce readable error messages when called with unsupported types
// (not array, slice, or map). This covers the "unsupported type" error
// paths that previously used %q formatting.
func TestSubsetNotSubsetUnsupportedTypes(t *testing.T) {
t.Parallel()

t.Run("Subset with unsupported list type", func(t *testing.T) {
mockT := new(mockTestingT)
Subset(mockT, 42, []int{1})
msg := mockT.errorString()
Contains(t, msg, "has an unsupported type")
Contains(t, msg, "int")
NotContains(t, msg, "%!q")
})

t.Run("Subset with unsupported subset type", func(t *testing.T) {
mockT := new(mockTestingT)
Subset(mockT, []int{1, 2}, "not a slice")
msg := mockT.errorString()
Contains(t, msg, "has an unsupported type")
Contains(t, msg, "string")
NotContains(t, msg, "%!q")
})

t.Run("NotSubset with unsupported list type", func(t *testing.T) {
mockT := new(mockTestingT)
NotSubset(mockT, true, []bool{true})
msg := mockT.errorString()
Contains(t, msg, "has an unsupported type")
Contains(t, msg, "bool")
NotContains(t, msg, "%!q")
})

t.Run("NotSubset with unsupported subset type", func(t *testing.T) {
mockT := new(mockTestingT)
NotSubset(mockT, []int{1, 2}, 42)
msg := mockT.errorString()
Contains(t, msg, "has an unsupported type")
Contains(t, msg, "int")
NotContains(t, msg, "%!q")
})

t.Run("ElementsMatch with unsupported type", func(t *testing.T) {
mockT := new(mockTestingT)
ElementsMatch(mockT, "not a slice", []int{1})
msg := mockT.errorString()
Contains(t, msg, "has an unsupported type")
Contains(t, msg, "expecting array or slice")
NotContains(t, msg, "%!q")
})
}

func Test_containsElement(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -4032,8 +4224,8 @@ func TestNotSubsetWithSliceTooLongToPrint(t *testing.T) {
NotSubset(mockT, longSlice, longSlice)
Contains(t, mockT.errorString(), `
Error Trace:
Error: ['\x00' '\x00' '\x00'`)
Contains(t, mockT.errorString(), `<... truncated> is a subset of ['\x00' '\x00' '\x00'`)
Error: []int{0, 0, 0`)
Contains(t, mockT.errorString(), `<... truncated> is a subset of []int{0, 0, 0`)
}

func TestNotSubsetWithMapTooLongToPrint(t *testing.T) {
Expand All @@ -4043,8 +4235,8 @@ func TestNotSubsetWithMapTooLongToPrint(t *testing.T) {
NotSubset(mockT, map[int][]int{1: longSlice}, map[int][]int{1: longSlice})
Contains(t, mockT.errorString(), `
Error Trace:
Error: map['\x01':['\x00' '\x00' '\x00'`)
Contains(t, mockT.errorString(), `<... truncated> is a subset of map['\x01':['\x00' '\x00' '\x00'`)
Error: map[int][]int{1:[]int{0, 0, 0`)
Contains(t, mockT.errorString(), `<... truncated> is a subset of map[int][]int{1:[]int{0, 0, 0`)
}

func TestSameWithSliceTooLongToPrint(t *testing.T) {
Expand Down