From f443c9f85c4b58278b0f44095ba7541233787363 Mon Sep 17 00:00:00 2001 From: Alex Plischke Date: Tue, 10 Dec 2024 08:47:32 -0800 Subject: [PATCH] feat: cancellable project uploads and job commands (#990) * feat: cancellable project upload * feat: cancellable jobs commands --- internal/cmd/jobs/get.go | 8 +-- internal/cmd/jobs/list.go | 10 ++-- internal/cmd/run/cucumber.go | 2 +- internal/cmd/run/cypress.go | 2 +- internal/cmd/run/espresso.go | 7 +-- internal/cmd/run/playwright.go | 2 +- internal/cmd/run/replay.go | 7 +-- internal/cmd/run/testcafe.go | 2 +- internal/cmd/run/xcuitest.go | 7 +-- internal/http/rdcservice_test.go | 6 +-- internal/http/webdriver_test.go | 6 +-- internal/saucecloud/cloud.go | 52 +++++++++---------- internal/saucecloud/cloud_test.go | 3 +- internal/saucecloud/cucumber.go | 10 ++-- internal/saucecloud/cypress.go | 10 ++-- internal/saucecloud/espresso.go | 13 +++-- internal/saucecloud/playwright.go | 10 ++-- internal/saucecloud/replay.go | 10 ++-- internal/saucecloud/retry/basicretrier.go | 3 +- .../saucecloud/retry/basicretrier_test.go | 3 +- internal/saucecloud/retry/junitretrier.go | 8 +-- .../saucecloud/retry/junitretrier_test.go | 2 +- internal/saucecloud/retry/retrier.go | 8 ++- .../saucecloud/retry/saucereportretrier.go | 12 ++--- .../retry/saucereportretrier_test.go | 2 +- internal/saucecloud/testcafe.go | 10 ++-- internal/saucecloud/xcuitest.go | 11 ++-- 27 files changed, 120 insertions(+), 106 deletions(-) diff --git a/internal/cmd/jobs/get.go b/internal/cmd/jobs/get.go index 84a2b80ee..c6b6919a4 100644 --- a/internal/cmd/jobs/get.go +++ b/internal/cmd/jobs/get.go @@ -36,11 +36,11 @@ func GetCommand() *cobra.Command { _ = tracker.Close() }() }, - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { if out != JSONOutput && out != TextOutput { return errors.New("unknown output format") } - return get(args[0], out) + return get(cmd.Context(), args[0], out) }, } flags := cmd.PersistentFlags() @@ -49,8 +49,8 @@ func GetCommand() *cobra.Command { return cmd } -func get(jobID, outputFormat string) error { - j, err := jobService.ReadJob(context.Background(), jobID) +func get(ctx context.Context, jobID, outputFormat string) error { + j, err := jobService.ReadJob(ctx, jobID) if err != nil { return fmt.Errorf("failed to get job: %w", err) } diff --git a/internal/cmd/jobs/list.go b/internal/cmd/jobs/list.go index 557ff64eb..5d13911dc 100644 --- a/internal/cmd/jobs/list.go +++ b/internal/cmd/jobs/list.go @@ -86,7 +86,7 @@ func ListCommand() *cobra.Command { _ = tracker.Close() }() }, - RunE: func(_ *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { if page < 0 { return errors.New("invalid page") } @@ -112,7 +112,7 @@ func ListCommand() *cobra.Command { return errors.New("invalid job resource. Options: vdc, rdc, api") } - return list(out, page, size, status, job.Source(jobSource)) + return list(cmd.Context(), out, page, size, status, job.Source(jobSource)) }, } flags := cmd.PersistentFlags() @@ -125,8 +125,8 @@ func ListCommand() *cobra.Command { return cmd } -func list(format string, page int, size int, status string, source job.Source) error { - user, err := userService.User(context.Background()) +func list(ctx context.Context, format string, page int, size int, status string, source job.Source) error { + user, err := userService.User(ctx) if err != nil { return fmt.Errorf("failed to get user: %w", err) } @@ -139,7 +139,7 @@ func list(format string, page int, size int, status string, source job.Source) e Source: source, } - jobs, err := jobService.ListJobs(context.Background(), opts) + jobs, err := jobService.ListJobs(ctx, opts) if err != nil { return fmt.Errorf("failed to get jobs: %w", err) } diff --git a/internal/cmd/run/cucumber.go b/internal/cmd/run/cucumber.go index 57bafeb1a..a36e3c15b 100644 --- a/internal/cmd/run/cucumber.go +++ b/internal/cmd/run/cucumber.go @@ -167,7 +167,7 @@ func runCucumber(cmd *cobra.Command, isCLIDriven bool) (int, error) { } p.Npm.Packages = cleanPlaywrightPackages(p.Npm, p.Playwright.Version) - return r.RunProject() + return r.RunProject(cmd.Context()) } func applyCucumberFlags(p *cucumber.Project) error { diff --git a/internal/cmd/run/cypress.go b/internal/cmd/run/cypress.go index 11c3120f3..4582aaadf 100644 --- a/internal/cmd/run/cypress.go +++ b/internal/cmd/run/cypress.go @@ -200,5 +200,5 @@ func runCypress(cmd *cobra.Command, cflags cypressFlags, isCLIDriven bool) (int, } p.CleanPackages() - return r.RunProject() + return r.RunProject(cmd.Context()) } diff --git a/internal/cmd/run/espresso.go b/internal/cmd/run/espresso.go index a6a1fa7cd..a6d51f135 100644 --- a/internal/cmd/run/espresso.go +++ b/internal/cmd/run/espresso.go @@ -1,6 +1,7 @@ package run import ( + "context" "os" cmds "github.com/saucelabs/saucectl/internal/cmd" @@ -130,10 +131,10 @@ func runEspresso(cmd *cobra.Command, espressoFlags espressoFlags, isCLIDriven bo cleanupArtifacts(p.Artifacts) - return runEspressoInCloud(p, regio) + return runEspressoInCloud(cmd.Context(), p, regio) } -func runEspressoInCloud(p espresso.Project, regio region.Region) (int, error) { +func runEspressoInCloud(ctx context.Context, p espresso.Project, regio region.Region) (int, error) { log.Info(). Str("region", regio.String()). Str("tunnel", p.Sauce.Tunnel.Name). @@ -180,7 +181,7 @@ func runEspressoInCloud(p espresso.Project, regio region.Region) (int, error) { }, } - return r.RunProject() + return r.RunProject(ctx) } func hasKey(testOptions map[string]interface{}, key string) bool { diff --git a/internal/cmd/run/playwright.go b/internal/cmd/run/playwright.go index 01dc42999..f25b15a4d 100644 --- a/internal/cmd/run/playwright.go +++ b/internal/cmd/run/playwright.go @@ -212,7 +212,7 @@ func runPlaywright(cmd *cobra.Command, pf playwrightFlags, isCLIDriven bool) (in } p.Npm.Packages = cleanPlaywrightPackages(p.Npm, p.Playwright.Version) - return r.RunProject() + return r.RunProject(cmd.Context()) } func applyPlaywrightFlags(p *playwright.Project) error { diff --git a/internal/cmd/run/replay.go b/internal/cmd/run/replay.go index 15e83f636..f291cf7ba 100644 --- a/internal/cmd/run/replay.go +++ b/internal/cmd/run/replay.go @@ -1,6 +1,7 @@ package run import ( + "context" "errors" "os" @@ -116,10 +117,10 @@ func runReplay(cmd *cobra.Command, isCLIDriven bool) (int, error) { cleanupArtifacts(p.Artifacts) - return runPuppeteerReplayInSauce(p, regio) + return runPuppeteerReplayInSauce(cmd.Context(), p, regio) } -func runPuppeteerReplayInSauce(p replay.Project, regio region.Region) (int, error) { +func runPuppeteerReplayInSauce(ctx context.Context, p replay.Project, regio region.Region) (int, error) { log.Info(). Str("region", regio.String()). Str("tunnel", p.Sauce.Tunnel.Name). @@ -163,7 +164,7 @@ func runPuppeteerReplayInSauce(p replay.Project, regio region.Region) (int, erro }, } - return r.RunProject() + return r.RunProject(ctx) } func applyPuppeteerReplayFlags(p *replay.Project) error { diff --git a/internal/cmd/run/testcafe.go b/internal/cmd/run/testcafe.go index 145934296..20e3ae04f 100644 --- a/internal/cmd/run/testcafe.go +++ b/internal/cmd/run/testcafe.go @@ -235,7 +235,7 @@ func runTestcafe(cmd *cobra.Command, tcFlags testcafeFlags, isCLIDriven bool) (i } cleanTestCafePackages(&p) - return r.RunProject() + return r.RunProject(cmd.Context()) } func applyTestcafeFlags(p *testcafe.Project, flags testcafeFlags) error { diff --git a/internal/cmd/run/xcuitest.go b/internal/cmd/run/xcuitest.go index d2a4004dc..4daae0509 100644 --- a/internal/cmd/run/xcuitest.go +++ b/internal/cmd/run/xcuitest.go @@ -1,6 +1,7 @@ package run import ( + "context" "os" cmds "github.com/saucelabs/saucectl/internal/cmd" @@ -130,10 +131,10 @@ func runXcuitest(cmd *cobra.Command, xcuiFlags xcuitestFlags, isCLIDriven bool) cleanupArtifacts(p.Artifacts) - return runXcuitestInCloud(p, regio) + return runXcuitestInCloud(cmd.Context(), p, regio) } -func runXcuitestInCloud(p xcuitest.Project, regio region.Region) (int, error) { +func runXcuitestInCloud(ctx context.Context, p xcuitest.Project, regio region.Region) (int, error) { log.Info(). Str("region", regio.String()). Str("tunnel", p.Sauce.Tunnel.Name). @@ -180,7 +181,7 @@ func runXcuitestInCloud(p xcuitest.Project, regio region.Region) (int, error) { }, }, } - return r.RunProject() + return r.RunProject(ctx) } func applyXCUITestFlags(p *xcuitest.Project, flags xcuitestFlags) error { diff --git a/internal/http/rdcservice_test.go b/internal/http/rdcservice_test.go index effa49e8f..6a40f729b 100644 --- a/internal/http/rdcservice_test.go +++ b/internal/http/rdcservice_test.go @@ -493,7 +493,7 @@ func TestRDCService_StartJob(t *testing.T) { { name: "Happy path", args: args{ - ctx: context.TODO(), + ctx: context.Background(), jobStarterPayload: job.StartOptions{ User: "fake-user", AccessKey: "fake-access-key", @@ -519,7 +519,7 @@ func TestRDCService_StartJob(t *testing.T) { { name: "Non 2xx status code", args: args{ - ctx: context.TODO(), + ctx: context.Background(), jobStarterPayload: job.StartOptions{}, }, want: job.Job{}, @@ -531,7 +531,7 @@ func TestRDCService_StartJob(t *testing.T) { { name: "Unknown error", args: args{ - ctx: context.TODO(), + ctx: context.Background(), jobStarterPayload: job.StartOptions{}, }, want: job.Job{}, diff --git a/internal/http/webdriver_test.go b/internal/http/webdriver_test.go index 24db43863..a0fddd7ea 100644 --- a/internal/http/webdriver_test.go +++ b/internal/http/webdriver_test.go @@ -34,7 +34,7 @@ func TestWebdriver_StartJob(t *testing.T) { { name: "Happy path", args: args{ - ctx: context.TODO(), + ctx: context.Background(), jobStarterPayload: job.StartOptions{ User: "fake-user", AccessKey: "fake-access-key", @@ -61,7 +61,7 @@ func TestWebdriver_StartJob(t *testing.T) { { name: "Non 2xx status code", args: args{ - ctx: context.TODO(), + ctx: context.Background(), jobStarterPayload: job.StartOptions{}, }, want: job.Job{}, @@ -74,7 +74,7 @@ func TestWebdriver_StartJob(t *testing.T) { { name: "Unknown error", args: args{ - ctx: context.TODO(), + ctx: context.Background(), jobStarterPayload: job.StartOptions{}, }, want: job.Job{}, diff --git a/internal/saucecloud/cloud.go b/internal/saucecloud/cloud.go index a1fd1c414..550932cc7 100644 --- a/internal/saucecloud/cloud.go +++ b/internal/saucecloud/cloud.go @@ -90,13 +90,13 @@ type result struct { // ConsoleLogAsset represents job asset log file name. const ConsoleLogAsset = "console.log" -func (r *CloudRunner) createWorkerPool(ccy int, maxRetries int) (chan job.StartOptions, chan result) { +func (r *CloudRunner) createWorkerPool(ctx context.Context, ccy int, maxRetries int) (chan job.StartOptions, chan result) { jobOpts := make(chan job.StartOptions, maxRetries+1) results := make(chan result, ccy) log.Info().Int("concurrency", ccy).Msg("Launching workers.") for i := 0; i < ccy; i++ { - go r.runJobs(jobOpts, results) + go r.runJobs(ctx, jobOpts, results) } return jobOpts, results @@ -300,7 +300,7 @@ func (r *CloudRunner) shouldRetry(opts job.StartOptions, jobData job.Job, skippe (shouldRetryJob(jobData, skipped) || belowThreshold(opts)) } -func (r *CloudRunner) runJobs(jobOpts chan job.StartOptions, results chan<- result) { +func (r *CloudRunner) runJobs(ctx context.Context, jobOpts chan job.StartOptions, results chan<- result) { for opts := range jobOpts { start := time.Now() @@ -353,7 +353,7 @@ func (r *CloudRunner) runJobs(jobOpts chan job.StartOptions, results chan<- resu Status: jobData.Status, TestSuites: junit.TestSuites{}, }) - go r.Retrier.Retry(jobOpts, opts, jobData) + go r.Retrier.Retry(ctx, jobOpts, opts, jobData) continue } @@ -409,7 +409,7 @@ func (r *CloudRunner) runJobs(jobOpts chan job.StartOptions, results chan<- resu // remoteArchiveProject archives the contents of the folder and uploads to remote storage. // Returns the app URI for the uploaded project and additional URIs for the // runner config, node_modules, and other resources. -func (r *CloudRunner) remoteArchiveProject(project interface{}, projectDir string, sauceignoreFile string, dryRun bool) (app string, otherApps []string, err error) { +func (r *CloudRunner) remoteArchiveProject(ctx context.Context, project interface{}, projectDir string, sauceignoreFile string, dryRun bool) (app string, otherApps []string, err error) { tempDir, err := os.MkdirTemp(os.TempDir(), "saucectl-app-payload-") if err != nil { return @@ -434,7 +434,7 @@ func (r *CloudRunner) remoteArchiveProject(project interface{}, projectDir strin return } - uris, err := r.uploadFiles(archives, dryRun) + uris, err := r.uploadFiles(ctx, archives, dryRun) if err != nil { return } @@ -444,7 +444,7 @@ func (r *CloudRunner) remoteArchiveProject(project interface{}, projectDir strin return } if need { - nodeModulesURI, err := r.handleNodeModules(tempDir, projectDir, matcher, dryRun) + nodeModulesURI, err := r.handleNodeModules(ctx, tempDir, projectDir, matcher, dryRun) if err != nil { return "", nil, err } @@ -501,7 +501,7 @@ func (r *CloudRunner) createArchives(tempDir, projectDir string, project interfa // Checks if npm dependencies are taggable and if a tagged version of node_modules already exists in storage. // If an existing archive is found, it returns the URI of that archive. // If not, it creates a new archive, uploads it, and returns the storage ID. -func (r *CloudRunner) handleNodeModules(tempDir, projectDir string, matcher sauceignore.Matcher, dryRun bool) (string, error) { +func (r *CloudRunner) handleNodeModules(ctx context.Context, tempDir, projectDir string, matcher sauceignore.Matcher, dryRun bool) (string, error) { var tags []string if taggableModules(projectDir, r.NPMDependencies) { @@ -512,7 +512,7 @@ func (r *CloudRunner) handleNodeModules(tempDir, projectDir string, matcher sauc tags = append(tags, tag) log.Info().Msgf("Searching remote node_modules archive by tag %s", tag) - existingURI := r.findTaggedArchives(tag) + existingURI := r.findTaggedArchives(ctx, tag) if existingURI != "" { log.Info().Msgf("Skipping upload, using %s", existingURI) return existingURI, nil @@ -524,7 +524,7 @@ func (r *CloudRunner) handleNodeModules(tempDir, projectDir string, matcher sauc return "", fmt.Errorf("failed to archive node_modules: %w", err) } - return r.uploadArchive(storage.FileInfo{Name: archive, Tags: tags}, nodeModulesUpload, dryRun) + return r.uploadArchive(ctx, storage.FileInfo{Name: archive, Tags: tags}, nodeModulesUpload, dryRun) } func needsNodeModules(projectDir string, matcher sauceignore.Matcher, dependencies []string) (bool, error) { @@ -554,8 +554,8 @@ func taggableModules(dir string, npmDependencies []string) bool { } // findTaggedArchives searches storage for a tagged archive with a matching tag. -func (r *CloudRunner) findTaggedArchives(tag string) string { - list, err := r.ProjectUploader.List(context.TODO(), storage.ListOptions{Tags: []string{tag}, MaxResults: 1}) +func (r *CloudRunner) findTaggedArchives(ctx context.Context, tag string) string { + list, err := r.ProjectUploader.List(ctx, storage.ListOptions{Tags: []string{tag}, MaxResults: 1}) if err != nil { log.Err(err).Msgf("Failed to retrieve file with tag %q from storage", tag) return "" @@ -568,10 +568,10 @@ func (r *CloudRunner) findTaggedArchives(tag string) string { } // uploadFiles uploads each archive and returns a map of URIs. -func (r *CloudRunner) uploadFiles(archives map[uploadType]string, dryRun bool) (map[uploadType]string, error) { +func (r *CloudRunner) uploadFiles(ctx context.Context, archives map[uploadType]string, dryRun bool) (map[uploadType]string, error) { uris := make(map[uploadType]string) for uploadType, path := range archives { - uri, err := r.uploadArchive(storage.FileInfo{Name: path}, uploadType, dryRun) + uri, err := r.uploadArchive(ctx, storage.FileInfo{Name: path}, uploadType, dryRun) if err != nil { return nil, fmt.Errorf("failed to upload %s archive: %w", uploadType, err) } @@ -586,7 +586,7 @@ func fileExists(path string) bool { } // remoteArchiveFiles archives the files to a remote storage. -func (r *CloudRunner) remoteArchiveFiles(project interface{}, files []string, sauceignoreFile string, dryRun bool) (string, error) { +func (r *CloudRunner) remoteArchiveFiles(ctx context.Context, project interface{}, files []string, sauceignoreFile string, dryRun bool) (string, error) { tempDir, err := os.MkdirTemp(os.TempDir(), "saucectl-app-payload-") if err != nil { return "", err @@ -616,7 +616,7 @@ func (r *CloudRunner) remoteArchiveFiles(project interface{}, files []string, sa var uris []string for k, v := range archives { - uri, err := r.uploadArchive(storage.FileInfo{Name: v}, k, dryRun) + uri, err := r.uploadArchive(ctx, storage.FileInfo{Name: v}, k, dryRun) if err != nil { return "", err } @@ -686,10 +686,10 @@ var ( otherAppsUpload uploadType = "other applications" ) -func (r *CloudRunner) uploadArchives(filenames []string, pType uploadType, dryRun bool) ([]string, error) { +func (r *CloudRunner) uploadArchives(ctx context.Context, filenames []string, pType uploadType, dryRun bool) ([]string, error) { var IDs []string for _, f := range filenames { - ID, err := r.uploadArchive(storage.FileInfo{Name: f}, pType, dryRun) + ID, err := r.uploadArchive(ctx, storage.FileInfo{Name: f}, pType, dryRun) if err != nil { return []string{}, err } @@ -699,7 +699,7 @@ func (r *CloudRunner) uploadArchives(filenames []string, pType uploadType, dryRu return IDs, nil } -func (r *CloudRunner) uploadArchive(fileInfo storage.FileInfo, pType uploadType, dryRun bool) (string, error) { +func (r *CloudRunner) uploadArchive(ctx context.Context, fileInfo storage.FileInfo, pType uploadType, dryRun bool) (string, error) { filename := fileInfo.Name if dryRun { log.Info().Str("file", filename).Msgf("Skipping upload in dry run.") @@ -714,7 +714,7 @@ func (r *CloudRunner) uploadArchive(fileInfo storage.FileInfo, pType uploadType, log.Info().Msgf("Downloading from remote: %s", filename) progress.Show("Downloading %s", filename) - dest, err := r.download(filename) + dest, err := r.download(ctx, filename) progress.Stop() if err != nil { return "", fmt.Errorf("unable to download app from %s: %w", filename, err) @@ -725,7 +725,7 @@ func (r *CloudRunner) uploadArchive(fileInfo storage.FileInfo, pType uploadType, } log.Info().Msgf("Checking if %s has already been uploaded previously", filename) - if storageID, _ := r.isFileStored(filename); storageID != "" { + if storageID, _ := r.isFileStored(ctx, filename); storageID != "" { log.Info().Msgf("Skipping upload, using storage:%s", storageID) return fmt.Sprintf("storage:%s", storageID), nil } @@ -743,7 +743,7 @@ func (r *CloudRunner) uploadArchive(fileInfo storage.FileInfo, pType uploadType, progress.Show("Uploading %s %s", pType, filename) start := time.Now() resp, err := r.ProjectUploader.UploadStream( - context.TODO(), + ctx, storage.FileInfo{ Name: filepath.Base(filename), Description: fileInfo.Description, @@ -764,7 +764,7 @@ func (r *CloudRunner) uploadArchive(fileInfo storage.FileInfo, pType uploadType, // isFileStored calculates the checksum of the given file and looks up its existence in the Sauce Labs app storage. // Returns an empty string if no file was found. -func (r *CloudRunner) isFileStored(filename string) (storageID string, err error) { +func (r *CloudRunner) isFileStored(ctx context.Context, filename string) (storageID string, err error) { hash, err := hashio.SHA256(filename) if err != nil { return "", err @@ -772,7 +772,7 @@ func (r *CloudRunner) isFileStored(filename string) (storageID string, err error log.Info().Msgf("Checksum: %s", hash) - l, err := r.ProjectUploader.List(context.TODO(), storage.ListOptions{ + l, err := r.ProjectUploader.List(ctx, storage.ListOptions{ SHA256: hash, MaxResults: 1, }) @@ -1105,8 +1105,8 @@ func arrayContains(list []string, want string) bool { } // download downloads the resource the URL points to and returns its local path. -func (r *CloudRunner) download(url string) (string, error) { - reader, _, err := r.ProjectUploader.DownloadURL(context.TODO(), url) +func (r *CloudRunner) download(ctx context.Context, url string) (string, error) { + reader, _, err := r.ProjectUploader.DownloadURL(ctx, url) if err != nil { return "", err } diff --git a/internal/saucecloud/cloud_test.go b/internal/saucecloud/cloud_test.go index 318e43d10..73d61fd68 100644 --- a/internal/saucecloud/cloud_test.go +++ b/internal/saucecloud/cloud_test.go @@ -1,6 +1,7 @@ package saucecloud import ( + "context" "os" "path/filepath" "strings" @@ -45,7 +46,7 @@ func TestRunJobsSkipped(t *testing.T) { opts := make(chan job.StartOptions) results := make(chan result) - go r.runJobs(opts, results) + go r.runJobs(context.Background(), opts, results) opts <- job.StartOptions{} close(opts) res := <-results diff --git a/internal/saucecloud/cucumber.go b/internal/saucecloud/cucumber.go index ad51f4653..f67f6ca88 100644 --- a/internal/saucecloud/cucumber.go +++ b/internal/saucecloud/cucumber.go @@ -22,7 +22,7 @@ type CucumberRunner struct { } // RunProject runs the defined tests on sauce cloud -func (r *CucumberRunner) RunProject() (int, error) { +func (r *CucumberRunner) RunProject(ctx context.Context) (int, error) { m, err := r.MetadataSearchStrategy.Find(context.Background(), r.MetadataService, playwright.Kind, r.Project.Playwright.Version) if err != nil { r.logFrameworkError(err) @@ -46,7 +46,7 @@ func (r *CucumberRunner) RunProject() (int, error) { return 1, err } - app, otherApps, err := r.remoteArchiveProject(r.Project, r.Project.RootDir, r.Project.Sauce.Sauceignore, r.Project.DryRun) + app, otherApps, err := r.remoteArchiveProject(ctx, r.Project, r.Project.RootDir, r.Project.Sauce.Sauceignore, r.Project.DryRun) if err != nil { return 1, err } @@ -56,7 +56,7 @@ func (r *CucumberRunner) RunProject() (int, error) { return 0, nil } - passed := r.runSuites(app, otherApps) + passed := r.runSuites(ctx, app, otherApps) if !passed { return 1, nil } @@ -133,11 +133,11 @@ func (r *CucumberRunner) getSuiteNames() []string { return names } -func (r *CucumberRunner) runSuites(app string, otherApps []string) bool { +func (r *CucumberRunner) runSuites(ctx context.Context, app string, otherApps []string) bool { sigChan := r.registerSkipSuitesOnSignal() defer unregisterSignalCapture(sigChan) - jobOpts, results := r.createWorkerPool(r.Project.Sauce.Concurrency, r.Project.Sauce.Retries) + jobOpts, results := r.createWorkerPool(ctx, r.Project.Sauce.Concurrency, r.Project.Sauce.Retries) defer close(results) suites := r.Project.Suites diff --git a/internal/saucecloud/cypress.go b/internal/saucecloud/cypress.go index bfbc68214..998a27994 100644 --- a/internal/saucecloud/cypress.go +++ b/internal/saucecloud/cypress.go @@ -21,7 +21,7 @@ type CypressRunner struct { } // RunProject runs the tests defined in cypress.Project. -func (r *CypressRunner) RunProject() (int, error) { +func (r *CypressRunner) RunProject(ctx context.Context) (int, error) { m, err := r.MetadataSearchStrategy.Find(context.Background(), r.MetadataService, cypress.Kind, r.Project.GetVersion()) if err != nil { r.logFrameworkError(err) @@ -45,7 +45,7 @@ func (r *CypressRunner) RunProject() (int, error) { return 1, err } - app, otherApps, err := r.remoteArchiveProject(r.Project, r.Project.GetRootDir(), r.Project.GetSauceCfg().Sauceignore, r.Project.IsDryRun()) + app, otherApps, err := r.remoteArchiveProject(ctx, r.Project, r.Project.GetRootDir(), r.Project.GetSauceCfg().Sauceignore, r.Project.IsDryRun()) if err != nil { return 1, err } @@ -55,7 +55,7 @@ func (r *CypressRunner) RunProject() (int, error) { return 0, nil } - passed := r.runSuites(app, otherApps) + passed := r.runSuites(ctx, app, otherApps) if !passed { return 1, nil } @@ -125,10 +125,10 @@ func (r *CypressRunner) validateFramework(m framework.Metadata) error { return nil } -func (r *CypressRunner) runSuites(app string, otherApps []string) bool { +func (r *CypressRunner) runSuites(ctx context.Context, app string, otherApps []string) bool { sigChan := r.registerSkipSuitesOnSignal() defer unregisterSignalCapture(sigChan) - jobOpts, results := r.createWorkerPool(r.Project.GetSauceCfg().Concurrency, r.Project.GetSauceCfg().Retries) + jobOpts, results := r.createWorkerPool(ctx, r.Project.GetSauceCfg().Concurrency, r.Project.GetSauceCfg().Retries) defer close(results) suites := r.Project.GetSuites() diff --git a/internal/saucecloud/espresso.go b/internal/saucecloud/espresso.go index 4c8ccebc1..7c4b2b01c 100644 --- a/internal/saucecloud/espresso.go +++ b/internal/saucecloud/espresso.go @@ -1,6 +1,7 @@ package saucecloud import ( + "context" "fmt" "strings" @@ -33,7 +34,7 @@ type EspressoRunner struct { } // RunProject runs the tests defined in cypress.Project. -func (r *EspressoRunner) RunProject() (int, error) { +func (r *EspressoRunner) RunProject(ctx context.Context) (int, error) { exitCode := 1 if err := r.validateTunnel( @@ -47,6 +48,7 @@ func (r *EspressoRunner) RunProject() (int, error) { var err error r.Project.Espresso.App, err = r.uploadArchive( + ctx, storage.FileInfo{Name: r.Project.Espresso.App, Description: r.Project.Espresso.AppDescription}, appUpload, r.Project.DryRun, @@ -55,7 +57,7 @@ func (r *EspressoRunner) RunProject() (int, error) { return exitCode, err } - r.Project.Espresso.OtherApps, err = r.uploadArchives(r.Project.Espresso.OtherApps, otherAppsUpload, r.Project.DryRun) + r.Project.Espresso.OtherApps, err = r.uploadArchives(ctx, r.Project.Espresso.OtherApps, otherAppsUpload, r.Project.DryRun) if err != nil { return exitCode, err } @@ -68,6 +70,7 @@ func (r *EspressoRunner) RunProject() (int, error) { } testAppURL, err := r.uploadArchive( + ctx, storage.FileInfo{Name: suite.TestApp, Description: suite.TestAppDescription}, testAppUpload, r.Project.DryRun, @@ -84,7 +87,7 @@ func (r *EspressoRunner) RunProject() (int, error) { return 0, nil } - passed := r.runSuites() + passed := r.runSuites(ctx) if passed { exitCode = 0 } @@ -92,12 +95,12 @@ func (r *EspressoRunner) RunProject() (int, error) { return exitCode, nil } -func (r *EspressoRunner) runSuites() bool { +func (r *EspressoRunner) runSuites(ctx context.Context) bool { sigChan := r.registerSkipSuitesOnSignal() defer unregisterSignalCapture(sigChan) jobOpts, results := r.createWorkerPool( - r.Project.Sauce.Concurrency, r.Project.Sauce.Retries, + ctx, r.Project.Sauce.Concurrency, r.Project.Sauce.Retries, ) defer close(results) diff --git a/internal/saucecloud/playwright.go b/internal/saucecloud/playwright.go index 839248cdc..386257f27 100644 --- a/internal/saucecloud/playwright.go +++ b/internal/saucecloud/playwright.go @@ -26,7 +26,7 @@ var PlaywrightBrowserMap = map[string]string{ } // RunProject runs the tests defined in cypress.Project. -func (r *PlaywrightRunner) RunProject() (int, error) { +func (r *PlaywrightRunner) RunProject(ctx context.Context) (int, error) { m, err := r.MetadataSearchStrategy.Find(context.Background(), r.MetadataService, playwright.Kind, r.Project.Playwright.Version) if err != nil { r.logFrameworkError(err) @@ -50,7 +50,7 @@ func (r *PlaywrightRunner) RunProject() (int, error) { return 1, err } - app, otherApps, err := r.remoteArchiveProject(r.Project, r.Project.RootDir, r.Project.Sauce.Sauceignore, r.Project.DryRun) + app, otherApps, err := r.remoteArchiveProject(ctx, r.Project, r.Project.RootDir, r.Project.Sauce.Sauceignore, r.Project.DryRun) if err != nil { return 1, err } @@ -60,7 +60,7 @@ func (r *PlaywrightRunner) RunProject() (int, error) { return 0, nil } - passed := r.runSuites(app, otherApps) + passed := r.runSuites(ctx, app, otherApps) if !passed { return 1, nil } @@ -138,11 +138,11 @@ func (r *PlaywrightRunner) getSuiteNames() []string { return names } -func (r *PlaywrightRunner) runSuites(app string, otherApps []string) bool { +func (r *PlaywrightRunner) runSuites(ctx context.Context, app string, otherApps []string) bool { sigChan := r.registerSkipSuitesOnSignal() defer unregisterSignalCapture(sigChan) - jobOpts, results := r.createWorkerPool(r.Project.Sauce.Concurrency, r.Project.Sauce.Retries) + jobOpts, results := r.createWorkerPool(ctx, r.Project.Sauce.Concurrency, r.Project.Sauce.Retries) defer close(results) suites := r.Project.Suites diff --git a/internal/saucecloud/replay.go b/internal/saucecloud/replay.go index 4eaf88f0d..bc50aacbd 100644 --- a/internal/saucecloud/replay.go +++ b/internal/saucecloud/replay.go @@ -18,7 +18,7 @@ type ReplayRunner struct { } // RunProject runs the tests defined in cypress.Project. -func (r *ReplayRunner) RunProject() (int, error) { +func (r *ReplayRunner) RunProject(ctx context.Context) (int, error) { exitCode := 1 m, err := r.MetadataSearchStrategy.Find(context.Background(), r.MetadataService, replay.Kind, "latest") @@ -53,7 +53,7 @@ func (r *ReplayRunner) RunProject() (int, error) { files = append(files, suite.Recording) } - fileURIs, err := r.remoteArchiveFiles(r.Project, files, "", r.Project.DryRun) + fileURIs, err := r.remoteArchiveFiles(ctx, r.Project, files, "", r.Project.DryRun) if err != nil { return exitCode, err } @@ -63,7 +63,7 @@ func (r *ReplayRunner) RunProject() (int, error) { return 0, nil } - passed := r.runSuites(fileURIs) + passed := r.runSuites(ctx, fileURIs) if passed { exitCode = 0 } @@ -71,11 +71,11 @@ func (r *ReplayRunner) RunProject() (int, error) { return exitCode, nil } -func (r *ReplayRunner) runSuites(fileURI string) bool { +func (r *ReplayRunner) runSuites(ctx context.Context, fileURI string) bool { sigChan := r.registerSkipSuitesOnSignal() defer unregisterSignalCapture(sigChan) - jobOpts, results := r.createWorkerPool(r.Project.Sauce.Concurrency, r.Project.Sauce.Retries) + jobOpts, results := r.createWorkerPool(ctx, r.Project.Sauce.Concurrency, r.Project.Sauce.Retries) defer close(results) suites := r.Project.Suites diff --git a/internal/saucecloud/retry/basicretrier.go b/internal/saucecloud/retry/basicretrier.go index 31c527cdb..ec87acc93 100644 --- a/internal/saucecloud/retry/basicretrier.go +++ b/internal/saucecloud/retry/basicretrier.go @@ -1,6 +1,7 @@ package retry import ( + "context" "fmt" "github.com/rs/zerolog/log" @@ -9,7 +10,7 @@ import ( type BasicRetrier struct{} -func (b *BasicRetrier) Retry(jobOpts chan<- job.StartOptions, opt job.StartOptions, _ job.Job) { +func (b *BasicRetrier) Retry(_ context.Context, jobOpts chan<- job.StartOptions, opt job.StartOptions, _ job.Job) { log.Info().Str("suite", opt.DisplayName). Str("attempt", fmt.Sprintf("%d of %d", opt.Attempt+1, opt.Retries+1)). Msg("Retrying suite.") diff --git a/internal/saucecloud/retry/basicretrier_test.go b/internal/saucecloud/retry/basicretrier_test.go index b684acdf4..6bb7cf601 100644 --- a/internal/saucecloud/retry/basicretrier_test.go +++ b/internal/saucecloud/retry/basicretrier_test.go @@ -1,6 +1,7 @@ package retry import ( + "context" "testing" "github.com/saucelabs/saucectl/internal/job" @@ -35,7 +36,7 @@ func TestBasicRetrier_Retry(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b := &BasicRetrier{} - go b.Retry(tt.args.jobOpts, tt.args.opt, tt.args.previous) + go b.Retry(context.Background(), tt.args.jobOpts, tt.args.opt, tt.args.previous) newOpt := <-tt.args.jobOpts assert.Equal(t, tt.expected, newOpt) }) diff --git a/internal/saucecloud/retry/junitretrier.go b/internal/saucecloud/retry/junitretrier.go index 035c51534..133fddc5f 100644 --- a/internal/saucecloud/retry/junitretrier.go +++ b/internal/saucecloud/retry/junitretrier.go @@ -17,11 +17,11 @@ type JunitRetrier struct { JobService job.Service } -func (b *JunitRetrier) Retry(jobOpts chan<- job.StartOptions, opt job.StartOptions, previous job.Job) { +func (b *JunitRetrier) Retry(ctx context.Context, jobOpts chan<- job.StartOptions, opt job.StartOptions, previous job.Job) { var tests []string if opt.SmartRetry.FailedOnly { - tests = b.retryFailedTests(&opt, previous) + tests = b.retryFailedTests(ctx, &opt, previous) if len(tests) == 0 { log.Info().Msg(msg.SkippingSmartRetries) } @@ -40,14 +40,14 @@ func (b *JunitRetrier) Retry(jobOpts chan<- job.StartOptions, opt job.StartOptio jobOpts <- opt } -func (b *JunitRetrier) retryFailedTests(opt *job.StartOptions, previous job.Job) []string { +func (b *JunitRetrier) retryFailedTests(ctx context.Context, opt *job.StartOptions, previous job.Job) []string { if previous.Status == job.StateError { log.Warn().Msg(msg.UnreliableReport) return nil } content, err := b.JobService.Artifact( - context.Background(), previous.ID, junit.FileName, previous.IsRDC, + ctx, previous.ID, junit.FileName, previous.IsRDC, ) if err != nil { log.Err(err).Msgf(msg.UnableToFetchFile, junit.FileName) diff --git a/internal/saucecloud/retry/junitretrier_test.go b/internal/saucecloud/retry/junitretrier_test.go index a28343052..3be3adeab 100644 --- a/internal/saucecloud/retry/junitretrier_test.go +++ b/internal/saucecloud/retry/junitretrier_test.go @@ -334,7 +334,7 @@ func TestAppsRetrier_Retry(t *testing.T) { b := &JunitRetrier{ JobService: tt.init.JobService, } - go b.Retry(tt.args.jobOpts, tt.args.opt, tt.args.previous) + go b.Retry(context.Background(), tt.args.jobOpts, tt.args.opt, tt.args.previous) newOpt := <-tt.args.jobOpts assert.Equal(t, tt.expected, newOpt) }) diff --git a/internal/saucecloud/retry/retrier.go b/internal/saucecloud/retry/retrier.go index 55b4ac708..98d8d755b 100644 --- a/internal/saucecloud/retry/retrier.go +++ b/internal/saucecloud/retry/retrier.go @@ -1,8 +1,12 @@ package retry -import "github.com/saucelabs/saucectl/internal/job" +import ( + "context" + + "github.com/saucelabs/saucectl/internal/job" +) // Retrier represents the retry strategy. type Retrier interface { - Retry(c chan<- job.StartOptions, opt job.StartOptions, previous job.Job) + Retry(ctx context.Context, c chan<- job.StartOptions, opt job.StartOptions, previous job.Job) } diff --git a/internal/saucecloud/retry/saucereportretrier.go b/internal/saucecloud/retry/saucereportretrier.go index 9e9d33530..10bcd1980 100644 --- a/internal/saucecloud/retry/saucereportretrier.go +++ b/internal/saucecloud/retry/saucereportretrier.go @@ -23,9 +23,9 @@ type SauceReportRetrier struct { Project Project } -func (r *SauceReportRetrier) Retry(jobOpts chan<- job.StartOptions, opt job.StartOptions, previous job.Job) { +func (r *SauceReportRetrier) Retry(ctx context.Context, jobOpts chan<- job.StartOptions, opt job.StartOptions, previous job.Job) { if opt.SmartRetry.FailedOnly { - if ok := r.retryFailedTests(&opt, previous); !ok { + if ok := r.retryFailedTests(ctx, &opt, previous); !ok { log.Info().Msg(msg.SkippingSmartRetries) } } @@ -36,7 +36,7 @@ func (r *SauceReportRetrier) Retry(jobOpts chan<- job.StartOptions, opt job.Star jobOpts <- opt } -func (r *SauceReportRetrier) retryFailedTests(opt *job.StartOptions, previous job.Job) bool { +func (r *SauceReportRetrier) retryFailedTests(ctx context.Context, opt *job.StartOptions, previous job.Job) bool { if previous.Status == job.StateError { log.Warn().Msg(msg.UnreliableReport) return false @@ -66,7 +66,7 @@ func (r *SauceReportRetrier) retryFailedTests(opt *job.StartOptions, previous jo return false } - storageID, err := r.uploadConfig(runnerFile) + storageID, err := r.uploadConfig(ctx, runnerFile) if err != nil { log.Err(err).Msgf(msg.UnableToUploadConfig, runnerFile) return false @@ -85,7 +85,7 @@ func (r *SauceReportRetrier) retryFailedTests(opt *job.StartOptions, previous jo return true } -func (r *SauceReportRetrier) uploadConfig(filename string) (string, error) { +func (r *SauceReportRetrier) uploadConfig(ctx context.Context, filename string) (string, error) { filename, err := filepath.Abs(filename) if err != nil { return "", err @@ -98,7 +98,7 @@ func (r *SauceReportRetrier) uploadConfig(filename string) (string, error) { progress.Show("Uploading runner config %s", filename) start := time.Now() - resp, err := r.ProjectUploader.UploadStream(context.TODO(), storage.FileInfo{Name: filepath.Base(filename)}, file) + resp, err := r.ProjectUploader.UploadStream(ctx, storage.FileInfo{Name: filepath.Base(filename)}, file) progress.Stop() if err != nil { return "", err diff --git a/internal/saucecloud/retry/saucereportretrier_test.go b/internal/saucecloud/retry/saucereportretrier_test.go index 2717d06e5..2ae86c992 100644 --- a/internal/saucecloud/retry/saucereportretrier_test.go +++ b/internal/saucecloud/retry/saucereportretrier_test.go @@ -191,7 +191,7 @@ func TestSauceReportRetrier_Retry(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b := tt.retrier - go b.Retry(tt.args.jobOpts, tt.args.opt, tt.args.previous) + go b.Retry(context.Background(), tt.args.jobOpts, tt.args.opt, tt.args.previous) newOpt := <-tt.args.jobOpts assert.Equal(t, tt.expected, newOpt) }) diff --git a/internal/saucecloud/testcafe.go b/internal/saucecloud/testcafe.go index 7a5bb7e11..de0f42d8e 100644 --- a/internal/saucecloud/testcafe.go +++ b/internal/saucecloud/testcafe.go @@ -21,7 +21,7 @@ type TestcafeRunner struct { } // RunProject runs the defined tests on sauce cloud -func (r *TestcafeRunner) RunProject() (int, error) { +func (r *TestcafeRunner) RunProject(ctx context.Context) (int, error) { m, err := r.MetadataSearchStrategy.Find(context.Background(), r.MetadataService, testcafe.Kind, r.Project.Testcafe.Version) if err != nil { r.logFrameworkError(err) @@ -45,7 +45,7 @@ func (r *TestcafeRunner) RunProject() (int, error) { return 1, err } - app, otherApps, err := r.remoteArchiveProject(r.Project, r.Project.RootDir, r.Project.Sauce.Sauceignore, r.Project.DryRun) + app, otherApps, err := r.remoteArchiveProject(ctx, r.Project, r.Project.RootDir, r.Project.Sauce.Sauceignore, r.Project.DryRun) if err != nil { return 1, err } @@ -55,7 +55,7 @@ func (r *TestcafeRunner) RunProject() (int, error) { return 0, nil } - passed := r.runSuites(app, otherApps) + passed := r.runSuites(ctx, app, otherApps) if !passed { return 1, nil } @@ -133,11 +133,11 @@ func (r *TestcafeRunner) getSuiteNames() []string { return names } -func (r *TestcafeRunner) runSuites(app string, otherApps []string) bool { +func (r *TestcafeRunner) runSuites(ctx context.Context, app string, otherApps []string) bool { sigChan := r.registerSkipSuitesOnSignal() defer unregisterSignalCapture(sigChan) - jobOpts, results := r.createWorkerPool(r.Project.Sauce.Concurrency, r.Project.Sauce.Retries) + jobOpts, results := r.createWorkerPool(ctx, r.Project.Sauce.Concurrency, r.Project.Sauce.Retries) defer close(results) suites := r.Project.Suites diff --git a/internal/saucecloud/xcuitest.go b/internal/saucecloud/xcuitest.go index b9f228dab..5b8c2f1a1 100644 --- a/internal/saucecloud/xcuitest.go +++ b/internal/saucecloud/xcuitest.go @@ -1,6 +1,7 @@ package saucecloud import ( + "context" "fmt" "os" "path" @@ -56,7 +57,7 @@ var ( ) // RunProject runs the tests defined in xcuitest.Project. -func (r *XcuitestRunner) RunProject() (int, error) { +func (r *XcuitestRunner) RunProject(ctx context.Context) (int, error) { exitCode := 1 if err := r.validateTunnel( @@ -84,7 +85,7 @@ func (r *XcuitestRunner) RunProject() (int, error) { cachedUpload := func(path string, description string, pType uploadType, dryRun bool) (string, error) { return uploadCache.lookup(path, func() (string, error) { - return r.uploadArchive(storage.FileInfo{Name: path, Description: description}, pType, dryRun) + return r.uploadArchive(ctx, storage.FileInfo{Name: path, Description: description}, pType, dryRun) }) } @@ -142,7 +143,7 @@ func (r *XcuitestRunner) RunProject() (int, error) { return 0, nil } - passed := r.runSuites() + passed := r.runSuites(ctx) if passed { exitCode = 0 } @@ -161,11 +162,11 @@ func (r *XcuitestRunner) dryRun() { fmt.Println() } -func (r *XcuitestRunner) runSuites() bool { +func (r *XcuitestRunner) runSuites(ctx context.Context) bool { sigChan := r.registerSkipSuitesOnSignal() defer unregisterSignalCapture(sigChan) - jobOpts, results := r.createWorkerPool(r.Project.Sauce.Concurrency, r.Project.Sauce.Retries) + jobOpts, results := r.createWorkerPool(ctx, r.Project.Sauce.Concurrency, r.Project.Sauce.Retries) defer close(results) suites := r.Project.Suites