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