diff options
| author | Ethan Reesor <ethan.reesor@gmail.com> | 2026-03-05 14:20:45 -0600 |
|---|---|---|
| committer | Damien Neil <dneil@google.com> | 2026-03-06 15:06:52 -0800 |
| commit | b9545da71c2f5e93355d82a1f9b5ead02f2bc617 (patch) | |
| tree | c67871247d5d8b5e44c9a13186b0a65b722595a9 /src/testing | |
| parent | 90b428ebf565f61a8ef13d2b6a59c55704923c74 (diff) | |
| download | go-b9545da71c2f5e93355d82a1f9b5ead02f2bc617.tar.xz | |
testing: escapes framing markers
Uses `^[` to escape the framing marker `^V` used to delimit test output. A test
that itself executes a go test binary, or otherwise emits that control
character, previously would corrupt the test2json parse of the enclosing run.
Updates #62728.
Change-Id: I0e8790a05fd7af469cd7ee2e8ccc13786cc372dc
Reviewed-on: https://go-review.googlesource.com/c/go/+/751940
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Michael Matloob <matloob@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
Diffstat (limited to 'src/testing')
| -rw-r--r-- | src/testing/sub_test.go | 19 | ||||
| -rw-r--r-- | src/testing/testing.go | 49 |
2 files changed, 61 insertions, 7 deletions
diff --git a/src/testing/sub_test.go b/src/testing/sub_test.go index 5d5573ccec..fedb0a052d 100644 --- a/src/testing/sub_test.go +++ b/src/testing/sub_test.go @@ -222,6 +222,10 @@ func TestTRun(t *T) { sub_test.go:NNN: fail ^V--- FAIL: chatty with recursion and json/#00/#02 (N.NNs) ^V=== NAME chatty with recursion and json/#00 +^V=== RUN chatty with recursion and json/#00/#03 + sub_test.go:NNN: ^[^V^[^[ +^V--- PASS: chatty with recursion and json/#00/#03 (N.NNs) +^V=== NAME chatty with recursion and json/#00 ^V--- FAIL: chatty with recursion and json/#00 (N.NNs) ^V=== NAME chatty with recursion and json ^V--- FAIL: chatty with recursion and json (N.NNs) @@ -231,6 +235,7 @@ func TestTRun(t *T) { t.Run("", func(t *T) {}) t.Run("", func(t *T) { t.Skip("skip") }) t.Run("", func(t *T) { t.Fatal("fail") }) + t.Run("", func(t *T) { t.Log(string(markFraming) + string(markEscape)) }) }) }, }, { @@ -629,7 +634,7 @@ func TestTRun(t *T) { want := strings.TrimSpace(tc.output) re := makeRegexp(want) if ok, err := regexp.MatchString(re, got); !ok || err != nil { - t.Errorf("%s:output:\ngot:\n%s\nwant:\n%s", tc.desc, got, want) + t.Errorf("%s:output:\ngot:\n%s\nwant:\n%s", tc.desc, notateOutput(got), want) } }) } @@ -823,14 +828,24 @@ func TestBRun(t *T) { } } +// makeRegexp transforms a line in the text notation to a pattern. func makeRegexp(s string) string { s = regexp.QuoteMeta(s) - s = strings.ReplaceAll(s, "^V", "\x16") + s = strings.ReplaceAll(s, "^V", string(markFraming)) + s = strings.ReplaceAll(s, "^\\[", string(markEscape)) s = strings.ReplaceAll(s, ":NNN:", `:\d\d\d\d?:`) s = strings.ReplaceAll(s, "N\\.NNs", `\d*\.\d*s`) return s } +// notateOutput transforms an output line into something more easily comparable +// to text notation. +func notateOutput(s string) string { + s = strings.ReplaceAll(s, string(markFraming), "^V") + s = strings.ReplaceAll(s, string(markEscape), "^[") + return s +} + func TestBenchmarkOutput(t *T) { // Ensure Benchmark initialized common.w by invoking it with an error and // normal case. diff --git a/src/testing/testing.go b/src/testing/testing.go index bf95f1cfbb..6cce1a451f 100644 --- a/src/testing/testing.go +++ b/src/testing/testing.go @@ -562,11 +562,14 @@ func (f *chattyFlag) Get() any { return f.on } -const marker = byte(0x16) // ^V for framing +const ( + markFraming byte = 'V' &^ '@' // ^V: framing + markEscape byte = '[' &^ '@' // ^[: escape +) func (f *chattyFlag) prefix() string { if f.json { - return string(marker) + return string(markFraming) } return "" } @@ -588,7 +591,7 @@ func newChattyPrinter(w io.Writer) *chattyPrinter { // that as not in json mode (because it's not chatty at all). func (p *chattyPrinter) prefix() string { if p != nil && p.json { - return string(marker) + return string(markFraming) } return "" } @@ -869,8 +872,8 @@ func (w indenter) Write(b []byte) (n int, err error) { // An indent of 4 spaces will neatly align the dashes with the status // indicator of the parent. line := b[:end] - if line[0] == marker { - w.c.output = append(w.c.output, marker) + if line[0] == markFraming { + w.c.output = append(w.c.output, markFraming) line = line[1:] } w.c.output = append(w.c.output, indent...) @@ -1166,6 +1169,9 @@ func (o *outputWriter) Write(p []byte) (int, error) { // writeLine generates the output for a given line. func (o *outputWriter) writeLine(b []byte) { if !o.c.done && (o.c.chatty != nil) { + // Escape the framing marker. + b = escapeMarkers(b) + if o.c.bench { // Benchmarks don't print === CONT, so we should skip the test // printer and just print straight to stdout. @@ -1179,6 +1185,39 @@ func (o *outputWriter) writeLine(b []byte) { o.c.output = append(o.c.output, b...) } +func escapeMarkers(b []byte) []byte { + j := nextMark(b) + if j < 0 { + // Allocation-free fast path. + return b + } + + c := make([]byte, 0, len(b)+10) + i := 0 + for i < len(b) && j >= i { + if j > i { + c = append(c, b[i:j]...) + } + c = append(c, markEscape, b[j]) + i = j + 1 + j = i + nextMark(b[i:]) + } + if i < len(b) { + c = append(c, b[i:]...) + } + return c +} + +func nextMark(b []byte) int { + for i, b := range b { + switch b { + case markFraming, markEscape: + return i + } + } + return -1 +} + // Log formats its arguments using default formatting, analogous to [fmt.Println], // and records the text in the error log. For tests, the text will be printed only if // the test fails or the -test.v flag is set. For benchmarks, the text is always |
