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  }