devices_test.dart 20.6 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
import 'dart:async';
6
import 'dart:io' as io;
7

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

30
import '../../src/common.dart';
31
import '../../src/fake_process_manager.dart';
32

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

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

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

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

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

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

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

168 169 170
    testWithoutContext('Supports debug, profile, and release modes', () {
      final IOSDevice device = IOSDevice(
        'device-123',
171
        iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
172
        fileSystem: fileSystem,
173 174 175 176 177 178 179
        logger: logger,
        platform: macPlatform,
        iosDeploy: iosDeploy,
        iMobileDevice: iMobileDevice,
        name: 'iPhone 1',
        sdkVersion: '13.3',
        cpuArchitecture: DarwinArch.arm64,
180
        interfaceType: IOSDeviceConnectionInterface.usb,
181 182 183 184 185 186 187 188
      );

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

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

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

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

      IOSDeviceLogReader createLogReader(
          IOSDevice device,
250
          IOSApp appPackage,
251
          Process process) {
252 253 254
        final IOSDeviceLogReader logReader = IOSDeviceLogReader.create(
          device: device,
          app: appPackage,
255
          iMobileDevice: IMobileDevice.test(processManager: FakeProcessManager.any()),
256
        );
257 258 259 260 261
        logReader.idevicesyslogProcess = process;
        return logReader;
      }

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

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

303
        await device.dispose();
304

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

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

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

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

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

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

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

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

      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();
416
      expect(xcdevice.getAvailableIOSDevicesCount, 1);
417

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

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

429
      xcdevice.deviceEventController.add(<XCDeviceEvent, String>{
430 431 432
        XCDeviceEvent.detach: 'd83d5bc53967baa0ee18626ba87b6254b2ab5418'
      });
      await removed.future;
433
      expect(iosDevices.deviceNotifier!.items, <Device>[device2]);
434 435 436

      // Remove stream will throw over-completion if called more than once
      // which proves this is ignored.
437
      xcdevice.deviceEventController.add(<XCDeviceEvent, String>{
438 439 440 441 442 443 444
        XCDeviceEvent.detach: 'bogus'
      });

      expect(addedCount, 2);

      await iosDevices.stopPolling();

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

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

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

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

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

      // Pretend xcdevice crashed.
470
      await xcdevice.deviceEventController.close();
471 472 473 474 475 476 477 478 479
      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);
480 481
    });

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

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

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

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

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

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

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

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

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

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

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

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

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

  @override
  final String name;
}

583
class FakeIOSWorkflow extends Fake implements IOSWorkflow { }
584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604

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
605
  Future<List<IOSDevice>> getAvailableIOSDevices({Duration? timeout}) async {
606 607 608 609 610 611 612 613 614 615 616 617 618
    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;
  }
}