Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
assert: first draft of inline golden variables
  • Loading branch information
dnephin committed May 29, 2022
commit a0e2cd3809c1c70b8bc819453eca5e8d40197e48
64 changes: 64 additions & 0 deletions assert/assert_ext_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package assert_test

import (
"fmt"
"testing"

"gotest.tools/v3/assert"
"gotest.tools/v3/internal/source"
)

func TestEqual_WithGoldenUpdate(t *testing.T) {
t.Run("assert failed with update=false", func(t *testing.T) {
ft := &fakeTestingT{}
actual := `not this value`
assert.Equal(ft, actual, expectedOne)
assert.Assert(t, ft.failNowed)
})

t.Run("value is updated when -update=true", func(t *testing.T) {
patchUpdate(t)
ft := &fakeTestingT{}

actual := `this is the
actual value
that we are testing against`
assert.Equal(ft, actual, expectedOne)

// reset
fmt.Println("WHHHHHHHHHHY")
assert.Equal(ft, "\n\n\n", expectedOne)
})
}

var expectedOne = `


`

func patchUpdate(t *testing.T) {
source.Update = true
t.Cleanup(func() {
source.Update = false
})
}

type fakeTestingT struct {
failNowed bool
failed bool
msgs []string
}

func (f *fakeTestingT) FailNow() {
f.failNowed = true
}

func (f *fakeTestingT) Fail() {
f.failed = true
}

func (f *fakeTestingT) Log(args ...interface{}) {
f.msgs = append(f.msgs, args[0].(string))
}

func (f *fakeTestingT) Helper() {}
8 changes: 4 additions & 4 deletions assert/cmp/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func DeepEqual(x, y interface{}, opts ...cmp.Option) Comparison {
if diff == "" {
return ResultSuccess
}
return multiLineDiffResult(diff)
return multiLineDiffResult(diff, x, y)
}
}

