From ef2d634d8aa4be0859034af4e023866483817ac5 Mon Sep 17 00:00:00 2001 From: Robert Liebowitz Date: Wed, 24 Jan 2024 23:02:58 -0500 Subject: [PATCH] Add All/Any assertions Closes #2 --- README.md | 2 + be/compose.go | 68 ++++++++++++++++ be/compose_test.go | 190 +++++++++++++++++++++++++++++++++++++++++++++ example_test.go | 27 +++++++ 4 files changed, 287 insertions(+) diff --git a/README.md b/README.md index 665d7ec..77879a5 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,8 @@ g.Should(be.Eventually(func() ghost.Result { }, 3*time.Second, 100*time.Millisecond)) ``` +For details on other composers such as `be.Any` or `be.All`, see the [godoc][]. + #### Custom Assertions Custom assertions are easy to write and easy to use. diff --git a/be/compose.go b/be/compose.go index 88e9528..f5d5287 100644 --- a/be/compose.go +++ b/be/compose.go @@ -2,12 +2,80 @@ package be import ( "fmt" + "regexp" + "strings" "time" "github.com/rliebz/ghost" "github.com/rliebz/ghost/ghostlib" ) +// All asserts that every one of the provided assertions is true. +func All(results ...ghost.Result) ghost.Result { + args := ghostlib.ArgsFromAST(results) + return applyVariadicBooleanLogic( + true, + func(acc, val bool) bool { + return acc && val + }, + results, + args, + ) +} + +// Any asserts that at least one of the provided assertions is true. +func Any(results ...ghost.Result) ghost.Result { + args := ghostlib.ArgsFromAST(results) + return applyVariadicBooleanLogic( + false, + func(acc, val bool) bool { + return acc || val + }, + results, + args, + ) +} + +func applyVariadicBooleanLogic( + initial bool, + apply func(acc, val bool) bool, + results []ghost.Result, + args []string, +) ghost.Result { + if len(results) == 0 { + return ghost.Result{ + Ok: initial, + Message: "no assertions were provided", + } + } + + out := ghost.Result{Ok: initial} + for i, result := range results { + out.Ok = apply(out.Ok, result.Ok) + + var b strings.Builder + if i != 0 { + b.WriteString("\n\n") + } + fmt.Fprintf(&b, "assertion `%s` is %t", args[i], result.Ok) + b.WriteString("\n\t") + b.WriteString(indentString(result.Message)) + + out.Message += b.String() + } + + return out +} + +var reWhitespaceLine = regexp.MustCompile(`\n[ \t]+\n`) + +func indentString(s string) string { + s = strings.ReplaceAll(s, "\n", "\n\t") + s = reWhitespaceLine.ReplaceAllString(s, "\n\n") + s = strings.TrimSpace(s) + return s +} + // Eventually asserts that a function eventually returns an Ok [ghost.Result]. func Eventually( f func() ghost.Result, diff --git a/be/compose_test.go b/be/compose_test.go index 6659aa5..93b84a8 100644 --- a/be/compose_test.go +++ b/be/compose_test.go @@ -1,6 +1,7 @@ package be_test import ( + "fmt" "testing" "time" @@ -8,6 +9,195 @@ import ( "github.com/rliebz/ghost/be" ) +func TestAll(t *testing.T) { + t.Run("no arguments passed", func(t *testing.T) { + g := ghost.New(t) + + result := be.All() + g.Should(be.True(result.Ok)) + g.Should(be.Equal(result.Message, "no assertions were provided")) + }) + + t.Run("one valid", func(t *testing.T) { + g := ghost.New(t) + + result := be.All( + be.Equal(1, 0), + be.Equal(1, 1), + be.Equal(1, 2), + ) + + g.Should(be.False(result.Ok)) + g.Should(be.Equal( + result.Message, + fmt.Sprintf(`assertion %s is false + 1 != 0 + got: 1 + want: 0 + +assertion %s is true + 1 == 1 + +assertion %s is false + 1 != 2 + got: 1 + want: 2`, + "`be.Equal(1, 0)`", + "`be.Equal(1, 1)`", + "`be.Equal(1, 2)`", + ), + )) + }) + + t.Run("all valid", func(t *testing.T) { + g := ghost.New(t) + + result := be.All( + be.Equal(1, 1), + be.Equal(2, 2), + ) + + g.Should(be.True(result.Ok)) + g.Should(be.Equal( + result.Message, + fmt.Sprintf("assertion %s is true"+` + 1 == 1 + +assertion %s is true + 2 == 2`, + "`be.Equal(1, 1)`", + "`be.Equal(2, 2)`", + ), + )) + }) + + t.Run("nested", func(t *testing.T) { + g := ghost.New(t) + + result := be.Any( + be.Any( + be.Equal(1, 0), + be.Equal(1, 2), + ), + ) + + g.Should(be.False(result.Ok)) + g.Should(be.Equal( + result.Message, + fmt.Sprintf(`assertion %s is false + assertion %s is false + 1 != 0 + got: 1 + want: 0 + + assertion %s is false + 1 != 2 + got: 1 + want: 2`, + "`be.Any(be.Equal(1, 0), be.Equal(1, 2))`", + "`be.Equal(1, 0)`", + "`be.Equal(1, 2)`", + ), + )) + }) +} +func TestAny(t *testing.T) { + t.Run("no arguments passed", func(t *testing.T) { + g := ghost.New(t) + + result := be.Any() + g.Should(be.False(result.Ok)) + g.Should(be.Equal(result.Message, "no assertions were provided")) + }) + + t.Run("one valid", func(t *testing.T) { + g := ghost.New(t) + + result := be.Any( + be.Equal(1, 0), + be.Equal(1, 1), + be.Equal(1, 2), + ) + + g.Should(be.True(result.Ok)) + g.Should(be.Equal( + result.Message, + fmt.Sprintf(`assertion %s is false + 1 != 0 + got: 1 + want: 0 + +assertion %s is true + 1 == 1 + +assertion %s is false + 1 != 2 + got: 1 + want: 2`, + "`be.Equal(1, 0)`", + "`be.Equal(1, 1)`", + "`be.Equal(1, 2)`", + ), + )) + }) + + t.Run("none valid", func(t *testing.T) { + g := ghost.New(t) + + result := be.Any( + be.Equal(1, 0), + be.Equal(1, 2), + ) + + g.Should(be.False(result.Ok)) + g.Should(be.Equal( + result.Message, + fmt.Sprintf("assertion %s is false"+` + 1 != 0 + got: 1 + want: 0 + +assertion %s is false + 1 != 2 + got: 1 + want: 2`, + "`be.Equal(1, 0)`", + "`be.Equal(1, 2)`", + ), + )) + }) + + t.Run("nested", func(t *testing.T) { + g := ghost.New(t) + + result := be.Any( + be.Any( + be.Equal(1, 0), + be.Equal(1, 2), + ), + ) + + g.Should(be.False(result.Ok)) + g.Should(be.Equal( + result.Message, + fmt.Sprintf(`assertion %s is false + assertion %s is false + 1 != 0 + got: 1 + want: 0 + + assertion %s is false + 1 != 2 + got: 1 + want: 2`, + "`be.Any(be.Equal(1, 0), be.Equal(1, 2))`", + "`be.Equal(1, 0)`", + "`be.Equal(1, 2)`", + ), + )) + }) +} + func TestEventually(t *testing.T) { g := ghost.New(t) diff --git a/example_test.go b/example_test.go index 9608f4c..bc37006 100644 --- a/example_test.go +++ b/example_test.go @@ -42,6 +42,11 @@ func TestExample(t *testing.T) { g.Should(be.JSONEqual(`{"b": 1, "a": 0}`, `{"a": 0, "b": 1}`)) g.ShouldNot(be.JSONEqual(`{"a":1}`, `{"a":2}`)) +} + +func ExampleEventually() { + t := new(testing.T) // from the test + g := ghost.New(t) count := 0 g.Should(be.Eventually(func() ghost.Result { @@ -49,3 +54,25 @@ func TestExample(t *testing.T) { return be.Equal(count, 3) }, 100*time.Millisecond, 10*time.Millisecond)) } + +func ExampleAny() { + t := new(testing.T) // from the test + g := ghost.New(t) + + g.Should(be.Any( + be.Equal(0, 1), + be.Equal(1, 1), + be.Equal(2, 1), + )) +} + +func ExampleAll() { + t := new(testing.T) // from the test + g := ghost.New(t) + + g.Should(be.All( + be.Equal(1, 1), + be.Equal(2, 2), + be.Equal(3, 3), + )) +}