Steps to reproduce
patrol_cli spawns idevicesyslog without a UDID for real iOS devices. On a host
with more than one device attached, every concurrent patrol test run reads the same
device's syslog — so the Flutter output, the step progress and the Test summary in a
run can all belong to a device that run is not testing.
--device is honoured everywhere else: xcodebuild gets the right
-destination id=… and the right -only-testing, and the correct test executes on
the correct device. Only the log stream is unscoped, so the failure is silent — a run
prints a plausible, complete, wrong log.
Version
Reproduced with patrol_cli 4.7.0. The same code is present in 4.4.0 through 4.7.0,
so this is long-standing rather than a regression — it only becomes observable once
two real devices are attached to one host and two runs overlap.
Root cause
packages/patrol_cli/lib/src/ios/ios_test_backend.dart:
final patrolLogCommand = device.real
? ['idevicesyslog'] // ← no --udid: attaches to the first device found
: [
'xcrun', 'simctl', 'spawn',
device.id, // ← the simulator branch does scope it
'log', 'stream', '--type', 'log', '--color', 'none',
];
final processLogs = await _processManager.start(patrolLogCommand, runInShell: true);
device.id is available at that point and is used two lines below for the simulator.
libimobiledevice falls back to whichever device it enumerates first, which is not
stable across reboots or reconnects.
Two of the three log paths in the same codebase already scope correctly, which is what
makes this look like an oversight:
| backend |
log command |
scoped to the device? |
| iOS, real device |
idevicesyslog |
no |
| iOS, simulator |
xcrun simctl spawn <device.id> log stream |
yes |
| Android |
_adb.logcat(device: device.id, …) |
yes |
Impact
The pass/fail verdict is unaffected — execute() takes it from xcodebuild's exit
code, and the log stream is killed immediately afterwards:
final exitCode = await process.exitCode; // ← the only thing that decides the result
patrolLogReader.stopTimer();
processLogs.kill();
...
_logger.info(patrolLogReader.summary); // ← the stream is only ever printed
Everything derived from the stream is wrong, and nothing marks it as wrong:
- the
⏳ / ✅ step progress lines
--show-flutter-logs output, including app-side exceptions and stack traces
- the final
Test summary, including which .dart target is named as failed
A run therefore reports another device's exception under its own target name. In CI
that is worse than a missing log, because the output looks complete and internally
consistent.
Reproduction
Two real iOS devices attached to one macOS host. Build once with two targets, then
start two runs concurrently, each pinned to a different device and a different suite,
with distinct server ports so nothing else is shared:
patrol build ios --target integration_test/test_a.dart \
--target integration_test/test_b.dart --release
# terminal 1
patrol test-without-building --device <UDID-1> \
--test-server-port 8081 --app-server-port 8082 \
--release --show-flutter-logs --only "test_a Suite A"
# terminal 2
patrol test-without-building --device <UDID-2> \
--test-server-port 8091 --app-server-port 8092 \
--release --show-flutter-logs --only "test_b Suite B"
Observed:
- The two runs' Flutter-log sections are byte-identical — same line count, empty
diff, identical millisecond timestamps, and the same CoreBluetooth peripheral
UUID. That identifier is assigned per host-device pair, so two different phones
cannot produce it.
- The run that requested Suite B, and whose
xcodebuild invocation correctly ran
Suite B's generated test method, printed Failed: Suite A (/test_a.dart) in its
summary.
- When the two runs differ in duration, the shorter run's stream is an exact
prefix of the longer run's — one stream, two readers, one stopping earlier.
The tab-indented xcodebuild output in each run is unaffected and shows each device
running the test it was asked for. That contrast is the quickest way to confirm the
diagnosis: the native half is right and the Dart-side log is another device's.
Suggested fix
Scope the real-device branch the way the simulator branch already is:
final patrolLogCommand = device.real
? ['idevicesyslog', '-u', device.id]
: [ /* unchanged */ ];
idevicesyslog has accepted -u, --udid UDID for as long as it has existed, so this
needs no version gate, and a single-device host behaves exactly as before.
Actual results
.
Logs
Logs
<!-- Replace this line with your logs. Do not remove the backticks! -->
Patrol version
patrol_cli: ^4.7.0
Patrol Doctor output
Patrol Doctor output
<!-- Replace this line with your logs. Do not remove the backticks! -->
Flutter Doctor output
Flutter Doctor output
<!-- Replace this line with your logs. Do not remove the backticks! -->
Steps to reproduce
patrol_clispawnsidevicesyslogwithout a UDID for real iOS devices. On a hostwith more than one device attached, every concurrent
patrol testrun reads the samedevice's syslog — so the Flutter output, the step progress and the
Test summaryin arun can all belong to a device that run is not testing.
--deviceis honoured everywhere else:xcodebuildgets the right-destination id=…and the right-only-testing, and the correct test executes onthe correct device. Only the log stream is unscoped, so the failure is silent — a run
prints a plausible, complete, wrong log.
Version
Reproduced with
patrol_cli4.7.0. The same code is present in 4.4.0 through 4.7.0,so this is long-standing rather than a regression — it only becomes observable once
two real devices are attached to one host and two runs overlap.
Root cause
packages/patrol_cli/lib/src/ios/ios_test_backend.dart:device.idis available at that point and is used two lines below for the simulator.libimobiledevicefalls back to whichever device it enumerates first, which is notstable across reboots or reconnects.
Two of the three log paths in the same codebase already scope correctly, which is what
makes this look like an oversight:
idevicesyslogxcrun simctl spawn <device.id> log stream_adb.logcat(device: device.id, …)Impact
The pass/fail verdict is unaffected —
execute()takes it fromxcodebuild's exitcode, and the log stream is killed immediately afterwards:
Everything derived from the stream is wrong, and nothing marks it as wrong:
⏳/✅step progress lines--show-flutter-logsoutput, including app-side exceptions and stack tracesTest summary, including which.darttarget is named as failedA run therefore reports another device's exception under its own target name. In CI
that is worse than a missing log, because the output looks complete and internally
consistent.
Reproduction
Two real iOS devices attached to one macOS host. Build once with two targets, then
start two runs concurrently, each pinned to a different device and a different suite,
with distinct server ports so nothing else is shared:
patrol build ios --target integration_test/test_a.dart \ --target integration_test/test_b.dart --release # terminal 1 patrol test-without-building --device <UDID-1> \ --test-server-port 8081 --app-server-port 8082 \ --release --show-flutter-logs --only "test_a Suite A" # terminal 2 patrol test-without-building --device <UDID-2> \ --test-server-port 8091 --app-server-port 8092 \ --release --show-flutter-logs --only "test_b Suite B"Observed:
diff, identical millisecond timestamps, and the same CoreBluetooth peripheralUUID. That identifier is assigned per host-device pair, so two different phones
cannot produce it.
xcodebuildinvocation correctly ranSuite B's generated test method, printed
Failed: Suite A (/test_a.dart)in itssummary.
prefix of the longer run's — one stream, two readers, one stopping earlier.
The tab-indented
xcodebuildoutput in each run is unaffected and shows each devicerunning the test it was asked for. That contrast is the quickest way to confirm the
diagnosis: the native half is right and the Dart-side log is another device's.
Suggested fix
Scope the real-device branch the way the simulator branch already is:
idevicesysloghas accepted-u, --udid UDIDfor as long as it has existed, so thisneeds no version gate, and a single-device host behaves exactly as before.
Actual results
.
Logs
Logs
Patrol version
patrol_cli: ^4.7.0
Patrol Doctor output
Patrol Doctor output
Flutter Doctor output
Flutter Doctor output