github.com/saucelabs/saucectl@v0.175.1/internal/cmd/run/run.go (about) 1 package run 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "path/filepath" 8 "runtime" 9 "syscall" 10 "time" 11 12 "github.com/fatih/color" 13 "github.com/rs/zerolog/log" 14 "github.com/saucelabs/saucectl/internal/report/buildtable" 15 "github.com/saucelabs/saucectl/internal/report/json" 16 "github.com/saucelabs/saucectl/internal/report/junit" 17 "github.com/saucelabs/saucectl/internal/report/spotlight" 18 "github.com/spf13/cobra" 19 "github.com/spf13/pflag" 20 21 "github.com/saucelabs/saucectl/internal/apitest" 22 "github.com/saucelabs/saucectl/internal/build" 23 "github.com/saucelabs/saucectl/internal/config" 24 "github.com/saucelabs/saucectl/internal/credentials" 25 "github.com/saucelabs/saucectl/internal/cucumber" 26 "github.com/saucelabs/saucectl/internal/cypress" 27 "github.com/saucelabs/saucectl/internal/espresso" 28 "github.com/saucelabs/saucectl/internal/flags" 29 "github.com/saucelabs/saucectl/internal/http" 30 "github.com/saucelabs/saucectl/internal/imagerunner" 31 "github.com/saucelabs/saucectl/internal/msg" 32 "github.com/saucelabs/saucectl/internal/notification/slack" 33 "github.com/saucelabs/saucectl/internal/playwright" 34 "github.com/saucelabs/saucectl/internal/puppeteer/replay" 35 "github.com/saucelabs/saucectl/internal/report" 36 "github.com/saucelabs/saucectl/internal/report/captor" 37 "github.com/saucelabs/saucectl/internal/report/github" 38 "github.com/saucelabs/saucectl/internal/testcafe" 39 "github.com/saucelabs/saucectl/internal/version" 40 "github.com/saucelabs/saucectl/internal/xcuitest" 41 ) 42 43 var ( 44 runUse = "run" 45 runShort = "Runs tests on Sauce Labs" 46 47 // General Request Timeouts 48 testComposerTimeout = 15 * time.Minute 49 webdriverTimeout = 15 * time.Minute 50 rdcTimeout = 15 * time.Minute 51 insightsTimeout = 10 * time.Second 52 iamTimeout = 10 * time.Second 53 apitestingTimeout = 30 * time.Second 54 imgExecTimeout = 30 * time.Second 55 56 typeDef config.TypeDef 57 58 // ErrEmptySuiteName is thrown when a flag is specified that has a dependency on the --name flag. 59 ErrEmptySuiteName = errors.New(msg.EmptyAdhocSuiteName) 60 ) 61 62 // gFlags contains all global flags that are set when 'run' is invoked. 63 var gFlags = globalFlags{} 64 65 type globalFlags struct { 66 cfgFilePath string 67 selectedSuite string 68 testEnvSilent bool 69 async bool 70 failFast bool 71 noAutoTagging bool 72 73 globalTimeout time.Duration 74 appStoreTimeout time.Duration 75 } 76 77 // Command creates the `run` command 78 func Command() *cobra.Command { 79 sc := flags.SnakeCharmer{Fmap: map[string]*pflag.Flag{}} 80 81 cmd := &cobra.Command{ 82 Use: runUse, 83 Short: runShort, 84 SilenceUsage: true, 85 TraverseChildren: true, 86 PreRunE: func(cmd *cobra.Command, args []string) error { 87 return preRun() 88 }, 89 Run: func(cmd *cobra.Command, args []string) { 90 exitCode, err := Run(cmd) 91 if err != nil { 92 log.Err(err).Msg("failed to execute run command") 93 } 94 os.Exit(exitCode) 95 }, 96 } 97 98 sc.Fset = cmd.PersistentFlags() 99 100 defaultCfgPath := filepath.Join(".sauce", "config.yml") 101 cmd.PersistentFlags().StringVarP(&gFlags.cfgFilePath, "config", "c", defaultCfgPath, "Specifies which config file to use") 102 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)") 103 cmd.PersistentFlags().BoolVar(&gFlags.async, "async", false, "Launches tests without waiting for test results") 104 cmd.PersistentFlags().BoolVar(&gFlags.failFast, "fail-fast", false, "Stops suites after the first failure") 105 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.") 106 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.") 107 sc.StringP("region", "r", "sauce::region", "", "The sauce labs region. Options: us-west-1, eu-central-1.") 108 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!") 109 sc.Bool("show-console-log", "showConsoleLog", false, "Shows suites console.log locally. By default console.log is only shown on failures.") 110 sc.Int("ccy", "sauce::concurrency", 2, "Concurrency specifies how many suites are run at the same time.") 111 sc.String("tunnel-name", "sauce::tunnel::name", "", "Sets the sauce-connect tunnel name to be used for the run.") 112 sc.String("tunnel-owner", "sauce::tunnel::owner", "", "Sets the sauce-connect tunnel owner to be used for the run.") 113 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.") 114 sc.String("runner-version", "runnerVersion", "", "Overrides the automatically determined runner version.") 115 sc.String("sauceignore", "sauce::sauceignore", ".sauceignore", "Specifies the path to the .sauceignore file.") 116 sc.String("root-dir", "rootDir", ".", "Specifies the project directory. Not applicable to mobile frameworks.") 117 sc.StringToString("experiment", "sauce::experiment", map[string]string{}, "Specifies a list of experimental flags and values") 118 sc.Bool("dry-run", "dryRun", false, "Simulate a test run without actually running any tests.") 119 sc.Int("retries", "sauce::retries", 0, "Retries specifies the number of times to retry a failed suite") 120 sc.String("launch-order", "sauce::launchOrder", "", `Launch jobs based on the failure rate. Jobs with the highest failure rate launch first. Supports values: ["fail rate"]`) 121 sc.Bool("live-logs", "liveLogs", false, "Display live logs for a running job (supported only by Sauce Orchestrate).") 122 123 // Metadata 124 sc.StringSlice("tags", "sauce::metadata::tags", []string{}, "Adds tags to tests") 125 sc.String("build", "sauce::metadata::build", "", "Associates tests with a build") 126 127 // Artifacts 128 sc.String("artifacts.download.when", "artifacts::download::when", "never", "Specifies when to download test artifacts") 129 sc.StringSlice("artifacts.download.match", "artifacts::download::match", []string{}, "Specifies which test artifacts to download") 130 sc.String("artifacts.download.directory", "artifacts::download::directory", "", "Specifies the location where to download test artifacts to") 131 sc.Bool("artifacts.cleanup", "artifacts::cleanup", false, "Specifies whether to remove all contents of artifacts directory") 132 133 // Reporters 134 sc.Bool("reporters.junit.enabled", "reporters::junit::enabled", false, "Toggle saucectl's own junit reporting on/off. This only affects the reports that saucectl itself generates as a summary of your tests. Each Job in Sauce Labs has an independent report regardless.") 135 sc.String("reporters.junit.filename", "reporters::junit::filename", "saucectl-report.xml", "Specifies the report filename.") 136 sc.Bool("reporters.json.enabled", "reporters::json::enabled", false, "Toggle saucectl's JSON test result reporting on/off. This only affects the reports that saucectl itself generates as a summary of your tests.") 137 sc.String("reporters.json.filename", "reporters::json::filename", "saucectl-report.json", "Specifies the report filename.") 138 sc.String("reporters.json.webhookURL", "reporters::json::webhookURL", "", "Specifies the webhook URL. When saucectl test is finished, it'll send a HTTP POST payload to the configured webhook URL.") 139 140 cmd.PersistentFlags().StringVar(&gFlags.selectedSuite, "select-suite", "", "Run specified test suite.") 141 cmd.PersistentFlags().BoolVar(&gFlags.testEnvSilent, "test-env-silent", false, "Skips the test environment announcement.") 142 cmd.PersistentFlags().BoolVar(&gFlags.noAutoTagging, "no-auto-tagging", false, "Disable the automatic tagging of jobs with metadata, such as CI or GIT information.") 143 144 // Hide undocumented flags that the user does not need to care about. 145 _ = cmd.PersistentFlags().MarkHidden("runner-version") 146 _ = cmd.PersistentFlags().MarkHidden("experiment") 147 148 // Deprecated flags 149 _ = sc.Fset.MarkDeprecated("uploadTimeout", "please use --upload-timeout instead") 150 151 sc.BindAll() 152 153 cmd.AddCommand( 154 NewCypressCmd(), 155 NewEspressoCmd(), 156 NewPlaywrightCmd(), 157 NewReplayCmd(), 158 NewTestcafeCmd(), 159 NewXCUITestCmd(), 160 NewCucumberCmd(), 161 ) 162 163 return cmd 164 } 165 166 // preRun is a pre-run step that is executed before the main 'run` step. All shared dependencies are initialized here. 167 func preRun() error { 168 err := http.CheckProxy() 169 if err != nil { 170 return fmt.Errorf("invalid HTTP_PROXY value") 171 } 172 173 println("Running version", version.Version) 174 checkForUpdates() 175 go awaitGlobalTimeout() 176 177 creds := credentials.Get() 178 if !creds.IsSet() { 179 color.Red("\nSauceCTL requires a valid Sauce Labs account!\n\n") 180 fmt.Println(`Set up your credentials by running: 181 > saucectl configure`) 182 println() 183 return fmt.Errorf("no credentials set") 184 } 185 186 d, err := config.Describe(gFlags.cfgFilePath) 187 if err != nil { 188 return err 189 } 190 typeDef = d 191 192 return nil 193 } 194 195 // Run runs the command 196 func Run(cmd *cobra.Command) (int, error) { 197 if typeDef.Kind == cypress.Kind { 198 return runCypress(cmd, false) 199 } 200 if typeDef.Kind == playwright.Kind { 201 return runPlaywright(cmd, false) 202 } 203 if typeDef.Kind == testcafe.Kind { 204 return runTestcafe(cmd, testcafeFlags{}, false) 205 } 206 if typeDef.Kind == replay.Kind { 207 return runReplay(cmd, false) 208 } 209 if typeDef.Kind == espresso.Kind { 210 return runEspresso(cmd, espressoFlags{}, false) 211 } 212 if typeDef.Kind == xcuitest.Kind { 213 return runXcuitest(cmd, xcuitestFlags{}, false) 214 } 215 if typeDef.Kind == apitest.Kind { 216 return runApitest(cmd, false) 217 } 218 if typeDef.Kind == cucumber.Kind { 219 return runCucumber(cmd, false) 220 } 221 if typeDef.Kind == imagerunner.Kind { 222 return runImageRunner(cmd) 223 } 224 225 msg.LogUnsupportedFramework(typeDef.Kind) 226 return 1, errors.New(msg.UnknownFrameworkConfig) 227 } 228 229 // awaitGlobalTimeout waits for the global timeout event. In case of global timeout event, it attempts to interrupt the 230 // current process. Should this fail, a hard immediate exit is performed. 231 func awaitGlobalTimeout() { 232 if gFlags.globalTimeout == 0 { 233 return 234 } 235 236 <-time.After(gFlags.globalTimeout) 237 msg.LogGlobalTimeoutShutdown() 238 239 // A timeout for soft shutdown. 240 go func() { 241 <-time.After(10 * time.Second) 242 color.Red("Unable to perform soft shutdown. Exiting immediately...") 243 os.Exit(1) 244 }() 245 246 // Can't send interrupt signals on windows. A hard exit is our only choice. 247 if runtime.GOOS == "windows" { 248 os.Exit(1) 249 } 250 251 p, err := os.FindProcess(os.Getpid()) 252 if err == nil { 253 _ = p.Signal(syscall.SIGINT) 254 } 255 } 256 257 // checkForUpdates check if there is a saucectl update available. 258 func checkForUpdates() { 259 v, err := http.DefaultGitHub.IsUpdateAvailable(version.Version) 260 if err != nil { 261 return 262 } 263 if v != "" { 264 log.Warn().Msgf("A new version of saucectl is available (%s)", v) 265 } 266 } 267 268 func createReporters(c config.Reporters, ntfs config.Notifications, metadata config.Metadata, 269 svc slack.Service, buildReader build.Reader, framework, env string, async bool) []report.Reporter { 270 githubReporter := github.NewJobSummaryReporter() 271 272 reps := []report.Reporter{ 273 &captor.Default, 274 &githubReporter, 275 } 276 277 // Running async means that jobs aren't done by the time reports are 278 // generated. Therefore, we disable all reporters that depend on the Job 279 // results. 280 if !async { 281 if c.JUnit.Enabled { 282 reps = append(reps, &junit.Reporter{ 283 Filename: c.JUnit.Filename, 284 }) 285 } 286 if c.JSON.Enabled { 287 reps = append(reps, &json.Reporter{ 288 WebhookURL: c.JSON.WebhookURL, 289 Filename: c.JSON.Filename, 290 }) 291 } 292 if c.Spotlight.Enabled { 293 reps = append(reps, &spotlight.Reporter{ 294 Dst: os.Stdout, 295 }) 296 } 297 } 298 299 buildReporter := buildtable.New() 300 reps = append(reps, &buildReporter) 301 302 reps = append(reps, &slack.Reporter{ 303 Channels: ntfs.Slack.Channels, 304 Framework: framework, 305 Metadata: metadata, 306 TestEnv: env, 307 TestResults: []report.TestResult{}, 308 Config: ntfs, 309 Service: svc, 310 }) 311 312 return reps 313 } 314 315 // cleanupArtifacts removes any files in the artifact folder. Does nothing if cleanup is turned off. 316 func cleanupArtifacts(c config.Artifacts) { 317 if !c.Cleanup { 318 return 319 } 320 321 err := os.RemoveAll(c.Download.Directory) 322 if err != nil { 323 log.Err(err).Msg("Unable to clean up previous artifacts") 324 } 325 }