Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Put XCTest parameters into a config struct #507

Merged
merged 4 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ios/nskeyedarchiver/archiver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"reflect"
"testing"

"github.com/Masterminds/semver"
"github.com/danielpaulus/go-ios/ios/nskeyedarchiver"
archiver "github.com/danielpaulus/go-ios/ios/nskeyedarchiver"
"github.com/google/uuid"
Expand Down Expand Up @@ -36,7 +37,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, false)
config := nskeyedarchiver.NewXCTestConfiguration("productmodulename", uuid, "targetAppBundle", "targetAppPath", "testBundleUrl", nil, nil, false, semver.MustParse("17.0.0"))
result, err := nskeyedarchiver.ArchiveXML(config)
if err != nil {
log.Error(err)
Expand Down
8 changes: 7 additions & 1 deletion ios/nskeyedarchiver/objectivec_classes.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"regexp"
"time"

"github.com/Masterminds/semver"
"github.com/google/uuid"
"howett.net/plist"
)
Expand Down Expand Up @@ -84,6 +85,7 @@ func NewXCTestConfiguration(
testsToRun []string,
testsToSkip []string,
isXCTest bool,
version *semver.Version,
) XCTestConfiguration {
contents := map[string]interface{}{}

Expand Down Expand Up @@ -119,7 +121,11 @@ func NewXCTestConfiguration(
}

contents["aggregateStatisticsBeforeCrash"] = map[string]interface{}{"XCSuiteRecordsKey": map[string]interface{}{}}
contents["automationFrameworkPath"] = "/Developer/Library/PrivateFrameworks/XCTAutomationSupport.framework"
if version.Major() >= 17 {
contents["automationFrameworkPath"] = "/System/Developer/Library/PrivateFrameworks/XCTAutomationSupport.framework"
} else {
contents["automationFrameworkPath"] = "/Developer/Library/PrivateFrameworks/XCTAutomationSupport.framework"
}
contents["baselineFileRelativePath"] = plist.UID(0)
contents["baselineFileURL"] = plist.UID(0)
contents["defaultTestExecutionTimeAllowance"] = plist.UID(0)
Expand Down
138 changes: 59 additions & 79 deletions ios/testmanagerd/xcuitestrunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"path"
"strings"

"github.com/Masterminds/semver"
"github.com/danielpaulus/go-ios/ios/appservice"

"github.com/danielpaulus/go-ios/ios/house_arrest"
Expand Down Expand Up @@ -220,94 +221,72 @@ const (

const testBundleSuffix = "UITests.xctrunner"

func RunXCUITest(bundleID string, testRunnerBundleID string, xctestConfigName string, device ios.DeviceEntry, env map[string]interface{}, 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)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("RunXCUITest: cannot connect to installation proxy: %w", err)
}
defer installationProxy.Close()

if testRunnerBundleID == "" {
testRunnerBundleID = bundleID + testBundleSuffix
}

apps, err := installationProxy.BrowseUserApps()
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("RunXCUITest: cannot browse user apps: %w", err)
}

if bundleID != "" && xctestConfigName == "" {
info, err := getappInfo(bundleID, apps)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("RunXCUITest: cannot get app information: %w", err)
}

xctestConfigName = info.bundleName + "UITests.xctest"
}

return RunXCUIWithBundleIdsCtx(context.TODO(), bundleID, testRunnerBundleID, xctestConfigName, device, nil, env, testsToRun, testsToSkip, testListener, isXCTest)
// TestConfig specifies the parameters of a test execution
type TestConfig struct {
// The identifier of the app under test
BundleId string
// The identifier of the test runner. For unit tests (non-UI tests) this is also the
// app under test (BundleId can be left empty) as the .xctest bundle is packaged into the app under test
TestRunnerBundleId string
// XctestConfigName is the name of the
XctestConfigName string
// Env is passed as environment variables to the test runner
Env map[string]any
// Args are passed to the test runner as launch arguments
Args []string
// TestsToRun specifies a list of tests that should be executed. All other tests are ignored. To execute all tests
// pass nil.
// The format of the values is {PRODUCT_MODULE_NAME}.{CLASS}/{METHOD} where {PRODUCT_MODULE_NAME} and {METHOD} are
// optional. If {METHOD} is omitted, all tests of {CLASS} are executed
TestsToRun []string
// TestsToSkip specifies a list of tests that should be skipped. See TestsToRun for the format
TestsToSkip []string
// XcTest needs to be set to true if the TestRunnerBundleId is a unit test and not a UI test
XcTest bool
// The device on which the test is executed
Device ios.DeviceEntry
// The listener for receiving results
Listener *TestListener
}

