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