diff --git a/assert/assertions.go b/assert/assertions.go index 0b7570f21..ca7c8f06e 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -300,14 +300,25 @@ func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { // 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 { - // append alignLen+1 spaces to align with "{{longestLabel}}:" before adding tab - outBuf.WriteString("\n\t" + strings.Repeat(" ", longestLabelLen+1) + "\t") + msgScanner := bufio.NewScanner(strings.NewReader(message)) + + // a buffer is set manually to store the tokenization of the message string + // so that we can re-use the same buffer for each line of the message without + // any additional allocations. We set the maximum buffer length to 1 more + // than the length of the message to avoid exceeding the default + // MaxScanTokenSize while scanning lines. This can happen when there is a + // single very long line. Refer to issue #1525 + msgScanner.Buffer(make([]byte, len(message)+1), len(message)+1) + + isFirstLine := true + indent := fmt.Sprintf("\n\t%*s\t", longestLabelLen+1, "") + for msgScanner.Scan() { + if !isFirstLine { + outBuf.WriteString(indent) } - outBuf.WriteString(scanner.Text()) + + outBuf.Write(msgScanner.Bytes()) + isFirstLine = false } return outBuf.String() diff --git a/assert/assertions_priv_test.go b/assert/assertions_priv_test.go new file mode 100644 index 000000000..82beb7922 --- /dev/null +++ b/assert/assertions_priv_test.go @@ -0,0 +1,87 @@ +package assert + +import ( + "bufio" + "bytes" + "strings" + "testing" +) + +func Test_indentMessageLines(t *testing.T) { + tt := []struct { + name string + longestLabelLen int + + // the input is constructed based on the below parameters + bytesPerLine int + lineCount int + }{ + { + name: "single line - over the bufio default limit", + longestLabelLen: 11, + bytesPerLine: bufio.MaxScanTokenSize + 10, + lineCount: 1, + }, + { + name: "multi line - over the bufio default limit", + longestLabelLen: 11, + bytesPerLine: bufio.MaxScanTokenSize + 10, + lineCount: 3, + }, + { + name: "single line - just under the bufio default limit", + longestLabelLen: 11, + bytesPerLine: bufio.MaxScanTokenSize - 10, + lineCount: 1, + }, + { + name: "single line - just under the bufio default limit", + longestLabelLen: 11, + bytesPerLine: bufio.MaxScanTokenSize - 10, + lineCount: 1, + }, + { + name: "single line - equal to the bufio default limit", + longestLabelLen: 11, + bytesPerLine: bufio.MaxScanTokenSize, + lineCount: 1, + }, + { + name: "multi line - equal to the bufio default limit", + longestLabelLen: 11, + bytesPerLine: bufio.MaxScanTokenSize, + lineCount: 3, + }, + { + name: "longest label length is zero", + longestLabelLen: 0, + bytesPerLine: 10, + lineCount: 1, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + var input bytes.Buffer + for i := 0; i < tc.lineCount; i++ { + input.WriteString(strings.Repeat("#", tc.bytesPerLine)) + input.WriteRune('\n') + } + + output := indentMessageLines( + strings.TrimSpace(input.String()), tc.longestLabelLen, + ) + outputLines := strings.Split(output, "\n") + for i, line := range outputLines { + if i > 0 { + // count the leading white spaces. It should be equal to the longest + // label length + 3. The +3 is to account for the 2 '\t' and 1 extra + // space. Read the comment in the function for more context + Equal(t, tc.longestLabelLen+3, strings.Index(line, "#")) + } + + Len(t, strings.TrimSpace(line), tc.bytesPerLine) + } + }) + } +}