func RunXCUIWithBundleIdsCtx(
ctx context.Context,
bundleID string,
testRunnerBundleID string,
xctestConfigFileName string,
device ios.DeviceEntry,
args []string,
env map[string]interface{},
testsToRun []string,
testsToSkip []string,
testListener *TestListener,
isXCTest bool,
) ([]TestSuite, error) {
version, err := ios.GetProductVersion(device)
func RunTestWithConfig(ctx context.Context, testConfig TestConfig) ([]TestSuite, error) {
version, err := ios.GetProductVersion(testConfig.Device)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("RunXCUIWithBundleIdsCtx: cannot determine iOS version: %w", err)
}

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, isXCTest)
return runXCUIWithBundleIdsXcode11Ctx(ctx, testConfig, version)
}

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, isXCTest)
return runXUITestWithBundleIdsXcode12Ctx(ctx, testConfig, version)
}

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

func runXUITestWithBundleIdsXcode15Ctx(
ctx context.Context,
bundleID string,
testRunnerBundleID string,
xctestConfigFileName string,
device ios.DeviceEntry,
args []string,
env map[string]interface{},
testsToRun []string,
testsToSkip []string,
testListener *TestListener,
isXCTest bool,
config TestConfig,
version *semver.Version,
) ([]TestSuite, error) {
conn1, err := dtx.NewTunnelConnection(device, testmanagerdiOS17)
conn1, err := dtx.NewTunnelConnection(config.Device, testmanagerdiOS17)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot create a tunnel connection to testmanagerd: %w", err)
}
defer conn1.Close()

conn2, err := dtx.NewTunnelConnection(device, testmanagerdiOS17)
conn2, err := dtx.NewTunnelConnection(config.Device, testmanagerdiOS17)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot create a tunnel connection to testmanagerd: %w", err)
}
defer conn2.Close()

installationProxy, err := installationproxy.New(device)
installationProxy, err := installationproxy.New(config.Device)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot connect to installation proxy: %w", err)
}
Expand All @@ -317,7 +296,7 @@ func runXUITestWithBundleIdsXcode15Ctx(
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot browse user apps: %w", err)
}

testAppInfo, err := getappInfo(testRunnerBundleID, apps)
testAppInfo, err := getappInfo(config.TestRunnerBundleId, apps)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot get test app information: %w", err)
}
Expand All @@ -326,8 +305,8 @@ func runXUITestWithBundleIdsXcode15Ctx(
testApp: testAppInfo,
}

