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>
--- /dev/null
+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
--- /dev/null
+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
+```
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
[]byte("=== PASS "),
[]byte("=== FAIL "),
[]byte("=== SKIP "),
+ []byte("=== ATTR "),
}
reports = [][]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
--- /dev/null
+{"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"}
--- /dev/null
+=== RUN TestAttr
+=== ATTR TestAttr key value
+=== RUN TestAttr/sub
+=== ATTR TestAttr/sub key value
+--- PASS: TestAttr (0.00s)
+ --- PASS: TestAttr/sub (0.00s)
+PASS
// 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)
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
})
}
+// 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"))