devices_test.dart 20.2 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
// @dart = 2.8

7
import 'dart:async';
8
import 'dart:io' as io;
9

10
import 'package:file/file.dart';
11
import 'package:file/memory.dart';
12
import 'package:flutter_tools/src/artifacts.dart';
13
import 'package:flutter_tools/src/base/common.dart';
14
import 'package:flutter_tools/src/base/file_system.dart';
15
import 'package:flutter_tools/src/base/io.dart';
16
import 'package:flutter_tools/src/base/logger.dart';
17
import 'package:flutter_tools/src/base/os.dart';
18
import 'package:flutter_tools/src/base/platform.dart';
19
import 'package:flutter_tools/src/build_info.dart';
20
import 'package:flutter_tools/src/cache.dart';
21
import 'package:flutter_tools/src/device.dart';
22
import 'package:flutter_tools/src/device_port_forwarder.dart';
23
import 'package:flutter_tools/src/ios/application_package.dart';
24
import 'package:flutter_tools/src/ios/devices.dart';
25
import 'package:flutter_tools/src/ios/ios_deploy.dart';
26
import 'package:flutter_tools/src/ios/ios_workflow.dart';
27
import 'package:flutter_tools/src/ios/iproxy.dart';
28
import 'package:flutter_tools/src/ios/mac.dart';
29
import 'package:flutter_tools/src/macos/xcdevice.dart';
30
import 'package:test/fake.dart';
31

32
import '../../src/common.dart';
33
import '../../src/fake_process_manager.dart';
34

35
void main() {
36 37 38
  final FakePlatform macPlatform = FakePlatform(operatingSystem: 'macos');
  final FakePlatform linuxPlatform = FakePlatform(operatingSystem: 'linux');
  final FakePlatform windowsPlatform = FakePlatform(operatingSystem: 'windows');
39

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

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

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

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

151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
    testWithoutContext('has build number in sdkNameAndVersion', () async {
      final IOSDevice device = IOSDevice(
        'device-123',
        iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
        fileSystem: nullFileSystem,
        logger: logger,
        platform: macPlatform,
        iosDeploy: iosDeploy,
        iMobileDevice: iMobileDevice,
        name: 'iPhone 1',
        sdkVersion: '13.3 17C54',
        cpuArchitecture: DarwinArch.arm64,
        interfaceType: IOSDeviceInterface.usb,
      );

      expect(await device.sdkNameAndVersion,'iOS 13.3 17C54');
    });

169 170 171
    testWithoutContext('Supports debug, profile, and release modes', () {
      final IOSDevice device = IOSDevice(
        'device-123',
172
        iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
173
        fileSystem: nullFileSystem,
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
        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);
    });

190
    for (final Platform platform in unsupportedPlatforms) {
191
      testWithoutContext('throws UnsupportedError exception if instantiated on ${platform.operatingSystem}', () {
192
        expect(
193 194 195
          () {
            IOSDevice(
              'device-123',
196
              iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
197
              fileSystem: nullFileSystem,
198
              logger: logger,
199 200
              platform: platform,
              iosDeploy: iosDeploy,
201
              iMobileDevice: iMobileDevice,
202 203 204
              name: 'iPhone 1',
              sdkVersion: '13.3',
              cpuArchitecture: DarwinArch.arm64,
205
              interfaceType: IOSDeviceInterface.usb,
206 207
            );
          },
Dan Field's avatar
Dan Field committed
208
          throwsAssertionError,
209
        );
210 211 212
      });
    }

213 214
    group('.dispose()', () {
      IOSDevice device;
215 216
      FakeIOSApp appPackage1;
      FakeIOSApp appPackage2;
217 218
      IOSDeviceLogReader logReader1;
      IOSDeviceLogReader logReader2;
219 220 221
      FakeProcess process1;
      FakeProcess process2;
      FakeProcess process3;
222 223
      IOSDevicePortForwarder portForwarder;
      ForwardedPort forwardedPort;
224
      Cache cache;
225
      Logger logger;
226
      IOSDeploy iosDeploy;
227
      FileSystem nullFileSystem;
228
      IProxy iproxy;
229 230 231 232

      IOSDevicePortForwarder createPortForwarder(
          ForwardedPort forwardedPort,
          IOSDevice device) {
233
        iproxy = IProxy.test(logger: logger, processManager: FakeProcessManager.any());
234 235 236
        final IOSDevicePortForwarder portForwarder = IOSDevicePortForwarder(
          id: device.id,
          logger: logger,
237
          operatingSystemUtils: OperatingSystemUtils(
238
            fileSystem: nullFileSystem,
239 240 241 242
            logger: logger,
            platform: FakePlatform(operatingSystem: 'macos'),
            processManager: FakeProcessManager.any(),
          ),
243
          iproxy: iproxy,
244
        );
245 246 247 248 249 250
        portForwarder.addForwardedPorts(<ForwardedPort>[forwardedPort]);
        return portForwarder;
      }

      IOSDeviceLogReader createLogReader(
          IOSDevice device,
251
          IOSApp appPackage,
252
          Process process) {
253 254 255 256 257
        final IOSDeviceLogReader logReader = IOSDeviceLogReader.create(
          device: device,
          app: appPackage,
          iMobileDevice: null, // not used by this test.
        );
258 259 260 261 262
        logReader.idevicesyslogProcess = process;
        return logReader;
      }

      setUp(() {
263 264 265 266 267 268
        appPackage1 = FakeIOSApp('flutterApp1');
        appPackage2 = FakeIOSApp('flutterApp2');
        process1 = FakeProcess();
        process2 = FakeProcess();
        process3 = FakeProcess();
        forwardedPort = ForwardedPort.withContext(123, 456, process3);
269 270 271
        cache = Cache.test(
          processManager: FakeProcessManager.any(),
        );
272
        iosDeploy = IOSDeploy(
273
          artifacts: Artifacts.test(),
274
          cache: cache,
275
          logger: logger,
276 277 278
          platform: macPlatform,
          processManager: FakeProcessManager.any(),
        );
279 280
      });

281 282 283
      testWithoutContext('kills all log readers & port forwarders', () async {
        device = IOSDevice(
          '123',
284
          iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
285
          fileSystem: nullFileSystem,
286
          logger: logger,
287 288
          platform: macPlatform,
          iosDeploy: iosDeploy,
289
          iMobileDevice: iMobileDevice,
290 291 292
          name: 'iPhone 1',
          sdkVersion: '13.3',
          cpuArchitecture: DarwinArch.arm64,
293
          interfaceType: IOSDeviceInterface.usb,
294
        );
295 296
        logReader1 = createLogReader(device, appPackage1, process1);
        logReader2 = createLogReader(device, appPackage2, process2);
297 298 299 300 301
        portForwarder = createPortForwarder(forwardedPort, device);
        device.setLogReader(appPackage1, logReader1);
        device.setLogReader(appPackage2, logReader2);
        device.portForwarder = portForwarder;

302
        await device.dispose();
303

304 305 306
        expect(process1.killed, true);
        expect(process2.killed, true);
        expect(process3.killed, true);
307 308
      });
    });
309
  });
