massert: implement Subset, Has, Fatal, and Error

This commit is contained in:
Brian Picciano 2018-07-08 18:19:09 +00:00
parent c297e09446
commit 69e3019fbf
2 changed files with 168 additions and 70 deletions

View File

@ -9,6 +9,7 @@ import (
"reflect" "reflect"
"runtime" "runtime"
"strings" "strings"
"testing"
"text/tabwriter" "text/tabwriter"
) )
@ -26,23 +27,31 @@ func fmtBlock(str string) string {
return "\n\t" + strings.Replace(str, "\n", "\n\t", -1) + "\n" return "\n\t" + strings.Replace(str, "\n", "\n\t", -1) + "\n"
} }
func fmtMultiDescr(prefix string, aa ...Assertion) string { func fmtMultiBlock(prefix string, elems ...string) string {
if len(aa) == 0 { if len(elems) == 0 {
return prefix + "()" return prefix + "()"
} else if len(aa) == 1 { } else if len(elems) == 1 {
return prefix + "(" + fmtBlock(aa[0].Description()) + ")" return prefix + "(" + fmtBlock(elems[0]) + ")"
} }
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
fmt.Fprintf(buf, "%s(\n", prefix) fmt.Fprintf(buf, "%s(\n", prefix)
for _, a := range aa { for _, el := range elems {
descrStr := "\t" + strings.Replace(a.Description(), "\n", "\n\t", -1) elStr := "\t" + strings.Replace(el, "\n", "\n\t", -1)
fmt.Fprintf(buf, "%s,\n", descrStr) fmt.Fprintf(buf, "%s,\n", elStr)
} }
fmt.Fprintf(buf, ")") fmt.Fprintf(buf, ")")
return buf.String() return buf.String()
} }
func fmtMultiDescr(prefix string, aa ...Assertion) string {
descrs := make([]string, len(aa))
for i := range aa {
descrs[i] = aa[i].Description()
}
return fmtMultiBlock(prefix, descrs...)
}
func fmtStack(frames []runtime.Frame) string { func fmtStack(frames []runtime.Frame) string {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
tw := tabwriter.NewWriter(buf, 0, 4, 2, ' ', 0) tw := tabwriter.NewWriter(buf, 0, 4, 2, ' ', 0)
@ -134,6 +143,24 @@ func (a *assertion) Stack() []runtime.Frame {
return a.stack return a.stack
} }
////////////////////////////////////////////////////////////////////////////////
// Fatal is a convenience function which performs the Assertion and calls Fatal
// on the testing.T instance if the assertion fails.
func Fatal(t *testing.T, a Assertion) {
if err := a.Assert(); err != nil {
t.Fatal(err)
}
}
// Error is a convenience function which performs the Assertion and calls Error
// on the testing.T instance if the assertion fails.
func Error(t *testing.T, a Assertion) {
if err := a.Assert(); err != nil {
t.Error(err)
}
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// Assertion wrappers // Assertion wrappers
@ -256,48 +283,95 @@ func None(aa ...Assertion) Assertion {
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
var typeOfInt64 = reflect.TypeOf(int64(0))
func toStr(i interface{}) string { func toStr(i interface{}) string {
return fmt.Sprintf("%T(%#v)", i, i) return fmt.Sprintf("%T(%#v)", i, i)
} }
// Equal asserts that the two values given are equal. The equality checking // Equal asserts that the two values are exactly equal, and uses the
// done is to some degree fuzzy in the following ways: // reflect.DeepEqual function to determine if they are.
//
// * All pointers are dereferenced.
// * All ints and uints are converted to int64.
//
func Equal(a, b interface{}) Assertion { func Equal(a, b interface{}) Assertion {
normalize := func(v reflect.Value) reflect.Value {
v = reflect.Indirect(v)
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
v = v.Convert(typeOfInt64)
}
return v
}
fn := func() error {
aV, bV := reflect.ValueOf(a), reflect.ValueOf(b)
aV, bV = normalize(aV), normalize(bV)
if !reflect.DeepEqual(aV.Interface(), bV.Interface()) {
return errors.New("not equal")
}
return nil
}
return newAssertion(fn, toStr(a)+" == "+toStr(b), 0)
}
// Exactly asserts that the two values are exactly equal, and uses the
// reflect.DeepEquals function to determine if they are.
func Exactly(a, b interface{}) Assertion {
return newAssertion(func() error { return newAssertion(func() error {
if !reflect.DeepEqual(a, b) { if !reflect.DeepEqual(a, b) {
return errors.New("not exactly equal") return errors.New("not exactly equal")
} }
return nil return nil
}, toStr(a)+" === "+toStr(b), 0) }, toStr(a)+" == "+toStr(b), 0)
}
func toSet(i interface{}, keyedMap bool) ([]interface{}, error) {
v := reflect.ValueOf(i)
switch v.Kind() {
case reflect.Array, reflect.Slice:
vv := make([]interface{}, v.Len())
for i := range vv {
vv[i] = v.Index(i).Interface()
}
return vv, nil
case reflect.Map:
keys := v.MapKeys()
vv := make([]interface{}, len(keys))
for i := range keys {
if keyedMap {
vv[i] = struct{ k, v interface{} }{
k: keys[i].Interface(),
v: v.MapIndex(keys[i]).Interface(),
}
} else {
vv[i] = v.MapIndex(keys[i]).Interface()
}
}
return vv, nil
default:
return nil, fmt.Errorf("cannot turn value of type %s into a set", v.Type())
}
}
// Subset asserts that the given subset is a subset of the given set. Both must
// be of the same type and may be arrays, slices, or maps.
func Subset(set, subset interface{}) Assertion {
return newAssertion(func() error {
if reflect.TypeOf(set) != reflect.TypeOf(subset) {
return errors.New("set and subset aren't of same type")
}
setVV, err := toSet(set, true)
if err != nil {
return err
}
subsetVV, err := toSet(subset, true)
if err != nil {
return err
}
// this is obviously not the most efficient way to do this
outer:
for i := range subsetVV {
for j := range setVV {
if reflect.DeepEqual(setVV[j], subsetVV[i]) {
continue outer
}
}
return fmt.Errorf("missing element %s", toStr(subsetVV[i]))
}
return nil
}, toStr(set)+" has subset "+toStr(subset), 0)
}
// Has asserts that the given set has the given element as a value in it. The
// set may be an array, a slice, or a map, and if it's a map then the elem will
// need to be a value in it.
func Has(set, elem interface{}) Assertion {
return newAssertion(func() error {
setVV, err := toSet(set, false)
if err != nil {
return err
}
for i := range setVV {
if reflect.DeepEqual(setVV[i], elem) {
return nil
}
}
return errors.New("value not in set")
}, toStr(set)+" has value "+toStr(elem), 0)
} }

View File

@ -95,41 +95,65 @@ func TestNone(t *T) {
} }
} }
// TODO pointers, structs, slices, maps, nils
func TestEqual(t *T) { func TestEqual(t *T) {
if err := All( Fatal(t, All(
Equal(1, 1), Equal(1, 1),
Equal("foo", "foo"),
))
Fatal(t, None(
Equal(1, 2),
Equal(1, int64(1)), Equal(1, int64(1)),
Equal(1, uint64(1)), Equal(1, uint64(1)),
Equal("foo", "foo"),
).Assert(); err != nil {
t.Fatal(err)
}
if err := None(
Equal(1, 2),
Equal(1, int64(2)),
Equal(1, uint64(2)),
Equal("foo", "bar"), Equal("foo", "bar"),
).Assert(); err != nil { ))
t.Fatal(err)
}
} }
func TestExactly(t *T) { func TestSubset(t *T) {
if err := All( Fatal(t, All(
Exactly(1, 1), Subset([]int{1, 2, 3}, []int{}),
Exactly("foo", "foo"), Subset([]int{1, 2, 3}, []int{1}),
).Assert(); err != nil { Subset([]int{1, 2, 3}, []int{2}),
t.Fatal(err) Subset([]int{1, 2, 3}, []int{1, 2}),
Subset([]int{1, 2, 3}, []int{2, 1}),
Subset([]int{1, 2, 3}, []int{1, 2, 3}),
Subset([]int{1, 2, 3}, []int{1, 3, 2}),
Subset(map[int]int{1: 1, 2: 2}, map[int]int{}),
Subset(map[int]int{1: 1, 2: 2}, map[int]int{1: 1}),
Subset(map[int]int{1: 1, 2: 2}, map[int]int{1: 1, 2: 2}),
))
Fatal(t, None(
Subset([]int64{1, 2, 3}, []int{1}),
Subset([]int{}, []int{1, 2, 3}),
Subset([]int{1, 2, 3}, []int{4}),
Subset([]int{1, 2, 3}, []int{1, 3, 2, 4}),
Subset(map[int]int{1: 1, 2: 2}, map[int]int64{1: 1}),
Subset(map[int]int{1: 1, 2: 2}, map[int]int{1: 2}),
Subset(map[int]int{1: 1, 2: 2}, map[int]int{1: 1, 3: 3}),
))
} }
if err := None( func TestHas(t *T) {
Exactly(1, 2), Fatal(t, All(
Exactly(1, int64(1)), Has([]int{1}, 1),
Exactly(1, uint64(1)), Has([]int{1, 2}, 1),
Exactly("foo", "bar"), Has([]int{2, 1}, 1),
).Assert(); err != nil { Has(map[int]int{1: 1}, 1),
t.Fatal(err) Has(map[int]int{1: 2}, 2),
} Has(map[int]int{1: 2, 2: 1}, 1),
Has(map[int]int{1: 2, 2: 2}, 2),
))
Fatal(t, None(
Has([]int{}, 1),
Has([]int{1}, 2),
Has([]int{2, 1}, 3),
Has(map[int]int{}, 1),
Has(map[int]int{1: 1}, 2),
Has(map[int]int{1: 2}, 1),
Has(map[int]int{1: 2, 2: 1}, 3),
))
} }