devices_test.dart 35.2 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 425 426 427 428 429 430 431 432 433 434 435 436 437 438
      testUsingContext(' succeeds with --cache-sksl', () async {
        final IOSDevice device = IOSDevice('123');
        device.setLogReader(mockApp, mockLogReader);
        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));

        List<String> args;
        when(mockIosDeploy.runApp(
          deviceId: anyNamed('deviceId'),
          bundlePath: anyNamed('bundlePath'),
          launchArguments: anyNamed('launchArguments'),
        )).thenAnswer((Invocation inv) {
          args = inv.namedArguments[const Symbol('launchArguments')];
          return Future<int>.value(0);
        });

        final LaunchResult launchResult = await device.startApp(mockApp,
          prebuiltApplication: true,
          debuggingOptions: DebuggingOptions.enabled(
              const BuildInfo(BuildMode.debug, null),
              cacheSkSL: true,
          ),
          platformArgs: <String, dynamic>{},
        );
        expect(launchResult.started, isTrue);
        expect(args, contains('--cache-sksl'));
        expect(await device.stopApp(mockApp), isFalse);
      }, overrides: <Type, Generator>{
        Artifacts: () => mockArtifacts,
        Cache: () => mockCache,
        FileSystem: () => mockFileSystem,
        MDnsObservatoryDiscovery: () => mockMDnsObservatoryDiscovery,
        Platform: () => macPlatform,
        ProcessManager: () => mockProcessManager,
        Usage: () => mockUsage,
        IOSDeploy: () => mockIosDeploy,
      });

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
      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, '', ''));
          });

469 470 471 472 473 474 475 476 477 478 479 480 481 482 483
          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''',
                '',
          )));

484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
          // 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,
          ]);

502 503
          final IOSApp app = await AbsoluteBuildableIOSApp.fromProject(
            FlutterProject.fromDirectory(projectDir).ios);
504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537
          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);
538 539
    });

540 541 542 543 544 545 546 547 548
    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>(
549 550
        'DYLD_LIBRARY_PATH',
        '/path/to/libraries',
551 552 553 554
      );
      final Map<String, String> env = Map<String, String>.fromEntries(
          <MapEntry<String, String>>[libraryEntry]
      );
555

556 557 558 559 560 561 562 563 564 565 566
      setUp(() {
        mockApp = MockIOSApp();
        mockArtifacts = MockArtifacts();
        mockCache = MockCache();
        when(mockCache.dyLdLibEntry).thenReturn(libraryEntry);
        mockFileSystem = MockFileSystem();
        mockProcessManager = MockProcessManager();
        when(
            mockArtifacts.getArtifactPath(
                Artifact.ideviceinstaller,
                platform: anyNamed('platform'),
567
            ),
568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 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 618 619 620 621 622 623 624 625
        ).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,
      });
626 627
    });
  });
628

629
  group('getAttachedDevices', () {
630
    MockIMobileDevice mockIMobileDevice;
631 632

    setUp(() {
633
      mockIMobileDevice = MockIMobileDevice();
634 635
    });

636
    testUsingContext('return no devices if Xcode is not installed', () async {
637
      when(mockIMobileDevice.isInstalled).thenReturn(false);
638
      expect(await IOSDevice.getAttachedDevices(), isEmpty);
639
    }, overrides: <Type, Generator>{
640
      IMobileDevice: () => mockIMobileDevice,
641
      Platform: () => macPlatform,
642 643 644
    });

    testUsingContext('returns no devices if none are attached', () async {
645
      when(iMobileDevice.isInstalled).thenReturn(true);
646
      when(iMobileDevice.getAvailableDeviceIDs())
647
          .thenAnswer((Invocation invocation) => Future<String>.value(''));
648
      final List<IOSDevice> devices = await IOSDevice.getAttachedDevices();
649 650
      expect(devices, isEmpty);
    }, overrides: <Type, Generator>{
651
      IMobileDevice: () => mockIMobileDevice,
652
      Platform: () => macPlatform,
653 654
    });

655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670
    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,
      });
    }

671
    testUsingContext('returns attached devices', () async {
672
      when(iMobileDevice.isInstalled).thenReturn(true);
673
      when(iMobileDevice.getAvailableDeviceIDs())
674
          .thenAnswer((Invocation invocation) => Future<String>.value('''
