Skip to content

feat: enable parallel iOS simulator execution with per-device session tracking#3138

Open
qwertey6 wants to merge 1 commit intomobile-dev-inc:mainfrom
ReverentPeer:pr/1-parallel-ios-execution
Open

feat: enable parallel iOS simulator execution with per-device session tracking#3138
qwertey6 wants to merge 1 commit intomobile-dev-inc:mainfrom
ReverentPeer:pr/1-parallel-ios-execution

Conversation

@qwertey6
Copy link
Copy Markdown

@qwertey6 qwertey6 commented Apr 4, 2026

Proposed changes

Parallel execution on iOS is completely broken. Two maestro test processes targeting different iOS simulators collide on the XCTest HTTP server port and interfere with each other's sessions. This makes --shard-split unusable on iOS and forces sequential execution.

Impact

  • Unblocks iOS parallel testing: --shard-split and multi-process execution now work on iOS
  • Resolves 5 open issues and supersedes 2 stale open PRs
  • Removes the Thread.sleep(1000) hack from Fix Android Parallel/Shards Race Condition #1867 — the per-device session check eliminates the race condition it was working around

Root cause

Two bugs compound (same analysis as #2339 by @avinash-bharti):

  1. Port collision: iOS simulators share the host's localhost. Two Maestro processes both bind to the same XCTest server port.
  2. Session interference: hasActiveSessions() checks if any session exists on the same platform — regardless of device. Process B sees process A's session and skips driver.open(), connecting to the wrong device.

What this PR does differently from #2339 and #2821

Builds on the same architectural approach (per-device session tracking + --driver-host-port), with additional correctness fixes discovered during testing:

Fix #2339 #2821 This PR
Per-device session tracking Yes No Yes
--driver-host-port flag Yes Yes Yes
Session self-detection bug fix No No Yes — heartbeat at initialDelay=0 fires before session check, causing process to find itself and skip driver.open(). Broke Android completely.
Cross-process file locking No No Yes~/.maestro/sessions had no FileLock, concurrent processes could corrupt session data
Per-device shutdown No No YesshouldCloseSession(platform, deviceId) replaces global activeSessions().isEmpty()
Port availability check No No YesisPortAvailable() with correct check-then-claim ordering
Debug log directory race No No Yes — PID appended to prevent parallel collision
Unit tests No No Yes — 8 tests, fakes not mocks, Java 8 compatible

Changes

  • SessionStore.kt — Refactored from singleton to injectable class. Key format: "{platform}_{deviceId}_{sessionId}". New methods: hasActiveSessionForDevice(), shouldCloseSession(), activeSessionsForDevice()
  • MaestroSessionManager.kt — Uses SessionStore.default, per-device heartbeat/delete/close
  • KeyValueStore.kt — Added FileLock for cross-process safety
  • App.kt + TestCommand.kt--driver-host-port CLI option
  • TestCommand.ktselectPort() with isPortAvailable() check
  • SocketUtils.ktisPortAvailable() function
  • DebugLogStore.kt — PID in log directory name (Java 8 compatible via ManagementFactory)
  • Command files — Pass parent?.driverHostPort instead of null

Verification

Scenario Result
3 iOS sims parallel, cold start Pass
3 iOS sims parallel, warm start Pass
Android emulator, single device Pass
--driver-host-port explicit ports Pass
--shard-split=2 with 2 flows Pass
Solo + shard simultaneous (3 devices) Pass

Testing

  • 8 new unit tests in SessionStoreTest (per-device isolation, key format, lifecycle, shutdown)
  • useJUnitPlatform() added to maestro-cli/build.gradle.kts (was missing)
  • Uses fakes, not mocks (per CONTRIBUTING.md)
  • Java 8 compatible

Note: This is PR 1 of 4 in a stacked series. Subsequent PRs build on this:

  • PR 2: ~4x faster startup (skip unnecessary driver reinstall)
  • PR 3: Version safety (driver hash checks prevent stale drivers)
  • PR 4: Fix waitToSettleTimeoutMs on iOS (settle timeout was silently broken)

Issues fixed

Closes #1853, #2556, #1485, #2082, #2703

Supersedes #2339 and #2821 — credit to @avinash-bharti and @omnarayan for the root cause analysis and architectural direction.

iOS simulators share the host's localhost, causing port collisions when
multiple Maestro processes target different sims simultaneously. Session
tracking was per-platform, so two processes on different devices would
interfere with each other's sessions.

Changes:
- Per-device session tracking: SessionStore keys are now
  "{platform}_{deviceId}_{sessionId}" instead of "{platform}_{sessionId}"
- Add --driver-host-port CLI flag for explicit XCTest server port
- Auto-select available ports with isPortAvailable() check
- Refactor SessionStore from singleton to injectable class (DI)
- Add shouldCloseSession(platform, deviceId) for per-device shutdown
  instead of global activeSessions().isEmpty()
- Add cross-process file locking to KeyValueStore (~/.maestro/sessions)
- Append PID to debug log directory to prevent parallel race
- Enable useJUnitPlatform() in maestro-cli (was missing)
- Add SessionStoreTest with 8 tests covering isolation and lifecycle

Verified: 3 iOS simulators + Android emulator running simultaneously,
all passing. Both --driver-host-port (explicit) and auto-port-selection
work correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@qwertey6
Copy link
Copy Markdown
Author

qwertey6 commented Apr 4, 2026

CI note: The E2E failures on Android and iOS are pre-existing and unrelated to this PR:

  • Android (killApp test): 44/45 flows pass, 1 fails with "Form Test" is not visible after a killApp command. App didn't return to expected state — flaky assertion.
  • iOS (ios-advanced-flow): Wikipedia app crashes during clearState + assertVisible "Next" in launch-clearstate-ios.yaml.

Both failures reproduce on other unrelated PRs in the repo (env-file-support, pr/contains-child-fix, pr/skill, etc.) — every recent E2E run on this workflow shows the same pattern.

Build, unit tests, web E2E, and iOS XCTest runner tests all pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unable to run parallel/sharded tests on Android

1 participant