summaryrefslogtreecommitdiffstats
path: root/src/cmd/dist/testjson.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/dist/testjson.go')
-rw-r--r--src/cmd/dist/testjson.go204
1 files changed, 204 insertions, 0 deletions
diff --git a/src/cmd/dist/testjson.go b/src/cmd/dist/testjson.go
new file mode 100644
index 0000000..6204593
--- /dev/null
+++ b/src/cmd/dist/testjson.go
@@ -0,0 +1,204 @@
+// 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)
+}