devices_test.dart 20.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:async';

7
import 'package:file/file.dart';
8
import 'package:file/memory.dart';
9
import 'package:flutter_tools/src/application_package.dart';
10
import 'package:flutter_tools/src/artifacts.dart';
11
import 'package:flutter_tools/src/base/file_system.dart';
12
import 'package:flutter_tools/src/base/io.dart';
13
import 'package:flutter_tools/src/base/logger.dart';
14
import 'package:flutter_tools/src/base/os.dart';
15
import 'package:flutter_tools/src/base/platform.dart';
16
import 'package:flutter_tools/src/build_info.dart';
17
import 'package:flutter_tools/src/cache.dart';
18
import 'package:flutter_tools/src/device.dart';
19
import 'package:flutter_tools/src/ios/devices.dart';
20
import 'package:flutter_tools/src/ios/ios_deploy.dart';
21
import 'package:flutter_tools/src/ios/ios_workflow.dart';
22
import 'package:flutter_tools/src/ios/iproxy.dart';
23
import 'package:flutter_tools/src/ios/mac.dart';
24
import 'package:flutter_tools/src/macos/xcode.dart';
25
import 'package:mockito/mockito.dart';
26
import 'package:vm_service/vm_service.dart';
27

28 29 30
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/mocks.dart';
31

