From f4128a63e334825d4af02e3493a811ccd02565fc Mon Sep 17 00:00:00 2001 From: Diego Perini Date: Wed, 20 Dec 2023 17:59:32 +0300 Subject: [PATCH] Implementing blocking test runs on iOS17 --- ios/appservice/appservice.go | 2 - ios/testmanagerd/xcuitestrunner.go | 125 ++++++++++++++++---------- ios/testmanagerd/xcuitestrunner_11.go | 39 ++++---- ios/testmanagerd/xcuitestrunner_12.go | 39 ++++---- main.go | 12 ++- 5 files changed, 126 insertions(+), 91 deletions(-) diff --git a/ios/appservice/appservice.go b/ios/appservice/appservice.go index cffe99bc..21b1e56e 100644 --- a/ios/appservice/appservice.go +++ b/ios/appservice/appservice.go @@ -151,8 +151,6 @@ func buildAppLaunchPayload(deviceId string, bundleId string, args []interface{}, panic(err) } - env["TERM"] = "xterm-256color" - return buildRequest(deviceId, "com.apple.coredevice.feature.launchapplication", map[string]interface{}{ "applicationSpecifier": map[string]interface{}{ "bundleIdentifier": map[string]interface{}{ diff --git a/ios/testmanagerd/xcuitestrunner.go b/ios/testmanagerd/xcuitestrunner.go index 0971f1a7..af463354 100644 --- a/ios/testmanagerd/xcuitestrunner.go +++ b/ios/testmanagerd/xcuitestrunner.go @@ -178,6 +178,8 @@ type ProxyDispatcher struct { testRunnerReadyWithCapabilities dtx.MethodWithResponse dtxConnection *dtx.Connection id string + closeChannel chan interface{} + closedChannel chan interface{} } func (p ProxyDispatcher) Dispatch(m dtx.Message) { @@ -202,7 +204,10 @@ func (p ProxyDispatcher) Dispatch(m dtx.Message) { p.dtxConnection.Send(messageBytes) case "_XCT_didFinishExecutingTestPlan": log.Info("_XCT_didFinishExecutingTestPlan received. Closing test.") - CloseXCUITestRunner() + p.DispatchClose() + case "_XCT_didFailToBootstrapWithError:": + log.Info("_XCT_didFailToBootstrapWithError received. Closing test.") + p.DispatchClose() default: log.WithFields(log.Fields{"sel": method}).Infof("device called local method") } @@ -213,10 +218,24 @@ func (p ProxyDispatcher) Dispatch(m dtx.Message) { log.Tracef("dispatcher received: %s", m.String()) } +func (p *ProxyDispatcher) DispatchClose() error { + var signal interface{} + go func() { + p.closeChannel <- signal + log.Debug("DIEGO") + }() + select { + case <-p.closedChannel: + return nil + case <-time.After(10 * time.Second): + return fmt.Errorf("Failed closing, exiting due to timeout") + } +} + func newDtxProxyWithConfig(dtxConnection *dtx.Connection, testConfig nskeyedarchiver.XCTestConfiguration) dtxproxy { testBundleReadyChannel := make(chan dtx.Message, 1) //(xide XCTestManager_IDEInterface) - proxyDispatcher := ProxyDispatcher{testBundleReadyChannel: testBundleReadyChannel, dtxConnection: dtxConnection, testRunnerReadyWithCapabilities: testRunnerReadyWithCapabilitiesConfig(testConfig)} + proxyDispatcher := ProxyDispatcher{testBundleReadyChannel: testBundleReadyChannel, dtxConnection: dtxConnection, testRunnerReadyWithCapabilities: testRunnerReadyWithCapabilitiesConfig(testConfig), closeChannel: make(chan interface{}, 1), closedChannel: make(chan interface{}, 1)} IDEDaemonProxy := dtxConnection.RequestChannelIdentifier(ideToDaemonProxyChannelName, proxyDispatcher) ideInterface := XCTestManager_IDEInterface{IDEDaemonProxy: IDEDaemonProxy, testConfig: testConfig, testBundleReadyChannel: testBundleReadyChannel} @@ -263,13 +282,17 @@ func RunXCUITest(bundleID string, testRunnerBundleID string, xctestConfigName st xctestConfigName = info.targetAppBundleName + "UITests.xctest" } - return RunXCUIWithBundleIdsCtx(nil, bundleID, testRunnerBundleID, xctestConfigName, device, nil, env) + _, err = RunXCUIWithBundleIdsCtx(nil, bundleID, testRunnerBundleID, xctestConfigName, device, nil, env) + return err } -var ( - closeChan = make(chan interface{}) - closedChan = make(chan interface{}) -) +type TestRunner struct { + proxyDispatcher ProxyDispatcher +} + +func (t *TestRunner) Close() error { + return t.proxyDispatcher.DispatchClose() +} func RunXCUIWithBundleIdsCtx( ctx context.Context, @@ -279,10 +302,10 @@ func RunXCUIWithBundleIdsCtx( device ios.DeviceEntry, wdaargs []string, wdaenv []string, -) error { +) (*TestRunner, error) { version, err := ios.GetProductVersion(device) if err != nil { - return err + return nil, err } if version.LessThan(ios.IOS14()) { @@ -296,46 +319,36 @@ func RunXCUIWithBundleIdsCtx( } log.Debugf("iOS version: %s detected, running with ios17 support", version) - return runXUITestWithBundleIdsXcode15Ctx(bundleID, testRunnerBundleID, xctestConfigFileName, device) -} - -func CloseXCUITestRunner() error { - var signal interface{} - go func() { closeChan <- signal }() - select { - case <-closedChan: - return nil - case <-time.After(5 * time.Second): - return fmt.Errorf("Failed closing, exiting due to timeout") - } + return runXUITestWithBundleIdsXcode15Ctx(ctx, bundleID, testRunnerBundleID, xctestConfigFileName, device) } func runXUITestWithBundleIdsXcode15Ctx( + ctx context.Context, bundleID string, testRunnerBundleID string, xctestConfigFileName string, device ios.DeviceEntry, -) error { +) (*TestRunner, error) { conn1, err := dtx.NewTunnelConnection(device, testmanagerdiOS17) if err != nil { - return err + return nil, err } defer conn1.Close() conn2, err := dtx.NewTunnelConnection(device, testmanagerdiOS17) if err != nil { - return err + return nil, err } defer conn2.Close() installationProxy, err := installationproxy.New(device) if err != nil { - return err + return nil, err } defer installationProxy.Close() apps, err := installationProxy.BrowseUserApps() if err != nil { - return err + return nil, err } info, err := getAppInfos(bundleID, testRunnerBundleID, apps) @@ -348,17 +361,17 @@ func runXUITestWithBundleIdsXcode15Ctx( proto, err := ideDaemonProxy1.daemonConnection.initiateSessionWithIdentifier(testSessionID, 29) if err != nil { - return err + return nil, err } log.WithField("proto", proto).Info("got capabilities") appserviceConn, err := appservice.New(device) if err != nil { - return err + return nil, err } defer appserviceConn.Close() - pid, err := startTestRunner17(device, appserviceConn, "", testRunnerBundleID, strings.ToUpper(testSessionID.String()), info.testrunnerAppPath+"/PlugIns/"+xctestConfigFileName, []string{}, []string{}) + pid, err := startTestRunner17(device, appserviceConn, "", testRunnerBundleID, strings.ToUpper(testSessionID.String()), info.testrunnerAppPath+"/PlugIns/"+xctestConfigFileName) localCaps := nskeyedarchiver.XCTCapabilities{CapabilitiesDictionary: map[string]interface{}{ "XCTIssue capability": uint64(1), @@ -376,37 +389,61 @@ func runXUITestWithBundleIdsXcode15Ctx( ideDaemonProxy2 := newDtxProxyWithConfig(conn2, testconfig) caps, err := ideDaemonProxy1.daemonConnection.initiateControlSessionWithCapabilities(localCaps) if err != nil { - return err + return nil, err } log.WithField("caps", caps).Info("got capabilities") authorized, err := ideDaemonProxy2.daemonConnection.authorizeTestSessionWithProcessID(pid) if err != nil { - return err + return nil, err } log.WithField("authorized", authorized).Info("authorized") err = ideDaemonProxy2.daemonConnection.initiateControlSession(pid, proto) if err != nil { - return err + return nil, err } log.Debug("control session initiated") - ideInterfaceChannel := ideDaemonProxy1.dtxConnection.ForChannelRequest(ProxyDispatcher{id: "dtxproxy:XCTestDriverInterface:XCTestManager_IDEInterface"}) + proxyDispatcher := ProxyDispatcher{id: "dtxproxy:XCTestDriverInterface:XCTestManager_IDEInterface", closeChannel: make(chan interface{}, 1), closedChannel: make(chan interface{}, 1)} + ideInterfaceChannel := ideDaemonProxy1.dtxConnection.ForChannelRequest(proxyDispatcher) err = ideDaemonProxy1.daemonConnection.startExecutingTestPlanWithProtocolVersion(ideInterfaceChannel, proto) - time.Sleep(60 * time.Second) + if ctx != nil { + select { + case <-ctx.Done(): + log.Infof("Killing test runner with pid %d ...", pid) + err = appserviceConn.KillProcess(int(pid)) + if err != nil { + return nil, err + } + log.Info("Test runner killed with success") + } + return &TestRunner{proxyDispatcher: proxyDispatcher}, nil + } - return nil + <-proxyDispatcher.closeChannel + log.Debugf("Killing test runner with pid %d ...", pid) + err = appserviceConn.KillProcess(int(pid)) + if err != nil { + return nil, err + } + log.Debugf("Test runner killed with success") + var signal interface{} + proxyDispatcher.closedChannel <- signal + + return &TestRunner{proxyDispatcher: proxyDispatcher}, nil } -func startTestRunner17(device ios.DeviceEntry, appserviceConn *appservice.Connection, xctestConfigPath string, bundleID string, - sessionIdentifier string, testBundlePath string, wdaargs []string, wdaenv []string, +func startTestRunner17( + device ios.DeviceEntry, + appserviceConn *appservice.Connection, + xctestConfigPath string, + bundleID string, + sessionIdentifier string, + testBundlePath string, ) (uint64, error) { args := []interface{}{} - for _, arg := range wdaargs { - args = append(args, arg) - } env := map[string]interface{}{ "CA_ASSERT_MAIN_THREAD_TRANSACTIONS": "0", @@ -425,14 +462,6 @@ func startTestRunner17(device ios.DeviceEntry, appserviceConn *appservice.Connec "XCTestSessionIdentifier": strings.ToUpper(sessionIdentifier), } - for _, entrystring := range wdaenv { - entry := strings.Split(entrystring, "=") - key := entry[0] - value := entry[1] - env[key] = value - log.Debugf("adding extra env %s=%s", key, value) - } - opts := map[string]interface{}{ "ActivateSuspended": uint64(1), "StartSuspendedKey": uint64(0), diff --git a/ios/testmanagerd/xcuitestrunner_11.go b/ios/testmanagerd/xcuitestrunner_11.go index 3008aee9..d8bf5857 100644 --- a/ios/testmanagerd/xcuitestrunner_11.go +++ b/ios/testmanagerd/xcuitestrunner_11.go @@ -18,23 +18,23 @@ func RunXCUIWithBundleIdsXcode11Ctx( device ios.DeviceEntry, args []string, env []string, -) error { +) (*TestRunner, error) { log.Debugf("set up xcuitest") testSessionId, xctestConfigPath, testConfig, testInfo, err := setupXcuiTest(device, bundleID, testRunnerBundleID, xctestConfigFileName) if err != nil { - return err + return nil, err } log.Debugf("test session setup ok") conn, err := dtx.NewUsbmuxdConnection(device, testmanagerd) if err != nil { - return err + return nil, err } defer conn.Close() ideDaemonProxy := newDtxProxyWithConfig(conn, testConfig) conn2, err := dtx.NewUsbmuxdConnection(device, testmanagerd) if err != nil { - return err + return nil, err } defer conn2.Close() log.Debug("connections ready") @@ -44,27 +44,28 @@ func RunXCUIWithBundleIdsXcode11Ctx( protocolVersion := uint64(25) _, err = ideDaemonProxy.daemonConnection.initiateSessionWithIdentifier(testSessionId, protocolVersion) if err != nil { - return err + return nil, err } pControl, err := instruments.NewProcessControl(device) if err != nil { - return err + return nil, err } defer pControl.Close() pid, err := startTestRunner11(pControl, xctestConfigPath, testRunnerBundleID, testSessionId.String(), testInfo.testrunnerAppPath+"/PlugIns/"+xctestConfigFileName, args, env) if err != nil { - return err + return nil, err } log.Debugf("Runner started with pid:%d, waiting for testBundleReady", pid) err = ideDaemonProxy2.daemonConnection.initiateControlSession(pid, protocolVersion) if err != nil { - return err + return nil, err } log.Debugf("control session initiated") - ideInterfaceChannel := ideDaemonProxy.dtxConnection.ForChannelRequest(ProxyDispatcher{id: "emty"}) + proxyDispatcher := ProxyDispatcher{id: "emty", closeChannel: make(chan interface{}), closedChannel: make(chan interface{})} + ideInterfaceChannel := ideDaemonProxy.dtxConnection.ForChannelRequest(proxyDispatcher) log.Debug("start executing testplan") err = ideDaemonProxy2.daemonConnection.startExecutingTestPlanWithProtocolVersion(ideInterfaceChannel, 25) @@ -74,26 +75,26 @@ func RunXCUIWithBundleIdsXcode11Ctx( if ctx != nil { select { case <-ctx.Done(): - log.Infof("Killing WebDriverAgent with pid %d ...", pid) + log.Infof("Killing test runner with pid %d ...", pid) err = pControl.KillProcess(pid) if err != nil { - return err + return nil, err } - log.Info("WDA killed with success") + log.Info("Test runner killed with success") } - return nil + return &TestRunner{proxyDispatcher: proxyDispatcher}, nil } log.Debugf("done starting test") - <-closeChan - log.Infof("Killing WebDriverAgent with pid %d ...", pid) + <-proxyDispatcher.closeChannel + log.Infof("Killing test runner with pid %d ...", pid) err = pControl.KillProcess(pid) if err != nil { - return err + return nil, err } - log.Info("WDA killed with success") + log.Info("Test runner killed with success") var signal interface{} - closedChan <- signal - return nil + proxyDispatcher.closedChannel <- signal + return &TestRunner{proxyDispatcher: proxyDispatcher}, nil } func startTestRunner11(pControl *instruments.ProcessControl, xctestConfigPath string, bundleID string, diff --git a/ios/testmanagerd/xcuitestrunner_12.go b/ios/testmanagerd/xcuitestrunner_12.go index c57bd765..71f78ddc 100644 --- a/ios/testmanagerd/xcuitestrunner_12.go +++ b/ios/testmanagerd/xcuitestrunner_12.go @@ -14,22 +14,22 @@ import ( func RunXUITestWithBundleIdsXcode12Ctx(ctx context.Context, bundleID string, testRunnerBundleID string, xctestConfigFileName string, device ios.DeviceEntry, args []string, env []string, -) error { +) (*TestRunner, error) { conn, err := dtx.NewUsbmuxdConnection(device, testmanagerdiOS14) if err != nil { - return err + return nil, err } testSessionId, xctestConfigPath, testConfig, testInfo, err := setupXcuiTest(device, bundleID, testRunnerBundleID, xctestConfigFileName) if err != nil { - return err + return nil, err } defer conn.Close() ideDaemonProxy := newDtxProxyWithConfig(conn, testConfig) conn2, err := dtx.NewUsbmuxdConnection(device, testmanagerdiOS14) if err != nil { - return err + return nil, err } defer conn2.Close() log.Debug("connections ready") @@ -37,7 +37,7 @@ func RunXUITestWithBundleIdsXcode12Ctx(ctx context.Context, bundleID string, tes ideDaemonProxy2.ideInterface.testConfig = testConfig caps, err := ideDaemonProxy.daemonConnection.initiateControlSessionWithCapabilities(nskeyedarchiver.XCTCapabilities{}) if err != nil { - return err + return nil, err } log.Debug(caps) localCaps := nskeyedarchiver.XCTCapabilities{CapabilitiesDictionary: map[string]interface{}{ @@ -48,22 +48,23 @@ func RunXUITestWithBundleIdsXcode12Ctx(ctx context.Context, bundleID string, tes caps2, err := ideDaemonProxy2.daemonConnection.initiateSessionWithIdentifierAndCaps(testSessionId, localCaps) if err != nil { - return err + return nil, err } log.Debug(caps2) pControl, err := instruments.NewProcessControl(device) if err != nil { - return err + return nil, err } defer pControl.Close() pid, err := startTestRunner12(pControl, xctestConfigPath, testRunnerBundleID, testSessionId.String(), testInfo.testrunnerAppPath+"/PlugIns/"+xctestConfigFileName, args, env) if err != nil { - return err + return nil, err } log.Debugf("Runner started with pid:%d, waiting for testBundleReady", pid) - ideInterfaceChannel := ideDaemonProxy2.dtxConnection.ForChannelRequest(ProxyDispatcher{id: "emty"}) + proxyDispatcher := ProxyDispatcher{id: "emty", closeChannel: make(chan interface{}), closedChannel: make(chan interface{})} + ideInterfaceChannel := ideDaemonProxy2.dtxConnection.ForChannelRequest(proxyDispatcher) time.Sleep(time.Second) @@ -72,32 +73,32 @@ func RunXUITestWithBundleIdsXcode12Ctx(ctx context.Context, bundleID string, tes err = ideDaemonProxy2.daemonConnection.startExecutingTestPlanWithProtocolVersion(ideInterfaceChannel, 36) if err != nil { log.Error(err) - return err + return nil, err } if ctx != nil { select { case <-ctx.Done(): - log.Infof("Killing WebDriverAgent with pid %d ...", pid) + log.Infof("Killing test runner with pid %d ...", pid) err = pControl.KillProcess(pid) if err != nil { - return err + return nil, err } - log.Info("WDA killed with success") + log.Info("Test runner killed with success") } - return nil + return &TestRunner{proxyDispatcher: proxyDispatcher}, nil } - <-closeChan + <-proxyDispatcher.closeChannel log.Debugf("Killing UITest with pid %d ...", pid) err = pControl.KillProcess(pid) if err != nil { - return err + return nil, err } - log.Debugf("WDA killed with success") + log.Debugf("Test runner killed with success") var signal interface{} - closedChan <- signal - return nil + proxyDispatcher.closedChannel <- signal + return &TestRunner{proxyDispatcher: proxyDispatcher}, nil } func startTestRunner12(pControl *instruments.ProcessControl, xctestConfigPath string, bundleID string, diff --git a/main.go b/main.go index 0901b3b9..cd26ea1c 100644 --- a/main.go +++ b/main.go @@ -793,7 +793,9 @@ The commands work as following: applaunch, err := conn.LaunchApp( bundleID, []interface{}{}, - map[string]interface{}{}, + map[string]interface{}{ + "TERM": "xterm-256color", + }, map[string]interface{}{}, false, ) @@ -1086,18 +1088,22 @@ func runWdaCommand(device ios.DeviceEntry, arguments docopt.Opts) bool { return true } log.WithFields(log.Fields{"bundleid": bundleID, "testbundleid": testbundleID, "xctestconfig": xctestconfig}).Info("Running wda") + + testRunnerChannel := make(chan *testmanagerd.TestRunner) go func() { - err := testmanagerd.RunXCUIWithBundleIdsCtx(context.Background(), bundleID, testbundleID, xctestconfig, device, wdaargs, wdaenv) + testRunner, err := testmanagerd.RunXCUIWithBundleIdsCtx(context.Background(), bundleID, testbundleID, xctestconfig, device, wdaargs, wdaenv) if err != nil { log.WithFields(log.Fields{"error": err}).Fatal("Failed running WDA") } + testRunnerChannel <- testRunner }() c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) signal := <-c log.Infof("os signal:%d received, closing..", signal) - err := testmanagerd.CloseXCUITestRunner() + testRunner := <-testRunnerChannel + err := testRunner.Close() if err != nil { log.Error("Failed closing wda-testrunner") os.Exit(1)