Skip to content

Commit 8e6bb5e

Browse files
dmitshurgopherbot
authored andcommitted
cmd/coordinator/internal/dashboard: add LUCI build result support
Make some progress on the dashboard v2 package, which is cleaner¹ and easier to prototype changes in compared to the original legacydash v1 package. Notably, add support for displaying failing test results and noise results (e.g., a LUCI build with INFRA_FAILURE status). For golang/go#65913. ¹ In large part this is due to it being focused on displaying results only; it doesn't handle receiving build results and writing them to Datastore as legacydash v1 does. Change-Id: I2f0032a275dc8d41af81865dd6ec1f0ea9ef7997 Reviewed-on: https://go-review.googlesource.com/c/build/+/567576 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Michael Knyszek <[email protected]> Auto-Submit: Dmitri Shuralyov <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]>
1 parent 8d59e02 commit 8e6bb5e

File tree

5 files changed

+169
-50
lines changed

5 files changed

+169
-50
lines changed

cmd/coordinator/internal/dashboard/dashboard.html

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,14 @@ <h2 class="Dashboard-packageName">{{$.Package.Name}}</h2>
165165
{{range $a := .Archs}}
166166
<td class="Build-result {{if not $a.FirstClass}} unsupported{{end}}" data-builder="{{$a.Name}}">
167167
{{with $c.ResultForBuilder $a.Name}}
168-
{{if .OK}}
169-
<a class="Build-resultOK" href="https://build.golang.org/log/{{.LogHash}}"
170-
title="Build log of {{$a.Name}} on commit {{shortHash $c.Hash}}.">ok</a>
171-
{{end}}
168+
{{if .BuildingURL}}
169+
<a href="{{.BuildingURL}}"><img src="https://go.dev/favicon.ico" height=16 width=16 border=0></a>
170+
{{else}}
171+
<a class="Build-result{{if .OK}}OK{{else}}Fail{{end}}
172+
{{- if .Noise}} Build-resultNoise{{end}}"
173+
href="{{.LogURL}}"
174+
title="Build log of {{$a.Name}} on commit {{shortHash $c.Hash}}.">{{if .OK}}ok{{else}}fail{{end}}</a>
175+
{{end}}
172176
{{end}}
173177
</td>
174178
{{end}}

cmd/coordinator/internal/dashboard/datastore.go

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ package dashboard
99
import (
1010
"context"
1111
"errors"
12+
"fmt"
1213
"log"
1314

1415
"cloud.google.com/go/datastore"
16+
bbpb "go.chromium.org/luci/buildbucket/proto"
17+
"golang.org/x/build/cmd/coordinator/internal/lucipoll"
1518
)
1619

