Skip to content
Permalink
master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
// 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[00m"
colorComment = "\u001b[31m"
colorFile = "\u001b[02m"
colorType = "\u001b[02m"
)