if bundleID != "" {
appInfo, err := getappInfo(bundleID, apps)
if config.BundleId != "" {
appInfo, err := getappInfo(config.BundleId, apps)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot get app information: %w", err)
}
Expand All @@ -336,8 +315,8 @@ func runXUITestWithBundleIdsXcode15Ctx(
}

testSessionID := uuid.New()
testconfig := createTestConfig(info, testSessionID, xctestConfigFileName, testsToRun, testsToSkip, isXCTest)
ideDaemonProxy1 := newDtxProxyWithConfig(conn1, testconfig, testListener)
testconfig := createTestConfig(info, testSessionID, config.XctestConfigName, config.TestsToRun, config.TestsToSkip, config.XcTest, version)
ideDaemonProxy1 := newDtxProxyWithConfig(conn1, testconfig, config.Listener)

localCaps := nskeyedarchiver.XCTCapabilities{CapabilitiesDictionary: map[string]interface{}{
"XCTIssue capability": uint64(1),
Expand All @@ -357,26 +336,26 @@ func runXUITestWithBundleIdsXcode15Ctx(
}
log.WithField("receivedCaps", receivedCaps).Info("got capabilities")

appserviceConn, err := appservice.New(device)
appserviceConn, err := appservice.New(config.Device)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot connect to app service: %w", err)
}
defer appserviceConn.Close()

testRunnerLaunch, err := startTestRunner17(device, appserviceConn, "", testRunnerBundleID, strings.ToUpper(testSessionID.String()), info.testApp.path+"/PlugIns/"+xctestConfigFileName, args, env, isXCTest)
testRunnerLaunch, err := startTestRunner17(appserviceConn, config.TestRunnerBundleId, strings.ToUpper(testSessionID.String()), info.testApp.path+"/PlugIns/"+config.XctestConfigName, config.Args, config.Env, config.XcTest)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot start test runner: %w", err)
}

defer testRunnerLaunch.Close()
go func() {
_, err := io.Copy(testListener.logWriter, testRunnerLaunch)
_, err := io.Copy(config.Listener.logWriter, testRunnerLaunch)
if err != nil {
log.Warn("copying stdout failed", log.WithError(err))
}
}()

ideDaemonProxy2 := newDtxProxyWithConfig(conn2, testconfig, testListener)
ideDaemonProxy2 := newDtxProxyWithConfig(conn2, testconfig, config.Listener)
caps, err := ideDaemonProxy2.daemonConnection.initiateControlSessionWithCapabilities(nskeyedarchiver.XCTCapabilities{CapabilitiesDictionary: map[string]interface{}{}})
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot initiate a control session with capabilities: %w", err)
Expand All @@ -402,16 +381,16 @@ func runXUITestWithBundleIdsXcode15Ctx(
if !errors.Is(conn1.Err(), dtx.ErrConnectionClosed) {
log.WithError(conn1.Err()).Error("conn1 closed unexpectedly")
}
testListener.FinishWithError(errors.New("lost connection to testmanagerd. the test-runner may have been killed"))
config.Listener.FinishWithError(errors.New("lost connection to testmanagerd. the test-runner may have been killed"))
break
case <-conn2.Closed():
log.Debug("conn2 closed")
if !errors.Is(conn2.Err(), dtx.ErrConnectionClosed) {
log.WithError(conn2.Err()).Error("conn2 closed unexpectedly")
}
testListener.FinishWithError(errors.New("lost connection to testmanagerd. the test-runner may have been killed"))
config.Listener.FinishWithError(errors.New("lost connection to testmanagerd. the test-runner may have been killed"))
break
case <-testListener.Done():
case <-config.Listener.Done():
break
case <-ctx.Done():
break
Expand All @@ -426,7 +405,7 @@ func runXUITestWithBundleIdsXcode15Ctx(

log.Debugf("Done running test")

return testListener.TestSuites, testListener.err
return config.Listener.TestSuites, config.Listener.err
}

type processKiller interface {
Expand All @@ -444,7 +423,7 @@ 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 map[string]interface{}, isXCTest bool) (appservice.LaunchedAppWithStdIo, error) {
func startTestRunner17(appserviceConn *appservice.Connection, bundleID string, sessionIdentifier string, testBundlePath string, testArgs []string, testEnv map[string]interface{}, isXCTest bool) (appservice.LaunchedAppWithStdIo, error) {
args := []interface{}{}
for _, arg := range testArgs {
args = append(args, arg)
Expand Down Expand Up @@ -500,7 +479,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, isXCTest bool) (uuid.UUID, string, nskeyedarchiver.XCTestConfiguration, testInfo, error) {
func setupXcuiTest(device ios.DeviceEntry, bundleID string, testRunnerBundleID string, xctestConfigFileName string, testsToRun []string, testsToSkip []string, isXCTest bool, version *semver.Version) (uuid.UUID, string, nskeyedarchiver.XCTestConfiguration, testInfo, error) {
testSessionID := uuid.New()
installationProxy, err := installationproxy.New(device)
if err != nil {
Expand Down Expand Up @@ -538,21 +517,22 @@ 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, isXCTest)
testConfigPath, testConfig, err := createTestConfigOnDevice(testSessionID, info, houseArrestService, xctestConfigFileName, testsToRun, testsToSkip, isXCTest, version)
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, isXCTest bool) (string, nskeyedarchiver.XCTestConfiguration, error) {
func createTestConfigOnDevice(testSessionID uuid.UUID, info testInfo, houseArrestService *house_arrest.Connection, xctestConfigFileName string, testsToRun []string, testsToSkip []string, isXCTest bool, version *semver.Version) (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, isXCTest)
testConfig := nskeyedarchiver.NewXCTestConfiguration(info.targetApp.bundleName, testSessionID, info.targetApp.bundleID, info.targetApp.path, testBundleURL, testsToRun, testsToSkip, isXCTest, version)
config := testConfig
result, err := nskeyedarchiver.ArchiveXML(config)
if err != nil {
return "", nskeyedarchiver.XCTestConfiguration{}, err
Expand All @@ -562,13 +542,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, isXCTest), nil
return xctestConfigPath, testConfig, nil
}

func createTestConfig(info testInfo, testSessionID uuid.UUID, xctestConfigFileName string, testsToRun []string, testsToSkip []string, isXCTest bool) nskeyedarchiver.XCTestConfiguration {
func createTestConfig(info testInfo, testSessionID uuid.UUID, xctestConfigFileName string, testsToRun []string, testsToSkip []string, isXCTest bool, version *semver.Version) 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, isXCTest)
return nskeyedarchiver.NewXCTestConfiguration(productModuleName, testSessionID, info.targetApp.bundleID, info.targetApp.path, "PlugIns/"+xctestConfigFileName, testsToRun, testsToSkip, isXCTest, version)
}

type testInfo struct {
Expand Down
Loading
Loading