devices_test.dart 33.4 KB
Newer Older
1 2 3 4
// Copyright 2017 The Chromium Authors. All rights reserved.
// 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:convert';
7

8
import 'package:args/command_runner.dart';
9
import 'package:file/file.dart';
10
import 'package:file/memory.dart';
11
import 'package:flutter_tools/src/application_package.dart';
12
import 'package:flutter_tools/src/artifacts.dart';
13
import 'package:flutter_tools/src/base/file_system.dart';
14
import 'package:flutter_tools/src/base/io.dart';
15
import 'package:flutter_tools/src/build_info.dart';
16
import 'package:flutter_tools/src/cache.dart';
17
import 'package:flutter_tools/src/commands/create.dart';
18
import 'package:flutter_tools/src/device.dart';
19
import 'package:flutter_tools/src/doctor.dart';
20
import 'package:flutter_tools/src/ios/devices.dart';
21
import 'package:flutter_tools/src/ios/mac.dart';
22
import 'package:flutter_tools/src/ios/ios_workflow.dart';
23
import 'package:flutter_tools/src/macos/xcode.dart';
24
import 'package:flutter_tools/src/mdns_discovery.dart';
25
import 'package:flutter_tools/src/project.dart';
26
import 'package:flutter_tools/src/reporting/reporting.dart';
27
import 'package:meta/meta.dart';
28
import 'package:mockito/mockito.dart';
29
import 'package:platform/platform.dart';
30
import 'package:process/process.dart';
31
import 'package:quiver/testing/async.dart';
32

33 34 35
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/mocks.dart';
36

37
class MockIOSApp extends Mock implements IOSApp {}
38
class MockApplicationPackage extends Mock implements ApplicationPackage {}
39 40 41 42
class MockArtifacts extends Mock implements Artifacts {}
class MockCache extends Mock implements Cache {}
class MockDirectory extends Mock implements Directory {}
class MockFileSystem extends Mock implements FileSystem {}
43
class MockForwardedPort extends Mock implements ForwardedPort {}
44
class MockIMobileDevice extends Mock implements IMobileDevice {}
45
class MockIOSDeploy extends Mock implements IOSDeploy {}
46
class MockDevicePortForwarder extends Mock implements DevicePortForwarder {}
47
class MockMDnsObservatoryDiscovery extends Mock implements MDnsObservatoryDiscovery {}
48
class MockMDnsObservatoryDiscoveryResult extends Mock implements MDnsObservatoryDiscoveryResult {}
49
class MockXcode extends Mock implements Xcode {}
50
class MockFile extends Mock implements File {}
51
class MockPortForwarder extends Mock implements DevicePortForwarder {}
52
class MockUsage extends Mock implements Usage {}
53 54

