github.com/saucelabs/saucectl@v0.175.1/internal/report/spotlight/spotlight.go (about) 1 package spotlight 2 3 import ( 4 "fmt" 5 "io" 6 "sync" 7 8 "github.com/fatih/color" 9 "github.com/saucelabs/saucectl/internal/imagerunner" 10 "github.com/saucelabs/saucectl/internal/job" 11 "github.com/saucelabs/saucectl/internal/junit" 12 "github.com/saucelabs/saucectl/internal/report" 13 ) 14 15 // Reporter implements report.Reporter and highlights the most important test 16 // results. 17 type Reporter struct { 18 TestResults []report.TestResult 19 Dst io.Writer 20 lock sync.Mutex 21 } 22 23 // Add adds the test result that can be rendered by Render. 24 func (r *Reporter) Add(t report.TestResult) { 25 r.lock.Lock() 26 defer r.lock.Unlock() 27 28 // skip in-progress jobs 29 if !job.Done(t.Status) && !imagerunner.Done(t.Status) && !t.TimedOut { 30 return 31 } 32 // skip passed jobs 33 if t.Status == job.StatePassed || t.Status == imagerunner.StateSucceeded { 34 return 35 } 36 37 if t.TimedOut { 38 t.Status = job.StateUnknown 39 } 40 41 r.TestResults = append(r.TestResults, t) 42 } 43 44 // Render renders out a test summary. 45 func (r *Reporter) Render() { 46 r.lock.Lock() 47 defer r.lock.Unlock() 48 49 r.println() 50 rl := color.New(color.FgBlue, color.Underline, color.Bold).Sprintf("Spotlight:") 51 52 if len(r.TestResults) == 0 { 53 r.printf(" %s Nothing stands out!\n", rl) 54 return 55 } 56 57 r.printf(" %s\n", rl) 58 r.println() 59 60 for _, ts := range r.TestResults { 61 r.println("", jobStatusSymbol(ts.Status), ts.Name) 62 r.println(" ● URL:", ts.URL) 63 64 var junitReports []junit.TestSuites 65 for _, attempt := range ts.Attempts { 66 junitReports = append(junitReports, attempt.TestSuites) 67 } 68 if len(junitReports) > 0 { 69 junitReport := junit.MergeReports(junitReports...) 70 testCases := junitReport.TestCases() 71 72 var failedTests []string 73 for _, tc := range testCases { 74 if tc.IsError() || tc.IsFailure() { 75 failedTests = append(failedTests, fmt.Sprintf("%s %s › %s", testCaseStatusSymbol(tc), tc.ClassName, tc.Name)) 76 } 77 // only show the first 5 failed tests to conserve space 78 if len(failedTests) == 5 { 79 break 80 } 81 } 82 83 if len(failedTests) > 0 { 84 r.println(" ● Failed Tests: (showing max. 5)") 85 for _, test := range failedTests { 86 r.println(" ", test) 87 } 88 } 89 r.println() 90 } 91 } 92 } 93 94 func (r *Reporter) println(a ...any) { 95 _, _ = fmt.Fprintln(r.Dst, a...) 96 } 97 98 func (r *Reporter) printf(format string, a ...any) { 99 _, _ = fmt.Fprintf(r.Dst, format, a...) 100 } 101 102 // Reset resets the reporter to its initial state. This action will delete all test results. 103 func (r *Reporter) Reset() { 104 r.lock.Lock() 105 defer r.lock.Unlock() 106 r.TestResults = make([]report.TestResult, 0) 107 } 108 109 // ArtifactRequirements returns a list of artifact types this reporter requires 110 // to create a proper report. 111 func (r *Reporter) ArtifactRequirements() []report.ArtifactType { 112 return []report.ArtifactType{report.JUnitArtifact} 113 } 114 115 func jobStatusSymbol(status string) string { 116 switch status { 117 case job.StatePassed, imagerunner.StateSucceeded: 118 return color.GreenString("✔") 119 case job.StateInProgress, job.StateQueued, job.StateNew, imagerunner.StateRunning, imagerunner.StatePending, 120 imagerunner.StateUploading: 121 return color.BlueString("*") 122 default: 123 return color.RedString("✖") 124 } 125 } 126 127 func testCaseStatusSymbol(tc junit.TestCase) string { 128 if tc.IsError() || tc.IsFailure() { 129 return color.RedString("✖") 130 } 131 if tc.IsSkipped() { 132 return color.YellowString("-") 133 } 134 return color.GreenString("✔") 135 }