cmd/coordinator: collapse duplicate execution headers

Following CL 371474, cmd/dist may include a "Test execution environment"
header which describes the test execution environment.

When sharding tests, we want to surface the proper execution environment
for each test, which may vary from shard to shard.

We could simply print the metadata for each shard in its output, as
shard results are printed in an atomic block. However, shards are pretty
small and that adds quite a bit of noise to logs.

Instead, treat the metadata block like the test header banners: as long
as the metadata doesn't change, we don't need to print it again.

On the other hand, if the metadata changes, we do print the test header
banner again. This isn't strictly necessary, it just serves to improve
readability by ensuring that tests are always immediately preceeded by
their banner rather than metadata (in the case that metadata changes in
the middle of a header block).

This CL should be submitted and deployed before CL 371474.

For golang/go#50146.

Change-Id: Ifca30f7f31237fd8cd0fcd801d198d9c341f695e
Reviewed-on: https://go-review.googlesource.com/c/build/+/372538
Trust: Michael Pratt <mpratt@google.com>
Run-TryBot: Michael Pratt <mpratt@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Alex Rakoczy <alex@golang.org>
This commit is contained in:
Michael Pratt 2021-12-15 16:20:59 -05:00
Родитель 8ddb22a865
Коммит 2ea69a3ac4
2 изменённых файлов: 168 добавлений и 25 удалений

Просмотреть файл

