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  }