Expand Down Expand Up @@ -102,7 +102,7 @@ func Equal(x, y interface{}) Comparison {
return ResultSuccess
case isMultiLineStringCompare(x, y):
diff := format.UnifiedDiff(format.DiffConfig{A: x.(string), B: y.(string)})
return multiLineDiffResult(diff)
return multiLineDiffResult(diff, x, y)
}
return ResultFailureTemplate(`
{{- printf "%v" .Data.x}} (
Expand All @@ -128,12 +128,12 @@ func isMultiLineStringCompare(x, y interface{}) bool {
return strings.Contains(strX, "\n") || strings.Contains(strY, "\n")
}

func multiLineDiffResult(diff string) Result {
func multiLineDiffResult(diff string, x, y interface{}) Result {
return ResultFailureTemplate(`
--- {{ with callArg 0 }}{{ formatNode . }}{{else}}←{{end}}
+++ {{ with callArg 1 }}{{ formatNode . }}{{else}}→{{end}}
{{ .Data.diff }}`,
map[string]interface{}{"diff": diff})
map[string]interface{}{"diff": diff, "x": x, "y": y})
}

// Len succeeds if the sequence has the expected length.
Expand Down
5 changes: 5 additions & 0 deletions assert/cmp/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ func (r templatedResult) FailureMessage(args []ast.Expr) string {
return msg
}

func (r templatedResult) UpdatedExpected(stackIndex int) error {
// TODO: would be nice to have structured data instead of a map
return source.UpdateExpectedValue(stackIndex+1, r.data["x"], r.data["y"])
}

// ResultFailureTemplate returns a Result with a template string and data which
// can be used to format a failure message. The template may access data from .Data,
// the comparison args with the callArg function, and the formatNode function may
Expand Down
29 changes: 26 additions & 3 deletions icmd/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
exec "golang.org/x/sys/execabs"
"gotest.tools/v3/assert"
"gotest.tools/v3/fs"
"gotest.tools/v3/golden"
"gotest.tools/v3/internal/maint"
)

Expand Down Expand Up @@ -120,9 +119,22 @@ func TestResult_Match_NotMatched(t *testing.T) {
}
err := result.match(exp)
assert.ErrorContains(t, err, "Failures")
golden.Assert(t, err.Error(), "result-match-no-match.golden")
assert.Equal(t, err.Error(), expectedMatch)
}

var expectedMatch = `
Command: binary arg1
ExitCode: 99 (timeout)
Error: exit code 99
Stdout: the output
Stderr: the stderr

Failures:
ExitCode was 99 expected 101
Expected command to finish, but it hit the timeout
Expected stdout to contain "Something else"
Expected stderr to contain "[NOTHING]"`

func newLockedBuffer(s string) *lockedBuffer {
return &lockedBuffer{buf: *bytes.NewBufferString(s)}
}
Expand All @@ -140,9 +152,20 @@ func TestResult_Match_NotMatchedNoError(t *testing.T) {
}
err := result.match(exp)
assert.ErrorContains(t, err, "Failures")
golden.Assert(t, err.Error(), "result-match-no-match-no-error.golden")
assert.Equal(t, err.Error(), expectedResultMatchNoMatch)
}

var expectedResultMatchNoMatch = `
Command: binary arg1
ExitCode: 0
Stdout: the output
Stderr: the stderr

Failures:
ExitCode was 0 expected 101
Expected stdout to contain "Something else"
Expected stderr to contain "[NOTHING]"`

func TestResult_Match_Match(t *testing.T) {
result := &Result{
Cmd: exec.Command("binary", "arg1"),
Expand Down
10 changes: 0 additions & 10 deletions icmd/testdata/result-match-no-match-no-error.golden

This file was deleted.

12 changes: 0 additions & 12 deletions icmd/testdata/result-match-no-match.golden

This file was deleted.

21 changes: 21 additions & 0 deletions internal/assert/result.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package assert

import (
"errors"
"fmt"
"go/ast"

Expand All @@ -25,6 +26,22 @@ func RunComparison(
return true
}

if source.Update {
if updater, ok := result.(updateExpected); ok {
const stackIndex = 3 // Assert/Check, assert, RunComparison
err := updater.UpdatedExpected(stackIndex)
switch {
case err == nil:
return true
case errors.Is(err, source.ErrNotFound):
// do nothing, fallthrough to regular failure message
default:
t.Log("failed to update source", err)
return false
}
}
}

var message string
switch typed := result.(type) {
case resultWithComparisonArgs:
Expand Down Expand Up @@ -52,6 +69,10 @@ type resultBasic interface {
FailureMessage() string
}

type updateExpected interface {
UpdatedExpected(stackIndex int) error
}

// filterPrintableExpr filters the ast.Expr slice to only include Expr that are
// easy to read when printed and contain relevant information to an assertion.
//
Expand Down
2 changes: 1 addition & 1 deletion internal/source/defers.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func guessDefer(node ast.Node) (ast.Node, error) {
defers := collectDefers(node)
switch len(defers) {
case 0:
return nil, fmt.Errorf("failed to expression in defer")
return nil, fmt.Errorf("failed to find expression in defer")
case 1:
return defers[0].Call, nil
default:
Expand Down
2 changes: 1 addition & 1 deletion internal/source/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func CallExprArgs(stackIndex int) ([]ast.Expr, error) {
return expr, nil
}

func getNodeAtLine(fileset *token.FileSet, astFile *ast.File, lineNum int) (ast.Node, error) {
func getNodeAtLine(fileset *token.FileSet, astFile ast.Node, lineNum int) (ast.Node, error) {
if node := scanToLine(fileset, astFile, lineNum); node != nil {
return node, nil
}
Expand Down
120 changes: 118 additions & 2 deletions internal/source/update.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,127 @@
package source

import "flag"
import (
"bytes"
"errors"
"flag"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"os"
"runtime"
"strings"
)

// Update is set by the -update flag. It indicates the user running the tests
// would like to update any golden values.
var Update bool

func init() {
flag.BoolVar(&Update, "update", false, "update golden files")
flag.BoolVar(&Update, "update", false, "update golden values")
}

// ErrNotFound indicates that UpdateExpectedValue failed to find the
// variable to update, likely because it is not a package level variable.
var ErrNotFound = fmt.Errorf("failed to find variable for update of golden value")

// UpdateExpectedValue looks for a package-level variable with a name that
// starts with expected in the arguments to the caller. If the variable is
// found, the value of the variable will be updated to value of the other
// argument to the caller.
func UpdateExpectedValue(stackIndex int, x, y interface{}) error {
_, filename, line, ok := runtime.Caller(stackIndex + 1)
if !ok {
return errors.New("failed to get call stack")
}
debug("call stack position: %s:%d", filename, line)

fileset := token.NewFileSet()
astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors|parser.ParseComments)
if err != nil {
return fmt.Errorf("failed to parse source file %s: %w", filename, err)
}

debug("before modification: %v", debugFormatNode{astFile})

expr, err := getCallExprArgs(fileset, astFile, line)
if err != nil {
return fmt.Errorf("call from %s:%d: %w", filename, line, err)
}

if len(expr) < 3 {
debug("not enough arguments %d: %v",
len(expr), debugFormatNode{Node: &ast.CallExpr{Args: expr}})
return ErrNotFound
}

argIndex, varName := getVarNameForExpectedValueArg(expr)
if argIndex < 0 || varName == "" {
debug("no arguments started with the word 'expected': %v",
debugFormatNode{Node: &ast.CallExpr{Args: expr}})
return ErrNotFound
}

value := x
if argIndex == 1 {
value = y
}

obj := astFile.Scope.Objects[varName]
if obj == nil {
return ErrNotFound
}
if obj.Kind != ast.Con && obj.Kind != ast.Var {
debug("can only update var and const, found %v", obj.Kind)
return ErrNotFound
}

spec, ok := obj.Decl.(*ast.ValueSpec)
if !ok {
debug("can only update *ast.ValueSpec, found %T", obj.Decl)
return ErrNotFound
}
if len(spec.Names) != 1 {
debug("more than one name in ast.ValueSpec")
return ErrNotFound
}

// TODO: allow a function to wrap the string literal
spec.Values[0] = &ast.BasicLit{
Kind: token.STRING,
// TODO: safer
Value: "`" + value.(string) + "`",
}

debug("after modification: %v", debugFormatNode{astFile})

var buf bytes.Buffer
if err := format.Node(&buf, fileset, astFile); err != nil {
return fmt.Errorf("failed to format file after update: %w", err)
}

fh, err := os.Create(filename)
if err != nil {
return fmt.Errorf("failed to open file %v: %w", filename, err)
}
if _, err = fh.Write(buf.Bytes()); err != nil {
return fmt.Errorf("failed to write file %v: %w", filename, err)
}
if err := fh.Sync(); err != nil {
return fmt.Errorf("failed to sync file %v: %w", filename, err)
}
return nil
}

func getVarNameForExpectedValueArg(expr []ast.Expr) (int, string) {
for i := 1; i < 3; i++ {
switch e := expr[i].(type) {
case *ast.Ident:
if strings.HasPrefix(strings.ToLower(e.Name), "expected") {
return i, e.Name
}
}
}
return -1, ""
}