Skip to content

Commit

Permalink
Add Support for XCTest (#464)
Browse files Browse the repository at this point in the history
Provide the ability to run an XCTEST by passing the flag --xctest to ios runtest
Test the ability to run a XCUITEST on a Real Device
Test the ability to run a XCTEST on a Real Device
by the default, if the flag --xctest is not provided we will run XCUITEST. so to be able to run an XCTEST we need the flag to be present.
Thanks bahrimootaz!

---------

Co-authored-by: David Missmann <[email protected]>
  • Loading branch information
bahrimootaz and dmissmann authored Aug 20, 2024
1 parent bc47af1 commit 89cbe52
Show file tree
Hide file tree
Showing 6 changed files with 37 additions and 27 deletions.
2 changes: 1 addition & 1 deletion ios/nskeyedarchiver/archiver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestArchiveSlice(t *testing.T) {
// TODO currently only partially decoding XCTestConfig is supported, fix later
func TestXCTestconfig(t *testing.T) {
uuid := uuid.New()
config := nskeyedarchiver.NewXCTestConfiguration("productmodulename", uuid, "targetAppBundle", "targetAppPath", "testBundleUrl", nil, nil)
config := nskeyedarchiver.NewXCTestConfiguration("productmodulename", uuid, "targetAppBundle", "targetAppPath", "testBundleUrl", nil, nil, false)
result, err := nskeyedarchiver.ArchiveXML(config)
if err != nil {
log.Error(err)
Expand Down
3 changes: 2 additions & 1 deletion ios/nskeyedarchiver/objectivec_classes.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func NewXCTestConfiguration(
testBundleURL string,
testsToRun []string,
testsToSkip []string,
isXCTest bool,
) XCTestConfiguration {
contents := map[string]interface{}{}

Expand Down Expand Up @@ -126,7 +127,7 @@ func NewXCTestConfiguration(
contents["emitOSLogs"] = false
// contents["formatVersion"] = 2
contents["gatherLocalizableStringsData"] = false
contents["initializeForUITesting"] = true
contents["initializeForUITesting"] = !isXCTest
contents["maximumTestExecutionTimeAllowance"] = plist.UID(0)
contents["randomExecutionOrderingSeed"] = plist.UID(0)
contents["reportActivities"] = true
Expand Down
39 changes: 23 additions & 16 deletions ios/testmanagerd/xcuitestrunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ const (

const testBundleSuffix = "UITests.xctrunner"

func RunXCUITest(bundleID string, testRunnerBundleID string, xctestConfigName string, device ios.DeviceEntry, env []string, testsToRun []string, testsToSkip []string, testListener *TestListener) ([]TestSuite, error) {
func RunXCUITest(bundleID string, testRunnerBundleID string, xctestConfigName string, device ios.DeviceEntry, env []string, testsToRun []string, testsToSkip []string, testListener *TestListener, isXCTest bool) ([]TestSuite, error) {
// FIXME: this is redundant code, getting the app list twice and creating the appinfos twice
// just to generate the xctestConfigFileName. Should be cleaned up at some point.
installationProxy, err := installationproxy.New(device)
Expand All @@ -246,7 +246,7 @@ func RunXCUITest(bundleID string, testRunnerBundleID string, xctestConfigName st
xctestConfigName = info.bundleName + "UITests.xctest"
}

return RunXCUIWithBundleIdsCtx(context.TODO(), bundleID, testRunnerBundleID, xctestConfigName, device, nil, env, testsToRun, testsToSkip, testListener)
return RunXCUIWithBundleIdsCtx(context.TODO(), bundleID, testRunnerBundleID, xctestConfigName, device, nil, env, testsToRun, testsToSkip, testListener, isXCTest)
}

func RunXCUIWithBundleIdsCtx(
Expand All @@ -260,6 +260,7 @@ func RunXCUIWithBundleIdsCtx(
testsToRun []string,
testsToSkip []string,
testListener *TestListener,
isXCTest bool,
) ([]TestSuite, error) {
version, err := ios.GetProductVersion(device)
if err != nil {
Expand All @@ -268,16 +269,16 @@ func RunXCUIWithBundleIdsCtx(

if version.LessThan(ios.IOS14()) {
log.Debugf("iOS version: %s detected, running with ios11 support", version)
return runXCUIWithBundleIdsXcode11Ctx(ctx, bundleID, testRunnerBundleID, xctestConfigFileName, device, args, env, testsToRun, testsToSkip, testListener)
return runXCUIWithBundleIdsXcode11Ctx(ctx, bundleID, testRunnerBundleID, xctestConfigFileName, device, args, env, testsToRun, testsToSkip, testListener, isXCTest)
}

if version.LessThan(ios.IOS17()) {
log.Debugf("iOS version: %s detected, running with ios14 support", version)
return runXUITestWithBundleIdsXcode12Ctx(ctx, bundleID, testRunnerBundleID, xctestConfigFileName, device, args, env, testsToRun, testsToSkip, testListener)
return runXUITestWithBundleIdsXcode12Ctx(ctx, bundleID, testRunnerBundleID, xctestConfigFileName, device, args, env, testsToRun, testsToSkip, testListener, isXCTest)
}

log.Debugf("iOS version: %s detected, running with ios17 support", version)
return runXUITestWithBundleIdsXcode15Ctx(ctx, bundleID, testRunnerBundleID, xctestConfigFileName, device, args, env, testsToRun, testsToSkip, testListener)
return runXUITestWithBundleIdsXcode15Ctx(ctx, bundleID, testRunnerBundleID, xctestConfigFileName, device, args, env, testsToRun, testsToSkip, testListener, isXCTest)
}

func runXUITestWithBundleIdsXcode15Ctx(
Expand All @@ -291,6 +292,7 @@ func runXUITestWithBundleIdsXcode15Ctx(
testsToRun []string,
testsToSkip []string,
testListener *TestListener,
isXCTest bool,
) ([]TestSuite, error) {
conn1, err := dtx.NewTunnelConnection(device, testmanagerdiOS17)
if err != nil {
Expand Down Expand Up @@ -333,7 +335,7 @@ func runXUITestWithBundleIdsXcode15Ctx(
}

testSessionID := uuid.New()
testconfig := createTestConfig(info, testSessionID, xctestConfigFileName, testsToRun, testsToSkip)
testconfig := createTestConfig(info, testSessionID, xctestConfigFileName, testsToRun, testsToSkip, isXCTest)
ideDaemonProxy1 := newDtxProxyWithConfig(conn1, testconfig, testListener)

localCaps := nskeyedarchiver.XCTCapabilities{CapabilitiesDictionary: map[string]interface{}{
Expand All @@ -360,7 +362,7 @@ func runXUITestWithBundleIdsXcode15Ctx(
}
defer appserviceConn.Close()

testRunnerLaunch, err := startTestRunner17(device, appserviceConn, "", testRunnerBundleID, strings.ToUpper(testSessionID.String()), info.testApp.path+"/PlugIns/"+xctestConfigFileName, args, env)
testRunnerLaunch, err := startTestRunner17(device, appserviceConn, "", testRunnerBundleID, strings.ToUpper(testSessionID.String()), info.testApp.path+"/PlugIns/"+xctestConfigFileName, args, env, isXCTest)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot start test runner: %w", err)
}
Expand Down Expand Up @@ -441,16 +443,21 @@ func killTestRunner(killer processKiller, pid int) error {
return nil
}

func startTestRunner17(device ios.DeviceEntry, appserviceConn *appservice.Connection, xctestConfigPath string, bundleID string, sessionIdentifier string, testBundlePath string, testArgs []string, testEnv []string) (appservice.LaunchedAppWithStdIo, error) {
func startTestRunner17(device ios.DeviceEntry, appserviceConn *appservice.Connection, xctestConfigPath string, bundleID string, sessionIdentifier string, testBundlePath string, testArgs []string, testEnv []string, isXCTest bool) (appservice.LaunchedAppWithStdIo, error) {
args := []interface{}{}
for _, arg := range testArgs {
args = append(args, arg)
}

libraries := "/Developer/usr/lib/libMainThreadChecker.dylib"
if isXCTest {
libraries += ":/System/Developer/usr/lib/libXCTestBundleInject.dylib"
}

env := map[string]interface{}{
"CA_ASSERT_MAIN_THREAD_TRANSACTIONS": "0",
"CA_DEBUG_TRANSACTIONS": "0",
"DYLD_INSERT_LIBRARIES": "/Developer/usr/lib/libMainThreadChecker.dylib",
"DYLD_INSERT_LIBRARIES": libraries,
"DYLD_FRAMEWORK_PATH": "/System/Developer/Library/Frameworks",
"DYLD_LIBRARY_PATH": "/System/Developer/usr/lib",

Expand Down Expand Up @@ -492,7 +499,7 @@ func startTestRunner17(device ios.DeviceEntry, appserviceConn *appservice.Connec
return appLaunch, nil
}

func setupXcuiTest(device ios.DeviceEntry, bundleID string, testRunnerBundleID string, xctestConfigFileName string, testsToRun []string, testsToSkip []string) (uuid.UUID, string, nskeyedarchiver.XCTestConfiguration, testInfo, error) {
func setupXcuiTest(device ios.DeviceEntry, bundleID string, testRunnerBundleID string, xctestConfigFileName string, testsToRun []string, testsToSkip []string, isXCTest bool) (uuid.UUID, string, nskeyedarchiver.XCTestConfiguration, testInfo, error) {
testSessionID := uuid.New()
installationProxy, err := installationproxy.New(device)
if err != nil {
Expand Down Expand Up @@ -530,21 +537,21 @@ func setupXcuiTest(device ios.DeviceEntry, bundleID string, testRunnerBundleID s
return uuid.UUID{}, "", nskeyedarchiver.XCTestConfiguration{}, testInfo{}, err
}
log.Debugf("creating test config")
testConfigPath, testConfig, err := createTestConfigOnDevice(testSessionID, info, houseArrestService, xctestConfigFileName, testsToRun, testsToSkip)
testConfigPath, testConfig, err := createTestConfigOnDevice(testSessionID, info, houseArrestService, xctestConfigFileName, testsToRun, testsToSkip, isXCTest)
if err != nil {
return uuid.UUID{}, "", nskeyedarchiver.XCTestConfiguration{}, testInfo{}, err
}

return testSessionID, testConfigPath, testConfig, info, nil
}

func createTestConfigOnDevice(testSessionID uuid.UUID, info testInfo, houseArrestService *house_arrest.Connection, xctestConfigFileName string, testsToRun []string, testsToSkip []string) (string, nskeyedarchiver.XCTestConfiguration, error) {
func createTestConfigOnDevice(testSessionID uuid.UUID, info testInfo, houseArrestService *house_arrest.Connection, xctestConfigFileName string, testsToRun []string, testsToSkip []string, isXCTest bool) (string, nskeyedarchiver.XCTestConfiguration, error) {
relativeXcTestConfigPath := path.Join("tmp", testSessionID.String()+".xctestconfiguration")
xctestConfigPath := path.Join(info.testApp.homePath, relativeXcTestConfigPath)

testBundleURL := path.Join(info.testApp.path, "PlugIns", xctestConfigFileName)

config := nskeyedarchiver.NewXCTestConfiguration(info.targetApp.bundleName, testSessionID, info.targetApp.bundleID, info.targetApp.path, testBundleURL, testsToRun, testsToSkip)
config := nskeyedarchiver.NewXCTestConfiguration(info.targetApp.bundleName, testSessionID, info.targetApp.bundleID, info.targetApp.path, testBundleURL, testsToRun, testsToSkip, isXCTest)
result, err := nskeyedarchiver.ArchiveXML(config)
if err != nil {
return "", nskeyedarchiver.XCTestConfiguration{}, err
Expand All @@ -554,13 +561,13 @@ func createTestConfigOnDevice(testSessionID uuid.UUID, info testInfo, houseArres
if err != nil {
return "", nskeyedarchiver.XCTestConfiguration{}, err
}
return xctestConfigPath, nskeyedarchiver.NewXCTestConfiguration(info.targetApp.bundleName, testSessionID, info.targetApp.bundleID, info.targetApp.path, testBundleURL, testsToRun, testsToSkip), nil
return xctestConfigPath, nskeyedarchiver.NewXCTestConfiguration(info.targetApp.bundleName, testSessionID, info.targetApp.bundleID, info.targetApp.path, testBundleURL, testsToRun, testsToSkip, isXCTest), nil
}

func createTestConfig(info testInfo, testSessionID uuid.UUID, xctestConfigFileName string, testsToRun []string, testsToSkip []string) nskeyedarchiver.XCTestConfiguration {
func createTestConfig(info testInfo, testSessionID uuid.UUID, xctestConfigFileName string, testsToRun []string, testsToSkip []string, isXCTest bool) nskeyedarchiver.XCTestConfiguration {
// the default value for this generated by Xcode is the target name, and the same name is used for the '.xctest' bundle name per default
productModuleName := strings.ReplaceAll(xctestConfigFileName, ".xctest", "")
return nskeyedarchiver.NewXCTestConfiguration(productModuleName, testSessionID, info.targetApp.bundleID, info.targetApp.path, "PlugIns/"+xctestConfigFileName, testsToRun, testsToSkip)
return nskeyedarchiver.NewXCTestConfiguration(productModuleName, testSessionID, info.targetApp.bundleID, info.targetApp.path, "PlugIns/"+xctestConfigFileName, testsToRun, testsToSkip, isXCTest)
}

type testInfo struct {
Expand Down
3 changes: 2 additions & 1 deletion ios/testmanagerd/xcuitestrunner_11.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ func runXCUIWithBundleIdsXcode11Ctx(
testsToRun []string,
testsToSkip []string,
testListener *TestListener,
isXCTest bool,
) ([]TestSuite, error) {
log.Debugf("set up xcuitest")
testSessionId, xctestConfigPath, testConfig, testInfo, err := setupXcuiTest(device, bundleID, testRunnerBundleID, xctestConfigFileName, testsToRun, testsToSkip)
testSessionId, xctestConfigPath, testConfig, testInfo, err := setupXcuiTest(device, bundleID, testRunnerBundleID, xctestConfigFileName, testsToRun, testsToSkip, isXCTest)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("RunXCUIWithBundleIdsXcode11Ctx: cannot create test config: %w", err)
}
Expand Down
4 changes: 2 additions & 2 deletions ios/testmanagerd/xcuitestrunner_12.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import (
)

func runXUITestWithBundleIdsXcode12Ctx(ctx context.Context, bundleID string, testRunnerBundleID string, xctestConfigFileName string,
device ios.DeviceEntry, args []string, env []string, testsToRun []string, testsToSkip []string, testListener *TestListener,
device ios.DeviceEntry, args []string, env []string, testsToRun []string, testsToSkip []string, testListener *TestListener, isXCTest bool,
) ([]TestSuite, error) {
conn, err := dtx.NewUsbmuxdConnection(device, testmanagerdiOS14)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("RunXUITestWithBundleIdsXcode12Ctx: cannot create a usbmuxd connection to testmanagerd: %w", err)
}

testSessionId, xctestConfigPath, testConfig, testInfo, err := setupXcuiTest(device, bundleID, testRunnerBundleID, xctestConfigFileName, testsToRun, testsToSkip)
testSessionId, xctestConfigPath, testConfig, testInfo, err := setupXcuiTest(device, bundleID, testRunnerBundleID, xctestConfigFileName, testsToRun, testsToSkip, isXCTest)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("RunXUITestWithBundleIdsXcode12Ctx: cannot setup test config: %w", err)
}
Expand Down
13 changes: 7 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ Usage:
ios info [display | lockdown] [options]
ios image list [options]
ios image mount [--path=<imagepath>] [options]
ios image unmount [options]
ios image auto [--basedir=<where_dev_images_are_stored>] [options]
ios syslog [options]
ios screenshot [options] [--output=<outfile>] [--stream] [--port=<port>]
Expand Down Expand Up @@ -111,7 +110,7 @@ Usage:
ios apps [--system] [--all] [--list] [--filesharing] [options]
ios launch <bundleID> [--wait] [--kill-existing] [options]
ios kill (<bundleID> | --pid=<processID> | --process=<processName>) [options]
ios runtest [--bundle-id=<bundleid>] [--test-runner-bundle-id=<testrunnerbundleid>] [--xctest-config=<xctestconfig>] [--log-output=<file>] [--test-to-run=<tests>]... [--test-to-skip=<tests>]... [--env=<e>]... [options]
ios runtest [--bundle-id=<bundleid>] [--test-runner-bundle-id=<testrunnerbundleid>] [--xctest-config=<xctestconfig>] [--log-output=<file>] [--xctest] [--test-to-run=<tests>]... [--test-to-skip=<tests>]... [--env=<e>]... [options]
ios runwda [--bundleid=<bundleid>] [--testrunnerbundleid=<testbundleid>] [--xctestconfig=<xctestconfig>] [--log-output=<file>] [--arg=<a>]... [--env=<e>]... [options]
ios ax [--font=<fontSize>] [options]
ios debug [options] [--stop-at-entry] <app_path>
Expand Down Expand Up @@ -222,7 +221,7 @@ The commands work as following:
ios apps [--system] [--all] [--list] [--filesharing] Retrieves a list of installed applications. --system prints out preinstalled system apps. --all prints all apps, including system, user, and hidden apps. --list only prints bundle ID, bundle name and version number. --filesharing only prints apps which enable documents sharing.
ios launch <bundleID> [--wait] [--kill-existing] [options] Launch app with the bundleID on the device. Get your bundle ID from the apps command. --wait keeps the connection open if you want logs.
ios kill (<bundleID> | --pid=<processID> | --process=<processName>) [options] Kill app with the specified bundleID, process id, or process name on the device.
ios runtest [--bundle-id=<bundleid>] [--test-runner-bundle-id=<testbundleid>] [--xctest-config=<xctestconfig>] [--log-output=<file>] [--test-to-run=<tests>]... [--test-to-skip=<tests>]... [--env=<e>]... [options] Run a XCUITest. If you provide only bundle-id go-ios will try to dynamically create test-runner-bundle-id and xctest-config.
ios runtest [--bundle-id=<bundleid>] [--test-runner-bundle-id=<testbundleid>] [--xctest-config=<xctestconfig>] [--log-output=<file>] [--xctest] [--test-to-run=<tests>]... [--test-to-skip=<tests>]... [--env=<e>]... [options] Run a XCUITest. If you provide only bundle-id go-ios will try to dynamically create test-runner-bundle-id and xctest-config.
> If you provide '-' as log output, it prints resuts to stdout.
> To be able to filter for tests to run or skip, use one argument per test selector. Example: runtest --test-to-run=(TestTarget.)TestClass/testMethod --test-to-run=(TestTarget.)TestClass/testMethod (the value for 'TestTarget' is optional)
> The method name can also be omitted and in this case all tests of the specified class are run
Expand Down Expand Up @@ -907,6 +906,8 @@ The commands work as following:
rawTestlog, rawTestlogErr := arguments.String("--log-output")
env := arguments["--env"].([]string)

isXCTest, _ := arguments.Bool("--xctest")

if rawTestlogErr == nil {
var writer *os.File = os.Stdout
if rawTestlog != "-" {
Expand All @@ -916,14 +917,14 @@ The commands work as following:
}
defer writer.Close()

testResults, err := testmanagerd.RunXCUITest(bundleID, testRunnerBundleId, xctestConfig, device, env, testsToRun, testsToSkip, testmanagerd.NewTestListener(writer, writer, os.TempDir()))
testResults, err := testmanagerd.RunXCUITest(bundleID, testRunnerBundleId, xctestConfig, device, env, testsToRun, testsToSkip, testmanagerd.NewTestListener(writer, writer, os.TempDir()), isXCTest)
if err != nil {
log.WithFields(log.Fields{"error": err}).Info("Failed running Xcuitest")
}

log.Info(fmt.Printf("%+v", testResults))
} else {
_, err := testmanagerd.RunXCUITest(bundleID, testRunnerBundleId, xctestConfig, device, env, testsToRun, testsToSkip, testmanagerd.NewTestListener(io.Discard, io.Discard, os.TempDir()))
_, err := testmanagerd.RunXCUITest(bundleID, testRunnerBundleId, xctestConfig, device, env, testsToRun, testsToSkip, testmanagerd.NewTestListener(io.Discard, io.Discard, os.TempDir()), isXCTest)
if err != nil {
log.WithFields(log.Fields{"error": err}).Info("Failed running Xcuitest")
}
Expand Down Expand Up @@ -1214,7 +1215,7 @@ func runWdaCommand(device ios.DeviceEntry, arguments docopt.Opts) bool {
defer close(errorChannel)
ctx, stopWda := context.WithCancel(context.Background())
go func() {
_, err := testmanagerd.RunXCUIWithBundleIdsCtx(ctx, bundleID, testbundleID, xctestconfig, device, wdaargs, wdaenv, nil, nil, testmanagerd.NewTestListener(writer, writer, os.TempDir()))
_, err := testmanagerd.RunXCUIWithBundleIdsCtx(ctx, bundleID, testbundleID, xctestconfig, device, wdaargs, wdaenv, nil, nil, testmanagerd.NewTestListener(writer, writer, os.TempDir()), false)
if err != nil {
errorChannel <- err
}
Expand Down

0 comments on commit 89cbe52

Please sign in to comment.