310

311
  group('polling', () {
312
    FakeXcdevice xcdevice;
313
    Cache cache;
314
    FakeProcessManager fakeProcessManager;
315
    BufferLogger logger;
316
    IOSDeploy iosDeploy;
317
    IMobileDevice iMobileDevice;
318
    IOSWorkflow iosWorkflow;
319 320
    IOSDevice device1;
    IOSDevice device2;
321 322

    setUp(() {
323
      xcdevice = FakeXcdevice();
324
      final Artifacts artifacts = Artifacts.test();
325
      cache = Cache.test(processManager: FakeProcessManager.any());
326
      logger = BufferLogger.test();
327
      iosWorkflow = FakeIOSWorkflow();
328
      fakeProcessManager = FakeProcessManager.any();
329
      iosDeploy = IOSDeploy(
330
        artifacts: artifacts,
331
        cache: cache,
332
        logger: logger,
333
        platform: macPlatform,
334
        processManager: fakeProcessManager,
335
      );
336
      iMobileDevice = IMobileDevice(
337
        artifacts: artifacts,
338
        cache: cache,
339 340 341
        processManager: fakeProcessManager,
        logger: logger,
      );
342 343 344 345 346 347

      device1 = IOSDevice(
        'd83d5bc53967baa0ee18626ba87b6254b2ab5418',
        name: 'Paired iPhone',
        sdkVersion: '13.3',
        cpuArchitecture: DarwinArch.arm64,
348
        iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
349 350 351 352 353 354 355 356 357
        iosDeploy: iosDeploy,
        iMobileDevice: iMobileDevice,
        logger: logger,
        platform: macPlatform,
        fileSystem: MemoryFileSystem.test(),
        interfaceType: IOSDeviceInterface.usb,
      );

      device2 = IOSDevice(
358 359
        '00008027-00192736010F802E',
        name: 'iPad Pro',
360 361
        sdkVersion: '13.3',
        cpuArchitecture: DarwinArch.arm64,
362
        iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
363 364 365 366 367 368 369 370 371
        iosDeploy: iosDeploy,
        iMobileDevice: iMobileDevice,
        logger: logger,
        platform: macPlatform,
        fileSystem: MemoryFileSystem.test(),
        interfaceType: IOSDeviceInterface.usb,
      );
    });

372 373 374
    testWithoutContext('start polling without Xcode', () async {
      final IOSDevices iosDevices = IOSDevices(
        platform: macPlatform,
375 376
        xcdevice: xcdevice,
        iosWorkflow: iosWorkflow,
377 378
        logger: logger,
      );
379
      xcdevice.isInstalled = false;
380 381

      await iosDevices.startPolling();
382
      expect(xcdevice.getAvailableIOSDevicesCount, 0);
383 384
    });

385 386 387
    testWithoutContext('start polling', () async {
      final IOSDevices iosDevices = IOSDevices(
        platform: macPlatform,
388 389
        xcdevice: xcdevice,
        iosWorkflow: iosWorkflow,
390 391
        logger: logger,
      );
392 393 394 395
      xcdevice.isInstalled = true;
      xcdevice.devices
        ..add(<IOSDevice>[])
        ..add(<IOSDevice>[device1, device2]);
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414

      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();
      });

      await iosDevices.startPolling();
415
      expect(xcdevice.getAvailableIOSDevicesCount, 1);
416 417

      expect(iosDevices.deviceNotifier.items, isEmpty);
418
      expect(xcdevice.deviceEventController.hasListener, isTrue);
419

420
      xcdevice.deviceEventController.add(<XCDeviceEvent, String>{
421 422 423 424 425 426 427
        XCDeviceEvent.attach: 'd83d5bc53967baa0ee18626ba87b6254b2ab5418'
      });
      await added.future;
      expect(iosDevices.deviceNotifier.items.length, 2);
      expect(iosDevices.deviceNotifier.items, contains(device1));
      expect(iosDevices.deviceNotifier.items, contains(device2));

428
      xcdevice.deviceEventController.add(<XCDeviceEvent, String>{
429 430 431 432 433 434 435
        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.
436
      xcdevice.deviceEventController.add(<XCDeviceEvent, String>{
437 438 439 440 441 442 443
        XCDeviceEvent.detach: 'bogus'
      });

      expect(addedCount, 2);

      await iosDevices.stopPolling();

444
      expect(xcdevice.deviceEventController.hasListener, isFalse);
445 446 447 448 449
    });

    testWithoutContext('polling can be restarted if stream is closed', () async {
      final IOSDevices iosDevices = IOSDevices(
        platform: macPlatform,
450 451
        xcdevice: xcdevice,
        iosWorkflow: iosWorkflow,
452 453
        logger: logger,
      );
454 455 456
      xcdevice.isInstalled = true;
      xcdevice.devices.add(<IOSDevice>[]);
      xcdevice.devices.add(<IOSDevice>[]);
457 458 459

      final StreamController<Map<XCDeviceEvent, String>> rescheduledStream = StreamController<Map<XCDeviceEvent, String>>();

460 461 462
      unawaited(xcdevice.deviceEventController.done.whenComplete(() {
        xcdevice.deviceEventController = rescheduledStream;
      }));
463 464

      await iosDevices.startPolling();
465 466
      expect(xcdevice.deviceEventController.hasListener, isTrue);
      expect(xcdevice.getAvailableIOSDevicesCount, 1);
467 468

      // Pretend xcdevice crashed.
469
      await xcdevice.deviceEventController.close();
470 471 472 473 474 475 476 477 478
      expect(logger.traceText, contains('xcdevice observe stopped'));

      // Confirm a restart still gets streamed events.
      await iosDevices.startPolling();

      expect(rescheduledStream.hasListener, isTrue);

      await iosDevices.stopPolling();
      expect(rescheduledStream.hasListener, isFalse);
479 480
    });

481 482 483
    testWithoutContext('dispose cancels polling subscription', () async {
      final IOSDevices iosDevices = IOSDevices(
        platform: macPlatform,
484 485
        xcdevice: xcdevice,
        iosWorkflow: iosWorkflow,
486 487
        logger: logger,
      );
488 489
      xcdevice.isInstalled = true;
      xcdevice.devices.add(<IOSDevice>[]);
490 491 492

      await iosDevices.startPolling();
      expect(iosDevices.deviceNotifier.items, isEmpty);
493
      expect(xcdevice.deviceEventController.hasListener, isTrue);
494

495
      iosDevices.dispose();
496
      expect(xcdevice.deviceEventController.hasListener, isFalse);
497 498
    });

499
    final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform];
500
    for (final Platform unsupportedPlatform in unsupportedPlatforms) {
501
      testWithoutContext('pollingGetDevices throws Unsupported Operation exception on ${unsupportedPlatform.operatingSystem}', () async {
502 503
        final IOSDevices iosDevices = IOSDevices(
          platform: unsupportedPlatform,
504 505
          xcdevice: xcdevice,
          iosWorkflow: iosWorkflow,
506
          logger: logger,
507
        );
508
        xcdevice.isInstalled = false;
509
        expect(
510 511
          () async { await iosDevices.pollingGetDevices(); },
          throwsUnsupportedError,
512 513 514 515
        );
      });
    }

516
    testWithoutContext('pollingGetDevices returns attached devices', () async {
517 518
      final IOSDevices iosDevices = IOSDevices(
        platform: macPlatform,
519 520
        xcdevice: xcdevice,
        iosWorkflow: iosWorkflow,
521
        logger: logger,
522
      );
523 524
      xcdevice.isInstalled = true;
      xcdevice.devices.add(<IOSDevice>[device1]);
525

526
      final List<Device> devices = await iosDevices.pollingGetDevices();
527

528
      expect(devices, hasLength(1));
529
      expect(devices.first, same(device1));
530
    });
531
  });
