| // Copyright 2023 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "io" |
| "sync" |
| "time" |
| ) |
| |
| // lockedWriter serializes Write calls to an underlying Writer. |
| type lockedWriter struct { |
| lock sync.Mutex |
| w io.Writer |
| } |
| |
| func (w *lockedWriter) Write(b []byte) (int, error) { |
| w.lock.Lock() |
| defer w.lock.Unlock() |
| return w.w.Write(b) |
| } |
| |
| // testJSONFilter is an io.Writer filter that replaces the Package field in |
| // test2json output. |
| type testJSONFilter struct { |
| w io.Writer // Underlying writer |
| variant string // Add ":variant" to Package field |
| |
| lineBuf bytes.Buffer // Buffer for incomplete lines |
| } |
| |
| func (f *testJSONFilter) Write(b []byte) (int, error) { |
| bn := len(b) |
| |
| // Process complete lines, and buffer any incomplete lines. |
| for len(b) > 0 { |
| nl := bytes.IndexByte(b, '\n') |
| if nl < 0 { |
| f.lineBuf.Write(b) |
| break |
| } |
| var line []byte |
| if f.lineBuf.Len() > 0 { |
| // We have buffered data. Add the rest of the line from b and |
| // process the complete line. |
| f.lineBuf.Write(b[:nl+1]) |
| line = f.lineBuf.Bytes() |
| } else { |
| // Process a complete line from b. |
| line = b[:nl+1] |
| } |
| b = b[nl+1:] |
| f.process(line) |
| f.lineBuf.Reset() |
| } |
| |
| return bn, nil |
| } |
| |
| func (f *testJSONFilter) Flush() { |
| // Write any remaining partial line to the underlying writer. |
| if f.lineBuf.Len() > 0 { |
| f.w.Write(f.lineBuf.Bytes()) |
| f.lineBuf.Reset() |
| } |
| } |
| |
| func (f *testJSONFilter) process(line []byte) { |
| if len(line) > 0 && line[0] == '{' { |
| // Plausible test2json output. Parse it generically. |
| // |
| // We go to some effort here to preserve key order while doing this |
| // generically. This will stay robust to changes in the test2json |
| // struct, or other additions outside of it. If humans are ever looking |
| // at the output, it's really nice to keep field order because it |
| // preserves a lot of regularity in the output. |
| dec := json.NewDecoder(bytes.NewBuffer(line)) |
| dec.UseNumber() |
| val, err := decodeJSONValue(dec) |
| if err == nil && val.atom == json.Delim('{') { |
| // Rewrite the Package field. |
| found := false |
| for i := 0; i < len(val.seq); i += 2 { |
| if val.seq[i].atom == "Package" { |
| if pkg, ok := val.seq[i+1].atom.(string); ok { |
| val.seq[i+1].atom = pkg + ":" + f.variant |
| found = true |
| break |
| } |
| } |
| } |
| if found { |
| data, err := json.Marshal(val) |
| if err != nil { |
| // Should never happen. |
| panic(fmt.Sprintf("failed to round-trip JSON %q: %s", string(line), err)) |
| } |
| f.w.Write(data) |
| // Copy any trailing text. We expect at most a "\n" here, but |
| // there could be other text and we want to feed that through. |
| io.Copy(f.w, dec.Buffered()) |
| return |
| } |
| } |
| } |
| |
| // Something went wrong. Just pass the line through. |
| f.w.Write(line) |
| } |
| |
| type jsonValue struct { |
| atom json.Token // If json.Delim, then seq will be filled |
| seq []jsonValue // If atom == json.Delim('{'), alternating pairs |
| } |
| |
| var jsonPop = errors.New("end of JSON sequence") |
| |
| func decodeJSONValue(dec *json.Decoder) (jsonValue, error) { |
| t, err := dec.Token() |
| if err != nil { |
| if err == io.EOF { |
| err = io.ErrUnexpectedEOF |
| } |
| return jsonValue{}, err |
| } |
| |
| switch t := t.(type) { |
| case json.Delim: |
| if t == '}' || t == ']' { |
| return jsonValue{}, jsonPop |
| } |
| |
| var seq []jsonValue |
| for { |
| val, err := decodeJSONValue(dec) |
| if err == jsonPop { |
| break |
| } else if err != nil { |
| return jsonValue{}, err |
| } |
| seq = append(seq, val) |
| } |
| return jsonValue{t, seq}, nil |
| default: |
| return jsonValue{t, nil}, nil |
| } |
| } |
| |
| func (v jsonValue) MarshalJSON() ([]byte, error) { |
| var buf bytes.Buffer |
| var marshal1 func(v jsonValue) error |
| marshal1 = func(v jsonValue) error { |
| if t, ok := v.atom.(json.Delim); ok { |
| buf.WriteRune(rune(t)) |
| for i, v2 := range v.seq { |
| if t == '{' && i%2 == 1 { |
| buf.WriteByte(':') |
| } else if i > 0 { |
| buf.WriteByte(',') |
| } |
| if err := marshal1(v2); err != nil { |
| return err |
| } |
| } |
| if t == '{' { |
| buf.WriteByte('}') |
| } else { |
| buf.WriteByte(']') |
| } |
| return nil |
| } |
| bytes, err := json.Marshal(v.atom) |
| if err != nil { |
| return err |
| } |
| buf.Write(bytes) |
| return nil |
| } |
| err := marshal1(v) |
| return buf.Bytes(), err |
| } |
| |
| func synthesizeSkipEvent(enc *json.Encoder, pkg, msg string) { |
| type event struct { |
| Time time.Time |
| Action string |
| Package string |
| Output string `json:",omitempty"` |
| } |
| ev := event{Time: time.Now(), Package: pkg, Action: "start"} |
| enc.Encode(ev) |
| ev.Action = "output" |
| ev.Output = msg |
| enc.Encode(ev) |
| ev.Action = "skip" |
| ev.Output = "" |
| enc.Encode(ev) |
| } |