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 OutputType string `json:",omitempty"`
39 FailedBuild string `json:",omitempty"`
40 Key string `json:",omitempty"`
41 Value string `json:",omitempty"`
42 Path string `json:",omitempty"`
43 }
44
45
46
47
48
49 type textBytes []byte
50
51 func (b textBytes) MarshalText() ([]byte, error) { return b, nil }
52
53
54
55
56 type Converter struct {
57 w io.Writer
58 pkg string
59 mode Mode
60 start time.Time
61 testName string
62 report []*event
63 result string
64 input lineBuffer
65 output lineBuffer
66 markFraming bool
67 markErrEnd bool
68 markEscape bool
69 isFraming bool
70
71
72
73 failedBuild string
74 }
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95 var (
96 inBuffer = 4096
97 outBuffer = 1024
98 )
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125 func NewConverter(w io.Writer, pkg string, mode Mode) *Converter {
126 c := new(Converter)
127 *c = Converter{
128 w: w,
129 pkg: pkg,
130 mode: mode,
131 start: time.Now(),
132 input: lineBuffer{
133 b: make([]byte, 0, inBuffer),
134 line: c.handleInputLine,
135 part: c.output.write,
136 },
137 output: lineBuffer{
138 b: make([]byte, 0, outBuffer),
139 line: c.writeOutputEvent,
140 part: c.writeOutputEvent,
141 },
142 }
143 c.writeEvent(&event{Action: "start"})
144 return c
145 }
146
147
148 func (c *Converter) Write(b []byte) (int, error) {
149 c.input.write(b)
150 return len(b), nil
151 }
152
153
154 func (c *Converter) Exited(err error) {
155 if err == nil {
156 if c.result != "skip" {
157 c.result = "pass"
158 }
159 } else {
160 c.result = "fail"
161 }
162 }
163
164
165
166
167 func (c *Converter) SetFailedBuild(pkgID string) {
168 c.failedBuild = pkgID
169 }
170
171 const (
172 markFraming byte = 'V' &^ '@'
173 markErrBegin byte = 'O' &^ '@'
174 markErrEnd byte = 'N' &^ '@'
175 markEscape byte = '[' &^ '@'
176 )
177
178 var (
179
180 bigPass = []byte("PASS")
181
182
183 bigFail = []byte("FAIL")
184
185
186
187 bigFailErrorPrefix = []byte("FAIL\t")
188
189
190 emptyName = []byte("=== NAME")
191 emptyNameLine = []byte("=== NAME \n")
192
193 updates = [][]byte{
194 []byte("=== RUN "),
195 []byte("=== PAUSE "),
196 []byte("=== CONT "),
197 []byte("=== NAME "),
198 []byte("=== PASS "),
199 []byte("=== FAIL "),
200 []byte("=== SKIP "),
201 []byte("=== ATTR "),
202 []byte("=== ARTIFACTS "),
203 }
204
205 reports = [][]byte{
206 []byte("--- PASS: "),
207 []byte("--- FAIL: "),
208 []byte("--- SKIP: "),
209 []byte("--- BENCH: "),
210 }
211
212 fourSpace = []byte(" ")
213
214 skipLinePrefix = []byte("? \t")
215 skipLineSuffix = []byte("\t[no test files]")
216 )
217
218
219
220
221 func (c *Converter) handleInputLine(line []byte) {
222 if len(line) == 0 {
223 return
224 }
225 sawMarker := false
226 if c.markFraming && line[0] != markFraming {
227 c.output.write(line)
228 return
229 }
230 if line[0] == markFraming {
231 c.output.flush()
232 sawMarker = true
233 line = line[1:]
234 }
235
236
237 trim := line
238 if len(trim) > 0 && trim[len(trim)-1] == '\n' {
239 trim = trim[:len(trim)-1]
240 if len(trim) > 0 && trim[len(trim)-1] == '\r' {
241 trim = trim[:len(trim)-1]
242 }
243 }
244
245
246 if bytes.Equal(trim, emptyName) {
247 line = emptyNameLine
248 trim = line[:len(line)-1]
249 }
250
251
252 if bytes.Equal(trim, bigPass) || bytes.Equal(trim, bigFail) || bytes.HasPrefix(trim, bigFailErrorPrefix) {
253 c.flushReport(0)
254 c.testName = ""
255 c.markFraming = sawMarker
256 c.writeFraming(line)
257 if bytes.Equal(trim, bigPass) {
258 c.result = "pass"
259 } else {
260 c.result = "fail"
261 }
262 return
263 }
264
265
266
267 if bytes.HasPrefix(line, skipLinePrefix) && bytes.HasSuffix(trim, skipLineSuffix) && len(c.report) == 0 {
268 c.result = "skip"
269 }
270
271
272
273
274 origLine := line
275 ok := false
276 indent := 0
277 for _, magic := range updates {
278 if bytes.HasPrefix(line, magic) {
279 ok = true
280 break
281 }
282 }
283 if !ok {
284
285
286
287
288
289 for bytes.HasPrefix(line, fourSpace) {
290 line = line[4:]
291 indent++
292 }
293 for _, magic := range reports {
294 if bytes.HasPrefix(line, magic) {
295 ok = true
296 break
297 }
298 }
299 }
300
301
302 if !ok {
303
304
305
306
307
308
309
310 if indent > 0 && indent <= len(c.report) {
311 c.testName = c.report[indent-1].Test
312 }
313 c.output.write(origLine)
314 return
315 }
316
317
318 action, name, _ := strings.Cut(string(line[len("=== "):]), " ")
319 action = strings.TrimSuffix(action, ":")
320 action = strings.ToLower(action)
321 name = strings.TrimSpace(name)
322
323 e := &event{Action: action}
324 if line[0] == '-' {
325
326 if i := strings.Index(name, " ("); i >= 0 {
327 if strings.HasSuffix(name, "s)") {
328 t, err := strconv.ParseFloat(name[i+2:len(name)-2], 64)
329 if err == nil {
330 if c.mode&Timestamp != 0 {
331 e.Elapsed = &t
332 }
333 }
334 }
335 name = name[:i]
336 }
337 if len(c.report) < indent {
338
339
340 c.output.write(origLine)
341 return
342 }
343
344 c.markFraming = sawMarker
345 c.flushReport(indent)
346 e.Test = name
347 c.testName = name
348 c.report = append(c.report, e)
349 c.writeFraming(origLine)
350 return
351 }
352 switch action {
353 case "artifacts":
354 name, e.Path, _ = strings.Cut(name, " ")
355 case "attr":
356 var rest string
357 name, rest, _ = strings.Cut(name, " ")
358 e.Key, e.Value, _ = strings.Cut(rest, " ")
359 }
360
361
362 c.markFraming = sawMarker
363 c.flushReport(0)
364 c.testName = name
365
366 if action == "name" {
367
368
369 return
370 }
371
372 if action == "pause" {
373
374
375
376 c.writeFraming(origLine)
377 }
378 c.writeEvent(e)
379 if action != "pause" {
380 c.writeFraming(origLine)
381 }
382
383 return
384 }
385
386 func (c *Converter) writeFraming(line []byte) {
387
388
389 c.isFraming = true
390 defer func() { c.isFraming = false }()
391 c.output.write(line)
392 }
393
394
395 func (c *Converter) flushReport(depth int) {
396 c.testName = ""
397 for len(c.report) > depth {
398 e := c.report[len(c.report)-1]
399 c.report = c.report[:len(c.report)-1]
400 c.writeEvent(e)
401 }
402 }
403
404
405
406
407 func (c *Converter) Close() error {
408 c.input.flush()
409 c.output.flush()
410 if c.result != "" {
411 e := &event{Action: c.result}
412 if c.mode&Timestamp != 0 {
413 dt := time.Since(c.start).Round(1 * time.Millisecond).Seconds()
414 e.Elapsed = &dt
415 }
416 if c.result == "fail" {
417 e.FailedBuild = c.failedBuild
418 }
419 c.writeEvent(e)
420 }
421 return nil
422 }
423
424
425 func (c *Converter) writeOutputEvent(out []byte) {
426 var typ string
427 if c.isFraming {
428 typ = "frame"
429 } else if c.markErrEnd {
430 typ = "error-continue"
431 }
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446 for i := 0; i < len(out); i++ {
447 if c.markEscape {
448 c.markEscape = false
449 continue
450 }
451
452 switch out[i] {
453 case markEscape:
454
455 out = append(out[:i], out[i+1:]...)
456 i--
457
458
459 c.markEscape = true
460
461 case markErrBegin:
462
463 if i > 0 {
464 out2 := out[:i]
465 c.writeEvent(&event{
466 Action: "output",
467 Output: (*textBytes)(&out2),
468 OutputType: typ,
469 })
470 }
471
472
473 c.markErrEnd = true
474 typ = "error"
475 out = out[i+1:]
476 i = 0
477
478 case markErrEnd:
479
480 out = append(out[:i], out[i+1:]...)
481
482
483 if i < len(out) && out[i] == '\n' {
484 i++
485 }
486
487
488 out2 := out[:i]
489 c.writeEvent(&event{
490 Action: "output",
491 Output: (*textBytes)(&out2),
492 OutputType: typ,
493 })
494
495
496 c.markErrEnd = false
497 typ = ""
498 out = out[i:]
499 i = 0
500 }
501 }
502
503
504 if len(out) > 0 {
505 c.writeEvent(&event{
506 Action: "output",
507 Output: (*textBytes)(&out),
508 OutputType: typ,
509 })
510 }
511 }
512
513
514
515 func (c *Converter) writeEvent(e *event) {
516 e.Package = c.pkg
517 if c.mode&Timestamp != 0 {
518 t := time.Now()
519 e.Time = &t
520 }
521 if e.Test == "" {
522 e.Test = c.testName
523 }
524 js, err := json.Marshal(e)
525 if err != nil {
526
527 fmt.Fprintf(c.w, "testjson internal error: %v\n", err)
528 return
529 }
530 js = append(js, '\n')
531 c.w.Write(js)
532 }
533
534
535
536
537
538
539
540
541
542
543
544 type lineBuffer struct {
545 b []byte
546 mid bool
547 line func([]byte)
548 part func([]byte)
549 escaped bool
550 }
551
552
553 func (l *lineBuffer) write(b []byte) {
554 for len(b) > 0 {
555
556 m := copy(l.b[len(l.b):cap(l.b)], b)
557 l.b = l.b[:len(l.b)+m]
558 b = b[m:]
559
560
561 i := 0
562 for i < len(l.b) {
563 j, w := l.indexEOL(l.b[i:])
564 if j < 0 {
565 if !l.mid {
566 if j := bytes.IndexByte(l.b[i:], '\t'); j >= 0 {
567 if isBenchmarkName(bytes.TrimRight(l.b[i:i+j], " ")) {
568 l.part(l.b[i : i+j+1])
569 l.mid = true
570 i += j + 1
571 }
572 }
573 }
574 break
575 }
576 e := i + j + w
577 if l.mid {
578
579 l.part(l.b[i:e])
580 l.mid = false
581 } else {
582
583 l.line(l.b[i:e])
584 }
585 i = e
586 }
587
588
589 if i == 0 && len(l.b) == cap(l.b) {
590
591
592 t := trimUTF8(l.b)
593 l.part(l.b[:t])
594 l.b = l.b[:copy(l.b, l.b[t:])]
595 l.mid = true
596 }
597
598
599
600 if i > 0 {
601 l.b = l.b[:copy(l.b, l.b[i:])]
602 }
603 }
604 }
605
606
607
608
609
610
611 func (l *lineBuffer) indexEOL(b []byte) (pos, wid int) {
612 for i, c := range b {
613
614 if c == '\n' {
615 return i, 1
616 }
617
618
619 if l.escaped {
620 l.escaped = false
621 continue
622 }
623
624
625 if c == markEscape {
626 l.escaped = true
627 continue
628 }
629
630 if c == markFraming && i > 0 {
631 return i, 0
632 }
633 }
634 return -1, 0
635 }
636
637
638 func (l *lineBuffer) flush() {
639 if len(l.b) > 0 {
640
641 l.part(l.b)
642 l.b = l.b[:0]
643 }
644 }
645
646 var benchmark = []byte("Benchmark")
647
648
649
650 func isBenchmarkName(b []byte) bool {
651 if !bytes.HasPrefix(b, benchmark) {
652 return false
653 }
654 if len(b) == len(benchmark) {
655 return true
656 }
657 r, _ := utf8.DecodeRune(b[len(benchmark):])
658 return !unicode.IsLower(r)
659 }
660
661
662
663
664
665
666 func trimUTF8(b []byte) int {
667
668 for i := 1; i < utf8.UTFMax && i <= len(b); i++ {
669 if c := b[len(b)-i]; c&0xc0 != 0x80 {
670 switch {
671 case c&0xe0 == 0xc0:
672 if i < 2 {
673 return len(b) - i
674 }
675 case c&0xf0 == 0xe0:
676 if i < 3 {
677 return len(b) - i
678 }
679 case c&0xf8 == 0xf0:
680 if i < 4 {
681 return len(b) - i
682 }
683 }
684 break
685 }
686 }
687 return len(b)
688 }
689
View as plain text