Unverified Commit f4d26a3b authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Change iOS device discovery from polling to long-running observation (#58137)

parent bbb95e57
...@@ -104,6 +104,12 @@ class ItemListNotifier<T> { ...@@ -104,6 +104,12 @@ class ItemListNotifier<T> {
removedItems.forEach(_removedController.add); removedItems.forEach(_removedController.add);
} }
void removeItem(T item) {
if (_items.remove(item)) {
_removedController.add(item);
}
}
/// Close the streams. /// Close the streams.
void dispose() { void dispose() {
_addedController.close(); _addedController.close();
......
...@@ -790,18 +790,20 @@ class DeviceDomain extends Domain { ...@@ -790,18 +790,20 @@ class DeviceDomain extends Domain {
/// Enable device events. /// Enable device events.
Future<void> enable(Map<String, dynamic> args) { Future<void> enable(Map<String, dynamic> args) {
final List<Future<void>> calls = <Future<void>>[];
for (final PollingDeviceDiscovery discoverer in _discoverers) { for (final PollingDeviceDiscovery discoverer in _discoverers) {
discoverer.startPolling(); calls.add(discoverer.startPolling());
} }
return Future<void>.value(); return Future.wait<void>(calls);
} }
/// Disable device events. /// Disable device events.
Future<void> disable(Map<String, dynamic> args) { Future<void> disable(Map<String, dynamic> args) async {
final List<Future<void>> calls = <Future<void>>[];
for (final PollingDeviceDiscovery discoverer in _discoverers) { for (final PollingDeviceDiscovery discoverer in _discoverers) {
discoverer.stopPolling(); calls.add(discoverer.stopPolling());
} }
return Future<void>.value(); return Future.wait<void>(calls);
} }
/// Forward a host port to a device port. /// Forward a host port to a device port.
......
...@@ -82,6 +82,7 @@ class DeviceManager { ...@@ -82,6 +82,7 @@ class DeviceManager {
platform: globals.platform, platform: globals.platform,
xcdevice: globals.xcdevice, xcdevice: globals.xcdevice,
iosWorkflow: globals.iosWorkflow, iosWorkflow: globals.iosWorkflow,
logger: globals.logger,
), ),
IOSSimulators(iosSimulatorUtils: globals.iosSimulatorUtils), IOSSimulators(iosSimulatorUtils: globals.iosSimulatorUtils),
FuchsiaDevices( FuchsiaDevices(
...@@ -289,14 +290,18 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { ...@@ -289,14 +290,18 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery {
static const Duration _pollingTimeout = Duration(seconds: 30); static const Duration _pollingTimeout = Duration(seconds: 30);
final String name; final String name;
ItemListNotifier<Device> _items;
@protected
@visibleForTesting
ItemListNotifier<Device> deviceNotifier;
Timer _timer; Timer _timer;
Future<List<Device>> pollingGetDevices({ Duration timeout }); Future<List<Device>> pollingGetDevices({ Duration timeout });
void startPolling() { Future<void> startPolling() async {
if (_timer == null) { if (_timer == null) {
_items ??= ItemListNotifier<Device>(); deviceNotifier ??= ItemListNotifier<Device>();
// Make initial population the default, fast polling timeout. // Make initial population the default, fast polling timeout.
_timer = _initTimer(null); _timer = _initTimer(null);
} }
...@@ -306,7 +311,7 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { ...@@ -306,7 +311,7 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery {
return Timer(_pollingInterval, () async { return Timer(_pollingInterval, () async {
try { try {
final List<Device> devices = await pollingGetDevices(timeout: pollingTimeout); final List<Device> devices = await pollingGetDevices(timeout: pollingTimeout);
_items.updateWithNewList(devices); deviceNotifier.updateWithNewList(devices);
} on TimeoutException { } on TimeoutException {
globals.printTrace('Device poll timed out. Will retry.'); globals.printTrace('Device poll timed out. Will retry.');
} }
...@@ -315,7 +320,7 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { ...@@ -315,7 +320,7 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery {
}); });
} }
void stopPolling() { Future<void> stopPolling() async {
_timer?.cancel(); _timer?.cancel();
_timer = null; _timer = null;
} }
...@@ -327,23 +332,23 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { ...@@ -327,23 +332,23 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery {
@override @override
Future<List<Device>> discoverDevices({ Duration timeout }) async { Future<List<Device>> discoverDevices({ Duration timeout }) async {
_items = null; deviceNotifier = null;
return _populateDevices(timeout: timeout); return _populateDevices(timeout: timeout);
} }
Future<List<Device>> _populateDevices({ Duration timeout }) async { Future<List<Device>> _populateDevices({ Duration timeout }) async {
_items ??= ItemListNotifier<Device>.from(await pollingGetDevices(timeout: timeout)); deviceNotifier ??= ItemListNotifier<Device>.from(await pollingGetDevices(timeout: timeout));
return _items.items; return deviceNotifier.items;
} }
Stream<Device> get onAdded { Stream<Device> get onAdded {
_items ??= ItemListNotifier<Device>(); deviceNotifier ??= ItemListNotifier<Device>();
return _items.onAdded; return deviceNotifier.onAdded;
} }
Stream<Device> get onRemoved { Stream<Device> get onRemoved {
_items ??= ItemListNotifier<Device>(); deviceNotifier ??= ItemListNotifier<Device>();
return _items.onRemoved; return deviceNotifier.onRemoved;
} }
void dispose() => stopPolling(); void dispose() => stopPolling();
......
...@@ -16,6 +16,7 @@ import '../base/io.dart'; ...@@ -16,6 +16,7 @@ import '../base/io.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/platform.dart'; import '../base/platform.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../base/utils.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../convert.dart'; import '../convert.dart';
import '../device.dart'; import '../device.dart';
...@@ -35,14 +36,22 @@ class IOSDevices extends PollingDeviceDiscovery { ...@@ -35,14 +36,22 @@ class IOSDevices extends PollingDeviceDiscovery {
Platform platform, Platform platform,
XCDevice xcdevice, XCDevice xcdevice,
IOSWorkflow iosWorkflow, IOSWorkflow iosWorkflow,
Logger logger,
}) : _platform = platform ?? globals.platform, }) : _platform = platform ?? globals.platform,
_xcdevice = xcdevice ?? globals.xcdevice, _xcdevice = xcdevice ?? globals.xcdevice,
_iosWorkflow = iosWorkflow ?? globals.iosWorkflow, _iosWorkflow = iosWorkflow ?? globals.iosWorkflow,
_logger = logger ?? globals.logger,
super('iOS devices'); super('iOS devices');
@override
void dispose() {
_observedDeviceEventsSubscription?.cancel();
}
final Platform _platform; final Platform _platform;
final XCDevice _xcdevice; final XCDevice _xcdevice;
final IOSWorkflow _iosWorkflow; final IOSWorkflow _iosWorkflow;
final Logger _logger;
@override @override
bool get supportsPlatform => _platform.isMacOS; bool get supportsPlatform => _platform.isMacOS;
...@@ -50,6 +59,60 @@ class IOSDevices extends PollingDeviceDiscovery { ...@@ -50,6 +59,60 @@ class IOSDevices extends PollingDeviceDiscovery {
@override @override
bool get canListAnything => _iosWorkflow.canListDevices; bool get canListAnything => _iosWorkflow.canListDevices;
StreamSubscription<Map<XCDeviceEvent, String>> _observedDeviceEventsSubscription;
@override
Future<void> startPolling() async {
if (!_platform.isMacOS) {
throw UnsupportedError(
'Control of iOS devices or simulators only supported on macOS.'
);
}
deviceNotifier ??= ItemListNotifier<Device>();
// Start by populating all currently attached devices.
deviceNotifier.updateWithNewList(await pollingGetDevices());
// cancel any outstanding subscriptions.
await _observedDeviceEventsSubscription?.cancel();
_observedDeviceEventsSubscription = _xcdevice.observedDeviceEvents().listen(
_onDeviceEvent,
onError: (dynamic error, StackTrace stack) {
_logger.printTrace('Process exception running xcdevice observe:\n$error\n$stack');
}, onDone: () {
// If xcdevice is killed or otherwise dies, polling will be stopped.
// No retry is attempted and the polling client will have to restart polling
// (restart the IDE). Avoid hammering on a process that is
// continuously failing.
_logger.printTrace('xcdevice observe stopped');
},
cancelOnError: true,
);
}
Future<void> _onDeviceEvent(Map<XCDeviceEvent, String> event) async {
final XCDeviceEvent eventType = event.containsKey(XCDeviceEvent.attach) ? XCDeviceEvent.attach : XCDeviceEvent.detach;
final String deviceIdentifier = event[eventType];
final Device knownDevice = deviceNotifier.items
.firstWhere((Device device) => device.id == deviceIdentifier, orElse: () => null);
// Ignore already discovered devices (maybe populated at the beginning).
if (eventType == XCDeviceEvent.attach && knownDevice == null) {
// There's no way to get details for an individual attached device,
// so repopulate them all.
final List<Device> devices = await pollingGetDevices();
deviceNotifier.updateWithNewList(devices);
} else if (eventType == XCDeviceEvent.detach && knownDevice != null) {
deviceNotifier.removeItem(knownDevice);
}
}
@override
Future<void> stopPolling() async {
await _observedDeviceEventsSubscription?.cancel();
}
@override @override
Future<List<Device>> pollingGetDevices({ Duration timeout }) async { Future<List<Device>> pollingGetDevices({ Duration timeout }) async {
if (!_platform.isMacOS) { if (!_platform.isMacOS) {
......
...@@ -194,6 +194,11 @@ class Xcode { ...@@ -194,6 +194,11 @@ class Xcode {
} }
} }
enum XCDeviceEvent {
attach,
detach,
}
/// A utility class for interacting with Xcode xcdevice command line tools. /// A utility class for interacting with Xcode xcdevice command line tools.
class XCDevice { class XCDevice {
XCDevice({ XCDevice({
...@@ -218,7 +223,14 @@ class XCDevice { ...@@ -218,7 +223,14 @@ class XCDevice {
platform: platform, platform: platform,
processManager: processManager, processManager: processManager,
), ),
_xcode = xcode; _xcode = xcode {
_setupDeviceIdentifierByEventStream();
}
void dispose() {
_deviceObservationProcess?.kill();
}
final ProcessUtils _processUtils; final ProcessUtils _processUtils;
final Logger _logger; final Logger _logger;
...@@ -226,6 +238,19 @@ class XCDevice { ...@@ -226,6 +238,19 @@ class XCDevice {
final IOSDeploy _iosDeploy; final IOSDeploy _iosDeploy;
final Xcode _xcode; final Xcode _xcode;
List<dynamic> _cachedListResults;
Process _deviceObservationProcess;
StreamController<Map<XCDeviceEvent, String>> _deviceIdentifierByEvent;
void _setupDeviceIdentifierByEventStream() {
// _deviceIdentifierByEvent Should always be available for listeners
// in case polling needs to be stopped and restarted.
_deviceIdentifierByEvent = StreamController<Map<XCDeviceEvent, String>>.broadcast(
onListen: _startObservingTetheredIOSDevices,
onCancel: _stopObservingTetheredIOSDevices,
);
}
bool get isInstalled => _xcode.isInstalledAndMeetsVersionCheck && xcdevicePath != null; bool get isInstalled => _xcode.isInstalledAndMeetsVersionCheck && xcdevicePath != null;
String _xcdevicePath; String _xcdevicePath;
...@@ -287,7 +312,99 @@ class XCDevice { ...@@ -287,7 +312,99 @@ class XCDevice {
return null; return null;
} }
List<dynamic> _cachedListResults; /// Observe identifiers (UDIDs) of devices as they attach and detach.
///
/// Each attach and detach event is a tuple of one event type
/// and identifier.
Stream<Map<XCDeviceEvent, String>> observedDeviceEvents() {
if (!isInstalled) {
_logger.printTrace("Xcode not found. Run 'flutter doctor' for more information.");
return null;
}
return _deviceIdentifierByEvent.stream;
}
// Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
// Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
final RegExp _observationIdentifierPattern = RegExp(r'^(\w*): (\w*)$');
Future<void> _startObservingTetheredIOSDevices() async {
try {
if (_deviceObservationProcess != null) {
throw Exception('xcdevice observe restart failed');
}
// Run in interactive mode (via script) to convince
// xcdevice it has a terminal attached in order to redirect stdout.
_deviceObservationProcess = await _processUtils.start(
<String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'observe',
'--both',
],
);
final StreamSubscription<String> stdoutSubscription = _deviceObservationProcess.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
// xcdevice observe example output of UDIDs:
//
// Listening for all devices, on both interfaces.
// Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
// Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
// Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
final RegExpMatch match = _observationIdentifierPattern.firstMatch(line);
if (match != null && match.groupCount == 2) {
final String verb = match.group(1).toLowerCase();
final String identifier = match.group(2);
if (verb.startsWith('attach')) {
_deviceIdentifierByEvent.add(<XCDeviceEvent, String>{
XCDeviceEvent.attach: identifier
});
} else if (verb.startsWith('detach')) {
_deviceIdentifierByEvent.add(<XCDeviceEvent, String>{
XCDeviceEvent.detach: identifier
});
}
}
});
final StreamSubscription<String> stderrSubscription = _deviceObservationProcess.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
_logger.printTrace('xcdevice observe error: $line');
});
unawaited(_deviceObservationProcess.exitCode.then((int status) {
_logger.printTrace('xcdevice exited with code $exitCode');
unawaited(stdoutSubscription.cancel());
unawaited(stderrSubscription.cancel());
}).whenComplete(() async {
if (_deviceIdentifierByEvent.hasListener) {
// Tell listeners the process died.
await _deviceIdentifierByEvent.close();
}
_deviceObservationProcess = null;
// Reopen it so new listeners can resume polling.
_setupDeviceIdentifierByEventStream();
}));
} on ProcessException catch (exception, stackTrace) {
_deviceIdentifierByEvent.addError(exception, stackTrace);
} on ArgumentError catch (exception, stackTrace) {
_deviceIdentifierByEvent.addError(exception, stackTrace);
}
}
void _stopObservingTetheredIOSDevices() {
_deviceObservationProcess?.kill();
}
/// [timeout] defaults to 2 seconds. /// [timeout] defaults to 2 seconds.
Future<List<IOSDevice>> getAvailableIOSDevices({ Duration timeout }) async { Future<List<IOSDevice>> getAvailableIOSDevices({ Duration timeout }) async {
......
...@@ -18,19 +18,25 @@ void main() { ...@@ -18,19 +18,25 @@ void main() {
final Future<List<String>> removedStreamItems = list.onRemoved.toList(); final Future<List<String>> removedStreamItems = list.onRemoved.toList();
list.updateWithNewList(<String>['aaa']); list.updateWithNewList(<String>['aaa']);
list.updateWithNewList(<String>['aaa', 'bbb']); list.removeItem('bogus');
list.updateWithNewList(<String>['bbb']); list.updateWithNewList(<String>['aaa', 'bbb', 'ccc']);
list.updateWithNewList(<String>['bbb', 'ccc']);
list.removeItem('bbb');
expect(list.items, <String>['ccc']);
list.dispose(); list.dispose();
final List<String> addedItems = await addedStreamItems; final List<String> addedItems = await addedStreamItems;
final List<String> removedItems = await removedStreamItems; final List<String> removedItems = await removedStreamItems;
expect(addedItems.length, 2); expect(addedItems.length, 3);
expect(addedItems.first, 'aaa'); expect(addedItems.first, 'aaa');
expect(addedItems[1], 'bbb'); expect(addedItems[1], 'bbb');
expect(addedItems[2], 'ccc');
expect(removedItems.length, 1); expect(removedItems.length, 2);
expect(removedItems.first, 'aaa'); expect(removedItems.first, 'aaa');
expect(removedItems[1], 'bbb');
}); });
}); });
} }
...@@ -66,9 +66,9 @@ void main() { ...@@ -66,9 +66,9 @@ void main() {
group('PollingDeviceDiscovery', () { group('PollingDeviceDiscovery', () {
testUsingContext('startPolling', () async { testUsingContext('startPolling', () async {
FakeAsync().run((FakeAsync time) { FakeAsync().run((FakeAsync time) async {
final FakePollingDeviceDiscovery pollingDeviceDiscovery = FakePollingDeviceDiscovery(); final FakePollingDeviceDiscovery pollingDeviceDiscovery = FakePollingDeviceDiscovery();
pollingDeviceDiscovery.startPolling(); await pollingDeviceDiscovery.startPolling();
time.elapse(const Duration(milliseconds: 4001)); time.elapse(const Duration(milliseconds: 4001));
time.flushMicrotasks(); time.flushMicrotasks();
// First check should use the default polling timeout // First check should use the default polling timeout
...@@ -79,7 +79,7 @@ void main() { ...@@ -79,7 +79,7 @@ void main() {
time.flushMicrotasks(); time.flushMicrotasks();
// Subsequent polling should be much longer. // Subsequent polling should be much longer.
expect(pollingDeviceDiscovery.lastPollingTimeout, const Duration(seconds: 30)); expect(pollingDeviceDiscovery.lastPollingTimeout, const Duration(seconds: 30));
pollingDeviceDiscovery.stopPolling(); await pollingDeviceDiscovery.stopPolling();
}); });
}); });
}); });
......
...@@ -262,15 +262,17 @@ void main() { ...@@ -262,15 +262,17 @@ void main() {
}); });
}); });
group('pollingGetDevices', () { group('polling', () {
MockXcdevice mockXcdevice; MockXcdevice mockXcdevice;
MockArtifacts mockArtifacts; MockArtifacts mockArtifacts;
MockCache mockCache; MockCache mockCache;
FakeProcessManager fakeProcessManager; FakeProcessManager fakeProcessManager;
Logger logger; BufferLogger logger;
IOSDeploy iosDeploy; IOSDeploy iosDeploy;
IMobileDevice iMobileDevice; IMobileDevice iMobileDevice;
IOSWorkflow mockIosWorkflow; IOSWorkflow mockIosWorkflow;
IOSDevice device1;
IOSDevice device2;
setUp(() { setUp(() {
mockXcdevice = MockXcdevice(); mockXcdevice = MockXcdevice();
...@@ -292,15 +294,163 @@ void main() { ...@@ -292,15 +294,163 @@ void main() {
processManager: fakeProcessManager, processManager: fakeProcessManager,
logger: logger, logger: logger,
); );
device1 = IOSDevice(
'd83d5bc53967baa0ee18626ba87b6254b2ab5418',
name: 'Paired iPhone',
sdkVersion: '13.3',
cpuArchitecture: DarwinArch.arm64,
artifacts: mockArtifacts,
iosDeploy: iosDeploy,
iMobileDevice: iMobileDevice,
logger: logger,
platform: macPlatform,
fileSystem: MemoryFileSystem.test(),
interfaceType: IOSDeviceInterface.usb,
);
device2 = IOSDevice(
'43ad2fda7991b34fe1acbda82f9e2fd3d6ddc9f7',
name: 'iPhone 6s',
sdkVersion: '13.3',
cpuArchitecture: DarwinArch.arm64,
artifacts: mockArtifacts,
iosDeploy: iosDeploy,
iMobileDevice: iMobileDevice,
logger: logger,
platform: macPlatform,
fileSystem: MemoryFileSystem.test(),
interfaceType: IOSDeviceInterface.usb,
);
});
testWithoutContext('start polling', () async {
final IOSDevices iosDevices = IOSDevices(
platform: macPlatform,
xcdevice: mockXcdevice,
iosWorkflow: mockIosWorkflow,
logger: logger,
);
when(mockXcdevice.isInstalled).thenReturn(true);
int fetchDevicesCount = 0;
when(mockXcdevice.getAvailableIOSDevices())
.thenAnswer((Invocation invocation) {
if (fetchDevicesCount == 0) {
// Initial time, no devices.
fetchDevicesCount++;
return Future<List<IOSDevice>>.value(<IOSDevice>[]);
} else if (fetchDevicesCount == 1) {
// Simulate 2 devices added later.
fetchDevicesCount++;
return Future<List<IOSDevice>>.value(<IOSDevice>[device1, device2]);
}
fail('Too many calls to getAvailableTetheredIOSDevices');
});
int addedCount = 0;
final Completer<void> added = Completer<void>();
iosDevices.onAdded.listen((Device device) {
addedCount++;
// 2 devices will be added.
// Will throw over-completion if called more than twice.
if (addedCount >= 2) {
added.complete();
}
});
final Completer<void> removed = Completer<void>();
iosDevices.onRemoved.listen((Device device) {
// Will throw over-completion if called more than once.
removed.complete();
});
final StreamController<Map<XCDeviceEvent, String>> eventStream = StreamController<Map<XCDeviceEvent, String>>();
when(mockXcdevice.observedDeviceEvents()).thenAnswer((_) => eventStream.stream);
await iosDevices.startPolling();
verify(mockXcdevice.getAvailableIOSDevices()).called(1);
expect(iosDevices.deviceNotifier.items, isEmpty);
expect(eventStream.hasListener, isTrue);
eventStream.add(<XCDeviceEvent, String>{
XCDeviceEvent.attach: 'd83d5bc53967baa0ee18626ba87b6254b2ab5418'
});
await added.future;
expect(iosDevices.deviceNotifier.items.length, 2);
expect(iosDevices.deviceNotifier.items, contains(device1));
expect(iosDevices.deviceNotifier.items, contains(device2));
eventStream.add(<XCDeviceEvent, String>{
XCDeviceEvent.detach: 'd83d5bc53967baa0ee18626ba87b6254b2ab5418'
});
await removed.future;
expect(iosDevices.deviceNotifier.items, <Device>[device2]);
// Remove stream will throw over-completion if called more than once
// which proves this is ignored.
eventStream.add(<XCDeviceEvent, String>{
XCDeviceEvent.detach: 'bogus'
});
expect(addedCount, 2);
await iosDevices.stopPolling();
expect(eventStream.hasListener, isFalse);
});
testWithoutContext('polling can be restarted if stream is closed', () async {
final IOSDevices iosDevices = IOSDevices(
platform: macPlatform,
xcdevice: mockXcdevice,
iosWorkflow: mockIosWorkflow,
logger: logger,
);
when(mockXcdevice.isInstalled).thenReturn(true);
when(mockXcdevice.getAvailableIOSDevices())
.thenAnswer((Invocation invocation) => Future<List<IOSDevice>>.value(<IOSDevice>[]));
final StreamController<Map<XCDeviceEvent, String>> eventStream = StreamController<Map<XCDeviceEvent, String>>();
final StreamController<Map<XCDeviceEvent, String>> rescheduledStream = StreamController<Map<XCDeviceEvent, String>>();
bool reschedule = false;
when(mockXcdevice.observedDeviceEvents()).thenAnswer((Invocation invocation) {
if (!reschedule) {
reschedule = true;
return eventStream.stream;
}
return rescheduledStream.stream;
});
await iosDevices.startPolling();
expect(eventStream.hasListener, isTrue);
verify(mockXcdevice.getAvailableIOSDevices()).called(1);
// Pretend xcdevice crashed.
await eventStream.close();
expect(logger.traceText, contains('xcdevice observe stopped'));
// Confirm a restart still gets streamed events.
await iosDevices.startPolling();
expect(eventStream.hasListener, isFalse);
expect(rescheduledStream.hasListener, isTrue);
await iosDevices.stopPolling();
expect(rescheduledStream.hasListener, isFalse);
}); });
final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform]; final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform];
for (final Platform unsupportedPlatform in unsupportedPlatforms) { for (final Platform unsupportedPlatform in unsupportedPlatforms) {
testWithoutContext('throws Unsupported Operation exception on ${unsupportedPlatform.operatingSystem}', () async { testWithoutContext('pollingGetDevices throws Unsupported Operation exception on ${unsupportedPlatform.operatingSystem}', () async {
final IOSDevices iosDevices = IOSDevices( final IOSDevices iosDevices = IOSDevices(
platform: unsupportedPlatform, platform: unsupportedPlatform,
xcdevice: mockXcdevice, xcdevice: mockXcdevice,
iosWorkflow: mockIosWorkflow, iosWorkflow: mockIosWorkflow,
logger: logger,
); );
when(mockXcdevice.isInstalled).thenReturn(false); when(mockXcdevice.isInstalled).thenReturn(false);
expect( expect(
...@@ -310,43 +460,33 @@ void main() { ...@@ -310,43 +460,33 @@ void main() {
}); });
} }
testWithoutContext('returns attached devices', () async { testWithoutContext('pollingGetDevices returns attached devices', () async {
final IOSDevices iosDevices = IOSDevices( final IOSDevices iosDevices = IOSDevices(
platform: macPlatform, platform: macPlatform,
xcdevice: mockXcdevice, xcdevice: mockXcdevice,
iosWorkflow: mockIosWorkflow, iosWorkflow: mockIosWorkflow,
logger: logger,
); );
when(mockXcdevice.isInstalled).thenReturn(true); when(mockXcdevice.isInstalled).thenReturn(true);
final IOSDevice device = IOSDevice(
'd83d5bc53967baa0ee18626ba87b6254b2ab5418',
name: 'Paired iPhone',
sdkVersion: '13.3',
cpuArchitecture: DarwinArch.arm64,
artifacts: mockArtifacts,
iosDeploy: iosDeploy,
iMobileDevice: iMobileDevice,
logger: logger,
platform: macPlatform,
fileSystem: MemoryFileSystem.test(),
interfaceType: IOSDeviceInterface.usb,
);
when(mockXcdevice.getAvailableIOSDevices()) when(mockXcdevice.getAvailableIOSDevices())
.thenAnswer((Invocation invocation) => Future<List<IOSDevice>>.value(<IOSDevice>[device])); .thenAnswer((Invocation invocation) => Future<List<IOSDevice>>.value(<IOSDevice>[device1]));
final List<Device> devices = await iosDevices.pollingGetDevices(); final List<Device> devices = await iosDevices.pollingGetDevices();
expect(devices, hasLength(1)); expect(devices, hasLength(1));
expect(identical(devices.first, device), isTrue); expect(identical(devices.first, device1), isTrue);
}); });
}); });
group('getDiagnostics', () { group('getDiagnostics', () {
MockXcdevice mockXcdevice; MockXcdevice mockXcdevice;
IOSWorkflow mockIosWorkflow; IOSWorkflow mockIosWorkflow;
Logger logger;
setUp(() { setUp(() {
mockXcdevice = MockXcdevice(); mockXcdevice = MockXcdevice();
mockIosWorkflow = MockIOSWorkflow(); mockIosWorkflow = MockIOSWorkflow();
logger = BufferLogger.test();
}); });
final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform]; final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform];
...@@ -356,6 +496,7 @@ void main() { ...@@ -356,6 +496,7 @@ void main() {
platform: unsupportedPlatform, platform: unsupportedPlatform,
xcdevice: mockXcdevice, xcdevice: mockXcdevice,
iosWorkflow: mockIosWorkflow, iosWorkflow: mockIosWorkflow,
logger: logger,
); );
when(mockXcdevice.isInstalled).thenReturn(false); when(mockXcdevice.isInstalled).thenReturn(false);
expect((await iosDevices.getDiagnostics()).first, 'Control of iOS devices or simulators only supported on macOS.'); expect((await iosDevices.getDiagnostics()).first, 'Control of iOS devices or simulators only supported on macOS.');
...@@ -367,6 +508,7 @@ void main() { ...@@ -367,6 +508,7 @@ void main() {
platform: macPlatform, platform: macPlatform,
xcdevice: mockXcdevice, xcdevice: mockXcdevice,
iosWorkflow: mockIosWorkflow, iosWorkflow: mockIosWorkflow,
logger: logger,
); );
when(mockXcdevice.isInstalled).thenReturn(true); when(mockXcdevice.isInstalled).thenReturn(true);
when(mockXcdevice.getDiagnostics()) when(mockXcdevice.getDiagnostics())
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult; import 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult;
...@@ -19,7 +21,7 @@ import '../../src/common.dart'; ...@@ -19,7 +21,7 @@ import '../../src/common.dart';
import '../../src/context.dart'; import '../../src/context.dart';
void main() { void main() {
Logger logger; BufferLogger logger;
setUp(() { setUp(() {
logger = BufferLogger.test(); logger = BufferLogger.test();
...@@ -353,6 +355,59 @@ void main() { ...@@ -353,6 +355,59 @@ void main() {
}); });
}); });
group('observe device events', () {
testWithoutContext('Xcode not installed', () async {
when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(false);
expect(xcdevice.observedDeviceEvents(), isNull);
expect(logger.traceText, contains("Xcode not found. Run 'flutter doctor' for more information."));
});
testUsingContext('relays events', () async {
when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
fakeProcessManager.addCommand(const FakeCommand(
command: <String>['xcrun', '--find', 'xcdevice'],
stdout: '/path/to/xcdevice',
));
fakeProcessManager.addCommand(const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'observe',
'--both',
], stdout: 'Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418\n'
'Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418',
stderr: 'Some error',
));
final Completer<void> attach = Completer<void>();
final Completer<void> detach = Completer<void>();
// Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
// Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
xcdevice.observedDeviceEvents().listen((Map<XCDeviceEvent, String> event) {
expect(event.length, 1);
if (event.containsKey(XCDeviceEvent.attach)) {
expect(event[XCDeviceEvent.attach], 'd83d5bc53967baa0ee18626ba87b6254b2ab5418');
attach.complete();
} else if (event.containsKey(XCDeviceEvent.detach)) {
expect(event[XCDeviceEvent.detach], 'd83d5bc53967baa0ee18626ba87b6254b2ab5418');
detach.complete();
} else {
fail('Unexpected event');
}
});
await attach.future;
await detach.future;
expect(logger.traceText, contains('xcdevice observe error: Some error'));
});
});
group('available devices', () { group('available devices', () {
final FakePlatform macPlatform = FakePlatform(operatingSystem: 'macos'); final FakePlatform macPlatform = FakePlatform(operatingSystem: 'macos');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment