amseltools/vendor/github.com/matryer/is/is.go

404 lines
8.8 KiB
Go
Raw Permalink Normal View History

2023-03-29 15:26:21 +02:00
// Package is provides a lightweight extension to the
// standard library's testing capabilities.
//
// Comments on the assertion lines are used to add
// a description.
//
// The following failing test:
//
// func Test(t *testing.T) {
// is := is.New(t)
// a, b := 1, 2
// is.Equal(a, b) // expect to be the same
// }
//
// Will output:
//
// your_test.go:123: 1 != 2 // expect to be the same
//
// Usage
//
// The following code shows a range of useful ways you can use
// the helper methods:
//
// func Test(t *testing.T) {
// // always start tests with this
// is := is.New(t)
//
// signedin, err := isSignedIn(ctx)
// is.NoErr(err) // isSignedIn error
// is.Equal(signedin, true) // must be signed in
//
// body := readBody(r)
// is.True(strings.Contains(body, "Hi there"))
// }
package is
import (
"bufio"
"bytes"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
)
// T reports when failures occur.
// testing.T implements this interface.
type T interface {
// Fail indicates that the test has failed but
// allowed execution to continue.
// Fail is called in relaxed mode (via NewRelaxed).
Fail()
// FailNow indicates that the test has failed and
// aborts the test.
// FailNow is called in strict mode (via New).
FailNow()
}
// I is the test helper harness.
type I struct {
t T
fail func()
out io.Writer
colorful bool
helpers map[string]struct{} // functions to be skipped when writing file/line info
}
var noColorFlag bool
func init() {
var envNoColor bool
// prefer https://no-color.org (with any value)
if _, ok := os.LookupEnv("NO_COLOR"); ok {
envNoColor = true
}
if v, ok := os.LookupEnv("IS_NO_COLOR"); ok {
if b, err := strconv.ParseBool(v); err == nil {
envNoColor = b
}
}
flag.BoolVar(&noColorFlag, "nocolor", envNoColor, "turns off colors")
}
// New makes a new testing helper using the specified
// T through which failures will be reported.
// In strict mode, failures call T.FailNow causing the test
// to be aborted. See NewRelaxed for alternative behavior.
func New(t T) *I {
return &I{t, t.FailNow, os.Stdout, !noColorFlag, map[string]struct{}{}}
}
// NewRelaxed makes a new testing helper using the specified
// T through which failures will be reported.
// In relaxed mode, failures call T.Fail allowing
// multiple failures per test.
func NewRelaxed(t T) *I {
return &I{t, t.Fail, os.Stdout, !noColorFlag, map[string]struct{}{}}
}
func (is *I) log(args ...interface{}) {
s := is.decorate(fmt.Sprint(args...))
fmt.Fprintf(is.out, s)
is.fail()
}
func (is *I) logf(format string, args ...interface{}) {
is.log(fmt.Sprintf(format, args...))
}
// Fail immediately fails the test.
//
// func Test(t *testing.T) {
// is := is.New(t)
// is.Fail() // TODO: write this test
// }
//
// In relaxed mode, execution will continue after a call to
// Fail, but that test will still fail.
func (is *I) Fail() {
is.log("failed")
}
// True asserts that the expression is true. The expression
// code itself will be reported if the assertion fails.
//
// func Test(t *testing.T) {
// is := is.New(t)
// val := method()
// is.True(val != nil) // val should never be nil
// }
//
// Will output:
//
// your_test.go:123: not true: val != nil
func (is *I) True(expression bool) {
if !expression {
is.log("not true: $ARGS")
}
}
// Equal asserts that a and b are equal.
//
// func Test(t *testing.T) {
// is := is.New(t)
// a := greet("Mat")
// is.Equal(a, "Hi Mat") // greeting
// }
//
// Will output:
//
// your_test.go:123: Hey Mat != Hi Mat // greeting
func (is *I) Equal(a, b interface{}) {
if areEqual(a, b) {
return
}
if isNil(a) || isNil(b) {
is.logf("%s != %s", is.valWithType(a), is.valWithType(b))
} else if reflect.ValueOf(a).Type() == reflect.ValueOf(b).Type() {
is.logf("%v != %v", a, b)
} else {
is.logf("%s != %s", is.valWithType(a), is.valWithType(b))
}
}
// New is a method wrapper around the New function.
// It allows you to write subtests using a similar
// pattern:
//
// func Test(t *testing.T) {
// is := is.New(t)
// t.Run("sub", func(t *testing.T) {
// is := is.New(t)
// // TODO: test
// })
// }
func (is *I) New(t T) *I {
return New(t)
}
// NewRelaxed is a method wrapper around the NewRelaxed
// method. It allows you to write subtests using a similar
// pattern:
//
// func Test(t *testing.T) {
// is := is.NewRelaxed(t)
// t.Run("sub", func(t *testing.T) {
// is := is.NewRelaxed(t)
// // TODO: test
// })
// }
func (is *I) NewRelaxed(t T) *I {
return NewRelaxed(t)
}
func (is *I) valWithType(v interface{}) string {
if isNil(v) {
return "<nil>"
}
if is.colorful {
return fmt.Sprintf("%[1]s%[3]T(%[2]s%[3]v%[1]s)%[2]s", colorType, colorNormal, v)
}
return fmt.Sprintf("%[1]T(%[1]v)", v)
}
// NoErr asserts that err is nil.
//
// func Test(t *testing.T) {
// is := is.New(t)
// val, err := getVal()
// is.NoErr(err) // getVal error
// is.True(len(val) > 10) // val cannot be short
// }
//
// Will output:
//
// your_test.go:123: err: not found // getVal error
func (is *I) NoErr(err error) {
if err != nil {
is.logf("err: %s", err.Error())
}
}
// isNil gets whether the object is nil or not.
func isNil(object interface{}) bool {
if object == nil {
return true
}
value := reflect.ValueOf(object)
kind := value.Kind()
if kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil() {
return true
}
return false
}
// areEqual gets whether a equals b or not.
func areEqual(a, b interface{}) bool {
if isNil(a) && isNil(b) {
return true
}
if isNil(a) || isNil(b) {
return false
}
if reflect.DeepEqual(a, b) {
return true
}
aValue := reflect.ValueOf(a)
bValue := reflect.ValueOf(b)
return aValue == bValue
}
// loadComment gets the Go comment from the specified line
// in the specified file.
func loadComment(path string, line int) (string, bool) {
f, err := os.Open(path)
if err != nil {
return "", false
}
defer f.Close()
s := bufio.NewScanner(f)
i := 1
for s.Scan() {
if i != line {
i++
continue
}
text := s.Text()
commentI := strings.Index(text, "// ")
if commentI == -1 {
return "", false // no comment
}
text = text[commentI+2:]
text = strings.TrimSpace(text)
return text, true
}
return "", false
}
// loadArguments gets the arguments from the function call
// on the specified line of the file.
func loadArguments(path string, line int) (string, bool) {
f, err := os.Open(path)
if err != nil {
return "", false
}
defer f.Close()
s := bufio.NewScanner(f)
i := 1
for s.Scan() {
if i != line {
i++
continue
}
text := s.Text()
braceI := strings.Index(text, "(")
if braceI == -1 {
return "", false
}
text = text[braceI+1:]
cs := bufio.NewScanner(strings.NewReader(text))
cs.Split(bufio.ScanBytes)
j := 0
c := 1
for cs.Scan() {
switch cs.Text() {
case ")":
c--
case "(":
c++
}
if c == 0 {
break
}
j++
}
text = text[:j]
return text, true
}
return "", false
}
// decorate prefixes the string with the file and line of the call site
// and inserts the final newline if needed and indentation tabs for formatting.
// this function was copied from the testing framework and modified.
func (is *I) decorate(s string) string {
path, lineNumber, ok := is.callerinfo() // decorate + log + public function.
file := filepath.Base(path)
if ok {
// Truncate file name at last file name separator.
if index := strings.LastIndex(file, "/"); index >= 0 {
file = file[index+1:]
} else if index = strings.LastIndex(file, "\\"); index >= 0 {
file = file[index+1:]
}
} else {
file = "???"
lineNumber = 1
}
buf := new(bytes.Buffer)
// Every line is indented at least one tab.
buf.WriteByte('\t')
if is.colorful {
buf.WriteString(colorFile)
}
fmt.Fprintf(buf, "%s:%d: ", file, lineNumber)
if is.colorful {
buf.WriteString(colorNormal)
}
s = escapeFormatString(s)
lines := strings.Split(s, "\n")
if l := len(lines); l > 1 && lines[l-1] == "" {
lines = lines[:l-1]
}
for i, line := range lines {
if i > 0 {
// Second and subsequent lines are indented an extra tab.
buf.WriteString("\n\t\t")
}
// expand arguments (if $ARGS is present)
if strings.Contains(line, "$ARGS") {
args, _ := loadArguments(path, lineNumber)
line = strings.Replace(line, "$ARGS", args, -1)
}
buf.WriteString(line)
}
comment, ok := loadComment(path, lineNumber)
if ok {
if is.colorful {
buf.WriteString(colorComment)
}
buf.WriteString(" // ")
comment = escapeFormatString(comment)
buf.WriteString(comment)
if is.colorful {
buf.WriteString(colorNormal)
}
}
buf.WriteString("\n")
return buf.String()
}
// escapeFormatString escapes strings for use in formatted functions like Sprintf.
func escapeFormatString(fmt string) string {
return strings.Replace(fmt, "%", "%%", -1)
}
const (
colorNormal = "\u001b[39m"
colorComment = "\u001b[31m"
colorFile = "\u001b[90m"
colorType = "\u001b[90m"
)