From 04e01963d52ead4ad531cd678f122bd0f0ce073c Mon Sep 17 00:00:00 2001 From: Mike Han <56001373+mhan83@users.noreply.github.com> Date: Fri, 16 Aug 2024 11:37:03 -0600 Subject: [PATCH] refactor: Encapsulate artifact downloading (#936) * Add Downloader wrapper * Extract artifact downloading out of api clients * Move test to new package * lint * lint --- internal/cmd/artifacts/cmd.go | 3 +- internal/cmd/ini/initializer.go | 2 +- internal/cmd/run/cucumber.go | 8 ++- internal/cmd/run/cypress.go | 8 ++- internal/cmd/run/espresso.go | 11 ++-- internal/cmd/run/playwright.go | 8 ++- internal/cmd/run/replay.go | 8 ++- internal/cmd/run/testcafe.go | 8 ++- internal/cmd/run/xcuitest.go | 10 +-- internal/http/rdcservice.go | 62 +++--------------- internal/http/rdcservice_test.go | 65 +++---------------- internal/http/resto.go | 52 ++------------- internal/saucecloud/downloader/downloader.go | 59 +++++++++++++++++ .../saucecloud/downloader/downloader_test.go | 60 +++++++++++++++++ 14 files changed, 181 insertions(+), 183 deletions(-) create mode 100644 internal/saucecloud/downloader/downloader.go create mode 100644 internal/saucecloud/downloader/downloader_test.go diff --git a/internal/cmd/artifacts/cmd.go b/internal/cmd/artifacts/cmd.go index 910aa6b75..ec4b6acad 100644 --- a/internal/cmd/artifacts/cmd.go +++ b/internal/cmd/artifacts/cmd.go @@ -5,7 +5,6 @@ import ( "time" "github.com/saucelabs/saucectl/internal/artifacts" - "github.com/saucelabs/saucectl/internal/config" "github.com/saucelabs/saucectl/internal/credentials" "github.com/saucelabs/saucectl/internal/http" "github.com/saucelabs/saucectl/internal/region" @@ -45,7 +44,7 @@ func Command(preRun func(cmd *cobra.Command, args []string)) *cobra.Command { creds := credentials.Get() url := reg.APIBaseURL() restoClient := http.NewResto(url, creds.Username, creds.AccessKey, restoTimeout) - rdcClient := http.NewRDCService(url, creds.Username, creds.AccessKey, rdcTimeout, config.ArtifactDownload{}) + rdcClient := http.NewRDCService(url, creds.Username, creds.AccessKey, rdcTimeout) testcompClient := http.NewTestComposer(url, creds, testComposerTimeout) artifactSvc = saucecloud.NewArtifactService(&restoClient, &rdcClient, &testcompClient) diff --git a/internal/cmd/ini/initializer.go b/internal/cmd/ini/initializer.go index 8646e305e..6ddc2ee2d 100644 --- a/internal/cmd/ini/initializer.go +++ b/internal/cmd/ini/initializer.go @@ -59,7 +59,7 @@ type initializer struct { func newInitializer(stdio terminal.Stdio, creds iam.Credentials, cfg *initConfig) *initializer { r := region.FromString(cfg.region) tc := http.NewTestComposer(r.APIBaseURL(), creds, testComposerTimeout) - rc := http.NewRDCService(r.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout, config.ArtifactDownload{}) + rc := http.NewRDCService(r.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout) rs := http.NewResto(r.APIBaseURL(), creds.Username, creds.AccessKey, restoTimeout) us := http.NewUserService(r.APIBaseURL(), creds, 5*time.Second) diff --git a/internal/cmd/run/cucumber.go b/internal/cmd/run/cucumber.go index 033d69e32..9e060ad01 100644 --- a/internal/cmd/run/cucumber.go +++ b/internal/cmd/run/cucumber.go @@ -13,6 +13,7 @@ import ( "github.com/saucelabs/saucectl/internal/http" "github.com/saucelabs/saucectl/internal/region" "github.com/saucelabs/saucectl/internal/saucecloud" + "github.com/saucelabs/saucectl/internal/saucecloud/downloader" "github.com/saucelabs/saucectl/internal/saucecloud/retry" "github.com/saucelabs/saucectl/internal/segment" "github.com/saucelabs/saucectl/internal/usage" @@ -116,14 +117,15 @@ func runCucumber(cmd *cobra.Command, isCLIDriven bool) (int, error) { creds := regio.Credentials() restoClient := http.NewResto(regio.APIBaseURL(), creds.Username, creds.AccessKey, 0) - restoClient.ArtifactConfig = p.Artifacts.Download testcompClient := http.NewTestComposer(regio.APIBaseURL(), creds, testComposerTimeout) webdriverClient := http.NewWebdriver(regio.WebDriverBaseURL(), creds, webdriverTimeout) appsClient := *http.NewAppStore(regio.APIBaseURL(), creds.Username, creds.AccessKey, gFlags.appStoreTimeout) - rdcClient := http.NewRDCService(regio.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout, config.ArtifactDownload{}) + rdcClient := http.NewRDCService(regio.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout) insightsClient := http.NewInsightsService(regio.APIBaseURL(), creds, insightsTimeout) iamClient := http.NewUserService(regio.APIBaseURL(), creds, iamTimeout) + vdcDownloader := downloader.NewArtifactDownloader(&restoClient, p.Artifacts.Download) + log.Info().Msg("Running Playwright-Cucumberjs in Sauce Labs") r := saucecloud.CucumberRunner{ Project: p, @@ -137,7 +139,7 @@ func runCucumber(cmd *cobra.Command, isCLIDriven bool) (int, error) { VDCWriter: &testcompClient, VDCStopper: &restoClient, RDCStopper: &rdcClient, - VDCDownloader: &restoClient, + VDCDownloader: &vdcDownloader, }, TunnelService: &restoClient, MetadataService: &testcompClient, diff --git a/internal/cmd/run/cypress.go b/internal/cmd/run/cypress.go index 9b5063f9e..48ee6dddf 100644 --- a/internal/cmd/run/cypress.go +++ b/internal/cmd/run/cypress.go @@ -21,6 +21,7 @@ import ( "github.com/saucelabs/saucectl/internal/msg" "github.com/saucelabs/saucectl/internal/region" "github.com/saucelabs/saucectl/internal/saucecloud" + "github.com/saucelabs/saucectl/internal/saucecloud/downloader" "github.com/saucelabs/saucectl/internal/saucecloud/retry" "github.com/saucelabs/saucectl/internal/segment" "github.com/saucelabs/saucectl/internal/usage" @@ -151,14 +152,15 @@ func runCypress(cmd *cobra.Command, cflags cypressFlags, isCLIDriven bool) (int, creds := regio.Credentials() restoClient := http.NewResto(regio.APIBaseURL(), creds.Username, creds.AccessKey, 0) - restoClient.ArtifactConfig = p.GetArtifactsCfg().Download testcompClient := http.NewTestComposer(regio.APIBaseURL(), creds, testComposerTimeout) webdriverClient := http.NewWebdriver(regio.WebDriverBaseURL(), creds, webdriverTimeout) appsClient := *http.NewAppStore(regio.APIBaseURL(), creds.Username, creds.AccessKey, gFlags.appStoreTimeout) - rdcClient := http.NewRDCService(regio.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout, config.ArtifactDownload{}) + rdcClient := http.NewRDCService(regio.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout) insightsClient := http.NewInsightsService(regio.APIBaseURL(), creds, insightsTimeout) iamClient := http.NewUserService(regio.APIBaseURL(), creds, iamTimeout) + vdcDownloader := downloader.NewArtifactDownloader(&restoClient, p.GetArtifactsCfg().Download) + log.Info().Msg("Running Cypress in Sauce Labs") r := saucecloud.CypressRunner{ Project: p, @@ -172,7 +174,7 @@ func runCypress(cmd *cobra.Command, cflags cypressFlags, isCLIDriven bool) (int, VDCWriter: &testcompClient, VDCStopper: &restoClient, RDCStopper: &rdcClient, - VDCDownloader: &restoClient, + VDCDownloader: &vdcDownloader, }, MetadataService: &testcompClient, TunnelService: &restoClient, diff --git a/internal/cmd/run/espresso.go b/internal/cmd/run/espresso.go index 2e04b21c2..a32327540 100644 --- a/internal/cmd/run/espresso.go +++ b/internal/cmd/run/espresso.go @@ -19,6 +19,7 @@ import ( "github.com/saucelabs/saucectl/internal/framework" "github.com/saucelabs/saucectl/internal/region" "github.com/saucelabs/saucectl/internal/saucecloud" + "github.com/saucelabs/saucectl/internal/saucecloud/downloader" "github.com/saucelabs/saucectl/internal/saucecloud/retry" "github.com/saucelabs/saucectl/internal/segment" "github.com/saucelabs/saucectl/internal/usage" @@ -137,14 +138,16 @@ func runEspressoInCloud(p espresso.Project, regio region.Region) (int, error) { creds := regio.Credentials() restoClient := http.NewResto(regio.APIBaseURL(), creds.Username, creds.AccessKey, 0) - restoClient.ArtifactConfig = p.Artifacts.Download testcompClient := http.NewTestComposer(regio.APIBaseURL(), creds, testComposerTimeout) webdriverClient := http.NewWebdriver(regio.WebDriverBaseURL(), creds, webdriverTimeout) appsClient := *http.NewAppStore(regio.APIBaseURL(), creds.Username, creds.AccessKey, gFlags.appStoreTimeout) - rdcClient := http.NewRDCService(regio.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout, p.Artifacts.Download) + rdcClient := http.NewRDCService(regio.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout) insightsClient := http.NewInsightsService(regio.APIBaseURL(), creds, insightsTimeout) iamClient := http.NewUserService(regio.APIBaseURL(), creds, iamTimeout) + vdcDownloader := downloader.NewArtifactDownloader(&restoClient, p.Artifacts.Download) + rdcDownloader := downloader.NewArtifactDownloader(&rdcClient, p.Artifacts.Download) + r := saucecloud.EspressoRunner{ Project: p, CloudRunner: saucecloud.CloudRunner{ @@ -157,8 +160,8 @@ func runEspressoInCloud(p espresso.Project, regio region.Region) (int, error) { VDCWriter: &testcompClient, VDCStopper: &restoClient, RDCStopper: &rdcClient, - VDCDownloader: &restoClient, - RDCDownloader: &rdcClient, + VDCDownloader: &vdcDownloader, + RDCDownloader: &rdcDownloader, }, TunnelService: &restoClient, MetadataService: &testcompClient, diff --git a/internal/cmd/run/playwright.go b/internal/cmd/run/playwright.go index 17f752a91..11ea90a4f 100644 --- a/internal/cmd/run/playwright.go +++ b/internal/cmd/run/playwright.go @@ -22,6 +22,7 @@ import ( "github.com/saucelabs/saucectl/internal/playwright" "github.com/saucelabs/saucectl/internal/region" "github.com/saucelabs/saucectl/internal/saucecloud" + "github.com/saucelabs/saucectl/internal/saucecloud/downloader" "github.com/saucelabs/saucectl/internal/saucecloud/retry" "github.com/saucelabs/saucectl/internal/segment" "github.com/saucelabs/saucectl/internal/usage" @@ -162,14 +163,15 @@ func runPlaywright(cmd *cobra.Command, pf playwrightFlags, isCLIDriven bool) (in creds := regio.Credentials() restoClient := http.NewResto(regio.APIBaseURL(), creds.Username, creds.AccessKey, 0) - restoClient.ArtifactConfig = p.Artifacts.Download testcompClient := http.NewTestComposer(regio.APIBaseURL(), creds, testComposerTimeout) webdriverClient := http.NewWebdriver(regio.WebDriverBaseURL(), creds, webdriverTimeout) appsClient := *http.NewAppStore(regio.APIBaseURL(), creds.Username, creds.AccessKey, gFlags.appStoreTimeout) - rdcClient := http.NewRDCService(regio.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout, config.ArtifactDownload{}) + rdcClient := http.NewRDCService(regio.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout) insightsClient := http.NewInsightsService(regio.APIBaseURL(), creds, insightsTimeout) iamClient := http.NewUserService(regio.APIBaseURL(), creds, iamTimeout) + vdcDownloader := downloader.NewArtifactDownloader(&restoClient, p.Artifacts.Download) + log.Info().Msg("Running Playwright in Sauce Labs") r := saucecloud.PlaywrightRunner{ Project: p, @@ -183,7 +185,7 @@ func runPlaywright(cmd *cobra.Command, pf playwrightFlags, isCLIDriven bool) (in VDCWriter: &testcompClient, VDCStopper: &restoClient, RDCStopper: &rdcClient, - VDCDownloader: &restoClient, + VDCDownloader: &vdcDownloader, }, TunnelService: &restoClient, MetadataService: &testcompClient, diff --git a/internal/cmd/run/replay.go b/internal/cmd/run/replay.go index f51231678..590d65443 100644 --- a/internal/cmd/run/replay.go +++ b/internal/cmd/run/replay.go @@ -21,6 +21,7 @@ import ( "github.com/saucelabs/saucectl/internal/puppeteer/replay" "github.com/saucelabs/saucectl/internal/region" "github.com/saucelabs/saucectl/internal/saucecloud" + "github.com/saucelabs/saucectl/internal/saucecloud/downloader" "github.com/saucelabs/saucectl/internal/saucecloud/retry" "github.com/saucelabs/saucectl/internal/segment" "github.com/saucelabs/saucectl/internal/usage" @@ -125,14 +126,15 @@ func runPuppeteerReplayInSauce(p replay.Project, regio region.Region) (int, erro creds := regio.Credentials() restoClient := http.NewResto(regio.APIBaseURL(), creds.Username, creds.AccessKey, 0) - restoClient.ArtifactConfig = p.Artifacts.Download testcompClient := http.NewTestComposer(regio.APIBaseURL(), creds, testComposerTimeout) webdriverClient := http.NewWebdriver(regio.WebDriverBaseURL(), creds, webdriverTimeout) appsClient := *http.NewAppStore(regio.APIBaseURL(), creds.Username, creds.AccessKey, gFlags.appStoreTimeout) - rdcClient := http.NewRDCService(regio.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout, config.ArtifactDownload{}) + rdcClient := http.NewRDCService(regio.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout) insightsClient := http.NewInsightsService(regio.APIBaseURL(), creds, insightsTimeout) iamClient := http.NewUserService(regio.APIBaseURL(), creds, iamTimeout) + vdcDownloader := downloader.NewArtifactDownloader(&restoClient, p.Artifacts.Download) + r := saucecloud.ReplayRunner{ Project: p, CloudRunner: saucecloud.CloudRunner{ @@ -145,7 +147,7 @@ func runPuppeteerReplayInSauce(p replay.Project, regio region.Region) (int, erro VDCWriter: &testcompClient, VDCStopper: &restoClient, RDCStopper: &rdcClient, - VDCDownloader: &restoClient, + VDCDownloader: &vdcDownloader, }, TunnelService: &restoClient, MetadataService: &testcompClient, diff --git a/internal/cmd/run/testcafe.go b/internal/cmd/run/testcafe.go index 87d4fd01b..361bfa80c 100644 --- a/internal/cmd/run/testcafe.go +++ b/internal/cmd/run/testcafe.go @@ -21,6 +21,7 @@ import ( "github.com/saucelabs/saucectl/internal/msg" "github.com/saucelabs/saucectl/internal/region" "github.com/saucelabs/saucectl/internal/saucecloud" + "github.com/saucelabs/saucectl/internal/saucecloud/downloader" "github.com/saucelabs/saucectl/internal/saucecloud/retry" "github.com/saucelabs/saucectl/internal/segment" "github.com/saucelabs/saucectl/internal/testcafe" @@ -185,14 +186,15 @@ func runTestcafe(cmd *cobra.Command, tcFlags testcafeFlags, isCLIDriven bool) (i creds := regio.Credentials() restoClient := http.NewResto(regio.APIBaseURL(), creds.Username, creds.AccessKey, 0) - restoClient.ArtifactConfig = p.Artifacts.Download testcompClient := http.NewTestComposer(regio.APIBaseURL(), creds, testComposerTimeout) webdriverClient := http.NewWebdriver(regio.WebDriverBaseURL(), creds, webdriverTimeout) appsClient := *http.NewAppStore(regio.APIBaseURL(), creds.Username, creds.AccessKey, gFlags.appStoreTimeout) - rdcClient := http.NewRDCService(regio.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout, config.ArtifactDownload{}) + rdcClient := http.NewRDCService(regio.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout) insightsClient := http.NewInsightsService(regio.APIBaseURL(), creds, insightsTimeout) iamClient := http.NewUserService(regio.APIBaseURL(), creds, iamTimeout) + vdcDownloader := downloader.NewArtifactDownloader(&restoClient, p.Artifacts.Download) + log.Info().Msg("Running Testcafe in Sauce Labs") r := saucecloud.TestcafeRunner{ Project: p, @@ -206,7 +208,7 @@ func runTestcafe(cmd *cobra.Command, tcFlags testcafeFlags, isCLIDriven bool) (i VDCWriter: &testcompClient, VDCStopper: &restoClient, RDCStopper: &rdcClient, - VDCDownloader: &restoClient, + VDCDownloader: &vdcDownloader, }, TunnelService: &restoClient, MetadataService: &testcompClient, diff --git a/internal/cmd/run/xcuitest.go b/internal/cmd/run/xcuitest.go index 279d4b7e2..d59d8d2ed 100644 --- a/internal/cmd/run/xcuitest.go +++ b/internal/cmd/run/xcuitest.go @@ -18,6 +18,7 @@ import ( "github.com/saucelabs/saucectl/internal/framework" "github.com/saucelabs/saucectl/internal/region" "github.com/saucelabs/saucectl/internal/saucecloud" + "github.com/saucelabs/saucectl/internal/saucecloud/downloader" "github.com/saucelabs/saucectl/internal/saucecloud/retry" "github.com/saucelabs/saucectl/internal/segment" "github.com/saucelabs/saucectl/internal/usage" @@ -138,14 +139,15 @@ func runXcuitestInCloud(p xcuitest.Project, regio region.Region) (int, error) { creds := regio.Credentials() restoClient := http.NewResto(regio.APIBaseURL(), creds.Username, creds.AccessKey, 0) - restoClient.ArtifactConfig = p.Artifacts.Download testcompClient := http.NewTestComposer(regio.APIBaseURL(), creds, testComposerTimeout) webdriverClient := http.NewWebdriver(regio.WebDriverBaseURL(), creds, webdriverTimeout) appsClient := *http.NewAppStore(regio.APIBaseURL(), creds.Username, creds.AccessKey, gFlags.appStoreTimeout) - rdcClient := http.NewRDCService(regio.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout, p.Artifacts.Download) + rdcClient := http.NewRDCService(regio.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout) insightsClient := http.NewInsightsService(regio.APIBaseURL(), creds, insightsTimeout) iamClient := http.NewUserService(regio.APIBaseURL(), creds, iamTimeout) + vdcDownloader := downloader.NewArtifactDownloader(&restoClient, p.Artifacts.Download) + rdcDownloader := downloader.NewArtifactDownloader(&rdcClient, p.Artifacts.Download) r := saucecloud.XcuitestRunner{ Project: p, CloudRunner: saucecloud.CloudRunner{ @@ -158,8 +160,8 @@ func runXcuitestInCloud(p xcuitest.Project, regio region.Region) (int, error) { VDCWriter: &testcompClient, VDCStopper: &restoClient, RDCStopper: &rdcClient, - VDCDownloader: &restoClient, - RDCDownloader: &rdcClient, + VDCDownloader: &vdcDownloader, + RDCDownloader: &rdcDownloader, }, TunnelService: &restoClient, MetadataService: &testcompClient, diff --git a/internal/http/rdcservice.go b/internal/http/rdcservice.go index 4fc9e8c73..03f7eb08c 100644 --- a/internal/http/rdcservice.go +++ b/internal/http/rdcservice.go @@ -8,31 +8,25 @@ import ( "fmt" "io" "net/http" - "os" - "path/filepath" "strings" "time" "github.com/saucelabs/saucectl/internal/slice" "github.com/hashicorp/go-retryablehttp" - "github.com/rs/zerolog/log" - "github.com/saucelabs/saucectl/internal/config" "github.com/saucelabs/saucectl/internal/devices" "github.com/saucelabs/saucectl/internal/espresso" - "github.com/saucelabs/saucectl/internal/fpath" "github.com/saucelabs/saucectl/internal/job" "github.com/saucelabs/saucectl/internal/xcuitest" ) // RDCService http client. type RDCService struct { - Client *retryablehttp.Client - URL string - Username string - AccessKey string - ArtifactConfig config.ArtifactDownload + Client *retryablehttp.Client + URL string + Username string + AccessKey string } type rdcJob struct { @@ -87,13 +81,12 @@ type DeviceQuery struct { } // NewRDCService creates a new client. -func NewRDCService(url, username, accessKey string, timeout time.Duration, artifactConfig config.ArtifactDownload) RDCService { +func NewRDCService(url, username, accessKey string, timeout time.Duration) RDCService { return RDCService{ - Client: NewRetryableClient(timeout), - URL: url, - Username: username, - AccessKey: accessKey, - ArtifactConfig: artifactConfig, + Client: NewRetryableClient(timeout), + URL: url, + Username: username, + AccessKey: accessKey, } } @@ -386,43 +379,6 @@ func (c *RDCService) GetJobAssetFileContent(ctx context.Context, jobID, fileName return io.ReadAll(resp.Body) } -// DownloadArtifact downloads artifacts and returns a list of downloaded files. -func (c *RDCService) DownloadArtifact(jobID, suiteName string, realDevice bool) []string { - targetDir, err := config.GetSuiteArtifactFolder(suiteName, c.ArtifactConfig) - if err != nil { - log.Error().Msgf("Unable to create artifacts folder (%v)", err) - return []string{} - } - - files, err := c.GetJobAssetFileNames(context.Background(), jobID, realDevice) - if err != nil { - log.Error().Msgf("Unable to fetch artifacts list (%v)", err) - return []string{} - } - - filepaths := fpath.MatchFiles(files, c.ArtifactConfig.Match) - var artifacts []string - for _, f := range filepaths { - targetFile, err := c.downloadArtifact(targetDir, jobID, f, realDevice) - if err != nil { - log.Err(err).Msg("Unable to download artifacts") - return artifacts - } - artifacts = append(artifacts, targetFile) - } - - return artifacts -} - -func (c *RDCService) downloadArtifact(targetDir, jobID, fileName string, realDevice bool) (string, error) { - content, err := c.GetJobAssetFileContent(context.Background(), jobID, fileName, realDevice) - if err != nil { - return "", err - } - targetFile := filepath.Join(targetDir, fileName) - return targetFile, os.WriteFile(targetFile, content, 0644) -} - // GetDevices returns the list of available devices using a specific operating system. func (c *RDCService) GetDevices(ctx context.Context, OS string) ([]devices.Device, error) { req, err := NewRetryableRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/v1/rdc/devices/filtered", c.URL), nil) diff --git a/internal/http/rdcservice_test.go b/internal/http/rdcservice_test.go index 18003331c..4db2ba35e 100644 --- a/internal/http/rdcservice_test.go +++ b/internal/http/rdcservice_test.go @@ -7,15 +7,12 @@ import ( "fmt" "net/http" "net/http/httptest" - "os" - "path/filepath" "reflect" "sort" "testing" "time" "github.com/hashicorp/go-retryablehttp" - "github.com/saucelabs/saucectl/internal/config" "github.com/saucelabs/saucectl/internal/devices" "github.com/saucelabs/saucectl/internal/job" "github.com/stretchr/testify/assert" @@ -45,7 +42,7 @@ func TestRDCService_ReadJob(t *testing.T) { })) defer ts.Close() timeout := 3 * time.Second - client := NewRDCService(ts.URL, "test-user", "test-key", timeout, config.ArtifactDownload{}) + client := NewRDCService(ts.URL, "test-user", "test-key", timeout) testCases := []struct { name string @@ -142,7 +139,7 @@ func TestRDCService_PollJob(t *testing.T) { }{ { name: "get job details with ID 1 and status 'complete'", - client: NewRDCService(ts.URL, "test", "123", timeout, config.ArtifactDownload{}), + client: NewRDCService(ts.URL, "test", "123", timeout), jobID: "1", expectedResp: job.Job{ ID: "1", @@ -155,7 +152,7 @@ func TestRDCService_PollJob(t *testing.T) { }, { name: "get job details with ID 2 and status 'error'", - client: NewRDCService(ts.URL, "test", "123", timeout, config.ArtifactDownload{}), + client: NewRDCService(ts.URL, "test", "123", timeout), jobID: "2", expectedResp: job.Job{ ID: "2", @@ -168,28 +165,28 @@ func TestRDCService_PollJob(t *testing.T) { }, { name: "job not found error from external API", - client: NewRDCService(ts.URL, "test", "123", timeout, config.ArtifactDownload{}), + client: NewRDCService(ts.URL, "test", "123", timeout), jobID: "3", expectedResp: job.Job{}, expectedErr: ErrJobNotFound, }, { name: "http status is not 200, but 401 from external API", - client: NewRDCService(ts.URL, "test", "123", timeout, config.ArtifactDownload{}), + client: NewRDCService(ts.URL, "test", "123", timeout), jobID: "4", expectedResp: job.Job{}, expectedErr: errors.New("unexpected statusCode: 401"), }, { name: "unexpected status code from external API", - client: NewRDCService(ts.URL, "test", "123", timeout, config.ArtifactDownload{}), + client: NewRDCService(ts.URL, "test", "123", timeout), jobID: "333", expectedResp: job.Job{}, expectedErr: errors.New("internal server error"), }, { name: "get job details with ID 5. retry 2 times and succeed", - client: NewRDCService(ts.URL, "test", "123", timeout, config.ArtifactDownload{}), + client: NewRDCService(ts.URL, "test", "123", timeout), jobID: "5", expectedResp: job.Job{ ID: "5", @@ -241,7 +238,7 @@ func TestRDCService_GetJobAssetFileNames(t *testing.T) { } })) defer ts.Close() - client := NewRDCService(ts.URL, "test-user", "test-password", 1*time.Second, config.ArtifactDownload{}) + client := NewRDCService(ts.URL, "test-user", "test-password", 1*time.Second) testCases := []struct { name string @@ -314,7 +311,7 @@ func TestRDCService_GetJobAssetFileContent(t *testing.T) { } })) defer ts.Close() - client := NewRDCService(ts.URL, "test-user", "test-password", 1*time.Second, config.ArtifactDownload{}) + client := NewRDCService(ts.URL, "test-user", "test-password", 1*time.Second) testCases := []struct { name string @@ -353,50 +350,6 @@ func TestRDCService_GetJobAssetFileContent(t *testing.T) { } } -func TestRDCService_DownloadArtifact(t *testing.T) { - fileContent := "junit.xml" - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var err error - switch r.URL.Path { - case "/v1/rdc/jobs/test-123": - _, err = w.Write([]byte(`{"automation_backend":"espresso"}`)) - case "/v1/rdc/jobs/test-123/junit.xml": - _, err = w.Write([]byte(fileContent)) - default: - w.WriteHeader(http.StatusNotFound) - } - - if err != nil { - t.Errorf("failed to respond: %v", err) - } - })) - defer ts.Close() - - tempDir, err := os.MkdirTemp("", "saucectl-download-artifact") - if err != nil { - t.Errorf("Failed to create temp dir: %v", err) - } - defer func() { - _ = os.RemoveAll(tempDir) - }() - - rc := NewRDCService(ts.URL, "dummy-user", "dummy-key", 10*time.Second, config.ArtifactDownload{ - Directory: tempDir, - Match: []string{"junit.xml"}, - }) - rc.DownloadArtifact("test-123", "suite name", true) - - fileName := filepath.Join(tempDir, "suite_name", "junit.xml") - d, err := os.ReadFile(fileName) - if err != nil { - t.Errorf("file '%s' not found: %v", fileName, err) - } - - if string(d) != fileContent { - t.Errorf("file content mismatch: got '%v', expects: '%v'", d, fileContent) - } -} - func TestRDCService_GetDevices(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var err error diff --git a/internal/http/resto.go b/internal/http/resto.go index 9e54a9841..0d217a2c2 100644 --- a/internal/http/resto.go +++ b/internal/http/resto.go @@ -7,19 +7,13 @@ import ( "fmt" "io" "net/http" - "os" - "path/filepath" "reflect" "sort" "strings" "time" "github.com/hashicorp/go-retryablehttp" - "github.com/rs/zerolog/log" - "github.com/ryanuber/go-glob" - "github.com/saucelabs/saucectl/internal/build" - "github.com/saucelabs/saucectl/internal/config" "github.com/saucelabs/saucectl/internal/job" tunnels "github.com/saucelabs/saucectl/internal/tunnel" "github.com/saucelabs/saucectl/internal/vmd" @@ -54,11 +48,10 @@ type restoJob struct { // Resto http client. type Resto struct { - Client *retryablehttp.Client - URL string - Username string - AccessKey string - ArtifactConfig config.ArtifactDownload + Client *retryablehttp.Client + URL string + Username string + AccessKey string } type tunnel struct { @@ -348,43 +341,6 @@ func (c *Resto) StopJob(ctx context.Context, jobID string, realDevice bool) (job return c.parseJob(resp.Body) } -// DownloadArtifact downloads artifacts and returns a list of what was downloaded. -func (c *Resto) DownloadArtifact(jobID, suiteName string, realDevice bool) []string { - targetDir, err := config.GetSuiteArtifactFolder(suiteName, c.ArtifactConfig) - if err != nil { - log.Error().Msgf("Unable to create artifacts folder (%v)", err) - return []string{} - } - files, err := c.GetJobAssetFileNames(context.Background(), jobID, realDevice) - if err != nil { - log.Error().Msgf("Unable to fetch artifacts list (%v)", err) - return []string{} - } - var artifacts []string - for _, f := range files { - for _, pattern := range c.ArtifactConfig.Match { - if glob.Glob(pattern, f) { - if err := c.downloadArtifact(targetDir, jobID, f); err != nil { - log.Error().Err(err).Msgf("Failed to download file: %s", f) - } else { - artifacts = append(artifacts, filepath.Join(targetDir, f)) - } - break - } - } - } - return artifacts -} - -func (c *Resto) downloadArtifact(targetDir, jobID, fileName string) error { - content, err := c.GetJobAssetFileContent(context.Background(), jobID, fileName, false) - if err != nil { - return err - } - targetFile := filepath.Join(targetDir, fileName) - return os.WriteFile(targetFile, content, 0644) -} - // GetVirtualDevices returns the list of available virtual devices. func (c *Resto) GetVirtualDevices(ctx context.Context, kind string) ([]vmd.VirtualDevice, error) { req, err := NewRetryableRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/rest/v1.1/info/platforms/all", c.URL), nil) diff --git a/internal/saucecloud/downloader/downloader.go b/internal/saucecloud/downloader/downloader.go new file mode 100644 index 000000000..95706ce49 --- /dev/null +++ b/internal/saucecloud/downloader/downloader.go @@ -0,0 +1,59 @@ +package downloader + +import ( + "context" + "os" + "path/filepath" + + "github.com/rs/zerolog/log" + "github.com/saucelabs/saucectl/internal/config" + "github.com/saucelabs/saucectl/internal/fpath" + "github.com/saucelabs/saucectl/internal/job" +) + +type ArtifactDownloader struct { + reader job.Reader + config config.ArtifactDownload +} + +func NewArtifactDownloader(reader job.Reader, artifactConfig config.ArtifactDownload) ArtifactDownloader { + return ArtifactDownloader{ + reader: reader, + config: artifactConfig, + } +} + +func (d *ArtifactDownloader) DownloadArtifact(jobID string, suiteName string, realDevice bool) []string { + targetDir, err := config.GetSuiteArtifactFolder(suiteName, d.config) + if err != nil { + log.Error().Msgf("Unable to create artifacts folder (%v)", err) + return []string{} + } + files, err := d.reader.GetJobAssetFileNames(context.Background(), jobID, realDevice) + if err != nil { + log.Error().Msgf("Unable to fetch artifacts list (%v)", err) + return []string{} + } + + filepaths := fpath.MatchFiles(files, d.config.Match) + var artifacts []string + for _, f := range filepaths { + targetFile, err := d.downloadArtifact(targetDir, jobID, f, realDevice) + if err != nil { + log.Err(err).Msg("Unable to download artifacts") + return artifacts + } + artifacts = append(artifacts, targetFile) + } + + return artifacts +} + +func (d *ArtifactDownloader) downloadArtifact(targetDir, jobID, fileName string, realDevice bool) (string, error) { + content, err := d.reader.GetJobAssetFileContent(context.Background(), jobID, fileName, realDevice) + if err != nil { + return "", err + } + targetFile := filepath.Join(targetDir, fileName) + return targetFile, os.WriteFile(targetFile, content, 0644) +} diff --git a/internal/saucecloud/downloader/downloader_test.go b/internal/saucecloud/downloader/downloader_test.go new file mode 100644 index 000000000..490c7260e --- /dev/null +++ b/internal/saucecloud/downloader/downloader_test.go @@ -0,0 +1,60 @@ +package downloader + +import ( + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + "time" + + "github.com/saucelabs/saucectl/internal/config" + httpServices "github.com/saucelabs/saucectl/internal/http" +) + +func TestArtifactDownloader_DownloadArtifact(t *testing.T) { + fileContent := "junit.xml" + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var err error + switch r.URL.Path { + case "/v1/rdc/jobs/test-123": + _, err = w.Write([]byte(`{"automation_backend":"espresso"}`)) + case "/v1/rdc/jobs/test-123/junit.xml": + _, err = w.Write([]byte(fileContent)) + default: + w.WriteHeader(http.StatusNotFound) + } + + if err != nil { + t.Errorf("failed to respond: %v", err) + } + })) + defer ts.Close() + + tempDir, err := os.MkdirTemp("", "saucectl-download-artifact") + if err != nil { + t.Errorf("Failed to create temp dir: %v", err) + } + defer func() { + _ = os.RemoveAll(tempDir) + }() + + rc := httpServices.NewRDCService(ts.URL, "dummy-user", "dummy-key", 10*time.Second) + artifactCfg := config.ArtifactDownload{ + Directory: tempDir, + Match: []string{"junit.xml"}, + } + + downloader := NewArtifactDownloader(&rc, artifactCfg) + downloader.DownloadArtifact("test-123", "suite name", true) + + fileName := filepath.Join(tempDir, "suite_name", "junit.xml") + d, err := os.ReadFile(fileName) + if err != nil { + t.Errorf("file '%s' not found: %v", fileName, err) + } + + if string(d) != fileContent { + t.Errorf("file content mismatch: got '%v', expects: '%v'", d, fileContent) + } +}