go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/frontend/ui/build_legacy.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 //go:generate stringer -type=ComponentType 16 17 package ui 18 19 import ( 20 "context" 21 "encoding/json" 22 "regexp" 23 "strings" 24 "time" 25 26 "go.chromium.org/luci/common/clock" 27 "go.chromium.org/luci/milo/internal/model/milostatus" 28 ) 29 30 // MiloBuildLegacy denotes a full renderable Milo build page. 31 // This is slated to be deprecated in April 2019 after the BuildBot turndown. 32 // This is to be replaced by a new BuildPage struct, 33 // which encapsulates a Buildbucket Build Proto. 34 type MiloBuildLegacy struct { 35 // Summary is a top level summary of the page. 36 Summary BuildComponent 37 38 // Trigger gives information about how and with what information 39 // the build was triggered. 40 Trigger *Trigger 41 42 // Components is a detailed list of components and subcomponents of the page. 43 // This is most often used for steps or deps. 44 Components []*BuildComponent 45 46 // PropertyGroup is a list of input and output property of this page. 47 // This is used for build and emitted properties (buildbot) and quest 48 // information (luci). This is also grouped by the "type" of property 49 // so different categories of properties can be separated and sorted. 50 // 51 // This is not a map so code that constructs MiloBuildLegacy can control the 52 // order of property groups, for example show important properties 53 // first. 54 PropertyGroup []*PropertyGroup 55 56 // Blame is a list of people and commits that is likely to be in relation to 57 // the thing displayed on this page. 58 Blame []*Commit 59 60 // Mode to render the steps. 61 StepDisplayPref StepDisplayPref 62 63 // Iff true, show all log links whose name starts with '$'. 64 ShowDebugLogsPref bool 65 } 66 67 var statusPrecendence = map[milostatus.Status]int{ 68 milostatus.Canceled: 0, 69 milostatus.Expired: 1, 70 milostatus.Exception: 2, 71 milostatus.InfraFailure: 3, 72 milostatus.Failure: 4, 73 milostatus.Warning: 5, 74 milostatus.Success: 6, 75 } 76 77 // fixComponent fixes possible display inconsistencies with the build, including: 78 // 79 // * If the build is complete, all open steps should be closed. 80 // * Parent steps containing failed steps should also be marked as failed. 81 // * Components' Collapsed field is set based on StepDisplayPref field. 82 // * Parent step name prefix is trimmed from the nested substeps. 83 // * Enforce correct values for StepDisplayPref (set to Collapsed if incorrect). 84 func fixComponent(comp *BuildComponent, buildFinished time.Time, stripPrefix string, collapseGreen bool) { 85 // If the build is finished but the step is not finished. 86 if !buildFinished.IsZero() && comp.ExecutionTime.Finished.IsZero() { 87 // Then set the finish time to be the same as the build finish time. 88 comp.ExecutionTime.Finished = buildFinished 89 comp.ExecutionTime.Duration = buildFinished.Sub(comp.ExecutionTime.Started) 90 comp.Status = milostatus.InfraFailure 91 } 92 93 // Fix substeps recursively. 94 for _, substep := range comp.Children { 95 fixComponent( 96 substep, buildFinished, comp.Label.String()+".", collapseGreen) 97 } 98 99 // When parent step finishes running, compute its final status as worst 100 // status, as determined by statusPrecendence map above, among direct children 101 // and its own status. 102 if comp.Status.Terminal() { 103 for _, substep := range comp.Children { 104 substepStatusPrecedence, ok := statusPrecendence[substep.Status] 105 if ok && substepStatusPrecedence < statusPrecendence[comp.Status] { 106 comp.Status = substep.Status 107 } 108 } 109 } 110 111 comp.Collapsed = collapseGreen && comp.Status == milostatus.Success 112 113 // Strip parent component name from the title. 114 if comp.Label != nil { 115 comp.Label.Label = strings.TrimPrefix(comp.Label.Label, stripPrefix) 116 } 117 } 118 119 var ( 120 farFutureTime = time.Date(2038, time.January, 19, 3, 14, 07, 0, time.UTC) 121 farPastTime = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC) 122 ) 123 124 // fixComponentDuration makes all parent steps have the max duration of all 125 // of its children. 126 func fixComponentDuration(c context.Context, comp *BuildComponent) Interval { 127 // Leaf nodes do not require fixing. 128 if len(comp.Children) == 0 { 129 return comp.ExecutionTime 130 } 131 // Set start and end times to be out of bounds. 132 // Each variable can have 3 states: 133 // 1. Undefined. In which case they're set to farFuture/PastTime 134 // 2. Current (fin only). In which case it's set to zero time (time.Time{}) 135 // 3. Definite. In which case it's set to a time that isn't either of the above. 136 start := farFutureTime 137 fin := farPastTime 138 for _, subcomp := range comp.Children { 139 i := fixComponentDuration(c, subcomp) 140 if i.Started.Before(start) { 141 start = i.Started 142 } 143 switch { 144 case fin.IsZero(): 145 continue // fin is current, it can't get any farther in the future than that. 146 case i.Finished.IsZero(), i.Finished.After(fin): 147 fin = i.Finished // Both of these cased are considered "after". 148 } 149 } 150 comp.ExecutionTime = NewInterval(c, start, fin) 151 return comp.ExecutionTime 152 } 153 154 // Fix fixes various inconsistencies that users expect to see as part of the 155 // Build, but didn't make sense as part of the individual components, including: 156 // * If the build is complete, all open steps should be closed. 157 // * Parent steps containing failed steps should also be marked as failed. 158 // * Components' Collapsed field is set based on StepDisplayPref field. 159 // * Parent step name prefix is trimmed from the nested substeps. 160 // * Enforce correct values for StepDisplayPref (set to Default if incorrect). 161 // * Set parent step durations to be the combination of all children. 162 func (b *MiloBuildLegacy) Fix(c context.Context) { 163 if b.StepDisplayPref != StepDisplayExpanded && b.StepDisplayPref != StepDisplayNonGreen { 164 b.StepDisplayPref = StepDisplayDefault 165 } 166 167 for _, comp := range b.Components { 168 fixComponent( 169 comp, b.Summary.ExecutionTime.Finished, "", 170 b.StepDisplayPref == StepDisplayDefault) 171 // Run duration fixing after component fixing to make sure all of the 172 // end times are set correctly. 173 fixComponentDuration(c, comp) 174 } 175 } 176 177 // Trigger is the combination of pointing to a single commit, with information 178 // about where that commit came from (e.g. the repository), and the project 179 // that triggered it. 180 type Trigger struct { 181 Commit 182 // Source is the trigger source. In buildbot, this would be the "Reason". 183 // This has no meaning in SwarmBucket and DM yet. 184 Source string 185 // Project is the name of the LUCI project responsible for 186 // triggering the build. 187 Project string 188 } 189 190 // Property specifies k/v pair representing some 191 // sort of property, such as buildbot property, quest property, etc. 192 type Property struct { 193 Key string 194 Value string 195 } 196 197 // PropertyGroup is a cluster of similar properties. In buildbot land this would be the "source". 198 // This is a way to segregate different types of properties such as Quest properties, 199 // swarming properties, emitted properties, revision properties, etc. 200 type PropertyGroup struct { 201 GroupName string 202 Property []*Property 203 } 204 205 func (p PropertyGroup) Len() int { return len(p.Property) } 206 func (p PropertyGroup) Swap(i, j int) { 207 p.Property[i], p.Property[j] = p.Property[j], p.Property[i] 208 } 209 func (p PropertyGroup) Less(i, j int) bool { return p.Property[i].Key < p.Property[j].Key } 210 211 // BuildProgress is a way to show progress. Percent should always be specified. 212 type BuildProgress struct { 213 // The total number of entries. Shows up as a tooltip. Leave at 0 to 214 // disable the tooltip. 215 total uint64 216 217 // The number of entries completed. Shows up as <progress> / <total>. 218 progress uint64 219 220 // A number between 0 to 100 denoting the percentage completed. 221 percent uint32 222 } 223 224 // ComponentType is the type of build component. 225 type ComponentType int 226 227 const ( 228 // Recipe corresponds to a full recipe run. Dependencies are recipes. 229 Recipe ComponentType = iota 230 231 // StepLegacy is a single step of a recipe. 232 StepLegacy 233 234 // Summary denotes that this does not pretain to any particular step. 235 Summary 236 ) 237 238 // MarshalJSON renders enums into String rather than an int when marshalling. 239 func (c ComponentType) MarshalJSON() ([]byte, error) { 240 return json.Marshal(c.String()) 241 } 242 243 // LogoBanner is a banner of logos that define the OS and devices that a 244 // component is associated with. 245 type LogoBanner struct { 246 OS []Logo 247 Device []Logo 248 } 249 250 // Interval is a time interval which has a start, an end and a duration. 251 type Interval struct { 252 // Started denotes the start time of this interval. 253 Started time.Time 254 // Finished denotest the end time of this interval. 255 Finished time.Time 256 // Duration is the length of the interval. 257 Duration time.Duration 258 } 259 260 // NewInterval returns a new interval struct. If end time is empty (eg. Not completed) 261 // set end time to empty but set duration to the difference between start and now. 262 // Getting called with an empty start time and non-empty end time is undefined. 263 func NewInterval(c context.Context, start, end time.Time) Interval { 264 i := Interval{Started: start, Finished: end} 265 if start.IsZero() { 266 return i 267 } 268 if end.IsZero() { 269 i.Duration = clock.Now(c).Sub(start) 270 } else { 271 i.Duration = end.Sub(start) 272 } 273 return i 274 } 275 276 // BuildComponent represents a single Step, subsetup, attempt, or recipe. 277 type BuildComponent struct { 278 // The parent of this component. For buildbot and swarmbucket builds, this 279 // refers to the builder. For DM, this refers to whatever triggered the Quest. 280 ParentLabel *Link `json:",omitempty"` 281 282 // The main label for the component. 283 Label *Link 284 285 // Status of the build. 286 Status milostatus.Status 287 288 // Banner is a banner of logos that define the OS and devices this 289 // component is associated with. 290 Banner *LogoBanner `json:",omitempty"` 291 292 // Bot is the machine or execution instance that this component ran on. 293 Bot *Link `json:",omitempty"` 294 295 // Recipe is a link to the recipe this component is based on. 296 Recipe *Link `json:",omitempty"` 297 298 // Source is a link to the external (buildbot, swarming, dm, etc) data 299 // source that this component relates to. 300 Source *Link `json:",omitempty"` 301 302 // Links to show adjacent to the main label. 303 MainLink LinkSet `json:",omitempty"` 304 305 // Links to show right below the main label. Top-level slice are rows of 306 // links, second level shows up as 307 SubLink []LinkSet `json:",omitempty"` 308 309 // Designates the progress of the current component. Set null for no progress. 310 Progress *BuildProgress `json:",omitempty"` 311 312 // Pending is time interval that this build was pending. 313 PendingTime Interval 314 315 // Execution is time interval that this build was executing. 316 ExecutionTime Interval 317 318 // The type of component. This manifests itself as a little label on the 319 // top left corner of the component. 320 // This is either "RECIPE" or "STEP". An attempt is considered a recipe. 321 Type ComponentType 322 323 // Arbitrary text to display below links. One line per entry, 324 // newlines are stripped. 325 Text []string 326 327 // Children of the step. Undefined for other types of build components. 328 Children []*BuildComponent 329 330 // Render a step as collapsed or expanded. Undefined for other types of build 331 // components. 332 Collapsed bool 333 } 334 335 var rLineBreak = regexp.MustCompile("<br */?>") 336 337 // TextBR returns Text, but also splits each line by <br> 338 func (bc *BuildComponent) TextBR() []string { 339 var result []string 340 for _, t := range bc.Text { 341 result = append(result, rLineBreak.Split(t, -1)...) 342 } 343 return result 344 } 345 346 // Navigation is the top bar of the page, used for navigating out of the page. 347 type Navigation struct { 348 // Title of the site, is generally consistent throughout the whole site. 349 SiteTitle *Link 350 351 // Title of the page, usually talks about what's currently here 352 PageTitle *Link 353 354 // List of clickable thing to navigate down the hierarchy. 355 Breadcrumbs []*Link 356 357 // List of (smaller) clickable things to display on the right side. 358 Right []*Link 359 } 360 361 // LinkSet is an ordered collection of Link objects that will be rendered on the 362 // same line. 363 type LinkSet []*Link 364 365 // IsDebugLink returns true iff the first link in the linkset starts with "$". 366 func (l LinkSet) IsDebugLink() bool { 367 if len(l) > 0 { 368 return strings.HasPrefix(l[0].Label, "$") 369 } 370 return false 371 }