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 }