github.com/saucelabs/saucectl@v0.175.1/internal/report/table/table.go (about)

     1  package table
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/fatih/color"
    10  	"github.com/jedib0t/go-pretty/v6/table"
    11  	"github.com/jedib0t/go-pretty/v6/text"
    12  	"github.com/saucelabs/saucectl/internal/imagerunner"
    13  	"github.com/saucelabs/saucectl/internal/job"
    14  	"github.com/saucelabs/saucectl/internal/report"
    15  )
    16  
    17  var defaultTableStyle = table.Style{
    18  	Name: "saucy",
    19  	Box: table.BoxStyle{
    20  		BottomLeft:       "└",
    21  		BottomRight:      "┘",
    22  		BottomSeparator:  "",
    23  		EmptySeparator:   text.RepeatAndTrim(" ", text.RuneCount("+")),
    24  		Left:             "│",
    25  		LeftSeparator:    "",
    26  		MiddleHorizontal: "─",
    27  		MiddleSeparator:  "",
    28  		MiddleVertical:   "",
    29  		PaddingLeft:      "  ",
    30  		PaddingRight:     "  ",
    31  		PageSeparator:    "\n",
    32  		Right:            "│",
    33  		RightSeparator:   "",
    34  		TopLeft:          "┌",
    35  		TopRight:         "┐",
    36  		TopSeparator:     "",
    37  		UnfinishedRow:    " ...",
    38  	},
    39  	Color: table.ColorOptionsDefault,
    40  	Format: table.FormatOptions{
    41  		Footer: text.FormatDefault,
    42  		Header: text.FormatDefault,
    43  		Row:    text.FormatDefault,
    44  	},
    45  	HTML: table.DefaultHTMLOptions,
    46  	Options: table.Options{
    47  		DrawBorder:      false,
    48  		SeparateColumns: false,
    49  		SeparateFooter:  true,
    50  		SeparateHeader:  true,
    51  		SeparateRows:    false,
    52  	},
    53  	Title: table.TitleOptionsDefault,
    54  }
    55  
    56  // Reporter is a table writer implementation for report.Reporter.
    57  type Reporter struct {
    58  	TestResults []report.TestResult
    59  	Dst         io.Writer
    60  	lock        sync.Mutex
    61  }
    62  
    63  // Add adds the test result to the summary table.
    64  func (r *Reporter) Add(t report.TestResult) {
    65  	r.lock.Lock()
    66  	defer r.lock.Unlock()
    67  	r.TestResults = append(r.TestResults, t)
    68  }
    69  
    70  // Render renders out a test summary table to the destination of Reporter.Dst.
    71  func (r *Reporter) Render() {
    72  	r.lock.Lock()
    73  	defer r.lock.Unlock()
    74  
    75  	t := table.NewWriter()
    76  	t.SetOutputMirror(r.Dst)
    77  	t.SetStyle(defaultTableStyle)
    78  	t.SuppressEmptyColumns()
    79  
    80  	t.AppendHeader(table.Row{"", "Name", "Duration", "Status", "Browser", "Platform", "Device", "Attempts"})
    81  	t.SetColumnConfigs([]table.ColumnConfig{
    82  		{
    83  			Number:   0, // it's the first nameless column that contains the passed/fail icon
    84  			WidthMax: 1,
    85  		},
    86  		{
    87  			Name:     "Name",
    88  			WidthMin: 30,
    89  		},
    90  		{
    91  			Name:        "Duration",
    92  			Align:       text.AlignRight,
    93  			AlignFooter: text.AlignRight,
    94  		},
    95  	})
    96  
    97  	var (
    98  		errors     int
    99  		inProgress int
   100  		totalDur   time.Duration
   101  	)
   102  	for _, ts := range r.TestResults {
   103  		if !job.Done(ts.Status) && !imagerunner.Done(ts.Status) && !ts.TimedOut {
   104  			inProgress++
   105  		}
   106  		if ts.Status == job.StateFailed || ts.Status == imagerunner.StateFailed ||
   107  			ts.Status == imagerunner.StateCancelled || ts.Status == imagerunner.StateTerminated {
   108  			errors++
   109  		}
   110  		if ts.TimedOut {
   111  			errors++
   112  			ts.Status = job.StateUnknown
   113  		}
   114  
   115  		totalDur += ts.Duration
   116  
   117  		// the order of values must match the order of the header
   118  		t.AppendRow(table.Row{statusSymbol(ts.Status), ts.Name, ts.Duration.Truncate(1 * time.Second),
   119  			statusText(ts.Status), ts.Browser, ts.Platform, ts.DeviceName, len(ts.Attempts)})
   120  	}
   121  
   122  	t.AppendFooter(footer(errors, inProgress, len(r.TestResults), calDuration(r.TestResults)))
   123  
   124  	_, _ = fmt.Fprintln(r.Dst)
   125  	t.Render()
   126  }
   127  
   128  // Reset resets the reporter to its initial state. This action will delete all test results.
   129  func (r *Reporter) Reset() {
   130  	r.lock.Lock()
   131  	defer r.lock.Unlock()
   132  	r.TestResults = make([]report.TestResult, 0)
   133  }
   134  
   135  // ArtifactRequirements returns a list of artifact types are this reporter requires to create a proper report.
   136  func (r *Reporter) ArtifactRequirements() []report.ArtifactType {
   137  	return nil
   138  }
   139  
   140  func footer(errors, inProgress, tests int, dur time.Duration) table.Row {
   141  	if errors != 0 {
   142  		relative := float64(errors) / float64(tests) * 100
   143  		return table.Row{statusSymbol(job.StateError), fmt.Sprintf("%d of %d suites have failed (%.0f%%)", errors, tests, relative), dur.Truncate(1 * time.Second)}
   144  	}
   145  	if inProgress != 0 {
   146  		return table.Row{statusSymbol(job.StateInProgress), "All suites have launched", dur.Truncate(1 * time.Second)}
   147  	}
   148  	return table.Row{statusSymbol(job.StatePassed), "All suites have passed", dur.Truncate(1 * time.Second)}
   149  }
   150  
   151  func statusText(status string) string {
   152  	switch status {
   153  	case job.StatePassed, imagerunner.StateSucceeded:
   154  		return color.GreenString(status)
   155  	case job.StateInProgress, job.StateQueued, job.StateNew, imagerunner.StateRunning, imagerunner.StatePending,
   156  		imagerunner.StateUploading:
   157  		return color.BlueString(status)
   158  	default:
   159  		return color.RedString(status)
   160  	}
   161  }
   162  
   163  func statusSymbol(status string) string {
   164  	switch status {
   165  	case job.StatePassed, imagerunner.StateSucceeded:
   166  		return color.GreenString("✔")
   167  	case job.StateInProgress, job.StateQueued, job.StateNew, imagerunner.StateRunning, imagerunner.StatePending,
   168  		imagerunner.StateUploading:
   169  		return color.BlueString("*")
   170  	default:
   171  		return color.RedString("✖")
   172  	}
   173  }
   174  
   175  func calDuration(results []report.TestResult) time.Duration {
   176  	start := time.Now()
   177  	end := start
   178  	for _, r := range results {
   179  		if r.StartTime.Before(start) && !r.StartTime.IsZero() {
   180  			start = r.StartTime
   181  		}
   182  		if r.EndTime.After(end) && !r.StartTime.IsZero() {
   183  			end = r.EndTime
   184  		}
   185  	}
   186  
   187  	return end.Sub(start)
   188  }