hot_test.dart 23.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 8 9
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart';
10
import 'package:flutter_tools/src/resident_devtools_handler.dart';
11
import 'package:vm_service/vm_service.dart' as vm_service;
12
import 'package:flutter_tools/src/artifacts.dart';
13
import 'package:flutter_tools/src/base/io.dart';
14
import 'package:flutter_tools/src/build_info.dart';
15
import 'package:flutter_tools/src/compile.dart';
16
import 'package:flutter_tools/src/devfs.dart';
17 18
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/resident_runner.dart';
19
import 'package:flutter_tools/src/run_hot.dart';
20
import 'package:flutter_tools/src/vmservice.dart';
21 22
import 'package:meta/meta.dart';
import 'package:mockito/mockito.dart';
23

24 25 26
import '../src/common.dart';
import '../src/context.dart';
import '../src/mocks.dart';
27

28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
final vm_service.Isolate fakeUnpausedIsolate = vm_service.Isolate(
  id: '1',
  pauseEvent: vm_service.Event(
    kind: vm_service.EventKind.kResume,
    timestamp: 0
  ),
  breakpoints: <vm_service.Breakpoint>[],
  exceptionPauseMode: null,
  libraries: <vm_service.LibraryRef>[],
  livePorts: 0,
  name: 'test',
  number: '1',
  pauseOnExit: false,
  runnable: true,
  startTime: 0,
43
  isSystemIsolate: false,
44
  isolateFlags: <vm_service.IsolateFlag>[],
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
);

final FlutterView fakeFlutterView = FlutterView(
  id: 'a',
  uiIsolate: fakeUnpausedIsolate,
);

final FakeVmServiceRequest listViews = FakeVmServiceRequest(
  method: kListViewsMethod,
  jsonResponse: <String, Object>{
    'views': <Object>[
      fakeFlutterView.toJson(),
    ],
  },
);
60
void main() {
61 62
  group('validateReloadReport', () {
    testUsingContext('invalid', () async {
63
      expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
64 65 66
        'type': 'ReloadReport',
        'success': false,
        'details': <String, dynamic>{},
67 68
      })), false);
      expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
69 70 71 72 73 74
        'type': 'ReloadReport',
        'success': false,
        'details': <String, dynamic>{
          'notices': <Map<String, dynamic>>[
          ],
        },
75 76
      })), false);
      expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
77 78 79 80 81 82 83
        'type': 'ReloadReport',
        'success': false,
        'details': <String, dynamic>{
          'notices': <String, dynamic>{
            'message': 'error',
          },
        },
84 85
      })), false);
      expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
86 87 88 89 90
        'type': 'ReloadReport',
        'success': false,
        'details': <String, dynamic>{
          'notices': <Map<String, dynamic>>[],
        },
91 92
      })), false);
      expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
93 94 95 96
        'type': 'ReloadReport',
        'success': false,
        'details': <String, dynamic>{
          'notices': <Map<String, dynamic>>[
97
            <String, dynamic>{'message': false},
98 99
          ],
        },
100 101
      })), false);
      expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
102 103 104 105
        'type': 'ReloadReport',
        'success': false,
        'details': <String, dynamic>{
          'notices': <Map<String, dynamic>>[
106
            <String, dynamic>{'message': <String>['error']},
107 108
          ],
        },
109 110
      })), false);
      expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
111 112 113 114
        'type': 'ReloadReport',
        'success': false,
        'details': <String, dynamic>{
          'notices': <Map<String, dynamic>>[
115 116
            <String, dynamic>{'message': 'error'},
            <String, dynamic>{'message': <String>['error']},
117 118
          ],
        },
119 120
      })), false);
      expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
121 122 123 124
        'type': 'ReloadReport',
        'success': false,
        'details': <String, dynamic>{
          'notices': <Map<String, dynamic>>[
125
            <String, dynamic>{'message': 'error'},
126 127
          ],
        },
128 129
      })), false);
      expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
130 131
        'type': 'ReloadReport',
        'success': true,
