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 <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
Auto-Submit: Damien Neil <dneil@google.com>
This commit is contained in:
Damien Neil 2025-04-02 17:37:34 -07:00 committed by Gopher Robot
parent 763963505e
commit 3cc8b532f9
7 changed files with 117 additions and 0 deletions

4
api/next/43936.txt Normal file
View file

@ -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

View file

@ -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
```

View 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

View file

@ -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"}

View file

@ -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

View 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

View 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"))