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

5 6
// @dart = 2.8

7
import 'package:file/memory.dart';
8
import 'package:flutter_tools/src/base/file_system.dart';
9 10
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
11
import 'package:flutter_tools/src/base/platform.dart';
12
import 'package:flutter_tools/src/base/process.dart';
13
import 'package:flutter_tools/src/build_info.dart';
14
import 'package:flutter_tools/src/devfs.dart';
15
import 'package:flutter_tools/src/device.dart';
16
import 'package:flutter_tools/src/device_port_forwarder.dart';
17
import 'package:flutter_tools/src/globals_null_migrated.dart' as globals;
18
import 'package:flutter_tools/src/ios/application_package.dart';
19
import 'package:flutter_tools/src/ios/plist_parser.dart';
20
import 'package:flutter_tools/src/ios/simulators.dart';
21
import 'package:flutter_tools/src/macos/xcode.dart';
22
import 'package:flutter_tools/src/project.dart';
23
import 'package:test/fake.dart';
24

25 26
import '../../src/common.dart';
import '../../src/context.dart';
27
import '../../src/fakes.dart';
28

29 30 31 32 33 34 35
final Platform macosPlatform = FakePlatform(
  operatingSystem: 'macos',
  environment: <String, String>{
    'HOME': '/'
  },
);