32
void main() {
33 34 35
  final FakePlatform macPlatform = FakePlatform(operatingSystem: 'macos');
  final FakePlatform linuxPlatform = FakePlatform(operatingSystem: 'linux');
  final FakePlatform windowsPlatform = FakePlatform(operatingSystem: 'windows');
36

37 38
  group('IOSDevice', () {
    final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform];
39
    Cache cache;
40
    Logger logger;
41
    IOSDeploy iosDeploy;
42
    IMobileDevice iMobileDevice;
43
    FileSystem nullFileSystem;
44

45
    setUp(() {
46
      final Artifacts artifacts = Artifacts.test();
47
      cache = Cache.test();
48
      logger = BufferLogger.test();
49
      iosDeploy = IOSDeploy(
50
        artifacts: artifacts,
51
        cache: cache,
52
        logger: logger,
53 54 55
        platform: macPlatform,
        processManager: FakeProcessManager.any(),
      );
56
      iMobileDevice = IMobileDevice(
57
        artifacts: artifacts,
58
        cache: cache,
59 60 61
        logger: logger,
        processManager: FakeProcessManager.any(),
      );
62 63
    });

64 65 66
    testWithoutContext('successfully instantiates on Mac OS', () {
      IOSDevice(
        'device-123',
67
        iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
68
        fileSystem: nullFileSystem,
69
        logger: logger,
70 71
        platform: macPlatform,
        iosDeploy: iosDeploy,
72
        iMobileDevice: iMobileDevice,
73 74
        name: 'iPhone 1',
        sdkVersion: '13.3',
75 76
        cpuArchitecture: DarwinArch.arm64,
        interfaceType: IOSDeviceInterface.usb,
77 78 79 80 81 82
      );
    });

    testWithoutContext('parses major version', () {
      expect(IOSDevice(
        'device-123',
83
        iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
84
        fileSystem: nullFileSystem,
85
        logger: logger,
86 87
        platform: macPlatform,
        iosDeploy: iosDeploy,
88
        iMobileDevice: iMobileDevice,
89 90
        name: 'iPhone 1',
        cpuArchitecture: DarwinArch.arm64,
91 92
        sdkVersion: '1.0.0',
        interfaceType: IOSDeviceInterface.usb,
93 94 95
      ).majorSdkVersion, 1);
      expect(IOSDevice(
        'device-123',
96
        iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
97
        fileSystem: nullFileSystem,
98
        logger: logger,
99 100
        platform: macPlatform,
        iosDeploy: iosDeploy,
101
        iMobileDevice: iMobileDevice,
102 103
        name: 'iPhone 1',
        cpuArchitecture: DarwinArch.arm64,
104 105
        sdkVersion: '13.1.1',
        interfaceType: IOSDeviceInterface.usb,
106 107 108
      ).majorSdkVersion, 13);
      expect(IOSDevice(
        'device-123',
109
        iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
110
        fileSystem: nullFileSystem,
111
        logger: logger,
112 113
        platform: macPlatform,
        iosDeploy: iosDeploy,
114
        iMobileDevice: iMobileDevice,
115 116
        name: 'iPhone 1',
        cpuArchitecture: DarwinArch.arm64,
117 118
        sdkVersion: '10',
        interfaceType: IOSDeviceInterface.usb,
119 120 121
      ).majorSdkVersion, 10);
      expect(IOSDevice(
        'device-123',
122
        iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
123
        fileSystem: nullFileSystem,
124
        logger: logger,
125 126
        platform: macPlatform,
        iosDeploy: iosDeploy,
127
        iMobileDevice: iMobileDevice,
128 129
        name: 'iPhone 1',
        cpuArchitecture: DarwinArch.arm64,
130 131
        sdkVersion: '0',
        interfaceType: IOSDeviceInterface.usb,
132 133 134
      ).majorSdkVersion, 0);
      expect(IOSDevice(
        'device-123',
135
        iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
136
        fileSystem: nullFileSystem,
137
        logger: logger,
138 139
        platform: macPlatform,
        iosDeploy: iosDeploy,
140
        iMobileDevice: iMobileDevice,
141 142
        name: 'iPhone 1',
        cpuArchitecture: DarwinArch.arm64,
143 144
        sdkVersion: 'bogus',
        interfaceType: IOSDeviceInterface.usb,
145
      ).majorSdkVersion, 0);
146 147
    });

148 149 150
    testWithoutContext('Supports debug, profile, and release modes', () {
      final IOSDevice device = IOSDevice(
        'device-123',
151
        iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
152
        fileSystem: nullFileSystem,
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
        logger: logger,
        platform: macPlatform,
        iosDeploy: iosDeploy,
        iMobileDevice: iMobileDevice,
        name: 'iPhone 1',
        sdkVersion: '13.3',
        cpuArchitecture: DarwinArch.arm64,
        interfaceType: IOSDeviceInterface.usb,
      );

      expect(device.supportsRuntimeMode(BuildMode.debug), true);
      expect(device.supportsRuntimeMode(BuildMode.profile), true);
      expect(device.supportsRuntimeMode(BuildMode.release), true);
      expect(device.supportsRuntimeMode(BuildMode.jitRelease), false);
    });

169
    for (final Platform platform in unsupportedPlatforms) {
170
      testWithoutContext('throws UnsupportedError exception if instantiated on ${platform.operatingSystem}', () {
171
        expect(
172 173 174
          () {
            IOSDevice(
              'device-123',
175
              iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
176
              fileSystem: nullFileSystem,
177
              logger: logger,
178 179
              platform: platform,
              iosDeploy: iosDeploy,
180
              iMobileDevice: iMobileDevice,
181 182 183
              name: 'iPhone 1',
              sdkVersion: '13.3',
              cpuArchitecture: DarwinArch.arm64,
184
              interfaceType: IOSDeviceInterface.usb,
185 186
            );
          },
Dan Field's avatar
Dan Field committed
187
          throwsAssertionError,
188
        );
189 190 191
      });
    }

192 193
    group('.dispose()', () {
      IOSDevice device;
194 195
      MockIOSApp appPackage1;
      MockIOSApp appPackage2;
196 197 198 199 200 201 202
      IOSDeviceLogReader logReader1;
      IOSDeviceLogReader logReader2;
      MockProcess mockProcess1;
      MockProcess mockProcess2;
      MockProcess mockProcess3;
      IOSDevicePortForwarder portForwarder;
      ForwardedPort forwardedPort;
203
      Cache cache;
204
      Logger logger;
205
      IOSDeploy iosDeploy;
206
      FileSystem nullFileSystem;
207
      IProxy iproxy;
208 209 210 211

      IOSDevicePortForwarder createPortForwarder(
          ForwardedPort forwardedPort,
          IOSDevice device) {
212
        iproxy = IProxy.test(logger: logger, processManager: FakeProcessManager.any());
213 214 215
        final IOSDevicePortForwarder portForwarder = IOSDevicePortForwarder(
          id: device.id,
          logger: logger,
216
          operatingSystemUtils: OperatingSystemUtils(
217
            fileSystem: nullFileSystem,
218 219 220 221
            logger: logger,
            platform: FakePlatform(operatingSystem: 'macos'),
            processManager: FakeProcessManager.any(),
          ),
222
          iproxy: iproxy,
223
        );
224 225 226 227 228 229
        portForwarder.addForwardedPorts(<ForwardedPort>[forwardedPort]);
        return portForwarder;
      }

      IOSDeviceLogReader createLogReader(
          IOSDevice device,
230
          IOSApp appPackage,
231
          Process process) {
232 233 234 235 236
        final IOSDeviceLogReader logReader = IOSDeviceLogReader.create(
          device: device,
          app: appPackage,
          iMobileDevice: null, // not used by this test.
        );
237 238 239 240 241
        logReader.idevicesyslogProcess = process;
        return logReader;
      }

      setUp(() {
242 243
        appPackage1 = MockIOSApp();
        appPackage2 = MockIOSApp();
244 245 246 247 248 249
        when(appPackage1.name).thenReturn('flutterApp1');
        when(appPackage2.name).thenReturn('flutterApp2');
        mockProcess1 = MockProcess();
        mockProcess2 = MockProcess();
        mockProcess3 = MockProcess();
        forwardedPort = ForwardedPort.withContext(123, 456, mockProcess3);
250
        cache = Cache.test();
251
        iosDeploy = IOSDeploy(
252
          artifacts: Artifacts.test(),
253
          cache: cache,
254
          logger: logger,
255 256 257
          platform: macPlatform,
          processManager: FakeProcessManager.any(),
        );
258 259
      });

260 261 262
      testWithoutContext('kills all log readers & port forwarders', () async {
        device = IOSDevice(
          '123',
263
          iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
264
          fileSystem: nullFileSystem,
265
          logger: logger,
266 267
          platform: macPlatform,
          iosDeploy: iosDeploy,
268
          iMobileDevice: iMobileDevice,
269 270 271
          name: 'iPhone 1',
          sdkVersion: '13.3',
          cpuArchitecture: DarwinArch.arm64,
272
          interfaceType: IOSDeviceInterface.usb,
273
        );
274 275 276 277 278 279 280
        logReader1 = createLogReader(device, appPackage1, mockProcess1);
        logReader2 = createLogReader(device, appPackage2, mockProcess2);
        portForwarder = createPortForwarder(forwardedPort, device);
        device.setLogReader(appPackage1, logReader1);
        device.setLogReader(appPackage2, logReader2);
        device.portForwarder = portForwarder;

281
        await device.dispose();
282 283 284 285 286 287

        verify(mockProcess1.kill());
        verify(mockProcess2.kill());
        verify(mockProcess3.kill());
      });
    });
288
  });
