diff options
Diffstat (limited to 'src/cmd/dist/testjson.go')
-rw-r--r-- | src/cmd/dist/testjson.go | 204 |
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) +} |