diff --git a/.travis.yml b/.travis.yml index a7efa7b..ff6379e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,40 @@ +os: + - linux + - osx + - windows + language: go sudo: required go: + # "1.x" always refers to the latest Go version, inc. the patch release. + # e.g. "1.x" is 1.13 until 1.13.1 is available. + - 1.x - 1.6.x - 1.7.x + - 1.8.x + - 1.9.x + - 1.10.x + - 1.11.x + - 1.12.x + - 1.13.x + - 1.14.x - tip -env: - - GIMME_OS=linux GIMME_ARCH=amd64 - - GIMME_OS=darwin GIMME_ARCH=amd64 - - GIMME_OS=windows GIMME_ARCH=amd64 +matrix: + allow_failures: + - os: windows + go: tip + exclude: + # OSX 1.6.4 is not present in travis. + # https://github.com/travis-ci/travis-ci/issues/10309 + - go: 1.6.x + os: osx install: - - go get -d -v ./... + - go get -d -v ./... script: - - go build -v ./... + - go build -v ./... + - go test ./... diff --git a/README.md b/README.md index eb2a72f..7034a96 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,12 @@ func Test(t *testing.T) { ## Color -To turn off the colors, run `go test` with the `-nocolor` flag. +To turn off the colors, run `go test` with the `-nocolor` flag, or with the env var `IS_NO_COLOR=true`. ``` go test -nocolor ``` + +``` +IS_NO_COLOR=true go test +``` diff --git a/is-1.7.go b/is-1.7.go new file mode 100644 index 0000000..c19f23f --- /dev/null +++ b/is-1.7.go @@ -0,0 +1,64 @@ +// +build go1.7 + +package is + +import ( + "regexp" + "runtime" +) + +// Helper marks the calling function as a test helper function. +// When printing file and line information, that function will be skipped. +// +// Available with Go 1.7 and later. +func (is *I) Helper() { + is.helpers[callerName(1)] = struct{}{} +} + +// callerName gives the function name (qualified with a package path) +// for the caller after skip frames (where 0 means the current function). +func callerName(skip int) string { + // Make room for the skip PC. + var pc [1]uintptr + n := runtime.Callers(skip+2, pc[:]) // skip + runtime.Callers + callerName + if n == 0 { + panic("is: zero callers found") + } + frames := runtime.CallersFrames(pc[:n]) + frame, _ := frames.Next() + return frame.Function +} + +// The maximum number of stack frames to go through when skipping helper functions for +// the purpose of decorating log messages. +const maxStackLen = 50 + +var reIsSourceFile = regexp.MustCompile("is(-1.7)?\\.go$") + +func (is *I) callerinfo() (path string, line int, ok bool) { + var pc [maxStackLen]uintptr + // Skip two extra frames to account for this function + // and runtime.Callers itself. + n := runtime.Callers(2, pc[:]) + if n == 0 { + panic("is: zero callers found") + } + frames := runtime.CallersFrames(pc[:n]) + var firstFrame, frame runtime.Frame + for more := true; more; { + frame, more = frames.Next() + if reIsSourceFile.MatchString(frame.File) { + continue + } + if firstFrame.PC == 0 { + firstFrame = frame + } + if _, ok := is.helpers[frame.Function]; ok { + // Frame is inside a helper function. + continue + } + return frame.File, frame.Line, true + } + // If no "non-helper" frame is found, the first non is frame is returned. + return firstFrame.File, firstFrame.Line, true +} diff --git a/is-1.7_test.go b/is-1.7_test.go new file mode 100644 index 0000000..f4249c7 --- /dev/null +++ b/is-1.7_test.go @@ -0,0 +1,53 @@ +// +build go1.7 + +package is + +import ( + "bytes" + "strings" + "testing" +) + +// TestSubtests ensures subtests work as expected. +// https://github.com/matryer/is/issues/1 +func TestSubtests(t *testing.T) { + t.Run("sub1", func(t *testing.T) { + is := New(t) + is.Equal(1+1, 2) + }) +} + +func TestHelper(t *testing.T) { + tests := []struct { + name string + helper bool + expectedFilename string + }{ + { + name: "without helper", + helper: false, + expectedFilename: "is_helper_test.go", + }, + { + name: "with helper", + helper: true, + expectedFilename: "is-1.7_test.go", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tt := &mockT{} + is := NewRelaxed(tt) + + var buf bytes.Buffer + is.out = &buf + helper(is, tc.helper) + actual := buf.String() + t.Log(actual) + if !strings.Contains(actual, tc.expectedFilename) { + t.Errorf("string does not contain correct filename: %s", actual) + } + }) + } +} diff --git a/is-before-1.7.go b/is-before-1.7.go new file mode 100644 index 0000000..c62f872 --- /dev/null +++ b/is-before-1.7.go @@ -0,0 +1,23 @@ +// +build !go1.7 + +package is + +import ( + "regexp" + "runtime" +) + +var reIsSourceFile = regexp.MustCompile("is(-before-1.7)?\\.go$") + +func (is *I) callerinfo() (path string, line int, ok bool) { + for i := 0; ; i++ { + _, path, line, ok = runtime.Caller(i) + if !ok { + return + } + if reIsSourceFile.MatchString(path) { + continue + } + return path, line, true + } +} diff --git a/is.go b/is.go index 74a6073..c363c82 100644 --- a/is.go +++ b/is.go @@ -45,7 +45,6 @@ import ( "os" "path/filepath" "reflect" - "runtime" "strings" "testing" ) @@ -69,12 +68,15 @@ type I struct { 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() { - flag.BoolVar(&noColorFlag, "nocolor", false, "turns off colors") + envNoColor := os.Getenv("IS_NO_COLOR") == "true" + flag.BoolVar(&noColorFlag, "nocolor", envNoColor, "turns off colors") } // New makes a new testing helper using the specified @@ -82,7 +84,7 @@ func init() { // 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} + return &I{t, t.FailNow, os.Stdout, !noColorFlag, map[string]struct{}{}} } // NewRelaxed makes a new testing helper using the specified @@ -90,7 +92,7 @@ func New(t T) *I { // 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} + return &I{t, t.Fail, os.Stdout, !noColorFlag, map[string]struct{}{}} } func (is *I) log(args ...interface{}) { @@ -146,23 +148,14 @@ func (is *I) True(expression bool) { // // your_test.go:123: Hey Mat != Hi Mat // greeting func (is *I) Equal(a, b interface{}) { - if !areEqual(a, b) { - if isNil(a) || isNil(b) { - aLabel := is.valWithType(a) - bLabel := is.valWithType(b) - if isNil(a) { - aLabel = "" - } - if isNil(b) { - bLabel = "" - } - is.logf("%s != %s", aLabel, bLabel) - return - } - if reflect.ValueOf(a).Type() == reflect.ValueOf(b).Type() { - is.logf("%v != %v", a, b) - return - } + 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)) } } @@ -198,6 +191,9 @@ func (is *I) NewRelaxed(t *testing.T) *I { } func (is *I) valWithType(v interface{}) string { + if isNil(v) { + return "" + } if is.colorful { return fmt.Sprintf("%[1]s%[3]T(%[2]s%[3]v%[1]s)%[2]s", colorType, colorNormal, v) } @@ -237,15 +233,12 @@ func isNil(object interface{}) bool { // areEqual gets whether a equals b or not. func areEqual(a, b interface{}) bool { - if isNil(a) || isNil(b) { - if isNil(a) && !isNil(b) { - return false - } - if !isNil(a) && isNil(b) { - return false - } + if isNil(a) && isNil(b) { return true } + if isNil(a) || isNil(b) { + return false + } if reflect.DeepEqual(a, b) { return true } @@ -254,19 +247,6 @@ func areEqual(a, b interface{}) bool { return aValue == bValue } -func callerinfo() (path string, line int, ok bool) { - for i := 0; ; i++ { - _, path, line, ok = runtime.Caller(i) - if !ok { - return - } - if strings.HasSuffix(path, "is.go") { - continue - } - return path, line, true - } -} - // loadComment gets the Go comment from the specified line // in the specified file. func loadComment(path string, line int) (string, bool) { @@ -280,7 +260,7 @@ func loadComment(path string, line int) (string, bool) { for s.Scan() { if i == line { text := s.Text() - commentI := strings.Index(text, "//") + commentI := strings.Index(text, "// ") if commentI == -1 { return "", false // no comment } @@ -339,7 +319,7 @@ func loadArguments(path string, line int) (string, bool) { // 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 := callerinfo() // decorate + log + public function. + path, lineNumber, ok := is.callerinfo() // decorate + log + public function. file := filepath.Base(path) if ok { // Truncate file name at last file name separator. @@ -362,6 +342,9 @@ func (is *I) decorate(s string) string { 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] @@ -384,6 +367,7 @@ func (is *I) decorate(s string) string { buf.WriteString(colorComment) } buf.WriteString(" // ") + comment = escapeFormatString(comment) buf.WriteString(comment) if is.colorful { buf.WriteString(colorNormal) @@ -393,9 +377,14 @@ func (is *I) decorate(s string) string { 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[32m" + colorComment = "\u001b[31m" colorFile = "\u001b[90m" colorType = "\u001b[90m" ) diff --git a/is_helper_test.go b/is_helper_test.go new file mode 100644 index 0000000..6bcbf77 --- /dev/null +++ b/is_helper_test.go @@ -0,0 +1,10 @@ +// +build go1.7 + +package is + +func helper(is *I, helper bool) { + if helper { + is.Helper() + } + is.True(false) +} diff --git a/is_test.go b/is_test.go index b27dcf2..7cb48a7 100644 --- a/is_test.go +++ b/is_test.go @@ -140,6 +140,15 @@ var tests = []struct { }, Fail: ``, }, + { + N: `Equal("20% VAT", "0.2 VAT")`, + F: func(is *I) { + s1 := "20% VAT" + s2 := "0.2 VAT" + is.Equal(s1, s2) // strings + }, + Fail: ` // strings`, + }, // Fail { @@ -268,11 +277,16 @@ func TestLoadArguments(t *testing.T) { } } -// TestSubtests ensures subtests work as expected. -// https://github.com/matryer/is/issues/1 -func TestSubtests(t *testing.T) { - t.Run("sub1", func(t *testing.T) { - is := New(t) - is.Equal(1+1, 2) - }) +// TestArgumentsEscape ensures strings are correctly escaped before printing. +// https://github.com/matryer/is/issues/27 +func TestFormatStringEscape(t *testing.T) { + tt := &mockT{} + is := NewRelaxed(tt) + var buf bytes.Buffer + is.out = &buf + is.Equal("20% VAT", "0.2 VAT") // % symbol should be correctly printed + actual := buf.String() + if strings.Contains(actual, `%!`) { + t.Errorf("string was not escaped correctly: %s", actual) + } } diff --git a/misc/delicious-failures.png b/misc/delicious-failures.png index b1e0d01..676d893 100644 Binary files a/misc/delicious-failures.png and b/misc/delicious-failures.png differ