Skip to content

Commit

Permalink
feat: Add spotlight reporter (#828)
Browse files Browse the repository at this point in the history
* feat: Add spotlight reporter

* Capture reporter usage
  • Loading branch information
alexplischke authored Aug 29, 2023
1 parent 824a184 commit 14cb46c
Show file tree
Hide file tree
Showing 17 changed files with 368 additions and 34 deletions.
10 changes: 10 additions & 0 deletions api/saucectl.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,16 @@
"default": "saucectl-test-result.xml"
}
}
},
"spotlight": {
"type": "object",
"description": "The spotlight reporter prints a an overview of failed, or otherwise interesting, jobs.",
"properties": {
"enabled": {
"description": "Toggles the reporter on/off.",
"type": "boolean"
}
}
}
},
"additionalProperties": false
Expand Down
10 changes: 10 additions & 0 deletions api/v1alpha/subschema/reporters.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@
"default": "saucectl-test-result.xml"
}
}
},
"spotlight": {
"type": "object",
"description": "The spotlight reporter prints a an overview of failed, or otherwise interesting, jobs.",
"properties": {
"enabled": {
"description": "Toggles the reporter on/off.",
"type": "boolean"
}
}
}
},
"additionalProperties": false
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/run/cucumber.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func runCucumber(cmd *cobra.Command, isCLIDriven bool) (int, error) {
props.SetFramework("playwright-cucumberjs").SetFVersion(p.Playwright.Version).SetFlags(cmd.Flags()).SetSauceConfig(p.Sauce).
SetArtifacts(p.Artifacts).SetNPM(p.Npm).SetNumSuites(len(p.Suites)).SetJobs(captor.Default.TestResults).
SetSlack(p.Notifications.Slack).SetSharding(cucumber.IsSharded(p.Suites)).SetLaunchOrder(p.Sauce.LaunchOrder).
SetSmartRetry(p.IsSmartRetried())
SetSmartRetry(p.IsSmartRetried()).SetReporters(p.Reporters)
tracker.Collect(cases.Title(language.English).String(cmds.FullName(cmd)), props)
_ = tracker.Close()
}()
Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/run/cypress.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func runCypress(cmd *cobra.Command, isCLIDriven bool) (int, error) {
props.SetFramework("cypress").SetFVersion(p.GetVersion()).SetFlags(cmd.Flags()).SetSauceConfig(p.GetSauceCfg()).
SetArtifacts(p.GetArtifactsCfg()).SetNPM(p.GetNpm()).SetNumSuites(len(p.GetSuites())).SetJobs(captor.Default.TestResults).
SetSlack(p.GetNotifications().Slack).SetSharding(p.IsSharded()).SetLaunchOrder(p.GetSauceCfg().LaunchOrder).
SetSmartRetry(p.IsSmartRetried())
SetSmartRetry(p.IsSmartRetried()).SetReporters(p.GetReporters())

tracker.Collect(cases.Title(language.English).String(cmds.FullName(cmd)), props)
_ = tracker.Close()
Expand Down Expand Up @@ -163,7 +163,7 @@ func runCypress(cmd *cobra.Command, isCLIDriven bool) (int, error) {
BuildService: &restoClient,
Region: regio,
ShowConsoleLog: p.IsShowConsoleLog(),
Reporters: createReporters(p.GetReporter(), p.GetNotifications(), p.GetSauceCfg().Metadata, &testcompClient, &restoClient,
Reporters: createReporters(p.GetReporters(), p.GetNotifications(), p.GetSauceCfg().Metadata, &testcompClient, &restoClient,
"cypress", "sauce", gFlags.async),
Async: gFlags.async,
FailFast: gFlags.failFast,
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/run/espresso.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func runEspresso(cmd *cobra.Command, espressoFlags espressoFlags, isCLIDriven bo
props.SetFramework("espresso").SetFlags(cmd.Flags()).SetSauceConfig(p.Sauce).SetArtifacts(p.Artifacts).
SetNumSuites(len(p.Suites)).SetJobs(captor.Default.TestResults).SetSlack(p.Notifications.Slack).
SetSharding(espresso.IsSharded(p.Suites)).SetLaunchOrder(p.Sauce.LaunchOrder).
SetSmartRetry(p.IsSmartRetried())
SetSmartRetry(p.IsSmartRetried()).SetReporters(p.Reporters)
tracker.Collect(cases.Title(language.English).String(cmds.FullName(cmd)), props)
_ = tracker.Close()
}()
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/run/playwright.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func runPlaywright(cmd *cobra.Command, isCLIDriven bool) (int, error) {
props.SetFramework("playwright").SetFVersion(p.Playwright.Version).SetFlags(cmd.Flags()).SetSauceConfig(p.Sauce).
SetArtifacts(p.Artifacts).SetNPM(p.Npm).SetNumSuites(len(p.Suites)).SetJobs(captor.Default.TestResults).
SetSlack(p.Notifications.Slack).SetSharding(playwright.IsSharded(p.Suites)).SetLaunchOrder(p.Sauce.LaunchOrder).
SetSmartRetry(p.IsSmartRetried())
SetSmartRetry(p.IsSmartRetried()).SetReporters(p.Reporters)
tracker.Collect(cases.Title(language.English).String(cmds.FullName(cmd)), props)
_ = tracker.Close()
}()
Expand Down
39 changes: 25 additions & 14 deletions internal/cmd/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import (

"github.com/fatih/color"
"github.com/rs/zerolog/log"
"github.com/saucelabs/saucectl/internal/report/buildtable"
"github.com/saucelabs/saucectl/internal/report/json"
"github.com/saucelabs/saucectl/internal/report/junit"
"github.com/saucelabs/saucectl/internal/report/spotlight"
"github.com/spf13/cobra"
"github.com/spf13/pflag"

Expand All @@ -30,10 +33,8 @@ import (
"github.com/saucelabs/saucectl/internal/playwright"
"github.com/saucelabs/saucectl/internal/puppeteer/replay"
"github.com/saucelabs/saucectl/internal/report"
"github.com/saucelabs/saucectl/internal/report/buildtable"
"github.com/saucelabs/saucectl/internal/report/captor"
"github.com/saucelabs/saucectl/internal/report/github"
"github.com/saucelabs/saucectl/internal/report/json"
"github.com/saucelabs/saucectl/internal/testcafe"
"github.com/saucelabs/saucectl/internal/version"
"github.com/saucelabs/saucectl/internal/xcuitest"
Expand Down Expand Up @@ -283,27 +284,37 @@ func checkForUpdates() {

func createReporters(c config.Reporters, ntfs config.Notifications, metadata config.Metadata,
svc slack.Service, buildReader build.Reader, framework, env string, async bool) []report.Reporter {
buildReporter := buildtable.New(buildReader)
githubReporter := github.NewJobSummaryReporter()

reps := []report.Reporter{
&captor.Default,
&buildReporter,
&githubReporter,
}

if !async && c.JUnit.Enabled {
reps = append(reps, &junit.Reporter{
Filename: c.JUnit.Filename,
})
// Running async means that jobs aren't done by the time reports are
// generated. Therefore, we disable all reporters that depend on the Job
// results.
if !async {
if c.JUnit.Enabled {
reps = append(reps, &junit.Reporter{
Filename: c.JUnit.Filename,
})
}
if c.JSON.Enabled {
reps = append(reps, &json.Reporter{
WebhookURL: c.JSON.WebhookURL,
Filename: c.JSON.Filename,
})
}
if c.Spotlight.Enabled {
reps = append(reps, &spotlight.Reporter{
Dst: os.Stdout,
})
}
}

if !async && c.JSON.Enabled {
reps = append(reps, &json.Reporter{
WebhookURL: c.JSON.WebhookURL,
Filename: c.JSON.Filename,
})
}
buildReporter := buildtable.New(buildReader)
reps = append(reps, &buildReporter)

reps = append(reps, &slack.Reporter{
Channels: ntfs.Slack.Channels,
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/run/testcafe.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ func runTestcafe(cmd *cobra.Command, tcFlags testcafeFlags, isCLIDriven bool) (i
props.SetFramework("testcafe").SetFVersion(p.Testcafe.Version).SetFlags(cmd.Flags()).SetSauceConfig(p.Sauce).
SetArtifacts(p.Artifacts).SetNPM(p.Npm).SetNumSuites(len(p.Suites)).SetJobs(captor.Default.TestResults).
SetSlack(p.Notifications.Slack).SetSharding(testcafe.IsSharded(p.Suites)).SetLaunchOrder(p.Sauce.LaunchOrder).
SetSmartRetry(p.IsSmartRetried())
SetSmartRetry(p.IsSmartRetried()).SetReporters(p.Reporters)
tracker.Collect(cases.Title(language.English).String(cmds.FullName(cmd)), props)
_ = tracker.Close()
}()
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/run/xcuitest.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func runXcuitest(cmd *cobra.Command, xcuiFlags xcuitestFlags, isCLIDriven bool)
props.SetFramework("xcuitest").SetFlags(cmd.Flags()).SetSauceConfig(p.Sauce).SetArtifacts(p.Artifacts).
SetNumSuites(len(p.Suites)).SetJobs(captor.Default.TestResults).SetSlack(p.Notifications.Slack).
SetSharding(xcuitest.IsSharded(p.Suites)).SetLaunchOrder(p.Sauce.LaunchOrder).
SetSmartRetry(p.IsSmartRetried())
SetSmartRetry(p.IsSmartRetried()).SetReporters(p.Reporters)
tracker.Collect(cases.Title(language.English).String(cmds.FullName(cmd)), props)
_ = tracker.Close()
}()
Expand Down
4 changes: 4 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ type Artifacts struct {

// Reporters represents the reporter configuration.
type Reporters struct {
Spotlight struct {
Enabled bool `yaml:"enabled"`
}

JUnit struct {
Enabled bool `yaml:"enabled"`
Filename string `yaml:"filename"`
Expand Down
2 changes: 1 addition & 1 deletion internal/cypress/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type Project interface {
GetArtifactsCfg() config.Artifacts
IsShowConsoleLog() bool
GetBeforeExec() []string
GetReporter() config.Reporters
GetReporters() config.Reporters
GetNotifications() config.Notifications
GetNpm() config.Npm
SetCLIFlags(map[string]interface{})
Expand Down
2 changes: 1 addition & 1 deletion internal/cypress/v1/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ func (p *Project) GetBeforeExec() []string {
}

// GetReporter returns config.Reporters
func (p *Project) GetReporter() config.Reporters {
func (p *Project) GetReporters() config.Reporters {
return p.Reporters
}

Expand Down
2 changes: 1 addition & 1 deletion internal/cypress/v1alpha/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ func (p *Project) GetBeforeExec() []string {
}

// GetReporter returns config.Reporters
func (p *Project) GetReporter() config.Reporters {
func (p *Project) GetReporters() config.Reporters {
return p.Reporters
}

Expand Down
135 changes: 135 additions & 0 deletions internal/report/spotlight/spotlight.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package spotlight

import (
"fmt"
"io"
"sync"

"github.com/fatih/color"
"github.com/saucelabs/saucectl/internal/imagerunner"
"github.com/saucelabs/saucectl/internal/job"
"github.com/saucelabs/saucectl/internal/junit"
"github.com/saucelabs/saucectl/internal/report"
)

// Reporter implements report.Reporter and highlights the most important test
// results.
type Reporter struct {
TestResults []report.TestResult
Dst io.Writer
lock sync.Mutex
}

// Add adds the test result that can be rendered by Render.
func (r *Reporter) Add(t report.TestResult) {
r.lock.Lock()
defer r.lock.Unlock()

// skip in-progress jobs
if !job.Done(t.Status) && !imagerunner.Done(t.Status) && !t.TimedOut {
return
}
// skip passed jobs
if t.Status == job.StatePassed || t.Status == imagerunner.StateSucceeded {
return
}

if t.TimedOut {
t.Status = job.StateUnknown
}

r.TestResults = append(r.TestResults, t)
}

// Render renders out a test summary.
func (r *Reporter) Render() {
r.lock.Lock()
defer r.lock.Unlock()

r.println()
rl := color.New(color.FgBlue, color.Underline, color.Bold).Sprintf("Spotlight:")

if len(r.TestResults) == 0 {
r.printf(" %s Nothing stands out!\n", rl)
return
}

r.printf(" %s\n", rl)
r.println()

for _, ts := range r.TestResults {
r.println("", jobStatusSymbol(ts.Status), ts.Name)
r.println(" ● URL:", ts.URL)

var junitReports []junit.TestSuites
for _, attempt := range ts.Attempts {
junitReports = append(junitReports, attempt.TestSuites)
}
if len(junitReports) > 0 {
junitReport := junit.MergeReports(junitReports...)
testCases := junitReport.TestCases()

var failedTests []string
for _, tc := range testCases {
if tc.IsError() || tc.IsFailure() {
failedTests = append(failedTests, fmt.Sprintf("%s %s › %s", testCaseStatusSymbol(tc), tc.ClassName, tc.Name))
}
// only show the first 5 failed tests to conserve space
if len(failedTests) == 5 {
break
}
}

if len(failedTests) > 0 {
r.println(" ● Failed Tests: (showing max. 5)")
for _, test := range failedTests {
r.println(" ", test)
}
}
r.println()
}
}
}

func (r *Reporter) println(a ...any) {
_, _ = fmt.Fprintln(r.Dst, a...)
}

func (r *Reporter) printf(format string, a ...any) {
_, _ = fmt.Fprintf(r.Dst, format, a...)
}

// Reset resets the reporter to its initial state. This action will delete all test results.
func (r *Reporter) Reset() {
r.lock.Lock()
defer r.lock.Unlock()
r.TestResults = make([]report.TestResult, 0)
}

// ArtifactRequirements returns a list of artifact types this reporter requires
// to create a proper report.
func (r *Reporter) ArtifactRequirements() []report.ArtifactType {
return []report.ArtifactType{report.JUnitArtifact}
}

func jobStatusSymbol(status string) string {
switch status {
case job.StatePassed, imagerunner.StateSucceeded:
return color.GreenString("✔")
case job.StateInProgress, job.StateQueued, job.StateNew, imagerunner.StateRunning, imagerunner.StatePending,
imagerunner.StateUploading:
return color.BlueString("*")
default:
return color.RedString("✖")
}
}

func testCaseStatusSymbol(tc junit.TestCase) string {
if tc.IsError() || tc.IsFailure() {
return color.RedString("✖")
}
if tc.IsSkipped() {
return color.YellowString("-")
}
return color.GreenString("✔")
}
Loading

0 comments on commit 14cb46c

Please sign in to comment.