36
void main() {
37
  FakePlatform osx;
38 39
  FileSystemUtils fsUtils;
  MemoryFileSystem fileSystem;
40 41

  setUp(() {
42 43 44 45
    osx = FakePlatform(
      environment: <String, String>{},
      operatingSystem: 'macos',
    );
46
    fileSystem = MemoryFileSystem.test();
47
    fsUtils = FileSystemUtils(fileSystem: fileSystem, platform: osx);
48 49
  });

50
  group('_IOSSimulatorDevicePortForwarder', () {
51
    FakeSimControl simControl;
52
    Xcode xcode;
53 54

    setUp(() {
55
      simControl = FakeSimControl();
56
      xcode = Xcode.test(processManager: FakeProcessManager.any());
57 58
    });

59
    testUsingContext('dispose() does not throw an exception', () async {
60 61
      final IOSSimulator simulator = IOSSimulator(
        '123',
62
        simControl: simControl,
63
      );
64 65 66 67 68 69
      final DevicePortForwarder portForwarder = simulator.portForwarder;
      await portForwarder.forward(123);
      await portForwarder.forward(124);
      expect(portForwarder.forwardedPorts.length, 2);
      try {
        await portForwarder.dispose();
70
      } on Exception catch (e) {
71 72 73 74 75
        fail('Encountered exception: $e');
      }
      expect(portForwarder.forwardedPorts.length, 0);
    }, overrides: <Type, Generator>{
      Platform: () => osx,
76 77
      FileSystem: () => fileSystem,
      ProcessManager: () => FakeProcessManager.any(),
78
      Xcode: () => xcode,
79 80 81
    }, testOn: 'posix');
  });

82 83 84
  testUsingContext('simulators only support debug mode', () async {
    final IOSSimulator simulator = IOSSimulator(
      '123',
85
      simControl: FakeSimControl(),
86 87 88 89 90 91 92 93 94 95 96 97
    );

    expect(simulator.supportsRuntimeMode(BuildMode.debug), true);
    expect(simulator.supportsRuntimeMode(BuildMode.profile), false);
    expect(simulator.supportsRuntimeMode(BuildMode.release), false);
    expect(simulator.supportsRuntimeMode(BuildMode.jitRelease), false);
  }, overrides: <Type, Generator>{
    Platform: () => osx,
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.any(),
  });

98
  group('logFilePath', () {
99
    FakeSimControl simControl;
100 101

    setUp(() {
102
      simControl = FakeSimControl();
103 104
    });

105 106
    testUsingContext('defaults to rooted from HOME', () {
      osx.environment['HOME'] = '/foo/bar';
107 108
      final IOSSimulator simulator = IOSSimulator(
        '123',
109
        simControl: simControl,
110 111
      );
      expect(simulator.logFilePath, '/foo/bar/Library/Logs/CoreSimulator/123/system.log');
112 113
    }, overrides: <Type, Generator>{
      Platform: () => osx,
114 115 116
      FileSystemUtils: () => fsUtils,
      FileSystem: () => fileSystem,
      ProcessManager: () => FakeProcessManager.any(),
117 118 119 120 121
    }, testOn: 'posix');

    testUsingContext('respects IOS_SIMULATOR_LOG_FILE_PATH', () {
      osx.environment['HOME'] = '/foo/bar';
      osx.environment['IOS_SIMULATOR_LOG_FILE_PATH'] = '/baz/qux/%{id}/system.log';
122 123
      final IOSSimulator simulator = IOSSimulator(
        '456',
124
        simControl: simControl,
125 126
      );
      expect(simulator.logFilePath, '/baz/qux/456/system.log');
127 128
    }, overrides: <Type, Generator>{
      Platform: () => osx,
129 130 131
      FileSystemUtils: () => fsUtils,
      FileSystem: () => fileSystem,
      ProcessManager: () => FakeProcessManager.any(),
132 133
    });
  });
134

135
  group('compareIosVersions', () {
136
    testWithoutContext('compares correctly', () {
137
      // This list must be sorted in ascending preference order
138
      final List<String> testList = <String>[
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
        '8', '8.0', '8.1', '8.2',
        '9', '9.0', '9.1', '9.2',
        '10', '10.0', '10.1',
      ];

      for (int i = 0; i < testList.length; i++) {
        expect(compareIosVersions(testList[i], testList[i]), 0);
      }

      for (int i = 0; i < testList.length - 1; i++) {
        for (int j = i + 1; j < testList.length; j++) {
          expect(compareIosVersions(testList[i], testList[j]), lessThan(0));
          expect(compareIosVersions(testList[j], testList[i]), greaterThan(0));
        }
      }
    });
  });

  group('compareIphoneVersions', () {
158
    testWithoutContext('compares correctly', () {
159
      // This list must be sorted in ascending preference order
160
      final List<String> testList = <String>[
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
        'com.apple.CoreSimulator.SimDeviceType.iPhone-4s',
        'com.apple.CoreSimulator.SimDeviceType.iPhone-5',
        'com.apple.CoreSimulator.SimDeviceType.iPhone-5s',
        'com.apple.CoreSimulator.SimDeviceType.iPhone-6strange',
        'com.apple.CoreSimulator.SimDeviceType.iPhone-6-Plus',
        'com.apple.CoreSimulator.SimDeviceType.iPhone-6',
        'com.apple.CoreSimulator.SimDeviceType.iPhone-6s-Plus',
        'com.apple.CoreSimulator.SimDeviceType.iPhone-6s',
      ];

      for (int i = 0; i < testList.length; i++) {
        expect(compareIphoneVersions(testList[i], testList[i]), 0);
      }

      for (int i = 0; i < testList.length - 1; i++) {
        for (int j = i + 1; j < testList.length; j++) {
          expect(compareIphoneVersions(testList[i], testList[j]), lessThan(0));
          expect(compareIphoneVersions(testList[j], testList[i]), greaterThan(0));
        }
      }
    });
  });
183

184
  group('sdkMajorVersion', () {
185
    FakeSimControl simControl;
186 187

    setUp(() {
188
      simControl = FakeSimControl();
189 190
    });

191
    // This new version string appears in SimulatorApp-850 CoreSimulator-518.16 beta.
192 193 194 195 196
    testWithoutContext('can be parsed from iOS-11-3', () async {
      final IOSSimulator device = IOSSimulator(
        'x',
        name: 'iPhone SE',
        simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3',
197
        simControl: simControl,
198
      );
199 200 201 202

      expect(await device.sdkMajorVersion, 11);
    });

203 204 205 206 207
    testWithoutContext('can be parsed from iOS 11.2', () async {
      final IOSSimulator device = IOSSimulator(
        'x',
        name: 'iPhone SE',
        simulatorCategory: 'iOS 11.2',
208
        simControl: simControl,
209
      );
210 211 212

      expect(await device.sdkMajorVersion, 11);
    });
213

214 215 216 217 218
    testWithoutContext('Has a simulator category', () async {
      final IOSSimulator device = IOSSimulator(
        'x',
        name: 'iPhone SE',
        simulatorCategory: 'iOS 11.2',
219
        simControl: simControl,
220
      );
221 222 223

      expect(device.category, Category.mobile);
    });
224 225
  });

226
  group('IOSSimulator.isSupported', () {
227
    FakeSimControl simControl;
228 229

    setUp(() {
230
      simControl = FakeSimControl();
231 232
    });

233
    testUsingContext('Apple TV is unsupported', () {
234 235 236
      final IOSSimulator simulator = IOSSimulator(
        'x',
        name: 'Apple TV',
237
        simControl: simControl,
238 239
      );
      expect(simulator.isSupported(), false);
240 241
    }, overrides: <Type, Generator>{
      Platform: () => osx,
242 243
      FileSystem: () => fileSystem,
      ProcessManager: () => FakeProcessManager.any(),
244 245
    });

246
    testUsingContext('Apple Watch is unsupported', () {
247 248 249
      expect(IOSSimulator(
        'x',
        name: 'Apple Watch',
250
        simControl: simControl,
251
      ).isSupported(), false);
252 253
    }, overrides: <Type, Generator>{
      Platform: () => osx,
254 255
      FileSystem: () => fileSystem,
      ProcessManager: () => FakeProcessManager.any(),
256 257
    });

258
    testUsingContext('iPad 2 is supported', () {
259 260 261
      expect(IOSSimulator(
        'x',
        name: 'iPad 2',
262
        simControl: simControl,
263
      ).isSupported(), true);
264 265
    }, overrides: <Type, Generator>{
      Platform: () => osx,
266 267
      FileSystem: () => fileSystem,
      ProcessManager: () => FakeProcessManager.any(),
268 269
    });

270
    testUsingContext('iPad Retina is supported', () {
271 272 273
      expect(IOSSimulator(
        'x',
        name: 'iPad Retina',
274
        simControl: simControl,
275
      ).isSupported(), true);
276 277
    }, overrides: <Type, Generator>{
      Platform: () => osx,
278 279
      FileSystem: () => fileSystem,
      ProcessManager: () => FakeProcessManager.any(),
280 281
    });

282
    testUsingContext('iPhone 5 is supported', () {
283 284 285
      expect(IOSSimulator(
        'x',
        name: 'iPhone 5',
286
        simControl: simControl,
287
      ).isSupported(), true);
288 289
    }, overrides: <Type, Generator>{
      Platform: () => osx,
290 291
      FileSystem: () => fileSystem,
      ProcessManager: () => FakeProcessManager.any(),
292 293
    });

294
    testUsingContext('iPhone 5s is supported', () {
295 296 297
      expect(IOSSimulator(
        'x',
        name: 'iPhone 5s',
298
        simControl: simControl,
299
      ).isSupported(), true);
300 301
    }, overrides: <Type, Generator>{
      Platform: () => osx,
302 303
      FileSystem: () => fileSystem,
      ProcessManager: () => FakeProcessManager.any(),
304 305
    });

306
    testUsingContext('iPhone SE is supported', () {
307 308 309
      expect(IOSSimulator(
        'x',
        name: 'iPhone SE',
310
        simControl: simControl,
311
      ).isSupported(), true);
312 313
    }, overrides: <Type, Generator>{
      Platform: () => osx,
314 315
      FileSystem: () => fileSystem,
      ProcessManager: () => FakeProcessManager.any(),
316 317
    });

318
    testUsingContext('iPhone 7 Plus is supported', () {
319 320 321
      expect(IOSSimulator(
        'x',
        name: 'iPhone 7 Plus',
322
        simControl: simControl,
323
      ).isSupported(), true);
324 325
    }, overrides: <Type, Generator>{
      Platform: () => osx,
326 327
      FileSystem: () => fileSystem,
      ProcessManager: () => FakeProcessManager.any(),
328
    });
329 330

    testUsingContext('iPhone X is supported', () {
331 332 333
      expect(IOSSimulator(
        'x',
        name: 'iPhone X',
334
        simControl: simControl,
335
      ).isSupported(), true);
336 337
    }, overrides: <Type, Generator>{
      Platform: () => osx,
338 339
      FileSystem: () => fileSystem,
      ProcessManager: () => FakeProcessManager.any(),
340
    });
341
  });
342 343

  group('Simulator screenshot', () {
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
    testWithoutContext('supports screenshots', () async {
      final Xcode xcode = Xcode.test(processManager: FakeProcessManager.any());
      final Logger logger = BufferLogger.test();
      final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
        const FakeCommand(
          command: <String>[
            'xcrun',
            'simctl',
            'io',
            'x',
            'screenshot',
            'screenshot.png',
          ],
        ),
      ]);
359

360
      // Test a real one. Screenshot doesn't require instance states.
361
      final SimControl simControl = SimControl(
362 363 364
        processManager: fakeProcessManager,
        logger: logger,
        xcode: xcode,
365
      );
366
      // Doesn't matter what the device is.
367
      final IOSSimulator deviceUnderTest = IOSSimulator(
368 369 370 371
        'x',
        name: 'iPhone SE',
        simControl: simControl,
      );
372

373 374 375 376
      final File screenshot = MemoryFileSystem.test().file('screenshot.png');
      await deviceUnderTest.takeScreenshot(screenshot);
      expect(fakeProcessManager.hasRemainingExpectations, isFalse);
    });
377
  });
