aboutsummaryrefslogtreecommitdiff
path: root/src/testing
diff options
context:
space:
mode:
authorEthan Reesor <ethan.reesor@gmail.com>2026-03-05 14:20:45 -0600
committerDamien Neil <dneil@google.com>2026-03-06 15:06:52 -0800
commitb9545da71c2f5e93355d82a1f9b5ead02f2bc617 (patch)
treec67871247d5d8b5e44c9a13186b0a65b722595a9 /src/testing
parent90b428ebf565f61a8ef13d2b6a59c55704923c74 (diff)
downloadgo-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.go19
-rw-r--r--src/testing/testing.go49
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