289

290
  group('polling', () {
291
    MockXcdevice mockXcdevice;
292
    Cache cache;
293
    FakeProcessManager fakeProcessManager;
294
    BufferLogger logger;
295
    IOSDeploy iosDeploy;
296
    IMobileDevice iMobileDevice;
297
    IOSWorkflow mockIosWorkflow;
298 299
    IOSDevice device1;
    IOSDevice device2;
300 301

    setUp(() {
302
      mockXcdevice = MockXcdevice();
303
      final Artifacts artifacts = Artifacts.test();
304
      cache = Cache.test();
305
      logger = BufferLogger.test();
306 307
      mockIosWorkflow = MockIOSWorkflow();
      fakeProcessManager = FakeProcessManager.any();
308
      iosDeploy = IOSDeploy(
309
        artifacts: artifacts,
310
        cache: cache,
311
        logger: logger,
312
        platform: macPlatform,
313
        processManager: fakeProcessManager,
314
      );
315
      iMobileDevice = IMobileDevice(
316
        artifacts: artifacts,
317
        cache: cache,
318 319 320
        processManager: fakeProcessManager,
        logger: logger,
      );
321 322 323 324 325 326

      device1 = IOSDevice(
        'd83d5bc53967baa0ee18626ba87b6254b2ab5418',
        name: 'Paired iPhone',
        sdkVersion: '13.3',
        cpuArchitecture: DarwinArch.arm64,
327
        iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
328 329 330 331 332 333 334 335 336
        iosDeploy: iosDeploy,
        iMobileDevice: iMobileDevice,
        logger: logger,
        platform: macPlatform,
        fileSystem: MemoryFileSystem.test(),
        interfaceType: IOSDeviceInterface.usb,
      );

      device2 = IOSDevice(
337 338
        '00008027-00192736010F802E',
        name: 'iPad Pro',
339 340
        sdkVersion: '13.3',
        cpuArchitecture: DarwinArch.arm64,
341
        iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
342 343 344 345 346 347 348 349 350
        iosDeploy: iosDeploy,
        iMobileDevice: iMobileDevice,
        logger: logger,
        platform: macPlatform,
        fileSystem: MemoryFileSystem.test(),
        interfaceType: IOSDeviceInterface.usb,
      );
    });

351 352 353 354 355 356 357 358 359 360 361 362 363
    testWithoutContext('start polling without Xcode', () async {
      final IOSDevices iosDevices = IOSDevices(
        platform: macPlatform,
        xcdevice: mockXcdevice,
        iosWorkflow: mockIosWorkflow,
        logger: logger,
      );
      when(mockXcdevice.isInstalled).thenReturn(false);

      await iosDevices.startPolling();
      verifyNever(mockXcdevice.getAvailableIOSDevices());
    });

364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480
    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);
481 482
    });

