From: Damien Neil Date: Thu, 3 Apr 2025 00:37:34 +0000 (-0700) Subject: testing: add Attr X-Git-Tag: go1.25rc1~97 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=3cc8b532f9d561397dd0c66496e1e1a82667c926;p=gostls13.git testing: add Attr Add a new Attr method to testing.TB that emits a test attribute. An attribute is an arbitrary key/value pair. Fixes #43936 Change-Id: I7ef299efae41f2cf39f2dc61ad4cdd4c3975cdb6 Reviewed-on: https://go-review.googlesource.com/c/go/+/662437 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan Auto-Submit: Damien Neil --- diff --git a/api/next/43936.txt b/api/next/43936.txt new file mode 100644 index 0000000000..e32bd75ed9 --- /dev/null +++ b/api/next/43936.txt @@ -0,0 +1,4 @@ +pkg testing, method (*B) Attr(string, string) #43936 +pkg testing, method (*F) Attr(string, string) #43936 +pkg testing, method (*T) Attr(string, string) #43936 +pkg testing, type TB interface, Attr(string, string) #43936 diff --git a/doc/next/6-stdlib/99-minor/testing/43936.md b/doc/next/6-stdlib/99-minor/testing/43936.md new file mode 100644 index 0000000000..5be98d5db8 --- /dev/null +++ b/doc/next/6-stdlib/99-minor/testing/43936.md @@ -0,0 +1,10 @@ +The new methods [T.Attr], [B.Attr], and [F.Attr] emit an +attribute to the test log. An attribute is an arbitrary +key and value associated with a test. + +For example, in a test named `TestAttr`, +`t.Attr("key", "value")` emits: + +``` +=== ATTR TestAttr key value +``` diff --git a/src/cmd/internal/test2json/test2json.go b/src/cmd/internal/test2json/test2json.go index ed78764d26..d08ef389f8 100644 --- a/src/cmd/internal/test2json/test2json.go +++ b/src/cmd/internal/test2json/test2json.go @@ -36,6 +36,8 @@ type event struct { Elapsed *float64 `json:",omitempty"` Output *textBytes `json:",omitempty"` FailedBuild string `json:",omitempty"` + Key string `json:",omitempty"` + Value string `json:",omitempty"` } // textBytes is a hack to get JSON to emit a []byte as a string @@ -177,6 +179,7 @@ var ( []byte("=== PASS "), []byte("=== FAIL "), []byte("=== SKIP "), + []byte("=== ATTR "), } reports = [][]byte{ @@ -333,6 +336,11 @@ func (c *Converter) handleInputLine(line []byte) { c.output.write(origLine) return } + if action == "attr" { + var rest string + name, rest, _ = strings.Cut(name, " ") + e.Key, e.Value, _ = strings.Cut(rest, " ") + } // === update. // Finish any pending PASS/FAIL reports. c.needMarker = sawMarker diff --git a/src/cmd/internal/test2json/testdata/attr.json b/src/cmd/internal/test2json/testdata/attr.json new file mode 100644 index 0000000000..9d7b0195ba --- /dev/null +++ b/src/cmd/internal/test2json/testdata/attr.json @@ -0,0 +1,15 @@ +{"Action":"start"} +{"Action":"run","Test":"TestAttr"} +{"Action":"output","Test":"TestAttr","Output":"=== RUN TestAttr\n"} +{"Action":"attr","Test":"TestAttr","Key":"key","Value":"value"} +{"Action":"output","Test":"TestAttr","Output":"=== ATTR TestAttr key value\n"} +{"Action":"run","Test":"TestAttr/sub"} +{"Action":"output","Test":"TestAttr/sub","Output":"=== RUN TestAttr/sub\n"} +{"Action":"attr","Test":"TestAttr/sub","Key":"key","Value":"value"} +{"Action":"output","Test":"TestAttr/sub","Output":"=== ATTR TestAttr/sub key value\n"} +{"Action":"output","Test":"TestAttr","Output":"--- PASS: TestAttr (0.00s)\n"} +{"Action":"output","Test":"TestAttr/sub","Output":" --- PASS: TestAttr/sub (0.00s)\n"} +{"Action":"pass","Test":"TestAttr/sub"} +{"Action":"pass","Test":"TestAttr"} +{"Action":"output","Output":"PASS\n"} +{"Action":"pass"} diff --git a/src/cmd/internal/test2json/testdata/attr.test b/src/cmd/internal/test2json/testdata/attr.test new file mode 100644 index 0000000000..6ec9f116ed --- /dev/null +++ b/src/cmd/internal/test2json/testdata/attr.test @@ -0,0 +1,7 @@ +=== RUN TestAttr +=== ATTR TestAttr key value +=== RUN TestAttr/sub +=== ATTR TestAttr/sub key value +--- PASS: TestAttr (0.00s) + --- PASS: TestAttr/sub (0.00s) +PASS diff --git a/src/testing/testing.go b/src/testing/testing.go index 13f19a2a22..85ac1aeb32 100644 --- a/src/testing/testing.go +++ b/src/testing/testing.go @@ -879,6 +879,7 @@ func fmtDuration(d time.Duration) string { // TB is the interface common to [T], [B], and [F]. type TB interface { + Attr(key, value string) Cleanup(func()) Error(args ...any) Errorf(format string, args ...any) @@ -1491,6 +1492,31 @@ func (c *common) Context() context.Context { return c.ctx } +// Attr emits a test attribute associated with this test. +// +// The key must not contain whitespace. +// The value must not contain newlines or carriage returns. +// +// The meaning of different attribute keys is left up to +// continuous integration systems and test frameworks. +// +// Test attributes are emitted immediately in the test log, +// but they are intended to be treated as unordered. +func (c *common) Attr(key, value string) { + if strings.ContainsFunc(key, unicode.IsSpace) { + c.Errorf("disallowed whitespace in attribute key %q", key) + return + } + if strings.ContainsAny(value, "\r\n") { + c.Errorf("disallowed newline in attribute value %q", value) + return + } + if c.chatty == nil { + return + } + c.chatty.Updatef(c.name, "=== ATTR %s %v %v\n", c.name, key, value) +} + // panicHandling controls the panic handling used by runCleanup. type panicHandling int diff --git a/src/testing/testing_test.go b/src/testing/testing_test.go index 209291d322..f4f5817a37 100644 --- a/src/testing/testing_test.go +++ b/src/testing/testing_test.go @@ -975,6 +975,53 @@ func TestContext(t *testing.T) { }) } +// TestAttrExample is used by TestAttrSet, +// and also serves as a convenient test to run that sets an attribute. +func TestAttrExample(t *testing.T) { + t.Attr("key", "value") +} + +func TestAttrSet(t *testing.T) { + out := string(runTest(t, "TestAttrExample")) + + want := "=== ATTR TestAttrExample key value\n" + if !strings.Contains(out, want) { + t.Errorf("expected output containing %q, got:\n%q", want, out) + } +} + +func TestAttrInvalid(t *testing.T) { + tests := []struct { + key string + value string + }{ + {"k ey", "value"}, + {"k\tey", "value"}, + {"k\rey", "value"}, + {"k\ney", "value"}, + {"key", "val\rue"}, + {"key", "val\nue"}, + } + + if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" { + for i, test := range tests { + t.Run(fmt.Sprint(i), func(t *testing.T) { + t.Attr(test.key, test.value) + }) + } + return + } + + out := string(runTest(t, "TestAttrInvalid")) + + for i := range tests { + want := fmt.Sprintf("--- FAIL: TestAttrInvalid/%v ", i) + if !strings.Contains(out, want) { + t.Errorf("expected output containing %q, got:\n%q", want, out) + } + } +} + func TestBenchmarkBLoopIterationCorrect(t *testing.T) { out := runTest(t, "BenchmarkBLoopPrint") c := bytes.Count(out, []byte("Printing from BenchmarkBLoopPrint"))