132 133 134 135 136 137 138 139 140
      })), true);
    });

    testWithoutContext('ReasonForCancelling toString has a hint for specific errors', () {
      final ReasonForCancelling reasonForCancelling = ReasonForCancelling(
        message: 'Const class cannot remove fields',
      );

      expect(reasonForCancelling.toString(), contains('Try performing a hot restart instead.'));
141 142
    });
  });
143 144

  group('hotRestart', () {
145
    final MockResidentCompiler residentCompiler = MockResidentCompiler();
146
    final MockDevFs mockDevFs = MockDevFs();
147
    FileSystem fileSystem;
148

149
    when(mockDevFs.update(
150
      mainUri: anyNamed('mainUri'),
151 152 153 154 155 156 157 158 159 160
      target: anyNamed('target'),
      bundle: anyNamed('bundle'),
      firstBuildTime: anyNamed('firstBuildTime'),
      bundleFirstUpload: anyNamed('bundleFirstUpload'),
      generator: anyNamed('generator'),
      fullRestart: anyNamed('fullRestart'),
      dillOutputPath: anyNamed('dillOutputPath'),
      trackWidgetCreation: anyNamed('trackWidgetCreation'),
      projectRootPath: anyNamed('projectRootPath'),
      pathToReload: anyNamed('pathToReload'),
161
      invalidatedFiles: anyNamed('invalidatedFiles'),
162
      packageConfig: anyNamed('packageConfig'),
163 164
    )).thenAnswer((Invocation _) => Future<UpdateFSReport>.value(
        UpdateFSReport(success: true, syncedBytes: 1000, invalidatedSourcesCount: 1)));
165
    when(mockDevFs.assetPathsToEvict).thenReturn(<String>{});
166
    when(mockDevFs.baseUri).thenReturn(Uri.file('test'));
167 168
    when(mockDevFs.sources).thenReturn(<Uri>[Uri.file('test')]);
    when(mockDevFs.lastCompiled).thenReturn(DateTime.now());
169

170
    setUp(() {
171
      fileSystem = MemoryFileSystem.test();
172 173
    });

174
    testUsingContext('Does not hot restart when device does not support it', () async {
175
      fileSystem.file('.packages')
176 177
        ..createSync(recursive: true)
        ..writeAsStringSync('\n');
178 179 180 181
      // Setup mocks
      final MockDevice mockDevice = MockDevice();
      when(mockDevice.supportsHotReload).thenReturn(true);
      when(mockDevice.supportsHotRestart).thenReturn(false);
182
      when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester);
183 184
      // Trigger hot restart.
      final List<FlutterDevice> devices = <FlutterDevice>[
185
        FlutterDevice(mockDevice, generator: residentCompiler, buildInfo: BuildInfo.debug)..devFS = mockDevFs,
186
      ];
187 188 189
      final OperationResult result = await HotRunner(
        devices,
        debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
190
        target: 'main.dart',
191
        devtoolsHandler: createNoOpHandler,
192
      ).restart(fullRestart: true);
193
      // Expect hot restart failed.
194
      expect(result.isOk, false);
195
      expect(result.message, 'hotRestart not supported');
196
    }, overrides: <Type, Generator>{
197
      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
198 199 200 201
      Artifacts: () => Artifacts.test(),
      FileSystem: () => fileSystem,
      Platform: () => FakePlatform(operatingSystem: 'linux'),
      ProcessManager: () => FakeProcessManager.any(),
202 203 204
    });

    testUsingContext('Does not hot restart when one of many devices does not support it', () async {
205
      fileSystem.file('.packages')
206 207
        ..createSync(recursive: true)
        ..writeAsStringSync('\n');
208 209 210 211 212 213 214 215 216
      // Setup mocks
      final MockDevice mockDevice = MockDevice();
      final MockDevice mockHotDevice = MockDevice();
      when(mockDevice.supportsHotReload).thenReturn(true);
      when(mockDevice.supportsHotRestart).thenReturn(false);
      when(mockHotDevice.supportsHotReload).thenReturn(true);
      when(mockHotDevice.supportsHotRestart).thenReturn(true);
      // Trigger hot restart.
      final List<FlutterDevice> devices = <FlutterDevice>[
217 218
        FlutterDevice(mockDevice, generator: residentCompiler, buildInfo: BuildInfo.debug)..devFS = mockDevFs,
        FlutterDevice(mockHotDevice, generator: residentCompiler, buildInfo: BuildInfo.debug)..devFS = mockDevFs,
219
      ];
220 221
      final OperationResult result = await HotRunner(
        devices,
222 223
        debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
        target: 'main.dart',
224
        devtoolsHandler: createNoOpHandler,
225
      ).restart(fullRestart: true);
226 227 228 229
      // Expect hot restart failed.
      expect(result.isOk, false);
      expect(result.message, 'hotRestart not supported');
    }, overrides: <Type, Generator>{
230
      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
231 232 233 234
      Artifacts: () => Artifacts.test(),
      FileSystem: () => fileSystem,
      Platform: () => FakePlatform(operatingSystem: 'linux'),
      ProcessManager: () => FakeProcessManager.any(),
235 236 237
    });

    testUsingContext('Does hot restarts when all devices support it', () async {
238
      final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
239 240 241 242 243 244 245
        listViews,
        FakeVmServiceRequest(
          method: 'getIsolate',
          args: <String, Object>{
            'isolateId': fakeUnpausedIsolate.id,
          },
          jsonResponse: fakeUnpausedIsolate.toJson(),
246 247 248 249 250
        ),
        FakeVmServiceRequest(
          method: 'getVM',
          jsonResponse: vm_service.VM.parse(<String, Object>{}).toJson()
        ),
251 252 253 254 255 256 257 258
        listViews,
        FakeVmServiceRequest(
          method: 'getIsolate',
          args: <String, Object>{
            'isolateId': fakeUnpausedIsolate.id,
          },
          jsonResponse: fakeUnpausedIsolate.toJson(),
        ),
259 260 261 262
        FakeVmServiceRequest(
          method: 'getVM',
          jsonResponse: vm_service.VM.parse(<String, Object>{}).toJson()
        ),
263 264
        listViews,
        listViews,
265
        const FakeVmServiceRequest(
266 267 268
          method: 'streamListen',
          args: <String, Object>{
            'streamId': 'Isolate',
269 270 271
          }
        ),
        const FakeVmServiceRequest(
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
          method: 'streamListen',
          args: <String, Object>{
            'streamId': 'Isolate',
          }
        ),
        FakeVmServiceStreamResponse(
          streamId: 'Isolate',
          event: vm_service.Event(
            timestamp: 0,
            kind: vm_service.EventKind.kIsolateRunnable,
          )
        ),
        FakeVmServiceStreamResponse(
          streamId: 'Isolate',
          event: vm_service.Event(
            timestamp: 0,
            kind: vm_service.EventKind.kIsolateRunnable,
          )
        ),
        FakeVmServiceRequest(
          method: kRunInViewMethod,
          args: <String, Object>{
            'viewId': fakeFlutterView.id,
295
            'mainScript': 'main.dart.dill',
296 297 298 299 300 301 302
            'assetDirectory': 'build/flutter_assets',
          }
        ),
        FakeVmServiceRequest(
          method: kRunInViewMethod,
          args: <String, Object>{
            'viewId': fakeFlutterView.id,
303
            'mainScript': 'main.dart.dill',
304
            'assetDirectory': 'build/flutter_assets',
305 306 307
          }
        ),
      ]);
308 309 310 311 312 313 314 315 316
      // Setup mocks
      final MockDevice mockDevice = MockDevice();
      final MockDevice mockHotDevice = MockDevice();
      when(mockDevice.supportsHotReload).thenReturn(true);
      when(mockDevice.supportsHotRestart).thenReturn(true);
      when(mockHotDevice.supportsHotReload).thenReturn(true);
      when(mockHotDevice.supportsHotRestart).thenReturn(true);
      // Trigger a restart.
      final List<FlutterDevice> devices = <FlutterDevice>[
317 318 319 320 321 322
        FlutterDevice(mockDevice, generator: residentCompiler, buildInfo: BuildInfo.debug)
          ..vmService = fakeVmServiceHost.vmService
          ..devFS = mockDevFs,
        FlutterDevice(mockHotDevice, generator: residentCompiler, buildInfo: BuildInfo.debug)
          ..vmService = fakeVmServiceHost.vmService
          ..devFS = mockDevFs,
323
      ];
324 325 326
      final HotRunner hotRunner = HotRunner(
        devices,
        debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
327
        target: 'main.dart',
328
        devtoolsHandler: createNoOpHandler,
329
      );
330
      final OperationResult result = await hotRunner.restart(fullRestart: true);
331
      // Expect hot restart was successful.
332
      expect(hotRunner.uri, mockDevFs.baseUri);
333 334 335
      expect(result.isOk, true);
      expect(result.message, isNot('hotRestart not supported'));
    }, overrides: <Type, Generator>{
336
      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
337 338 339 340
      Artifacts: () => Artifacts.test(),
      FileSystem: () => fileSystem,
      Platform: () => FakePlatform(operatingSystem: 'linux'),
      ProcessManager: () => FakeProcessManager.any(),
341 342 343
    });

    testUsingContext('setup function fails', () async {
344
      fileSystem.file('.packages')
345 346
        ..createSync(recursive: true)
        ..writeAsStringSync('\n');
347 348 349
      final MockDevice mockDevice = MockDevice();
      when(mockDevice.supportsHotReload).thenReturn(true);
      when(mockDevice.supportsHotRestart).thenReturn(true);
350
      when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester);
351
      final List<FlutterDevice> devices = <FlutterDevice>[
352
        FlutterDevice(mockDevice, generator: residentCompiler, buildInfo: BuildInfo.debug)..devFS = MockDevFs(),
353
      ];
354 355 356
      final OperationResult result = await HotRunner(
        devices,
        debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
357
        target: 'main.dart',
358
        devtoolsHandler: createNoOpHandler,
359
      ).restart(fullRestart: true);
360 361
      expect(result.isOk, false);
      expect(result.message, 'setupHotRestart failed');
362
    }, overrides: <Type, Generator>{
363
      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: false),