532

533
  group('getDiagnostics', () {
534 535
    FakeXcdevice xcdevice;
    IOSWorkflow iosWorkflow;
536
    Logger logger;
537 538

    setUp(() {
539 540
      xcdevice = FakeXcdevice();
      iosWorkflow = FakeIOSWorkflow();
541
      logger = BufferLogger.test();
542 543 544 545 546
    });

    final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform];
    for (final Platform unsupportedPlatform in unsupportedPlatforms) {
      testWithoutContext('throws returns platform diagnostic exception on ${unsupportedPlatform.operatingSystem}', () async {
547 548
        final IOSDevices iosDevices = IOSDevices(
          platform: unsupportedPlatform,
549 550
          xcdevice: xcdevice,
          iosWorkflow: iosWorkflow,
551
          logger: logger,
552
        );
553
        xcdevice.isInstalled = false;
554
        expect((await iosDevices.getDiagnostics()).first, 'Control of iOS devices or simulators only supported on macOS.');
555 556 557
      });
    }

558 559 560
    testWithoutContext('returns diagnostics', () async {
      final IOSDevices iosDevices = IOSDevices(
        platform: macPlatform,
561 562
        xcdevice: xcdevice,
        iosWorkflow: iosWorkflow,
563
        logger: logger,
564
      );
565 566
      xcdevice.isInstalled = true;
      xcdevice.diagnostics.add('Generic pairing error');
567

568
      final List<String> diagnostics = await iosDevices.getDiagnostics();
569 570
      expect(diagnostics, hasLength(1));
      expect(diagnostics.first, 'Generic pairing error');
571
    });
572
  });
573
}
574

575 576 577 578 579 580 581
class FakeIOSApp extends Fake implements IOSApp {
  FakeIOSApp(this.name);

  @override
  final String name;
}

582
class FakeIOSWorkflow extends Fake implements IOSWorkflow { }
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617

class FakeXcdevice extends Fake implements XCDevice {
  int getAvailableIOSDevicesCount = 0;
  final List<List<IOSDevice>> devices = <List<IOSDevice>>[];
  final List<String> diagnostics = <String>[];
  StreamController<Map<XCDeviceEvent, String>> deviceEventController = StreamController<Map<XCDeviceEvent, String>>();

  @override
  bool isInstalled = true;

  @override
  Future<List<String>> getDiagnostics() async {
    return diagnostics;
  }

  @override
  Stream<Map<XCDeviceEvent, String>> observedDeviceEvents() {
    return deviceEventController.stream;
  }

  @override
  Future<List<IOSDevice>> getAvailableIOSDevices({Duration timeout}) async {
    return devices[getAvailableIOSDevicesCount++];
  }
}

class FakeProcess extends Fake implements Process {
  bool killed = false;

  @override
  bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
    killed = true;
    return true;
  }
}