From b3c37f6b6cdd3b091140ee823c8f75eab5adef82 Mon Sep 17 00:00:00 2001 From: Tian Feng <tian.feng@saucelabs.com> Date: Tue, 31 Oct 2023 20:42:11 -0700 Subject: [PATCH 1/9] feat: Add buildURL in JSON report --- internal/cmd/run/run.go | 1 + internal/report/json/json.go | 56 ++++++++++++++++++++++++++++++++++++ internal/report/report.go | 1 + 3 files changed, 58 insertions(+) diff --git a/internal/cmd/run/run.go b/internal/cmd/run/run.go index 8a1a978a8..b86ab1842 100644 --- a/internal/cmd/run/run.go +++ b/internal/cmd/run/run.go @@ -302,6 +302,7 @@ func createReporters(c config.Reporters, ntfs config.Notifications, metadata con } if c.JSON.Enabled { reps = append(reps, &json.Reporter{ + Service: buildReader, WebhookURL: c.JSON.WebhookURL, Filename: c.JSON.Filename, }) diff --git a/internal/report/json/json.go b/internal/report/json/json.go index 76d20cad3..61b1cda0e 100644 --- a/internal/report/json/json.go +++ b/internal/report/json/json.go @@ -2,17 +2,23 @@ package json import ( "bytes" + "context" "encoding/json" + "fmt" "io" "net/http" + "net/url" "os" + "strings" "github.com/rs/zerolog/log" + "github.com/saucelabs/saucectl/internal/build" "github.com/saucelabs/saucectl/internal/report" ) // Reporter represents struct to report in json format type Reporter struct { + Service build.Reader WebhookURL string Filename string Results []report.TestResult @@ -26,6 +32,7 @@ func (r *Reporter) Add(t report.TestResult) { // Render sends the result to specified webhook WebhookURL and log the result to the specified json file func (r *Reporter) Render() { r.cleanup() + r.buildData() body, err := json.Marshal(r.Results) if err != nil { log.Error().Msgf("failed to generate test result (%v)", err) @@ -79,3 +86,52 @@ func (r *Reporter) Reset() { func (r *Reporter) ArtifactRequirements() []report.ArtifactType { return nil } + +func (r *Reporter) buildData() { + if len(r.Results) < 1 { + return + } + + var vdcJobURL string + var rdcJobURL string + for _, result := range r.Results { + if !result.RDC && result.URL != "" { + vdcJobURL = result.URL + break + } + } + for _, result := range r.Results { + if result.RDC && result.URL != "" { + rdcJobURL = result.URL + break + } + } + vdcBuildURL := r.getBuildURL(vdcJobURL, build.VDC) + rdcBuildURL := r.getBuildURL(rdcJobURL, build.RDC) + for i, result := range r.Results { + if !result.RDC { + result.BuildURL = vdcBuildURL + } else { + result.BuildURL = rdcBuildURL + } + r.Results[i] = result + } +} + +func (r *Reporter) getBuildURL(jobURL string, buildSource build.Source) string { + pURL, err := url.Parse(jobURL) + if err != nil { + log.Debug().Err(err).Msgf("Failed to parse job url (%s)", jobURL) + return "" + } + p := strings.Split(pURL.Path, "/") + jID := p[len(p)-1] + + bID, err := r.Service.GetBuildID(context.Background(), jID, buildSource) + if err != nil { + log.Debug().Err(err).Msgf("Failed to retrieve build id for job (%s)", jID) + return "" + } + + return fmt.Sprintf("%s://%s/builds/%s/%s", pURL.Scheme, pURL.Host, buildSource, bID) +} diff --git a/internal/report/report.go b/internal/report/report.go index 0ac216c34..f2c3c4ac6 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -35,6 +35,7 @@ type TestResult struct { URL string `json:"url"` Artifacts []Artifact `json:"artifacts,omitempty"` Origin string `json:"origin"` + BuildURL string `json:"buildURL"` RDC bool `json:"-"` TimedOut bool `json:"-"` PassThreshold bool `json:"-"` From b6d3a269580786c913e5a9d13220a78e7dcb892e Mon Sep 17 00:00:00 2001 From: Tian Feng <tian.feng@saucelabs.com> Date: Wed, 1 Nov 2023 10:03:44 -0700 Subject: [PATCH 2/9] refact reporter --- internal/cmd/run/run.go | 3 +- internal/report/buildtable/buildtable.go | 44 ++++--------------- internal/report/json/json.go | 56 ------------------------ internal/saucecloud/cloud.go | 25 +++++++++++ 4 files changed, 35 insertions(+), 93 deletions(-) diff --git a/internal/cmd/run/run.go b/internal/cmd/run/run.go index b86ab1842..d4fa2152d 100644 --- a/internal/cmd/run/run.go +++ b/internal/cmd/run/run.go @@ -302,7 +302,6 @@ func createReporters(c config.Reporters, ntfs config.Notifications, metadata con } if c.JSON.Enabled { reps = append(reps, &json.Reporter{ - Service: buildReader, WebhookURL: c.JSON.WebhookURL, Filename: c.JSON.Filename, }) @@ -314,7 +313,7 @@ func createReporters(c config.Reporters, ntfs config.Notifications, metadata con } } - buildReporter := buildtable.New(buildReader) + buildReporter := buildtable.New() reps = append(reps, &buildReporter) reps = append(reps, &slack.Reporter{ diff --git a/internal/report/buildtable/buildtable.go b/internal/report/buildtable/buildtable.go index fef8627e1..825dc32d1 100644 --- a/internal/report/buildtable/buildtable.go +++ b/internal/report/buildtable/buildtable.go @@ -1,15 +1,11 @@ package buildtable import ( - "context" "fmt" - "net/url" "os" "strings" "github.com/fatih/color" - "github.com/rs/zerolog/log" - "github.com/saucelabs/saucectl/internal/build" "github.com/saucelabs/saucectl/internal/report" "github.com/saucelabs/saucectl/internal/report/table" ) @@ -17,14 +13,12 @@ import ( // Reporter is an implementation of report.Reporter // It wraps a table reporter and decorates it with additional metadata type Reporter struct { - Service build.Reader VDCTableReport table.Reporter RDCTableReport table.Reporter } -func New(svc build.Reader) Reporter { +func New() Reporter { return Reporter{ - Service: svc, VDCTableReport: table.Reporter{ Dst: os.Stdout, }, @@ -49,18 +43,16 @@ func (r *Reporter) Render() { printTitle() printPadding(2) - var jURL string - var bURL string if len(r.VDCTableReport.TestResults) > 0 { r.VDCTableReport.Render() - for _, tr := range r.VDCTableReport.TestResults { - if tr.URL != "" { - jURL = tr.URL + var bURL string + for _, result := range r.VDCTableReport.TestResults { + if result.BuildURL != "" { + bURL = result.BuildURL break } } - bURL = r.buildURLFromJobURL(jURL, build.VDC) if bURL == "" { bURL = "N/A" @@ -72,13 +64,13 @@ func (r *Reporter) Render() { if len(r.RDCTableReport.TestResults) > 0 { r.RDCTableReport.Render() - for _, tr := range r.RDCTableReport.TestResults { - if tr.URL != "" { - jURL = tr.URL + var bURL string + for _, result := range r.RDCTableReport.TestResults { + if result.BuildURL != "" { + bURL = result.BuildURL break } } - bURL = r.buildURLFromJobURL(jURL, build.RDC) if bURL == "" { bURL = "N/A" @@ -100,24 +92,6 @@ func (r *Reporter) ArtifactRequirements() []report.ArtifactType { return nil } -func (r *Reporter) buildURLFromJobURL(jobURL string, buildSource build.Source) string { - pURL, err := url.Parse(jobURL) - if err != nil { - log.Debug().Err(err).Msgf("Failed to parse job url (%s)", jobURL) - return "" - } - p := strings.Split(pURL.Path, "/") - jID := p[len(p)-1] - - bID, err := r.Service.GetBuildID(context.Background(), jID, buildSource) - if err != nil { - log.Debug().Err(err).Msgf("Failed to retrieve build id for job (%s)", jID) - return "" - } - - return fmt.Sprintf("%s://%s/builds/%s/%s", pURL.Scheme, pURL.Host, buildSource, bID) -} - func printPadding(repeat int) { fmt.Print(strings.Repeat("\n", repeat)) } diff --git a/internal/report/json/json.go b/internal/report/json/json.go index 61b1cda0e..76d20cad3 100644 --- a/internal/report/json/json.go +++ b/internal/report/json/json.go @@ -2,23 +2,17 @@ package json import ( "bytes" - "context" "encoding/json" - "fmt" "io" "net/http" - "net/url" "os" - "strings" "github.com/rs/zerolog/log" - "github.com/saucelabs/saucectl/internal/build" "github.com/saucelabs/saucectl/internal/report" ) // Reporter represents struct to report in json format type Reporter struct { - Service build.Reader WebhookURL string Filename string Results []report.TestResult @@ -32,7 +26,6 @@ func (r *Reporter) Add(t report.TestResult) { // Render sends the result to specified webhook WebhookURL and log the result to the specified json file func (r *Reporter) Render() { r.cleanup() - r.buildData() body, err := json.Marshal(r.Results) if err != nil { log.Error().Msgf("failed to generate test result (%v)", err) @@ -86,52 +79,3 @@ func (r *Reporter) Reset() { func (r *Reporter) ArtifactRequirements() []report.ArtifactType { return nil } - -func (r *Reporter) buildData() { - if len(r.Results) < 1 { - return - } - - var vdcJobURL string - var rdcJobURL string - for _, result := range r.Results { - if !result.RDC && result.URL != "" { - vdcJobURL = result.URL - break - } - } - for _, result := range r.Results { - if result.RDC && result.URL != "" { - rdcJobURL = result.URL - break - } - } - vdcBuildURL := r.getBuildURL(vdcJobURL, build.VDC) - rdcBuildURL := r.getBuildURL(rdcJobURL, build.RDC) - for i, result := range r.Results { - if !result.RDC { - result.BuildURL = vdcBuildURL - } else { - result.BuildURL = rdcBuildURL - } - r.Results[i] = result - } -} - -func (r *Reporter) getBuildURL(jobURL string, buildSource build.Source) string { - pURL, err := url.Parse(jobURL) - if err != nil { - log.Debug().Err(err).Msgf("Failed to parse job url (%s)", jobURL) - return "" - } - p := strings.Split(pURL.Path, "/") - jID := p[len(p)-1] - - bID, err := r.Service.GetBuildID(context.Background(), jID, buildSource) - if err != nil { - log.Debug().Err(err).Msgf("Failed to retrieve build id for job (%s)", jID) - return "" - } - - return fmt.Sprintf("%s://%s/builds/%s/%s", pURL.Scheme, pURL.Host, buildSource, bID) -} diff --git a/internal/saucecloud/cloud.go b/internal/saucecloud/cloud.go index dd31b2197..4c7d22aef 100644 --- a/internal/saucecloud/cloud.go +++ b/internal/saucecloud/cloud.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "net/url" "os" "os/signal" "path" @@ -155,6 +156,7 @@ func (r *CloudRunner) collectResults(artifactCfg config.ArtifactDownload, result if res.job.ID != "" { url = fmt.Sprintf("%s/tests/%s", r.Region.AppBaseURL(), res.job.ID) } + buildURL := r.getBuildURL(url, res.job.IsRDC) tr := report.TestResult{ Name: res.name, Duration: res.duration, @@ -170,6 +172,7 @@ func (r *CloudRunner) collectResults(artifactCfg config.ArtifactDownload, result RDC: res.job.IsRDC, TimedOut: res.job.TimedOut, Attempts: res.attempts, + BuildURL: buildURL, } for _, rep := range r.Reporters { rep.Add(tr) @@ -195,6 +198,28 @@ func (r *CloudRunner) collectResults(artifactCfg config.ArtifactDownload, result return passed } +func (r *CloudRunner) getBuildURL(jobURL string, isRDC bool) string { + pURL, err := url.Parse(jobURL) + if err != nil { + log.Debug().Err(err).Msgf("Failed to parse job url (%s)", jobURL) + return "" + } + p := strings.Split(pURL.Path, "/") + jID := p[len(p)-1] + + buildSource := build.RDC + if !isRDC { + buildSource = build.VDC + } + bID, err := r.BuildService.GetBuildID(context.Background(), jID, buildSource) + if err != nil { + log.Debug().Err(err).Msgf("Failed to retrieve build id for job (%s)", jID) + return "" + } + + return fmt.Sprintf("%s://%s/builds/%s/%s", pURL.Scheme, pURL.Host, buildSource, bID) +} + func (r *CloudRunner) runJob(opts job.StartOptions) (j job.Job, skipped bool, err error) { log.Info().Str("suite", opts.DisplayName).Str("region", r.Region.String()).Msg("Starting suite.") From 0bf60e46cfab2a8f2bfdaa24cee3f8eb1a2f2609 Mon Sep 17 00:00:00 2001 From: Tian Feng <tian.feng@saucelabs.com> Date: Wed, 1 Nov 2023 12:03:06 -0700 Subject: [PATCH 3/9] make buildURL omitempty --- internal/report/report.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/report/report.go b/internal/report/report.go index f2c3c4ac6..a2b1ab42a 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -35,7 +35,7 @@ type TestResult struct { URL string `json:"url"` Artifacts []Artifact `json:"artifacts,omitempty"` Origin string `json:"origin"` - BuildURL string `json:"buildURL"` + BuildURL string `json:"buildURL,omitempty"` RDC bool `json:"-"` TimedOut bool `json:"-"` PassThreshold bool `json:"-"` From 2c2e762a8a6bde1fd40d3796b357214a750b1858 Mon Sep 17 00:00:00 2001 From: Tian Feng <tian.feng@saucelabs.com> Date: Wed, 1 Nov 2023 13:29:59 -0700 Subject: [PATCH 4/9] refine --- internal/saucecloud/cloud.go | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/internal/saucecloud/cloud.go b/internal/saucecloud/cloud.go index 4c7d22aef..87e4967a6 100644 --- a/internal/saucecloud/cloud.go +++ b/internal/saucecloud/cloud.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "net/url" "os" "os/signal" "path" @@ -156,7 +155,7 @@ func (r *CloudRunner) collectResults(artifactCfg config.ArtifactDownload, result if res.job.ID != "" { url = fmt.Sprintf("%s/tests/%s", r.Region.AppBaseURL(), res.job.ID) } - buildURL := r.getBuildURL(url, res.job.IsRDC) + buildURL := r.getBuildURL(res.job.ID, res.job.IsRDC) tr := report.TestResult{ Name: res.name, Duration: res.duration, @@ -198,26 +197,18 @@ func (r *CloudRunner) collectResults(artifactCfg config.ArtifactDownload, result return passed } -func (r *CloudRunner) getBuildURL(jobURL string, isRDC bool) string { - pURL, err := url.Parse(jobURL) - if err != nil { - log.Debug().Err(err).Msgf("Failed to parse job url (%s)", jobURL) - return "" - } - p := strings.Split(pURL.Path, "/") - jID := p[len(p)-1] - +func (r *CloudRunner) getBuildURL(jobID string, isRDC bool) string { buildSource := build.RDC if !isRDC { buildSource = build.VDC } - bID, err := r.BuildService.GetBuildID(context.Background(), jID, buildSource) + bID, err := r.BuildService.GetBuildID(context.Background(), jobID, buildSource) if err != nil { - log.Debug().Err(err).Msgf("Failed to retrieve build id for job (%s)", jID) + log.Debug().Err(err).Msgf("Failed to retrieve build id for job (%s)", jobID) return "" } - return fmt.Sprintf("%s://%s/builds/%s/%s", pURL.Scheme, pURL.Host, buildSource, bID) + return fmt.Sprintf("%s/builds/%s/%s", r.Region.AppBaseURL(), buildSource, bID) } func (r *CloudRunner) runJob(opts job.StartOptions) (j job.Job, skipped bool, err error) { From a8fa20f9da0f19d5fbcbf22d5d3bd0f6919efdb3 Mon Sep 17 00:00:00 2001 From: Tian Feng <tian.feng@saucelabs.com> Date: Wed, 1 Nov 2023 14:26:39 -0700 Subject: [PATCH 5/9] add cached build url --- internal/saucecloud/cloud.go | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/internal/saucecloud/cloud.go b/internal/saucecloud/cloud.go index 87e4967a6..49383ae99 100644 --- a/internal/saucecloud/cloud.go +++ b/internal/saucecloud/cloud.go @@ -64,6 +64,8 @@ type CloudRunner struct { NPMDependencies []string interrupted bool + VDCBuildURL string + RDCBuildURL string } type result struct { @@ -198,17 +200,32 @@ func (r *CloudRunner) collectResults(artifactCfg config.ArtifactDownload, result } func (r *CloudRunner) getBuildURL(jobID string, isRDC bool) string { - buildSource := build.RDC + var buildSource build.Source if !isRDC { + if r.VDCBuildURL != "" { + return r.VDCBuildURL + } buildSource = build.VDC + } else { + if r.RDCBuildURL != "" { + return r.RDCBuildURL + } + buildSource = build.RDC } + bID, err := r.BuildService.GetBuildID(context.Background(), jobID, buildSource) if err != nil { log.Debug().Err(err).Msgf("Failed to retrieve build id for job (%s)", jobID) return "" } - return fmt.Sprintf("%s/builds/%s/%s", r.Region.AppBaseURL(), buildSource, bID) + bURL := fmt.Sprintf("%s/builds/%s/%s", r.Region.AppBaseURL(), buildSource, bID) + if !isRDC { + r.VDCBuildURL = bURL + } else { + r.RDCBuildURL = bURL + } + return bURL } func (r *CloudRunner) runJob(opts job.StartOptions) (j job.Job, skipped bool, err error) { From 082d67d7696cb74761326894dd640ab820fee402 Mon Sep 17 00:00:00 2001 From: Tian Feng <tian.feng@saucelabs.com> Date: Wed, 1 Nov 2023 14:41:28 -0700 Subject: [PATCH 6/9] re-structure the CloudRunner structure --- internal/saucecloud/cloud.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/internal/saucecloud/cloud.go b/internal/saucecloud/cloud.go index 49383ae99..c5262c51d 100644 --- a/internal/saucecloud/cloud.go +++ b/internal/saucecloud/cloud.go @@ -64,6 +64,10 @@ type CloudRunner struct { NPMDependencies []string interrupted bool + CachedData CachedData +} + +type CachedData struct { VDCBuildURL string RDCBuildURL string } @@ -202,13 +206,13 @@ func (r *CloudRunner) collectResults(artifactCfg config.ArtifactDownload, result func (r *CloudRunner) getBuildURL(jobID string, isRDC bool) string { var buildSource build.Source if !isRDC { - if r.VDCBuildURL != "" { - return r.VDCBuildURL + if r.CachedData.VDCBuildURL != "" { + return r.CachedData.VDCBuildURL } buildSource = build.VDC } else { - if r.RDCBuildURL != "" { - return r.RDCBuildURL + if r.CachedData.RDCBuildURL != "" { + return r.CachedData.RDCBuildURL } buildSource = build.RDC } @@ -221,9 +225,9 @@ func (r *CloudRunner) getBuildURL(jobID string, isRDC bool) string { bURL := fmt.Sprintf("%s/builds/%s/%s", r.Region.AppBaseURL(), buildSource, bID) if !isRDC { - r.VDCBuildURL = bURL + r.CachedData.VDCBuildURL = bURL } else { - r.RDCBuildURL = bURL + r.CachedData.RDCBuildURL = bURL } return bURL } From 442fa0b575270d5a62ae3848f8733dc2c2a05aa8 Mon Sep 17 00:00:00 2001 From: Tian Feng <tian.feng@saucelabs.com> Date: Wed, 1 Nov 2023 14:57:57 -0700 Subject: [PATCH 7/9] change log level --- internal/saucecloud/cloud.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/saucecloud/cloud.go b/internal/saucecloud/cloud.go index c5262c51d..6207c5402 100644 --- a/internal/saucecloud/cloud.go +++ b/internal/saucecloud/cloud.go @@ -219,7 +219,7 @@ func (r *CloudRunner) getBuildURL(jobID string, isRDC bool) string { bID, err := r.BuildService.GetBuildID(context.Background(), jobID, buildSource) if err != nil { - log.Debug().Err(err).Msgf("Failed to retrieve build id for job (%s)", jobID) + log.Warn().Err(err).Msgf("Failed to retrieve build id for job (%s)", jobID) return "" } From cfdd18c2b98ef7cb1fbcd05adee9fc8041dddf75 Mon Sep 17 00:00:00 2001 From: Tian Feng <tian.feng@saucelabs.com> Date: Wed, 1 Nov 2023 16:57:19 -0700 Subject: [PATCH 8/9] Update internal/saucecloud/cloud.go Co-authored-by: Alex Plischke <alex.plischke@saucelabs.com> --- internal/saucecloud/cloud.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/saucecloud/cloud.go b/internal/saucecloud/cloud.go index 6207c5402..92879f315 100644 --- a/internal/saucecloud/cloud.go +++ b/internal/saucecloud/cloud.go @@ -64,10 +64,10 @@ type CloudRunner struct { NPMDependencies []string interrupted bool - CachedData CachedData + Cache Cache } -type CachedData struct { +type Cache struct { VDCBuildURL string RDCBuildURL string } From 41cff9c81c1b2b53cb7fc53945c3978f93554234 Mon Sep 17 00:00:00 2001 From: Tian Feng <tian.feng@saucelabs.com> Date: Wed, 1 Nov 2023 16:58:10 -0700 Subject: [PATCH 9/9] rename CachedData to Cache --- internal/saucecloud/cloud.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/saucecloud/cloud.go b/internal/saucecloud/cloud.go index 92879f315..f891cb8e1 100644 --- a/internal/saucecloud/cloud.go +++ b/internal/saucecloud/cloud.go @@ -64,7 +64,7 @@ type CloudRunner struct { NPMDependencies []string interrupted bool - Cache Cache + Cache Cache } type Cache struct { @@ -206,13 +206,13 @@ func (r *CloudRunner) collectResults(artifactCfg config.ArtifactDownload, result func (r *CloudRunner) getBuildURL(jobID string, isRDC bool) string { var buildSource build.Source if !isRDC { - if r.CachedData.VDCBuildURL != "" { - return r.CachedData.VDCBuildURL + if r.Cache.VDCBuildURL != "" { + return r.Cache.VDCBuildURL } buildSource = build.VDC } else { - if r.CachedData.RDCBuildURL != "" { - return r.CachedData.RDCBuildURL + if r.Cache.RDCBuildURL != "" { + return r.Cache.RDCBuildURL } buildSource = build.RDC } @@ -225,9 +225,9 @@ func (r *CloudRunner) getBuildURL(jobID string, isRDC bool) string { bURL := fmt.Sprintf("%s/builds/%s/%s", r.Region.AppBaseURL(), buildSource, bID) if !isRDC { - r.CachedData.VDCBuildURL = bURL + r.Cache.VDCBuildURL = bURL } else { - r.CachedData.RDCBuildURL = bURL + r.Cache.RDCBuildURL = bURL } return bURL }