675 676
98206e7a4afd4aedaff06e687594e089dede3c44
f577a7903cc54959be2e34bc4f7f80b7009efcf4
677
'''));
678
      when(iMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'DeviceName'))
679
          .thenAnswer((_) => Future<String>.value('La tele me regarde'));
680
      when(iMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'ProductVersion'))
681
          .thenAnswer((_) => Future<String>.value('10.3.2'));
682
      when(iMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'DeviceName'))
683
          .thenAnswer((_) => Future<String>.value('Puits sans fond'));
684
      when(iMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'ProductVersion'))
685
          .thenAnswer((_) => Future<String>.value('11.0'));
686
      final List<IOSDevice> devices = await IOSDevice.getAttachedDevices();
687 688 689 690 691 692
      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>{
693
      IMobileDevice: () => mockIMobileDevice,
694
      Platform: () => macPlatform,
695
    });
696 697 698 699 700 701 702 703 704 705 706

    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'))
707
          .thenThrow(const IOSDeviceNotFoundError('Device not found'));
708 709 710 711 712 713
      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,
714
      Platform: () => macPlatform,
715
    });
716 717
  });

718 719 720 721 722 723 724 725 726 727 728
  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!');
    });
  });
729 730
  group('logging', () {
    MockIMobileDevice mockIMobileDevice;
731
    MockIosProject mockIosProject;
732 733

    setUp(() {
734 735
      mockIMobileDevice = MockIMobileDevice();
      mockIosProject = MockIosProject();
736 737
    });

738
    testUsingContext('suppresses non-Flutter lines from output', () async {
739
      when(mockIMobileDevice.startLogger('123456')).thenAnswer((Invocation invocation) {
740 741 742 743 744 745 746 747 748
        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])
        );
749
        return Future<Process>.value(mockProcess);
750 751
      });

752
      final IOSDevice device = IOSDevice('123456');
753
      final DeviceLogReader logReader = device.getLogReader(
754
        app: await BuildableIOSApp.fromProject(mockIosProject),
755 756 757 758 759 760
      );

      final List<String> lines = await logReader.logLines.toList();
      expect(lines, <String>['A is for ari', 'I is for ichigo']);
    }, overrides: <Type, Generator>{
      IMobileDevice: () => mockIMobileDevice,
761
      Platform: () => macPlatform,
762
    });
763
    testUsingContext('includes multi-line Flutter logs in the output', () async {
764
      when(mockIMobileDevice.startLogger('123456')).thenAnswer((Invocation invocation) {
765 766 767
        final Process mockProcess = MockProcess(
          stdout: Stream<List<int>>.fromIterable(<List<int>>['''
Runner(Flutter)[297] <Notice>: This is a multi-line message,
768
  with another Flutter message following it.
769
Runner(Flutter)[297] <Notice>: This is a multi-line message,
770
  with a non-Flutter log message following it.
771 772 773
Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
'''.codeUnits]),
        );
774
        return Future<Process>.value(mockProcess);
775 776
      });

777
      final IOSDevice device = IOSDevice('123456');
778
      final DeviceLogReader logReader = device.getLogReader(
779
        app: await BuildableIOSApp.fromProject(mockIosProject),
780 781 782 783 784 785 786 787 788
      );

      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.',
      ]);
789
      expect(device.category, Category.mobile);
790 791
    }, overrides: <Type, Generator>{
      IMobileDevice: () => mockIMobileDevice,
792
      Platform: () => macPlatform,
793
    });
794
  });
795 796 797 798 799 800 801 802 803 804
  testUsingContext('IOSDevice.isSupportedForProject is true on module project', () async {
    fs.file('pubspec.yaml')
      ..createSync()
      ..writeAsStringSync(r'''
name: example

flutter:
  module: {}
''');
    fs.file('.packages').createSync();
805
    final FlutterProject flutterProject = FlutterProject.current();
806 807 808 809

    expect(IOSDevice('test').isSupportedForProject(flutterProject), true);
  }, overrides: <Type, Generator>{
    FileSystem: () => MemoryFileSystem(),
810
    ProcessManager: () => FakeProcessManager(<FakeCommand>[]),
811
    Platform: () => macPlatform,
812 813 814 815 816
  });
  testUsingContext('IOSDevice.isSupportedForProject is true with editable host app', () async {
    fs.file('pubspec.yaml').createSync();
    fs.file('.packages').createSync();
    fs.directory('ios').createSync();
817
    final FlutterProject flutterProject = FlutterProject.current();
818 819 820 821

    expect(IOSDevice('test').isSupportedForProject(flutterProject), true);
  }, overrides: <Type, Generator>{
    FileSystem: () => MemoryFileSystem(),
822
    ProcessManager: () => FakeProcessManager(<FakeCommand>[]),
823
    Platform: () => macPlatform,
824 825 826 827 828
  });

  testUsingContext('IOSDevice.isSupportedForProject is false with no host app and no module', () async {
    fs.file('pubspec.yaml').createSync();
    fs.file('.packages').createSync();
829
    final FlutterProject flutterProject = FlutterProject.current();
830 831 832 833

    expect(IOSDevice('test').isSupportedForProject(flutterProject), false);
  }, overrides: <Type, Generator>{
    FileSystem: () => MemoryFileSystem(),
834
    ProcessManager: () => FakeProcessManager(<FakeCommand>[]),
835
    Platform: () => macPlatform,
836
  });
837
}
838 839

class AbsoluteBuildableIOSApp extends BuildableIOSApp {
840 841 842 843 844 845 846
  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);
  }
847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863

  @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>[];
864
      if (iosWorkflow.appliesToHostPlatform) {
865
        _workflows.add(iosWorkflow);
866
      }
867 868 869 870
    }
    return _workflows;
  }
}