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  }