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  }