Skip to content

Commit 079e5ce

Browse files
committed
parser/gotest: Add support for parsing lines longer than 64K
The gotest parser used a bufio.Scanner to read its input, which prevented us from reading lines larger than 64K. In order to support reading larger lines, bufio.Scanner has been replaced with bufio.Reader. The maximum line size has been increased to 4MiB and instead of returning an error when reading lines that exceed the maximum size, we truncate that line and continue parsing. Fixes #135
1 parent 7875e13 commit 079e5ce

File tree

2 files changed

+113
-4
lines changed

2 files changed

+113
-4
lines changed

parser/gotest/gotest.go

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package gotest
33

44
import (
55
"bufio"
6+
"bytes"
67
"fmt"
78
"io"
89
"regexp"
@@ -101,6 +102,12 @@ func SetSubtestMode(mode SubtestMode) Option {
101102
}
102103
}
103104

105+
const (
106+
// maxLineSize is the maximum amount of bytes we'll read for a single line.
107+
// Lines longer than maxLineSize will be truncated.
108+
maxLineSize = 4 * 1024 * 1024
109+
)
110+
104111
// Parser is a Go test output Parser.
105112
type Parser struct {
106113
packageName string
@@ -124,11 +131,59 @@ func NewParser(options ...Option) *Parser {
124131
// gtr.Report.
125132
func (p *Parser) Parse(r io.Reader) (gtr.Report, error) {
126133
p.events = nil
127-
s := bufio.NewScanner(r)
128-
for s.Scan() {
129-
p.parseLine(s.Text())
134+
s := bufio.NewReader(r)
135+
for {
136+
line, isPrefix, err := s.ReadLine()
137+
if err == io.EOF {
138+
break
139+
} else if err != nil {
140+
return gtr.Report{}, err
141+
}
142+
143+
if !isPrefix {
144+
p.parseLine(string(line))
145+
continue
146+
}
147+
148+
// Line is incomplete, keep reading until we reach the end of the line.
149+
var buf bytes.Buffer
150+
buf.Write(line) // ignore err, always nil
151+
for isPrefix {
152+
line, isPrefix, err = s.ReadLine()
153+
if err == io.EOF {
154+
break
155+
} else if err != nil {
156+
return gtr.Report{}, err
157+
}
158+
159+
if buf.Len() >= maxLineSize {
160+
// Stop writing to buf if we exceed maxLineSize. We continue
161+
// reading however to make sure we consume the entire line.
162+
continue
163+
}
164+
165+
buf.Write(line) // ignore err, always nil
166+
}
167+
168+
if buf.Len() > maxLineSize {
169+
buf.Truncate(maxLineSize)
170+
}
171+
172+
// Lines that exceed bufio.MaxScanTokenSize are not expected to contain
173+
// any relevant test infrastructure output, so instead of parsing them
174+
// we treat them as regular output to increase performance.
175+
//
176+
// Parser used a bufio.Scanner in the past, which only supported
177+
// reading lines up to bufio.MaxScanTokenSize in length. Since this
178+
// turned out to be fine in almost all cases, it seemed an appropriate
179+
// value to use to decide whether or not to attempt parsing this line.
180+
if buf.Len() > bufio.MaxScanTokenSize {
181+
p.output(buf.String())
182+
} else {
183+
p.parseLine(buf.String())
184+
}
130185
}
131-
return p.report(p.events), s.Err()
186+
return p.report(p.events), nil
132187
}
133188

134189
// report generates a gtr.Report from the given list of events.

parser/gotest/gotest_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package gotest
22

33
import (
4+
"bufio"
5+
"bytes"
46
"fmt"
57
"strings"
68
"testing"
@@ -216,6 +218,58 @@ func TestParseLine(t *testing.T) {
216218
}
217219
}
218220

221+
func TestParseLargeLine(t *testing.T) {
222+
tests := []struct {
223+
desc string
224+
inputSize int
225+
}{
226+
{"small size", 128},
227+
{"under buf size", 4095},
228+
{"buf size", 4096},
229+
{"multiple of buf size ", 4096 * 2},
230+
{"not multiple of buf size", 10 * 1024},
231+
{"bufio.MaxScanTokenSize", bufio.MaxScanTokenSize},
232+
{"over bufio.MaxScanTokenSize", bufio.MaxScanTokenSize + 1},
233+
{"under limit", maxLineSize - 1},
234+
{"at limit", maxLineSize},
235+
{"just over limit", maxLineSize + 1},
236+
{"over limit", maxLineSize + 128},
237+
}
238+
239+
createInput := func(lines ...string) *bytes.Buffer {
240+
buf := &bytes.Buffer{}
241+
buf.WriteString("=== RUN TestOne\n--- PASS: TestOne (0.00s)\n")
242+
buf.WriteString(strings.Join(lines, "\n"))
243+
return buf
244+
}
245+
246+
for _, test := range tests {
247+
t.Run(test.desc, func(t *testing.T) {
248+
line1 := string(make([]byte, test.inputSize))
249+
line2 := "other line"
250+
report, err := NewParser().Parse(createInput(line1, line2))
251+
if err != nil {
252+
t.Fatalf("Parse() returned error %v", err)
253+
} else if len(report.Packages) != 1 {
254+
t.Fatalf("Parse() returned unexpected number of packages, got %d want 1.", len(report.Packages))
255+
} else if len(report.Packages[0].Output) != 2 {
256+
t.Fatalf("Parse() returned unexpected number of output lines, got %d want 1.", len(report.Packages[0].Output))
257+
}
258+
259+
want := line1
260+
if len(want) > maxLineSize {
261+
want = want[:maxLineSize]
262+
}
263+
if got := report.Packages[0].Output[0]; got != want {
264+
t.Fatalf("Parse() output line1 mismatch, got len %d want len %d", len(got), len(want))
265+
}
266+
if report.Packages[0].Output[1] != line2 {
267+
t.Fatalf("Parse() output line2 mismatch, got %v want %v", report.Packages[0].Output[1], line2)
268+
}
269+
})
270+
}
271+
}
272+
219273
func TestReport(t *testing.T) {
220274
events := []Event{
221275
{Type: "run_test", Name: "TestOne"},

0 commit comments

Comments
 (0)