github.com/saucelabs/saucectl@v0.175.1/internal/report/github/summary.go (about) 1 package github 2 3 import ( 4 "fmt" 5 "os" 6 "time" 7 8 "github.com/saucelabs/saucectl/internal/job" 9 "github.com/saucelabs/saucectl/internal/report" 10 ) 11 12 // Reporter generates Job Summaries for GitHub. 13 // https://github.blog/2022-05-09-supercharging-github-actions-with-job-summaries/ 14 type Reporter struct { 15 startTime time.Time 16 stepSummaryFile string 17 results []report.TestResult 18 } 19 20 func NewJobSummaryReporter() Reporter { 21 return Reporter{ 22 startTime: time.Now(), 23 stepSummaryFile: os.Getenv("GITHUB_STEP_SUMMARY"), 24 } 25 } 26 27 func (r *Reporter) isActive() bool { 28 return r.stepSummaryFile != "" 29 } 30 31 func (r *Reporter) Add(t report.TestResult) { 32 if !r.isActive() { 33 return 34 } 35 r.results = append(r.results, t) 36 } 37 38 func hasDevice(results []report.TestResult) bool { 39 for _, t := range results { 40 if t.DeviceName != "" { 41 return true 42 } 43 } 44 return false 45 } 46 47 func (r *Reporter) Render() { 48 if !r.isActive() { 49 return 50 } 51 52 endTime := time.Now() 53 hasDevices := hasDevice(r.results) 54 errors := 0 55 inProgress := 0 56 57 content := renderHeader(hasDevices) 58 for _, result := range r.results { 59 if result.Status == job.StateInProgress || result.Status == job.StateNew { 60 inProgress++ 61 } 62 if result.Status == job.StateFailed || result.Status == job.StateError { 63 errors++ 64 } 65 content += renderTestResult(result, hasDevices) 66 } 67 content += renderFooter(errors, inProgress, len(r.results), endTime.Sub(r.startTime)) 68 69 err := os.WriteFile(r.stepSummaryFile, []byte(content), 0x644) 70 if err != nil { 71 fmt.Printf("Unable to save summary: %v\n", err) 72 } 73 } 74 75 func (r *Reporter) Reset() { 76 if !r.isActive() { 77 return 78 } 79 } 80 81 func (r *Reporter) ArtifactRequirements() []report.ArtifactType { 82 return []report.ArtifactType{} 83 } 84 85 func renderHeader(hasDevices bool) string { 86 deviceTitle := "" 87 deviceSeparator := "" 88 if hasDevices { 89 deviceTitle = " Device |" 90 deviceSeparator = " --- |" 91 } 92 content := fmt.Sprintf("| | Name | Duration | Status | Browser | Platform |%s\n", deviceTitle) 93 content += fmt.Sprintf("| --- | --- | --- | --- | --- | --- |%s\n", deviceSeparator) 94 return content 95 } 96 97 func statusToEmoji(status string) string { 98 switch status { 99 case job.StateInProgress, job.StateNew: 100 return ":clock10:" 101 case job.StatePassed, job.StateComplete: 102 return ":white_check_mark:" 103 case job.StateUnknown: 104 return ":interrobang:" 105 case job.StateError, job.StateFailed: 106 return ":x:" 107 default: 108 return ":warning:" 109 } 110 } 111 112 func renderTestResult(t report.TestResult, hasDevices bool) string { 113 content := "" 114 115 mark := statusToEmoji(t.Status) 116 deviceValue := "" 117 if hasDevices { 118 deviceValue = fmt.Sprintf(" %s |", t.DeviceName) 119 } 120 121 content += fmt.Sprintf("| %s | [%s](%s) | %.0fs | %s | %s | %s |%s\n", 122 mark, t.Name, t.URL, t.Duration.Seconds(), t.Status, t.Browser, t.Platform, deviceValue) 123 return content 124 } 125 126 func renderFooter(errors, inProgress, tests int, dur time.Duration) string { 127 if errors != 0 { 128 relative := float64(errors) / float64(tests) * 100 129 return fmt.Sprintf("\n:x: %d of %d suites have failed (%.0f%%) in %s\n\n", errors, tests, relative, dur.Truncate(1*time.Second)) 130 } 131 if inProgress != 0 { 132 return fmt.Sprintf("\n:clock10: All suites have launched in %s\n\n", dur.Truncate(1*time.Second)) 133 } 134 return fmt.Sprintf("\n:white_check_mark: All suites have passed in %s\n\n", dur.Truncate(1*time.Second)) 135 }