...

Source file src/github.com/onsi/gomega/gcustom/make_matcher.go

Documentation: github.com/onsi/gomega/gcustom

     1  /*
     2  package gcustom provides a simple mechanism for creating custom Gomega matchers
     3  */
     4  package gcustom
     5  
     6  import (
     7  	"fmt"
     8  	"reflect"
     9  	"strings"
    10  	"text/template"
    11  
    12  	"github.com/onsi/gomega/format"
    13  )
    14  
    15  var interfaceType = reflect.TypeOf((*interface{})(nil)).Elem()
    16  var errInterface = reflect.TypeOf((*error)(nil)).Elem()
    17  
    18  var defaultTemplate = template.Must(ParseTemplate("{{if .Failure}}Custom matcher failed for:{{else}}Custom matcher succeeded (but was expected to fail) for:{{end}}\n{{.FormattedActual}}"))
    19  
    20  func formatObject(object any, indent ...uint) string {
    21  	indentation := uint(0)
    22  	if len(indent) > 0 {
    23  		indentation = indent[0]
    24  	}
    25  	return format.Object(object, indentation)
    26  }
    27  
    28  /*
    29  ParseTemplate allows you to precompile templates for MakeMatcher's custom matchers.
    30  
    31  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.
    32  
    33  Once parsed you can pass the template in either as an argument to MakeMatcher(matchFunc, <template>) or using MakeMatcher(matchFunc).WithPrecompiledTemplate(template)
    34  */
    35  func ParseTemplate(templ string) (*template.Template, error) {
    36  	return template.New("template").Funcs(template.FuncMap{
    37  		"format": formatObject,
    38  	}).Parse(templ)
    39  }
    40  
    41  /*
    42  MakeMatcher builds a Gomega-compatible matcher from a function (the matchFunc).
    43  
    44  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 types for you and notifying the user if they use the matcher with an invalid type.
    45  
    46  MakeMatcher(matchFunc) builds a matcher with generic failure messages that look like:
    47  
    48  	Custom matcher failed for:
    49  	    <formatted actual>
    50  
    51  for the positive failure case (i.e. when Expect(actual).To(match) fails) and
    52  
    53  	Custom matcher succeeded (but was expected to fail) for:
    54  	     <formatted actual>
    55  
    56  for the negative case (i.e. when Expect(actual).NotTo(match) fails).
    57  
    58  There are two ways to provide a different message.  You can either provide a simple message string:
    59  
    60  	matcher := MakeMatcher(matchFunc, message)
    61  	matcher := MakeMatcher(matchFunc).WithMessage(message)
    62  
    63  (where message is of type string) or a template:
    64  
    65  	matcher := MakeMatcher(matchFunc).WithTemplate(templateString)
    66  
    67  where templateString is a string that is compiled by WithTemplate into a matcher.  Alternatively you can provide a precompiled template like this:
    68  
    69  	template, err = gcustom.ParseTemplate(templateString) //use gcustom's ParseTemplate to get some additional functions mixed in
    70  	matcher := MakeMatcher(matchFunc, template)
    71  	matcher := MakeMatcher(matchFunc).WithPrecompiled(template)
    72  
    73  When a simple message string is provided the positive failure message will look like:
    74  
    75  	Expected:
    76  		<formatted actual>
    77  	to <message>
    78  
    79  and the negative failure message will look like:
    80  
    81  	Expected:
    82  		<formatted actual>
    83  	not to <message>
    84  
    85  A template allows you to have greater control over the message.  For more details see the docs for WithTemplate
    86  */
    87  func MakeMatcher(matchFunc any, args ...any) CustomGomegaMatcher {
    88  	t := reflect.TypeOf(matchFunc)
    89  	if !(t.Kind() == reflect.Func && t.NumIn() == 1 && t.NumOut() == 2 && t.Out(0).Kind() == reflect.Bool && t.Out(1).Implements(errInterface)) {
    90  		panic("MakeMatcher must be passed a function that takes one argument and returns (bool, error)")
    91  	}
    92  	var finalMatchFunc func(actual any) (bool, error)
    93  	if t.In(0) == interfaceType {
    94  		finalMatchFunc = matchFunc.(func(actual any) (bool, error))
    95  	} else {
    96  		matchFuncValue := reflect.ValueOf(matchFunc)
    97  		finalMatchFunc = reflect.MakeFunc(reflect.TypeOf(finalMatchFunc),
    98  			func(args []reflect.Value) []reflect.Value {
    99  				actual := args[0].Interface()
   100  				if actual == nil && reflect.TypeOf(actual) == reflect.TypeOf(nil) {
   101  					return matchFuncValue.Call([]reflect.Value{reflect.New(t.In(0)).Elem()})
   102  				} else if reflect.TypeOf(actual).AssignableTo(t.In(0)) {
   103  					return matchFuncValue.Call([]reflect.Value{reflect.ValueOf(actual)})
   104  				} else {
   105  					return []reflect.Value{
   106  						reflect.ValueOf(false),
   107  						reflect.ValueOf(fmt.Errorf("Matcher expected actual of type <%s>.  Got:\n%s", t.In(0), format.Object(actual, 1))),
   108  					}
   109  				}
   110  			}).Interface().(func(actual any) (bool, error))
   111  	}
   112  
   113  	matcher := CustomGomegaMatcher{
   114  		matchFunc:       finalMatchFunc,
   115  		templateMessage: defaultTemplate,
   116  	}
   117  
   118  	for _, arg := range args {
   119  		switch v := arg.(type) {
   120  		case string:
   121  			matcher = matcher.WithMessage(v)
   122  		case *template.Template:
   123  			matcher = matcher.WithPrecompiledTemplate(v)
   124  		}
   125  	}
   126  
   127  	return matcher
   128  }
   129  
   130  // CustomGomegaMatcher is generated by MakeMatcher - you should always use MakeMatcher to construct custom matchers
   131  type CustomGomegaMatcher struct {
   132  	matchFunc                   func(actual any) (bool, error)
   133  	templateMessage             *template.Template
   134  	templateData                any
   135  	customFailureMessage        func(actual any) string
   136  	customNegatedFailureMessage func(actual any) string
   137  }
   138  
   139  /*
   140  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:
   141  
   142  	Expected:
   143  		<formatted actual>
   144  	to <message>
   145  
   146  and a negative failure message that looks like:
   147  
   148  	Expected:
   149  		<formatted actual>
   150  	not to <message>
   151  */
   152  func (c CustomGomegaMatcher) WithMessage(message string) CustomGomegaMatcher {
   153  	return c.WithTemplate("Expected:\n{{.FormattedActual}}\n{{.To}} " + message)
   154  }
   155  
   156  /*
   157  WithTemplate compiles the passed-in template and returns a CustomGomegaMatcher configured to use that template to generate failure messages.
   158  
   159  Templates are provided the following variables and functions:
   160  
   161  {{.Failure}} - a bool that, if true, indicates this should be a positive failure message, otherwise this should be a negated failure message
   162  {{.NegatedFailure}} - a bool that, if true, indicates this should be a negated failure message, otherwise this should be a positive failure message
   163  {{.To}} - is set to "to" if this is a positive failure message and "not to" if this is a negated failure message
   164  {{.Actual}} - the actual passed in to the matcher
   165  {{.FormattedActual}} - a string representing the formatted actual.  This can be multiple lines and is always generated with an indentation of 1
   166  {{format <object> <optional-indentation}} - 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.
   167  
   168  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}}.
   169  
   170  Here's a simple example of all these pieces working together:
   171  
   172  	func HaveWidget(widget Widget) OmegaMatcher {
   173  		return MakeMatcher(func(machine Machine) (bool, error) {
   174  			return machine.HasWidget(widget), nil
   175  		}).WithTemplate("Expected:\n{{.FormattedActual}}\n{{.To}} have widget named {{.Data.Name}}:\n{{format .Data 1}}", widget)
   176  	}
   177  
   178  	Expect(machine).To(HaveWidget(Widget{Name: "sprocket", Version: 2}))
   179  
   180  Would generate a failure message that looks like:
   181  
   182  	Expected:
   183  		<formatted machine>
   184  	to have widget named sprocket:
   185  		<formatted sprocket>
   186  */
   187  func (c CustomGomegaMatcher) WithTemplate(templ string, data ...any) CustomGomegaMatcher {
   188  	return c.WithPrecompiledTemplate(template.Must(ParseTemplate(templ)), data...)
   189  }
   190  
   191  /*
   192  WithPrecompiledTemplate returns a CustomGomegaMatcher configured to use the passed-in template.  The template should be precompiled with gcustom.ParseTemplate().
   193  
   194  As with WithTemplate() you can provide a single pice of additional data as an optional argument.  This is accessed in the template via {{.Data}}
   195  */
   196  func (c CustomGomegaMatcher) WithPrecompiledTemplate(templ *template.Template, data ...any) CustomGomegaMatcher {
   197  	c.templateMessage = templ
   198  	c.templateData = nil
   199  	if len(data) > 0 {
   200  		c.templateData = data[0]
   201  	}
   202  	return c
   203  }
   204  
   205  /*
   206  WithTemplateData() returns a CustomGomegaMatcher configured to provide it's template with the passed-in data.  The following are equivalent:
   207  
   208  MakeMatcher(matchFunc).WithTemplate(templateString, data)
   209  MakeMatcher(matchFunc).WithTemplate(templateString).WithTemplateData(data)
   210  */
   211  func (c CustomGomegaMatcher) WithTemplateData(data any) CustomGomegaMatcher {
   212  	c.templateData = data
   213  	return c
   214  }
   215  
   216  // Match runs the passed-in match func and satisfies the GomegaMatcher interface
   217  func (c CustomGomegaMatcher) Match(actual any) (bool, error) {
   218  	return c.matchFunc(actual)
   219  }
   220  
   221  // FailureMessage generates the positive failure message configured via WithMessage or WithTemplate/WithPrecompiledTemplate
   222  // i.e. this is the failure message when Expect(actual).To(match) fails
   223  func (c CustomGomegaMatcher) FailureMessage(actual any) string {
   224  	return c.renderTemplateMessage(actual, true)
   225  }
   226  
   227  // NegatedFailureMessage generates the negative failure message configured via WithMessage or WithTemplate/WithPrecompiledTemplate
   228  // i.e. this is the failure message when Expect(actual).NotTo(match) fails
   229  func (c CustomGomegaMatcher) NegatedFailureMessage(actual any) string {
   230  	return c.renderTemplateMessage(actual, false)
   231  }
   232  
   233  type templateData struct {
   234  	Failure         bool
   235  	NegatedFailure  bool
   236  	To              string
   237  	FormattedActual string
   238  	Actual          any
   239  	Data            any
   240  }
   241  
   242  func (c CustomGomegaMatcher) renderTemplateMessage(actual any, isFailure bool) string {
   243  	var data templateData
   244  	formattedActual := format.Object(actual, 1)
   245  	if isFailure {
   246  		data = templateData{
   247  			Failure:         true,
   248  			NegatedFailure:  false,
   249  			To:              "to",
   250  			FormattedActual: formattedActual,
   251  			Actual:          actual,
   252  			Data:            c.templateData,
   253  		}
   254  	} else {
   255  		data = templateData{
   256  			Failure:         false,
   257  			NegatedFailure:  true,
   258  			To:              "not to",
   259  			FormattedActual: formattedActual,
   260  			Actual:          actual,
   261  			Data:            c.templateData,
   262  		}
   263  	}
   264  	b := &strings.Builder{}
   265  	err := c.templateMessage.Execute(b, data)
   266  	if err != nil {
   267  		return fmt.Sprintf("Failed to render failure message template: %s", err.Error())
   268  	}
   269  	return b.String()
   270  }
   271  

View as plain text