1
2
3
4
5
6
7
8
9 package test2json
10
11 import (
12 "bytes"
13 "encoding/json"
14 "fmt"
15 "io"
16 "strconv"
17 "strings"
18 "time"
19 "unicode"
20 "unicode/utf8"
21 )
22
23
24 type Mode int
25
26 const (
27 Timestamp Mode = 1 << iota
28 )
29
30
31 type event struct {
32 Time *time.Time `json:",omitempty"`
33 Action string
34 Package string `json:",omitempty"`
35 Test string `json:",omitempty"`
36 Elapsed *float64 `json:",omitempty"`
37 Output *textBytes `json:",omitempty"`
38 FailedBuild string `json:",omitempty"`
39 Key string `json:",omitempty"`
40 Value string `json:",omitempty"`
41 Path string `json:",omitempty"`
42 }
43
44
45
46
47
48 type textBytes []byte
49
50 func (b textBytes) MarshalText() ([]byte, error) { return b, nil }
51
52
53
54
55 type Converter struct {
56 w io.Writer
57 pkg string
58 mode Mode
59 start time.Time
60 testName string
61 report []*event
62 result string
63 input lineBuffer
64 output lineBuffer
65 needMarker bool
66
67
68
69 failedBuild string
70 }
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91 var (
92 inBuffer = 4096
93 outBuffer = 1024
94 )
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112 func NewConverter(w io.Writer, pkg string, mode Mode) *Converter {
113 c := new(Converter)
114 *c = Converter{
115 w: w,
116 pkg: pkg,
117 mode: mode,
118 start: time.Now(),
119 input: lineBuffer{
120 b: make([]byte, 0, inBuffer),
121 line: c.handleInputLine,
122 part: c.output.write,
123 },
124 output: lineBuffer{
125 b: make([]byte, 0, outBuffer),
126 line: c.writeOutputEvent,
127 part: c.writeOutputEvent,
128 },
129 }
130 c.writeEvent(&event{Action: "start"})
131 return c
132 }
133
134
135 func (c *Converter) Write(b []byte) (int, error) {
136 c.input.write(b)
137 return len(b), nil
138 }
139
140
141 func (c *Converter) Exited(err error) {
142 if err == nil {
143 if c.result != "skip" {
144 c.result = "pass"
145 }
146 } else {
147 c.result = "fail"
148 }
149 }
150
151
152
153
154 func (c *Converter) SetFailedBuild(pkgID string) {
155 c.failedBuild = pkgID
156 }
157
158 const marker = byte(0x16)
159
160 var (
161
162 bigPass = []byte("PASS")
163
164
165 bigFail = []byte("FAIL")
166
167
168
169 bigFailErrorPrefix = []byte("FAIL\t")
170
171
172 emptyName = []byte("=== NAME")
173 emptyNameLine = []byte("=== NAME \n")
174
175 updates = [][]byte{
176 []byte("=== RUN "),
177 []byte("=== PAUSE "),
178 []byte("=== CONT "),
179 []byte("=== NAME "),
180 []byte("=== PASS "),
181 []byte("=== FAIL "),
182 []byte("=== SKIP "),
183 []byte("=== ATTR "),
184 []byte("=== ARTIFACTS "),
185 }
186
187 reports = [][]byte{
188 []byte("--- PASS: "),
189 []byte("--- FAIL: "),
190 []byte("--- SKIP: "),
191 []byte("--- BENCH: "),
192 }
193
194 fourSpace = []byte(" ")
195
196 skipLinePrefix = []byte("? \t")
197 skipLineSuffix = []byte("\t[no test files]")
198 )
199
200
201
202
203 func (c *Converter) handleInputLine(line []byte) {
204 if len(line) == 0 {
205 return
206 }
207 sawMarker := false
208 if c.needMarker && line[0] != marker {
209 c.output.write(line)
210 return
211 }
212 if line[0] == marker {
213 c.output.flush()
214 sawMarker = true
215 line = line[1:]
216 }
217
218
219 trim := line
220 if len(trim) > 0 && trim[len(trim)-1] == '\n' {
221 trim = trim[:len(trim)-1]
222 if len(trim) > 0 && trim[len(trim)-1] == '\r' {
223 trim = trim[:len(trim)-1]
224 }
225 }
226
227
228 if bytes.Equal(trim, emptyName) {
229 line = emptyNameLine
230 trim = line[:len(line)-1]
231 }
232
233
234 if bytes.Equal(trim, bigPass) || bytes.Equal(trim, bigFail) || bytes.HasPrefix(trim, bigFailErrorPrefix) {
235 c.flushReport(0)
236 c.testName = ""
237 c.needMarker = sawMarker
238 c.output.write(line)
239 if bytes.Equal(trim, bigPass) {
240 c.result = "pass"
241 } else {
242 c.result = "fail"
243 }
244 return
245 }
246
247
248
249 if bytes.HasPrefix(line, skipLinePrefix) && bytes.HasSuffix(trim, skipLineSuffix) && len(c.report) == 0 {
250 c.result = "skip"
251 }
252
253
254
255
256 origLine := line
257 ok := false
258 indent := 0
259 for _, magic := range updates {
260 if bytes.HasPrefix(line, magic) {
261 ok = true
262 break
263 }
264 }
265 if !ok {
266
267
268
269
270
271 for bytes.HasPrefix(line, fourSpace) {
272 line = line[4:]
273 indent++
274 }
275 for _, magic := range reports {
276 if bytes.HasPrefix(line, magic) {
277 ok = true
278 break
279 }
280 }
281 }
282
283
284 if !ok {
285
286
287
288
289
290
291
292 if indent > 0 && indent <= len(c.report) {
293 c.testName = c.report[indent-1].Test
294 }
295 c.output.write(origLine)
296 return
297 }
298
299
300 action, name, _ := strings.Cut(string(line[len("=== "):]), " ")
301 action = strings.TrimSuffix(action, ":")
302 action = strings.ToLower(action)
303 name = strings.TrimSpace(name)
304
305 e := &event{Action: action}
306 if line[0] == '-' {
307
308 if i := strings.Index(name, " ("); i >= 0 {
309 if strings.HasSuffix(name, "s)") {
310 t, err := strconv.ParseFloat(name[i+2:len(name)-2], 64)
311 if err == nil {
312 if c.mode&Timestamp != 0 {
313 e.Elapsed = &t
314 }
315 }
316 }
317 name = name[:i]
318 }
319 if len(c.report) < indent {
320
321
322 c.output.write(origLine)
323 return
324 }
325
326 c.needMarker = sawMarker
327 c.flushReport(indent)
328 e.Test = name
329 c.testName = name
330 c.report = append(c.report, e)
331 c.output.write(origLine)
332 return
333 }
334 switch action {
335 case "artifacts":
336 name, e.Path, _ = strings.Cut(name, " ")
337 case "attr":
338 var rest string
339 name, rest, _ = strings.Cut(name, " ")
340 e.Key, e.Value, _ = strings.Cut(rest, " ")
341 }
342
343
344 c.needMarker = sawMarker
345 c.flushReport(0)
346 c.testName = name
347
348 if action == "name" {
349
350
351 return
352 }
353
354 if action == "pause" {
355
356
357
358 c.output.write(origLine)
359 }
360 c.writeEvent(e)
361 if action != "pause" {
362 c.output.write(origLine)
363 }
364
365 return
366 }
367
368
369 func (c *Converter) flushReport(depth int) {
370 c.testName = ""
371 for len(c.report) > depth {
372 e := c.report[len(c.report)-1]
373 c.report = c.report[:len(c.report)-1]
374 c.writeEvent(e)
375 }
376 }
377
378
379
380
381 func (c *Converter) Close() error {
382 c.input.flush()
383 c.output.flush()
384 if c.result != "" {
385 e := &event{Action: c.result}
386 if c.mode&Timestamp != 0 {
387 dt := time.Since(c.start).Round(1 * time.Millisecond).Seconds()
388 e.Elapsed = &dt
389 }
390 if c.result == "fail" {
391 e.FailedBuild = c.failedBuild
392 }
393 c.writeEvent(e)
394 }
395 return nil
396 }
397
398
399 func (c *Converter) writeOutputEvent(out []byte) {
400 c.writeEvent(&event{
401 Action: "output",
402 Output: (*textBytes)(&out),
403 })
404 }
405
406
407
408 func (c *Converter) writeEvent(e *event) {
409 e.Package = c.pkg
410 if c.mode&Timestamp != 0 {
411 t := time.Now()
412 e.Time = &t
413 }
414 if e.Test == "" {
415 e.Test = c.testName
416 }
417 js, err := json.Marshal(e)
418 if err != nil {
419
420 fmt.Fprintf(c.w, "testjson internal error: %v\n", err)
421 return
422 }
423 js = append(js, '\n')
424 c.w.Write(js)
425 }
426
427
428
429
430
431
432
433
434
435
436
437 type lineBuffer struct {
438 b []byte
439 mid bool
440 line func([]byte)
441 part func([]byte)
442 }
443
444
445 func (l *lineBuffer) write(b []byte) {
446 for len(b) > 0 {
447
448 m := copy(l.b[len(l.b):cap(l.b)], b)
449 l.b = l.b[:len(l.b)+m]
450 b = b[m:]
451
452
453 i := 0
454 for i < len(l.b) {
455 j, w := indexEOL(l.b[i:])
456 if j < 0 {
457 if !l.mid {
458 if j := bytes.IndexByte(l.b[i:], '\t'); j >= 0 {
459 if isBenchmarkName(bytes.TrimRight(l.b[i:i+j], " ")) {
460 l.part(l.b[i : i+j+1])
461 l.mid = true
462 i += j + 1
463 }
464 }
465 }
466 break
467 }
468 e := i + j + w
469 if l.mid {
470
471 l.part(l.b[i:e])
472 l.mid = false
473 } else {
474
475 l.line(l.b[i:e])
476 }
477 i = e
478 }
479
480
481 if i == 0 && len(l.b) == cap(l.b) {
482
483
484 t := trimUTF8(l.b)
485 l.part(l.b[:t])
486 l.b = l.b[:copy(l.b, l.b[t:])]
487 l.mid = true
488 }
489
490
491
492 if i > 0 {
493 l.b = l.b[:copy(l.b, l.b[i:])]
494 }
495 }
496 }
497
498
499
500
501
502
503 func indexEOL(b []byte) (pos, wid int) {
504 for i, c := range b {
505 if c == '\n' {
506 return i, 1
507 }
508 if c == marker && i > 0 {
509 return i, 0
510 }
511 }
512 return -1, 0
513 }
514
515
516 func (l *lineBuffer) flush() {
517 if len(l.b) > 0 {
518
519 l.part(l.b)
520 l.b = l.b[:0]
521 }
522 }
523
524 var benchmark = []byte("Benchmark")
525
526
527
528 func isBenchmarkName(b []byte) bool {
529 if !bytes.HasPrefix(b, benchmark) {
530 return false
531 }
532 if len(b) == len(benchmark) {
533 return true
534 }
535 r, _ := utf8.DecodeRune(b[len(benchmark):])
536 return !unicode.IsLower(r)
537 }
538
539
540
541
542
543
544 func trimUTF8(b []byte) int {
545
546 for i := 1; i < utf8.UTFMax && i <= len(b); i++ {
547 if c := b[len(b)-i]; c&0xc0 != 0x80 {
548 switch {
549 case c&0xe0 == 0xc0:
550 if i < 2 {
551 return len(b) - i
552 }
553 case c&0xf0 == 0xe0:
554 if i < 3 {
555 return len(b) - i
556 }
557 case c&0xf8 == 0xf0:
558 if i < 4 {
559 return len(b) - i
560 }
561 }
562 break
563 }
564 }
565 return len(b)
566 }
567
View as plain text