364 365 366 367
      Artifacts: () => Artifacts.test(),
      FileSystem: () => fileSystem,
      Platform: () => FakePlatform(operatingSystem: 'linux'),
      ProcessManager: () => FakeProcessManager.any(),
368
    });
369 370

    testUsingContext('hot restart supported', () async {
371
      fileSystem.file('.packages')
372 373
        ..createSync(recursive: true)
        ..writeAsStringSync('\n');
374
      // Setup mocks
375
      final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
376 377 378 379 380 381 382
        listViews,
        FakeVmServiceRequest(
          method: 'getIsolate',
          args: <String, Object>{
            'isolateId': fakeUnpausedIsolate.id,
          },
          jsonResponse: fakeUnpausedIsolate.toJson(),
383 384 385
        ),
        FakeVmServiceRequest(
          method: 'getVM',
386
          jsonResponse: vm_service.VM.parse(<String, Object>{}).toJson(),
387
        ),
388
        listViews,
389
        const FakeVmServiceRequest(
390 391 392
          method: 'streamListen',
          args: <String, Object>{
            'streamId': 'Isolate',
393 394
          }
        ),
395 396 397 398
        FakeVmServiceRequest(
          method: kRunInViewMethod,
          args: <String, Object>{
            'viewId': fakeFlutterView.id,
399
            'mainScript': 'main.dart.dill',
400 401 402 403 404 405 406 407 408 409
            'assetDirectory': 'build/flutter_assets',
          }
        ),
        FakeVmServiceStreamResponse(
          streamId: 'Isolate',
          event: vm_service.Event(
            timestamp: 0,
            kind: vm_service.EventKind.kIsolateRunnable,
          )
        ),
410
      ]);
