Skip to content

Commit 3e2dc45

Browse files
committed
cmd/test2json: go tool test2json converts test output to JSON
Also add cmd/internal/test2json, the actual implementation, which will be called directly from cmd/go in addition to being a standalone command (like cmd/buildid and cmd/internal/buildid). For #2981. Change-Id: I244ce36d665f424bbf13f5ae00ece10b705d367d Reviewed-on: https://go-review.googlesource.com/76872 Run-TryBot: Russ Cox <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent 7badae8 commit 3e2dc45

File tree

11 files changed

+1368
-0
lines changed

11 files changed

+1368
-0
lines changed
+398
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
// Copyright 2017 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Package test2json implements conversion of test binary output to JSON.
6+
// It is used by cmd/test2json and cmd/go.
7+
//
8+
// See the cmd/test2json documentation for details of the JSON encoding.
9+
package test2json
10+
11+
import (
12+
"bytes"
13+
"encoding/json"
14+
"fmt"
15+
"io"
16+
"strconv"
17+
"strings"
18+
"time"
19+
"unicode/utf8"
20+
)
21+
22+
// Mode controls details of the conversion.
23+
type Mode int
24+
25+
const (
26+
Timestamp Mode = 1 << iota // include Time in events
27+
)
28+
29+
// event is the JSON struct we emit.
30+
type event struct {
31+
Time *time.Time `json:",omitempty"`
32+
Action string
33+
Package string `json:",omitempty"`
34+
Test string `json:",omitempty"`
35+
Elapsed *float64 `json:",omitempty"`
36+
Output *textBytes `json:",omitempty"`
37+
}
38+
39+
// textBytes is a hack to get JSON to emit a []byte as a string
40+
// without actually copying it to a string.
41+
// It implements encoding.TextMarshaler, which returns its text form as a []byte,
42+
// and then json encodes that text form as a string (which was our goal).
43+
type textBytes []byte
44+
45+
func (b textBytes) MarshalText() ([]byte, error) { return b, nil }
46+
47+
// A converter holds the state of a test-to-JSON conversion.
48+
// It implements io.WriteCloser; the caller writes test output in,
49+
// and the converter writes JSON output to w.
50+
type converter struct {
51+
w io.Writer // JSON output stream
52+
pkg string // package to name in events
53+
mode Mode // mode bits
54+
start time.Time // time converter started
55+
testName string // name of current test, for output attribution
56+
report []*event // pending test result reports (nested for subtests)
57+
passed bool // whether we've seen the final whole-package PASS line
58+
input lineBuffer // input buffer
59+
output lineBuffer // output buffer
60+
}
61+
62+
// inBuffer and outBuffer are the input and output buffer sizes.
63+
// They're variables so that they can be reduced during testing.
64+
//
65+
// The input buffer needs to be able to hold any single test
66+
// directive line we want to recognize, like:
67+
//
68+
// <many spaces> --- PASS: very/nested/s/u/b/t/e/s/t
69+
//
70+
// If anyone reports a test directive line > 4k not working, it will
71+
// be defensible to suggest they restructure their test or test names.
72+
//
73+
// The output buffer must be >= utf8.UTFMax, so that it can
74+
// accumulate any single UTF8 sequence. Lines that fit entirely
75+
// within the output buffer are emitted in single output events.
76+
// Otherwise they are split into multiple events.
77+
// The output buffer size therefore limits the size of the encoding
78+
// of a single JSON output event. 1k seems like a reasonable balance
79+
// between wanting to avoid splitting an output line and not wanting to
80+
// generate enormous output events.
81+
var (
82+
inBuffer = 4096
83+
outBuffer = 1024
84+
)
85+
86+
// NewConverter returns a "test to json" converter.
87+
// Writes on the returned writer are written as JSON to w,
88+
// with minimal delay.
89+
//
90+
// The writes to w are whole JSON events ending in \n,
91+
// so that it is safe to run multiple tests writing to multiple converters
92+
// writing to a single underlying output stream w.
93+
// As long as the underlying output w can handle concurrent writes
94+
// from multiple goroutines, the result will be a JSON stream
95+
// describing the relative ordering of execution in all the concurrent tests.
96+
//
97+
// The mode flag adjusts the behavior of the converter.
98+
// Passing ModeTime includes event timestamps and elapsed times.
99+
//
100+
// The pkg string, if present, specifies the import path to
101+
// report in the JSON stream.
102+
func NewConverter(w io.Writer, pkg string, mode Mode) io.WriteCloser {
103+
c := new(converter)
104+
*c = converter{
105+
w: w,
106+
pkg: pkg,
107+
mode: mode,
108+
start: time.Now(),
109+
input: lineBuffer{
110+
b: make([]byte, 0, inBuffer),
111+
line: c.handleInputLine,
112+
part: c.output.write,
113+
},
114+
output: lineBuffer{
115+
b: make([]byte, 0, outBuffer),
116+
line: c.writeOutputEvent,
117+
part: c.writeOutputEvent,
118+
},
119+
}
120+
return c
121+
}
122+
123+
// Write writes the test input to the converter.
124+
func (c *converter) Write(b []byte) (int, error) {
125+
c.input.write(b)
126+
return len(b), nil
127+
}
128+
129+
var (
130+
bigPass = []byte("PASS\n")
131+
bigFail = []byte("FAIL\n")
132+
133+
updates = [][]byte{
134+
[]byte("=== RUN "),
135+
[]byte("=== PAUSE "),
136+
[]byte("=== CONT "),
137+
}
138+
139+
reports = [][]byte{
140+
[]byte("--- PASS: "),
141+
[]byte("--- FAIL: "),
142+
}
143+
144+
fourSpace = []byte(" ")
145+
)
146+
147+
// handleInputLine handles a single whole test output line.
148+
// It must write the line to c.output but may choose to do so
149+
// before or after emitting other events.
150+
func (c *converter) handleInputLine(line []byte) {
151+
// Final PASS or FAIL.
152+
if bytes.Equal(line, bigPass) || bytes.Equal(line, bigFail) {
153+
c.flushReport(0)
154+
c.output.write(line)
155+
c.passed = bytes.Equal(line, bigPass)
156+
return
157+
}
158+
159+
// "=== RUN "
160+
// "=== PAUSE "
161+
// "=== CONT "
162+
origLine := line
163+
ok := false
164+
indent := 0
165+
for _, magic := range updates {
166+
if bytes.HasPrefix(line, magic) {
167+
ok = true
168+
break
169+
}
170+
}
171+
if !ok {
172+
// "--- PASS: "
173+
// "--- FAIL: "
174+
// but possibly indented.
175+
for bytes.HasPrefix(line, fourSpace) {
176+
line = line[4:]
177+
indent++
178+
}
179+
for _, magic := range reports {
180+
if bytes.HasPrefix(line, magic) {
181+
ok = true
182+
break
183+
}
184+
}
185+
}
186+
187+
if !ok {
188+
// Not a special test output line.
189+
c.output.write(origLine)
190+
return
191+
}
192+
193+
// Parse out action and test name.
194+
action := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(string(line[4:4+6])), ":"))
195+
name := strings.TrimSpace(string(line[4+6:]))
196+
197+
e := &event{Action: action}
198+
if line[0] == '-' { // PASS or FAIL report
199+
// Parse out elapsed time.
200+
if i := strings.Index(name, " ("); i >= 0 {
201+
if strings.HasSuffix(name, "s)") {
202+
t, err := strconv.ParseFloat(name[i+2:len(name)-2], 64)
203+
if err == nil {
204+
if c.mode&Timestamp != 0 {
205+
e.Elapsed = &t
206+
}
207+
}
208+
}
209+
name = name[:i]
210+
}
211+
if len(c.report) < indent {
212+
// Nested deeper than expected.
213+
// Treat this line as plain output.
214+
return
215+
}
216+
// Flush reports at this indentation level or deeper.
217+
c.flushReport(indent)
218+
e.Test = name
219+
c.testName = name
220+
c.report = append(c.report, e)
221+
c.output.write(origLine)
222+
return
223+
}
224+
// === update.
225+
// Finish any pending PASS/FAIL reports.
226+
c.flushReport(0)
227+
c.testName = name
228+
229+
if action == "pause" {
230+
// For a pause, we want to write the pause notification before
231+
// delivering the pause event, just so it doesn't look like the test
232+
// is generating output immediately after being paused.
233+
c.output.write(origLine)
234+
}
235+
c.writeEvent(e)
236+
if action != "pause" {
237+
c.output.write(origLine)
238+
}
239+
240+
return
241+
}
242+
243+
// flushReport flushes all pending PASS/FAIL reports at levels >= depth.
244+
func (c *converter) flushReport(depth int) {
245+
c.testName = ""
246+
for len(c.report) > depth {
247+
e := c.report[len(c.report)-1]
248+
c.report = c.report[:len(c.report)-1]
249+
c.writeEvent(e)
250+
}
251+
}
252+
253+
// Close marks the end of the go test output.
254+
// It flushes any pending input and then output (only partial lines at this point)
255+
// and then emits the final overall package-level pass/fail event.
256+
func (c *converter) Close() error {
257+
c.input.flush()
258+
c.output.flush()
259+
e := &event{Action: "fail"}
260+
if c.passed {
261+
e.Action = "pass"
262+
}
263+
if c.mode&Timestamp != 0 {
264+
dt := time.Since(c.start).Round(1 * time.Millisecond).Seconds()
265+
e.Elapsed = &dt
266+
}
267+
c.writeEvent(e)
268+
return nil
269+
}
270+
271+
// writeOutputEvent writes a single output event with the given bytes.
272+
func (c *converter) writeOutputEvent(out []byte) {
273+
c.writeEvent(&event{
274+
Action: "output",
275+
Output: (*textBytes)(&out),
276+
})
277+
}
278+
279+
// writeEvent writes a single event.
280+
// It adds the package, time (if requested), and test name (if needed).
281+
func (c *converter) writeEvent(e *event) {
282+
e.Package = c.pkg
283+
if c.mode&Timestamp != 0 {
284+
t := time.Now()
285+
e.Time = &t
286+
}
287+
if e.Test == "" {
288+
e.Test = c.testName
289+
}
290+
js, err := json.Marshal(e)
291+
if err != nil {
292+
// Should not happen - event is valid for json.Marshal.
293+
c.w.Write([]byte(fmt.Sprintf("testjson internal error: %v\n", err)))
294+
return
295+
}
296+
js = append(js, '\n')
297+
c.w.Write(js)
298+
}
299+
300+
// A lineBuffer is an I/O buffer that reacts to writes by invoking
301+
// input-processing callbacks on whole lines or (for long lines that
302+
// have been split) line fragments.
303+
//
304+
// It should be initialized with b set to a buffer of length 0 but non-zero capacity,
305+
// and line and part set to the desired input processors.
306+
// The lineBuffer will call line(x) for any whole line x (including the final newline)
307+
// that fits entirely in cap(b). It will handle input lines longer than cap(b) by
308+
// calling part(x) for sections of the line. The line will be split at UTF8 boundaries,
309+
// and the final call to part for a long line includes the final newline.
310+
type lineBuffer struct {
311+
b []byte // buffer
312+
mid bool // whether we're in the middle of a long line
313+
line func([]byte) // line callback
314+
part func([]byte) // partial line callback
315+
}
316+
317+
// write writes b to the buffer.
318+
func (l *lineBuffer) write(b []byte) {
319+
for len(b) > 0 {
320+
// Copy what we can into b.
321+
m := copy(l.b[len(l.b):cap(l.b)], b)
322+
l.b = l.b[:len(l.b)+m]
323+
b = b[m:]
324+
325+
// Process lines in b.
326+
i := 0
327+
for i < len(l.b) {
328+
j := bytes.IndexByte(l.b[i:], '\n')
329+
if j < 0 {
330+
break
331+
}
332+
e := i + j + 1
333+
if l.mid {
334+
// Found the end of a partial line.
335+
l.part(l.b[i:e])
336+
l.mid = false
337+
} else {
338+
// Found a whole line.
339+
l.line(l.b[i:e])
340+
}
341+
i = e
342+
}
343+
344+
// Whatever's left in l.b is a line fragment.
345+
if i == 0 && len(l.b) == cap(l.b) {
346+
// The whole buffer is a fragment.
347+
// Emit it as the beginning (or continuation) of a partial line.
348+
t := trimUTF8(l.b)
349+
l.part(l.b[:t])
350+
l.b = l.b[:copy(l.b, l.b[t:])]
351+
l.mid = true
352+
}
353+
354+
// There's room for more input.
355+
// Slide it down in hope of completing the line.
356+
if i > 0 {
357+
l.b = l.b[:copy(l.b, l.b[i:])]
358+
}
359+
}
360+
}
361+
362+
// flush flushes the line buffer.
363+
func (l *lineBuffer) flush() {
364+
if len(l.b) > 0 {
365+
// Must be a line without a \n, so a partial line.
366+
l.part(l.b)
367+
l.b = l.b[:0]
368+
}
369+
}
370+
371+
// trimUTF8 returns a length t as close to len(b) as possible such that b[:t]
372+
// does not end in the middle of a possibly-valid UTF-8 sequence.
373+
//
374+
// If a large text buffer must be split before position i at the latest,
375+
// splitting at position trimUTF(b[:i]) avoids splitting a UTF-8 sequence.
376+
func trimUTF8(b []byte) int {
377+
// Scan backward to find non-continuation byte.
378+
for i := 1; i < utf8.UTFMax && i <= len(b); i++ {
379+
if c := b[len(b)-i]; c&0xc0 != 0x80 {
380+
switch {
381+
case c&0xe0 == 0xc0:
382+
if i < 2 {
383+
return len(b) - i
384+
}
385+
case c&0xf0 == 0xe0:
386+
if i < 3 {
387+
return len(b) - i
388+
}
389+
case c&0xf8 == 0xf0:
390+
if i < 4 {
391+
return len(b) - i
392+
}
393+
}
394+
break
395+
}
396+
}
397+
return len(b)
398+
}

0 commit comments

Comments
 (0)