@ -1496,6 +1496,7 @@ func (st *buildStatus) runTests(helpers <-chan buildlet.Client) (remoteErr, err
close(buildletsGone) close(buildletsGone)
}() }()
var lastMetadata string
var lastHeader string var lastHeader string
var serialDuration time.Duration var serialDuration time.Duration
for _, ti := range set.items { for _, ti := range set.items {
@ -1516,9 +1517,24 @@ func (st *buildStatus) runTests(helpers <-chan buildlet.Client) (remoteErr, err
serialDuration += ti.execDuration serialDuration += ti.execDuration
if len(ti.output) > 0 { if len(ti.output) > 0 {
header, out := parseOutputAndHeader(ti.output) metadata, header, out := parseOutputAndHeader(ti.output)
printHeader := false
if metadata != lastMetadata {
lastMetadata = metadata
fmt.Fprintf(st, "\n%s\n", metadata)
// Always include the test header after
// metadata changes. This is a readability
// optimization that ensures that tests are
// always immediately preceeded by their test
// banner, even if it is duplicate banner
// because the test metadata changed.
printHeader = true
}
if header != lastHeader { if header != lastHeader {
lastHeader = header lastHeader = header
printHeader = true
}
if printHeader {
fmt.Fprintf(st, "\n%s\n", header) fmt.Fprintf(st, "\n%s\n", header)
} }
if pool.NewGCEConfiguration().InStaging() { if pool.NewGCEConfiguration().InStaging() {
@ -1551,31 +1567,63 @@ const (
banner = "XXXBANNERXXX:" // flag passed to dist banner = "XXXBANNERXXX:" // flag passed to dist
bannerPrefix = "\n" + banner // with the newline added by dist bannerPrefix = "\n" + banner // with the newline added by dist
metadataBannerPrefix = bannerPrefix + "Test execution environment."
outputBanner = "##### " // banner to display in output. outputBanner = "##### " // banner to display in output.
) )
var bannerPrefixBytes = []byte(bannerPrefix) var (
bannerPrefixBytes = []byte(bannerPrefix)
metadataBannerPrefixBytes = []byte(metadataBannerPrefix)
)
// parseOutputAndHeader parses b and returns the test display header (e.g., // parseOutputAndHeader parses b and returns the test (optional) environment
// "##### Testing packages.") and the following output. // metaadata, display header (e.g., "##### Testing packages.") and the
func parseOutputAndHeader(b []byte) (header string, out []byte) { // following output.
//
// metadata is the optional execution environment metadata block. e.g.,
//
// ##### Test execution environment.
// # GOARCH: amd64
// # CPU: Intel(R) Xeon(R) W-2135 CPU @ 3.70GHz
func parseOutputAndHeader(b []byte) (metadata, header string, out []byte) {
if !bytes.HasPrefix(b, bannerPrefixBytes) { if !bytes.HasPrefix(b, bannerPrefixBytes) {
return "", b return "", "", b
} }
b = b[1:] // skip newline if bytes.HasPrefix(b, metadataBannerPrefixBytes) {
// Header includes everything up to and including the next
// banner.
rem := b[len(metadataBannerPrefixBytes):]
i := bytes.Index(rem, bannerPrefixBytes)
if i == -1 {
// Metadata block without a following block doesn't
// make sense. Bail.
return "", "", b
}
bi := i + len(metadataBannerPrefixBytes)
// Metadata portion of header, skipping initial and trailing newlines.
metadata = strings.Trim(string(b[:bi]), "\n")
metadata = strings.Replace(metadata, banner, outputBanner, 1)
b = b[bi+1:] // skip newline at start of next banner.
} else {
b = b[1:] // skip newline
}
// Find end of primary test banner.
nl := bytes.IndexByte(b, '\n') nl := bytes.IndexByte(b, '\n')
if nl == -1 { if nl == -1 {
// No newline, everything is header.
header = string(b) header = string(b)
b = nil b = nil
} else { } else {
header = string(b[:nl]) header = string(b[:nl])
b = b[nl+1:] b = b[nl+1:]
} }
// Replace internal marker banner with the human-friendly
// version. // Replace internal marker banner with the human-friendly version.
header = strings.ReplaceAll(header, banner, outputBanner) header = strings.Replace(header, banner, outputBanner, 1)
return header, b return metadata, header, b
} }
// maxTestExecError is the number of test execution failures at which // maxTestExecError is the number of test execution failures at which

Просмотреть файл

@ -15,10 +15,11 @@ import (
// TestParseOutputAndHeader tests header parsing by parseOutputAndHeader. // TestParseOutputAndHeader tests header parsing by parseOutputAndHeader.
func TestParseOutputAndHeader(t *testing.T) { func TestParseOutputAndHeader(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
name string name string
input []byte input []byte
wantHeader string wantMetadata string
wantOut []byte wantHeader string
wantOut []byte
}{ }{
{ {
name: "standard", name: "standard",
@ -28,7 +29,8 @@ ok archive/tar 0.015s
ok archive/zip 0.406s ok archive/zip 0.406s
ok bufio 0.075s ok bufio 0.075s
`), `),
wantHeader: "##### Testing packages.", wantMetadata: "",
wantHeader: "##### Testing packages.",
wantOut: []byte(`ok archive/tar 0.015s wantOut: []byte(`ok archive/tar 0.015s
ok archive/zip 0.406s ok archive/zip 0.406s
ok bufio 0.075s ok bufio 0.075s
@ -39,15 +41,17 @@ ok bufio 0.075s
input: []byte(` input: []byte(`
XXXBANNERXXX:Testing packages. XXXBANNERXXX:Testing packages.
`), `),
wantHeader: "##### Testing packages.", wantMetadata: "",
wantOut: []byte(``), wantHeader: "##### Testing packages.",
wantOut: []byte(``),
}, },
{ {
name: "header only missing trailing newline", name: "header only missing trailing newline",
input: []byte(` input: []byte(`
XXXBANNERXXX:Testing packages.`), XXXBANNERXXX:Testing packages.`),
wantHeader: "##### Testing packages.", wantMetadata: "",
wantOut: []byte(``), wantHeader: "##### Testing packages.",
wantOut: []byte(``),
}, },
{ {
name: "no banner", name: "no banner",
@ -55,7 +59,8 @@ XXXBANNERXXX:Testing packages.`),
ok archive/zip 0.406s ok archive/zip 0.406s
ok bufio 0.075s ok bufio 0.075s
`), `),
wantHeader: "", wantMetadata: "",
wantHeader: "",
wantOut: []byte(`ok archive/tar 0.015s wantOut: []byte(`ok archive/tar 0.015s
ok archive/zip 0.406s ok archive/zip 0.406s
ok bufio 0.075s ok bufio 0.075s
@ -68,7 +73,8 @@ ok archive/tar 0.015s
ok archive/zip 0.406s ok archive/zip 0.406s
ok bufio 0.075s ok bufio 0.075s
`), `),
wantHeader: "", wantMetadata: "",
wantHeader: "",
wantOut: []byte(`XXXBANNERXXX:Testing packages. wantOut: []byte(`XXXBANNERXXX:Testing packages.
ok archive/tar 0.015s ok archive/tar 0.015s
ok archive/zip 0.406s ok archive/zip 0.406s
@ -83,7 +89,8 @@ ok archive/tar 0.015s
ok archive/zip 0.406s ok archive/zip 0.406s
ok bufio 0.075s ok bufio 0.075s
`), `),
wantHeader: "", wantMetadata: "",
wantHeader: "",
wantOut: []byte(` wantOut: []byte(`
##### Testing packages. ##### Testing packages.
ok archive/tar 0.015s ok archive/tar 0.015s
@ -91,11 +98,99 @@ ok archive/zip 0.406s
ok bufio 0.075s ok bufio 0.075s
`), `),
}, },
{
name: "metadata",
input: []byte(`
XXXBANNERXXX:Test execution environment.
# GOARCH: amd64
# CPU: Intel(R) Xeon(R) W-2135 CPU @ 3.70GHz
XXXBANNERXXX:Testing packages.
ok archive/tar 0.015s
ok archive/zip 0.406s
ok bufio 0.075s
`),
wantMetadata: `##### Test execution environment.
# GOARCH: amd64
# CPU: Intel(R) Xeon(R) W-2135 CPU @ 3.70GHz`,
wantHeader: "##### Testing packages.",
wantOut: []byte(`ok archive/tar 0.015s
ok archive/zip 0.406s
ok bufio 0.075s
`),
},
{
name: "metadata missing separator newline",
input: []byte(`
XXXBANNERXXX:Test execution environment.
# GOARCH: amd64
# CPU: Intel(R) Xeon(R) W-2135 CPU @ 3.70GHz
XXXBANNERXXX:Testing packages.
ok archive/tar 0.015s
ok archive/zip 0.406s
ok bufio 0.075s
`),
wantMetadata: `##### Test execution environment.
# GOARCH: amd64
# CPU: Intel(R) Xeon(R) W-2135 CPU @ 3.70GHz`,
wantHeader: "##### Testing packages.",
wantOut: []byte(`ok archive/tar 0.015s
ok archive/zip 0.406s
ok bufio 0.075s
`),
},
{
name: "metadata missing second banner",
input: []byte(`
XXXBANNERXXX:Test execution environment.
# GOARCH: amd64
# CPU: Intel(R) Xeon(R) W-2135 CPU @ 3.70GHz
`),
wantMetadata: "",
wantHeader: "",
wantOut: []byte(`
XXXBANNERXXX:Test execution environment.
# GOARCH: amd64
# CPU: Intel(R) Xeon(R) W-2135 CPU @ 3.70GHz
`),
},
{
name: "metadata missing body",
input: []byte(`
XXXBANNERXXX:Test execution environment.
# GOARCH: amd64
# CPU: Intel(R) Xeon(R) W-2135 CPU @ 3.70GHz
XXXBANNERXXX:Testing packages.
`),
wantMetadata: `##### Test execution environment.
# GOARCH: amd64
# CPU: Intel(R) Xeon(R) W-2135 CPU @ 3.70GHz`,
wantHeader: "##### Testing packages.",
wantOut: []byte(``),
},
{
name: "metadata missing body and newline",
input: []byte(`
XXXBANNERXXX:Test execution environment.
# GOARCH: amd64
# CPU: Intel(R) Xeon(R) W-2135 CPU @ 3.70GHz
XXXBANNERXXX:Testing packages.`),
wantMetadata: `##### Test execution environment.
# GOARCH: amd64
# CPU: Intel(R) Xeon(R) W-2135 CPU @ 3.70GHz`,
wantHeader: "##### Testing packages.",
wantOut: []byte(``),
},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
gotHeader, gotOut := parseOutputAndHeader(tc.input) gotMetadata, gotHeader, gotOut := parseOutputAndHeader(tc.input)
if gotMetadata != tc.wantMetadata {
t.Errorf("parseOutputAndBanner(%q) got metadata %q want metadata %q", string(tc.input), gotMetadata, tc.wantMetadata)
}
if gotHeader != tc.wantHeader { if gotHeader != tc.wantHeader {
t.Errorf("parseOutputAndBanner(%q) got banner %q want banner %q", string(tc.input), gotHeader, tc.wantHeader) t.Errorf("parseOutputAndBanner(%q) got header %q want header %q", string(tc.input), gotHeader, tc.wantHeader)
} }
if string(gotOut) != string(tc.wantOut) { if string(gotOut) != string(tc.wantOut) {
t.Errorf("parseOutputAndBanner(%q) got out %q want out %q", string(tc.input), string(gotOut), string(tc.wantOut)) t.Errorf("parseOutputAndBanner(%q) got out %q want out %q", string(tc.input), string(gotOut), string(tc.wantOut))