378

379
  group('device log tool', () {
380
    FakeProcessManager fakeProcessManager;
381
    FakeSimControl simControl;
382 383

    setUp(() {
384
      fakeProcessManager = FakeProcessManager.empty();
385
      simControl = FakeSimControl();
386 387
    });

388
    testUsingContext('syslog uses tail', () async {
389 390 391 392
      final IOSSimulator device = IOSSimulator(
        'x',
        name: 'iPhone SE',
        simulatorCategory: 'iOS 9.3',
393
        simControl: simControl,
394
      );
395 396 397 398 399 400 401
      fakeProcessManager.addCommand(const FakeCommand(command: <String>[
        'tail',
        '-n',
        '0',
        '-F',
        '/Library/Logs/CoreSimulator/x/system.log',
      ]));
402
      await launchDeviceSystemLogTool(device);
403
      expect(fakeProcessManager.hasRemainingExpectations, isFalse);
404 405
    },
    overrides: <Type, Generator>{
406
      ProcessManager: () => fakeProcessManager,
407
      FileSystem: () => fileSystem,
408 409 410 411
      Platform: () => macosPlatform,
      FileSystemUtils: () => FileSystemUtils(
        fileSystem: fileSystem,
        platform: macosPlatform,
412
      ),
413 414
    });

415
    testUsingContext('unified logging with app name', () async {
416 417 418 419
      final IOSSimulator device = IOSSimulator(
        'x',
        name: 'iPhone SE',
        simulatorCategory: 'iOS 11.0',
420
        simControl: simControl,
421
      );
422
      const String expectedPredicate = 'eventType = logEvent AND '
423 424 425 426 427 428
          'processImagePath ENDSWITH "My Super Awesome App" AND '
          '(senderImagePath ENDSWITH "/Flutter" OR senderImagePath ENDSWITH "/libswiftCore.dylib" OR processImageUUID == senderImageUUID) AND '
          'NOT(eventMessage CONTAINS ": could not find icon for representation -> com.apple.") AND '
          'NOT(eventMessage BEGINSWITH "assertion failed: ") AND '
          'NOT(eventMessage CONTAINS " libxpc.dylib ")';
      fakeProcessManager.addCommand(const FakeCommand(command: <String>[
429
        'xcrun',
430 431
        'simctl',
        'spawn',
432
        'x',
433 434 435 436 437
        'log',
        'stream',
        '--style',
        'json',
        '--predicate',
438 439 440 441 442
        expectedPredicate,
      ]));

      await launchDeviceUnifiedLogging(device, 'My Super Awesome App');
      expect(fakeProcessManager.hasRemainingExpectations, isFalse);
443
    },
444
      overrides: <Type, Generator>{
445
      ProcessManager: () => fakeProcessManager,
446
      FileSystem: () => fileSystem,
447 448
    });

449
    testUsingContext('unified logging without app name', () async {
450 451 452 453
      final IOSSimulator device = IOSSimulator(
        'x',
        name: 'iPhone SE',
        simulatorCategory: 'iOS 11.0',
454
        simControl: simControl,
455
      );
456
      const String expectedPredicate = 'eventType = logEvent AND '
457 458 459 460 461
          '(senderImagePath ENDSWITH "/Flutter" OR senderImagePath ENDSWITH "/libswiftCore.dylib" OR processImageUUID == senderImageUUID) AND '
          'NOT(eventMessage CONTAINS ": could not find icon for representation -> com.apple.") AND '
          'NOT(eventMessage BEGINSWITH "assertion failed: ") AND '
          'NOT(eventMessage CONTAINS " libxpc.dylib ")';
      fakeProcessManager.addCommand(const FakeCommand(command: <String>[
462
        'xcrun',
463 464 465 466 467 468 469 470
        'simctl',
        'spawn',
        'x',
        'log',
        'stream',
        '--style',
        'json',
        '--predicate',
471 472 473 474 475
        expectedPredicate,
      ]));

      await launchDeviceUnifiedLogging(device, null);
      expect(fakeProcessManager.hasRemainingExpectations, isFalse);
476
    },
477
      overrides: <Type, Generator>{
478
        ProcessManager: () => fakeProcessManager,
479 480
        FileSystem: () => fileSystem,
      });
481
  });
