drive_test.dart 20.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
yjbanov's avatar
yjbanov committed
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
import 'dart:async';

7
import 'package:file/memory.dart';
8 9
import 'package:platform/platform.dart';

10
import 'package:flutter_tools/src/android/android_device.dart';
11
import 'package:flutter_tools/src/base/common.dart';
yjbanov's avatar
yjbanov committed
12
import 'package:flutter_tools/src/base/file_system.dart';
13
import 'package:flutter_tools/src/base/io.dart';
14
import 'package:flutter_tools/src/cache.dart';
15 16
import 'package:flutter_tools/src/commands/drive.dart';
import 'package:flutter_tools/src/device.dart';
17
import 'package:flutter_tools/src/build_info.dart';
18
import 'package:mockito/mockito.dart';
19
import 'package:flutter_tools/src/globals.dart' as globals;
yjbanov's avatar
yjbanov committed
20

21 22 23
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/mocks.dart';
yjbanov's avatar
yjbanov committed
24

25
void main() {
yjbanov's avatar
yjbanov committed
26
  group('drive', () {
27 28
    DriveCommand command;
    Device mockDevice;
29
    Device mockUnsupportedDevice;
30
    MemoryFileSystem fs;
31
    Directory tempDir;
32

33 34 35 36
    setUpAll(() {
      Cache.disableLocking();
    });

yjbanov's avatar
yjbanov committed
37
    setUp(() {
38
      command = DriveCommand();
39
      applyMocksToCommand(command);
40
      fs = MemoryFileSystem();
41 42
      tempDir = fs.systemTempDirectory.createTempSync('flutter_drive_test.');
      fs.currentDirectory = tempDir;
43 44 45 46
      fs.directory('test').createSync();
      fs.directory('test_driver').createSync();
      fs.file('pubspec.yaml')..createSync();
      fs.file('.packages').createSync();
47
      setExitFunctionForTests();
48
      appStarter = (DriveCommand command) {
49 50
        throw 'Unexpected call to appStarter';
      };
51
      testRunner = (List<String> testArgs, Map<String, String> environment) {
52 53
        throw 'Unexpected call to testRunner';
      };
54
      appStopper = (DriveCommand command) {
55 56
        throw 'Unexpected call to appStopper';
      };
yjbanov's avatar
yjbanov committed
57 58 59
    });

    tearDown(() {
60
      command = null;
61
      restoreExitFunction();
62 63 64
      restoreAppStarter();
      restoreAppStopper();
      restoreTestRunner();
65
      tryToDelete(tempDir);
yjbanov's avatar
yjbanov committed
66 67
    });

68
    testUsingContext('returns 1 when test file is not found', () async {
69
      testDeviceManager.addDevice(MockDevice());
70

71 72 73
      final String testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart');
      final String testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart');
      globals.fs.file(testApp).createSync(recursive: true);
74

75
      final List<String> args = <String>[
yjbanov's avatar
yjbanov committed
76
        'drive',
77
        '--target=$testApp',
78
        '--no-pub',
yjbanov's avatar
yjbanov committed
79
      ];
80 81 82 83 84
      try {
        await createTestCommandRunner(command).run(args);
        fail('Expect exception');
      } on ToolExit catch (e) {
        expect(e.exitCode ?? 1, 1);
85
        expect(e.message, contains('Test file not found: $testFile'));
86
      }
87
    }, overrides: <Type, Generator>{
88
      FileSystem: () => fs,
89
      ProcessManager: () => FakeProcessManager.any(),
yjbanov's avatar
yjbanov committed
90 91 92
    });

    testUsingContext('returns 1 when app fails to run', () async {
93
      testDeviceManager.addDevice(MockDevice());
94
      appStarter = expectAsync1((DriveCommand command) async => null);
yjbanov's avatar
yjbanov committed
95

96 97
      final String testApp = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e.dart');
      final String testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart');
yjbanov's avatar
yjbanov committed
98

99
      final MemoryFileSystem memFs = fs;
100 101
      await memFs.file(testApp).writeAsString('main() { }');
      await memFs.file(testFile).writeAsString('main() { }');
yjbanov's avatar
yjbanov committed
102

103
      final List<String> args = <String>[
yjbanov's avatar
yjbanov committed
104 105
        'drive',
        '--target=$testApp',
106
        '--no-pub',
yjbanov's avatar
yjbanov committed
107
      ];
108 109 110 111 112
      try {
        await createTestCommandRunner(command).run(args);
        fail('Expect exception');
      } on ToolExit catch (e) {
        expect(e.exitCode, 1);
113
        expect(e.message, contains('Application failed to start. Will not run test. Quitting.'));
114
      }
115
    }, overrides: <Type, Generator>{
116
      FileSystem: () => fs,
117
      ProcessManager: () => FakeProcessManager.any(),
yjbanov's avatar
yjbanov committed
118 119
    });

120
    testUsingContext('returns 1 when app file is outside package', () async {
121 122
      final String appFile = globals.fs.path.join(tempDir.dirname, 'other_app', 'app.dart');
      globals.fs.file(appFile).createSync(recursive: true);
123
      final List<String> args = <String>[
124
        '--no-wrap',
125 126
        'drive',
        '--target=$appFile',
127
        '--no-pub',
128
      ];
129 130 131 132 133 134
      try {
        await createTestCommandRunner(command).run(args);
        fail('Expect exception');
      } on ToolExit catch (e) {
        expect(e.exitCode ?? 1, 1);
        expect(testLogger.errorText, contains(
135
            'Application file $appFile is outside the package directory ${tempDir.path}',
136
        ));
137
      }
138
    }, overrides: <Type, Generator>{
139
      FileSystem: () => fs,
140
      ProcessManager: () => FakeProcessManager.any(),
141 142 143
    });

    testUsingContext('returns 1 when app file is in the root dir', () async {
144 145
      final String appFile = globals.fs.path.join(tempDir.path, 'main.dart');
      globals.fs.file(appFile).createSync(recursive: true);
146
      final List<String> args = <String>[
147
        '--no-wrap',
148 149
        'drive',
        '--target=$appFile',
150
        '--no-pub',
151
      ];
152 153 154 155 156 157
      try {
        await createTestCommandRunner(command).run(args);
        fail('Expect exception');
      } on ToolExit catch (e) {
        expect(e.exitCode ?? 1, 1);
        expect(testLogger.errorText, contains(
158 159
            'Application file main.dart must reside in one of the '
            'sub-directories of the package structure, not in the root directory.',
160
        ));
161
      }
162
    }, overrides: <Type, Generator>{
163
      FileSystem: () => fs,
164
      ProcessManager: () => FakeProcessManager.any(),
165 166
    });

yjbanov's avatar
yjbanov committed
167
    testUsingContext('returns 0 when test ends successfully', () async {
168
      testDeviceManager.addDevice(MockDevice());
169

170 171
      final String testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart');
      final String testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart');
yjbanov's avatar
yjbanov committed
172

173
      appStarter = expectAsync1((DriveCommand command) async {
174
        return LaunchResult.succeeded();
175
      });
176
      testRunner = expectAsync2((List<String> testArgs, Map<String, String> environment) async {
177
        expect(testArgs, <String>[testFile]);
178 179 180 181 182 183 184 185
        // VM_SERVICE_URL is not set by drive command arguments
        expect(environment, <String, String>{
          'VM_SERVICE_URL': 'null',
          'SELENIUM_PORT': '4567',
          'BROWSER_NAME': 'firefox',
          'BROWSER_DIMENSION': '1024,768',
          'HEADLESS': 'false',
        });
186
        return null;
187
      });
188
      appStopper = expectAsync1((DriveCommand command) async {
189
        return true;
190
      });
yjbanov's avatar
yjbanov committed
191

192
      final MemoryFileSystem memFs = fs;
yjbanov's avatar
yjbanov committed
193 194 195
      await memFs.file(testApp).writeAsString('main() {}');
      await memFs.file(testFile).writeAsString('main() {}');

196
      final List<String> args = <String>[
yjbanov's avatar
yjbanov committed
197 198
        'drive',
        '--target=$testApp',
199
        '--no-pub',
200 201 202 203
        '--no-headless',
        '--driver-port=4567',
        '--browser-name=firefox',
        '--browser-dimension=1024,768',
yjbanov's avatar
yjbanov committed
204
      ];
205 206
      await createTestCommandRunner(command).run(args);
      expect(testLogger.errorText, isEmpty);
207
    }, overrides: <Type, Generator>{
208
      FileSystem: () => fs,
209
      ProcessManager: () => FakeProcessManager.any(),
yjbanov's avatar
yjbanov committed
210
    });
211

212
    testUsingContext('returns exitCode set by test runner', () async {
213
      testDeviceManager.addDevice(MockDevice());
214

215 216
      final String testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart');
      final String testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart');
217

218
      appStarter = expectAsync1((DriveCommand command) async {
219
        return LaunchResult.succeeded();
220
      });
221
      testRunner = (List<String> testArgs, Map<String, String> environment) async {
222 223
        throwToolExit(null, exitCode: 123);
      };
224
      appStopper = expectAsync1((DriveCommand command) async {
225
        return true;
226 227
      });

228
      final MemoryFileSystem memFs = fs;
229 230 231
      await memFs.file(testApp).writeAsString('main() {}');
      await memFs.file(testFile).writeAsString('main() {}');

232
      final List<String> args = <String>[
233 234
        'drive',
        '--target=$testApp',
235
        '--no-pub',
236
      ];
237 238 239 240 241 242 243
      try {
        await createTestCommandRunner(command).run(args);
        fail('Expect exception');
      } on ToolExit catch (e) {
        expect(e.exitCode ?? 1, 123);
        expect(e.message, isNull);
      }
244
    }, overrides: <Type, Generator>{
245
      FileSystem: () => fs,
246
      ProcessManager: () => FakeProcessManager.any(),
247 248
    });

249 250 251
    group('findTargetDevice', () {
      testUsingContext('uses specified device', () async {
        testDeviceManager.specifiedDeviceId = '123';
252 253
        mockDevice = MockDevice();
        testDeviceManager.addDevice(mockDevice);
254 255 256
        when(mockDevice.name).thenReturn('specified-device');
        when(mockDevice.id).thenReturn('123');

257
        final Device device = await findTargetDevice();
258
        expect(device.name, 'specified-device');
259
      }, overrides: <Type, Generator>{
260
        FileSystem: () => fs,
261
        ProcessManager: () => FakeProcessManager.any(),
262 263 264
      });
    });

265
    void findTargetDeviceOnOperatingSystem(String operatingSystem) {
266
      Platform platform() => FakePlatform(operatingSystem: operatingSystem);
267 268 269

      testUsingContext('returns null if no devices found', () async {
        expect(await findTargetDevice(), isNull);
270
      }, overrides: <Type, Generator>{
271
        FileSystem: () => fs,
272
        ProcessManager: () => FakeProcessManager.any(),
273
        Platform: platform,
274 275 276
      });

      testUsingContext('uses existing Android device', () async {
277
        mockDevice = MockAndroidDevice();
278
        when(mockDevice.name).thenReturn('mock-android-device');
279 280 281 282 283 284
        testDeviceManager.addDevice(mockDevice);

        final Device device = await findTargetDevice();
        expect(device.name, 'mock-android-device');
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
285
        ProcessManager: () => FakeProcessManager.any(),
286 287 288 289 290 291 292 293
        Platform: platform,
      });

      testUsingContext('skips unsupported device', () async {
        mockDevice = MockAndroidDevice();
        mockUnsupportedDevice = MockDevice();
        when(mockUnsupportedDevice.isSupportedForProject(any))
            .thenReturn(false);
294 295 296 297 298 299 300 301 302 303 304
        when(mockUnsupportedDevice.isSupported())
            .thenReturn(false);
        when(mockUnsupportedDevice.name).thenReturn('mock-web');
        when(mockUnsupportedDevice.id).thenReturn('web-1');
        when(mockUnsupportedDevice.targetPlatform).thenAnswer((_) => Future<TargetPlatform>(() => TargetPlatform.web_javascript));
        when(mockUnsupportedDevice.isLocalEmulator).thenAnswer((_) => Future<bool>(() => false));
        when(mockUnsupportedDevice.sdkNameAndVersion).thenAnswer((_) => Future<String>(() => 'html5'));
        when(mockDevice.name).thenReturn('mock-android-device');
        when(mockDevice.id).thenReturn('mad-28');
        when(mockDevice.isSupported())
            .thenReturn(true);
305 306
        when(mockDevice.isSupportedForProject(any))
            .thenReturn(true);
307 308 309
        when(mockDevice.targetPlatform).thenAnswer((_) => Future<TargetPlatform>(() => TargetPlatform.android_x64));
        when(mockDevice.isLocalEmulator).thenAnswer((_) => Future<bool>(() => false));
        when(mockDevice.sdkNameAndVersion).thenAnswer((_) => Future<String>(() => 'sdk-28'));
310 311
        testDeviceManager.addDevice(mockDevice);
        testDeviceManager.addDevice(mockUnsupportedDevice);
312

313
        final Device device = await findTargetDevice();
314
        expect(device.name, 'mock-android-device');
315
      }, overrides: <Type, Generator>{
316
        FileSystem: () => fs,
317
        ProcessManager: () => FakeProcessManager.any(),
318
        Platform: platform,
319
      });
320 321 322
    }

    group('findTargetDevice on Linux', () {
323
      findTargetDeviceOnOperatingSystem('linux');
324 325 326
    });

    group('findTargetDevice on Windows', () {
327
      findTargetDeviceOnOperatingSystem('windows');
328
    });
329 330 331 332

    group('findTargetDevice on macOS', () {
      findTargetDeviceOnOperatingSystem('macos');

333
      Platform macOsPlatform() => FakePlatform(operatingSystem: 'macos');
334 335

      testUsingContext('uses existing simulator', () async {
336
        testDeviceManager.addDevice(mockDevice);
337
        when(mockDevice.name).thenReturn('mock-simulator');
338
        when(mockDevice.isLocalEmulator)
339
            .thenAnswer((Invocation invocation) => Future<bool>.value(true));
340 341 342 343 344

        final Device device = await findTargetDevice();
        expect(device.name, 'mock-simulator');
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
345
        ProcessManager: () => FakeProcessManager.any(),
346 347 348
        Platform: macOsPlatform,
      });
    });
349 350 351 352 353 354 355 356 357

    group('build arguments', () {
      String testApp, testFile;

      setUp(() {
        restoreAppStarter();
      });

      Future<void> appStarterSetup() async {
358
        mockDevice = MockDevice();
359
        testDeviceManager.addDevice(mockDevice);
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374

        final MockDeviceLogReader mockDeviceLogReader = MockDeviceLogReader();
        when(mockDevice.getLogReader()).thenReturn(mockDeviceLogReader);
        final MockLaunchResult mockLaunchResult = MockLaunchResult();
        when(mockLaunchResult.started).thenReturn(true);
        when(mockDevice.startApp(
            null,
            mainPath: anyNamed('mainPath'),
            route: anyNamed('route'),
            debuggingOptions: anyNamed('debuggingOptions'),
            platformArgs: anyNamed('platformArgs'),
            prebuiltApplication: anyNamed('prebuiltApplication'),
        )).thenAnswer((_) => Future<LaunchResult>.value(mockLaunchResult));
        when(mockDevice.isAppInstalled(any)).thenAnswer((_) => Future<bool>.value(false));

375 376
        testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart');
        testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart');
377

378
        testRunner = (List<String> testArgs, Map<String, String> environment) async {
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398
          throwToolExit(null, exitCode: 123);
        };
        appStopper = expectAsync1(
            (DriveCommand command) async {
              return true;
            },
            count: 2,
        );

        final MemoryFileSystem memFs = fs;
        await memFs.file(testApp).writeAsString('main() {}');
        await memFs.file(testFile).writeAsString('main() {}');
      }

      testUsingContext('does not use pre-built app if no build arg provided', () async {
        await appStarterSetup();

        final List<String> args = <String>[
          'drive',
          '--target=$testApp',
399
          '--no-pub',
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
        ];
        try {
          await createTestCommandRunner(command).run(args);
        } on ToolExit catch (e) {
          expect(e.exitCode, 123);
          expect(e.message, null);
        }
        verify(mockDevice.startApp(
                null,
                mainPath: anyNamed('mainPath'),
                route: anyNamed('route'),
                debuggingOptions: anyNamed('debuggingOptions'),
                platformArgs: anyNamed('platformArgs'),
                prebuiltApplication: false,
        ));
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
417
        ProcessManager: () => FakeProcessManager.any(),
418 419 420 421 422 423 424 425 426
      });

      testUsingContext('does not use pre-built app if --build arg provided', () async {
        await appStarterSetup();

        final List<String> args = <String>[
          'drive',
          '--build',
          '--target=$testApp',
427
          '--no-pub',
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
        ];
        try {
          await createTestCommandRunner(command).run(args);
        } on ToolExit catch (e) {
          expect(e.exitCode, 123);
          expect(e.message, null);
        }
        verify(mockDevice.startApp(
                null,
                mainPath: anyNamed('mainPath'),
                route: anyNamed('route'),
                debuggingOptions: anyNamed('debuggingOptions'),
                platformArgs: anyNamed('platformArgs'),
                prebuiltApplication: false,
        ));
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
445
        ProcessManager: () => FakeProcessManager.any(),
446 447 448 449 450 451 452 453 454
      });

      testUsingContext('uses prebuilt app if --no-build arg provided', () async {
        await appStarterSetup();

        final List<String> args = <String>[
          'drive',
          '--no-build',
          '--target=$testApp',
455
          '--no-pub',
456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472
        ];
        try {
          await createTestCommandRunner(command).run(args);
        } on ToolExit catch (e) {
          expect(e.exitCode, 123);
          expect(e.message, null);
        }
        verify(mockDevice.startApp(
                null,
                mainPath: anyNamed('mainPath'),
                route: anyNamed('route'),
                debuggingOptions: anyNamed('debuggingOptions'),
                platformArgs: anyNamed('platformArgs'),
                prebuiltApplication: true,
        ));
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
473
        ProcessManager: () => FakeProcessManager.any(),
474 475
      });
    });
476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501

    group('debugging options', () {
      DebuggingOptions debuggingOptions;

      String testApp, testFile;

      setUp(() {
        restoreAppStarter();
      });

      Future<void> appStarterSetup() async {
        mockDevice = MockDevice();
        testDeviceManager.addDevice(mockDevice);

        final MockDeviceLogReader mockDeviceLogReader = MockDeviceLogReader();
        when(mockDevice.getLogReader()).thenReturn(mockDeviceLogReader);
        final MockLaunchResult mockLaunchResult = MockLaunchResult();
        when(mockLaunchResult.started).thenReturn(true);
        when(mockDevice.startApp(
          null,
          mainPath: anyNamed('mainPath'),
          route: anyNamed('route'),
          debuggingOptions: anyNamed('debuggingOptions'),
          platformArgs: anyNamed('platformArgs'),
          prebuiltApplication: anyNamed('prebuiltApplication'),
        )).thenAnswer((Invocation invocation) async {
502
          debuggingOptions = invocation.namedArguments[#debuggingOptions] as DebuggingOptions;
503 504 505 506 507
          return mockLaunchResult;
        });
        when(mockDevice.isAppInstalled(any))
            .thenAnswer((_) => Future<bool>.value(false));

508 509
        testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart');
        testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart');
510

511
        testRunner = (List<String> testArgs, Map<String, String> environment) async {
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 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 582 583
          throwToolExit(null, exitCode: 123);
        };
        appStopper = expectAsync1(
          (DriveCommand command) async {
            return true;
          },
          count: 2,
        );

        final MemoryFileSystem memFs = fs;
        await memFs.file(testApp).writeAsString('main() {}');
        await memFs.file(testFile).writeAsString('main() {}');
      }

      void _testOptionThatDefaultsToFalse(
        String optionName,
        bool setToTrue,
        bool optionValue(),
      ) {
        testUsingContext('$optionName ${setToTrue ? 'works' : 'defaults to false'}', () async {
          await appStarterSetup();

          final List<String> args = <String>[
            'drive',
            '--target=$testApp',
            if (setToTrue) optionName,
            '--no-pub',
          ];
          try {
            await createTestCommandRunner(command).run(args);
          } on ToolExit catch (e) {
            expect(e.exitCode, 123);
            expect(e.message, null);
          }
          verify(mockDevice.startApp(
            null,
            mainPath: anyNamed('mainPath'),
            route: anyNamed('route'),
            debuggingOptions: anyNamed('debuggingOptions'),
            platformArgs: anyNamed('platformArgs'),
            prebuiltApplication: false,
          ));
          expect(optionValue(), setToTrue ? isTrue : isFalse);
        }, overrides: <Type, Generator>{
          FileSystem: () => fs,
          ProcessManager: () => FakeProcessManager.any(),
        });
      }

      void testOptionThatDefaultsToFalse(
        String optionName,
        bool optionValue(),
      ) {
        _testOptionThatDefaultsToFalse(optionName, true, optionValue);
        _testOptionThatDefaultsToFalse(optionName, false, optionValue);
      }

      testOptionThatDefaultsToFalse(
        '--dump-skp-on-shader-compilation',
        () => debuggingOptions.dumpSkpOnShaderCompilation,
      );

      testOptionThatDefaultsToFalse(
        '--verbose-system-logs',
        () => debuggingOptions.verboseSystemLogs,
      );

      testOptionThatDefaultsToFalse(
        '--cache-sksl',
        () => debuggingOptions.cacheSkSL,
      );
    });
584
  });
yjbanov's avatar
yjbanov committed
585
}
586 587 588

class MockDevice extends Mock implements Device {
  MockDevice() {
589
    when(isSupported()).thenReturn(true);
590 591 592 593
  }
}

class MockAndroidDevice extends Mock implements AndroidDevice { }
594 595

class MockLaunchResult extends Mock implements LaunchResult { }