|
| 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