482 483

  group('log reader', () {
484
    FakeProcessManager fakeProcessManager;
485
    FakeIosProject mockIosProject;
486
    FakeSimControl simControl;
487
    Xcode xcode;
488 489

    setUp(() {
490
      fakeProcessManager = FakeProcessManager.empty();
491
      mockIosProject = FakeIosProject();
492
      simControl = FakeSimControl();
493
      xcode = Xcode.test(processManager: FakeProcessManager.any());
494 495
    });

496 497 498 499 500 501
    group('syslog', () {
      setUp(() {
        final File syslog = fileSystem.file('system.log')..createSync();
        osx.environment['IOS_SIMULATOR_LOG_FILE_PATH'] = syslog.path;
      });

502 503 504 505
      testUsingContext('simulator can parse Xcode 8/iOS 10-style logs', () async {
        fakeProcessManager
          ..addCommand(const FakeCommand(
            command:  <String>['tail', '-n', '0', '-F', 'system.log'],
506 507 508
            stdout: '''
Dec 20 17:04:32 md32-11-vm1 My Super Awesome App[88374]: flutter: Observatory listening on http://127.0.0.1:64213/1Uoeu523990=/
Dec 20 17:04:32 md32-11-vm1 Another App[88374]: Ignore this text'''
509 510 511 512 513 514 515 516
          ))
          ..addCommand(const FakeCommand(
            command:  <String>['tail', '-n', '0', '-F', '/private/var/log/system.log']
          ));

        final IOSSimulator device = IOSSimulator(
          '123456',
          simulatorCategory: 'iOS 10.0',
517
          simControl: simControl,
518 519
        );
        final DeviceLogReader logReader = device.getLogReader(
520
          app: await BuildableIOSApp.fromProject(mockIosProject, null),
521 522 523 524 525 526 527 528 529 530 531
        );

        final List<String> lines = await logReader.logLines.toList();
        expect(lines, <String>[
          'flutter: Observatory listening on http://127.0.0.1:64213/1Uoeu523990=/',
        ]);
        expect(fakeProcessManager.hasRemainingExpectations, isFalse);
      }, overrides: <Type, Generator>{
        ProcessManager: () => fakeProcessManager,
        FileSystem: () => fileSystem,
        Platform: () => osx,
532
        Xcode: () => xcode,
533 534
      });

535
      testUsingContext('simulator can output `)`', () async {
536 537 538 539
        fakeProcessManager
          ..addCommand(const FakeCommand(
            command:  <String>['tail', '-n', '0', '-F', 'system.log'],
            stdout: '''
540 541 542
2017-09-13 15:26:57.228948-0700  localhost My Super Awesome App[37195]: (Flutter) Observatory listening on http://127.0.0.1:57701/
2017-09-13 15:26:57.228948-0700  localhost My Super Awesome App[37195]: (Flutter) ))))))))))
2017-09-13 15:26:57.228948-0700  localhost My Super Awesome App[37195]: (Flutter) #0      Object.noSuchMethod (dart:core-patch/dart:core/object_patch.dart:46)'''
543 544 545 546
          ))
          ..addCommand(const FakeCommand(
            command:  <String>['tail', '-n', '0', '-F', '/private/var/log/system.log']
          ));
547

548 549 550
        final IOSSimulator device = IOSSimulator(
          '123456',
          simulatorCategory: 'iOS 10.3',
551
          simControl: simControl,
552 553
        );
        final DeviceLogReader logReader = device.getLogReader(
554
          app: await BuildableIOSApp.fromProject(mockIosProject, null),
555 556 557 558 559 560 561 562
        );

        final List<String> lines = await logReader.logLines.toList();
        expect(lines, <String>[
          'Observatory listening on http://127.0.0.1:57701/',
          '))))))))))',
          '#0      Object.noSuchMethod (dart:core-patch/dart:core/object_patch.dart:46)',
        ]);
563
        expect(fakeProcessManager.hasRemainingExpectations, isFalse);
564
      }, overrides: <Type, Generator>{
565
        ProcessManager: () => fakeProcessManager,
566 567
        FileSystem: () => fileSystem,
        Platform: () => osx,
568
        Xcode: () => xcode,
569 570
      });

571 572 573 574 575
      testUsingContext('multiline messages', () async {
        fakeProcessManager
          ..addCommand(const FakeCommand(
            command:  <String>['tail', '-n', '0', '-F', 'system.log'],
            stdout: '''
576 577
2017-09-13 15:26:57.228948-0700  localhost My Super Awesome App[37195]: (Flutter) Single line message
2017-09-13 15:26:57.228948-0700  localhost My Super Awesome App[37195]: (Flutter) Multi line message
578 579
  continues...
  continues...
580
2020-03-11 15:58:28.207175-0700  localhost My Super Awesome App[72166]: (libnetwork.dylib) [com.apple.network:] [28 www.googleapis.com:443 stream, pid: 72166, tls] cancelled
581 582 583 584
	[28.1 64A98447-EABF-4983-A387-7DB9D0C1785F 10.0.1.200.57912<->172.217.6.74:443]
	Connected Path: satisfied (Path is satisfied), interface: en18
	Duration: 0.271s, DNS @0.000s took 0.001s, TCP @0.002s took 0.019s, TLS took 0.046s
	bytes in/out: 4468/1933, packets in/out: 11/10, rtt: 0.016s, retransmitted packets: 0, out-of-order packets: 0
585
2017-09-13 15:36:57.228948-0700  localhost My Super Awesome App[37195]: (Flutter) Multi line message again
586 587
  and it goes...
  and goes...
588
2017-09-13 15:36:57.228948-0700  localhost My Super Awesome App[37195]: (Flutter) Single line message, not the part of the above
589
'''
590 591 592 593
          ))
          ..addCommand(const FakeCommand(
            command:  <String>['tail', '-n', '0', '-F', '/private/var/log/system.log']
          ));
594

595 596 597
        final IOSSimulator device = IOSSimulator(
          '123456',
          simulatorCategory: 'iOS 10.3',
598
          simControl: simControl,
599 600
        );
        final DeviceLogReader logReader = device.getLogReader(
601
          app: await BuildableIOSApp.fromProject(mockIosProject, null),
602 603 604 605 606 607 608 609 610 611 612 613 614
        );

        final List<String> lines = await logReader.logLines.toList();
        expect(lines, <String>[
          'Single line message',
          'Multi line message',
          '  continues...',
          '  continues...',
          'Multi line message again',
          '  and it goes...',
          '  and goes...',
          'Single line message, not the part of the above'
        ]);
615
        expect(fakeProcessManager.hasRemainingExpectations, isFalse);
616
      }, overrides: <Type, Generator>{
617
        ProcessManager: () => fakeProcessManager,
618 619
        FileSystem: () => fileSystem,
        Platform: () => osx,
620
        Xcode: () => xcode,
621 622
      });
    });
623

624 625
    group('unified logging', () {
      testUsingContext('log reader handles escaped multiline messages', () async {
626
        const String logPredicate = 'eventType = logEvent AND processImagePath ENDSWITH "My Super Awesome App" '
627 628 629 630 631 632
          'AND (senderImagePath ENDSWITH "/Flutter" OR senderImagePath ENDSWITH "/libswiftCore.dylib" '
          'OR processImageUUID == senderImageUUID) AND NOT(eventMessage CONTAINS ": could not find icon '
          'for representation -> com.apple.") AND NOT(eventMessage BEGINSWITH "assertion failed: ") '
          'AND NOT(eventMessage CONTAINS " libxpc.dylib ")';
        fakeProcessManager.addCommand(const FakeCommand(
            command:  <String>[
633
              'xcrun',
634 635 636 637 638 639 640 641 642 643
              'simctl',
              'spawn',
              '123456',
              'log',
              'stream',
              '--style',
              'json',
              '--predicate',
              logPredicate,
            ],
644
            stdout: r'''
645 646 647 648 649 650
},{
  "traceID" : 37579774151491588,
  "eventMessage" : "Single line message",
  "eventType" : "logEvent"
},{
  "traceID" : 37579774151491588,
651
  "eventMessage" : "Multi line message\n  continues...\n  continues..."
652 653 654 655 656 657
},{
  "traceID" : 37579774151491588,
  "eventMessage" : "Single line message, not the part of the above",
  "eventType" : "logEvent"
},{
'''
658
          ));
659 660 661 662

        final IOSSimulator device = IOSSimulator(
          '123456',
          simulatorCategory: 'iOS 11.0',
663
          simControl: simControl,
664 665
        );
        final DeviceLogReader logReader = device.getLogReader(
666
          app: await BuildableIOSApp.fromProject(mockIosProject, null),
667 668 669 670 671 672 673
        );

        final List<String> lines = await logReader.logLines.toList();
        expect(lines, <String>[
          'Single line message', 'Multi line message\n  continues...\n  continues...',
          'Single line message, not the part of the above'
        ]);
674
        expect(fakeProcessManager.hasRemainingExpectations, isFalse);
675
      }, overrides: <Type, Generator>{
676
        ProcessManager: () => fakeProcessManager,
677 678
        FileSystem: () => fileSystem,
      });
679
    });
680
  });
