diff --git a/mtest/massert/massert.go b/mtest/massert/massert.go index de7cf15..968e341 100644 --- a/mtest/massert/massert.go +++ b/mtest/massert/massert.go @@ -26,6 +26,23 @@ func fmtBlock(str string) string { return "\n\t" + strings.Replace(str, "\n", "\n\t", -1) + "\n" } +func fmtMultiDescr(prefix string, aa ...Assertion) string { + if len(aa) == 0 { + return prefix + "()" + } else if len(aa) == 1 { + return prefix + "(" + fmtBlock(aa[0].Description()) + ")" + } + + buf := new(bytes.Buffer) + fmt.Fprintf(buf, "%s(\n", prefix) + for _, a := range aa { + descrStr := "\t" + strings.Replace(a.Description(), "\n", "\n\t", -1) + fmt.Fprintf(buf, "%s,\n", descrStr) + } + fmt.Fprintf(buf, ")") + return buf.String() +} + func fmtStack(frames []runtime.Frame) string { buf := new(bytes.Buffer) tw := tabwriter.NewWriter(buf, 0, 4, 2, ' ', 0) @@ -41,6 +58,7 @@ func fmtStack(frames []runtime.Frame) string { func (ae AssertErr) Error() string { buf := new(bytes.Buffer) + fmt.Fprintf(buf, "\n") fmt.Fprintf(buf, "Assertion: %s\n", fmtBlock(ae.Assertion.Description())) fmt.Fprintf(buf, "Error: %s\n", fmtBlock(ae.Err.Error())) fmt.Fprintf(buf, "Stack: %s\n", fmtBlock(fmtStack(ae.Assertion.Stack()))) @@ -93,6 +111,8 @@ func newAssertion(assertFn func() error, descr string, skip int) Assertion { err := assertFn() if err == nil { return nil + } else if ae, ok := err.(AssertErr); ok { + return ae } return AssertErr{ Err: err, @@ -114,31 +134,6 @@ func (a *assertion) Stack() []runtime.Frame { return a.stack } -// Assertions represents a set of Assertions which can be tested all at once. -type Assertions []Assertion - -// New returns an empty set of Assertions which can be Add'd to. -func New() Assertions { - return make(Assertions, 0, 8) -} - -// Add adds the given Assertion to the set. -func (aa *Assertions) Add(a Assertion) { - (*aa) = append(*aa, a) -} - -// Assert performs the Assert method of each of the set's Assertions -// sequentially, stopping at the first error and generating a new one which -// includes the Assertion's string and stack information. -func (aa Assertions) Assert() error { - for _, a := range aa { - if err := a.Assert(); err != nil { - return err - } - } - return nil -} - //////////////////////////////////////////////////////////////////////////////// // Assertion wrappers @@ -168,7 +163,7 @@ func (dw descrWrap) Description() string { return dw.descr } -// Comment prepends a formatted string to the given Assertions string +// Comment prepends a formatted string to the given Assertion's string // description. func Comment(a Assertion, msg string, args ...interface{}) Assertion { msg = strings.TrimSpace(msg) @@ -177,28 +172,86 @@ func Comment(a Assertion, msg string, args ...interface{}) Assertion { return wrap{descrWrap{Assertion: a, descr: descr}} } -type not struct { - Assertion -} - -func (n not) Assert() error { - if err := n.Assertion.Assert(); err == nil { - return AssertErr{ - Err: errors.New("assertion should have failed"), - Assertion: n, - } - } - return nil -} - -func (n not) Description() string { - return "not(" + fmtBlock(n.Assertion.Description()) + ")" -} - // Not negates an Assertion, so that it fails if the given Assertion does not, // and vice-versa. func Not(a Assertion) Assertion { - return not{Assertion: a} + fn := func() error { + if err := a.Assert(); err == nil { + return errors.New("assertion should have failed") + } + return nil + } + return newAssertion(fn, fmtMultiDescr("Not", a), 0) +} + +// Any asserts that at least one of the given Assertions succeeds. +func Any(aa ...Assertion) Assertion { + fn := func() error { + for _, a := range aa { + if err := a.Assert(); err == nil { + return nil + } + } + return errors.New("no assertions succeeded") + } + return newAssertion(fn, fmtMultiDescr("Any", aa...), 0) +} + +// AnyOne asserts that exactly one of the given Assertions succeeds. +func AnyOne(aa ...Assertion) Assertion { + fn := func() error { + any := -1 + for i, a := range aa { + if err := a.Assert(); err == nil { + if any >= 0 { + return fmt.Errorf("assertions indices %d and %d both succeeded", any, i) + } + any = i + } + } + if any == -1 { + return errors.New("no assertions succeeded") + } + return nil + } + return newAssertion(fn, fmtMultiDescr("AnyOne", aa...), 0) +} + +// All asserts that at all of the given Assertions succeed. Its Assert method +// will return the error of whichever Assertion failed. +func All(aa ...Assertion) Assertion { + fn := func() error { + for _, a := range aa { + if err := a.Assert(); err != nil { + // newAssertion will pass this error through, so that its + // description and callstack is what gets displayed as the + // error. This isn't totally consistent with Any's behavior, but + // it's fine. + return err + } + } + return nil + } + return newAssertion(fn, fmtMultiDescr("All", aa...), 0) +} + +// None asserts that all of the given Assertions fail. +// +// NOTE this is functionally equivalent to doing `Not(Any(aa...))`, but the +// error returned is more helpful. +func None(aa ...Assertion) Assertion { + fn := func() error { + for _, a := range aa { + if err := a.Assert(); err == nil { + return AssertErr{ + Err: errors.New("assertion should not have succeeded"), + Assertion: a, + } + } + } + return nil + } + return newAssertion(fn, fmtMultiDescr("None", aa...), 0) } //////////////////////////////////////////////////////////////////////////////// diff --git a/mtest/massert/massert_test.go b/mtest/massert/massert_test.go index 47c9737..a4e5081 100644 --- a/mtest/massert/massert_test.go +++ b/mtest/massert/massert_test.go @@ -1,24 +1,135 @@ package massert -import . "testing" +import ( + "errors" + . "testing" +) -func TestAssertions(t *T) { - a := Equal(1, 1) - b := Equal(2, 2) - if err := (Assertions{a, b}).Assert(); err != nil { - t.Fatalf("first Assertions shouldn't return error, returned: %s", err) - } - - c := Comment(Equal(3, 3), "this part would succeed") - c = Comment(Not(c), "but it's being wrapped in a not, so it then won't") - - aa := New() - aa.Add(a) - aa.Add(b) - aa.Add(c) - err := aa.Assert() - if err == nil { - t.Fatalf("second Assertions should have returned an error, returned nil") - } - t.Logf("got expected second Assertions error:\n%s", err) +func succeed() Assertion { + return newAssertion(func() error { return nil }, "Succeed", 0) +} + +func fail() Assertion { + return newAssertion(func() error { return errors.New("failure") }, "Fail", 0) +} + +func TestNot(t *T) { + if err := Not(succeed()).Assert(); err == nil { + t.Fatal("Not(succeed()) should have failed") + } + + if err := Not(fail()).Assert(); err != nil { + t.Fatal(err) + } +} + +func TestAny(t *T) { + if err := Any().Assert(); err == nil { + t.Fatal("empty Any should fail") + } + + if err := Any(succeed(), succeed()).Assert(); err != nil { + t.Fatal(err) + } + + if err := Any(succeed(), fail()).Assert(); err != nil { + t.Fatal(err) + } + + if err := Any(fail(), fail()).Assert(); err == nil { + t.Fatal("Any should have failed with all inner fail Assertions") + } +} + +func TestAnyOne(t *T) { + if err := AnyOne().Assert(); err == nil { + t.Fatal("empty AnyOne should fail") + } + + if err := AnyOne(succeed(), succeed()).Assert(); err == nil { + t.Fatal("AnyOne with two succeeds should fail") + } + + if err := AnyOne(succeed(), fail()).Assert(); err != nil { + t.Fatal(err) + } + + if err := AnyOne(fail(), fail()).Assert(); err == nil { + t.Fatal("AnyOne should have failed with all inner fail Assertions") + } +} + +func TestAll(t *T) { + if err := All().Assert(); err != nil { + t.Fatal(err) + } + + if err := All(succeed(), succeed()).Assert(); err != nil { + t.Fatal(err) + } + + if err := All(succeed(), fail()).Assert(); err == nil { + t.Fatal("All should have failed with one inner fail Assertion") + } + + if err := All(fail(), fail()).Assert(); err == nil { + t.Fatal("All should have failed with all inner fail Assertions") + } +} + +func TestNone(t *T) { + if err := None().Assert(); err != nil { + t.Fatal(err) + } + + if err := None(succeed(), succeed()).Assert(); err == nil { + t.Fatal("None should have failed with all inner succeed Assertions") + } + + if err := None(succeed(), fail()).Assert(); err == nil { + t.Fatal("None should have failed with one inner succeed Assertion") + } + + if err := None(fail(), fail()).Assert(); err != nil { + t.Fatal(err) + } +} + +// TODO pointers, structs, slices, maps, nils +func TestEqual(t *T) { + if err := All( + Equal(1, 1), + Equal(1, int64(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"), + ).Assert(); err != nil { + t.Fatal(err) + } +} + +func TestExactly(t *T) { + if err := All( + Exactly(1, 1), + Exactly("foo", "foo"), + ).Assert(); err != nil { + t.Fatal(err) + } + + if err := None( + Exactly(1, 2), + Exactly(1, int64(1)), + Exactly(1, uint64(1)), + Exactly("foo", "bar"), + ).Assert(); err != nil { + t.Fatal(err) + } }