void main() {
55 56
  final FakePlatform macPlatform = FakePlatform.fromPlatform(const LocalPlatform());
  macPlatform.operatingSystem = 'macos';
57 58 59 60
  final FakePlatform linuxPlatform = FakePlatform.fromPlatform(const LocalPlatform());
  linuxPlatform.operatingSystem = 'linux';
  final FakePlatform windowsPlatform = FakePlatform.fromPlatform(const LocalPlatform());
  windowsPlatform.operatingSystem = 'windows';
61

62 63
  group('IOSDevice', () {
    final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform];
64

65 66
    testUsingContext('successfully instantiates on Mac OS', () {
      IOSDevice('device-123');
67 68 69 70
    }, overrides: <Type, Generator>{
      Platform: () => macPlatform,
    });

71 72 73
    for (Platform platform in unsupportedPlatforms) {
      testUsingContext('throws UnsupportedError exception if instantiated on ${platform.operatingSystem}', () {
        expect(
74 75
          () { IOSDevice('device-123'); },
          throwsA(isInstanceOf<AssertionError>()),
76
        );
77 78 79 80 81
      }, overrides: <Type, Generator>{
        Platform: () => platform,
      });
    }

82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
    group('.dispose()', () {
      IOSDevice device;
      MockApplicationPackage appPackage1;
      MockApplicationPackage appPackage2;
      IOSDeviceLogReader logReader1;
      IOSDeviceLogReader logReader2;
      MockProcess mockProcess1;
      MockProcess mockProcess2;
      MockProcess mockProcess3;
      IOSDevicePortForwarder portForwarder;
      ForwardedPort forwardedPort;

      IOSDevicePortForwarder createPortForwarder(
          ForwardedPort forwardedPort,
          IOSDevice device) {
        final IOSDevicePortForwarder portForwarder = IOSDevicePortForwarder(device);
        portForwarder.addForwardedPorts(<ForwardedPort>[forwardedPort]);
        return portForwarder;
      }

      IOSDeviceLogReader createLogReader(
          IOSDevice device,
          ApplicationPackage appPackage,
          Process process) {
        final IOSDeviceLogReader logReader = IOSDeviceLogReader(device, appPackage);
        logReader.idevicesyslogProcess = process;
        return logReader;
      }

      setUp(() {
        appPackage1 = MockApplicationPackage();
        appPackage2 = MockApplicationPackage();
        when(appPackage1.name).thenReturn('flutterApp1');
        when(appPackage2.name).thenReturn('flutterApp2');
        mockProcess1 = MockProcess();
        mockProcess2 = MockProcess();
        mockProcess3 = MockProcess();
        forwardedPort = ForwardedPort.withContext(123, 456, mockProcess3);
      });

      testUsingContext(' kills all log readers & port forwarders', () async {
        device = IOSDevice('123');
        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;

        device.dispose();

        verify(mockProcess1.kill());
        verify(mockProcess2.kill());
        verify(mockProcess3.kill());
      }, overrides: <Type, Generator>{
        Platform: () => macPlatform,
      });
    });

141 142 143 144 145 146 147
    group('startApp', () {
      MockIOSApp mockApp;
      MockArtifacts mockArtifacts;
      MockCache mockCache;
      MockFileSystem mockFileSystem;
      MockProcessManager mockProcessManager;
      MockDeviceLogReader mockLogReader;
148
      MockMDnsObservatoryDiscovery mockMDnsObservatoryDiscovery;
149
      MockPortForwarder mockPortForwarder;
150 151
      MockIMobileDevice mockIMobileDevice;
      MockIOSDeploy mockIosDeploy;
152
      MockUsage mockUsage;
153 154 155

      Directory tempDir;
      Directory projectDir;
156 157 158 159 160

      const int devicePort = 499;
      const int hostPort = 42;
      const String installerPath = '/path/to/ideviceinstaller';
      const String iosDeployPath = '/path/to/iosdeploy';
161
      const String iproxyPath = '/path/to/iproxy';
162 163
      const MapEntry<String, String> libraryEntry = MapEntry<String, String>(
          'DYLD_LIBRARY_PATH',
164
          '/path/to/libraries',
165 166 167 168 169 170
      );
      final Map<String, String> env = Map<String, String>.fromEntries(
          <MapEntry<String, String>>[libraryEntry]
      );

      setUp(() {
171 172
        Cache.disableLocking();

173 174 175 176 177
        mockApp = MockIOSApp();
        mockArtifacts = MockArtifacts();
        mockCache = MockCache();
        when(mockCache.dyLdLibEntry).thenReturn(libraryEntry);
        mockFileSystem = MockFileSystem();
178
        mockMDnsObservatoryDiscovery = MockMDnsObservatoryDiscovery();
179 180 181
        mockProcessManager = MockProcessManager();
        mockLogReader = MockDeviceLogReader();
        mockPortForwarder = MockPortForwarder();
182 183
        mockIMobileDevice = MockIMobileDevice();
        mockIosDeploy = MockIOSDeploy();
184
        mockUsage = MockUsage();
185 186 187

        tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_create_test.');
        projectDir = tempDir.childDirectory('flutter_project');
188 189 190 191 192

        when(
            mockArtifacts.getArtifactPath(
                Artifact.ideviceinstaller,
                platform: anyNamed('platform'),
193
            ),
194 195 196 197 198 199
        ).thenReturn(installerPath);

        when(
            mockArtifacts.getArtifactPath(
                Artifact.iosDeploy,
                platform: anyNamed('platform'),
200
            ),
201 202
        ).thenReturn(iosDeployPath);

203 204 205 206 207 208 209
        when(
            mockArtifacts.getArtifactPath(
                Artifact.iproxy,
                platform: anyNamed('platform'),
            ),
        ).thenReturn(iproxyPath);

210 211 212 213 214 215 216 217 218 219 220 221 222
        when(mockPortForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
          .thenAnswer((_) async => hostPort);
        when(mockPortForwarder.forwardedPorts)
          .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
        when(mockPortForwarder.unforward(any))
          .thenAnswer((_) async => null);

        const String bundlePath = '/path/to/bundle';
        final List<String> installArgs = <String>[installerPath, '-i', bundlePath];
        when(mockApp.deviceBundlePath).thenReturn(bundlePath);
        final MockDirectory directory = MockDirectory();
        when(mockFileSystem.directory(bundlePath)).thenReturn(directory);
        when(directory.existsSync()).thenReturn(true);
223 224 225
        when(mockProcessManager.run(
          installArgs,
          workingDirectory: anyNamed('workingDirectory'),
226
          environment: env,
227 228 229
        )).thenAnswer(
          (_) => Future<ProcessResult>.value(ProcessResult(1, 0, '', ''))
        );
230 231 232

        when(mockIMobileDevice.getInfoForDevice(any, 'CPUArchitecture'))
            .thenAnswer((_) => Future<String>.value('arm64'));
233 234 235 236
      });

      tearDown(() {
        mockLogReader.dispose();
237 238 239
        tryToDelete(tempDir);

        Cache.enableLocking();
240 241
      });

242
      testUsingContext(' succeeds in debug mode via mDNS', () async {
243 244 245
        final IOSDevice device = IOSDevice('123');
        device.portForwarder = mockPortForwarder;
        device.setLogReader(mockApp, mockLogReader);
246 247 248 249 250 251 252 253
        final Uri uri = Uri(
          scheme: 'http',
          host: '127.0.0.1',
          port: 1234,
          path: 'observatory',
        );
        when(mockMDnsObservatoryDiscovery.getObservatoryUri(any, any, any))
          .thenAnswer((Invocation invocation) => Future<Uri>.value(uri));
254 255 256 257 258 259

        final LaunchResult launchResult = await device.startApp(mockApp,
          prebuiltApplication: true,
          debuggingOptions: DebuggingOptions.enabled(const BuildInfo(BuildMode.debug, null)),
          platformArgs: <String, dynamic>{},
        );
260
        verify(mockUsage.sendEvent('ios-mdns', 'success')).called(1);
261 262 263 264 265 266 267
        expect(launchResult.started, isTrue);
        expect(launchResult.hasObservatory, isTrue);
        expect(await device.stopApp(mockApp), isFalse);
      }, overrides: <Type, Generator>{
        Artifacts: () => mockArtifacts,
        Cache: () => mockCache,
        FileSystem: () => mockFileSystem,
268
        MDnsObservatoryDiscovery: () => mockMDnsObservatoryDiscovery,
269 270
        Platform: () => macPlatform,
        ProcessManager: () => mockProcessManager,
271
        Usage: () => mockUsage,
272 273
      });

274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
      // By default, the .forward() method will try every port between 1024
      // and 65535; this test verifies we are killing iproxy processes when
      // we timeout on a port
      testUsingContext(' .forward() will kill iproxy processes before invoking a second', () async {
        const String deviceId = '123';
        const int devicePort = 456;
        final IOSDevice device = IOSDevice(deviceId);
        final IOSDevicePortForwarder portForwarder = IOSDevicePortForwarder(device);
        bool firstRun = true;
        final MockProcess successProcess = MockProcess(
          exitCode: Future<int>.value(0),
          stdout: Stream<List<int>>.fromIterable(<List<int>>['Hello'.codeUnits]),
        );
        final MockProcess failProcess = MockProcess(
          exitCode: Future<int>.value(1),
          stdout: const Stream<List<int>>.empty(),
        );

        final ProcessFactory factory = (List<String> command) {
          if (!firstRun) {
            return successProcess;
          }
          firstRun = false;
          return failProcess;
        };
        mockProcessManager.processFactory = factory;
        final int hostPort = await portForwarder.forward(devicePort);
        // First port tried (1024) should fail, then succeed on the next
        expect(hostPort, 1024 + 1);
        verifyNever(successProcess.kill());
        verify(failProcess.kill());
      }, overrides: <Type, Generator>{
        Artifacts: () => mockArtifacts,
        Cache: () => mockCache,
        Platform: () => macPlatform,
        ProcessManager: () => mockProcessManager,
        Usage: () => mockUsage,
      });

313
      testUsingContext(' succeeds in debug mode when mDNS fails by falling back to manual protocol discovery', () async {
314
        final IOSDevice device = IOSDevice('123');
315 316 317 318 319 320 321 322 323 324
        device.portForwarder = mockPortForwarder;
        device.setLogReader(mockApp, mockLogReader);
        // Now that the reader is used, start writing messages to it.
        Timer.run(() {
          mockLogReader.addLine('Foo');
          mockLogReader.addLine('Observatory listening on http://127.0.0.1:$devicePort');
        });
        when(mockMDnsObservatoryDiscovery.getObservatoryUri(any, any, any))
          .thenAnswer((Invocation invocation) => Future<Uri>.value(null));

325 326
        final LaunchResult launchResult = await device.startApp(mockApp,
          prebuiltApplication: true,
327
          debuggingOptions: DebuggingOptions.enabled(const BuildInfo(BuildMode.debug, null)),
328 329
          platformArgs: <String, dynamic>{},
        );
330
        verify(mockUsage.sendEvent('ios-mdns', 'failure')).called(1);
331
        verify(mockUsage.sendEvent('ios-mdns', 'fallback-success')).called(1);
332
        expect(launchResult.started, isTrue);
333
        expect(launchResult.hasObservatory, isTrue);
334 335 336 337 338
        expect(await device.stopApp(mockApp), isFalse);
      }, overrides: <Type, Generator>{
        Artifacts: () => mockArtifacts,
        Cache: () => mockCache,
        FileSystem: () => mockFileSystem,
339
        MDnsObservatoryDiscovery: () => mockMDnsObservatoryDiscovery,
340 341
        Platform: () => macPlatform,
        ProcessManager: () => mockProcessManager,
342
        Usage: () => mockUsage,
343 344
      });

345
      testUsingContext(' fails in debug mode when mDNS fails and when Observatory URI is malformed', () async {
346 347 348 349 350 351 352 353 354
        final IOSDevice device = IOSDevice('123');
        device.portForwarder = mockPortForwarder;
        device.setLogReader(mockApp, mockLogReader);

        // Now that the reader is used, start writing messages to it.
        Timer.run(() {
          mockLogReader.addLine('Foo');
          mockLogReader.addLine('Observatory listening on http:/:/127.0.0.1:$devicePort');
        });
355 356
        when(mockMDnsObservatoryDiscovery.getObservatoryUri(any, any, any))
          .thenAnswer((Invocation invocation) => Future<Uri>.value(null));
357 358 359 360 361 362

        final LaunchResult launchResult = await device.startApp(mockApp,
            prebuiltApplication: true,
            debuggingOptions: DebuggingOptions.enabled(const BuildInfo(BuildMode.debug, null)),
            platformArgs: <String, dynamic>{},
        );
363 364
        verify(mockUsage.sendEvent('ios-mdns', 'failure')).called(1);
        verify(mockUsage.sendEvent('ios-mdns', 'fallback-failure')).called(1);
365 366
        expect(launchResult.started, isFalse);
        expect(launchResult.hasObservatory, isFalse);
367 368 369 370 371 372 373
      }, overrides: <Type, Generator>{
        Artifacts: () => mockArtifacts,
        Cache: () => mockCache,
        FileSystem: () => mockFileSystem,
        MDnsObservatoryDiscovery: () => mockMDnsObservatoryDiscovery,
        Platform: () => macPlatform,
        ProcessManager: () => mockProcessManager,
374
        Usage: () => mockUsage,
375 376 377 378 379 380 381 382 383 384 385 386
      });

      testUsingContext(' succeeds in release mode', () async {
        final IOSDevice device = IOSDevice('123');
        final LaunchResult launchResult = await device.startApp(mockApp,
          prebuiltApplication: true,
          debuggingOptions: DebuggingOptions.disabled(const BuildInfo(BuildMode.release, null)),
          platformArgs: <String, dynamic>{},
        );
        expect(launchResult.started, isTrue);
        expect(launchResult.hasObservatory, isFalse);
        expect(await device.stopApp(mockApp), isFalse);
387 388 389 390 391 392 393
      }, overrides: <Type, Generator>{
        Artifacts: () => mockArtifacts,
        Cache: () => mockCache,
        FileSystem: () => mockFileSystem,
        Platform: () => macPlatform,
        ProcessManager: () => mockProcessManager,
      });
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

      void testNonPrebuilt({
        @required bool showBuildSettingsFlakes,
      }) {
        const String name = ' non-prebuilt succeeds in debug mode';
        testUsingContext(name + ' flaky: $showBuildSettingsFlakes', () async {
          final Directory targetBuildDir =
              projectDir.childDirectory('build/ios/iphoneos/Debug-arm64');

          // The -showBuildSettings calls have a timeout and so go through
          // processManager.start().
          mockProcessManager.processFactory = flakyProcessFactory(
            flakes: showBuildSettingsFlakes ? 1 : 0,
            delay: const Duration(seconds: 62),
            filter: (List<String> args) => args.contains('-showBuildSettings'),
            stdout:
                () => Stream<String>
                  .fromIterable(
                      <String>['TARGET_BUILD_DIR = ${targetBuildDir.path}\n'])
                  .transform(utf8.encoder),
          );

          // Make all other subcommands succeed.
          when(mockProcessManager.run(
              any,
              workingDirectory: anyNamed('workingDirectory'),
              environment: anyNamed('environment'),
          )).thenAnswer((Invocation inv) {
            return Future<ProcessResult>.value(ProcessResult(0, 0, '', ''));
          });

425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
          when(mockProcessManager.run(
            argThat(contains('find-identity')),
            environment: anyNamed('environment'),
            workingDirectory: anyNamed('workingDirectory'),
          )).thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(
                1, // pid
                0, // exitCode
                '''
    1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)"
    2) da4b9237bacccdf19c0760cab7aec4a8359010b0 "iPhone Developer: Profile 2 (2222BBBB22)"
    3) 5bf1fd927dfb8679496a2e6cf00cbe50c1c87145 "iPhone Developer: Profile 3 (3333CCCC33)"
        3 valid identities found''',
                '',
          )));

440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457
          // Deploy works.
          when(mockIosDeploy.runApp(
            deviceId: anyNamed('deviceId'),
            bundlePath: anyNamed('bundlePath'),
            launchArguments: anyNamed('launchArguments'),
          )).thenAnswer((_) => Future<int>.value(0));

          // Create a dummy project to avoid mocking out the whole directory
          // structure expected by device.startApp().
          Cache.flutterRoot = '../..';
          final CreateCommand command = CreateCommand();
          final CommandRunner<void> runner = createTestCommandRunner(command);
          await runner.run(<String>[
            'create',
            '--no-pub',
            projectDir.path,
          ]);

458 459
          final IOSApp app = await AbsoluteBuildableIOSApp.fromProject(
            FlutterProject.fromDirectory(projectDir).ios);
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493
          final IOSDevice device = IOSDevice('123');

          // Pre-create the expected build products.
          targetBuildDir.createSync(recursive: true);
          projectDir.childDirectory('build/ios/iphoneos/Runner.app').createSync(recursive: true);

          final Completer<LaunchResult> completer = Completer<LaunchResult>();
          FakeAsync().run((FakeAsync time) {
            device.startApp(
              app,
              prebuiltApplication: false,
              debuggingOptions: DebuggingOptions.disabled(const BuildInfo(BuildMode.debug, null)),
              platformArgs: <String, dynamic>{},
            ).then((LaunchResult result) {
              completer.complete(result);
            });
            time.flushMicrotasks();
            time.elapse(const Duration(seconds: 65));
          });
          final LaunchResult launchResult = await completer.future;
          expect(launchResult.started, isTrue);
          expect(launchResult.hasObservatory, isFalse);
          expect(await device.stopApp(mockApp), isFalse);
        }, overrides: <Type, Generator>{
          DoctorValidatorsProvider: () => FakeIosDoctorProvider(),
          IMobileDevice: () => mockIMobileDevice,
          IOSDeploy: () => mockIosDeploy,
          Platform: () => macPlatform,
          ProcessManager: () => mockProcessManager,
        });
      }

      testNonPrebuilt(showBuildSettingsFlakes: false);
      testNonPrebuilt(showBuildSettingsFlakes: true);
494 495
    });

496 497 498 499 500 501 502 503 504
    group('Process calls', () {
      MockIOSApp mockApp;
      MockArtifacts mockArtifacts;
      MockCache mockCache;
      MockFileSystem mockFileSystem;
      MockProcessManager mockProcessManager;
      const String installerPath = '/path/to/ideviceinstaller';
      const String appId = '789';
      const MapEntry<String, String> libraryEntry = MapEntry<String, String>(
505 506
        'DYLD_LIBRARY_PATH',
        '/path/to/libraries',
507 508 509 510
      );
      final Map<String, String> env = Map<String, String>.fromEntries(
          <MapEntry<String, String>>[libraryEntry]
      );
511

512 513 514 515 516 517 518 519 520 521 522
      setUp(() {
        mockApp = MockIOSApp();
        mockArtifacts = MockArtifacts();
        mockCache = MockCache();
        when(mockCache.dyLdLibEntry).thenReturn(libraryEntry);
        mockFileSystem = MockFileSystem();
        mockProcessManager = MockProcessManager();
        when(
            mockArtifacts.getArtifactPath(
                Artifact.ideviceinstaller,
                platform: anyNamed('platform'),
523
            ),
524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581
        ).thenReturn(installerPath);
      });

      testUsingContext('installApp() invokes process with correct environment', () async {
        final IOSDevice device = IOSDevice('123');
        const String bundlePath = '/path/to/bundle';
        final List<String> args = <String>[installerPath, '-i', bundlePath];
        when(mockApp.deviceBundlePath).thenReturn(bundlePath);
        final MockDirectory directory = MockDirectory();
        when(mockFileSystem.directory(bundlePath)).thenReturn(directory);
        when(directory.existsSync()).thenReturn(true);
        when(mockProcessManager.run(args, environment: env))
            .thenAnswer(
                (_) => Future<ProcessResult>.value(ProcessResult(1, 0, '', ''))
            );
        await device.installApp(mockApp);
        verify(mockProcessManager.run(args, environment: env));
      }, overrides: <Type, Generator>{
        Artifacts: () => mockArtifacts,
        Cache: () => mockCache,
        FileSystem: () => mockFileSystem,
        Platform: () => macPlatform,
        ProcessManager: () => mockProcessManager,
      });

      testUsingContext('isAppInstalled() invokes process with correct environment', () async {
        final IOSDevice device = IOSDevice('123');
        final List<String> args = <String>[installerPath, '--list-apps'];
        when(mockProcessManager.run(args, environment: env))
            .thenAnswer(
                (_) => Future<ProcessResult>.value(ProcessResult(1, 0, '', ''))
            );
        when(mockApp.id).thenReturn(appId);
        await device.isAppInstalled(mockApp);
        verify(mockProcessManager.run(args, environment: env));
      }, overrides: <Type, Generator>{
        Artifacts: () => mockArtifacts,
        Cache: () => mockCache,
        Platform: () => macPlatform,
        ProcessManager: () => mockProcessManager,
      });

      testUsingContext('uninstallApp() invokes process with correct environment', () async {
        final IOSDevice device = IOSDevice('123');
        final List<String> args = <String>[installerPath, '-U', appId];
        when(mockApp.id).thenReturn(appId);
        when(mockProcessManager.run(args, environment: env))
            .thenAnswer(
                (_) => Future<ProcessResult>.value(ProcessResult(1, 0, '', ''))
            );
        await device.uninstallApp(mockApp);
        verify(mockProcessManager.run(args, environment: env));
      }, overrides: <Type, Generator>{
        Artifacts: () => mockArtifacts,
        Cache: () => mockCache,
        Platform: () => macPlatform,
        ProcessManager: () => mockProcessManager,
      });
582 583
    });
  });