681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704

  group('SimControl', () {
    const String validSimControlOutput = '''
{
  "devices" : {
    "watchOS 4.3" : [
      {
        "state" : "Shutdown",
        "availability" : "(available)",
        "name" : "Apple Watch - 38mm",
        "udid" : "TEST-WATCH-UDID"
      }
    ],
    "iOS 11.4" : [
      {
        "state" : "Booted",
        "availability" : "(available)",
        "name" : "iPhone 5s",
        "udid" : "TEST-PHONE-UDID"
      }
    ],
    "tvOS 11.4" : [
      {
        "state" : "Shutdown",
705
        "availability" : "(available)",
706 707 708 709 710 711 712 713
        "name" : "Apple TV",
        "udid" : "TEST-TV-UDID"
      }
    ]
  }
}
    ''';

714 715
    FakeProcessManager fakeProcessManager;
    Xcode xcode;
716
    SimControl simControl;
717 718
    const String deviceId = 'smart-phone';
    const String appId = 'flutterApp';
719 720

    setUp(() {
721
      fakeProcessManager = FakeProcessManager.empty();
722
      xcode = Xcode.test(processManager: FakeProcessManager.any());
723
      simControl = SimControl(
724 725 726
        logger: BufferLogger.test(),
        processManager: fakeProcessManager,
        xcode: xcode,
727
      );
728 729
    });

730
    testWithoutContext('getDevices succeeds', () async {
731 732 733 734 735 736 737 738 739 740 741
      fakeProcessManager.addCommand(const FakeCommand(
        command: <String>[
          'xcrun',
          'simctl',
          'list',
          '--json',
          'devices',
        ],
        stdout: validSimControlOutput,
      ));

742
      final List<SimDevice> devices = await simControl.getDevices();
743 744 745 746

      final SimDevice watch = devices[0];
      expect(watch.category, 'watchOS 4.3');
      expect(watch.state, 'Shutdown');
747
      expect(watch.availability, '(available)');
748 749 750 751 752 753 754
      expect(watch.name, 'Apple Watch - 38mm');
      expect(watch.udid, 'TEST-WATCH-UDID');
      expect(watch.isBooted, isFalse);

      final SimDevice phone = devices[1];
      expect(phone.category, 'iOS 11.4');
      expect(phone.state, 'Booted');
755
      expect(phone.availability, '(available)');
756 757 758 759
      expect(phone.name, 'iPhone 5s');
      expect(phone.udid, 'TEST-PHONE-UDID');
      expect(phone.isBooted, isTrue);

760
      final SimDevice tv = devices[2];
761 762
      expect(tv.category, 'tvOS 11.4');
      expect(tv.state, 'Shutdown');
763
      expect(tv.availability, '(available)');
764 765 766
      expect(tv.name, 'Apple TV');
      expect(tv.udid, 'TEST-TV-UDID');
      expect(tv.isBooted, isFalse);
767
      expect(fakeProcessManager.hasRemainingExpectations, isFalse);
768
    });
769

770
    testWithoutContext('getDevices handles bad simctl output', () async {
771 772 773 774 775 776 777 778 779 780 781
      fakeProcessManager.addCommand(const FakeCommand(
        command: <String>[
          'xcrun',
          'simctl',
          'list',
          '--json',
          'devices',
        ],
        stdout: 'Install Started',
      ));

782 783 784
      final List<SimDevice> devices = await simControl.getDevices();

      expect(devices, isEmpty);
785
      expect(fakeProcessManager.hasRemainingExpectations, isFalse);
786
    });
787

788 789 790 791 792 793 794
    testWithoutContext('sdkMajorVersion defaults to 11 when sdkNameAndVersion is junk', () async {
      final IOSSimulator iosSimulatorA = IOSSimulator(
        'x',
        name: 'Testo',
        simulatorCategory: 'NaN',
        simControl: simControl,
      );
795 796 797

      expect(await iosSimulatorA.sdkMajorVersion, 11);
    });
798 799

    testWithoutContext('.install() handles exceptions', () async {
800 801
      fakeProcessManager.addCommand(const FakeCommand(
        command: <String>[
802 803 804 805 806 807
          'xcrun',
          'simctl',
          'install',
          deviceId,
          appId,
        ],
808
        exception: ProcessException('xcrun', <String>[]),
809 810
      ));

811
      expect(
812
        () async => simControl.install(deviceId, appId),
813 814 815 816 817
        throwsToolExit(message: r'Unable to install'),
      );
    });

    testWithoutContext('.uninstall() handles exceptions', () async {
818 819
      fakeProcessManager.addCommand(const FakeCommand(
        command: <String>[
820 821 822 823 824 825
          'xcrun',
          'simctl',
          'uninstall',
          deviceId,
          appId,
        ],
826
        exception: ProcessException('xcrun', <String>[]),
827 828
      ));

829
      expect(
830
        () async => simControl.uninstall(deviceId, appId),
831 832 833 834 835
        throwsToolExit(message: r'Unable to uninstall'),
      );
    });

    testWithoutContext('.launch() handles exceptions', () async {
836 837
      fakeProcessManager.addCommand(const FakeCommand(
        command: <String>[
838 839 840 841 842 843
          'xcrun',
          'simctl',
          'launch',
          deviceId,
          appId,
        ],
844
        exception: ProcessException('xcrun', <String>[]),
845 846
      ));

847
      expect(
848
        () async => simControl.launch(deviceId, appId),
849 850 851
        throwsToolExit(message: r'Unable to launch'),
      );
    });
852
  });
