github.com/saucelabs/saucectl@v0.175.1/internal/saucecloud/espresso.go (about) 1 package saucecloud 2 3 import ( 4 "fmt" 5 "strconv" 6 "strings" 7 8 "github.com/rs/zerolog/log" 9 "github.com/saucelabs/saucectl/internal/config" 10 "github.com/saucelabs/saucectl/internal/espresso" 11 "github.com/saucelabs/saucectl/internal/job" 12 "github.com/saucelabs/saucectl/internal/msg" 13 ) 14 15 // deviceConfig represent the configuration for a specific device. 16 type deviceConfig struct { 17 ID string 18 name string 19 platformName string 20 platformVersion string 21 orientation string 22 isRealDevice bool 23 hasCarrier bool 24 deviceType string 25 privateOnly bool 26 } 27 28 // EspressoRunner represents the Sauce Labs cloud implementation for cypress. 29 type EspressoRunner struct { 30 CloudRunner 31 Project espresso.Project 32 } 33 34 // RunProject runs the tests defined in cypress.Project. 35 func (r *EspressoRunner) RunProject() (int, error) { 36 exitCode := 1 37 38 if err := r.validateTunnel( 39 r.Project.Sauce.Tunnel.Name, 40 r.Project.Sauce.Tunnel.Owner, 41 r.Project.DryRun, 42 r.Project.Sauce.Tunnel.Timeout, 43 ); err != nil { 44 return 1, err 45 } 46 47 var err error 48 r.Project.Espresso.App, err = r.uploadProject(r.Project.Espresso.App, r.Project.Espresso.AppDescription, appUpload, r.Project.DryRun) 49 if err != nil { 50 return exitCode, err 51 } 52 53 r.Project.Espresso.OtherApps, err = r.uploadProjects(r.Project.Espresso.OtherApps, otherAppsUpload, r.Project.DryRun) 54 if err != nil { 55 return exitCode, err 56 } 57 58 cache := map[string]string{} 59 for i, suite := range r.Project.Suites { 60 if val, ok := cache[suite.TestApp]; ok { 61 r.Project.Suites[i].TestApp = val 62 continue 63 } 64 65 testAppURL, err := r.uploadProject(suite.TestApp, suite.TestAppDescription, testAppUpload, r.Project.DryRun) 66 if err != nil { 67 return exitCode, err 68 } 69 r.Project.Suites[i].TestApp = testAppURL 70 cache[suite.TestApp] = testAppURL 71 } 72 73 if r.Project.DryRun { 74 r.dryRun() 75 return 0, nil 76 } 77 78 passed := r.runSuites() 79 if passed { 80 exitCode = 0 81 } 82 83 return exitCode, nil 84 } 85 86 func (r *EspressoRunner) runSuites() bool { 87 sigChan := r.registerSkipSuitesOnSignal() 88 defer unregisterSignalCapture(sigChan) 89 90 jobOpts, results, err := r.createWorkerPool(r.Project.Sauce.Concurrency, r.Project.Sauce.Retries) 91 if err != nil { 92 return false 93 } 94 defer close(results) 95 96 suites := r.Project.Suites 97 if r.Project.Sauce.LaunchOrder != "" { 98 history, err := r.getHistory(r.Project.Sauce.LaunchOrder) 99 if err != nil { 100 log.Warn().Err(err).Msg(msg.RetrieveJobHistoryError) 101 } else { 102 suites = espresso.SortByHistory(suites, history) 103 } 104 } 105 // Submit suites to work on. 106 jobsCount := r.calculateJobsCount(suites) 107 go func() { 108 for _, s := range suites { 109 numShards, _ := getNumShardsAndShardIndex(s.TestOptions) 110 // Automatically apply ShardIndex if numShards is defined 111 if numShards > 0 { 112 for i := 0; i < numShards; i++ { 113 // Enforce copy of the map to ensure it is not shared. 114 testOptions := map[string]interface{}{} 115 for k, v := range s.TestOptions { 116 testOptions[k] = v 117 } 118 s.TestOptions = testOptions 119 s.TestOptions["shardIndex"] = i 120 for _, c := range enumerateDevices(s.Devices, s.Emulators) { 121 log.Debug().Str("suite", s.Name).Str("device", fmt.Sprintf("%v", c)).Msg("Starting job") 122 r.startJob(jobOpts, s, r.Project.Espresso.App, s.TestApp, r.Project.Espresso.OtherApps, c) 123 } 124 } 125 } else { 126 for _, c := range enumerateDevices(s.Devices, s.Emulators) { 127 log.Debug().Str("suite", s.Name).Str("device", fmt.Sprintf("%v", c)).Msg("Starting job") 128 r.startJob(jobOpts, s, r.Project.Espresso.App, s.TestApp, r.Project.Espresso.OtherApps, c) 129 } 130 } 131 } 132 }() 133 134 return r.collectResults(r.Project.Artifacts.Download, results, jobsCount) 135 } 136 137 func (r *EspressoRunner) dryRun() { 138 fmt.Println("\nThe following test suites would have run:") 139 for _, s := range r.Project.Suites { 140 fmt.Printf(" - %s\n", s.Name) 141 for _, c := range enumerateDevices(s.Devices, s.Emulators) { 142 fmt.Printf(" - on %s %s %s\n", c.name, c.platformName, c.platformVersion) 143 } 144 } 145 fmt.Println() 146 } 147 148 // enumerateDevices returns a list of emulators and devices targeted by the current suite. 149 func enumerateDevices(devices []config.Device, virtualDevices []config.VirtualDevice) []deviceConfig { 150 var configs []deviceConfig 151 152 for _, e := range virtualDevices { 153 for _, p := range e.PlatformVersions { 154 configs = append(configs, deviceConfig{ 155 name: e.Name, 156 platformName: e.PlatformName, 157 platformVersion: p, 158 orientation: e.Orientation, 159 }) 160 } 161 } 162 163 for _, d := range devices { 164 configs = append(configs, deviceConfig{ 165 ID: d.ID, 166 name: d.Name, 167 platformName: d.PlatformName, 168 platformVersion: d.PlatformVersion, 169 isRealDevice: true, 170 hasCarrier: d.Options.CarrierConnectivity, 171 deviceType: d.Options.DeviceType, 172 privateOnly: d.Options.Private, 173 }) 174 } 175 return configs 176 } 177 178 // getNumShardsAndShardIndex extracts numShards and shardIndex from testOptions. 179 func getNumShardsAndShardIndex(testOptions map[string]interface{}) (int, int) { 180 outNumShards := 0 181 outShardIndex := 0 182 numShards, hasNumShards := testOptions["numShards"] 183 shardIndex, hasShardIndex := testOptions["shardIndex"] 184 if hasNumShards { 185 if v, err := strconv.Atoi(fmt.Sprintf("%v", numShards)); err == nil { 186 outNumShards = v 187 } 188 } 189 if hasShardIndex { 190 if v, err := strconv.Atoi(fmt.Sprintf("%v", shardIndex)); err == nil { 191 outShardIndex = v 192 } 193 } 194 return outNumShards, outShardIndex 195 } 196 197 // startJob add the job to the list for the workers. 198 func (r *EspressoRunner) startJob(jobOpts chan<- job.StartOptions, s espresso.Suite, appFileURI, testAppFileURI string, otherAppsURIs []string, d deviceConfig) { 199 displayName := s.Name 200 numShards, shardIndex := getNumShardsAndShardIndex(s.TestOptions) 201 if numShards > 0 { 202 displayName = fmt.Sprintf("%s (shard %d/%d)", displayName, shardIndex+1, numShards) 203 } 204 205 jobOpts <- job.StartOptions{ 206 DisplayName: displayName, 207 Timeout: s.Timeout, 208 ConfigFilePath: r.Project.ConfigFilePath, 209 CLIFlags: r.Project.CLIFlags, 210 App: appFileURI, 211 TestApp: testAppFileURI, 212 Suite: testAppFileURI, 213 OtherApps: otherAppsURIs, 214 Framework: "espresso", 215 FrameworkVersion: "1.0.0-stable", 216 PlatformName: d.platformName, 217 PlatformVersion: d.platformVersion, 218 DeviceID: d.ID, 219 DeviceName: d.name, 220 DeviceOrientation: d.orientation, 221 Name: displayName, 222 Build: r.Project.Sauce.Metadata.Build, 223 Tags: r.Project.Sauce.Metadata.Tags, 224 Tunnel: job.TunnelOptions{ 225 ID: r.Project.Sauce.Tunnel.Name, 226 Parent: r.Project.Sauce.Tunnel.Owner, 227 }, 228 Experiments: r.Project.Sauce.Experiments, 229 TestOptions: s.TestOptions, 230 Attempt: 0, 231 Retries: r.Project.Sauce.Retries, 232 Visibility: r.Project.Sauce.Visibility, 233 PassThreshold: s.PassThreshold, 234 SmartRetry: job.SmartRetry{ 235 FailedOnly: s.SmartRetry.IsRetryFailedOnly(), 236 }, 237 238 // RDC Specific flags 239 RealDevice: d.isRealDevice, 240 DeviceHasCarrier: d.hasCarrier, 241 DeviceType: d.deviceType, 242 DevicePrivateOnly: d.privateOnly, 243 244 // Overwrite device settings 245 RealDeviceKind: strings.ToLower(espresso.Android), 246 AppSettings: job.AppSettings{ 247 AudioCapture: s.AppSettings.AudioCapture, 248 Instrumentation: job.Instrumentation{ 249 NetworkCapture: s.AppSettings.Instrumentation.NetworkCapture, 250 }, 251 }, 252 } 253 } 254 255 func (r *EspressoRunner) calculateJobsCount(suites []espresso.Suite) int { 256 total := 0 257 for _, s := range suites { 258 jobs := len(enumerateDevices(s.Devices, s.Emulators)) 259 numShards, _ := getNumShardsAndShardIndex(s.TestOptions) 260 if numShards > 0 { 261 jobs *= numShards 262 } 263 total += jobs 264 } 265 return total 266 }