411 412 413
      final MockDevice mockDevice = MockDevice();
      when(mockDevice.supportsHotReload).thenReturn(true);
      when(mockDevice.supportsHotRestart).thenReturn(true);
414
      when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester);
415 416
      // Trigger hot restart.
      final List<FlutterDevice> devices = <FlutterDevice>[
417 418 419
        FlutterDevice(mockDevice, generator: residentCompiler, buildInfo: BuildInfo.debug)
          ..vmService = fakeVmServiceHost.vmService
          ..devFS = mockDevFs,
420
      ];
421 422 423
      final HotRunner hotRunner = HotRunner(
        devices,
        debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
424
        target: 'main.dart',
425
        devtoolsHandler: createNoOpHandler,
426
      );
427
      final OperationResult result = await hotRunner.restart(fullRestart: true);
428
      // Expect hot restart successful.
429
      expect(hotRunner.uri, mockDevFs.baseUri);
430 431 432
      expect(result.isOk, true);
      expect(result.message, isNot('setupHotRestart failed'));
    }, overrides: <Type, Generator>{
433
      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
434 435 436 437
      Artifacts: () => Artifacts.test(),
      FileSystem: () => fileSystem,
      Platform: () => FakePlatform(operatingSystem: 'linux'),
      ProcessManager: () => FakeProcessManager.any(),
438
    });
