]> Cypherpunks repositories - gostls13.git/commitdiff
testing: add Attr
authorDamien Neil <dneil@google.com>
Thu, 3 Apr 2025 00:37:34 +0000 (17:37 -0700)
committerGopher Robot <gobot@golang.org>
Wed, 21 May 2025 22:37:35 +0000 (15:37 -0700)
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 <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
Auto-Submit: Damien Neil <dneil@google.com>

api/next/43936.txt [new file with mode: 0644]
doc/next/6-stdlib/99-minor/testing/43936.md [new file with mode: 0644]
src/cmd/internal/test2json/test2json.go
src/cmd/internal/test2json/testdata/attr.json [new file with mode: 0644]
src/cmd/internal/test2json/testdata/attr.test [new file with mode: 0644]
src/testing/testing.go
src/testing/testing_test.go

diff --git a/api/next/43936.txt b/api/next/43936.txt
new file mode 100644 (file)
index 0000000..e32bd75
--- /dev/null
@@ -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 (file)
index 0000000..5be98d5
--- /dev/null
@@ -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
+```
index ed78764d26791f304edeb0bcf9f3aafcdbcde192..d08ef389f82a211b097d5e3fdab75c9cc1bda042 100644 (file)
@@ -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 (file)
index 0000000..9d7b019
--- /dev/null
@@ -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 (file)
index 0000000..6ec9f11
--- /dev/null
@@ -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
index 13f19a2a2281ebfe045852aa2e5978f5c19eb9ef..85ac1aeb329e6c1c8b216fc4c508d751f31461c1 100644 (file)
@@ -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
 
index 209291d3228532e25bd2718b47e315b413d89e34..f4f5817a37ddb37a69915cfae62100a9ebd49a9c 100644 (file)
@@ -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"))