853

854
  group('startApp', () {
855
    FakePlistParser testPlistParser;
856
    FakeSimControl simControl;
857
    Xcode xcode;
858 859

    setUp(() {
860
      simControl = FakeSimControl();
861
      xcode = Xcode.test(processManager: FakeProcessManager.any());
862
      testPlistParser = FakePlistParser();
863 864 865
    });

    testUsingContext("startApp uses compiled app's Info.plist to find CFBundleIdentifier", () async {
866 867 868 869 870 871
      final IOSSimulator device = IOSSimulator(
        'x',
        name: 'iPhone SE',
        simulatorCategory: 'iOS 11.2',
        simControl: simControl,
      );
872
      testPlistParser.setProperty('CFBundleIdentifier', 'correct');
873

874
      final Directory mockDir = globals.fs.currentDirectory;
875
      final IOSApp package = PrebuiltIOSApp(projectBundleId: 'incorrect', bundleName: 'name', bundleDir: mockDir);
876

877
      const BuildInfo mockInfo = BuildInfo(BuildMode.debug, 'flavor', treeShakeIcons: false);
878 879
      final DebuggingOptions mockOptions = DebuggingOptions.disabled(mockInfo);
      await device.startApp(package, prebuiltApplication: true, debuggingOptions: mockOptions);
880

881
      expect(simControl.requests.single.appIdentifier, 'correct');
882
    }, overrides: <Type, Generator>{
883
      PlistParser: () => testPlistParser,
884 885
      FileSystem: () => fileSystem,
      ProcessManager: () => FakeProcessManager.any(),
886
      Xcode: () => xcode,
887
    });
888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903

    testUsingContext('startApp respects the enable software rendering flag', () async {
      final IOSSimulator device = IOSSimulator(
        'x',
        name: 'iPhone SE',
        simulatorCategory: 'iOS 11.2',
        simControl: simControl,
      );

      final Directory mockDir = globals.fs.currentDirectory;
      final IOSApp package = PrebuiltIOSApp(projectBundleId: 'incorrect', bundleName: 'name', bundleDir: mockDir);

      const BuildInfo mockInfo = BuildInfo(BuildMode.debug, 'flavor', treeShakeIcons: false);
      final DebuggingOptions mockOptions = DebuggingOptions.enabled(mockInfo, enableSoftwareRendering: true);
      await device.startApp(package, prebuiltApplication: true, debuggingOptions: mockOptions);

904
      expect(simControl.requests.single.launchArgs, contains('--enable-software-rendering'));
905
    }, overrides: <Type, Generator>{
906
      PlistParser: () => testPlistParser,
907 908
      FileSystem: () => fileSystem,
      ProcessManager: () => FakeProcessManager.any(),
909
      Xcode: () => xcode,
910
    });
911 912
  });

