241 lines
6.6 KiB
Go
241 lines
6.6 KiB
Go
// Licensed under the MIT license, see LICENSE file for details.
|
|
|
|
package quicktest
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/printer"
|
|
"go/token"
|
|
"io"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// reportParams holds parameters for reporting a test error.
|
|
type reportParams struct {
|
|
// argNames holds the names for the arguments passed to the checker.
|
|
argNames []string
|
|
// got holds the value that was checked.
|
|
got interface{}
|
|
// args holds all other arguments (if any) provided to the checker.
|
|
args []interface{}
|
|
// comment optionally holds the comment passed when performing the check.
|
|
comments []Comment
|
|
// notes holds notes added while doing the check.
|
|
notes []note
|
|
// format holds the format function that must be used when outputting
|
|
// values.
|
|
format formatFunc
|
|
}
|
|
|
|
// Unquoted indicates that the string must not be pretty printed in the failure
|
|
// output. This is useful when a checker calls note and does not want the
|
|
// provided value to be quoted.
|
|
type Unquoted string
|
|
|
|
// SuppressedIfLong indicates that the value must be suppressed if verbose
|
|
// testing is off and the pretty printed version of the value is long. This is
|
|
// useful when a checker calls note and does not want the provided value to be
|
|
// printed in non-verbose test runs if the value is too long.
|
|
type SuppressedIfLong struct {
|
|
// Value holds the original annotated value.
|
|
Value interface{}
|
|
}
|
|
|
|
// longValueLines holds the number of lines after which a value is long.
|
|
const longValueLines = 10
|
|
|
|
// report generates a failure report for the given error, optionally including
|
|
// in the output the checker arguments, comment and notes included in the
|
|
// provided report parameters.
|
|
func report(err error, p reportParams) string {
|
|
var buf bytes.Buffer
|
|
buf.WriteByte('\n')
|
|
writeError(&buf, err, p)
|
|
writeStack(&buf)
|
|
return buf.String()
|
|
}
|
|
|
|
// writeError writes a pretty formatted output of the given error using the
|
|
// provided report parameters.
|
|
func writeError(w io.Writer, err error, p reportParams) {
|
|
ptrs := make(map[string]interface{})
|
|
values := make(map[string]string)
|
|
printPair := func(key string, value interface{}) {
|
|
fmt.Fprintln(w, key+":")
|
|
var v string
|
|
if u, ok := value.(Unquoted); ok {
|
|
v = string(u)
|
|
} else if s, ok := value.(SuppressedIfLong); ok {
|
|
v = p.format(s.Value)
|
|
if !testingVerbose() {
|
|
if n := strings.Count(v, "\n"); n > longValueLines {
|
|
v = fmt.Sprintf("<suppressed due to length (%d lines), use -v for full output>", n)
|
|
}
|
|
}
|
|
} else {
|
|
v = p.format(value)
|
|
}
|
|
isPtr := reflect.ValueOf(value).Kind() == reflect.Ptr
|
|
if k := values[v]; k != "" {
|
|
if previousValue, ok := ptrs[k]; ok && isPtr && previousValue != value {
|
|
fmt.Fprint(w, prefixf(prefix, "<same as %q but different pointer value>", k))
|
|
return
|
|
}
|
|
fmt.Fprint(w, prefixf(prefix, "<same as %q>", k))
|
|
return
|
|
}
|
|
if isPtr {
|
|
ptrs[key] = value
|
|
}
|
|
values[v] = key
|
|
fmt.Fprint(w, prefixf(prefix, "%s", v))
|
|
}
|
|
|
|
// Write the checker error.
|
|
if err != ErrSilent {
|
|
printPair("error", Unquoted(err.Error()))
|
|
}
|
|
|
|
// Write comments if provided.
|
|
for _, c := range p.comments {
|
|
if comment := c.String(); comment != "" {
|
|
printPair("comment", Unquoted(comment))
|
|
}
|
|
}
|
|
|
|
// Write notes if present.
|
|
for _, n := range p.notes {
|
|
printPair(n.key, n.value)
|
|
}
|
|
if IsBadCheck(err) || err == ErrSilent {
|
|
// For errors in the checker invocation or for silent errors, do not
|
|
// show output from args.
|
|
return
|
|
}
|
|
|
|
// Write provided args.
|
|
for i, arg := range append([]interface{}{p.got}, p.args...) {
|
|
printPair(p.argNames[i], arg)
|
|
}
|
|
}
|
|
|
|
// testingVerbose is defined as a variable for testing.
|
|
var testingVerbose = func() bool {
|
|
return testing.Verbose()
|
|
}
|
|
|
|
// writeStack writes the traceback information for the current failure into the
|
|
// provided writer.
|
|
func writeStack(w io.Writer) {
|
|
fmt.Fprintln(w, "stack:")
|
|
pc := make([]uintptr, 8)
|
|
sg := &stmtGetter{
|
|
fset: token.NewFileSet(),
|
|
files: make(map[string]*ast.File, 8),
|
|
config: &printer.Config{
|
|
Mode: printer.UseSpaces,
|
|
Tabwidth: 4,
|
|
},
|
|
}
|
|
runtime.Callers(5, pc)
|
|
frames := runtime.CallersFrames(pc)
|
|
thisPackage := reflect.TypeOf(C{}).PkgPath() + "."
|
|
for {
|
|
frame, more := frames.Next()
|
|
if strings.HasPrefix(frame.Function, "testing.") {
|
|
// Stop before getting back to stdlib test runner calls.
|
|
break
|
|
}
|
|
if fname := strings.TrimPrefix(frame.Function, thisPackage); fname != frame.Function {
|
|
if ast.IsExported(fname) {
|
|
// Continue without printing frames for quicktest exported API.
|
|
continue
|
|
}
|
|
// Stop when entering quicktest internal calls.
|
|
// This is useful for instance when using qtsuite.
|
|
break
|
|
}
|
|
fmt.Fprint(w, prefixf(prefix, "%s:%d", frame.File, frame.Line))
|
|
if strings.HasSuffix(frame.File, ".go") {
|
|
stmt, err := sg.Get(frame.File, frame.Line)
|
|
if err != nil {
|
|
fmt.Fprint(w, prefixf(prefix+prefix, "<%s>", err))
|
|
} else {
|
|
fmt.Fprint(w, prefixf(prefix+prefix, "%s", stmt))
|
|
}
|
|
}
|
|
if !more {
|
|
// There are no more callers.
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
type stmtGetter struct {
|
|
fset *token.FileSet
|
|
files map[string]*ast.File
|
|
config *printer.Config
|
|
}
|
|
|
|
// Get returns the lines of code of the statement at the given file and line.
|
|
func (sg *stmtGetter) Get(file string, line int) (string, error) {
|
|
f := sg.files[file]
|
|
if f == nil {
|
|
var err error
|
|
f, err = parser.ParseFile(sg.fset, file, nil, parser.ParseComments)
|
|
if err != nil {
|
|
return "", fmt.Errorf("cannot parse source file: %s", err)
|
|
}
|
|
sg.files[file] = f
|
|
}
|
|
var stmt string
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
if n == nil || stmt != "" {
|
|
return false
|
|
}
|
|
pos := sg.fset.Position(n.Pos()).Line
|
|
end := sg.fset.Position(n.End()).Line
|
|
// Go < v1.9 reports the line where the statements ends, not the line
|
|
// where it begins.
|
|
if line == pos || line == end {
|
|
var buf bytes.Buffer
|
|
// TODO: include possible comment after the statement.
|
|
sg.config.Fprint(&buf, sg.fset, &printer.CommentedNode{
|
|
Node: n,
|
|
Comments: f.Comments,
|
|
})
|
|
stmt = buf.String()
|
|
return false
|
|
}
|
|
return pos < line && line <= end
|
|
})
|
|
return stmt, nil
|
|
}
|
|
|
|
// prefixf formats the given string with the given args. It also inserts the
|
|
// final newline if needed and indentation with the given prefix.
|
|
func prefixf(prefix, format string, args ...interface{}) string {
|
|
var buf []byte
|
|
s := strings.TrimSuffix(fmt.Sprintf(format, args...), "\n")
|
|
for _, line := range strings.Split(s, "\n") {
|
|
buf = append(buf, prefix...)
|
|
buf = append(buf, line...)
|
|
buf = append(buf, '\n')
|
|
}
|
|
return string(buf)
|
|
}
|
|
|
|
// note holds a key/value annotation.
|
|
type note struct {
|
|
key string
|
|
value interface{}
|
|
}
|
|
|
|
// prefix is the string used to indent blocks of output.
|
|
const prefix = " "
|