439 440 441 442 443 444 445 446 447 448 449

    group('shutdown hook tests', () {
      TestHotRunnerConfig shutdownTestingConfig;

      setUp(() {
        shutdownTestingConfig = TestHotRunnerConfig(
          successfulSetup: true,
        );
      });

      testUsingContext('shutdown hook called after signal', () async {
450
        fileSystem.file('.packages')
451 452
          ..createSync(recursive: true)
          ..writeAsStringSync('\n');
453 454 455
        final MockDevice mockDevice = MockDevice();
        when(mockDevice.supportsHotReload).thenReturn(true);
        when(mockDevice.supportsHotRestart).thenReturn(true);
456
        when(mockDevice.supportsFlutterExit).thenReturn(false);
457
        final List<FlutterDevice> devices = <FlutterDevice>[
458
          FlutterDevice(mockDevice, generator: residentCompiler, buildInfo: BuildInfo.debug),
459
        ];
460 461
        await HotRunner(
          devices,
462 463
          debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
          target: 'main.dart',
464
        ).cleanupAfterSignal();
465
        expect(shutdownTestingConfig.shutdownHookCalled, true);
466
      }, overrides: <Type, Generator>{
467
        HotRunnerConfig: () => shutdownTestingConfig,
468 469 470 471
        Artifacts: () => Artifacts.test(),
        FileSystem: () => fileSystem,
        Platform: () => FakePlatform(operatingSystem: 'linux'),
        ProcessManager: () => FakeProcessManager.any(),
472 473 474
      });

      testUsingContext('shutdown hook called after app stop', () async {
475
        fileSystem.file('.packages')
476 477
          ..createSync(recursive: true)
          ..writeAsStringSync('\n');
478 479 480
        final MockDevice mockDevice = MockDevice();
        when(mockDevice.supportsHotReload).thenReturn(true);
        when(mockDevice.supportsHotRestart).thenReturn(true);
481
        when(mockDevice.supportsFlutterExit).thenReturn(false);
482
        final List<FlutterDevice> devices = <FlutterDevice>[
483
          FlutterDevice(mockDevice, generator: residentCompiler, buildInfo: BuildInfo.debug),
484
        ];
485 486
        await HotRunner(
          devices,
487 488
          debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
          target: 'main.dart',
489
        ).preExit();
490
        expect(shutdownTestingConfig.shutdownHookCalled, true);
491
      }, overrides: <Type, Generator>{
492
        HotRunnerConfig: () => shutdownTestingConfig,
493 494 495 496
        Artifacts: () => Artifacts.test(),
        FileSystem: () => fileSystem,
        Platform: () => FakePlatform(operatingSystem: 'linux'),
        ProcessManager: () => FakeProcessManager.any(),
497 498
      });
    });
499
  });