913
  group('IOSDevice.isSupportedForProject', () {
914
    FakeSimControl simControl;
915
    Xcode xcode;
916 917

    setUp(() {
918
      simControl = FakeSimControl();
919
      xcode = Xcode.test(processManager: FakeProcessManager.any());
920 921 922 923 924 925
    });

    testUsingContext('is true on module project', () async {
      globals.fs.file('pubspec.yaml')
        ..createSync()
        ..writeAsStringSync(r'''
926 927 928 929 930
name: example

flutter:
  module: {}
''');
931
      globals.fs.file('.packages').createSync();
932
      final FlutterProject flutterProject = FlutterProject.fromDirectoryTest(globals.fs.currentDirectory);
933

934 935
      final IOSSimulator simulator = IOSSimulator(
        'test',
936
        simControl: simControl,
937 938 939
      );
      expect(simulator.isSupportedForProject(flutterProject), true);
    }, overrides: <Type, Generator>{
940
      FileSystem: () => MemoryFileSystem.test(),
941
      ProcessManager: () => FakeProcessManager.any(),
942
      Xcode: () => xcode,
943
    });
944 945


946 947 948 949
    testUsingContext('is true with editable host app', () async {
      globals.fs.file('pubspec.yaml').createSync();
      globals.fs.file('.packages').createSync();
      globals.fs.directory('ios').createSync();
950
      final FlutterProject flutterProject = FlutterProject.fromDirectoryTest(globals.fs.currentDirectory);
951

952 953
      final IOSSimulator simulator = IOSSimulator(
        'test',
954
        simControl: simControl,
955 956 957
      );
      expect(simulator.isSupportedForProject(flutterProject), true);
    }, overrides: <Type, Generator>{
958
      FileSystem: () => MemoryFileSystem.test(),
959
      ProcessManager: () => FakeProcessManager.any(),
960
      Xcode: () => xcode,
961 962 963 964 965
    });

    testUsingContext('is false with no host app and no module', () async {
      globals.fs.file('pubspec.yaml').createSync();
      globals.fs.file('.packages').createSync();
966
      final FlutterProject flutterProject = FlutterProject.fromDirectoryTest(globals.fs.currentDirectory);
967

968 969
      final IOSSimulator simulator = IOSSimulator(
        'test',
970
        simControl: simControl,
971 972 973
      );
      expect(simulator.isSupportedForProject(flutterProject), false);
    }, overrides: <Type, Generator>{
974
      FileSystem: () => MemoryFileSystem.test(),
975
      ProcessManager: () => FakeProcessManager.any(),
976
      Xcode: () => xcode,
977
    });
978 979 980 981

    testUsingContext('createDevFSWriter returns a LocalDevFSWriter', () {
      final IOSSimulator simulator = IOSSimulator(
        'test',
982
        simControl: simControl,
983 984 985 986
      );

      expect(simulator.createDevFSWriter(null, ''), isA<LocalDevFSWriter>());
    });
987
  });
988
}
989 990 991 992 993 994 995 996

class FakeIosProject extends Fake implements IosProject {
  @override
  Future<String> productBundleIdentifier(BuildInfo buildInfo) async => 'com.example.test';

  @override
  Future<String> hostAppBundleName(BuildInfo buildInfo) async => 'My Super Awesome App.app';
}
997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019

class FakeSimControl extends Fake implements SimControl {
  final List<LaunchRequest> requests = <LaunchRequest>[];

  @override
  Future<RunResult> launch(String deviceId, String appIdentifier, [ List<String> launchArgs ]) async {
    requests.add(LaunchRequest(deviceId, appIdentifier, launchArgs));
    return RunResult(ProcessResult(0, 0, '', ''), <String>['test']);
  }

  @override
  Future<RunResult> install(String deviceId, String appPath) async {
    return RunResult(ProcessResult(0, 0, '', ''), <String>['test']);
  }
}

class LaunchRequest {
  const LaunchRequest(this.deviceId, this.appIdentifier, this.launchArgs);

  final String deviceId;
  final String appIdentifier;
  final List<String> launchArgs;
}