blob: 8fcb776484efcedb3ef2b7a05a657fe9f612fe35 [file] [log] [blame]
package display
/*
The display package provides utilities for displaying Task Drivers in a
human-friendly way.
*/
import (
"fmt"
"sort"
"strings"
"time"
multierror "github.com/hashicorp/go-multierror"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/task_driver/go/db"
"go.skia.org/infra/task_driver/go/td"
)
const (
ELLIPSES = "..."
MAX_ERROR_CHARS = 1000
MAX_ERROR_LINES = 20
)
// StepDisplay represents one step in a single run of a Task Driver.
type StepDisplay struct {
*td.StepProperties
Result td.StepResult `json:"result,omitempty"`
Errors []string `json:"errors,omitempty" go2ts:"ignorenil"`
Started time.Time `json:"started,omitempty"`
Finished time.Time `json:"finished,omitempty"`
Data []*db.StepData `json:"data,omitempty" go2ts:"ignorenil"`
Steps []*StepDisplay `json:"steps,omitempty" go2ts:"ignorenil"`
}
// StepSlice is a helper for sorting.
type StepSlice []*StepDisplay
func (s StepSlice) Len() int {
return len(s)
}
func (s StepSlice) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s StepSlice) Less(i, j int) bool {
return s[i].Started.Before(s[j].Started)
}
// TaskDriverRunDisplay represents a single run of a Task Driver.
type TaskDriverRunDisplay struct {
Id string `json:"id" go2ts:"ignore"`
Properties *td.RunProperties `json:"properties"`
*StepDisplay
}
// TaskDriverForDisplay converts a db.TaskDriver into a TaskDriver, which is
// more human-friendly for display purposes.
func TaskDriverForDisplay(t *db.TaskDriverRun) (*TaskDriverRunDisplay, error) {
rv := &TaskDriverRunDisplay{
Id: t.TaskId,
Properties: t.Properties.Copy(),
}
// Create each StepDisplay.
steps := make(map[string]*StepDisplay, len(t.Steps))
var helper func(string) error
helper = func(id string) error {
if _, ok := steps[id]; ok {
return nil
}
var errs error // Will hold non-critical errors.
orig, ok := t.Steps[id]
if !ok {
// We should do our best to display anyway. Store the
// error but keep going.
errs = multierror.Append(errs, fmt.Errorf("Unknown step %s", id))
return errs
}
var data []*db.StepData
if len(orig.Data) > 0 {
for _, d := range orig.Data {
// TODO(borenet): We should try to deep-copy the data.
data = append(data, d)
}
}
var errorMsgs []string
for _, errMsg := range orig.Errors {
errorMsgs = append(errorMsgs, truncateError(errMsg))
}
s := &StepDisplay{
StepProperties: orig.Properties.Copy(),
Result: orig.Result,
Errors: errorMsgs,
Started: orig.Started,
Finished: orig.Finished,
Data: data,
Steps: []*StepDisplay{},
}
if orig.Properties.Parent != "" {
if err := helper(orig.Properties.Parent); err != nil {
// We should do our best to display anyway.
// Store the error but keep going.
errs = multierror.Append(errs, err)
}
// Now parent should be in the steps map.
parent, ok := steps[orig.Properties.Parent]
if !ok {
// If our parent isn't in the steps map, then
// don't bother trying to display this step.
return multierror.Append(errs, fmt.Errorf("Parent step %s is not present!", orig.Properties.Parent))
}
parent.Steps = append(parent.Steps, s)
}
steps[s.Id] = s
return errs
}
for _, s := range t.Steps {
if s.Properties != nil {
if err := helper(s.Properties.Id); err != nil {
// The error will be non-nil if any step is missing;
// we want to do our best to display even if the pubsub
// messages arrived out of order and we received a
// message about a child step before its parent, so just
// ignore the error.
sklog.Infof("Error when gathering steps: %s", err)
continue
}
if s.Properties.Id == td.StepIDRoot {
rv.StepDisplay = steps[s.Properties.Id]
}
}
}
// Sort all steps by start time.
var sortHelper func(*StepDisplay)
sortHelper = func(s *StepDisplay) {
sort.Sort(StepSlice(s.Steps))
for _, child := range s.Steps {
sortHelper(child)
}
}
if rv.StepDisplay != nil {
sortHelper(rv.StepDisplay)
}
return rv, nil
}
// truncateError shortens an error message, returning the last part, so as not
// to transfer giant logs to the client. There is an endpoint to retrieve a
// specific error which will return the whole thing.
func truncateError(err string) string {
lines := strings.Split(err, "\n")
// Logs often end in a newline; trim any empty line.
if len(lines) > 0 {
if lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}
}
if len(lines) > MAX_ERROR_LINES {
lines = lines[len(lines)-MAX_ERROR_LINES:]
lines[0] = ELLIPSES + lines[0]
}
rv := strings.Join(lines, "\n")
if len(rv) > MAX_ERROR_CHARS {
rv = ELLIPSES + rv[len(rv)-MAX_ERROR_CHARS+len(ELLIPSES):]
}
sklog.Infof("Truncating error message of length %d to %d", len(err), len(rv))
return rv
}