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