17-
// getDatastoreResults populates result data on commits, fetched from Datastore.
20+
// getDatastoreResults populates result data fetched from Datastore into commits.
1821
func getDatastoreResults(ctx context.Context, cl *datastore.Client, commits []*commit, pkg string) {
1922
var keys []*datastore.Key
2023
for _, c := range commits {
@@ -27,7 +30,7 @@ func getDatastoreResults(ctx context.Context, cl *datastore.Client, commits []*c
2730
out := make([]*Commit, len(keys))
2831
// datastore.ErrNoSuchEntity is returned when we ask for a commit that we do not yet have test data.
2932
if err := cl.GetMulti(ctx, keys, out); err != nil && filterMultiError(err, ignoreNoSuchEntity) != nil {
30-
log.Printf("getResults: error fetching %d results: %v", len(keys), err)
33+
log.Printf("getDatastoreResults: error fetching %d results: %v", len(keys), err)
3134
return
3235
}
3336
hashOut := make(map[string]*Commit)
@@ -41,7 +44,48 @@ func getDatastoreResults(ctx context.Context, cl *datastore.Client, commits []*c
4144
c.ResultData = result.ResultData
4245
}
4346
}
44-
return
47+
}
48+
49+
// appendLUCIResults appends result data polled from LUCI to commits.
50+
func appendLUCIResults(luci lucipoll.Snapshot, commits []*commit, repo string) {
51+
commitBuilds, ok := luci.RepoCommitBuilds[repo]
52+
if !ok {
53+
return
54+
}
55+
for _, c := range commits {
56+
builds, ok := commitBuilds[c.Hash]
57+
if !ok {
58+
// No builds for this commit.
59+
continue
60+
}
61+
for _, b := range builds {
62+
switch b.Status {
63+
case bbpb.Status_STARTED:
64+
c.ResultData = append(c.ResultData, fmt.Sprintf("%s|%s",
65+
b.BuilderName,
66+
buildURL(b.ID),
67+
))
68+
case bbpb.Status_SUCCESS, bbpb.Status_FAILURE:
69+
c.ResultData = append(c.ResultData, fmt.Sprintf("%s|%t|%s|%s",
70+
b.BuilderName,
71+
b.Status == bbpb.Status_SUCCESS,
72+
buildURL(b.ID),
73+
c.Hash,
74+
))
75+
case bbpb.Status_INFRA_FAILURE:
76+
c.ResultData = append(c.ResultData, fmt.Sprintf("%s|%s|%s|%s",
77+
b.BuilderName,
78+
"infra_failure",
79+
buildURL(b.ID),
80+
c.Hash,
81+
))
82+
}
83+
}
84+
}
85+
}
86+
87+
func buildURL(buildID int64) string {
88+
return fmt.Sprintf("https://ci.chromium.org/b/%d", buildID)
4589
}
4690

4791
type ignoreFunc func(err error) error

cmd/coordinator/internal/dashboard/handler.go

Lines changed: 90 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ import (
1616
"log"
1717
"net/http"
1818
"sort"
19+
"strconv"
1920
"strings"
2021
"time"
2122

2223
"cloud.google.com/go/datastore"
24+
"golang.org/x/build/cmd/coordinator/internal/lucipoll"
2325
"golang.org/x/build/dashboard"
2426
"golang.org/x/build/internal/releasetargets"
2527
"golang.org/x/build/maintner/maintnerd/apipb"
@@ -44,39 +46,55 @@ type MaintnerClient interface {
4446
GetDashboard(ctx context.Context, in *apipb.DashboardRequest, opts ...grpc.CallOption) (*apipb.DashboardResponse, error)
4547
}
4648

49+
type luciClient interface {
50+
PostSubmitSnapshot() lucipoll.Snapshot
51+
}
52+
4753
type Handler struct {
4854
// Datastore is a client used for fetching build status. If nil, it uses in-memory storage of build status.
4955
Datastore *datastore.Client
5056
// Maintner is a client for Maintner, used for fetching lists of commits.
5157
Maintner MaintnerClient
58+
// LUCI is a client for LUCI, used for fetching build results from there.
59+
LUCI luciClient
5260

5361
// memoryResults is an in-memory storage of CI results. Used in development and testing for datastore data.
5462
memoryResults map[string][]string
5563
}
5664

57-
func (d *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
65+
func (d *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
66+
showLUCI, _ := strconv.ParseBool(req.URL.Query().Get("showluci"))
67+
if legacyOnly, _ := strconv.ParseBool(req.URL.Query().Get("legacyonly")); legacyOnly {
68+
showLUCI = false
69+
}
70+
71+
var luci lucipoll.Snapshot
72+
if d.LUCI != nil && showLUCI {
73+
luci = d.LUCI.PostSubmitSnapshot()
74+
}
75+
5876
dd := &data{
59-
Builders: d.getBuilders(dashboard.Builders),
60-
Commits: d.commits(r.Context()),
77+
Builders: d.getBuilders(dashboard.Builders, luci),
78+
Commits: d.commits(req.Context(), luci),
6179
Package: dashPackage{Name: "Go"},
6280
}
6381

6482
var buf bytes.Buffer
6583
if err := templ.Execute(&buf, dd); err != nil {
6684
log.Printf("handleDashboard: error rendering template: %v", err)
67-
http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
85+
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
6886
return
6987
}
70-
buf.WriteTo(rw)
88+
buf.WriteTo(w)
7189
}
7290

73-
func (d *Handler) commits(ctx context.Context) []*commit {
74-
var commits []*commit
91+
func (d *Handler) commits(ctx context.Context, luci lucipoll.Snapshot) []*commit {
7592
resp, err := d.Maintner.GetDashboard(ctx, &apipb.DashboardRequest{})
7693
if err != nil {
7794
log.Printf("handleDashboard: error fetching from maintner: %v", err)
78-
return commits
95+
return nil
7996
}
97+
var commits []*commit
8098
for _, c := range resp.GetCommits() {
8199
commits = append(commits, &commit{
82100
Desc: c.Title,
@@ -85,24 +103,26 @@ func (d *Handler) commits(ctx context.Context) []*commit {
85103
User: formatGitAuthor(c.AuthorName, c.AuthorEmail),
86104
})
87105
}
88-
d.getResults(ctx, commits)
106+
d.getResults(ctx, commits, luci)
89107
return commits
90108
}
91109

92-
// getResults populates result data on commits, fetched from Datastore or in-memory storage.
93-
func (d *Handler) getResults(ctx context.Context, commits []*commit) {
94-
if d.Datastore == nil {
110+
// getResults populates result data on commits, fetched from Datastore or in-memory storage
111+
// and, if luci is non-zero, also from LUCI.
112+
func (d *Handler) getResults(ctx context.Context, commits []*commit, luci lucipoll.Snapshot) {
113+
if d.Datastore != nil {
114+
getDatastoreResults(ctx, d.Datastore, commits, "go")
115+
} else {
95116
for _, c := range commits {
96117
if result, ok := d.memoryResults[c.Hash]; ok {
97118
c.ResultData = result
98119
}
99120
}
100-
return
101121
}
102-
getDatastoreResults(ctx, d.Datastore, commits, "go")
122+
appendLUCIResults(luci, commits, "go")
103123
}
104124

105-
func (d *Handler) getBuilders(conf map[string]*dashboard.BuildConfig) []*builder {
125+
func (d *Handler) getBuilders(conf map[string]*dashboard.BuildConfig, luci lucipoll.Snapshot) []*builder {
106126
bm := make(map[string]builder)
107127
for _, b := range conf {
108128
if !b.BuildsRepoPostSubmit("go", "master", "master") {
@@ -111,12 +131,36 @@ func (d *Handler) getBuilders(conf map[string]*dashboard.BuildConfig) []*builder
111131
db := bm[b.GOOS()]
112132
db.OS = b.GOOS()
113133
db.Archs = append(db.Archs, &arch{
114-
Arch: b.GOARCH(),
134+
os: b.GOOS(), Arch: b.GOARCH(),
115135
Name: b.Name,
116-
Tag: strings.TrimPrefix(b.Name, fmt.Sprintf("%s-%s-", b.GOOS(), b.GOARCH())),
136+
// Tag is the part after "os-arch", if any, without leading dash.
137+
Tag: strings.TrimPrefix(strings.TrimPrefix(b.Name, fmt.Sprintf("%s-%s", b.GOOS(), b.GOARCH())), "-"),
117138
})
118139
bm[b.GOOS()] = db
119140
}
141+
142+
for _, b := range luci.Builders {
143+
if b.Repo != "go" || b.GoBranch != "master" {
144+
continue
145+
}
146+
db := bm[b.Target.GOOS]
147+
db.OS = b.Target.GOOS
148+
tagFriendly := b.Name + "-🐇"
149+
if after, ok := strings.CutPrefix(tagFriendly, fmt.Sprintf("gotip-%s-%s_", b.Target.GOOS, b.Target.GOARCH)); ok {
150+
// Convert os-arch_osversion-mod1-mod2 (an underscore at start of "_osversion")
151+
// to have os-arch-osversion-mod1-mod2 (a dash at start of "-osversion") form.
152+
// The tag computation below uses this to find both "osversion-mod1" or "mod1".
153+
tagFriendly = fmt.Sprintf("gotip-%s-%s-", b.Target.GOOS, b.Target.GOARCH) + after
154+
}
155+
db.Archs = append(db.Archs, &arch{
156+
os: b.Target.GOOS, Arch: b.Target.GOARCH,
157+
Name: b.Name,
158+
// Tag is the part after "os-arch", if any, without leading dash.
159+
Tag: strings.TrimPrefix(strings.TrimPrefix(tagFriendly, fmt.Sprintf("gotip-%s-%s", b.Target.GOOS, b.Target.GOARCH)), "-"),
160+
})
161+
bm[b.Target.GOOS] = db
162+
}
163+
120164
var builders builderSlice
121165
for _, db := range bm {
122166
db := db
@@ -128,18 +172,12 @@ func (d *Handler) getBuilders(conf map[string]*dashboard.BuildConfig) []*builder
128172
}
129173

130174
type arch struct {
131-
Arch string
132-
Name string
133-
Tag string
175+
os, Arch string
176+
Name string
177+
Tag string
134178
}
135179

136-
func (a arch) FirstClass() bool {
137-
segs := strings.SplitN(a.Name, "-", 3)
138-
if len(segs) < 2 {
139-
return false
140-
}
141-
return releasetargets.IsFirstClass(segs[0], segs[1])
142-
}
180+
func (a arch) FirstClass() bool { return releasetargets.IsFirstClass(a.os, a.Arch) }
143181

144182
type archSlice []*arch
145183

@@ -217,8 +255,13 @@ type dashPackage struct {
217255
}
218256

219257
type commit struct {
220-
Desc string
221-
Hash string
258+
Desc string
259+
Hash string
260+
// ResultData is a copy of the [Commit.ResultData] field from datastore,
261+
// with an additional rule that the second '|'-separated value may be "infra_failure"
262+
// to indicate a problem with the infrastructure rather than the code being tested.
263+
//
264+
// It can also have the form of "builder|BuildingURL" for in progress builds.
222265
ResultData []string
223266
Time string
224267
User string
@@ -236,28 +279,43 @@ func (c *commit) ShortUser() string {
236279
return user
237280
}
238281

239-
func (c *commit) ResultForBuilder(builder string) result {
282+
func (c *commit) ResultForBuilder(builder string) *result {
240283
for _, rd := range c.ResultData {
241284
segs := strings.Split(rd, "|")
285+
if len(segs) == 2 && segs[0] == builder {
286+
return &result{
287+
BuildingURL: segs[1],
288+
}
289+
}
242290
if len(segs) < 4 {
243291
continue
244292
}
245293
if segs[0] == builder {
246-
return result{
294+
return &result{
247295
OK: segs[1] == "true",
296+
Noise: segs[1] == "infra_failure",
248297
LogHash: segs[2],
249298
}
250299
}
251300
}
252-
return result{}
301+
return nil
253302
}
254303

255304
type result struct {
256305
BuildingURL string
257306
OK bool
307+
Noise bool
258308
LogHash string
259309
}
260310

311+
func (r result) LogURL() string {
312+
if strings.HasPrefix(r.LogHash, "https://") {
313+
return r.LogHash
314+
} else {
315+
return "https://build.golang.org/log/" + r.LogHash
316+
}
317+
}
318+
261319
// formatGitAuthor formats the git author name and email (as split by
262320
// maintner) back into the unified string how they're stored in a git
263321
// commit, so the shortUser func (used by the HTML template) can parse

0 commit comments

Comments
 (0)