584

585
  group('getAttachedDevices', () {
586
    MockIMobileDevice mockIMobileDevice;
587 588

    setUp(() {
589
      mockIMobileDevice = MockIMobileDevice();
590 591
    });

592
    testUsingContext('return no devices if Xcode is not installed', () async {
593
      when(mockIMobileDevice.isInstalled).thenReturn(false);
594
      expect(await IOSDevice.getAttachedDevices(), isEmpty);
595
    }, overrides: <Type, Generator>{
596
      IMobileDevice: () => mockIMobileDevice,
597
      Platform: () => macPlatform,
598 599 600
    });

    testUsingContext('returns no devices if none are attached', () async {
601
      when(iMobileDevice.isInstalled).thenReturn(true);
602
      when(iMobileDevice.getAvailableDeviceIDs())
603
          .thenAnswer((Invocation invocation) => Future<String>.value(''));
604
      final List<IOSDevice> devices = await IOSDevice.getAttachedDevices();
605 606
      expect(devices, isEmpty);
    }, overrides: <Type, Generator>{
607
      IMobileDevice: () => mockIMobileDevice,
608
      Platform: () => macPlatform,
609 610
    });

611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626
    final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform];
    for (Platform platform in unsupportedPlatforms) {
      testUsingContext('throws Unsupported Operation exception on ${platform.operatingSystem}', () async {
        when(iMobileDevice.isInstalled).thenReturn(false);
        when(iMobileDevice.getAvailableDeviceIDs())
            .thenAnswer((Invocation invocation) => Future<String>.value(''));
        expect(
            () async { await IOSDevice.getAttachedDevices(); },
            throwsA(isInstanceOf<UnsupportedError>()),
        );
      }, overrides: <Type, Generator>{
        IMobileDevice: () => mockIMobileDevice,
        Platform: () => platform,
      });
    }

627
    testUsingContext('returns attached devices', () async {
628
      when(iMobileDevice.isInstalled).thenReturn(true);
629
      when(iMobileDevice.getAvailableDeviceIDs())
630
          .thenAnswer((Invocation invocation) => Future<String>.value('''
631 632
98206e7a4afd4aedaff06e687594e089dede3c44
f577a7903cc54959be2e34bc4f7f80b7009efcf4
633
'''));
634
      when(iMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'DeviceName'))
635
          .thenAnswer((_) => Future<String>.value('La tele me regarde'));
636
      when(iMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'ProductVersion'))
637
          .thenAnswer((_) => Future<String>.value('10.3.2'));
638
      when(iMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'DeviceName'))
