FIX: Android can now start/stop while ios tests are running #2404
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Proposed changes
The problem is when running tests locally, if ios tests are already running and you try to start/stop/restart android tests, it fails. Here's the ai generated details of the problem and solution:
Debugging Maestro iOS/Android Simultaneous Run Issue
Problem Statement
When running Maestro tests, if an iOS test is started first, attempting to subsequently start an Android test fails immediately with a
Connection refused
error (initially onlocalhost/[::1]:7560
, later on127.0.0.1:<port>
). Starting the Android test first, followed by the iOS test, works without issues. The goal is to allow both platforms to be started and stopped independently without causing failures.Debugging Steps & Findings
[ {
"command" : {
"applyConfigurationCommand" : {
"config" : {
"appId" : "com.rumbly"
},
"optional" : false
}
},
"metadata" : {
"status" : "COMPLETED",
"timestamp" : 1743355225699,
"duration" : 1
}
}, {
"command" : {
"stopAppCommand" : {
"appId" : "com.rumbly",
"optional" : false
}
},
"metadata" : {
"status" : "COMPLETED",
"timestamp" : 1743355225700,
"duration" : 28
}
}, {
"command" : {
"defineVariablesCommand" : {
"env" : {
"MAESTRO_FILENAME" : "killAndRelaunch"
},
"optional" : false
}
},
"metadata" : {
"status" : "COMPLETED",
"timestamp" : 1743355225695,
"duration" : 3
}
}, {
"command" : {
"launchAppCommand" : {
"appId" : "com.rumbly",
"clearState" : false,
"stopApp" : true,
"launchArguments" : {
"isMaestroTest" : "true",
"IS_MAESTRO_TEST" : "true",
"maestroTestRunning" : "true",
"testFile" : "killAndRelaunch"
},
"optional" : false
}
},
"metadata" : {
"status" : "FAILED",
"timestamp" : 1743355225729,
"duration" : 6090,
"error" : {
"message" : "Unable to launch app com.rumbly: null",
"stackTrace" : [ {
"classLoaderName" : "app",
"methodName" : "launchAppCommand",
"fileName" : "Orchestra.kt",
"lineNumber" : 890,
"nativeMethod" : false,
"className" : "maestro.orchestra.Orchestra"
}, {
"classLoaderName" : "app",
"methodName" : "executeCommand",
"fileName" : "Orchestra.kt",
"lineNumber" : 299,
"nativeMethod" : false,
"className" : "maestro.orchestra.Orchestra"
}, {
"classLoaderName" : "app",
"methodName" : "executeCommands",
"fileName" : "Orchestra.kt",
"lineNumber" : 208,
"nativeMethod" : false,
"className" : "maestro.orchestra.Orchestra"
}, {
"classLoaderName" : "app",
"methodName" : "runFlow",
"fileName" : "Orchestra.kt",
"lineNumber" : 139,
"nativeMethod" : false,
"className" : "maestro.orchestra.Orchestra"
}, {
"classLoaderName" : "app",
"methodName" : "runCommands",
"fileName" : "MaestroCommandRunner.kt",
"lineNumber" : 187,
"nativeMethod" : false,
"className" : "maestro.cli.runner.MaestroCommandRunner"
}, {
"classLoaderName" : "app",
"methodName" : "invoke",
"fileName" : "TestRunner.kt",
"lineNumber" : 70,
"nativeMethod" : false,
"className" : "maestro.cli.runner.TestRunner$runSingle$result$1"
}, {
"classLoaderName" : "app",
"methodName" : "invoke",
"fileName" : "TestRunner.kt",
"lineNumber" : 61,
"nativeMethod" : false,
"className" : "maestro.cli.runner.TestRunner$runSingle$result$1"
}, {
"classLoaderName" : "app",
"methodName" : "runCatching",
"fileName" : "TestRunner.kt",
"lineNumber" : 175,
"nativeMethod" : false,
"className" : "maestro.cli.runner.TestRunner"
}, {
"classLoaderName" : "app",
"methodName" : "runSingle",
"fileName" : "TestRunner.kt",
"lineNumber" : 61,
"nativeMethod" : false,
"className" : "maestro.cli.runner.TestRunner"
}, {
"classLoaderName" : "app",
"methodName" : "runSingleFlow",
"fileName" : "TestCommand.kt",
"lineNumber" : 416,
"nativeMethod" : false,
"className" : "maestro.cli.command.TestCommand"
}, {
"classLoaderName" : "app",
"methodName" : "access$runSingleFlow",
"fileName" : "TestCommand.kt",
"lineNumber" : 63,
"nativeMethod" : false,
"className" : "maestro.cli.command.TestCommand"
}, {
"classLoaderName" : "app",
"methodName" : "invoke",
"fileName" : "TestCommand.kt",
"lineNumber" : 391,
"nativeMethod" : false,
"className" : "maestro.cli.command.TestCommand$runShardSuite$2"
}, {
"classLoaderName" : "app",
"methodName" : "invoke",
"fileName" : "TestCommand.kt",
"lineNumber" : 361,
"nativeMethod" : false,
"className" : "maestro.cli.command.TestCommand$runShardSuite$2"
}, {
"classLoaderName" : "app",
"methodName" : "newSession",
"fileName" : "MaestroSessionManager.kt",
"lineNumber" : 110,
"nativeMethod" : false,
"className" : "maestro.cli.session.MaestroSessionManager"
}, {
"classLoaderName" : "app",
"methodName" : "newSession$default",
"fileName" : "MaestroSessionManager.kt",
"lineNumber" : 54,
"nativeMethod" : false,
"className" : "maestro.cli.session.MaestroSessionManager"
}, {
"classLoaderName" : "app",
"methodName" : "runShardSuite",
"fileName" : "TestCommand.kt",
"lineNumber" : 361,
"nativeMethod" : false,
"className" : "maestro.cli.command.TestCommand"
}, {
"classLoaderName" : "app",
"methodName" : "access$runShardSuite",
"fileName" : "TestCommand.kt",
"lineNumber" : 63,
"nativeMethod" : false,
"className" : "maestro.cli.command.TestCommand"
}, {
"classLoaderName" : "app",
"methodName" : "invokeSuspend",
"fileName" : "TestCommand.kt",
"lineNumber" : 321,
"nativeMethod" : false,
"className" : "maestro.cli.command.TestCommand$handleSessions$1$results$1$1"
}, {
"classLoaderName" : "app",
"methodName" : "resumeWith",
"fileName" : "ContinuationImpl.kt",
"lineNumber" : 33,
"nativeMethod" : false,
"className" : "kotlin.coroutines.jvm.internal.BaseContinuationImpl"
}, {
"classLoaderName" : "app",
"methodName" : "run",
"fileName" : "DispatchedTask.kt",
"lineNumber" : 104,
"nativeMethod" : false,
"className" : "kotlinx.coroutines.DispatchedTask"
}, {
"classLoaderName" : "app",
"methodName" : "run",
"fileName" : "LimitedDispatcher.kt",
"lineNumber" : 111,
"nativeMethod" : false,
"className" : "kotlinx.coroutines.internal.LimitedDispatcher$Worker"
}, {
"classLoaderName" : "app",
"methodName" : "run",
"fileName" : "Tasks.kt",
"lineNumber" : 99,
"nativeMethod" : false,
"className" : "kotlinx.coroutines.scheduling.TaskImpl"
}, {
"classLoaderName" : "app",
"methodName" : "runSafely",
"fileName" : "CoroutineScheduler.kt",
"lineNumber" : 585,
"nativeMethod" : false,
"className" : "kotlinx.coroutines.scheduling.CoroutineScheduler"
}, {
"classLoaderName" : "app",
"methodName" : "executeTask",
"fileName" : "CoroutineScheduler.kt",
"lineNumber" : 802,
"nativeMethod" : false,
"className" : "kotlinx.coroutines.scheduling.CoroutineScheduler$Worker"
}, {
"classLoaderName" : "app",
"methodName" : "runWorker",
"fileName" : "CoroutineScheduler.kt",
"lineNumber" : 706,
"nativeMethod" : false,
"className" : "kotlinx.coroutines.scheduling.CoroutineScheduler$Worker"
}, {
"classLoaderName" : "app",
"methodName" : "run",
"fileName" : "CoroutineScheduler.kt",
"lineNumber" : 693,
"nativeMethod" : false,
"className" : "kotlinx.coroutines.scheduling.CoroutineScheduler$Worker"
} ],
"localizedMessage" : "Unable to launch app com.rumbly: null"
}
}
} ]
Initial Hypothesis: Port Conflict (IPv6)
Connection refused: localhost/[0:0:0:0:0:0:0:1]:7560
suggested a potential conflict on the default port or an issue with IPv6.7560
. (Not found).TestCommand.kt
andMaestroSessionManager.kt
.selectPort
fromTestCommand.kt
and modifiedrunShardSuite
to passdriverHostPort = null
toMaestroSessionManager.newSession
, ensuring the manager's distinct port ranges (Android: 7100-7300, iOS: 8001-8200) and tracking were used.7001
(Connection refused: localhost/[::1]:7001
). This indicated the port unification alone wasn't the fix and suggested a potential default fallback.Hypothesis 2: Port Fallback / IPv4 Forcing
7001
, the default fallback inAndroidDriver
. TheMaestroSessionManager.createAndroid
function (used for specific device IDs) wasn't selecting a port likepickAndroidDevice
did; it passed the potentiallynull
driverHostPort
directly, causing the fallback. The connection was also still attempted over IPv6 (localhost
).AndroidDriver.kt
'sManagedChannelBuilder
to explicitly use"127.0.0.1"
instead of"localhost"
to force IPv4.MaestroSessionManager.createAndroid
to callselectAvailableAndroidPort()
ifdriverHostPort
was null, ensuring a port from the correct Android range was always selected and passed toAndroidDriver
.Connection refused: /127.0.0.1:7107
). This confirmed port selection and IP were fixed, but the underlying connection problem remained.Hypothesis 3:
adb forward
Failure / Interferenceadb forward
or starting the on-device server, potentially due to interference from the active iOS driver tooling (idb
/simctl
).dadb.tcpForward
(allocateForwarder
) andam instrument
(startInstrumentationSession
) calls inAndroidDriver.kt
.MaestroSessionManager
, but none of the new logs insideAndroidDriver.allocateForwarder
orAndroidDriver.startInstrumentationSession
appeared before theConnection refused
error. This indicated theAndroidDriver.open()
method was failing before these critical setup steps could log anything.driver.open()
call itself within theMaestro.android
factory method inMaestro.kt
.adb forward --list
during the Android test startup.adb forward
.dadb.tcpForward
call inAndroidDriver.allocateForwarder
.allocateForwarder
(e.g., "Allocating ADB forward for hostPort... (Attempt 1/2)") before theConnection refused: /127.0.0.1:7226
error occurs.Current Conclusion
The retry mechanism did not help because the failure occurs before or during the very first attempt to execute the
dadb.tcpForward
command inside theAndroidDriver.allocateForwarder
method. Even the initial logging statement right before thetry
block in the retry loop is not reached.This strongly indicates that the presence of an active iOS Maestro test run (and its associated tooling like
idb
orxcrun
) causes an immediate, potentially silent failure within theadb
/dadb
system when Maestro tries to set up port forwarding for the Android device. The interference prevents the Android driver's initialization sequence from even beginning its core setup steps.Potential Next Steps / Causes
dadb
library: The interaction betweendadb
and a potentially busy/interfered-withadb
server needs closer examination. Is there a waydadb.tcpForward
can fail without throwing a catchable exception immediately under specificadb
server states?adb
command: As a diagnostic step (not a final fix), try replacing thedadb.tcpForward
call temporarily with a direct execution of theadb forward tcp:<port> tcp:<port>
command viaRuntime.exec()
or similar, to see if it yields a different error or behavior.adb
. Is itidb
,xcrun simctl
, or something else?adb
operations complete fully before iOS-related tooling becomes highly active, though this might impact perceived startup time.Resolution: Incorrect Initialization Order
Connection refused
error occurred before theAndroidDriver.open()
method (containing theallocateForwarder
logic foradb forward
) was explicitly called in the failing scenario (iOS started first).Maestro.getCachedDeviceInfo
, which in turn callsAndroidDriver.deviceInfo()
. ThisdeviceInfo()
method attempted a gRPC call to the Android driver without first ensuring the driver connection and port forwarding were established viaAndroidDriver.open()
. When an iOS test was already running, the interference likely prevented any implicit or preliminary connection/forwarding from succeeding, causing the immediateConnection refused
whendeviceInfo()
tried to communicate.AndroidDriver.deviceInfo()
method was modified to explicitly check if the driver'sopen
flag was true. If not, it now callsAndroidDriver.open()
first, ensuring the necessary port forwarding (allocateForwarder
) and instrumentation startup (startInstrumentationSession
) complete before thedeviceInfo
gRPC call is attempted. This guarantees the connection is ready, regardless of whetherdeviceInfo()
is called before methods likelaunchApp
(which also callopen()
).This change allows Android and iOS tests to be started and stopped in any order without causing connection failures.
Testing
I can start/stop/restart android tests now no matter if ios tests are already running or not, on the same machine (local running tests). SUPER useful when writing new tests.
Issues fixed