500 501

  group('hot attach', () {
502
    FileSystem fileSystem;
503 504

    setUp(() {
505
      fileSystem = MemoryFileSystem.test();
506 507
    });

508 509
    testUsingContext('Exits with code 2 when when HttpException is thrown '
      'during VM service connection', () async {
510
      fileSystem.file('.packages')
511 512 513
        ..createSync(recursive: true)
        ..writeAsStringSync('\n');

514
      final MockResidentCompiler residentCompiler = MockResidentCompiler();
515 516 517 518 519 520 521 522 523 524 525
      final MockDevice mockDevice = MockDevice();
      when(mockDevice.supportsHotReload).thenReturn(true);
      when(mockDevice.supportsHotRestart).thenReturn(false);
      when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester);
      when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation _) async => 'Android 10');

      final List<FlutterDevice> devices = <FlutterDevice>[
        TestFlutterDevice(
          device: mockDevice,
          generator: residentCompiler,
          exception: const HttpException('Connection closed before full header was received, '
526
              'uri = http://127.0.0.1:63394/5ZmLv8A59xY=/ws'),
527 528 529 530 531
        ),
      ];

      final int exitCode = await HotRunner(devices,
        debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
532
        target: 'main.dart',
533 534 535
      ).attach(
        enableDevTools: false,
      );
536 537 538
      expect(exitCode, 2);
    }, overrides: <Type, Generator>{
      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
539 540 541 542
      Artifacts: () => Artifacts.test(),
      FileSystem: () => fileSystem,
      Platform: () => FakePlatform(operatingSystem: 'linux'),
      ProcessManager: () => FakeProcessManager.any(),
543 544
    });
  });
545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566

  group('hot cleanupAtFinish()', () {
    MockFlutterDevice mockFlutterDeviceFactory(Device device) {
      final MockFlutterDevice mockFlutterDevice = MockFlutterDevice();
      when(mockFlutterDevice.stopEchoingDeviceLog()).thenAnswer((Invocation invocation) => Future<void>.value(null));
      when(mockFlutterDevice.device).thenReturn(device);
      return mockFlutterDevice;
    }

    testUsingContext('disposes each device', () async {
      final MockDevice mockDevice1 = MockDevice();
      final MockDevice mockDevice2 = MockDevice();
      final MockFlutterDevice mockFlutterDevice1 = mockFlutterDeviceFactory(mockDevice1);
      final MockFlutterDevice mockFlutterDevice2 = mockFlutterDeviceFactory(mockDevice2);

      final List<FlutterDevice> devices = <FlutterDevice>[
        mockFlutterDevice1,
        mockFlutterDevice2,
      ];

      await HotRunner(devices,
        debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
567
        target: 'main.dart',
568 569 570 571 572 573 574 575
      ).cleanupAtFinish();

      verify(mockDevice1.dispose());
      verify(mockFlutterDevice1.stopEchoingDeviceLog());
      verify(mockDevice2.dispose());
      verify(mockFlutterDevice2.stopEchoingDeviceLog());
    });
  });
576 577
}

578 579
class MockDevFs extends Mock implements DevFS {}

580 581 582 583 584 585
class MockDevice extends Mock implements Device {
  MockDevice() {
    when(isSupported()).thenReturn(true);
  }
}

586 587
class MockFlutterDevice extends Mock implements FlutterDevice {}

588 589 590 591
class TestFlutterDevice extends FlutterDevice {
  TestFlutterDevice({
    @required Device device,
    @required this.exception,
592
    @required ResidentCompiler generator,
593
  })  : assert(exception != null),
594
        super(device, buildInfo: BuildInfo.debug, generator: generator);
595 596 597 598 599 600 601 602 603

  /// The exception to throw when the connect method is called.
  final Exception exception;

  @override
  Future<void> connect({
    ReloadSources reloadSources,
    Restart restart,
    CompileExpression compileExpression,
604
    GetSkSLMethod getSkSLMethod,
605
    PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
606
    bool disableServiceAuthCodes = false,
607 608
    bool disableDds = false,
    bool ipv6 = false,
609 610
    int hostVmServicePort,
    int ddsPort,
611
    bool allowExistingDdsInstance = false,
612 613 614 615 616
  }) async {
    throw exception;
  }
}

617
class TestHotRunnerConfig extends HotRunnerConfig {
618
  TestHotRunnerConfig({@required this.successfulSetup});
619
  bool successfulSetup;
620
  bool shutdownHookCalled = false;
621

622 623 624 625
  @override
  Future<bool> setupHotRestart() async {
    return successfulSetup;
  }
626 627 628 629 630

  @override
  Future<void> runPreShutdownOperations() async {
    shutdownHookCalled = true;
  }
631
}