| OLD | NEW |
| (Empty) |
| 1 // Copyright 2016 The LUCI Authors. All rights reserved. | |
| 2 // Use of this source code is governed under the Apache License, Version 2.0 | |
| 3 // that can be found in the LICENSE file. | |
| 4 | |
| 5 package buildbot | |
| 6 | |
| 7 import ( | |
| 8 "encoding/json" | |
| 9 "errors" | |
| 10 "fmt" | |
| 11 "io/ioutil" | |
| 12 "math" | |
| 13 "path/filepath" | |
| 14 "regexp" | |
| 15 "sort" | |
| 16 "strconv" | |
| 17 "strings" | |
| 18 "time" | |
| 19 | |
| 20 "golang.org/x/net/context" | |
| 21 | |
| 22 "github.com/luci/gae/service/datastore" | |
| 23 "github.com/luci/luci-go/common/data/stringset" | |
| 24 "github.com/luci/luci-go/common/logging" | |
| 25 "github.com/luci/luci-go/milo/api/resp" | |
| 26 "github.com/luci/luci-go/milo/common/model" | |
| 27 ) | |
| 28 | |
| 29 var errBuildNotFound = errors.New("Build not found") | |
| 30 | |
| 31 // getBuild fetches a buildbot build from the datastore and checks ACLs. | |
| 32 // The return code matches the master responses. | |
| 33 func getBuild(c context.Context, master, builder string, buildNum int) (*buildbo
tBuild, error) { | |
| 34 result := &buildbotBuild{ | |
| 35 Master: master, | |
| 36 Buildername: builder, | |
| 37 Number: buildNum, | |
| 38 } | |
| 39 | |
| 40 err := datastore.Get(c, result) | |
| 41 err = checkAccess(c, err, result.Internal) | |
| 42 if err == errMasterNotFound { | |
| 43 err = errBuildNotFound | |
| 44 } | |
| 45 | |
| 46 return result, err | |
| 47 } | |
| 48 | |
| 49 // result2Status translates a buildbot result integer into a model.Status. | |
| 50 func result2Status(s *int) (status model.Status) { | |
| 51 if s == nil { | |
| 52 return model.Running | |
| 53 } | |
| 54 switch *s { | |
| 55 case 0: | |
| 56 status = model.Success | |
| 57 case 1: | |
| 58 status = model.Warning | |
| 59 case 2: | |
| 60 status = model.Failure | |
| 61 case 3: | |
| 62 status = model.NotRun // Skipped | |
| 63 case 4: | |
| 64 status = model.Exception | |
| 65 case 5: | |
| 66 status = model.WaitingDependency // Retry | |
| 67 default: | |
| 68 panic(fmt.Errorf("Unknown status %d", s)) | |
| 69 } | |
| 70 return | |
| 71 } | |
| 72 | |
| 73 // buildbotTimeToTime converts a buildbot time representation (pointer to float | |
| 74 // of seconds since epoch) to a native time.Time object. | |
| 75 func buildbotTimeToTime(t *float64) (result time.Time) { | |
| 76 if t != nil { | |
| 77 result = time.Unix(int64(*t), int64(*t*1e9)%1e9).UTC() | |
| 78 } | |
| 79 return | |
| 80 } | |
| 81 | |
| 82 // parseTimes translates a buildbot time tuple (start, end) into a triplet | |
| 83 // of (Started time, Ending time, duration). | |
| 84 // If times[1] is nil and buildFinished is not, ended will be set to buildFinish
ed | |
| 85 // time. | |
| 86 func parseTimes(buildFinished *float64, times []*float64) (started, ended time.T
ime, duration time.Duration) { | |
| 87 if len(times) != 2 { | |
| 88 panic(fmt.Errorf("Expected 2 floats for times, got %v", times)) | |
| 89 } | |
| 90 if times[0] == nil { | |
| 91 // Some steps don't have timing info. In that case, just return
nils. | |
| 92 return | |
| 93 } | |
| 94 started = buildbotTimeToTime(times[0]) | |
| 95 switch { | |
| 96 case times[1] != nil: | |
| 97 ended = buildbotTimeToTime(times[1]) | |
| 98 duration = ended.Sub(started) | |
| 99 case buildFinished != nil: | |
| 100 ended = buildbotTimeToTime(buildFinished) | |
| 101 duration = ended.Sub(started) | |
| 102 default: | |
| 103 duration = time.Since(started) | |
| 104 } | |
| 105 return | |
| 106 } | |
| 107 | |
| 108 // getBanner parses the OS information from the build and maybe returns a banner
. | |
| 109 func getBanner(c context.Context, b *buildbotBuild) *resp.LogoBanner { | |
| 110 logging.Infof(c, "OS: %s/%s", b.OSFamily, b.OSVersion) | |
| 111 osLogo := func() *resp.Logo { | |
| 112 result := &resp.Logo{} | |
| 113 switch b.OSFamily { | |
| 114 case "windows": | |
| 115 result.LogoBase = resp.Windows | |
| 116 case "Darwin": | |
| 117 result.LogoBase = resp.OSX | |
| 118 case "Debian": | |
| 119 result.LogoBase = resp.Ubuntu | |
| 120 default: | |
| 121 return nil | |
| 122 } | |
| 123 result.Subtitle = b.OSVersion | |
| 124 return result | |
| 125 }() | |
| 126 if osLogo != nil { | |
| 127 return &resp.LogoBanner{ | |
| 128 OS: []resp.Logo{*osLogo}, | |
| 129 } | |
| 130 } | |
| 131 logging.Warningf(c, "No OS info found.") | |
| 132 return nil | |
| 133 } | |
| 134 | |
| 135 // summary extracts the top level summary from a buildbot build as a | |
| 136 // BuildComponent | |
| 137 func summary(c context.Context, b *buildbotBuild) resp.BuildComponent { | |
| 138 // TODO(hinoka): use b.toStatus() | |
| 139 // Status | |
| 140 var status model.Status | |
| 141 if b.Currentstep != nil { | |
| 142 status = model.Running | |
| 143 } else { | |
| 144 status = result2Status(b.Results) | |
| 145 } | |
| 146 | |
| 147 // Timing info | |
| 148 started, ended, duration := parseTimes(nil, b.Times) | |
| 149 | |
| 150 // Link to bot and original build. | |
| 151 host := "build.chromium.org/p" | |
| 152 if b.Internal { | |
| 153 host = "uberchromegw.corp.google.com/i" | |
| 154 } | |
| 155 bot := resp.NewLink( | |
| 156 b.Slave, | |
| 157 fmt.Sprintf("https://%s/%s/buildslaves/%s", host, b.Master, b.Sl
ave), | |
| 158 ) | |
| 159 source := resp.NewLink( | |
| 160 fmt.Sprintf("%s/%s/%d", b.Master, b.Buildername, b.Number), | |
| 161 fmt.Sprintf("https://%s/%s/builders/%s/builds/%d", | |
| 162 host, b.Master, b.Buildername, b.Number), | |
| 163 ) | |
| 164 | |
| 165 // The link to the builder page. | |
| 166 parent := resp.NewLink(b.Buildername, ".") | |
| 167 | |
| 168 // Do a best effort lookup for the bot information to fill in OS/Platfor
m info. | |
| 169 banner := getBanner(c, b) | |
| 170 | |
| 171 sum := resp.BuildComponent{ | |
| 172 ParentLabel: parent, | |
| 173 Label: fmt.Sprintf("#%d", b.Number), | |
| 174 Banner: banner, | |
| 175 Status: status, | |
| 176 Started: started, | |
| 177 Finished: ended, | |
| 178 Bot: bot, | |
| 179 Source: source, | |
| 180 Duration: duration, | |
| 181 Type: resp.Summary, // This is more or less ignored. | |
| 182 LevelsDeep: 1, | |
| 183 Text: []string{}, // Status messages. Eg "This build fai
led on..xyz" | |
| 184 } | |
| 185 | |
| 186 return sum | |
| 187 } | |
| 188 | |
| 189 var rLineBreak = regexp.MustCompile("<br */?>") | |
| 190 | |
| 191 // components takes a full buildbot build struct and extract step info from all | |
| 192 // of the steps and returns it as a list of milo Build Components. | |
| 193 func components(b *buildbotBuild) (result []*resp.BuildComponent) { | |
| 194 endingTime := b.Times[1] | |
| 195 for _, step := range b.Steps { | |
| 196 if step.Hidden == true { | |
| 197 continue | |
| 198 } | |
| 199 bc := &resp.BuildComponent{ | |
| 200 Label: step.Name, | |
| 201 } | |
| 202 // Step text sometimes contains <br>, which we want to parse int
o new lines. | |
| 203 for _, t := range step.Text { | |
| 204 for _, line := range rLineBreak.Split(t, -1) { | |
| 205 bc.Text = append(bc.Text, line) | |
| 206 } | |
| 207 } | |
| 208 | |
| 209 // Figure out the status. | |
| 210 if !step.IsStarted { | |
| 211 bc.Status = model.NotRun | |
| 212 } else if !step.IsFinished { | |
| 213 bc.Status = model.Running | |
| 214 } else { | |
| 215 if len(step.Results) > 0 { | |
| 216 status := int(step.Results[0].(float64)) | |
| 217 bc.Status = result2Status(&status) | |
| 218 } else { | |
| 219 bc.Status = model.Success | |
| 220 } | |
| 221 } | |
| 222 | |
| 223 // Raise the interesting-ness if the step is not "Success". | |
| 224 if bc.Status != model.Success { | |
| 225 bc.Verbosity = resp.Interesting | |
| 226 } | |
| 227 | |
| 228 remainingAliases := stringset.New(len(step.Aliases)) | |
| 229 for linkAnchor := range step.Aliases { | |
| 230 remainingAliases.Add(linkAnchor) | |
| 231 } | |
| 232 | |
| 233 getLinksWithAliases := func(logLink *resp.Link, isLog bool) resp
.LinkSet { | |
| 234 // Generate alias links. | |
| 235 var aliases resp.LinkSet | |
| 236 if remainingAliases.Del(logLink.Label) { | |
| 237 stepAliases := step.Aliases[logLink.Label] | |
| 238 aliases = make(resp.LinkSet, len(stepAliases)) | |
| 239 for i, alias := range stepAliases { | |
| 240 aliases[i] = alias.toLink() | |
| 241 } | |
| 242 } | |
| 243 | |
| 244 // Step log link takes primary, with aliases as secondar
y. | |
| 245 links := make(resp.LinkSet, 1, 1+len(aliases)) | |
| 246 links[0] = logLink | |
| 247 | |
| 248 for _, a := range aliases { | |
| 249 a.Alias = true | |
| 250 } | |
| 251 return append(links, aliases...) | |
| 252 } | |
| 253 | |
| 254 for _, l := range step.Logs { | |
| 255 logLink := resp.NewLink(l[0], l[1]) | |
| 256 | |
| 257 links := getLinksWithAliases(logLink, true) | |
| 258 if logLink.Label == "stdio" { | |
| 259 bc.MainLink = links | |
| 260 } else { | |
| 261 bc.SubLink = append(bc.SubLink, links) | |
| 262 } | |
| 263 } | |
| 264 | |
| 265 // Step links are stored as maps of name: url | |
| 266 // Because Go doesn't believe in nice things, we now create anot
her array | |
| 267 // just so that we can iterate through this map in order. | |
| 268 names := make([]string, 0, len(step.Urls)) | |
| 269 for name := range step.Urls { | |
| 270 names = append(names, name) | |
| 271 } | |
| 272 sort.Strings(names) | |
| 273 for _, name := range names { | |
| 274 logLink := resp.NewLink(name, step.Urls[name]) | |
| 275 | |
| 276 bc.SubLink = append(bc.SubLink, getLinksWithAliases(logL
ink, false)) | |
| 277 } | |
| 278 | |
| 279 // Add any unused aliases directly. | |
| 280 if remainingAliases.Len() > 0 { | |
| 281 unusedAliases := remainingAliases.ToSlice() | |
| 282 sort.Strings(unusedAliases) | |
| 283 | |
| 284 for _, label := range unusedAliases { | |
| 285 var baseLink resp.LinkSet | |
| 286 for _, alias := range step.Aliases[label] { | |
| 287 aliasLink := alias.toLink() | |
| 288 if len(baseLink) == 0 { | |
| 289 aliasLink.Label = label | |
| 290 } else { | |
| 291 aliasLink.Alias = true | |
| 292 } | |
| 293 baseLink = append(baseLink, aliasLink) | |
| 294 } | |
| 295 | |
| 296 if len(baseLink) > 0 { | |
| 297 bc.SubLink = append(bc.SubLink, baseLink
) | |
| 298 } | |
| 299 } | |
| 300 } | |
| 301 | |
| 302 // Figure out the times. | |
| 303 bc.Started, bc.Finished, bc.Duration = parseTimes(endingTime, st
ep.Times) | |
| 304 | |
| 305 result = append(result, bc) | |
| 306 } | |
| 307 return | |
| 308 } | |
| 309 | |
| 310 // parseProp returns a string representation of v. | |
| 311 func parseProp(v interface{}) string { | |
| 312 // if v is a whole number, force it into an int. json.Marshal() would t
urn | |
| 313 // it into what looks like a float instead. We want this to remain and | |
| 314 // int instead of a number. | |
| 315 if vf, ok := v.(float64); ok { | |
| 316 if math.Floor(vf) == vf { | |
| 317 return fmt.Sprintf("%d", int64(vf)) | |
| 318 } | |
| 319 } | |
| 320 // return the json representation of the value. | |
| 321 b, err := json.Marshal(v) | |
| 322 if err == nil { | |
| 323 return string(b) | |
| 324 } | |
| 325 return fmt.Sprintf("%v", v) | |
| 326 } | |
| 327 | |
| 328 // Prop is a struct used to store a value and group so that we can make a map | |
| 329 // of key:Prop to pass into parseProp() for the purpose of cross referencing | |
| 330 // one prop while working on another. | |
| 331 type Prop struct { | |
| 332 Value interface{} | |
| 333 Group string | |
| 334 } | |
| 335 | |
| 336 // properties extracts all properties from buildbot builds and groups them into | |
| 337 // property groups. | |
| 338 func properties(b *buildbotBuild) (result []*resp.PropertyGroup) { | |
| 339 groups := map[string]*resp.PropertyGroup{} | |
| 340 allProps := map[string]Prop{} | |
| 341 for _, prop := range b.Properties { | |
| 342 allProps[prop.Name] = Prop{ | |
| 343 Value: prop.Value, | |
| 344 Group: prop.Source, | |
| 345 } | |
| 346 } | |
| 347 for key, prop := range allProps { | |
| 348 value := prop.Value | |
| 349 groupName := prop.Group | |
| 350 if _, ok := groups[groupName]; !ok { | |
| 351 groups[groupName] = &resp.PropertyGroup{GroupName: group
Name} | |
| 352 } | |
| 353 vs := parseProp(value) | |
| 354 groups[groupName].Property = append(groups[groupName].Property,
&resp.Property{ | |
| 355 Key: key, | |
| 356 Value: vs, | |
| 357 }) | |
| 358 } | |
| 359 // Insert the groups into a list in alphabetical order. | |
| 360 // You have to make a separate sorting data structure because Go doesn't
like | |
| 361 // sorting things for you. | |
| 362 groupNames := []string{} | |
| 363 for n := range groups { | |
| 364 groupNames = append(groupNames, n) | |
| 365 } | |
| 366 sort.Strings(groupNames) | |
| 367 for _, k := range groupNames { | |
| 368 group := groups[k] | |
| 369 // Also take this oppertunity to sort the properties within the
groups. | |
| 370 sort.Sort(group) | |
| 371 result = append(result, group) | |
| 372 } | |
| 373 return | |
| 374 } | |
| 375 | |
| 376 // blame extracts the commit and blame information from a buildbot build and | |
| 377 // returns it as a list of Commits. | |
| 378 func blame(b *buildbotBuild) (result []*resp.Commit) { | |
| 379 for _, c := range b.Sourcestamp.Changes { | |
| 380 files := c.GetFiles() | |
| 381 result = append(result, &resp.Commit{ | |
| 382 AuthorEmail: c.Who, | |
| 383 Repo: c.Repository, | |
| 384 CommitTime: time.Unix(int64(c.When), 0).UTC(), | |
| 385 Revision: resp.NewLink(c.Revision, c.Revlink), | |
| 386 Description: c.Comments, | |
| 387 Title: strings.Split(c.Comments, "\n")[0], | |
| 388 File: files, | |
| 389 }) | |
| 390 } | |
| 391 return | |
| 392 } | |
| 393 | |
| 394 // sourcestamp extracts the source stamp from various parts of a buildbot build, | |
| 395 // including the properties. | |
| 396 func sourcestamp(c context.Context, b *buildbotBuild) *resp.SourceStamp { | |
| 397 ss := &resp.SourceStamp{} | |
| 398 rietveld := "" | |
| 399 gerrit := "" | |
| 400 got_revision := "" | |
| 401 repository := "" | |
| 402 issue := int64(-1) | |
| 403 for _, prop := range b.Properties { | |
| 404 switch prop.Name { | |
| 405 case "rietveld": | |
| 406 if v, ok := prop.Value.(string); ok { | |
| 407 rietveld = v | |
| 408 } else { | |
| 409 logging.Warningf(c, "Field rietveld is not a str
ing: %#v", prop.Value) | |
| 410 } | |
| 411 case "issue": | |
| 412 // Sometime this is a number (float), sometime it is a s
tring. | |
| 413 if v, ok := prop.Value.(float64); ok { | |
| 414 issue = int64(v) | |
| 415 } else if v, ok := prop.Value.(string); ok { | |
| 416 if vi, err := strconv.ParseInt(v, 10, 64); err =
= nil { | |
| 417 issue = int64(vi) | |
| 418 } else { | |
| 419 logging.Warningf(c, "Could not decode fi
eld issue: %q - %s", prop.Value, err) | |
| 420 } | |
| 421 } else { | |
| 422 logging.Warningf(c, "Field issue is not a string
or float: %#v", prop.Value) | |
| 423 } | |
| 424 | |
| 425 case "got_revision": | |
| 426 if v, ok := prop.Value.(string); ok { | |
| 427 got_revision = v | |
| 428 } else { | |
| 429 logging.Warningf(c, "Field got_revision is not a
string: %#v", prop.Value) | |
| 430 } | |
| 431 | |
| 432 case "patch_issue": | |
| 433 if v, ok := prop.Value.(float64); ok { | |
| 434 issue = int64(v) | |
| 435 } else { | |
| 436 logging.Warningf(c, "Field patch_issue is not a
float: %#v", prop.Value) | |
| 437 } | |
| 438 | |
| 439 case "patch_gerrit_url": | |
| 440 if v, ok := prop.Value.(string); ok { | |
| 441 gerrit = v | |
| 442 } else { | |
| 443 logging.Warningf(c, "Field gerrit is not a strin
g: %#v", prop.Value) | |
| 444 } | |
| 445 | |
| 446 case "repository": | |
| 447 if v, ok := prop.Value.(string); ok { | |
| 448 repository = v | |
| 449 } | |
| 450 } | |
| 451 } | |
| 452 if issue != -1 { | |
| 453 switch { | |
| 454 case rietveld != "": | |
| 455 rietveld = strings.TrimRight(rietveld, "/") | |
| 456 ss.Changelist = resp.NewLink( | |
| 457 fmt.Sprintf("Rietveld CL %d", issue), | |
| 458 fmt.Sprintf("%s/%d", rietveld, issue)) | |
| 459 case gerrit != "": | |
| 460 gerrit = strings.TrimRight(gerrit, "/") | |
| 461 ss.Changelist = resp.NewLink( | |
| 462 fmt.Sprintf("Gerrit CL %d", issue), | |
| 463 fmt.Sprintf("%s/c/%d", gerrit, issue)) | |
| 464 } | |
| 465 } | |
| 466 | |
| 467 if got_revision != "" { | |
| 468 ss.Revision = resp.NewLink(got_revision, "") | |
| 469 if repository != "" { | |
| 470 ss.Revision.URL = repository + "/+/" + got_revision | |
| 471 } | |
| 472 } | |
| 473 return ss | |
| 474 } | |
| 475 | |
| 476 func renderBuild(c context.Context, b *buildbotBuild) *resp.MiloBuild { | |
| 477 // Modify the build for rendering. | |
| 478 updatePostProcessBuild(b) | |
| 479 | |
| 480 // TODO(hinoka): Do all fields concurrently. | |
| 481 return &resp.MiloBuild{ | |
| 482 SourceStamp: sourcestamp(c, b), | |
| 483 Summary: summary(c, b), | |
| 484 Components: components(b), | |
| 485 PropertyGroup: properties(b), | |
| 486 Blame: blame(b), | |
| 487 } | |
| 488 } | |
| 489 | |
| 490 // DebugBuild fetches a debugging build for testing. | |
| 491 func DebugBuild(c context.Context, relBuildbotDir string, builder string, buildN
um int) (*resp.MiloBuild, error) { | |
| 492 fname := fmt.Sprintf("%s.%d.json", builder, buildNum) | |
| 493 // ../buildbot below assumes that | |
| 494 // - this code is not executed by tests outside of this dir | |
| 495 // - this dir is a sibling of frontend dir | |
| 496 path := filepath.Join(relBuildbotDir, "testdata", fname) | |
| 497 raw, err := ioutil.ReadFile(path) | |
| 498 if err != nil { | |
| 499 return nil, err | |
| 500 } | |
| 501 b := &buildbotBuild{} | |
| 502 if err := json.Unmarshal(raw, b); err != nil { | |
| 503 return nil, err | |
| 504 } | |
| 505 return renderBuild(c, b), nil | |
| 506 } | |
| 507 | |
| 508 // Build fetches a buildbot build and translates it into a miloBuild. | |
| 509 func Build(c context.Context, master, builder string, buildNum int) (*resp.MiloB
uild, error) { | |
| 510 b, err := getBuild(c, master, builder, buildNum) | |
| 511 if err != nil { | |
| 512 return nil, err | |
| 513 } | |
| 514 return renderBuild(c, b), nil | |
| 515 } | |
| 516 | |
| 517 // updatePostProcessBuild transforms a build from its raw JSON format into the | |
| 518 // format that should be presented to users. | |
| 519 // | |
| 520 // Post-processing includes: | |
| 521 // - If the build is LogDog-only, promotes aliases (LogDog links) to | |
| 522 // first-class links in the build. | |
| 523 func updatePostProcessBuild(b *buildbotBuild) { | |
| 524 // If this is a LogDog-only build, we want to promote the LogDog links. | |
| 525 if loc, ok := b.getPropertyValue("log_location").(string); ok && strings
.HasPrefix(loc, "logdog://") { | |
| 526 linkMap := map[string]string{} | |
| 527 for sidx := range b.Steps { | |
| 528 promoteLogDogLinks(&b.Steps[sidx], sidx == 0, linkMap) | |
| 529 } | |
| 530 | |
| 531 // Update "Logs". This field is part of BuildBot, and is the ama
lgamation | |
| 532 // of all logs in the build's steps. Since each log is out of co
ntext of its | |
| 533 // original step, we can't apply the promotion logic; instead, w
e will use | |
| 534 // the link map to map any old URLs that were matched in "promot
eLogDogLnks" | |
| 535 // to their new URLs. | |
| 536 for _, link := range b.Logs { | |
| 537 // "link" is in the form: [NAME, URL] | |
| 538 if len(link) != 2 { | |
| 539 continue | |
| 540 } | |
| 541 | |
| 542 if newURL, ok := linkMap[link[1]]; ok { | |
| 543 link[1] = newURL | |
| 544 } | |
| 545 } | |
| 546 } | |
| 547 } | |
| 548 | |
| 549 // promoteLogDogLinks updates the links in a BuildBot step to | |
| 550 // promote LogDog links. | |
| 551 // | |
| 552 // A build's links come in one of three forms: | |
| 553 // - Log Links, which link directly to BuildBot build logs. | |
| 554 // - URL Links, which are named links to arbitrary URLs. | |
| 555 // - Aliases, which attach to the label in one of the other types of links
and | |
| 556 // augment it with additional named links. | |
| 557 // | |
| 558 // LogDog uses aliases exclusively to attach LogDog logs to other links. When | |
| 559 // the build is LogDog-only, though, the original links are actually junk. What | |
| 560 // we want to do is remove the original junk links and replace them with their | |
| 561 // alias counterparts, so that the "natural" BuildBot links are actually LogDog | |
| 562 // links. | |
| 563 // | |
| 564 // As URLs are re-mapped, the supplied "linkMap" will be updated to map the old | |
| 565 // URLs to the new ones. | |
| 566 func promoteLogDogLinks(s *buildbotStep, isInitialStep bool, linkMap map[string]
string) { | |
| 567 type stepLog struct { | |
| 568 label string | |
| 569 url string | |
| 570 } | |
| 571 | |
| 572 remainingAliases := stringset.New(len(s.Aliases)) | |
| 573 for linkAnchor := range s.Aliases { | |
| 574 remainingAliases.Add(linkAnchor) | |
| 575 } | |
| 576 | |
| 577 maybePromoteAliases := func(sl *stepLog, isLog bool) []*stepLog { | |
| 578 // As a special case, if this is the first step ("steps" in Buil
dBot), we | |
| 579 // will refrain from promoting aliases for "stdio", since "stdio
" represents | |
| 580 // the raw BuildBot logs. | |
| 581 if isLog && isInitialStep && sl.label == "stdio" { | |
| 582 // No aliases, don't modify this log. | |
| 583 return []*stepLog{sl} | |
| 584 } | |
| 585 | |
| 586 // If there are no aliases, we should obviously not promote them
. This will | |
| 587 // be the case for pre-LogDog steps such as build setup. | |
| 588 aliases := s.Aliases[sl.label] | |
| 589 if len(aliases) == 0 { | |
| 590 return []*stepLog{sl} | |
| 591 } | |
| 592 | |
| 593 // We have chosen to promote the aliases. Therefore, we will not
include | |
| 594 // them as aliases in the modified step. | |
| 595 remainingAliases.Del(sl.label) | |
| 596 | |
| 597 result := make([]*stepLog, len(aliases)) | |
| 598 for i, alias := range aliases { | |
| 599 aliasStepLog := stepLog{alias.Text, alias.URL} | |
| 600 | |
| 601 // Any link named "logdog" (Annotee cosmetic implementat
ion detail) will | |
| 602 // inherit the name of the original log. | |
| 603 if isLog { | |
| 604 if aliasStepLog.label == "logdog" { | |
| 605 aliasStepLog.label = sl.label | |
| 606 } | |
| 607 } | |
| 608 | |
| 609 result[i] = &aliasStepLog | |
| 610 } | |
| 611 | |
| 612 // If we performed mapping, add the OLD -> NEW URL mapping to li
nkMap. | |
| 613 // | |
| 614 // Since multpiple aliases can apply to a single log, and we hav
e to pick | |
| 615 // one, here, we'll arbitrarily pick the last one. This is maybe
more | |
| 616 // consistent than the first one because linkMap, itself, will e
nd up | |
| 617 // holding the last mapping for any given URL. | |
| 618 if len(result) > 0 { | |
| 619 linkMap[sl.url] = result[len(result)-1].url | |
| 620 } | |
| 621 | |
| 622 return result | |
| 623 } | |
| 624 | |
| 625 // Update step logs. | |
| 626 newLogs := make([][]string, 0, len(s.Logs)) | |
| 627 for _, l := range s.Logs { | |
| 628 for _, res := range maybePromoteAliases(&stepLog{l[0], l[1]}, tr
ue) { | |
| 629 newLogs = append(newLogs, []string{res.label, res.url}) | |
| 630 } | |
| 631 } | |
| 632 s.Logs = newLogs | |
| 633 | |
| 634 // Update step URLs. | |
| 635 newURLs := make(map[string]string, len(s.Urls)) | |
| 636 for label, link := range s.Urls { | |
| 637 urlLinks := maybePromoteAliases(&stepLog{label, link}, false) | |
| 638 if len(urlLinks) > 0 { | |
| 639 // Use the last URL link, since our URL map can only tol
erate one link. | |
| 640 // The expected case here is that len(urlLinks) == 1, th
ough, but it's | |
| 641 // possible that multiple aliases can be included for a
single URL, so | |
| 642 // we need to handle that. | |
| 643 newValue := urlLinks[len(urlLinks)-1] | |
| 644 newURLs[newValue.label] = newValue.url | |
| 645 } else { | |
| 646 newURLs[label] = link | |
| 647 } | |
| 648 } | |
| 649 s.Urls = newURLs | |
| 650 | |
| 651 // Preserve any aliases that haven't been promoted. | |
| 652 var newAliases map[string][]*buildbotLinkAlias | |
| 653 if l := remainingAliases.Len(); l > 0 { | |
| 654 newAliases = make(map[string][]*buildbotLinkAlias, l) | |
| 655 remainingAliases.Iter(func(v string) bool { | |
| 656 newAliases[v] = s.Aliases[v] | |
| 657 return true | |
| 658 }) | |
| 659 } | |
| 660 s.Aliases = newAliases | |
| 661 } | |
| OLD | NEW |