Skip to content

Commit

Permalink
Include line comments in failure messages (#39)
Browse files Browse the repository at this point in the history
* Include line comments in failure messages

* Tweak README
  • Loading branch information
FollowTheProcess authored Aug 27, 2024
1 parent c714d91 commit 2195bb2
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 36 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ func TestSomething(t *testing.T) {
}
```

> [!TIP]
> Line comments on the line you call most `test` functions on will be shown in failure messages as additional context
### Non Comparable Types

`test` uses Go 1.18+ generics under the hood for most of the comparison, which is great, but what if your types don't satisfy `comparable`. We also provide
Expand Down
139 changes: 105 additions & 34 deletions test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
package test

import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"math"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"sync"
"testing"
Expand All @@ -23,31 +26,41 @@ const floatEqualityThreshold = 1e-8

// failure represents a test failure, including context and reason.
type failure[T any] struct {
got T // What we got
want T // Expected value
title string // Title of the failure, used as a header
reason string // Optional reason for additional context
got T // What we got
want T // Expected value
title string // Title of the failure, used as a header
reason string // Optional reason for additional context
comment string // Optional line comment for context
}

// String prints a failure.
func (f failure[T]) String() string {
if f.reason != "" {
return fmt.Sprintf(
"\n%s\n%s\nGot:\t%+v\nWanted:\t%+v\n\n%s\n",
var msg string
if f.comment != "" {
msg = fmt.Sprintf(
"\n%s // %s\n%s\nGot:\t%+v\nWanted:\t%+v\n",
f.title,
f.comment,
strings.Repeat("-", len(f.title)),
f.got,
f.want,
)
} else {
msg = fmt.Sprintf(
"\n%s\n%s\nGot:\t%+v\nWanted:\t%+v\n",
f.title,
strings.Repeat("-", len(f.title)),
f.got,
f.want,
f.reason,
)
}
return fmt.Sprintf(
"\n%s\n%s\nGot:\t%+v\nWanted:\t%+v\n",
f.title,
strings.Repeat("-", len(f.title)),
f.got,
f.want,
)

if f.reason != "" {
// Bolt the reason on the end
msg = fmt.Sprintf("%s\n%s\n", msg, f.reason)
}

return msg
}

// Equal fails if got != want.
Expand All @@ -58,9 +71,10 @@ func Equal[T comparable](tb testing.TB, got, want T) {
tb.Helper()
if got != want {
fail := failure[T]{
got: got,
want: want,
title: "Not Equal",
got: got,
want: want,
title: "Not Equal",
comment: getComment(),
}
tb.Fatal(fail.String())
}
Expand All @@ -85,6 +99,7 @@ func NearlyEqual[T ~float32 | ~float64](tb testing.TB, got, want T) {
diff,
floatEqualityThreshold,
),
comment: getComment(),
}
tb.Fatal(fail.String())
}
Expand All @@ -98,10 +113,11 @@ func EqualFunc[T any](tb testing.TB, got, want T, equal func(a, b T) bool) {
tb.Helper()
if !equal(got, want) {
fail := failure[T]{
got: got,
want: want,
title: "Not Equal",
reason: "equal(got, want) returned false",
got: got,
want: want,
title: "Not Equal",
reason: "equal(got, want) returned false",
comment: getComment(),
}
tb.Fatal(fail.String())
}
Expand Down Expand Up @@ -136,7 +152,13 @@ func NotEqualFunc[T any](tb testing.TB, got, want T, equal func(a, b T) bool) {
func Ok(tb testing.TB, err error) {
tb.Helper()
if err != nil {
tb.Fatalf("\nNot Ok\n------\nGot error:\t%v\n", err)
fail := failure[error]{
got: err,
want: nil,
title: "Not Ok",
comment: getComment(),
}
tb.Fatal(fail.String())
}
}

Expand All @@ -147,7 +169,13 @@ func Ok(tb testing.TB, err error) {
func Err(tb testing.TB, err error) {
tb.Helper()
if err == nil {
tb.Fatalf("\nNot Err\n-------\nError was nil\n")
fail := failure[error]{
got: nil,
want: errors.New("error"),
title: "Not Err",
comment: getComment(),
}
tb.Fatal(fail.String())
}
}

Expand Down Expand Up @@ -176,9 +204,10 @@ func True(tb testing.TB, v bool) {
tb.Helper()
if !v {
fail := failure[bool]{
got: v,
want: true,
title: "Not True",
got: v,
want: true,
title: "Not True",
comment: getComment(),
}
tb.Fatal(fail.String())
}
Expand All @@ -192,9 +221,10 @@ func False(tb testing.TB, v bool) {
tb.Helper()
if v {
fail := failure[bool]{
got: v,
want: false,
title: "Not False",
got: v,
want: false,
title: "Not False",
comment: getComment(),
}
tb.Fatal(fail.String())
}
Expand All @@ -214,10 +244,11 @@ func DeepEqual(tb testing.TB, got, want any) {
tb.Helper()
if !reflect.DeepEqual(got, want) {
fail := failure[any]{
got: got,
want: want,
title: "Not Equal",
reason: "reflect.DeepEqual(got, want) returned false",
got: got,
want: want,
title: "Not Equal",
reason: "reflect.DeepEqual(got, want) returned false",
comment: getComment(),
}
tb.Fatal(fail.String())
}
Expand Down Expand Up @@ -362,3 +393,43 @@ func CaptureOutput(tb testing.TB, fn func() error) (stdout, stderr string) {

return capturedStdout, capturedStderr
}

// getComment loads a Go line comment from a line where a test function has been called.
//
// If any error happens or there is no comment, an empty string is returned so as not
// to influence the test with an unrelated error.
func getComment() string {
skip := 2 // Skip 2 frames, one for this function, the other for the calling test function
_, file, line, ok := runtime.Caller(skip)
if !ok {
return ""
}

f, err := os.Open(file)
if err != nil {
return ""
}
defer f.Close()

currentLine := 1 // Line numbers in source files start from 1
scanner := bufio.NewScanner(f)
for scanner.Scan() {
// Skip through until we get to the line returned from runtime.Caller
if currentLine != line {
currentLine++
continue
}

_, comment, ok := strings.Cut(scanner.Text(), "//")
if !ok {
// There was no comment on this line
return ""
}

// Now comment will be everything from the "//" until the end of the line
return strings.TrimSpace(comment)
}

// Didn't find one
return ""
}
82 changes: 80 additions & 2 deletions test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ func TestPassFail(t *testing.T) {
wantFail: true,
wantOut: "\nNot Equal\n---------\nGot:\tapples\nWanted:\toranges\n",
},
{
name: "equal string fail with comment",
testFunc: func(tb testing.TB) {
tb.Helper()
test.Equal(tb, "apples", "oranges") // apples are not oranges
},
wantFail: true,
wantOut: "\nNot Equal // apples are not oranges\n---------\nGot:\tapples\nWanted:\toranges\n",
},
{
name: "equal int pass",
testFunc: func(tb testing.TB) {
Expand Down Expand Up @@ -94,6 +103,15 @@ func TestPassFail(t *testing.T) {
wantFail: true,
wantOut: "\nNot NearlyEqual\n---------------\nGot:\t3.0000001\nWanted:\t3\n\nDifference 9.999999983634211e-08 exceeds maximum tolerance of 1e-08\n",
},
{
name: "nearly equal fail with comment",
testFunc: func(tb testing.TB) {
tb.Helper()
test.NearlyEqual(tb, 3.0000001, 3.0) // Ooof so close
},
wantFail: true,
wantOut: "\nNot NearlyEqual // Ooof so close\n---------------\nGot:\t3.0000001\nWanted:\t3\n\nDifference 9.999999983634211e-08 exceeds maximum tolerance of 1e-08\n",
},
{
name: "not equal string pass",
testFunc: func(tb testing.TB) {
Expand Down Expand Up @@ -146,7 +164,16 @@ func TestPassFail(t *testing.T) {
test.Ok(tb, errors.New("uh oh"))
},
wantFail: true,
wantOut: "\nNot Ok\n------\nGot error:\tuh oh\n",
wantOut: "\nNot Ok\n------\nGot:\tuh oh\nWanted:\t<nil>\n",
},
{
name: "ok fail with comment",
testFunc: func(tb testing.TB) {
tb.Helper()
test.Ok(tb, errors.New("uh oh")) // Calling some function
},
wantFail: true,
wantOut: "\nNot Ok // Calling some function\n------\nGot:\tuh oh\nWanted:\t<nil>\n",
},
{
name: "err pass",
Expand All @@ -164,7 +191,16 @@ func TestPassFail(t *testing.T) {
test.Err(tb, nil)
},
wantFail: true,
wantOut: "\nNot Err\n-------\nError was nil\n",
wantOut: "\nNot Err\n-------\nGot:\t<nil>\nWanted:\terror\n",
},
{
name: "err fail with comment",
testFunc: func(tb testing.TB) {
tb.Helper()
test.Err(tb, nil) // Should have failed
},
wantFail: true,
wantOut: "\nNot Err // Should have failed\n-------\nGot:\t<nil>\nWanted:\terror\n",
},
{
name: "true pass",
Expand All @@ -184,6 +220,15 @@ func TestPassFail(t *testing.T) {
wantFail: true,
wantOut: "\nNot True\n--------\nGot:\tfalse\nWanted:\ttrue\n",
},
{
name: "true fail with comment",
testFunc: func(tb testing.TB) {
tb.Helper()
test.True(tb, false) // Comment here
},
wantFail: true,
wantOut: "\nNot True // Comment here\n--------\nGot:\tfalse\nWanted:\ttrue\n",
},
{
name: "false pass",
testFunc: func(tb testing.TB) {
Expand All @@ -202,6 +247,15 @@ func TestPassFail(t *testing.T) {
wantFail: true,
wantOut: "\nNot False\n---------\nGot:\ttrue\nWanted:\tfalse\n",
},
{
name: "false fail with comment",
testFunc: func(tb testing.TB) {
tb.Helper()
test.False(tb, true) // Should always be false
},
wantFail: true,
wantOut: "\nNot False // Should always be false\n---------\nGot:\ttrue\nWanted:\tfalse\n",
},
{
name: "equal func pass",
testFunc: func(tb testing.TB) {
Expand All @@ -226,6 +280,18 @@ func TestPassFail(t *testing.T) {
wantFail: true,
wantOut: "\nNot Equal\n---------\nGot:\tword\nWanted:\tword\n\nequal(got, want) returned false\n",
},
{
name: "equal func fail with comment",
testFunc: func(tb testing.TB) {
tb.Helper()
rubbishEqual := func(a, b string) bool {
return false // Never equal
}
test.EqualFunc(tb, "word", "word", rubbishEqual) // Uh oh
},
wantFail: true,
wantOut: "\nNot Equal // Uh oh\n---------\nGot:\tword\nWanted:\tword\n\nequal(got, want) returned false\n",
},
{
name: "not equal func pass",
testFunc: func(tb testing.TB) {
Expand Down Expand Up @@ -274,6 +340,18 @@ func TestPassFail(t *testing.T) {
wantFail: true,
wantOut: "\nNot Equal\n---------\nGot:\t[a b c]\nWanted:\t[d e f]\n\nreflect.DeepEqual(got, want) returned false\n",
},
{
name: "deep equal fail with comment",
testFunc: func(tb testing.TB) {
tb.Helper()
a := []string{"a", "b", "c"}
b := []string{"d", "e", "f"}

test.DeepEqual(tb, a, b) // Oh no!
},
wantFail: true,
wantOut: "\nNot Equal // Oh no!\n---------\nGot:\t[a b c]\nWanted:\t[d e f]\n\nreflect.DeepEqual(got, want) returned false\n",
},
{
name: "want err pass when got and wanted",
testFunc: func(tb testing.TB) {
Expand Down

0 comments on commit 2195bb2

Please sign in to comment.