483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500
    testWithoutContext('dispose cancels polling subscription', () 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>>();
      when(mockXcdevice.observedDeviceEvents()).thenAnswer((_) => eventStream.stream);

      await iosDevices.startPolling();
      expect(iosDevices.deviceNotifier.items, isEmpty);
      expect(eventStream.hasListener, isTrue);

501
      iosDevices.dispose();
502 503 504
      expect(eventStream.hasListener, isFalse);
    });

505
    final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform];
506
    for (final Platform unsupportedPlatform in unsupportedPlatforms) {
507
      testWithoutContext('pollingGetDevices throws Unsupported Operation exception on ${unsupportedPlatform.operatingSystem}', () async {
508 509 510 511
        final IOSDevices iosDevices = IOSDevices(
          platform: unsupportedPlatform,
          xcdevice: mockXcdevice,
          iosWorkflow: mockIosWorkflow,
512
          logger: logger,
513
        );
514
        when(mockXcdevice.isInstalled).thenReturn(false);
515
        expect(
516
            () async { await iosDevices.pollingGetDevices(); },
Dan Field's avatar
Dan Field committed
517
            throwsA(isA<UnsupportedError>()),
518 519 520 521
        );
      });
    }

522
    testWithoutContext('pollingGetDevices returns attached devices', () async {
523 524 525 526
      final IOSDevices iosDevices = IOSDevices(
        platform: macPlatform,
        xcdevice: mockXcdevice,
        iosWorkflow: mockIosWorkflow,
527
        logger: logger,
528
      );
529
      when(mockXcdevice.isInstalled).thenReturn(true);
530

531
      when(mockXcdevice.getAvailableIOSDevices())
532
          .thenAnswer((Invocation invocation) => Future<List<IOSDevice>>.value(<IOSDevice>[device1]));
533

534
      final List<Device> devices = await iosDevices.pollingGetDevices();
535
      expect(devices, hasLength(1));
536
      expect(identical(devices.first, device1), isTrue);
537
    });
538
  });
539

540 541
  group('getDiagnostics', () {
    MockXcdevice mockXcdevice;
542
    IOSWorkflow mockIosWorkflow;
543
    Logger logger;
544 545 546

    setUp(() {
      mockXcdevice = MockXcdevice();
547
      mockIosWorkflow = MockIOSWorkflow();
548
      logger = BufferLogger.test();
549 550 551 552 553
    });

    final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform];
    for (final Platform unsupportedPlatform in unsupportedPlatforms) {
      testWithoutContext('throws returns platform diagnostic exception on ${unsupportedPlatform.operatingSystem}', () async {
554 555 556 557
        final IOSDevices iosDevices = IOSDevices(
          platform: unsupportedPlatform,
          xcdevice: mockXcdevice,
          iosWorkflow: mockIosWorkflow,
558
          logger: logger,
559
        );
560
        when(mockXcdevice.isInstalled).thenReturn(false);
561
        expect((await iosDevices.getDiagnostics()).first, 'Control of iOS devices or simulators only supported on macOS.');
562 563 564
      });
    }

565 566 567 568 569
    testWithoutContext('returns diagnostics', () async {
      final IOSDevices iosDevices = IOSDevices(
        platform: macPlatform,
        xcdevice: mockXcdevice,
        iosWorkflow: mockIosWorkflow,
570
        logger: logger,
571
      );
572 573 574 575
      when(mockXcdevice.isInstalled).thenReturn(true);
      when(mockXcdevice.getDiagnostics())
          .thenAnswer((Invocation invocation) => Future<List<String>>.value(<String>['Generic pairing error']));

576
      final List<String> diagnostics = await iosDevices.getDiagnostics();
577 578
      expect(diagnostics, hasLength(1));
      expect(diagnostics.first, 'Generic pairing error');
579
    });
580
  });
581
}
582

583 584 585
class MockIOSApp extends Mock implements IOSApp {}
class MockIMobileDevice extends Mock implements IMobileDevice {}
class MockIOSDeploy extends Mock implements IOSDeploy {}
586
class MockIOSWorkflow extends Mock implements IOSWorkflow {}
587
class MockXcdevice extends Mock implements XCDevice {}
588
class MockVmService extends Mock implements VmService {}