mirror of https://github.com/stretchr/testify.git
Ensure that assert.Fail properly align its output
Previous implementation depended on tab alignment. This worked when the output was not prepended with anything, but would break if the output was prepended with further spaces (which can occur in environments like IntelliJ test runners). This commit fixes it so that the output is always aligned logically. Fixes #83pull/364/head
parent
976c720a22
commit
ddb91ee140
|
@ -157,7 +157,7 @@ func getWhitespaceString() string {
|
|||
parts := strings.Split(file, "/")
|
||||
file = parts[len(parts)-1]
|
||||
|
||||
return strings.Repeat(" ", len(fmt.Sprintf("%s:%d: ", file, line)))
|
||||
return strings.Repeat(" ", len(fmt.Sprintf("%s:%d: ", file, line)))
|
||||
|
||||
}
|
||||
|
||||
|
@ -174,22 +174,18 @@ func messageFromMsgAndArgs(msgAndArgs ...interface{}) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// Indents all lines of the message by appending a number of tabs to each line, in an output format compatible with Go's
|
||||
// test printing (see inner comment for specifics)
|
||||
func indentMessageLines(message string, tabs int) string {
|
||||
// Aligns the provided message so that all lines after the first line start at the same location as the first line.
|
||||
// Assumes that the first line starts at the correct location (after carriage return, tab, label, spacer and tab).
|
||||
// The longestLabelLen parameter specifies the length of the longest label in the output (required becaues this is the
|
||||
// basis on which the alignment occurs).
|
||||
func indentMessageLines(message string, longestLabelLen int) string {
|
||||
outBuf := new(bytes.Buffer)
|
||||
|
||||
for i, scanner := 0, bufio.NewScanner(strings.NewReader(message)); scanner.Scan(); i++ {
|
||||
// no need to align first line because it starts at the correct location (after the label)
|
||||
if i != 0 {
|
||||
outBuf.WriteRune('\n')
|
||||
}
|
||||
for ii := 0; ii < tabs; ii++ {
|
||||
outBuf.WriteRune('\t')
|
||||
// Bizarrely, all lines except the first need one fewer tabs prepended, so deliberately advance the counter
|
||||
// by 1 prematurely.
|
||||
if ii == 0 && i > 0 {
|
||||
ii++
|
||||
}
|
||||
// append alignLen+1 spaces to align with "{{longestLabel}}:" before adding tab
|
||||
outBuf.WriteString("\n\r\t" + strings.Repeat(" ", longestLabelLen +1) + "\t")
|
||||
}
|
||||
outBuf.WriteString(scanner.Text())
|
||||
}
|
||||
|
@ -221,29 +217,49 @@ func FailNow(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool
|
|||
|
||||
// Fail reports a failure through
|
||||
func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool {
|
||||
|
||||
message := messageFromMsgAndArgs(msgAndArgs...)
|
||||
|
||||
errorTrace := strings.Join(CallerInfo(), "\n\r\t\t\t")
|
||||
if len(message) > 0 {
|
||||
t.Errorf("\r%s\r\tError Trace:\t%s\n"+
|
||||
"\r\tError:%s\n"+
|
||||
"\r\tMessages:\t%s\n\r",
|
||||
getWhitespaceString(),
|
||||
errorTrace,
|
||||
indentMessageLines(failureMessage, 2),
|
||||
message)
|
||||
} else {
|
||||
t.Errorf("\r%s\r\tError Trace:\t%s\n"+
|
||||
"\r\tError:%s\n\r",
|
||||
getWhitespaceString(),
|
||||
errorTrace,
|
||||
indentMessageLines(failureMessage, 2))
|
||||
content := []labeledContent{
|
||||
{"Error Trace", strings.Join(CallerInfo(), "\n\r\t\t\t")},
|
||||
{"Error", failureMessage},
|
||||
}
|
||||
|
||||
message := messageFromMsgAndArgs(msgAndArgs...)
|
||||
if len(message) > 0 {
|
||||
content = append(content, labeledContent{"Messages", message})
|
||||
}
|
||||
|
||||
t.Errorf("\r" + getWhitespaceString() + labeledOutput(content...))
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type labeledContent struct {
|
||||
label string
|
||||
content string
|
||||
}
|
||||
|
||||
// labeledOutput returns a string consisting of the provided labeledContent. Each labeled output is appended in the following manner:
|
||||
//
|
||||
// \r\t{{label}}:{{align_spaces}}\t{{content}}\n
|
||||
//
|
||||
// The initial carriage return is required to undo/erase any padding added by testing.T.Errorf. The "\t{{label}}:" is for the label.
|
||||
// If a label is shorter than the longest label provided, padding spaces are added to make all the labels match in length. Once this
|
||||
// alignment is achieved, "\t{{content}}\n" is added for the output.
|
||||
//
|
||||
// If the content of the labeledOutput contains line breaks, the subsequent lines are aligned so that they start at the same location as the first line.
|
||||
func labeledOutput(content ...labeledContent) string {
|
||||
longestLabel := 0
|
||||
for _, v := range content {
|
||||
if len(v.label) > longestLabel {
|
||||
longestLabel = len(v.label)
|
||||
}
|
||||
}
|
||||
var output string
|
||||
for _, v := range content {
|
||||
output += "\r\t" + v.label + ":" + strings.Repeat(" ", longestLabel-len(v.label)) + "\t" + indentMessageLines(v.content, longestLabel) + "\n"
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
// Implements asserts that an object is implemented by the specified interface.
|
||||
//
|
||||
// assert.Implements(t, (*MyInterface)(nil), new(MyObject), "MyObject")
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
package assert
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
@ -195,6 +199,64 @@ func TestEqual(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
// bufferT implements TestingT. Its implementation of Errorf writes the output that would be produced by
|
||||
// testing.T.Errorf to an internal bytes.Buffer.
|
||||
type bufferT struct {
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (t *bufferT) Errorf(format string, args ...interface{}) {
|
||||
// implementation of decorate is copied from testing.T
|
||||
decorate := func(s string) string {
|
||||
_, file, line, ok := runtime.Caller(3) // decorate + log + public function.
|
||||
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 = "???"
|
||||
line = 1
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
// Every line is indented at least one tab.
|
||||
buf.WriteByte('\t')
|
||||
fmt.Fprintf(buf, "%s:%d: ", file, line)
|
||||
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")
|
||||
}
|
||||
buf.WriteString(line)
|
||||
}
|
||||
buf.WriteByte('\n')
|
||||
return buf.String()
|
||||
}
|
||||
t.buf.WriteString(decorate(fmt.Sprintf(format, args...)))
|
||||
}
|
||||
|
||||
func TestEqualFormatting(t *testing.T) {
|
||||
for i, currCase := range []struct {
|
||||
equalWant string
|
||||
equalGot string
|
||||
msgAndArgs []interface{}
|
||||
want string
|
||||
}{
|
||||
{equalWant:"want", equalGot: "got", want: "\tassertions.go:[0-9]+: \r \r\tError Trace:\t\n\t\t\r\tError: \tNot equal: \n\t\t\r\t \texpected: \"want\"\n\t\t\r\t \treceived: \"got\"\n"},
|
||||
{equalWant:"want", equalGot: "got", msgAndArgs: []interface{}{"hello, %v!", "world"}, want: "\tassertions.go:[0-9]+: \r \r\tError Trace:\t\n\t\t\r\tError: \tNot equal: \n\t\t\r\t \texpected: \"want\"\n\t\t\r\t \treceived: \"got\"\n\t\t\r\tMessages: \thello, world!\n"},
|
||||
} {
|
||||
mockT := &bufferT{}
|
||||
Equal(mockT, currCase.equalWant, currCase.equalGot, currCase.msgAndArgs...)
|
||||
Regexp(t, regexp.MustCompile(currCase.want), mockT.buf.String(), "Case %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatUnequalValues(t *testing.T) {
|
||||
expected, actual := formatUnequalValues("foo", "bar")
|
||||
Equal(t, `"foo"`, expected, "value should not include type")
|
||||
|
|
Loading…
Reference in New Issue