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  }