github.com/saucelabs/saucectl@v0.175.1/internal/cmd/run/xcuitest.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/flags" 18 "github.com/saucelabs/saucectl/internal/framework" 19 "github.com/saucelabs/saucectl/internal/region" 20 "github.com/saucelabs/saucectl/internal/report/captor" 21 "github.com/saucelabs/saucectl/internal/saucecloud" 22 "github.com/saucelabs/saucectl/internal/saucecloud/retry" 23 "github.com/saucelabs/saucectl/internal/segment" 24 "github.com/saucelabs/saucectl/internal/usage" 25 "github.com/saucelabs/saucectl/internal/xcuitest" 26 ) 27 28 type xcuitestFlags struct { 29 Device flags.Device 30 Simulator flags.Simulator 31 } 32 33 // NewXCUITestCmd creates the 'run' command for XCUITest. 34 func NewXCUITestCmd() *cobra.Command { 35 sc := flags.SnakeCharmer{Fmap: map[string]*pflag.Flag{}} 36 lflags := xcuitestFlags{} 37 38 cmd := &cobra.Command{ 39 Use: "xcuitest", 40 Short: "Run xcuitest 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 xcuitest -c "" --name "My Suite" --app app.ipa --testApp testApp.ipa --otherApps=a.ipa,b.ipa --device name="iPhone.*",platformVersion=14.0,carrierConnectivity=false,deviceType=PHONE,private=false`, 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 := runXcuitest(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", "", "Creates a new adhoc suite with this name. Suites defined in the config will be ignored.") 61 sc.String("app", "xcuitest::app", "", "Specifies the app under test") 62 sc.String("appDescription", "xcuitest::appDescription", "", "Specifies description for the app") 63 sc.String("testApp", "xcuitest::testApp", "", "Specifies the test app") 64 sc.String("testAppDescription", "xcuitest::testAppDescription", "", "Specifies description for the testApp") 65 sc.StringSlice("otherApps", "xcuitest::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 sc.String("shard", "suite::shard", "", "When shard is configured as concurrency, saucectl automatically splits the tests by concurrency so that they can easily run in parallel. Requires --name to be set.") 69 sc.String("testListFile", "suite::testListFile", "", "This file containing tests will be used in sharding by concurrency. Requires --name to be set.") 70 71 // Test Options 72 sc.StringSlice("testOptions.class", "suite::testOptions::class", []string{}, "Only run the specified classes. Requires --name to be set.") 73 sc.StringSlice("testOptions.notClass", "suite::testOptions::notClass", []string{}, "Run all classes except those specified here. Requires --name to be set.") 74 75 // Devices 76 cmd.Flags().Var(&lflags.Device, "device", "Specifies the device to use for testing. Requires --name to be set.") 77 cmd.Flags().Var(&lflags.Simulator, "simulator", "Specifies the simulator to use for testing. Requires --name to be set.") 78 79 // Overwrite devices settings 80 sc.Bool("audioCapture", "suite::appSettings::audioCapture", false, "Overwrite app settings for real device to capture audio.") 81 sc.Bool("networkCapture", "suite::appSettings::instrumentation::networkCapture", false, "Overwrite app settings for real device to capture network.") 82 83 return cmd 84 } 85 86 func runXcuitest(cmd *cobra.Command, xcuiFlags xcuitestFlags, isCLIDriven bool) (int, error) { 87 if !isCLIDriven { 88 config.ValidateSchema(gFlags.cfgFilePath) 89 } 90 91 p, err := xcuitest.FromFile(gFlags.cfgFilePath) 92 if err != nil { 93 return 1, err 94 } 95 96 p.CLIFlags = flags.CaptureCommandLineFlags(cmd.Flags()) 97 98 if err := applyXCUITestFlags(&p, xcuiFlags); err != nil { 99 return 1, err 100 } 101 xcuitest.SetDefaults(&p) 102 103 if err := xcuitest.Validate(p); err != nil { 104 return 1, err 105 } 106 if err := xcuitest.ShardSuites(&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("xcuitest").SetFlags(cmd.Flags()).SetSauceConfig(p.Sauce).SetArtifacts(p.Artifacts). 124 SetNumSuites(len(p.Suites)).SetJobs(captor.Default.TestResults).SetSlack(p.Notifications.Slack). 125 SetSharding(xcuitest.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 runXcuitestInCloud(p, regio) 134 } 135 136 func runXcuitestInCloud(p xcuitest.Project, regio region.Region) (int, error) { 137 log.Info().Msg("Running XCUITest in Sauce Labs") 138 139 creds := regio.Credentials() 140 141 restoClient := http.NewResto(regio.APIBaseURL(), creds.Username, creds.AccessKey, 0) 142 restoClient.ArtifactConfig = p.Artifacts.Download 143 testcompClient := http.NewTestComposer(regio.APIBaseURL(), creds, testComposerTimeout) 144 webdriverClient := http.NewWebdriver(regio.WebDriverBaseURL(), creds, webdriverTimeout) 145 appsClient := *http.NewAppStore(regio.APIBaseURL(), creds.Username, creds.AccessKey, gFlags.appStoreTimeout) 146 rdcClient := http.NewRDCService(regio.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout, p.Artifacts.Download) 147 insightsClient := http.NewInsightsService(regio.APIBaseURL(), creds, insightsTimeout) 148 iamClient := http.NewUserService(regio.APIBaseURL(), creds, iamTimeout) 149 150 r := saucecloud.XcuitestRunner{ 151 Project: p, 152 CloudRunner: saucecloud.CloudRunner{ 153 ProjectUploader: &appsClient, 154 JobService: saucecloud.JobService{ 155 VDCStarter: &webdriverClient, 156 RDCStarter: &rdcClient, 157 VDCReader: &restoClient, 158 RDCReader: &rdcClient, 159 VDCWriter: &testcompClient, 160 VDCStopper: &restoClient, 161 RDCStopper: &rdcClient, 162 VDCDownloader: &restoClient, 163 RDCDownloader: &rdcClient, 164 }, 165 TunnelService: &restoClient, 166 MetadataService: &testcompClient, 167 InsightsService: &insightsClient, 168 UserService: &iamClient, 169 BuildService: &restoClient, 170 Region: regio, 171 ShowConsoleLog: p.ShowConsoleLog, 172 Reporters: createReporters(p.Reporters, p.Notifications, p.Sauce.Metadata, &testcompClient, &restoClient, 173 "xcuitest", "sauce", gFlags.async), 174 Framework: framework.Framework{Name: xcuitest.Kind}, 175 Async: gFlags.async, 176 FailFast: gFlags.failFast, 177 Retrier: &retry.JunitRetrier{ 178 VDCReader: &restoClient, 179 RDCReader: &rdcClient, 180 }, 181 }, 182 } 183 return r.RunProject() 184 } 185 186 func applyXCUITestFlags(p *xcuitest.Project, flags xcuitestFlags) error { 187 if gFlags.selectedSuite != "" { 188 if err := xcuitest.FilterSuites(p, gFlags.selectedSuite); err != nil { 189 return err 190 } 191 } 192 193 if p.Suite.Name == "" { 194 isErr := len(p.Suite.TestOptions.Class) != 0 || 195 len(p.Suite.TestOptions.NotClass) != 0 || 196 flags.Device.Changed || 197 flags.Simulator.Changed 198 199 if isErr { 200 return ErrEmptySuiteName 201 } 202 203 return nil 204 } 205 206 if flags.Device.Changed { 207 p.Suite.Devices = append(p.Suite.Devices, flags.Device.Device) 208 } 209 210 if flags.Simulator.Changed { 211 p.Suite.Simulators = append(p.Suite.Simulators, flags.Simulator.Simulator) 212 } 213 214 p.Suites = []xcuitest.Suite{p.Suite} 215 216 return nil 217 }