639
          .thenAnswer((_) => Future<String>.value('Puits sans fond'));
640
      when(iMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'ProductVersion'))
641
          .thenAnswer((_) => Future<String>.value('11.0'));
642
      final List<IOSDevice> devices = await IOSDevice.getAttachedDevices();
643 644 645 646 647 648
      expect(devices, hasLength(2));
      expect(devices[0].id, '98206e7a4afd4aedaff06e687594e089dede3c44');
      expect(devices[0].name, 'La tele me regarde');
      expect(devices[1].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4');
      expect(devices[1].name, 'Puits sans fond');
    }, overrides: <Type, Generator>{
649
      IMobileDevice: () => mockIMobileDevice,
650
      Platform: () => macPlatform,
651
    });
652 653 654 655 656 657 658 659 660 661 662

    testUsingContext('returns attached devices and ignores devices that cannot be found by ideviceinfo', () async {
      when(iMobileDevice.isInstalled).thenReturn(true);
      when(iMobileDevice.getAvailableDeviceIDs())
          .thenAnswer((Invocation invocation) => Future<String>.value('''
98206e7a4afd4aedaff06e687594e089dede3c44
f577a7903cc54959be2e34bc4f7f80b7009efcf4
'''));
      when(iMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'DeviceName'))
          .thenAnswer((_) => Future<String>.value('La tele me regarde'));
      when(iMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'DeviceName'))
