From 181b20edff0fbe403aee542239e4ac23513ffc10 Mon Sep 17 00:00:00 2001 From: Alex Plischke Date: Tue, 2 Apr 2024 15:39:32 -0700 Subject: [PATCH] feat: allow tunnel timeout configuration (#896) * feat: allow tunnel timeout configuration * refactor: remove stutter --- api/saucectl.schema.json | 9 +++++++++ api/v1alpha/subschema/sauce.schema.json | 9 +++++++++ internal/apitest/runner.go | 9 ++++++++- internal/cmd/run/run.go | 18 ++++++++++-------- internal/config/config.go | 2 ++ internal/flags/binder.go | 8 ++++++++ internal/saucecloud/cloud.go | 4 ++-- internal/saucecloud/cucumber.go | 7 ++++++- internal/saucecloud/cypress.go | 7 ++++++- internal/saucecloud/espresso.go | 7 ++++++- internal/saucecloud/imagerunner.go | 9 ++++++++- internal/saucecloud/playwright.go | 7 ++++++- internal/saucecloud/replay.go | 7 ++++++- internal/saucecloud/testcafe.go | 7 ++++++- internal/saucecloud/xcuitest.go | 7 ++++++- internal/tunnel/tunnel.go | 13 ++++++++----- 16 files changed, 106 insertions(+), 24 deletions(-) diff --git a/api/saucectl.schema.json b/api/saucectl.schema.json index 229859a9a..ccd3638bc 100644 --- a/api/saucectl.schema.json +++ b/api/saucectl.schema.json @@ -202,6 +202,15 @@ "owner": { "description": "The owner (username) of the tunnel. Must be specified if the user that created the tunnel differs from the user that is running the tests.", "type": "string" + }, + "timeout": { + "description": "How long to wait for the specified tunnel to be ready. Supports duration values like '10s', '30m' etc.", + "type": "string", + "pattern": "^(?:\\d+h)?(?:\\d+m)?(?:\\d+s)?(?:\\d+ms)?$", + "examples": [ + "1m", + "30s" + ] } }, "required": [ diff --git a/api/v1alpha/subschema/sauce.schema.json b/api/v1alpha/subschema/sauce.schema.json index 0f2d820be..e07a5dda2 100644 --- a/api/v1alpha/subschema/sauce.schema.json +++ b/api/v1alpha/subschema/sauce.schema.json @@ -50,6 +50,15 @@ "owner": { "description": "The owner (username) of the tunnel. Must be specified if the user that created the tunnel differs from the user that is running the tests.", "type": "string" + }, + "timeout": { + "description": "How long to wait for the specified tunnel to be ready. Supports duration values like '10s', '30m' etc.", + "type": "string", + "pattern": "^(?:\\d+h)?(?:\\d+m)?(?:\\d+s)?(?:\\d+ms)?$", + "examples": [ + "1m", + "30s" + ] } }, "required": [ diff --git a/internal/apitest/runner.go b/internal/apitest/runner.go index 0430e91ba..85376c2eb 100644 --- a/internal/apitest/runner.go +++ b/internal/apitest/runner.go @@ -137,7 +137,14 @@ func FilterSuites(p *Project, suiteName string) error { // RunProject runs the tests defined in apitest.Project func (r *Runner) RunProject() (int, error) { exitCode := 1 - if err := tunnel.ValidateTunnel(r.TunnelService, r.Project.Sauce.Tunnel.Name, r.Project.Sauce.Tunnel.Owner, tunnel.V2AlphaFilter, false); err != nil { + if err := tunnel.Validate( + r.TunnelService, + r.Project.Sauce.Tunnel.Name, + r.Project.Sauce.Tunnel.Owner, + tunnel.V2AlphaFilter, + false, + r.Project.Sauce.Tunnel.Timeout, + ); err != nil { return 1, err } diff --git a/internal/cmd/run/run.go b/internal/cmd/run/run.go index 54975961c..53f08b233 100644 --- a/internal/cmd/run/run.go +++ b/internal/cmd/run/run.go @@ -63,14 +63,15 @@ var ( var gFlags = globalFlags{} type globalFlags struct { - cfgFilePath string + cfgFilePath string + selectedSuite string + testEnvSilent bool + async bool + failFast bool + noAutoTagging bool + globalTimeout time.Duration - selectedSuite string - testEnvSilent bool - async bool - failFast bool appStoreTimeout time.Duration - noAutoTagging bool } // Command creates the `run` command @@ -101,14 +102,15 @@ func Command() *cobra.Command { cmd.PersistentFlags().DurationVarP(&gFlags.globalTimeout, "timeout", "t", 0, "Global timeout that limits how long saucectl can run in total. Supports duration values like '10s', '30m' etc. (default: no timeout)") cmd.PersistentFlags().BoolVar(&gFlags.async, "async", false, "Launches tests without waiting for test results") cmd.PersistentFlags().BoolVar(&gFlags.failFast, "fail-fast", false, "Stops suites after the first failure") - cmd.PersistentFlags().DurationVar(&gFlags.appStoreTimeout, "uploadTimeout", 5*time.Minute, "Upload timeout that limits how long saucectl will wait for an upload to finish. Supports duration values like '10s' '30m' etc. (default: 5m)") - cmd.PersistentFlags().DurationVar(&gFlags.appStoreTimeout, "upload-timeout", 5*time.Minute, "Upload timeout that limits how long saucectl will wait for an upload to finish. Supports duration values like '10s' '30m' etc. (default: 5m)") + cmd.PersistentFlags().DurationVar(&gFlags.appStoreTimeout, "uploadTimeout", 5*time.Minute, "Upload timeout that limits how long saucectl will wait for an upload to finish. Supports duration values like '10s', '30m' etc.") + cmd.PersistentFlags().DurationVar(&gFlags.appStoreTimeout, "upload-timeout", 5*time.Minute, "Upload timeout that limits how long saucectl will wait for an upload to finish. Supports duration values like '10s', '30m' etc.") sc.StringP("region", "r", "sauce::region", "", "The sauce labs region. Options: us-west-1, eu-central-1.") sc.StringToStringP("env", "e", "envFlag", map[string]string{}, "Set environment variables, e.g. -e foo=bar. Not supported for RDC or Espresso on virtual devices!") sc.Bool("show-console-log", "showConsoleLog", false, "Shows suites console.log locally. By default console.log is only shown on failures.") sc.Int("ccy", "sauce::concurrency", 2, "Concurrency specifies how many suites are run at the same time.") sc.String("tunnel-name", "sauce::tunnel::name", "", "Sets the sauce-connect tunnel name to be used for the run.") sc.String("tunnel-owner", "sauce::tunnel::owner", "", "Sets the sauce-connect tunnel owner to be used for the run.") + sc.Duration("tunnel-timeout", "sauce::tunnel::timeout", 30*time.Second, "How long to wait for the specified tunnel to be ready. Supports duration values like '10s', '30m' etc.") sc.String("runner-version", "runnerVersion", "", "Overrides the automatically determined runner version.") sc.String("sauceignore", "sauce::sauceignore", ".sauceignore", "Specifies the path to the .sauceignore file.") sc.String("root-dir", "rootDir", ".", "Specifies the project directory. Not applicable to mobile frameworks.") diff --git a/internal/config/config.go b/internal/config/config.go index 542fecd9c..a76b2a766 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -174,6 +174,8 @@ type Tunnel struct { // Deprecated. Use Owner instead. Parent string `yaml:"parent,omitempty" json:"parent,omitempty"` Owner string `yaml:"owner,omitempty" json:"owner,omitempty"` + // Timeout represents the time to wait for the tunnel to be ready. + Timeout time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"` } // TypeDef represents the type definition of the config. diff --git a/internal/flags/binder.go b/internal/flags/binder.go index 284b3d429..f2a0243c8 100644 --- a/internal/flags/binder.go +++ b/internal/flags/binder.go @@ -1,6 +1,8 @@ package flags import ( + "time" + "github.com/rs/zerolog/log" "github.com/spf13/pflag" @@ -43,6 +45,12 @@ func (s *SnakeCharmer) BoolP(flagName, shorthand, fieldName string, value bool, s.addBind(flagName, fieldName) } +// Duration defines a duration flag with specified flagName, default value, usage string and then binds it to fieldName. +func (s *SnakeCharmer) Duration(flagName string, fieldName string, value time.Duration, usage string) { + s.Fset.Duration(flagName, value, usage) + s.addBind(flagName, fieldName) +} + // Float64 defines a float64 flag with specified flagName, default value, usage string and then binds it to fieldName. func (s *SnakeCharmer) Float64(flagName, fieldName string, value float64, usage string) { s.Fset.Float64(flagName, value, usage) diff --git a/internal/saucecloud/cloud.go b/internal/saucecloud/cloud.go index 92c0f099c..a4c00ac86 100644 --- a/internal/saucecloud/cloud.go +++ b/internal/saucecloud/cloud.go @@ -792,8 +792,8 @@ func (r *CloudRunner) logSuiteConsole(res result) { fmt.Println() } -func (r *CloudRunner) validateTunnel(name, owner string, dryRun bool) error { - return tunnel.ValidateTunnel(r.TunnelService, name, owner, tunnel.NoneFilter, dryRun) +func (r *CloudRunner) validateTunnel(name, owner string, dryRun bool, timeout time.Duration) error { + return tunnel.Validate(r.TunnelService, name, owner, tunnel.NoneFilter, dryRun, timeout) } // stopSuiteExecution stops the current execution on Sauce Cloud diff --git a/internal/saucecloud/cucumber.go b/internal/saucecloud/cucumber.go index 8235dcad5..bbbdf64e3 100644 --- a/internal/saucecloud/cucumber.go +++ b/internal/saucecloud/cucumber.go @@ -51,7 +51,12 @@ func (r *CucumberRunner) RunProject() (int, error) { } } - if err := r.validateTunnel(r.Project.Sauce.Tunnel.Name, r.Project.Sauce.Tunnel.Owner, r.Project.DryRun); err != nil { + if err := r.validateTunnel( + r.Project.Sauce.Tunnel.Name, + r.Project.Sauce.Tunnel.Owner, + r.Project.DryRun, + r.Project.Sauce.Tunnel.Timeout, + ); err != nil { return 1, err } diff --git a/internal/saucecloud/cypress.go b/internal/saucecloud/cypress.go index c75ce189a..307de9c20 100644 --- a/internal/saucecloud/cypress.go +++ b/internal/saucecloud/cypress.go @@ -51,7 +51,12 @@ func (r *CypressRunner) RunProject() (int, error) { } } - if err := r.validateTunnel(r.Project.GetSauceCfg().Tunnel.Name, r.Project.GetSauceCfg().Tunnel.Owner, r.Project.IsDryRun()); err != nil { + if err := r.validateTunnel( + r.Project.GetSauceCfg().Tunnel.Name, + r.Project.GetSauceCfg().Tunnel.Owner, + r.Project.IsDryRun(), + r.Project.GetSauceCfg().Tunnel.Timeout, + ); err != nil { return 1, err } diff --git a/internal/saucecloud/espresso.go b/internal/saucecloud/espresso.go index fe9d5d240..52a1b3a9e 100644 --- a/internal/saucecloud/espresso.go +++ b/internal/saucecloud/espresso.go @@ -35,7 +35,12 @@ type EspressoRunner struct { func (r *EspressoRunner) RunProject() (int, error) { exitCode := 1 - if err := r.validateTunnel(r.Project.Sauce.Tunnel.Name, r.Project.Sauce.Tunnel.Owner, r.Project.DryRun); err != nil { + if err := r.validateTunnel( + r.Project.Sauce.Tunnel.Name, + r.Project.Sauce.Tunnel.Owner, + r.Project.DryRun, + r.Project.Sauce.Tunnel.Timeout, + ); err != nil { return 1, err } diff --git a/internal/saucecloud/imagerunner.go b/internal/saucecloud/imagerunner.go index 616560eec..6a1882599 100644 --- a/internal/saucecloud/imagerunner.go +++ b/internal/saucecloud/imagerunner.go @@ -84,7 +84,14 @@ type execResult struct { } func (r *ImgRunner) RunProject() (int, error) { - if err := tunnel.ValidateTunnel(r.TunnelService, r.Project.Sauce.Tunnel.Name, r.Project.Sauce.Tunnel.Owner, tunnel.NoneFilter, false); err != nil { + if err := tunnel.Validate( + r.TunnelService, + r.Project.Sauce.Tunnel.Name, + r.Project.Sauce.Tunnel.Owner, + tunnel.NoneFilter, + false, + r.Project.Sauce.Tunnel.Timeout, + ); err != nil { return 1, err } diff --git a/internal/saucecloud/playwright.go b/internal/saucecloud/playwright.go index 6d08ec75d..b3f3b7e3c 100644 --- a/internal/saucecloud/playwright.go +++ b/internal/saucecloud/playwright.go @@ -57,7 +57,12 @@ func (r *PlaywrightRunner) RunProject() (int, error) { r.Project.Suites[i].Params.BrowserVersion = m.BrowserDefaults[PlaywrightBrowserMap[s.Params.BrowserName]] } - if err := r.validateTunnel(r.Project.Sauce.Tunnel.Name, r.Project.Sauce.Tunnel.Owner, r.Project.DryRun); err != nil { + if err := r.validateTunnel( + r.Project.Sauce.Tunnel.Name, + r.Project.Sauce.Tunnel.Owner, + r.Project.DryRun, + r.Project.Sauce.Tunnel.Timeout, + ); err != nil { return 1, err } diff --git a/internal/saucecloud/replay.go b/internal/saucecloud/replay.go index 1b82d40fc..c638e4f7c 100644 --- a/internal/saucecloud/replay.go +++ b/internal/saucecloud/replay.go @@ -37,7 +37,12 @@ func (r *ReplayRunner) RunProject() (int, error) { } } - if err := r.validateTunnel(r.Project.Sauce.Tunnel.Name, r.Project.Sauce.Tunnel.Owner, r.Project.DryRun); err != nil { + if err := r.validateTunnel( + r.Project.Sauce.Tunnel.Name, + r.Project.Sauce.Tunnel.Owner, + r.Project.DryRun, + r.Project.Sauce.Tunnel.Timeout, + ); err != nil { return 1, err } diff --git a/internal/saucecloud/testcafe.go b/internal/saucecloud/testcafe.go index 8a233a7a9..90924f8dc 100644 --- a/internal/saucecloud/testcafe.go +++ b/internal/saucecloud/testcafe.go @@ -50,7 +50,12 @@ func (r *TestcafeRunner) RunProject() (int, error) { } } - if err := r.validateTunnel(r.Project.Sauce.Tunnel.Name, r.Project.Sauce.Tunnel.Owner, r.Project.DryRun); err != nil { + if err := r.validateTunnel( + r.Project.Sauce.Tunnel.Name, + r.Project.Sauce.Tunnel.Owner, + r.Project.DryRun, + r.Project.Sauce.Tunnel.Timeout, + ); err != nil { return 1, err } diff --git a/internal/saucecloud/xcuitest.go b/internal/saucecloud/xcuitest.go index 94a28c882..50cc79c89 100644 --- a/internal/saucecloud/xcuitest.go +++ b/internal/saucecloud/xcuitest.go @@ -58,7 +58,12 @@ var ( func (r *XcuitestRunner) RunProject() (int, error) { exitCode := 1 - if err := r.validateTunnel(r.Project.Sauce.Tunnel.Name, r.Project.Sauce.Tunnel.Owner, r.Project.DryRun); err != nil { + if err := r.validateTunnel( + r.Project.Sauce.Tunnel.Name, + r.Project.Sauce.Tunnel.Owner, + r.Project.DryRun, + r.Project.Sauce.Tunnel.Timeout, + ); err != nil { return exitCode, err } diff --git a/internal/tunnel/tunnel.go b/internal/tunnel/tunnel.go index f27913045..3bbb31804 100644 --- a/internal/tunnel/tunnel.go +++ b/internal/tunnel/tunnel.go @@ -2,6 +2,7 @@ package tunnel import ( "context" + "errors" "time" "github.com/rs/zerolog/log" @@ -25,7 +26,7 @@ type Service interface { IsTunnelRunning(ctx context.Context, id, parent string, filter Filter, wait time.Duration) error } -func ValidateTunnel(service Service, name string, owner string, filter Filter, dryRun bool) error { +func Validate(service Service, name string, owner string, filter Filter, dryRun bool, timeout time.Duration) error { if name == "" { return nil } @@ -35,10 +36,12 @@ func ValidateTunnel(service Service, name string, owner string, filter Filter, d return nil } - // This wait value is deliberately not configurable. - wait := 30 * time.Second - log.Info().Str("timeout", wait.String()).Str("tunnel", name).Msg("Performing tunnel readiness check...") - if err := service.IsTunnelRunning(context.Background(), name, owner, filter, wait); err != nil { + if timeout <= 0 { + return errors.New("tunnel timeout must be greater than 0") + } + + log.Info().Str("timeout", timeout.String()).Str("tunnel", name).Msg("Performing tunnel readiness check...") + if err := service.IsTunnelRunning(context.Background(), name, owner, filter, timeout); err != nil { return err }