Skip to content

Commit

Permalink
godoc for gcustom
Browse files Browse the repository at this point in the history
  • Loading branch information
onsi committed Oct 27, 2022
1 parent 6a2e51e commit 0cfc53b
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 0 deletions.
118 changes: 118 additions & 0 deletions gcustom/make_matcher.go
@@ -1,3 +1,6 @@
/*
package gcustom provides a simple mechanism for creating custom Gomega matchers
*/
package gcustom

import (
Expand All @@ -22,12 +25,65 @@ func formatObject(object any, indent ...uint) string {
return format.Object(object, indentation)
}

/*
ParseTemplate allows you to precompile templates for MakeMatcher's custom matchers.
Use ParseTemplate if you are concerned about performance and would like to avoid repeatedly parsing failure message templates. The data made available to the template is documented in the WithTemplate() method of CustomGomegaMatcher.
Once parsed you can pass the template in either as an argument to MakeMatcher(matchFunc, <template>) or using MakeMatcher(matchFunc).WithPrecompiledTemplate(template)
*/
func ParseTemplate(templ string) (*template.Template, error) {
return template.New("template").Funcs(template.FuncMap{
"format": formatObject,
}).Parse(templ)
}

/*
MakeMatcher builds a Gomega-compatible matcher from a function (the matchFunc).
matchFunc must return (bool, error) and take a single argument. If you want to perform type-checking yourself pass in a matchFunc of type `func(actual any) (bool, error)`. If you want to only operate on a specific type, pass in `func(actual DesiredType) (bool, error)`; MakeMatcher will take care of checking typues for you and notifying the user if they use the matcher with an invalid type.
MakeMatcher(matchFunc) builds a matcher with generic failure messages that look like:
Custom matcher failed for:
<formatted actual>
for the positive failure case (i.e. when Expect(actual).To(match) fails) and
Custom matcher succeeded (but was expected to fail) for:
<formatted actual>
for the negative case (i.e. when Expect(actual).NotTo(match) fails).
There are two ways to provide a different message. You can either provide a simple message string:
matcher := MakeMatcher(matchFunc, message)
matcher := MakeMatcher(matchFunc).WithMessage(message)
(where message is of type string) or a template:
matcher := MakeMatcher(matchFunc).WithTemplate(templateString)
where templateString is a string that is compiled by WithTemplate into a matcher. Alternatively you can provide a precompiled template like this:
template, err = gcustom.ParseTemplate(templateString) //use gcustom's ParseTemplate to get some additional functions mixed in
matcher := MakeMatcher(matchFunc, template)
matcher := MakeMatcher(matchFunc).WithPrcompiled(template)
When a simple message string is provided the positive failure message will look like:
Expected:
<formatted actual>
to <message>
and the negative failure message will look like:
Expected:
<formatted actual>
not to <message>
A template allows you to have greater control over the message. For more details see the docs for WithTemplate
*/
func MakeMatcher(matchFunc any, args ...any) CustomGomegaMatcher {
t := reflect.TypeOf(matchFunc)
if !(t.Kind() == reflect.Func && t.NumIn() == 1 && t.NumOut() == 2 && t.Out(0).Kind() == reflect.Bool && t.Out(1).Implements(errInterface)) {
Expand Down Expand Up @@ -69,6 +125,7 @@ func MakeMatcher(matchFunc any, args ...any) CustomGomegaMatcher {
return matcher
}

// CustomGomegaMatcher is generated by MakeMatcher - you should always use MakeMatcher to construct custom matchers
type CustomGomegaMatcher struct {
matchFunc func(actual any) (bool, error)
templateMessage *template.Template
Expand All @@ -77,14 +134,63 @@ type CustomGomegaMatcher struct {
customNegatedFailureMessage func(actual any) string
}

/*
WithMessage returns a CustomGomegaMatcher configured with a message to display when failure occurs. Matchers configured this way produce a positive failure message that looks like:
Expected:
<formatted actual>
to <message>
and a negative failure message that looks like:
Expected:
<formatted actual>
not to <message>
*/
func (c CustomGomegaMatcher) WithMessage(message string) CustomGomegaMatcher {
return c.WithTemplate("Expected:\n{{.FormattedActual}}\n{{.To}} " + message)
}

/*
WithTemplate compiles the passed-in template and returns a CustomGomegaMatcher configured to use that template to generate failure messages.
Templates are provided the following variables and functions:
{{.Failure}} - a bool that, if true, indicates this should be a positive failure message, otherwise this should be a negated failure message
{{.NegatedFailure}} - a bool that, if true, indicates this should be a negated failure message, otherwise this should be a positive failure message
{{.To}} - is set to "to" if this is a positive failure message and "not to" if this is a negated failure message
{{.Actual}} - the actual passed in to the matcher
{{.FormattedActual}} - a string representing the formatted actual. This can be multiple lines and is always generated with an indentation of 1
{{format <object> <optional-indentaiton}} - a function that allows you to use Gomega's default formatting from within the template. The passed-in <object> is formatted and <optional-indentation> can be set to an integer to control indentation.
In addition, you can provide custom data to the template by calling WithTemplate(templateString, data) (where data can be anything). This is provided to the template as {{.Data}}.
Here's a simple example of all these pieces working together:
func HaveWidget(widget Widget) OmegaMatcher {
return MakeMatcher(func(machine Machine) (bool, error) {
return maching.HasWidget(widget), nil
}).WithTemplate("Expected:\n{{.FormattedActual}}\n{{.To}} have widget named {{.Data.Name}}:\n{{format .Data 1}}", widget)
}
Expect(maching).To(HaveWidget(Widget{Name: "sprocket", Version: 2}))
Would generate a failure message that looks like:
Expected:
<formatted machine>
to have widget named sprocker:
<formatted sprocket>
*/
func (c CustomGomegaMatcher) WithTemplate(templ string, data ...any) CustomGomegaMatcher {
return c.WithPrecompiledTemplate(template.Must(ParseTemplate(templ)), data...)
}

/*
WithPrecompiledTemplate returns a CustomGomegaMatcher configured to use the passed-in template. The template should be precompiled with gcustom.ParseTemplate().
As with WithTemplate() you can provide a single pice of additional data as an optional argument. This is accessed in the template via {{.Data}}
*/
func (c CustomGomegaMatcher) WithPrecompiledTemplate(templ *template.Template, data ...any) CustomGomegaMatcher {
c.templateMessage = templ
c.templateData = nil
Expand All @@ -94,19 +200,31 @@ func (c CustomGomegaMatcher) WithPrecompiledTemplate(templ *template.Template, d
return c
}

/*
WithTemplateData() returns a CustomGomegaMatcher configured to provide it's template with the passed-in data. The following are equivalent:
MakeMatcher(matchFunc).WithTemplate(templateString, data)
MakeMatcher(matchFunc).WithTemplate(templateString).WithTemplateData(data)
*/
func (c CustomGomegaMatcher) WithTemplateData(data any) CustomGomegaMatcher {
c.templateData = data
return c
}

// Match runs the passed-in match func and satisfies the GomegaMatcher interface
func (c CustomGomegaMatcher) Match(actual any) (bool, error) {
return c.matchFunc(actual)
}

// FailureMessage generates the positive failure message configured via WithMessage or WithTemplate/WithPrecompiledTemplate
// i.e. this is the failure message when Expect(actual).To(match) fails
func (c CustomGomegaMatcher) FailureMessage(actual any) string {
return c.renderTemplateMessage(actual, true)
}

// NegatedFailureMessage generates the negative failure message configured via WithMessage or WithTemplate/WithPrecompiledTemplate
// i.e. this is the failure message when Expect(actual).NotTo(match) fails
func (c CustomGomegaMatcher) NegatedFailureMessage(actual any) string {
return c.renderTemplateMessage(actual, false)
}
Expand Down
57 changes: 57 additions & 0 deletions gcustom/make_matcher_test.go
Expand Up @@ -2,17 +2,74 @@ package gcustom_test

import (
"errors"
"runtime"
"strings"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gcustom"
"github.com/onsi/gomega/internal"
)

// InstrumentedGomega
type InstrumentedGomega struct {
G *internal.Gomega
FailureMessage string
FailureSkip []int
RegisteredHelpers []string
}

func NewInstrumentedGomega() *InstrumentedGomega {
out := &InstrumentedGomega{}

out.G = internal.NewGomega(internal.FetchDefaultDurationBundle())
out.G.Fail = func(message string, skip ...int) {
out.FailureMessage = message
out.FailureSkip = skip
}
out.G.THelper = func() {
pc, _, _, _ := runtime.Caller(1)
f := runtime.FuncForPC(pc)
funcName := strings.TrimPrefix(f.Name(), "github.com/onsi/gomega/internal.")
out.RegisteredHelpers = append(out.RegisteredHelpers, funcName)
}

return out
}

type someType struct {
Name string
}

var _ = Describe("MakeMatcher", func() {
It("generatees a custom matcher that satisfies the GomegaMatcher interface and renders correct failure messages", func() {
m := gcustom.MakeMatcher(func(a int) (bool, error) {
if a == 0 {
return true, nil
}
if a == 1 {
return false, nil
}
return false, errors.New("bam")
}).WithMessage("match")

Ω(0).Should(m)
Ω(1).ShouldNot(m)

ig := NewInstrumentedGomega()
ig.G.Ω(1).Should(m)
Ω(ig.FailureMessage).Should(Equal("Expected:\n <int>: 1\nto match"))

ig.G.Ω(0).ShouldNot(m)
Ω(ig.FailureMessage).Should(Equal("Expected:\n <int>: 0\nnot to match"))

ig.G.Ω(2).Should(m)
Ω(ig.FailureMessage).Should(Equal("bam"))

ig.G.Ω(2).ShouldNot(m)
Ω(ig.FailureMessage).Should(Equal("bam"))
})

Describe("validating and wrapping the MatchFunc", func() {
DescribeTable("it panics when passed an invalid function", func(f any) {
Expect(func() {
Expand Down

0 comments on commit 0cfc53b

Please sign in to comment.