663
          .thenThrow(const IOSDeviceNotFoundError('Device not found'));
664 665 666 667 668 669
      final List<IOSDevice> devices = await IOSDevice.getAttachedDevices();
      expect(devices, hasLength(1));
      expect(devices[0].id, '98206e7a4afd4aedaff06e687594e089dede3c44');
      expect(devices[0].name, 'La tele me regarde');
    }, overrides: <Type, Generator>{
      IMobileDevice: () => mockIMobileDevice,
670
      Platform: () => macPlatform,
671
    });
672 673
  });

674 675 676 677 678 679 680 681 682 683 684
  group('decodeSyslog', () {
    test('decodes a syslog-encoded line', () {
      final String decoded = decodeSyslog(r'I \M-b\M^]\M-$\M-o\M-8\M^O syslog \M-B\M-/\134_(\M-c\M^C\M^D)_/\M-B\M-/ \M-l\M^F\240!');
      expect(decoded, r'I ❤️ syslog ¯\_(ツ)_/¯ 솠!');
    });

    test('passes through un-decodeable lines as-is', () {
      final String decoded = decodeSyslog(r'I \M-b\M^O syslog!');
      expect(decoded, r'I \M-b\M^O syslog!');
    });
  });
685 686
  group('logging', () {
    MockIMobileDevice mockIMobileDevice;
687
    MockIosProject mockIosProject;
688 689

    setUp(() {
690 691
      mockIMobileDevice = MockIMobileDevice();
      mockIosProject = MockIosProject();
692 693
    });

694
    testUsingContext('suppresses non-Flutter lines from output', () async {
695
      when(mockIMobileDevice.startLogger('123456')).thenAnswer((Invocation invocation) {
696 697 698 699 700 701 702 703 704
        final Process mockProcess = MockProcess(
          stdout: Stream<List<int>>.fromIterable(<List<int>>['''
Runner(Flutter)[297] <Notice>: A is for ari
Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt MobileGestaltSupport.m:153: pid 123 (Runner) does not have sandbox access for frZQaeyWLUvLjeuEK43hmg and IS NOT appropriately entitled
Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt MobileGestalt.c:550: no access to InverseDeviceID (see <rdar://problem/11744455>)
Runner(Flutter)[297] <Notice>: I is for ichigo
Runner(UIKit)[297] <Notice>: E is for enpitsu"
'''.codeUnits])
        );
705
        return Future<Process>.value(mockProcess);
706 707
      });

708
      final IOSDevice device = IOSDevice('123456');
709
      final DeviceLogReader logReader = device.getLogReader(
710
        app: await BuildableIOSApp.fromProject(mockIosProject),
711 712 713 714 715 716
      );

      final List<String> lines = await logReader.logLines.toList();
      expect(lines, <String>['A is for ari', 'I is for ichigo']);
    }, overrides: <Type, Generator>{
      IMobileDevice: () => mockIMobileDevice,
717
      Platform: () => macPlatform,
718
    });
719
    testUsingContext('includes multi-line Flutter logs in the output', () async {
720
      when(mockIMobileDevice.startLogger('123456')).thenAnswer((Invocation invocation) {
721 722 723
        final Process mockProcess = MockProcess(
          stdout: Stream<List<int>>.fromIterable(<List<int>>['''
Runner(Flutter)[297] <Notice>: This is a multi-line message,
724
  with another Flutter message following it.
725
Runner(Flutter)[297] <Notice>: This is a multi-line message,
726
  with a non-Flutter log message following it.
727 728 729
Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
'''.codeUnits]),
        );
730
        return Future<Process>.value(mockProcess);
731 732
      });

733
      final IOSDevice device = IOSDevice('123456');
734
      final DeviceLogReader logReader = device.getLogReader(
735
        app: await BuildableIOSApp.fromProject(mockIosProject),
736 737 738 739 740 741 742 743 744
      );

      final List<String> lines = await logReader.logLines.toList();
      expect(lines, <String>[
        'This is a multi-line message,',
        '  with another Flutter message following it.',
        'This is a multi-line message,',
        '  with a non-Flutter log message following it.',
      ]);
745
      expect(device.category, Category.mobile);
746 747
    }, overrides: <Type, Generator>{
      IMobileDevice: () => mockIMobileDevice,
748
      Platform: () => macPlatform,
749
    });
750
  });
