github.com/saucelabs/saucectl@v0.175.1/internal/cmd/run/espresso.go (about)

     1  package run
     2  
     3  import (
     4  	"os"
     5  
     6  	cmds "github.com/saucelabs/saucectl/internal/cmd"
     7  	"github.com/saucelabs/saucectl/internal/http"
     8  
     9  	"github.com/rs/zerolog/log"
    10  	"github.com/spf13/cobra"
    11  	"github.com/spf13/pflag"
    12  	"golang.org/x/text/cases"
    13  	"golang.org/x/text/language"
    14  
    15  	"github.com/saucelabs/saucectl/internal/ci"
    16  	"github.com/saucelabs/saucectl/internal/config"
    17  	"github.com/saucelabs/saucectl/internal/espresso"
    18  	"github.com/saucelabs/saucectl/internal/flags"
    19  	"github.com/saucelabs/saucectl/internal/framework"
    20  	"github.com/saucelabs/saucectl/internal/region"
    21  	"github.com/saucelabs/saucectl/internal/report/captor"
    22  	"github.com/saucelabs/saucectl/internal/saucecloud"
    23  	"github.com/saucelabs/saucectl/internal/saucecloud/retry"
    24  	"github.com/saucelabs/saucectl/internal/segment"
    25  	"github.com/saucelabs/saucectl/internal/usage"
    26  )
    27  
    28  type espressoFlags struct {
    29  	Emulator flags.Emulator
    30  	Device   flags.Device
    31  }
    32  
    33  // NewEspressoCmd creates the 'run' command for espresso.
    34  func NewEspressoCmd() *cobra.Command {
    35  	sc := flags.SnakeCharmer{Fmap: map[string]*pflag.Flag{}}
    36  	lflags := espressoFlags{}
    37  
    38  	cmd := &cobra.Command{
    39  		Use:              "espresso",
    40  		Short:            "Run espresso tests",
    41  		Long:             "Unlike 'saucectl run', this command allows you to bypass the config file partially or entirely by configuring an adhoc suite (--name) via flags.",
    42  		Example:          `saucectl run espresso -c "" --name "My Suite" --app app.apk --testApp testApp.apk --otherApps=a.apk,b.apk --device name="Google Pixel.*",platformVersion=14.0,carrierConnectivity=false,deviceType=PHONE,private=false --emulator name="Android Emulator",platformVersion=8.0`,
    43  		SilenceUsage:     true,
    44  		Hidden:           true, // TODO reveal command once ready
    45  		TraverseChildren: true,
    46  		PreRunE: func(cmd *cobra.Command, args []string) error {
    47  			sc.BindAll()
    48  			return preRun()
    49  		},
    50  		Run: func(cmd *cobra.Command, args []string) {
    51  			exitCode, err := runEspresso(cmd, lflags, true)
    52  			if err != nil {
    53  				log.Err(err).Msg("failed to execute run command")
    54  			}
    55  			os.Exit(exitCode)
    56  		},
    57  	}
    58  
    59  	sc.Fset = cmd.Flags()
    60  	sc.String("name", "suite::name", "", "Sets the name of the job as it will appear on Sauce Labs")
    61  	sc.String("app", "espresso::app", "", "Specifies the app under test")
    62  	sc.String("appDescription", "espresso::appDescription", "", "Specifies description for the app")
    63  	sc.String("testApp", "espresso::testApp", "", "Specifies the test app")
    64  	sc.String("testAppDescription", "espresso::testAppDescription", "", "Specifies description for the testApp")
    65  	sc.StringSlice("otherApps", "espresso::otherApps", []string{}, "Specifies any additional apps that are installed alongside the main app")
    66  	sc.Int("passThreshold", "suite::passThreshold", 1, "The minimum number of successful attempts for a suite to be considered as 'passed'.")
    67  
    68  	// Test Options
    69  	sc.StringSlice("testOptions.class", "suite::testOptions::class", []string{}, "Only run the specified classes. Requires --name to be set.")
    70  	sc.StringSlice("testOptions.notClass", "suite::testOptions::notClass", []string{}, "Run all classes except those specified here. Requires --name to be set.")
    71  	sc.String("testOptions.package", "suite::testOptions::package", "", "Include package. Requires --name to be set.")
    72  	sc.String("testOptions.size", "suite::testOptions::size", "", "Include tests based on size. Requires --name to be set.")
    73  	sc.String("testOptions.annotation", "suite::testOptions::annotation", "", "Include tests based on the annotation. Requires --name to be set.")
    74  	sc.String("testOptions.notAnnotation", "suite::testOptions::notAnnotation", "", "Run all tests except those with this annotation. Requires --name to be set.")
    75  	sc.Int("testOptions.numShards", "suite::testOptions::numShards", 0, "Total number of shards. Requires --name to be set.")
    76  	sc.Bool("testOptions.useTestOrchestrator", "suite::testOptions::useTestOrchestrator", false, "Set the instrumentation to start with Test Orchestrator. Requires --name to be set.")
    77  
    78  	// Emulators and Devices
    79  	cmd.Flags().Var(&lflags.Emulator, "emulator", "Specifies the emulator to use for testing. Requires --name to be set.")
    80  	cmd.Flags().Var(&lflags.Device, "device", "Specifies the device to use for testing. Requires --name to be set.")
    81  
    82  	// Overwrite devices settings
    83  	sc.Bool("audioCapture", "suite::appSettings::audioCapture", false, "Overwrite app settings for real device to capture audio.")
    84  	sc.Bool("networkCapture", "suite::appSettings::instrumentation::networkCapture", false, "Overwrite app settings for real device to capture network.")
    85  
    86  	return cmd
    87  }
    88  
    89  func runEspresso(cmd *cobra.Command, espressoFlags espressoFlags, isCLIDriven bool) (int, error) {
    90  	if !isCLIDriven {
    91  		config.ValidateSchema(gFlags.cfgFilePath)
    92  	}
    93  
    94  	p, err := espresso.FromFile(gFlags.cfgFilePath)
    95  	if err != nil {
    96  		return 1, err
    97  	}
    98  
    99  	p.CLIFlags = flags.CaptureCommandLineFlags(cmd.Flags())
   100  
   101  	if err := applyEspressoFlags(&p, espressoFlags); err != nil {
   102  		return 1, err
   103  	}
   104  	espresso.SetDefaults(&p)
   105  
   106  	if err := espresso.Validate(p); err != nil {
   107  		return 1, err
   108  	}
   109  
   110  	regio := region.FromString(p.Sauce.Region)
   111  
   112  	if !gFlags.noAutoTagging {
   113  		p.Sauce.Metadata.Tags = append(p.Sauce.Metadata.Tags, ci.GetTags()...)
   114  	}
   115  
   116  	tracker := segment.DefaultTracker
   117  	if regio == region.Staging {
   118  		tracker.Enabled = false
   119  	}
   120  
   121  	go func() {
   122  		props := usage.Properties{}
   123  		props.SetFramework("espresso").SetFlags(cmd.Flags()).SetSauceConfig(p.Sauce).SetArtifacts(p.Artifacts).
   124  			SetNumSuites(len(p.Suites)).SetJobs(captor.Default.TestResults).SetSlack(p.Notifications.Slack).
   125  			SetSharding(espresso.IsSharded(p.Suites)).SetLaunchOrder(p.Sauce.LaunchOrder).
   126  			SetSmartRetry(p.IsSmartRetried()).SetReporters(p.Reporters)
   127  		tracker.Collect(cases.Title(language.English).String(cmds.FullName(cmd)), props)
   128  		_ = tracker.Close()
   129  	}()
   130  
   131  	cleanupArtifacts(p.Artifacts)
   132  
   133  	return runEspressoInCloud(p, regio)
   134  }
   135  
   136  func runEspressoInCloud(p espresso.Project, regio region.Region) (int, error) {
   137  	log.Info().Msg("Running Espresso in Sauce Labs")
   138  
   139  	creds := regio.Credentials()
   140  	restoClient := http.NewResto(regio.APIBaseURL(), creds.Username, creds.AccessKey, 0)
   141  	restoClient.ArtifactConfig = p.Artifacts.Download
   142  	testcompClient := http.NewTestComposer(regio.APIBaseURL(), creds, testComposerTimeout)
   143  	webdriverClient := http.NewWebdriver(regio.WebDriverBaseURL(), creds, webdriverTimeout)
   144  	appsClient := *http.NewAppStore(regio.APIBaseURL(), creds.Username, creds.AccessKey, gFlags.appStoreTimeout)
   145  	rdcClient := http.NewRDCService(regio.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout, p.Artifacts.Download)
   146  	insightsClient := http.NewInsightsService(regio.APIBaseURL(), creds, insightsTimeout)
   147  	iamClient := http.NewUserService(regio.APIBaseURL(), creds, iamTimeout)
   148  
   149  	r := saucecloud.EspressoRunner{
   150  		Project: p,
   151  		CloudRunner: saucecloud.CloudRunner{
   152  			ProjectUploader: &appsClient,
   153  			JobService: saucecloud.JobService{
   154  				VDCStarter:    &webdriverClient,
   155  				RDCStarter:    &rdcClient,
   156  				VDCReader:     &restoClient,
   157  				RDCReader:     &rdcClient,
   158  				VDCWriter:     &testcompClient,
   159  				VDCStopper:    &restoClient,
   160  				RDCStopper:    &rdcClient,
   161  				VDCDownloader: &restoClient,
   162  				RDCDownloader: &rdcClient,
   163  			},
   164  			TunnelService:   &restoClient,
   165  			MetadataService: &testcompClient,
   166  			InsightsService: &insightsClient,
   167  			UserService:     &iamClient,
   168  			BuildService:    &restoClient,
   169  			Region:          regio,
   170  			ShowConsoleLog:  p.ShowConsoleLog,
   171  			Reporters: createReporters(p.Reporters, p.Notifications, p.Sauce.Metadata, &testcompClient, &restoClient,
   172  				"espresso", "sauce", gFlags.async),
   173  			Framework: framework.Framework{Name: espresso.Kind},
   174  			Async:     gFlags.async,
   175  			FailFast:  gFlags.failFast,
   176  			Retrier: &retry.JunitRetrier{
   177  				RDCReader: &rdcClient,
   178  				VDCReader: &restoClient,
   179  			},
   180  		},
   181  	}
   182  
   183  	return r.RunProject()
   184  }
   185  
   186  func hasKey(testOptions map[string]interface{}, key string) bool {
   187  	_, ok := testOptions[key]
   188  	return ok
   189  }
   190  
   191  func applyEspressoFlags(p *espresso.Project, flags espressoFlags) error {
   192  	if gFlags.selectedSuite != "" {
   193  		if err := espresso.FilterSuites(p, gFlags.selectedSuite); err != nil {
   194  			return err
   195  		}
   196  	}
   197  
   198  	if p.Suite.Name == "" {
   199  		isErr := hasKey(p.Suite.TestOptions, "class") ||
   200  			hasKey(p.Suite.TestOptions, "notClass") ||
   201  			hasKey(p.Suite.TestOptions, "package") ||
   202  			hasKey(p.Suite.TestOptions, "notPackage") ||
   203  			hasKey(p.Suite.TestOptions, "size") ||
   204  			hasKey(p.Suite.TestOptions, "annotation") ||
   205  			hasKey(p.Suite.TestOptions, "notAnnotation") ||
   206  			hasKey(p.Suite.TestOptions, "numShards") ||
   207  			hasKey(p.Suite.TestOptions, "useTestOrchestrator") ||
   208  			flags.Device.Changed ||
   209  			flags.Emulator.Changed
   210  
   211  		if isErr {
   212  			return ErrEmptySuiteName
   213  		}
   214  
   215  		return nil
   216  	}
   217  
   218  	if flags.Device.Changed {
   219  		p.Suite.Devices = append(p.Suite.Devices, flags.Device.Device)
   220  	}
   221  
   222  	if flags.Emulator.Changed {
   223  		p.Suite.Emulators = append(p.Suite.Emulators, flags.Emulator.Emulator)
   224  	}
   225  
   226  	p.Suites = []espresso.Suite{p.Suite}
   227  
   228  	return nil
   229  }