From 0b8d3be329b71c6e6c18ebc4d79a0d024ffde678 Mon Sep 17 00:00:00 2001 From: Robert Liebowitz Date: Sun, 3 May 2026 17:00:25 -0400 Subject: [PATCH 1/3] Delay error message construction until error messages are printed While on an individual basis, assertions are relatively cheap, it turns out to not be so cheap when you put a whole test suite together. This delays building the error message, and in particular, performing AST parsing, until someone actually attempts to print it. Which means a passing test suite never pays the cost. As future work in this vein, it might also make sense to cache parsed files such that multiple failing calls in the same file don't parse the file multiple times. This includes a breaking change to `ghostlib` to allow lazily evaluating arguments while still being roughly as easy to use. --- README.md | 4 +- be/assertions.go | 418 +++++++++++++++++++++++------------------- be/assertions_test.go | 200 ++++++++++---------- be/compose.go | 67 +++---- be/compose_test.go | 29 +-- be/errors.go | 185 ++++++++++--------- be/errors_test.go | 60 +++--- be/ordered.go | 108 ++++++----- be/ordered_test.go | 144 +++++++-------- ghost.go | 12 +- ghost_test.go | 16 +- ghostlib/ast.go | 76 ++++---- go.mod | 4 +- 13 files changed, 703 insertions(+), 620 deletions(-) diff --git a/README.md b/README.md index c0e2f52..17985fd 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,9 @@ func BeThirteen(i int) ghost.Result { return ghost.Result{ Ok: i == 13, - Message: fmt.Sprintf("%v is %d", args[0], i), + Message: func() string { + return fmt.Sprintf("%v is %d", args.Get(0), i) + } } } ``` diff --git a/be/assertions.go b/be/assertions.go index cdab1cc..a6c8944 100644 --- a/be/assertions.go +++ b/be/assertions.go @@ -18,12 +18,13 @@ import ( // The target must be a non-nil pointer. func AssignedAs[T any](value any, target *T) ghost.Result { args := ghostlib.ArgsFromAST(value, target) - argValue, argTarget := args[0], args[1] if target == nil { return ghost.Result{ - Ok: false, - Message: fmt.Sprintf("target %s cannot be nil", argTarget), + Ok: false, + Message: func() string { + return fmt.Sprintf("target %s cannot be nil", args.Get(1)) + }, } } @@ -31,15 +32,19 @@ func AssignedAs[T any](value any, target *T) ghost.Result { if !ok { return ghost.Result{ Ok: false, - Message: fmt.Sprintf( - `%s (%T) could not be assigned to %s (%T) + Message: func() string { + argValue, argTarget := args.Get(0), args.Get(1) + + return fmt.Sprintf( + `%s (%T) could not be assigned to %s (%T) value: %v`, - argValue, - value, - argTarget, - target, - value, - ), + argValue, + value, + argTarget, + target, + value, + ) + }, } } @@ -47,156 +52,168 @@ value: %v`, return ghost.Result{ Ok: true, - Message: fmt.Sprintf( - `%s (%T) was assigned to %s (%T) + Message: func() string { + argValue, argTarget := args.Get(0), args.Get(1) + + return fmt.Sprintf( + `%s (%T) was assigned to %s (%T) value: %v`, - argValue, - value, - argTarget, - target, - value, - ), + argValue, + value, + argTarget, + target, + value, + ) + }, } } // Close asserts that a value is within a delta of another. func Close[T constraints.Integer | constraints.Float](got, want, delta T) ghost.Result { args := ghostlib.ArgsFromAST(got, want, delta) - argGot, argWant := args[0], args[1] - gotDelta := want - got if gotDelta < 0 { gotDelta = 0 - gotDelta } - if _, err := strconv.ParseFloat(argGot, 64); err != nil { - argGot = fmt.Sprintf("%s (%v)", argGot, got) - } - - if _, err := strconv.ParseFloat(argWant, 64); err != nil { - argWant = fmt.Sprintf("%s (%v)", argWant, want) - } - if gotDelta <= delta { return ghost.Result{ Ok: true, - Message: fmt.Sprintf( - `delta %v between %s and %s is within %v + Message: func() string { + argGot, argWant := args.Get(0), args.Get(1) + argGot = formatNumeric(argGot, got) + argWant = formatNumeric(argWant, want) + + return fmt.Sprintf( + `delta %v between %s and %s is within %v got: %v want: %v delta: %v`, - gotDelta, argGot, argWant, delta, - got, - want, - gotDelta, - ), + gotDelta, argGot, argWant, delta, + got, + want, + gotDelta, + ) + }, } } return ghost.Result{ Ok: false, - Message: fmt.Sprintf( - `delta %v between %s and %s is not within %v + Message: func() string { + argGot, argWant := args.Get(0), args.Get(1) + argGot = formatNumeric(argGot, got) + argWant = formatNumeric(argWant, want) + + return fmt.Sprintf( + `delta %v between %s and %s is not within %v got: %v want: %v delta: %v`, - gotDelta, argGot, argWant, delta, - got, - want, - gotDelta, - ), + gotDelta, argGot, argWant, delta, + got, + want, + gotDelta, + ) + }, } } +// formatNumeric with the arg and value, or simply the value if the arg is +// a numeric literal. +func formatNumeric[T constraints.Integer | constraints.Float](arg string, value T) string { + if _, err := strconv.ParseFloat(arg, 64); err != nil { + return fmt.Sprintf("%s (%v)", arg, value) + } + + return arg +} + // DeepEqual asserts that two elements are deeply equal. func DeepEqual[T any](got, want T) ghost.Result { args := ghostlib.ArgsFromAST(got, want) - argGot, argWant := args[0], args[1] - if diff := colorDiff(want, got); diff != "" { return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`%v != %v -%v`, argGot, argWant, diff), + Message: func() string { + argGot, argWant := args.Get(0), args.Get(1) + return fmt.Sprintf(`%v != %v +%v`, argGot, argWant, diff) + }, } } return ghost.Result{ Ok: true, - Message: fmt.Sprintf(`%v == %v + Message: func() string { + argGot, argWant := args.Get(0), args.Get(1) + return fmt.Sprintf(`%v == %v value: %v -`, argGot, argWant, want), +`, argGot, argWant, want) + }, } } // Equal asserts that two elements are equal. func Equal[T comparable](got T, want T) ghost.Result { args := ghostlib.ArgsFromAST(got, want) - argGot, argWant := args[0], args[1] - if got == want { - switch fmt.Sprint(want) { - case argGot, argWant: - return ghost.Result{ - Ok: true, - Message: fmt.Sprintf(`%v == %v`, argGot, argWant), - } - default: - return ghost.Result{ - Ok: true, - Message: fmt.Sprintf(`%v == %v + return ghost.Result{ + Ok: true, + Message: func() string { + argGot, argWant := args.Get(0), args.Get(1) + switch fmt.Sprint(want) { + case argGot, argWant: + return fmt.Sprintf(`%v == %v`, argGot, argWant) + default: + return fmt.Sprintf(`%v == %v value: %v -`, argGot, argWant, want), - } +`, argGot, argWant, want) + } + }, } } - switch v := reflect.ValueOf(want); v.Kind() { - // These are types cmp tends to do particularly well - case - reflect.Array, - reflect.Interface, - reflect.Map, - reflect.Pointer, - reflect.Slice, - reflect.Struct: - - return ghost.Result{ - Ok: false, - Message: fmt.Sprintf(`%v != %v -%v`, argGot, argWant, colorDiff(want, got)), - } - case reflect.String: - if strings.ContainsAny(v.String(), "\n\r") || - strings.ContainsAny(reflect.ValueOf(got).String(), "\n\r") { - - return ghost.Result{ - Ok: false, - Message: fmt.Sprintf(`%v != %v -%v`, argGot, argWant, colorDiff(want, got)), - } - } - - return ghost.Result{ - Ok: false, - Message: fmt.Sprintf(`%v != %v + return ghost.Result{ + Ok: false, + Message: func() string { + argGot, argWant := args.Get(0), args.Get(1) + switch v := reflect.ValueOf(want); v.Kind() { + // These are types cmp tends to do particularly well + case + reflect.Array, + reflect.Interface, + reflect.Map, + reflect.Pointer, + reflect.Slice, + reflect.Struct: + + return fmt.Sprintf(`%v != %v +%v`, argGot, argWant, colorDiff(want, got)) + case reflect.String: + if strings.ContainsAny(v.String(), "\n\r") || + strings.ContainsAny(reflect.ValueOf(got).String(), "\n\r") { + + return fmt.Sprintf(`%v != %v +%v`, argGot, argWant, colorDiff(want, got)) + } + + return fmt.Sprintf(`%v != %v got: %v want: %v `, - argGot, - argWant, - quoteString(reflect.ValueOf(got).String()), - quoteString(reflect.ValueOf(want).String()), - ), - } - } + argGot, + argWant, + quoteString(reflect.ValueOf(got).String()), + quoteString(reflect.ValueOf(want).String()), + ) + } - return ghost.Result{ - Ok: false, - Message: fmt.Sprintf(`%v != %v + return fmt.Sprintf(`%v != %v got: %v want: %v -`, argGot, argWant, got, want), +`, argGot, argWant, got, want) + }, } } @@ -215,77 +232,87 @@ func quoteString(s string) string { // False asserts that a value is false. func False(b bool) ghost.Result { args := ghostlib.ArgsFromAST(b) - argB := args[0] - return ghost.Result{ - Ok: !b, - Message: fmt.Sprintf("%v is %t", argB, b), + Ok: !b, + Message: func() string { + return fmt.Sprintf("%v is %t", args.Get(0), b) + }, } } // JSONEqual asserts that two sets of JSON-encoded data are equivalent. func JSONEqual[T ~string | ~[]byte](got, want T) ghost.Result { args := ghostlib.ArgsFromAST(got, want) - argGot, argWant := args[0], args[1] - diff, kind := colorJSONDiff(got, want) switch kind { case jsondiff.Match: return ghost.Result{ - Ok: true, - Message: fmt.Sprintf("%v and %v are JSON equal", argGot, argWant), + Ok: true, + Message: func() string { + return fmt.Sprintf("%v and %v are JSON equal", args.Get(0), args.Get(1)) + }, } case jsondiff.GotInvalid: return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`%v is not valid JSON -value: %s`, argGot, got), + Message: func() string { + return fmt.Sprintf(`%v is not valid JSON +value: %s`, args.Get(0), got) + }, } case jsondiff.WantInvalid: return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`%v is not valid JSON -value: %s`, argWant, want), + Message: func() string { + return fmt.Sprintf(`%v is not valid JSON +value: %s`, args.Get(1), want) + }, } case jsondiff.BothInvalid: return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`%v and %v are not valid JSON + Message: func() string { + return fmt.Sprintf(`%v and %v are not valid JSON got: %s want: -%s`, argGot, argWant, got, want), +%s`, args.Get(0), args.Get(1), got, want) + }, } } return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`%v and %v are not JSON equal -%s`, argGot, argWant, diff), + Message: func() string { + return fmt.Sprintf(`%v and %v are not JSON equal +%s`, args.Get(0), args.Get(1), diff) + }, } } // MapLen asserts that the length of a map is a particular size. func MapLen[K comparable, V any](got map[K]V, want int) ghost.Result { args := ghostlib.ArgsFromAST(got, want) - argGot := args[0] - if len(got) == want { return ghost.Result{ Ok: true, - Message: fmt.Sprintf(`%v is length %d + Message: func() string { + return fmt.Sprintf(`%v is length %d map: %v -`, argGot, len(got), mapToString(got)), +`, args.Get(0), len(got), mapToString(got)) + }, } } return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`%v is length %d, not %d + Message: func() string { + return fmt.Sprintf(`%v is length %d, not %d map: %v -`, argGot, len(got), want, mapToString(got)), +`, args.Get(0), len(got), want, mapToString(got)) + }, } } @@ -305,18 +332,20 @@ func mapToString[K comparable, V any](m map[K]V) string { // Nil asserts that the given value is nil. func Nil(v any) ghost.Result { args := ghostlib.ArgsFromAST(v) - argV := args[0] - if isNil(v) { return ghost.Result{ - Ok: true, - Message: fmt.Sprintf("%v is nil", argV), + Ok: true, + Message: func() string { + return fmt.Sprintf("%v is nil", args.Get(0)) + }, } } return ghost.Result{ - Ok: false, - Message: fmt.Sprintf("%v is %v, not nil", argV, v), + Ok: false, + Message: func() string { + return fmt.Sprintf("%v is %v, not nil", args.Get(0), v) + }, } } @@ -344,36 +373,38 @@ func isNil(v any) bool { // SliceContaining asserts that an element exists in a given slice. func SliceContaining[T comparable](slice []T, element T) ghost.Result { args := ghostlib.ArgsFromAST(slice, element) - argSlice, argElement := args[0], args[1] - for _, x := range slice { if x == element { return ghost.Result{ Ok: true, - Message: fmt.Sprintf(`%v contains %v + Message: func() string { + return fmt.Sprintf(`%v contains %v slice: %v element: %v `, - argSlice, - argElement, - sliceElementToString(slice, element), - element, - ), + args.Get(0), + args.Get(1), + sliceElementToString(slice, element), + element, + ) + }, } } } return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`%v does not contain %v + Message: func() string { + return fmt.Sprintf(`%v does not contain %v slice: %v element: %v `, - argSlice, - argElement, - sliceElementToString(slice, element), - element, - ), + args.Get(0), + args.Get(1), + sliceElementToString(slice, element), + element, + ) + }, } } @@ -401,22 +432,24 @@ func sliceElementToString[T comparable](slice []T, element T) string { // SliceLen asserts that the length of a slice is a particular size. func SliceLen[T any](got []T, want int) ghost.Result { args := ghostlib.ArgsFromAST(got, want) - argGot := args[0] - if len(got) == want { return ghost.Result{ Ok: true, - Message: fmt.Sprintf(`%v is length %d + Message: func() string { + return fmt.Sprintf(`%v is length %d slice: %v -`, argGot, len(got), sliceToString(got)), +`, args.Get(0), len(got), sliceToString(got)) + }, } } return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`%v is length %d, not %d + Message: func() string { + return fmt.Sprintf(`%v is length %d, not %d slice: %v -`, argGot, len(got), want, sliceToString(got)), +`, args.Get(0), len(got), want, sliceToString(got)) + }, } } @@ -440,105 +473,110 @@ func sliceToString[T any](slice []T) string { // StringContaining asserts that a substring exists in a given string. func StringContaining(str, substr string) ghost.Result { args := ghostlib.ArgsFromAST(str, substr) - argStr, argSubstr := args[0], args[1] - if strings.Contains(str, substr) { return ghost.Result{ Ok: true, - Message: fmt.Sprintf(`%v contains %v + Message: func() string { + return fmt.Sprintf(`%v contains %v str: %s substr: %s -`, argStr, argSubstr, quoteString(str), quoteString(substr)), +`, args.Get(0), args.Get(1), quoteString(str), quoteString(substr)) + }, } } return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`%v does not contain %v + Message: func() string { + return fmt.Sprintf(`%v does not contain %v str: %s substr: %s -`, argStr, argSubstr, quoteString(str), quoteString(substr)), +`, args.Get(0), args.Get(1), quoteString(str), quoteString(substr)) + }, } } // StringMatching asserts that a given string matches a regular expression. func StringMatching(str string, expr string) ghost.Result { args := ghostlib.ArgsFromAST(str, expr) - argStr, argExpr := args[0], args[1] - re, err := regexp.Compile(expr) if err != nil { return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`%v is not a valid regular expression + Message: func() string { + return fmt.Sprintf(`%v is not a valid regular expression %v `, - argExpr, - err, - ), + args.Get(1), + err, + ) + }, } } if re.MatchString(str) { return ghost.Result{ Ok: true, - Message: fmt.Sprintf(`%v matches regular expression %v + Message: func() string { + return fmt.Sprintf(`%v matches regular expression %v str: %s expr: %s `, - argStr, argExpr, - quoteString(str), - re.String(), - ), + args.Get(0), args.Get(1), + quoteString(str), + re.String(), + ) + }, } } return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`%v does not match regular expression %v + Message: func() string { + return fmt.Sprintf(`%v does not match regular expression %v str: %s expr: %s `, - argStr, argExpr, - quoteString(str), - re.String(), - ), + args.Get(0), args.Get(1), + quoteString(str), + re.String(), + ) + }, } } // True asserts that a value is true. func True(b bool) ghost.Result { args := ghostlib.ArgsFromAST(b) - argB := args[0] - return ghost.Result{ - Ok: b, - Message: fmt.Sprintf("%v is %t", argB, b), + Ok: b, + Message: func() string { + return fmt.Sprintf("%v is %t", args.Get(0), b) + }, } } // Zero asserts that the given value equals its zero value. func Zero[T comparable](v T) ghost.Result { args := ghostlib.ArgsFromAST(v) - argV := args[0] - var zero T if v == zero { return ghost.Result{ - Ok: true, - Message: fmt.Sprintf("%v is the zero value", argV), - } - } - - if argV != fmt.Sprint(v) { - return ghost.Result{ - Ok: false, - Message: fmt.Sprintf("%v is non-zero\nvalue: %v", argV, v), + Ok: true, + Message: func() string { + return fmt.Sprintf("%v is the zero value", args.Get(0)) + }, } } return ghost.Result{ - Ok: false, - Message: fmt.Sprintf("%v is non-zero", argV), + Ok: false, + Message: func() string { + argV := args.Get(0) + if argV != fmt.Sprint(v) { + return fmt.Sprintf("%v is non-zero\nvalue: %v", argV, v) + } + return fmt.Sprintf("%v is non-zero", argV) + }, } } diff --git a/be/assertions_test.go b/be/assertions_test.go index 4af55c5..5739962 100644 --- a/be/assertions_test.go +++ b/be/assertions_test.go @@ -20,12 +20,12 @@ func TestAssignedAs(t *testing.T) { result := be.AssignedAs(got, &want) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `got (string) was assigned to &want (*string) + g.Should(be.Equal(result.Message(), `got (string) was assigned to &want (*string) value: some-value`)) result = be.AssignedAs("some-value", new(string)) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `"some-value" (string) was assigned to new(string) (*string) + g.Should(be.Equal(result.Message(), `"some-value" (string) was assigned to new(string) (*string) value: some-value`)) }) @@ -37,12 +37,12 @@ value: some-value`)) result := be.AssignedAs(got, &want) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `got (int) could not be assigned to &want (*string) + g.Should(be.Equal(result.Message(), `got (int) could not be assigned to &want (*string) value: 15`)) result = be.AssignedAs(15, new(string)) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `15 (int) could not be assigned to new(string) (*string) + g.Should(be.Equal(result.Message(), `15 (int) could not be assigned to new(string) (*string) value: 15`, )) }) @@ -55,13 +55,13 @@ value: 15`, result := be.AssignedAs(got, &want) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `got (*bytes.Buffer) was assigned to &want (*io.Reader) + g.Should(be.Equal(result.Message(), `got (*bytes.Buffer) was assigned to &want (*io.Reader) value: `)) result = be.AssignedAs(new(bytes.Buffer), new(io.Reader)) g.Check(result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `new(bytes.Buffer) (*bytes.Buffer) was assigned to new(io.Reader) (*io.Reader) value: `)) }) @@ -74,12 +74,16 @@ value: `)) result := be.AssignedAs(got, &want) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `got (int) could not be assigned to &want (*io.Reader) + g.Should(be.Equal( + result.Message(), + `got (int) could not be assigned to &want (*io.Reader) value: 15`)) result = be.AssignedAs(15, new(io.Reader)) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `15 (int) could not be assigned to new(io.Reader) (*io.Reader) + g.Should(be.Equal( + result.Message(), + `15 (int) could not be assigned to new(io.Reader) (*io.Reader) value: 15`)) }) @@ -91,7 +95,7 @@ value: 15`)) result := be.AssignedAs(got, want) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, "target want cannot be nil")) + g.Should(be.Equal(result.Message(), "target want cannot be nil")) }) t.Run("panic", func(t *testing.T) { @@ -117,7 +121,7 @@ func TestClose(t *testing.T) { result := be.Close(got, want, 1) g.Check(result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `delta 0.5 between got (32) and want (32.5) is within 1 got: 32 want: 32.5 @@ -127,7 +131,7 @@ delta: 0.5`, result = be.Close(32.0, 32.5, 1.0) g.Check(result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `delta 0.5 between 32.0 and 32.5 is within 1 got: 32 want: 32.5 @@ -137,7 +141,7 @@ delta: 0.5`, result = be.Close(32.5, 32.0, 1.0) g.Check(result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `delta 0.5 between 32.5 and 32.0 is within 1 got: 32.5 want: 32 @@ -154,7 +158,7 @@ delta: 0.5`, result := be.Close(got, want, 0.3) g.Check(!result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `delta 0.5 between got (32) and want (32.5) is not within 0.3 got: 32 want: 32.5 @@ -164,7 +168,7 @@ delta: 0.5`, result = be.Close(32.0, 32.5, 0.3) g.Check(!result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `delta 0.5 between 32.0 and 32.5 is not within 0.3 got: 32 want: 32.5 @@ -187,13 +191,13 @@ func TestDeepEqual(t *testing.T) { result := be.DeepEqual(got, want) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `got == want + g.Should(be.Equal(result.Message(), `got == want value: {foo [1 2]} `)) result = be.DeepEqual(T{"foo", []int{1}}, T{"foo", []int{1}}) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `T{"foo", []int{1}} == T{"foo", []int{1}} + g.Should(be.Equal(result.Message(), `T{"foo", []int{1}} == T{"foo", []int{1}} value: {foo [1]} `)) }) @@ -222,8 +226,8 @@ diff (-want +got): + b: 0, } ` - result.Message = strings.ReplaceAll(result.Message, "\u00a0", " ") - g.Should(be.Equal(result.Message, wantText)) + gotMsg := strings.ReplaceAll(result.Message(), "\u00a0", " ") + g.Should(be.Equal(gotMsg, wantText)) result = be.DeepEqual(T{"bar", 0}, T{"foo", 1}) g.Check(!result.Ok) @@ -237,8 +241,8 @@ diff (-want +got): + b: 0, } ` - result.Message = strings.ReplaceAll(result.Message, "\u00a0", " ") - g.Should(be.Equal(result.Message, wantText)) + gotMsg = strings.ReplaceAll(result.Message(), "\u00a0", " ") + g.Should(be.Equal(gotMsg, wantText)) }) } @@ -256,13 +260,13 @@ func TestEqual(t *testing.T) { result := be.Equal(got, want) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `got == want + g.Should(be.Equal(result.Message(), `got == want value: {foo 1} `)) result = be.Equal(T{"foo", 1}, T{"foo", 1}) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `T{"foo", 1} == T{"foo", 1} + g.Should(be.Equal(result.Message(), `T{"foo", 1} == T{"foo", 1} value: {foo 1} `)) }) @@ -274,7 +278,7 @@ value: {foo 1} result := be.Equal(got, 3) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `got == 3`)) + g.Should(be.Equal(result.Message(), `got == 3`)) }) t.Run("unequal int", func(t *testing.T) { @@ -290,7 +294,7 @@ value: {foo 1} got: 0 want: 1 ` - g.Should(be.Equal(result.Message, wantText)) + g.Should(be.Equal(result.Message(), wantText)) result = be.Equal(0, 1) g.Check(!result.Ok) @@ -299,7 +303,7 @@ want: 1 got: 0 want: 1 ` - g.Should(be.Equal(result.Message, wantText)) + g.Should(be.Equal(result.Message(), wantText)) }) t.Run("unequal string short", func(t *testing.T) { @@ -315,7 +319,7 @@ want: 1 got: "bar" want: "foo" ` - g.Should(be.Equal(result.Message, wantText)) + g.Should(be.Equal(result.Message(), wantText)) result = be.Equal("bar", "foo") g.Check(!result.Ok) @@ -324,7 +328,7 @@ want: "foo" got: "bar" want: "foo" ` - g.Should(be.Equal(result.Message, wantText)) + g.Should(be.Equal(result.Message(), wantText)) }) t.Run("unequal string long", func(t *testing.T) { @@ -343,8 +347,8 @@ diff (-want +got): + "bar", ) ` - result.Message = strings.ReplaceAll(result.Message, "\u00a0", " ") - g.Should(be.Equal(result.Message, wantText)) + got = strings.ReplaceAll(result.Message(), "\u00a0", " ") + g.Should(be.Equal(got, wantText)) result = be.Equal("bar", "foo\nbar\nbaz") g.Check(!result.Ok) @@ -356,8 +360,8 @@ diff (-want +got): + "bar", ) ` - result.Message = strings.ReplaceAll(result.Message, "\u00a0", " ") - g.Should(be.Equal(result.Message, wantText)) + got = strings.ReplaceAll(result.Message(), "\u00a0", " ") + g.Should(be.Equal(got, wantText)) }) t.Run("unequal struct", func(t *testing.T) { @@ -384,8 +388,8 @@ diff (-want +got): + B: 0, } ` - result.Message = strings.ReplaceAll(result.Message, "\u00a0", " ") - g.Should(be.Equal(result.Message, wantText)) + gotMsg := strings.ReplaceAll(result.Message(), "\u00a0", " ") + g.Should(be.Equal(gotMsg, wantText)) result = be.Equal(T{"bar", 0}, T{"foo", 1}) g.Check(!result.Ok) @@ -399,8 +403,8 @@ diff (-want +got): + B: 0, } ` - result.Message = strings.ReplaceAll(result.Message, "\u00a0", " ") - g.Should(be.Equal(result.Message, wantText)) + gotMsg = strings.ReplaceAll(result.Message(), "\u00a0", " ") + g.Should(be.Equal(gotMsg, wantText)) }) t.Run("custom string type", func(t *testing.T) { @@ -413,14 +417,14 @@ diff (-want +got): result := be.Equal(got, want) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `got != want + g.Should(be.Equal(result.Message(), `got != want got: "foo" want: "bar" `)) result = be.Equal(CustomString("foo"), CustomString("bar")) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `CustomString("foo") != CustomString("bar") + g.Should(be.Equal(result.Message(), `CustomString("foo") != CustomString("bar") got: "foo" want: "bar" `)) @@ -434,11 +438,11 @@ func TestFalse(t *testing.T) { v := true result := be.False(v) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, "v is true")) + g.Should(be.Equal(result.Message(), "v is true")) result = be.False(true) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, "true is true")) + g.Should(be.Equal(result.Message(), "true is true")) }) t.Run("false", func(t *testing.T) { @@ -447,11 +451,11 @@ func TestFalse(t *testing.T) { v := false result := be.False(v) g.Check(result.Ok) - g.Should(be.Equal(result.Message, "v is false")) + g.Should(be.Equal(result.Message(), "v is false")) result = be.False(false) g.Check(result.Ok) - g.Should(be.Equal(result.Message, "false is false")) + g.Should(be.Equal(result.Message(), "false is false")) }) } @@ -464,12 +468,12 @@ func TestJSONEqual(t *testing.T) { result := be.JSONEqual(got, want) g.Check(result.Ok) - g.Should(be.Equal(result.Message, "got and want are JSON equal")) + g.Should(be.Equal(result.Message(), "got and want are JSON equal")) result = be.JSONEqual(`{"bar": [1, 2], "foo": "value"}`, `{"foo": "value", "bar": [1, 2]}`) g.Check(result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), "`{\"bar\": [1, 2], \"foo\": \"value\"}` and "+ "`{\"foo\": \"value\", \"bar\": [1, 2]}` are JSON equal", )) @@ -483,12 +487,12 @@ func TestJSONEqual(t *testing.T) { result := be.JSONEqual(got, want) g.Check(!result.Ok) - g.Should(be.StringContaining(result.Message, "got and want are not JSON equal")) + g.Should(be.StringContaining(result.Message(), "got and want are not JSON equal")) result = be.JSONEqual(`{"bar": [2, 1], "foo": "other"}`, `{"foo": "value", "bar": [1, 2]}`) g.Check(!result.Ok) g.Should(be.StringContaining( - result.Message, + result.Message(), "`{\"bar\": [2, 1], \"foo\": \"other\"}` and "+ "`{\"foo\": \"value\", \"bar\": [1, 2]}` are not JSON equal", )) @@ -503,17 +507,17 @@ func TestJSONEqual(t *testing.T) { result := be.JSONEqual(valid, invalid) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `invalid is not valid JSON + g.Should(be.Equal(result.Message(), `invalid is not valid JSON value: {{`)) result = be.JSONEqual(invalid, valid) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `invalid is not valid JSON + g.Should(be.Equal(result.Message(), `invalid is not valid JSON value: {{`)) result = be.JSONEqual(invalid, invalid2) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `invalid and invalid2 are not valid JSON + g.Should(be.Equal(result.Message(), `invalid and invalid2 are not valid JSON got: {{ @@ -523,7 +527,7 @@ want: result = be.JSONEqual(`{"bar": [1, 2], "foo": "value"}`, `{"foo": "value", "bar": [1, 2]}`) g.Check(result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), "`{\"bar\": [1, 2], \"foo\": \"value\"}` and "+ "`{\"foo\": \"value\", \"bar\": [1, 2]}` are JSON equal", )) @@ -539,12 +543,12 @@ func TestMapLen(t *testing.T) { result := be.MapLen(m, wantLen) g.Check(result.Ok) - g.Should(be.StringContaining(result.Message, `m is length 3`)) + g.Should(be.StringContaining(result.Message(), `m is length 3`)) result = be.MapLen(map[string]int{"a": 1, "b": 2, "c": 3}, 3) g.Check(result.Ok) g.Should(be.StringContaining( - result.Message, + result.Message(), `map[string]int{"a": 1, "b": 2, "c": 3} is length 3`, )) }) @@ -557,12 +561,12 @@ func TestMapLen(t *testing.T) { result := be.MapLen(m, wantLen) g.Check(result.Ok) - g.Should(be.StringContaining(result.Message, `m is length 4`)) + g.Should(be.StringContaining(result.Message(), `m is length 4`)) result = be.MapLen(map[string]int{"a": 1, "b": 2, "c": 3, "d": 4}, 4) g.Check(result.Ok) g.Should(be.StringContaining( - result.Message, + result.Message(), `map[string]int{"a": 1, "b": 2, "c": 3, "d": 4} is length 4`, )) }) @@ -575,12 +579,12 @@ func TestMapLen(t *testing.T) { result := be.MapLen(m, wantLen) g.Check(!result.Ok) - g.Should(be.StringContaining(result.Message, `m is length 3, not 2`)) + g.Should(be.StringContaining(result.Message(), `m is length 3, not 2`)) result = be.MapLen(map[string]int{"a": 1, "b": 2, "c": 3}, 2) g.Check(!result.Ok) g.Should(be.StringContaining( - result.Message, + result.Message(), `map[string]int{"a": 1, "b": 2, "c": 3} is length 3, not 2`, )) }) @@ -593,12 +597,12 @@ func TestMapLen(t *testing.T) { result := be.MapLen(m, wantLen) g.Check(!result.Ok) - g.Should(be.StringContaining(result.Message, `m is length 4, not 3`)) + g.Should(be.StringContaining(result.Message(), `m is length 4, not 3`)) result = be.MapLen(map[string]int{"a": 1, "b": 2, "c": 3, "d": 4}, 3) g.Check(!result.Ok) g.Should(be.StringContaining( - result.Message, + result.Message(), `map[string]int{"a": 1, "b": 2, "c": 3, "d": 4} is length 4, not 3`, )) }) @@ -612,11 +616,11 @@ func TestNil(t *testing.T) { result := be.Nil(v) g.Check(result.Ok) - g.Should(be.Equal(result.Message, "v is nil")) + g.Should(be.Equal(result.Message(), "v is nil")) result = be.Nil(nil) g.Check(result.Ok) - g.Should(be.Equal(result.Message, "nil is nil")) + g.Should(be.Equal(result.Message(), "nil is nil")) }) t.Run("typed nil", func(t *testing.T) { @@ -627,11 +631,11 @@ func TestNil(t *testing.T) { result := be.Nil(i) g.Check(result.Ok) - g.Should(be.Equal(result.Message, "i is nil")) + g.Should(be.Equal(result.Message(), "i is nil")) result = be.Nil((*int)(nil)) g.Check(result.Ok) - g.Should(be.Equal(result.Message, "(*int)(nil) is nil")) + g.Should(be.Equal(result.Message(), "(*int)(nil) is nil")) }) t.Run("non-nil", func(t *testing.T) { @@ -641,11 +645,11 @@ func TestNil(t *testing.T) { result := be.Nil(v) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, "v is 0, not nil")) + g.Should(be.Equal(result.Message(), "v is 0, not nil")) result = be.Nil(-1 + 1) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, "-1 + 1 is 0, not nil")) + g.Should(be.Equal(result.Message(), "-1 + 1 is 0, not nil")) }) } @@ -658,14 +662,14 @@ func TestSliceContaining(t *testing.T) { result := be.SliceContaining(slice, elem) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `slice contains elem + g.Should(be.Equal(result.Message(), `slice contains elem slice: [1 2 3] element: 2 `)) result = be.SliceContaining([]int{1, 2, 3}, 2) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `[]int{1, 2, 3} contains 2 + g.Should(be.Equal(result.Message(), `[]int{1, 2, 3} contains 2 slice: [1 2 3] element: 2 `)) @@ -679,7 +683,7 @@ element: 2 result := be.SliceContaining(slice, elem) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `slice contains elem + g.Should(be.Equal(result.Message(), `slice contains elem slice: [ 1 > 2 @@ -691,7 +695,7 @@ element: 2 result = be.SliceContaining([]int{1, 2, 3, 4}, 2) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `[]int{1, 2, 3, 4} contains 2 + g.Should(be.Equal(result.Message(), `[]int{1, 2, 3, 4} contains 2 slice: [ 1 > 2 @@ -710,14 +714,14 @@ element: 2 result := be.SliceContaining(slice, elem) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `slice does not contain elem + g.Should(be.Equal(result.Message(), `slice does not contain elem slice: [1 2 3] element: 5 `)) result = be.SliceContaining([]int{1, 2, 3}, 5) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `[]int{1, 2, 3} does not contain 5 + g.Should(be.Equal(result.Message(), `[]int{1, 2, 3} does not contain 5 slice: [1 2 3] element: 5 `)) @@ -731,7 +735,7 @@ element: 5 result := be.SliceContaining(slice, elem) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `slice does not contain elem + g.Should(be.Equal(result.Message(), `slice does not contain elem slice: [ 1 2 @@ -743,7 +747,7 @@ element: 5 result = be.SliceContaining([]int{1, 2, 3, 4}, 5) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `[]int{1, 2, 3, 4} does not contain 5 + g.Should(be.Equal(result.Message(), `[]int{1, 2, 3, 4} does not contain 5 slice: [ 1 2 @@ -764,13 +768,13 @@ func TestSliceLen(t *testing.T) { result := be.SliceLen(slice, wantLen) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `slice is length 3 + g.Should(be.Equal(result.Message(), `slice is length 3 slice: [a b c] `)) result = be.SliceLen([]string{"a", "b", "c"}, 3) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `[]string{"a", "b", "c"} is length 3 + g.Should(be.Equal(result.Message(), `[]string{"a", "b", "c"} is length 3 slice: [a b c] `)) }) @@ -783,7 +787,7 @@ slice: [a b c] result := be.SliceLen(slice, wantLen) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `slice is length 4 + g.Should(be.Equal(result.Message(), `slice is length 4 slice: [ a b @@ -794,7 +798,7 @@ slice: [ result = be.SliceLen([]string{"a", "b", "c", "d"}, 4) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `[]string{"a", "b", "c", "d"} is length 4 + g.Should(be.Equal(result.Message(), `[]string{"a", "b", "c", "d"} is length 4 slice: [ a b @@ -812,13 +816,13 @@ slice: [ result := be.SliceLen(slice, wantLen) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `slice is length 3, not 2 + g.Should(be.Equal(result.Message(), `slice is length 3, not 2 slice: [a b c] `)) result = be.SliceLen([]string{"a", "b", "c"}, 2) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `[]string{"a", "b", "c"} is length 3, not 2 + g.Should(be.Equal(result.Message(), `[]string{"a", "b", "c"} is length 3, not 2 slice: [a b c] `)) }) @@ -831,7 +835,7 @@ slice: [a b c] result := be.SliceLen(slice, wantLen) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `slice is length 4, not 3 + g.Should(be.Equal(result.Message(), `slice is length 4, not 3 slice: [ a b @@ -842,7 +846,7 @@ slice: [ result = be.SliceLen([]string{"a", "b", "c", "d"}, 3) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `[]string{"a", "b", "c", "d"} is length 4, not 3 + g.Should(be.Equal(result.Message(), `[]string{"a", "b", "c", "d"} is length 4, not 3 slice: [ a b @@ -862,14 +866,14 @@ func TestStringContaining(t *testing.T) { result := be.StringContaining(outer, inner) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `outer contains inner + g.Should(be.Equal(result.Message(), `outer contains inner str: "foobar" substr: "oob" `)) result = be.StringContaining("foobar", "oob") g.Check(result.Ok) - g.Should(be.Equal(result.Message, `"foobar" contains "oob" + g.Should(be.Equal(result.Message(), `"foobar" contains "oob" str: "foobar" substr: "oob" `)) @@ -883,14 +887,14 @@ substr: "oob" result := be.StringContaining(outer, inner) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `outer does not contain inner + g.Should(be.Equal(result.Message(), `outer does not contain inner str: "foobar" substr: "boo" `)) result = be.StringContaining("foobar", "boo") g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `"foobar" does not contain "boo" + g.Should(be.Equal(result.Message(), `"foobar" does not contain "boo" str: "foobar" substr: "boo" `)) @@ -906,7 +910,7 @@ three result := be.StringContaining(outer, "two") g.Check(result.Ok) - g.Should(be.Equal(result.Message, `outer contains "two" + g.Should(be.Equal(result.Message(), `outer contains "two" str: `+` """ one @@ -928,14 +932,14 @@ func TestStringMatching(t *testing.T) { result := be.StringMatching(str, expr) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `str matches regular expression expr + g.Should(be.Equal(result.Message(), `str matches regular expression expr str: "foobar" expr: ^foo `)) result = be.StringMatching("foobar", "^foo") g.Check(result.Ok) - g.Should(be.Equal(result.Message, `"foobar" matches regular expression "^foo" + g.Should(be.Equal(result.Message(), `"foobar" matches regular expression "^foo" str: "foobar" expr: ^foo `)) @@ -949,14 +953,14 @@ expr: ^foo result := be.StringMatching(str, expr) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `str does not match regular expression expr + g.Should(be.Equal(result.Message(), `str does not match regular expression expr str: "foobar" expr: ^foo$ `)) result = be.StringMatching("foobar", "^foo$") g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `"foobar" does not match regular expression "^foo$" + g.Should(be.Equal(result.Message(), `"foobar" does not match regular expression "^foo$" str: "foobar" expr: ^foo$ `)) @@ -970,12 +974,12 @@ expr: ^foo$ result := be.StringMatching(str, expr) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `expr is not a valid regular expression + g.Should(be.Equal(result.Message(), `expr is not a valid regular expression error parsing regexp: invalid escape sequence: `+"`\\j`\n")) result = be.StringMatching("foobar", "^foo\\j") g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `"^foo\\j" is not a valid regular expression + g.Should(be.Equal(result.Message(), `"^foo\\j" is not a valid regular expression error parsing regexp: invalid escape sequence: `+"`\\j`\n")) }) } @@ -987,11 +991,11 @@ func TestTrue(t *testing.T) { v := true result := be.True(v) g.Check(result.Ok) - g.Should(be.Equal(result.Message, "v is true")) + g.Should(be.Equal(result.Message(), "v is true")) result = be.True(true) g.Check(result.Ok) - g.Should(be.Equal(result.Message, "true is true")) + g.Should(be.Equal(result.Message(), "true is true")) }) t.Run("false", func(t *testing.T) { @@ -1000,11 +1004,11 @@ func TestTrue(t *testing.T) { v := false result := be.True(v) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, "v is false")) + g.Should(be.Equal(result.Message(), "v is false")) result = be.True(false) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, "false is false")) + g.Should(be.Equal(result.Message(), "false is false")) }) } @@ -1015,11 +1019,11 @@ func TestZero(t *testing.T) { var v int result := be.Zero(v) g.Check(result.Ok) - g.Should(be.Equal(result.Message, "v is the zero value")) + g.Should(be.Equal(result.Message(), "v is the zero value")) result = be.Zero(0) g.Check(result.Ok) - g.Should(be.Equal(result.Message, "0 is the zero value")) + g.Should(be.Equal(result.Message(), "0 is the zero value")) }) t.Run("non-zero", func(t *testing.T) { @@ -1028,10 +1032,10 @@ func TestZero(t *testing.T) { v := 1 result := be.Zero(v) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, "v is non-zero\nvalue: 1")) + g.Should(be.Equal(result.Message(), "v is non-zero\nvalue: 1")) result = be.Zero(1) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, "1 is non-zero")) + g.Should(be.Equal(result.Message(), "1 is non-zero")) }) } diff --git a/be/compose.go b/be/compose.go index d33406d..de323ae 100644 --- a/be/compose.go +++ b/be/compose.go @@ -3,7 +3,6 @@ package be import ( "fmt" "regexp" - "strconv" "strings" "time" @@ -13,7 +12,12 @@ import ( // All asserts that every one of the provided assertions is true. func All(results ...ghost.Result) ghost.Result { - args := ghostlib.ArgsFromAST(results) + argsOfAny := make([]any, 0, len(results)) + for _, result := range results { + argsOfAny = append(argsOfAny, result) + } + args := ghostlib.ArgsFromAST(argsOfAny...) + return applyVariadicBooleanLogic( true, func(acc, val bool) bool { @@ -26,7 +30,12 @@ func All(results ...ghost.Result) ghost.Result { // Any asserts that at least one of the provided assertions is true. func Any(results ...ghost.Result) ghost.Result { - args := ghostlib.ArgsFromAST(results) + argsOfAny := make([]any, 0, len(results)) + for _, result := range results { + argsOfAny = append(argsOfAny, result) + } + args := ghostlib.ArgsFromAST(argsOfAny...) + return applyVariadicBooleanLogic( false, func(acc, val bool) bool { @@ -41,40 +50,35 @@ func applyVariadicBooleanLogic( initial bool, apply func(acc, val bool) bool, results []ghost.Result, - args []string, + args ghostlib.Args, ) ghost.Result { if len(results) == 0 { return ghost.Result{ Ok: initial, - Message: "no assertions were provided", + Message: func() string { return "no assertions were provided" }, } } - out := ghost.Result{Ok: initial} - for i, result := range results { - out.Ok = apply(out.Ok, result.Ok) - - // Not sure why AST parsing would fail, but sometimes it does. Seems to be - // environment dependent rather than code dependent. - var arg string - if len(args) > i { - arg = fmt.Sprintf("`%s`", args[i]) - } else { - arg = strconv.Itoa(i) - } - - var b strings.Builder - if i != 0 { - b.WriteString("\n\n") - } - fmt.Fprintf(&b, "assertion %s is %t", arg, result.Ok) - b.WriteString("\n\t") - b.WriteString(indentString(result.Message)) - - out.Message += b.String() + ok := initial + for _, result := range results { + ok = apply(ok, result.Ok) } - return out + return ghost.Result{ + Ok: ok, + Message: func() string { + var b strings.Builder + for i, result := range results { + if i != 0 { + b.WriteString("\n\n") + } + fmt.Fprintf(&b, "assertion `%s` is %t", args.Get(i), result.Ok) + b.WriteString("\n\t") + b.WriteString(indentString(result.Message())) + } + return b.String() + }, + } } var reWhitespaceLine = regexp.MustCompile(`\n[ \t]+\n`) @@ -93,7 +97,6 @@ func Eventually( interval time.Duration, ) ghost.Result { args := ghostlib.ArgsFromAST(f, timeout, interval) - argF := args[0] timer := time.NewTimer(timeout) defer timer.Stop() @@ -102,8 +105,10 @@ func Eventually( defer ticker.Stop() lastRun := ghost.Result{ - Ok: false, - Message: fmt.Sprintf("%s did not return value within %s timeout", argF, timeout), + Ok: false, + Message: func() string { + return fmt.Sprintf("%s did not return value within %s timeout", args.Get(0), timeout) + }, } ch := make(chan ghost.Result, 1) diff --git a/be/compose_test.go b/be/compose_test.go index 9c28b2e..763ab97 100644 --- a/be/compose_test.go +++ b/be/compose_test.go @@ -16,7 +16,7 @@ func TestAll(t *testing.T) { result := be.All() g.Check(result.Ok) - g.Should(be.Equal(result.Message, "no assertions were provided")) + g.Should(be.Equal(result.Message(), "no assertions were provided")) }) t.Run("one valid", func(t *testing.T) { @@ -30,7 +30,7 @@ func TestAll(t *testing.T) { g.Check(!result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), fmt.Sprintf(`assertion %s is false 1 != 0 got: 1 @@ -60,7 +60,7 @@ assertion %s is false g.Check(result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), fmt.Sprintf("assertion %s is true"+` 1 == 1 @@ -84,7 +84,7 @@ assertion %s is true g.Check(!result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), fmt.Sprintf(`assertion %s is false assertion %s is false 1 != 0 @@ -109,7 +109,7 @@ func TestAny(t *testing.T) { result := be.Any() g.Check(!result.Ok) - g.Should(be.Equal(result.Message, "no assertions were provided")) + g.Should(be.Equal(result.Message(), "no assertions were provided")) }) t.Run("one valid", func(t *testing.T) { @@ -123,7 +123,7 @@ func TestAny(t *testing.T) { g.Check(result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), fmt.Sprintf(`assertion %s is false 1 != 0 got: 1 @@ -153,7 +153,7 @@ assertion %s is false g.Check(!result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), fmt.Sprintf("assertion %s is false"+` 1 != 0 got: 1 @@ -181,7 +181,7 @@ assertion %s is false g.Check(!result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), fmt.Sprintf(`assertion %s is false assertion %s is false 1 != 0 @@ -211,7 +211,7 @@ func TestEventually(t *testing.T) { }, 100*time.Millisecond, 5*time.Millisecond) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `count == 3`)) + g.Should(be.Equal(result.Message(), `count == 3`)) }) t.Run("not ok", func(t *testing.T) { @@ -228,7 +228,7 @@ func TestEventually(t *testing.T) { matched, err := regexp.MatchString(`count != -1 got: \d+ want: -1 -`, result.Message) +`, result.Message()) g.NoError(err) g.Check(matched) }) @@ -242,7 +242,7 @@ want: -1 }, 10*time.Millisecond, 100*time.Millisecond) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `func() ghost.Result { + g.Should(be.Equal(result.Message(), `func() ghost.Result { time.Sleep(100 * time.Millisecond) return be.True(true) } did not return value within 10ms timeout`)) @@ -256,13 +256,14 @@ func TestNot(t *testing.T) { result := ghost.Result{ Ok: true, - Message: message, + Message: func() string { return message }, } negated := be.Not(result) g.Check(!negated.Ok) - g.Should(be.Equal(negated.Message, message)) + g.Should(be.Equal(negated.Message(), message)) doubleNegated := be.Not(negated) - g.Should(be.Equal(doubleNegated, result)) + g.Should(be.Equal(doubleNegated.Ok, result.Ok)) + g.Should(be.Equal(doubleNegated.Message(), result.Message())) } diff --git a/be/errors.go b/be/errors.go index 2e2a9ad..f357155 100644 --- a/be/errors.go +++ b/be/errors.go @@ -12,71 +12,78 @@ import ( // Error asserts that an error is non-nil. func Error(err error) ghost.Result { args := ghostlib.ArgsFromAST(err) - argErr := args[0] - if err == nil { return ghost.Result{ - Ok: false, - Message: argErr + " is nil", + Ok: false, + Message: func() string { + return args.Get(0) + " is nil" + }, } } return ghost.Result{ - Ok: true, - Message: fmt.Sprintf("%s has error value: %s", argErr, err), + Ok: true, + Message: func() string { + return fmt.Sprintf("%s has error value: %s", args.Get(0), err) + }, } } // ErrorContaining asserts that an error string contains a particular substring. func ErrorContaining(err error, msg string) ghost.Result { args := ghostlib.ArgsFromAST(err, msg) - argErr, argMsg := args[0], args[1] - switch { - case err == nil && argMsg == fmt.Sprintf("%q", msg): + case err == nil: return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`error %v is nil, does not contain message + Message: func() string { + argErr, argMsg := args.Get(0), args.Get(1) + if argMsg == fmt.Sprintf("%q", msg) { + return fmt.Sprintf(`error %v is nil, does not contain message got: want: %v`, - argErr, - msg, - ), - } - case err == nil: - return ghost.Result{ - Ok: false, - Message: fmt.Sprintf(`error %v is nil, does not contain %v + argErr, + msg, + ) + } + return fmt.Sprintf(`error %v is nil, does not contain %v got: want: %v`, - argErr, - argMsg, - msg, - ), + argErr, + argMsg, + msg, + ) + }, } case strings.Contains(err.Error(), msg): return ghost.Result{ Ok: true, - Message: fmt.Sprintf(`error %v contains message %v + Message: func() string { + argErr, argMsg := args.Get(0), args.Get(1) + return fmt.Sprintf(`error %v contains message %v got: %v want: %v`, - argErr, - argMsg, - err, - msg, - ), + argErr, + argMsg, + err, + msg, + ) + }, } default: return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`error %v does not contain message %v + Message: func() string { + argErr, argMsg := args.Get(0), args.Get(1) + return fmt.Sprintf(`error %v does not contain message %v got: %v want: %v`, - argErr, - argMsg, - err, - msg, - ), + argErr, + argMsg, + err, + msg, + ) + }, } } } @@ -84,119 +91,131 @@ want: %v`, // ErrorEqual asserts that an error string equals a particular message. func ErrorEqual(err error, msg string) ghost.Result { args := ghostlib.ArgsFromAST(err, msg) - argErr, argMsg := args[0], args[1] - if err == nil { return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`error %v is nil + Message: func() string { + return fmt.Sprintf(`error %v is nil got: want: %v`, - argErr, - msg, - ), + args.Get(0), + msg, + ) + }, } } if err.Error() == msg { return ghost.Result{ Ok: true, - Message: fmt.Sprintf(`error %v has message %v + Message: func() string { + return fmt.Sprintf(`error %v has message %v value: %v`, - argErr, - argMsg, - err, - ), + args.Get(0), + args.Get(1), + err, + ) + }, } } return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`error %v does not have message %v + Message: func() string { + return fmt.Sprintf(`error %v does not have message %v got: %v want: %v`, - argErr, - argMsg, - err, - msg, - ), + args.Get(0), + args.Get(1), + err, + msg, + ) + }, } } // ErrorIs asserts that an error matches another using [errors.Is]. func ErrorIs(err error, target error) ghost.Result { args := ghostlib.ArgsFromAST(err, target) - argErr, argTarget := args[0], args[1] - if errors.Is(err, target) { return ghost.Result{ Ok: true, - Message: fmt.Sprintf(`error %v is target %v + Message: func() string { + return fmt.Sprintf(`error %v is target %v error: %v target: %v`, - argErr, - argTarget, - err, - target, - ), + args.Get(0), + args.Get(1), + err, + target, + ) + }, } } return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`error %v is not target %v + Message: func() string { + return fmt.Sprintf(`error %v is not target %v error: %v target: %v`, - argErr, - argTarget, - err, - target, - ), + args.Get(0), + args.Get(1), + err, + target, + ) + }, } } // ErrorAs asserts that an error matches another using [errors.As]. func ErrorAs[T any](err error, target *T) ghost.Result { args := ghostlib.ArgsFromAST(err, target) - argErr, argTarget := args[0], args[1] - if err == nil { return ghost.Result{ - Ok: false, - Message: fmt.Sprintf("error %v was nil", argErr), + Ok: false, + Message: func() string { + return fmt.Sprintf("error %v was nil", args.Get(0)) + }, } } if target == nil { return ghost.Result{ - Ok: false, - Message: fmt.Sprintf("target %v cannot be nil", argTarget), + Ok: false, + Message: func() string { + return fmt.Sprintf("target %v cannot be nil", args.Get(1)) + }, } } if errors.As(err, target) { return ghost.Result{ Ok: true, - Message: fmt.Sprintf(`error %v set as target %v + Message: func() string { + return fmt.Sprintf(`error %v set as target %v error: %v target: %T`, - argErr, - argTarget, - err, - *target, - ), + args.Get(0), + args.Get(1), + err, + *target, + ) + }, } } return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`error %v cannot be set as target %v + Message: func() string { + return fmt.Sprintf(`error %v cannot be set as target %v error: %v target: %T`, - argErr, - argTarget, - err, - *target, - ), + args.Get(0), + args.Get(1), + err, + *target, + ) + }, } } diff --git a/be/errors_test.go b/be/errors_test.go index f18c3e3..7b34e27 100644 --- a/be/errors_test.go +++ b/be/errors_test.go @@ -18,11 +18,11 @@ func TestError(t *testing.T) { result := be.Error(err) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `err has error value: oopsie`)) + g.Should(be.Equal(result.Message(), `err has error value: oopsie`)) result = be.Error(errors.New("oopsie")) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `errors.New("oopsie") has error value: oopsie`)) + g.Should(be.Equal(result.Message(), `errors.New("oopsie") has error value: oopsie`)) }) t.Run("nil", func(t *testing.T) { @@ -32,11 +32,11 @@ func TestError(t *testing.T) { result := be.Error(err) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `err is nil`)) + g.Should(be.Equal(result.Message(), `err is nil`)) result = be.Error(nil) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `nil is nil`)) + g.Should(be.Equal(result.Message(), `nil is nil`)) }) } @@ -50,7 +50,7 @@ func TestErrorContaining(t *testing.T) { result := be.ErrorContaining(err, msg) g.Check(result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `error err contains message msg got: foobar want: oob`, @@ -59,7 +59,7 @@ want: oob`, result = be.ErrorContaining(errors.New("foobar"), "oob") g.Check(result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `error errors.New("foobar") contains message "oob" got: foobar want: oob`, @@ -75,7 +75,7 @@ want: oob`, result := be.ErrorContaining(err, msg) g.Check(!result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `error err does not contain message msg got: foobar want: boo`, @@ -84,7 +84,7 @@ want: boo`, result = be.ErrorContaining(errors.New("foobar"), "boo") g.Check(!result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `error errors.New("foobar") does not contain message "boo" got: foobar want: boo`, @@ -99,13 +99,13 @@ want: boo`, result := be.ErrorContaining(err, msg) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `error err is nil, does not contain msg + g.Should(be.Equal(result.Message(), `error err is nil, does not contain msg got: want: boo`)) result = be.ErrorContaining(nil, "boo") g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `error nil is nil, does not contain message + g.Should(be.Equal(result.Message(), `error nil is nil, does not contain message got: want: boo`)) }) @@ -121,7 +121,7 @@ func TestErrorEqual(t *testing.T) { result := be.ErrorEqual(err, msg) g.Check(result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `error err has message msg value: foobar`, )) @@ -129,7 +129,7 @@ value: foobar`, result = be.ErrorEqual(errors.New("foobar"), "foobar") g.Check(result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `error errors.New("foobar") has message "foobar" value: foobar`, )) @@ -144,7 +144,7 @@ value: foobar`, result := be.ErrorEqual(err, msg) g.Check(!result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `error err does not have message msg got: foobar want: boo`, @@ -153,7 +153,7 @@ want: boo`, result = be.ErrorEqual(errors.New("foobar"), "boo") g.Check(!result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `error errors.New("foobar") does not have message "boo" got: foobar want: boo`, @@ -168,13 +168,13 @@ want: boo`, result := be.ErrorEqual(err, msg) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `error err is nil + g.Should(be.Equal(result.Message(), `error err is nil got: want: boo`)) result = be.ErrorEqual(nil, "boo") g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `error nil is nil + g.Should(be.Equal(result.Message(), `error nil is nil got: want: boo`)) }) @@ -190,7 +190,7 @@ func TestErrorIs(t *testing.T) { result := be.ErrorIs(err, target) g.Check(result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `error err is target target error: wrapping: foobar target: foobar`, @@ -199,7 +199,7 @@ target: foobar`, result = be.ErrorIs(fmt.Errorf("wrapping: %w", target), target) g.Check(result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `error fmt.Errorf("wrapping: %w", target) is target target error: wrapping: foobar target: foobar`, @@ -215,7 +215,7 @@ target: foobar`, result := be.ErrorIs(err, target) g.Check(!result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `error err is not target target error: wrapping: foobar target: foobar`, @@ -224,7 +224,7 @@ target: foobar`, result = be.ErrorIs(fmt.Errorf("wrapping: %v", target), target) //nolint:errorlint // test case g.Check(!result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `error fmt.Errorf("wrapping: %v", target) is not target target error: wrapping: foobar target: foobar`, @@ -239,13 +239,13 @@ target: foobar`, result := be.ErrorIs(err, target) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `error err is target target + g.Should(be.Equal(result.Message(), `error err is target target error: target: `)) result = be.ErrorIs(nil, nil) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `error nil is target nil + g.Should(be.Equal(result.Message(), `error nil is target nil error: target: `)) }) @@ -261,7 +261,7 @@ func TestErrorAs(t *testing.T) { result := be.ErrorAs(err, &target) g.Check(result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `error err set as target &target error: strconv.Atoi: parsing "bad input": invalid syntax target: *strconv.NumError`, @@ -270,7 +270,7 @@ target: *strconv.NumError`, result = be.ErrorAs(fmt.Errorf("wrapping: %w", err), &target) g.Check(result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `error fmt.Errorf("wrapping: %w", err) set as target &target error: wrapping: strconv.Atoi: parsing "bad input": invalid syntax target: *strconv.NumError`, @@ -286,7 +286,7 @@ target: *strconv.NumError`, result := be.ErrorAs(err, &target) g.Check(!result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `error err cannot be set as target &target error: oh no target: *strconv.NumError`, @@ -295,7 +295,7 @@ target: *strconv.NumError`, result = be.ErrorAs(errors.New("oh no"), &target) g.Check(!result.Ok) g.Should(be.Equal( - result.Message, + result.Message(), `error errors.New("oh no") cannot be set as target &target error: oh no target: *strconv.NumError`, @@ -310,11 +310,11 @@ target: *strconv.NumError`, result := be.ErrorAs(err, &target) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `error err was nil`)) + g.Should(be.Equal(result.Message(), `error err was nil`)) result = be.ErrorAs(nil, new(error)) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `error nil was nil`)) + g.Should(be.Equal(result.Message(), `error nil was nil`)) }) t.Run("nil target", func(t *testing.T) { @@ -325,10 +325,10 @@ target: *strconv.NumError`, result := be.ErrorAs(err, target) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `target target cannot be nil`)) + g.Should(be.Equal(result.Message(), `target target cannot be nil`)) result = be.ErrorAs[error](errors.New("oh no"), nil) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `target cannot be nil`)) + g.Should(be.Equal(result.Message(), `target cannot be nil`)) }) } diff --git a/be/ordered.go b/be/ordered.go index d6a4f12..5ca9741 100644 --- a/be/ordered.go +++ b/be/ordered.go @@ -11,56 +11,62 @@ import ( // Greater asserts that the first value provided is strictly greater than the second. func Greater[T constraints.Ordered](a, b T) ghost.Result { args := ghostlib.ArgsFromAST(a, b) - argA, argB := args[0], args[1] - if a > b { return ghost.Result{ Ok: true, - Message: fmt.Sprintf(`%v is greater than %v`, - inline(a, argA), - inline(b, argB), - ), + Message: func() string { + return fmt.Sprintf(`%v is greater than %v`, + inline(a, args.Get(0)), + inline(b, args.Get(1)), + ) + }, } } return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`%v is not greater than %v`, - inline(a, argA), - inline(b, argB), - ), + Message: func() string { + return fmt.Sprintf(`%v is not greater than %v`, + inline(a, args.Get(0)), + inline(b, args.Get(1)), + ) + }, } } // GreaterOrEqual asserts that the first value provided is greater than or equal to the second. func GreaterOrEqual[T constraints.Ordered](a, b T) ghost.Result { args := ghostlib.ArgsFromAST(a, b) - argA, argB := args[0], args[1] - switch { case a > b: return ghost.Result{ Ok: true, - Message: fmt.Sprintf(`%v is greater than %v`, - inline(a, argA), - inline(b, argB), - ), + Message: func() string { + return fmt.Sprintf(`%v is greater than %v`, + inline(a, args.Get(0)), + inline(b, args.Get(1)), + ) + }, } case a == b: return ghost.Result{ Ok: true, - Message: fmt.Sprintf(`%v is equal to %v`, - inline(a, argA), - inline(b, argB), - ), + Message: func() string { + return fmt.Sprintf(`%v is equal to %v`, + inline(a, args.Get(0)), + inline(b, args.Get(1)), + ) + }, } default: return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`%v is not greater than or equal to %v`, - inline(a, argA), - inline(b, argB), - ), + Message: func() string { + return fmt.Sprintf(`%v is not greater than or equal to %v`, + inline(a, args.Get(0)), + inline(b, args.Get(1)), + ) + }, } } } @@ -68,56 +74,62 @@ func GreaterOrEqual[T constraints.Ordered](a, b T) ghost.Result { // Less asserts that the first value provided is strictly less than the second. func Less[T constraints.Ordered](a, b T) ghost.Result { args := ghostlib.ArgsFromAST(a, b) - argA, argB := args[0], args[1] - if a < b { return ghost.Result{ Ok: true, - Message: fmt.Sprintf(`%v is less than %v`, - inline(a, argA), - inline(b, argB), - ), + Message: func() string { + return fmt.Sprintf(`%v is less than %v`, + inline(a, args.Get(0)), + inline(b, args.Get(1)), + ) + }, } } return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`%v is not less than %v`, - inline(a, argA), - inline(b, argB), - ), + Message: func() string { + return fmt.Sprintf(`%v is not less than %v`, + inline(a, args.Get(0)), + inline(b, args.Get(1)), + ) + }, } } // LessOrEqual asserts that the first value provided is less than or equal to the second. func LessOrEqual[T constraints.Ordered](a, b T) ghost.Result { args := ghostlib.ArgsFromAST(a, b) - argA, argB := args[0], args[1] - switch { case a < b: return ghost.Result{ Ok: true, - Message: fmt.Sprintf(`%v is less than %v`, - inline(a, argA), - inline(b, argB), - ), + Message: func() string { + return fmt.Sprintf(`%v is less than %v`, + inline(a, args.Get(0)), + inline(b, args.Get(1)), + ) + }, } case a == b: return ghost.Result{ Ok: true, - Message: fmt.Sprintf(`%v is equal to %v`, - inline(a, argA), - inline(b, argB), - ), + Message: func() string { + return fmt.Sprintf(`%v is equal to %v`, + inline(a, args.Get(0)), + inline(b, args.Get(1)), + ) + }, } default: return ghost.Result{ Ok: false, - Message: fmt.Sprintf(`%v is not less than or equal to %v`, - inline(a, argA), - inline(b, argB), - ), + Message: func() string { + return fmt.Sprintf(`%v is not less than or equal to %v`, + inline(a, args.Get(0)), + inline(b, args.Get(1)), + ) + }, } } } diff --git a/be/ordered_test.go b/be/ordered_test.go index 32cea85..5d1b37f 100644 --- a/be/ordered_test.go +++ b/be/ordered_test.go @@ -16,11 +16,11 @@ func TestGreater(t *testing.T) { result := be.Greater(a, b) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `a (3) is not greater than b (4)`)) + g.Should(be.Equal(result.Message(), `a (3) is not greater than b (4)`)) result = be.Greater(3, 4) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `3 is not greater than 4`)) + g.Should(be.Equal(result.Message(), `3 is not greater than 4`)) }) t.Run("int greater", func(t *testing.T) { @@ -31,11 +31,11 @@ func TestGreater(t *testing.T) { result := be.Greater(a, b) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `a (4) is greater than b (3)`)) + g.Should(be.Equal(result.Message(), `a (4) is greater than b (3)`)) result = be.Greater(4, 3) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `4 is greater than 3`)) + g.Should(be.Equal(result.Message(), `4 is greater than 3`)) }) t.Run("int equal", func(t *testing.T) { @@ -46,11 +46,11 @@ func TestGreater(t *testing.T) { result := be.Greater(a, b) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `a (3) is not greater than b (3)`)) + g.Should(be.Equal(result.Message(), `a (3) is not greater than b (3)`)) result = be.Greater(3, 3) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `3 is not greater than 3`)) + g.Should(be.Equal(result.Message(), `3 is not greater than 3`)) }) t.Run("float less", func(t *testing.T) { @@ -61,11 +61,11 @@ func TestGreater(t *testing.T) { result := be.Greater(a, b) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `a (3.1) is not greater than b (4.1)`)) + g.Should(be.Equal(result.Message(), `a (3.1) is not greater than b (4.1)`)) result = be.Greater(3.1, 4.1) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `3.1 is not greater than 4.1`)) + g.Should(be.Equal(result.Message(), `3.1 is not greater than 4.1`)) }) t.Run("float greater", func(t *testing.T) { @@ -76,11 +76,11 @@ func TestGreater(t *testing.T) { result := be.Greater(a, b) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `a (4.1) is greater than b (3.1)`)) + g.Should(be.Equal(result.Message(), `a (4.1) is greater than b (3.1)`)) result = be.Greater(4.1, 3.1) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `4.1 is greater than 3.1`)) + g.Should(be.Equal(result.Message(), `4.1 is greater than 3.1`)) }) t.Run("float equal", func(t *testing.T) { @@ -91,11 +91,11 @@ func TestGreater(t *testing.T) { result := be.Greater(a, b) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `a (3.1) is not greater than b (3.1)`)) + g.Should(be.Equal(result.Message(), `a (3.1) is not greater than b (3.1)`)) result = be.Greater(3.1, 3.1) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `3.1 is not greater than 3.1`)) + g.Should(be.Equal(result.Message(), `3.1 is not greater than 3.1`)) }) t.Run("string less", func(t *testing.T) { @@ -106,11 +106,11 @@ func TestGreater(t *testing.T) { result := be.Greater(a, b) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `a ("bar") is not greater than b ("foo")`)) + g.Should(be.Equal(result.Message(), `a ("bar") is not greater than b ("foo")`)) result = be.Greater("bar", "foo") g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `"bar" is not greater than "foo"`)) + g.Should(be.Equal(result.Message(), `"bar" is not greater than "foo"`)) }) t.Run("string greater", func(t *testing.T) { @@ -121,11 +121,11 @@ func TestGreater(t *testing.T) { result := be.Greater(a, b) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `a ("foo") is greater than b ("bar")`)) + g.Should(be.Equal(result.Message(), `a ("foo") is greater than b ("bar")`)) result = be.Greater("foo", "bar") g.Check(result.Ok) - g.Should(be.Equal(result.Message, `"foo" is greater than "bar"`)) + g.Should(be.Equal(result.Message(), `"foo" is greater than "bar"`)) }) t.Run("string equal", func(t *testing.T) { @@ -136,11 +136,11 @@ func TestGreater(t *testing.T) { result := be.Greater(a, b) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `a ("foo") is not greater than b ("foo")`)) + g.Should(be.Equal(result.Message(), `a ("foo") is not greater than b ("foo")`)) result = be.Greater("foo", "foo") g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `"foo" is not greater than "foo"`)) + g.Should(be.Equal(result.Message(), `"foo" is not greater than "foo"`)) }) } @@ -153,11 +153,11 @@ func TestGreaterOrEqual(t *testing.T) { result := be.GreaterOrEqual(a, b) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `a (3) is not greater than or equal to b (4)`)) + g.Should(be.Equal(result.Message(), `a (3) is not greater than or equal to b (4)`)) result = be.GreaterOrEqual(3, 4) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `3 is not greater than or equal to 4`)) + g.Should(be.Equal(result.Message(), `3 is not greater than or equal to 4`)) }) t.Run("int greater", func(t *testing.T) { @@ -168,11 +168,11 @@ func TestGreaterOrEqual(t *testing.T) { result := be.GreaterOrEqual(a, b) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `a (4) is greater than b (3)`)) + g.Should(be.Equal(result.Message(), `a (4) is greater than b (3)`)) result = be.GreaterOrEqual(4, 3) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `4 is greater than 3`)) + g.Should(be.Equal(result.Message(), `4 is greater than 3`)) }) t.Run("int equal", func(t *testing.T) { @@ -183,11 +183,11 @@ func TestGreaterOrEqual(t *testing.T) { result := be.GreaterOrEqual(a, b) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `a (3) is equal to b (3)`)) + g.Should(be.Equal(result.Message(), `a (3) is equal to b (3)`)) result = be.GreaterOrEqual(3, 3) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `3 is equal to 3`)) + g.Should(be.Equal(result.Message(), `3 is equal to 3`)) }) t.Run("float less", func(t *testing.T) { @@ -198,11 +198,11 @@ func TestGreaterOrEqual(t *testing.T) { result := be.GreaterOrEqual(a, b) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `a (3.1) is not greater than or equal to b (4.1)`)) + g.Should(be.Equal(result.Message(), `a (3.1) is not greater than or equal to b (4.1)`)) result = be.GreaterOrEqual(3.1, 4.1) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `3.1 is not greater than or equal to 4.1`)) + g.Should(be.Equal(result.Message(), `3.1 is not greater than or equal to 4.1`)) }) t.Run("float greater", func(t *testing.T) { @@ -213,11 +213,11 @@ func TestGreaterOrEqual(t *testing.T) { result := be.GreaterOrEqual(a, b) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `a (4.1) is greater than b (3.1)`)) + g.Should(be.Equal(result.Message(), `a (4.1) is greater than b (3.1)`)) result = be.GreaterOrEqual(4.1, 3.1) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `4.1 is greater than 3.1`)) + g.Should(be.Equal(result.Message(), `4.1 is greater than 3.1`)) }) t.Run("float equal", func(t *testing.T) { @@ -228,11 +228,11 @@ func TestGreaterOrEqual(t *testing.T) { result := be.GreaterOrEqual(a, b) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `a (3.1) is equal to b (3.1)`)) + g.Should(be.Equal(result.Message(), `a (3.1) is equal to b (3.1)`)) result = be.GreaterOrEqual(3.1, 3.1) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `3.1 is equal to 3.1`)) + g.Should(be.Equal(result.Message(), `3.1 is equal to 3.1`)) }) t.Run("string less", func(t *testing.T) { @@ -243,11 +243,11 @@ func TestGreaterOrEqual(t *testing.T) { result := be.GreaterOrEqual(a, b) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `a ("bar") is not greater than or equal to b ("foo")`)) + g.Should(be.Equal(result.Message(), `a ("bar") is not greater than or equal to b ("foo")`)) result = be.GreaterOrEqual("bar", "foo") g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `"bar" is not greater than or equal to "foo"`)) + g.Should(be.Equal(result.Message(), `"bar" is not greater than or equal to "foo"`)) }) t.Run("string greater", func(t *testing.T) { @@ -258,11 +258,11 @@ func TestGreaterOrEqual(t *testing.T) { result := be.GreaterOrEqual(a, b) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `a ("foo") is greater than b ("bar")`)) + g.Should(be.Equal(result.Message(), `a ("foo") is greater than b ("bar")`)) result = be.GreaterOrEqual("foo", "bar") g.Check(result.Ok) - g.Should(be.Equal(result.Message, `"foo" is greater than "bar"`)) + g.Should(be.Equal(result.Message(), `"foo" is greater than "bar"`)) }) t.Run("string equal", func(t *testing.T) { @@ -273,11 +273,11 @@ func TestGreaterOrEqual(t *testing.T) { result := be.GreaterOrEqual(a, b) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `a ("foo") is equal to b ("foo")`)) + g.Should(be.Equal(result.Message(), `a ("foo") is equal to b ("foo")`)) result = be.GreaterOrEqual("foo", "foo") g.Check(result.Ok) - g.Should(be.Equal(result.Message, `"foo" is equal to "foo"`)) + g.Should(be.Equal(result.Message(), `"foo" is equal to "foo"`)) }) } @@ -290,11 +290,11 @@ func TestLess(t *testing.T) { result := be.Less(a, b) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `a (3) is less than b (4)`)) + g.Should(be.Equal(result.Message(), `a (3) is less than b (4)`)) result = be.Less(3, 4) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `3 is less than 4`)) + g.Should(be.Equal(result.Message(), `3 is less than 4`)) }) t.Run("int greater", func(t *testing.T) { @@ -305,11 +305,11 @@ func TestLess(t *testing.T) { result := be.Less(a, b) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `a (4) is not less than b (3)`)) + g.Should(be.Equal(result.Message(), `a (4) is not less than b (3)`)) result = be.Less(4, 3) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `4 is not less than 3`)) + g.Should(be.Equal(result.Message(), `4 is not less than 3`)) }) t.Run("int equal", func(t *testing.T) { @@ -320,11 +320,11 @@ func TestLess(t *testing.T) { result := be.Less(a, b) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `a (3) is not less than b (3)`)) + g.Should(be.Equal(result.Message(), `a (3) is not less than b (3)`)) result = be.Less(3, 3) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `3 is not less than 3`)) + g.Should(be.Equal(result.Message(), `3 is not less than 3`)) }) t.Run("float less", func(t *testing.T) { @@ -335,11 +335,11 @@ func TestLess(t *testing.T) { result := be.Less(a, b) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `a (3.1) is less than b (4.1)`)) + g.Should(be.Equal(result.Message(), `a (3.1) is less than b (4.1)`)) result = be.Less(3.1, 4.1) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `3.1 is less than 4.1`)) + g.Should(be.Equal(result.Message(), `3.1 is less than 4.1`)) }) t.Run("float greater", func(t *testing.T) { @@ -350,11 +350,11 @@ func TestLess(t *testing.T) { result := be.Less(a, b) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `a (4.1) is not less than b (3.1)`)) + g.Should(be.Equal(result.Message(), `a (4.1) is not less than b (3.1)`)) result = be.Less(4.1, 3.1) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `4.1 is not less than 3.1`)) + g.Should(be.Equal(result.Message(), `4.1 is not less than 3.1`)) }) t.Run("float equal", func(t *testing.T) { @@ -365,11 +365,11 @@ func TestLess(t *testing.T) { result := be.Less(a, b) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `a (3.1) is not less than b (3.1)`)) + g.Should(be.Equal(result.Message(), `a (3.1) is not less than b (3.1)`)) result = be.Less(3.1, 3.1) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `3.1 is not less than 3.1`)) + g.Should(be.Equal(result.Message(), `3.1 is not less than 3.1`)) }) t.Run("string less", func(t *testing.T) { @@ -380,11 +380,11 @@ func TestLess(t *testing.T) { result := be.Less(a, b) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `a ("bar") is less than b ("foo")`)) + g.Should(be.Equal(result.Message(), `a ("bar") is less than b ("foo")`)) result = be.Less("bar", "foo") g.Check(result.Ok) - g.Should(be.Equal(result.Message, `"bar" is less than "foo"`)) + g.Should(be.Equal(result.Message(), `"bar" is less than "foo"`)) }) t.Run("string greater", func(t *testing.T) { @@ -395,11 +395,11 @@ func TestLess(t *testing.T) { result := be.Less(a, b) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `a ("foo") is not less than b ("bar")`)) + g.Should(be.Equal(result.Message(), `a ("foo") is not less than b ("bar")`)) result = be.Less("foo", "bar") g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `"foo" is not less than "bar"`)) + g.Should(be.Equal(result.Message(), `"foo" is not less than "bar"`)) }) t.Run("string equal", func(t *testing.T) { @@ -410,11 +410,11 @@ func TestLess(t *testing.T) { result := be.Less(a, b) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `a ("foo") is not less than b ("foo")`)) + g.Should(be.Equal(result.Message(), `a ("foo") is not less than b ("foo")`)) result = be.Less("foo", "foo") g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `"foo" is not less than "foo"`)) + g.Should(be.Equal(result.Message(), `"foo" is not less than "foo"`)) }) } @@ -427,11 +427,11 @@ func TestLessOrEqual(t *testing.T) { result := be.LessOrEqual(a, b) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `a (3) is less than b (4)`)) + g.Should(be.Equal(result.Message(), `a (3) is less than b (4)`)) result = be.LessOrEqual(3, 4) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `3 is less than 4`)) + g.Should(be.Equal(result.Message(), `3 is less than 4`)) }) t.Run("int greater", func(t *testing.T) { @@ -442,11 +442,11 @@ func TestLessOrEqual(t *testing.T) { result := be.LessOrEqual(a, b) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `a (4) is not less than or equal to b (3)`)) + g.Should(be.Equal(result.Message(), `a (4) is not less than or equal to b (3)`)) result = be.LessOrEqual(4, 3) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `4 is not less than or equal to 3`)) + g.Should(be.Equal(result.Message(), `4 is not less than or equal to 3`)) }) t.Run("int equal", func(t *testing.T) { @@ -457,11 +457,11 @@ func TestLessOrEqual(t *testing.T) { result := be.LessOrEqual(a, b) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `a (3) is equal to b (3)`)) + g.Should(be.Equal(result.Message(), `a (3) is equal to b (3)`)) result = be.LessOrEqual(3, 3) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `3 is equal to 3`)) + g.Should(be.Equal(result.Message(), `3 is equal to 3`)) }) t.Run("float less", func(t *testing.T) { @@ -472,11 +472,11 @@ func TestLessOrEqual(t *testing.T) { result := be.LessOrEqual(a, b) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `a (3.1) is less than b (4.1)`)) + g.Should(be.Equal(result.Message(), `a (3.1) is less than b (4.1)`)) result = be.LessOrEqual(3.1, 4.1) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `3.1 is less than 4.1`)) + g.Should(be.Equal(result.Message(), `3.1 is less than 4.1`)) }) t.Run("float greater", func(t *testing.T) { @@ -487,11 +487,11 @@ func TestLessOrEqual(t *testing.T) { result := be.LessOrEqual(a, b) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `a (4.1) is not less than or equal to b (3.1)`)) + g.Should(be.Equal(result.Message(), `a (4.1) is not less than or equal to b (3.1)`)) result = be.LessOrEqual(4.1, 3.1) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `4.1 is not less than or equal to 3.1`)) + g.Should(be.Equal(result.Message(), `4.1 is not less than or equal to 3.1`)) }) t.Run("float equal", func(t *testing.T) { @@ -502,11 +502,11 @@ func TestLessOrEqual(t *testing.T) { result := be.LessOrEqual(a, b) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `a (3.1) is equal to b (3.1)`)) + g.Should(be.Equal(result.Message(), `a (3.1) is equal to b (3.1)`)) result = be.LessOrEqual(3.1, 3.1) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `3.1 is equal to 3.1`)) + g.Should(be.Equal(result.Message(), `3.1 is equal to 3.1`)) }) t.Run("string less", func(t *testing.T) { @@ -517,11 +517,11 @@ func TestLessOrEqual(t *testing.T) { result := be.LessOrEqual(a, b) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `a ("bar") is less than b ("foo")`)) + g.Should(be.Equal(result.Message(), `a ("bar") is less than b ("foo")`)) result = be.LessOrEqual("bar", "foo") g.Check(result.Ok) - g.Should(be.Equal(result.Message, `"bar" is less than "foo"`)) + g.Should(be.Equal(result.Message(), `"bar" is less than "foo"`)) }) t.Run("string greater", func(t *testing.T) { @@ -532,11 +532,11 @@ func TestLessOrEqual(t *testing.T) { result := be.LessOrEqual(a, b) g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `a ("foo") is not less than or equal to b ("bar")`)) + g.Should(be.Equal(result.Message(), `a ("foo") is not less than or equal to b ("bar")`)) result = be.LessOrEqual("foo", "bar") g.Check(!result.Ok) - g.Should(be.Equal(result.Message, `"foo" is not less than or equal to "bar"`)) + g.Should(be.Equal(result.Message(), `"foo" is not less than or equal to "bar"`)) }) t.Run("string equal", func(t *testing.T) { @@ -547,10 +547,10 @@ func TestLessOrEqual(t *testing.T) { result := be.LessOrEqual(a, b) g.Check(result.Ok) - g.Should(be.Equal(result.Message, `a ("foo") is equal to b ("foo")`)) + g.Should(be.Equal(result.Message(), `a ("foo") is equal to b ("foo")`)) result = be.LessOrEqual("foo", "foo") g.Check(result.Ok) - g.Should(be.Equal(result.Message, `"foo" is equal to "foo"`)) + g.Should(be.Equal(result.Message(), `"foo" is equal to "foo"`)) }) } diff --git a/ghost.go b/ghost.go index 5c3bfdb..f657c11 100644 --- a/ghost.go +++ b/ghost.go @@ -34,7 +34,7 @@ func (g Ghost) Should(result Result) bool { } if !result.Ok { - g.t.Log(result.Message) + g.t.Log(result.Message()) g.t.Fail() return false } @@ -51,7 +51,7 @@ func (g Ghost) ShouldNot(result Result) bool { } if result.Ok { - g.t.Log(result.Message) + g.t.Log(result.Message()) g.t.Fail() return false } @@ -93,7 +93,7 @@ func (g Ghost) Check(v bool) bool { args := ghostlib.ArgsFromAST(v) if !v { - g.t.Log(fmt.Sprintf("%s is %t", args[0], v)) + g.t.Log(fmt.Sprintf("%s is %t", args.Get(0), v)) g.t.Fail() return false } @@ -111,7 +111,7 @@ func (g Ghost) Assert(v bool) { args := ghostlib.ArgsFromAST(v) if !v { - g.t.Log(fmt.Sprintf("%s is %t", args[0], v)) + g.t.Log(fmt.Sprintf("%s is %t", args.Get(0), v)) g.t.FailNow() } } @@ -125,7 +125,7 @@ func (g Ghost) NoError(err error) { args := ghostlib.ArgsFromAST(err) if err != nil { - g.t.Log(fmt.Sprintf("%s has error value: %s", args[0], err)) + g.t.Log(fmt.Sprintf("%s has error value: %s", args.Get(0), err)) g.t.FailNow() } } @@ -139,5 +139,5 @@ type Result struct { // // A message should be present regardless of whether or not the assertion was // successful. - Message string + Message func() string } diff --git a/ghost_test.go b/ghost_test.go index 1205bbe..1df2ce7 100644 --- a/ghost_test.go +++ b/ghost_test.go @@ -19,7 +19,7 @@ func TestGhost_Should(t *testing.T) { ok := testG.Should(ghost.Result{ Ok: true, - Message: msg, + Message: func() string { return msg }, }) g.Should(be.True(ok)) @@ -37,7 +37,7 @@ func TestGhost_Should(t *testing.T) { ok := testG.Should(ghost.Result{ Ok: false, - Message: msg, + Message: func() string { return msg }, }) g.Should(be.False(ok)) @@ -58,7 +58,7 @@ func TestGhost_ShouldNot(t *testing.T) { ok := testG.ShouldNot(ghost.Result{ Ok: true, - Message: msg, + Message: func() string { return msg }, }) g.Should(be.False(ok)) @@ -77,7 +77,7 @@ func TestGhost_ShouldNot(t *testing.T) { ok := testG.ShouldNot(ghost.Result{ Ok: false, - Message: msg, + Message: func() string { return msg }, }) g.Should(be.True(ok)) @@ -97,7 +97,7 @@ func TestGhost_Must(t *testing.T) { testG.Must(ghost.Result{ Ok: true, - Message: msg, + Message: func() string { return msg }, }) g.Should(be.SliceLen(mockT.logCalls, 0)) @@ -114,7 +114,7 @@ func TestGhost_Must(t *testing.T) { testG.Must(ghost.Result{ Ok: false, - Message: msg, + Message: func() string { return msg }, }) g.Should(be.DeepEqual(mockT.logCalls, [][]any{{msg}})) @@ -132,7 +132,7 @@ func TestGhost_MustNot(t *testing.T) { testG.MustNot(ghost.Result{ Ok: true, - Message: msg, + Message: func() string { return msg }, }) g.Should(be.DeepEqual(mockT.logCalls, [][]any{{msg}})) @@ -148,7 +148,7 @@ func TestGhost_MustNot(t *testing.T) { testG.MustNot(ghost.Result{ Ok: false, - Message: msg, + Message: func() string { return msg }, }) g.Should(be.SliceLen(mockT.logCalls, 0)) diff --git a/ghostlib/ast.go b/ghostlib/ast.go index 6d81b70..da0762b 100644 --- a/ghostlib/ast.go +++ b/ghostlib/ast.go @@ -2,7 +2,6 @@ package ghostlib import ( "bytes" - "errors" "fmt" "go/ast" "go/format" @@ -11,25 +10,55 @@ import ( "os" "runtime" "strings" + "sync" ) +// Args captures the call site information for deferred AST lookup. +type Args struct { + once func() []string +} + +// Get the string representation of the argument in the specified position. +// Panics on out-of-bounds lookups. Safe for concurrent use. +func (a Args) Get(index int) string { + return a.once()[index] +} + // ArgsFromAST gets the string representation of the caller's arguments from // the AST. To handle situations where this cannot be done reliably, the raw // arguments should be passed so their values can be used as a backup. -func ArgsFromAST(unformatted ...any) []string { - return argsFromASTSkip(1, unformatted...) +// +// This function must be called directly from an assertion function to ensure +// that the intended arguments are captured. +func ArgsFromAST(unformatted ...any) Args { + pc, _, _, _ := runtime.Caller(1) + _, file, line, _ := runtime.Caller(2) + return Args{ + once: sync.OnceValue(func() []string { + return argsFromAST(pc, findSystemFilepath(file), line, unformatted...) + }), + } } -// argsFromASTSkip gets the string representation of the caller's arguments -// from the AST, skipping the number specified. -func argsFromASTSkip(skip int, unformatted ...any) []string { - args, err := callExprArgs(2 + skip) +func argsFromAST(pc uintptr, filename string, line int, unformatted ...any) []string { + wantFunc := runtime.FuncForPC(pc) + if wantFunc == nil { + return mapString(unformatted) + } + + fset := token.NewFileSet() + astFile, err := parser.ParseFile(fset, filename, nil, parser.AllErrors) if err != nil { return mapString(unformatted) } - out := make([]string, 0, len(args)) - for _, arg := range args { + node := callExprForFunc(wantFunc, fset, astFile, line) + if node == nil { + return mapString(unformatted) + } + + out := make([]string, 0, len(node.Args)) + for _, arg := range node.Args { out = append(out, nodeToString(arg)) } @@ -44,35 +73,6 @@ func mapString(s []any) []string { return out } -func callExprArgs(skip int) ([]ast.Expr, error) { - pc, _, _, ok := runtime.Caller(skip) - if !ok { - return nil, errors.New("failed to get file/line") - } - - _, filename, line, ok := runtime.Caller(skip + 1) - if !ok { - return nil, errors.New("failed to get file/line") - } - - filename = findSystemFilepath(filename) - - wantFunc := runtime.FuncForPC(pc) - - fset := token.NewFileSet() - astFile, err := parser.ParseFile(fset, filename, nil, parser.AllErrors) - if err != nil { - return nil, err - } - - node := callExprForFunc(wantFunc, fset, astFile, line) - if node == nil { - return nil, errors.New("no node found at line") - } - - return node.Args, nil -} - // Passing the -trimpath flag will prevent looking up filepaths directly. // In most cases, some suffix of the path will be a valid relative path, which // we can use instead. diff --git a/go.mod b/go.mod index 0c8c6ae..382f71f 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,7 @@ module github.com/rliebz/ghost -go 1.18 +go 1.25 + +toolchain go1.26.2 require github.com/google/go-cmp v0.5.9 From 828e4617ba65c08f4eff5f10064a177205711b0c Mon Sep 17 00:00:00 2001 From: Robert Liebowitz Date: Sun, 3 May 2026 21:04:24 -0400 Subject: [PATCH 2/3] Cache AST files to avoid re-parsing This moves to a single `FileSet` and one call to `ParseFile` per file. While this does add what should be a pretty small bit of overhead in terms of global state, what this accomplishes is that if more than one assertion needs to print information from the same file, we don't parse the file from disk over and over again. --- ghostlib/ast.go | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/ghostlib/ast.go b/ghostlib/ast.go index da0762b..50f04d7 100644 --- a/ghostlib/ast.go +++ b/ghostlib/ast.go @@ -40,14 +40,15 @@ func ArgsFromAST(unformatted ...any) Args { } } +var fset = token.NewFileSet() + func argsFromAST(pc uintptr, filename string, line int, unformatted ...any) []string { wantFunc := runtime.FuncForPC(pc) if wantFunc == nil { return mapString(unformatted) } - fset := token.NewFileSet() - astFile, err := parser.ParseFile(fset, filename, nil, parser.AllErrors) + astFile, err := parseFile(filename) if err != nil { return mapString(unformatted) } @@ -65,6 +66,33 @@ func argsFromAST(pc uintptr, filename string, line int, unformatted ...any) []st return out } +var ( + fileCache = map[string]*ast.File{} + fileMu sync.RWMutex +) + +func parseFile(filename string) (*ast.File, error) { + fileMu.RLock() + f, ok := fileCache[filename] + fileMu.RUnlock() + if ok { + return f, nil + } + + fileMu.Lock() + defer fileMu.Unlock() + if f, ok = fileCache[filename]; ok { + return f, nil + } + + f, err := parser.ParseFile(fset, filename, nil, parser.AllErrors) + if err != nil { + return nil, err + } + fileCache[filename] = f + return f, nil +} + func mapString(s []any) []string { out := make([]string, 0, len(s)) for _, ss := range s { From 6d2b5bf466dbface1b98bc7ba222ce9c197fb9ae Mon Sep 17 00:00:00 2001 From: Robert Liebowitz Date: Mon, 4 May 2026 10:02:38 -0400 Subject: [PATCH 3/3] Bump go and golangci-lint versions in CI --- .github/workflows/test.yml | 9 ++++----- tusk.yml | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7deea6e..891b206 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,9 +18,8 @@ jobs: - ubuntu-latest - windows-latest go-version: - - "1.22" - - "1.23" - - "1.24" + - "1.25" + - "1.26" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -43,8 +42,8 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.24" + go-version: "1.26" - name: Lint uses: golangci/golangci-lint-action@v7 with: - version: v2.0.2 + version: v2.12.1 diff --git a/tusk.yml b/tusk.yml index 119bb7d..caa34c6 100644 --- a/tusk.yml +++ b/tusk.yml @@ -3,7 +3,7 @@ tasks: usage: Clean up and format the code run: - go mod tidy - - go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.0.2 fmt + - go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.1 fmt lint: usage: Run static analysis @@ -13,7 +13,7 @@ tasks: short: f type: boolean rewrite: --fix - run: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.0.2 run ${fix} + run: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.1 run ${fix} test: usage: Run unit tests