751 752 753 754 755 756 757 758 759 760
  testUsingContext('IOSDevice.isSupportedForProject is true on module project', () async {
    fs.file('pubspec.yaml')
      ..createSync()
      ..writeAsStringSync(r'''
name: example

flutter:
  module: {}
''');
    fs.file('.packages').createSync();
761
    final FlutterProject flutterProject = FlutterProject.current();
762 763 764 765

    expect(IOSDevice('test').isSupportedForProject(flutterProject), true);
  }, overrides: <Type, Generator>{
    FileSystem: () => MemoryFileSystem(),
766
    Platform: () => macPlatform,
767 768 769 770 771
  });
  testUsingContext('IOSDevice.isSupportedForProject is true with editable host app', () async {
    fs.file('pubspec.yaml').createSync();
    fs.file('.packages').createSync();
    fs.directory('ios').createSync();
772
    final FlutterProject flutterProject = FlutterProject.current();
773 774 775 776

    expect(IOSDevice('test').isSupportedForProject(flutterProject), true);
  }, overrides: <Type, Generator>{
    FileSystem: () => MemoryFileSystem(),
777
    Platform: () => macPlatform,
778 779 780 781 782
  });

  testUsingContext('IOSDevice.isSupportedForProject is false with no host app and no module', () async {
    fs.file('pubspec.yaml').createSync();
    fs.file('.packages').createSync();
783
    final FlutterProject flutterProject = FlutterProject.current();
784 785 786 787

    expect(IOSDevice('test').isSupportedForProject(flutterProject), false);
  }, overrides: <Type, Generator>{
    FileSystem: () => MemoryFileSystem(),
788
    Platform: () => macPlatform,
789
  });
790
}
791 792

class AbsoluteBuildableIOSApp extends BuildableIOSApp {
793 794 795 796 797 798 799
  AbsoluteBuildableIOSApp(IosProject project, String projectBundleId) :
    super(project, projectBundleId);

  static Future<AbsoluteBuildableIOSApp> fromProject(IosProject project) async {
    final String projectBundleId = await project.productBundleIdentifier;
    return AbsoluteBuildableIOSApp(project, projectBundleId);
  }
800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816

  @override
  String get deviceBundlePath =>
      fs.path.join(project.parent.directory.path, 'build', 'ios', 'iphoneos', name);

}

class FakeIosDoctorProvider implements DoctorValidatorsProvider {
  List<Workflow> _workflows;

  @override
  List<DoctorValidator> get validators => <DoctorValidator>[];

  @override
  List<Workflow> get workflows {
    if (_workflows == null) {
      _workflows = <Workflow>[];
817
      if (iosWorkflow.appliesToHostPlatform) {
818
        _workflows.add(iosWorkflow);
819
      }
820 821 822 823
    }
    return _workflows;
  }
}