• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:flutter_tools/src/artifacts.dart';
8import 'package:flutter_tools/src/base/io.dart';
9import 'package:flutter_tools/src/base/logger.dart';
10import 'package:flutter_tools/src/build_info.dart';
11import 'package:flutter_tools/src/compile.dart';
12import 'package:flutter_tools/src/devfs.dart';
13import 'package:flutter_tools/src/device.dart';
14import 'package:flutter_tools/src/resident_runner.dart';
15import 'package:flutter_tools/src/run_hot.dart';
16import 'package:flutter_tools/src/vmservice.dart';
17import 'package:meta/meta.dart';
18import 'package:mockito/mockito.dart';
19
20import '../src/common.dart';
21import '../src/context.dart';
22import '../src/mocks.dart';
23
24void main() {
25  group('validateReloadReport', () {
26    testUsingContext('invalid', () async {
27      expect(HotRunner.validateReloadReport(<String, dynamic>{}), false);
28      expect(HotRunner.validateReloadReport(<String, dynamic>{
29        'type': 'ReloadReport',
30        'success': false,
31        'details': <String, dynamic>{},
32      }), false);
33      expect(HotRunner.validateReloadReport(<String, dynamic>{
34        'type': 'ReloadReport',
35        'success': false,
36        'details': <String, dynamic>{
37          'notices': <Map<String, dynamic>>[
38          ],
39        },
40      }), false);
41      expect(HotRunner.validateReloadReport(<String, dynamic>{
42        'type': 'ReloadReport',
43        'success': false,
44        'details': <String, dynamic>{
45          'notices': <String, dynamic>{
46            'message': 'error',
47          },
48        },
49      }), false);
50      expect(HotRunner.validateReloadReport(<String, dynamic>{
51        'type': 'ReloadReport',
52        'success': false,
53        'details': <String, dynamic>{
54          'notices': <Map<String, dynamic>>[],
55        },
56      }), false);
57      expect(HotRunner.validateReloadReport(<String, dynamic>{
58        'type': 'ReloadReport',
59        'success': false,
60        'details': <String, dynamic>{
61          'notices': <Map<String, dynamic>>[
62            <String, dynamic>{'message': false},
63          ],
64        },
65      }), false);
66      expect(HotRunner.validateReloadReport(<String, dynamic>{
67        'type': 'ReloadReport',
68        'success': false,
69        'details': <String, dynamic>{
70          'notices': <Map<String, dynamic>>[
71            <String, dynamic>{'message': <String>['error']},
72          ],
73        },
74      }), false);
75      expect(HotRunner.validateReloadReport(<String, dynamic>{
76        'type': 'ReloadReport',
77        'success': false,
78        'details': <String, dynamic>{
79          'notices': <Map<String, dynamic>>[
80            <String, dynamic>{'message': 'error'},
81            <String, dynamic>{'message': <String>['error']},
82          ],
83        },
84      }), false);
85      expect(HotRunner.validateReloadReport(<String, dynamic>{
86        'type': 'ReloadReport',
87        'success': false,
88        'details': <String, dynamic>{
89          'notices': <Map<String, dynamic>>[
90            <String, dynamic>{'message': 'error'},
91          ],
92        },
93      }), false);
94      expect(HotRunner.validateReloadReport(<String, dynamic>{
95        'type': 'ReloadReport',
96        'success': true,
97      }), true);
98    });
99  });
100
101  group('hotRestart', () {
102    final MockResidentCompiler residentCompiler = MockResidentCompiler();
103    final MockDevFs mockDevFs = MockDevFs();
104    MockLocalEngineArtifacts mockArtifacts;
105
106    when(mockDevFs.update(
107      mainPath: anyNamed('mainPath'),
108      target: anyNamed('target'),
109      bundle: anyNamed('bundle'),
110      firstBuildTime: anyNamed('firstBuildTime'),
111      bundleFirstUpload: anyNamed('bundleFirstUpload'),
112      generator: anyNamed('generator'),
113      fullRestart: anyNamed('fullRestart'),
114      dillOutputPath: anyNamed('dillOutputPath'),
115      trackWidgetCreation: anyNamed('trackWidgetCreation'),
116      projectRootPath: anyNamed('projectRootPath'),
117      pathToReload: anyNamed('pathToReload'),
118      invalidatedFiles: anyNamed('invalidatedFiles'),
119    )).thenAnswer((Invocation _) => Future<UpdateFSReport>.value(
120        UpdateFSReport(success: true, syncedBytes: 1000, invalidatedSourcesCount: 1)));
121    when(mockDevFs.assetPathsToEvict).thenReturn(<String>{});
122    when(mockDevFs.baseUri).thenReturn(Uri.file('test'));
123    when(mockDevFs.sources).thenReturn(<Uri>[Uri.file('test')]);
124    when(mockDevFs.lastCompiled).thenReturn(DateTime.now());
125
126    setUp(() {
127      mockArtifacts = MockLocalEngineArtifacts();
128      when(mockArtifacts.getArtifactPath(Artifact.flutterPatchedSdkPath)).thenReturn('some/path');
129    });
130
131    testUsingContext('Does not hot restart when device does not support it', () async {
132      // Setup mocks
133      final MockDevice mockDevice = MockDevice();
134      when(mockDevice.supportsHotReload).thenReturn(true);
135      when(mockDevice.supportsHotRestart).thenReturn(false);
136      when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester);
137      // Trigger hot restart.
138      final List<FlutterDevice> devices = <FlutterDevice>[
139        FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug)..devFS = mockDevFs,
140      ];
141      final OperationResult result = await HotRunner(devices).restart(fullRestart: true);
142      // Expect hot restart failed.
143      expect(result.isOk, false);
144      expect(result.message, 'hotRestart not supported');
145    }, overrides: <Type, Generator>{
146      Artifacts: () => mockArtifacts,
147      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
148    });
149
150    testUsingContext('Does not hot restart when one of many devices does not support it', () async {
151      // Setup mocks
152      final MockDevice mockDevice = MockDevice();
153      final MockDevice mockHotDevice = MockDevice();
154      when(mockDevice.supportsHotReload).thenReturn(true);
155      when(mockDevice.supportsHotRestart).thenReturn(false);
156      when(mockHotDevice.supportsHotReload).thenReturn(true);
157      when(mockHotDevice.supportsHotRestart).thenReturn(true);
158      // Trigger hot restart.
159      final List<FlutterDevice> devices = <FlutterDevice>[
160        FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug)..devFS = mockDevFs,
161        FlutterDevice(mockHotDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug)..devFS = mockDevFs,
162      ];
163      final OperationResult result = await HotRunner(devices).restart(fullRestart: true);
164      // Expect hot restart failed.
165      expect(result.isOk, false);
166      expect(result.message, 'hotRestart not supported');
167    }, overrides: <Type, Generator>{
168      Artifacts: () => mockArtifacts,
169      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
170    });
171
172    testUsingContext('Does hot restarts when all devices support it', () async {
173      // Setup mocks
174      final MockDevice mockDevice = MockDevice();
175      final MockDevice mockHotDevice = MockDevice();
176      when(mockDevice.supportsHotReload).thenReturn(true);
177      when(mockDevice.supportsHotRestart).thenReturn(true);
178      when(mockHotDevice.supportsHotReload).thenReturn(true);
179      when(mockHotDevice.supportsHotRestart).thenReturn(true);
180      // Trigger a restart.
181      final List<FlutterDevice> devices = <FlutterDevice>[
182        FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug)..devFS = mockDevFs,
183        FlutterDevice(mockHotDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug)..devFS = mockDevFs,
184      ];
185      final OperationResult result = await HotRunner(devices).restart(fullRestart: true);
186      // Expect hot restart was successful.
187      expect(result.isOk, true);
188      expect(result.message, isNot('hotRestart not supported'));
189    }, overrides: <Type, Generator>{
190      Artifacts: () => mockArtifacts,
191      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
192    });
193
194    testUsingContext('setup function fails', () async {
195      final MockDevice mockDevice = MockDevice();
196      when(mockDevice.supportsHotReload).thenReturn(true);
197      when(mockDevice.supportsHotRestart).thenReturn(true);
198      when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester);
199      final List<FlutterDevice> devices = <FlutterDevice>[
200        FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug),
201      ];
202      final OperationResult result = await HotRunner(devices).restart(fullRestart: true);
203      expect(result.isOk, false);
204      expect(result.message, 'setupHotRestart failed');
205    }, overrides: <Type, Generator>{
206      Artifacts: () => mockArtifacts,
207      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: false),
208    });
209
210    testUsingContext('hot restart supported', () async {
211      // Setup mocks
212      final MockDevice mockDevice = MockDevice();
213      when(mockDevice.supportsHotReload).thenReturn(true);
214      when(mockDevice.supportsHotRestart).thenReturn(true);
215      when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester);
216      // Trigger hot restart.
217      final List<FlutterDevice> devices = <FlutterDevice>[
218        FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug)..devFS = mockDevFs,
219      ];
220      final OperationResult result = await HotRunner(devices).restart(fullRestart: true);
221      // Expect hot restart successful.
222      expect(result.isOk, true);
223      expect(result.message, isNot('setupHotRestart failed'));
224    }, overrides: <Type, Generator>{
225      Artifacts: () => mockArtifacts,
226      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
227    });
228
229    group('shutdown hook tests', () {
230      TestHotRunnerConfig shutdownTestingConfig;
231
232      setUp(() {
233        shutdownTestingConfig = TestHotRunnerConfig(
234          successfulSetup: true,
235        );
236      });
237
238      testUsingContext('shutdown hook called after signal', () async {
239        final MockDevice mockDevice = MockDevice();
240        when(mockDevice.supportsHotReload).thenReturn(true);
241        when(mockDevice.supportsHotRestart).thenReturn(true);
242        when(mockDevice.supportsFlutterExit).thenReturn(false);
243        final List<FlutterDevice> devices = <FlutterDevice>[
244          FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug),
245        ];
246        await HotRunner(devices).cleanupAfterSignal();
247        expect(shutdownTestingConfig.shutdownHookCalled, true);
248      }, overrides: <Type, Generator>{
249        Artifacts: () => mockArtifacts,
250        HotRunnerConfig: () => shutdownTestingConfig,
251      });
252
253      testUsingContext('shutdown hook called after app stop', () async {
254        final MockDevice mockDevice = MockDevice();
255        when(mockDevice.supportsHotReload).thenReturn(true);
256        when(mockDevice.supportsHotRestart).thenReturn(true);
257        when(mockDevice.supportsFlutterExit).thenReturn(false);
258        final List<FlutterDevice> devices = <FlutterDevice>[
259          FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug),
260        ];
261        await HotRunner(devices).preExit();
262        expect(shutdownTestingConfig.shutdownHookCalled, true);
263      }, overrides: <Type, Generator>{
264        Artifacts: () => mockArtifacts,
265        HotRunnerConfig: () => shutdownTestingConfig,
266      });
267    });
268  });
269
270  group('hot attach', () {
271    MockResidentCompiler residentCompiler = MockResidentCompiler();
272    BufferLogger mockLogger;
273    MockLocalEngineArtifacts mockArtifacts;
274
275    setUp(() {
276      residentCompiler = MockResidentCompiler();
277      mockLogger = BufferLogger();
278      mockArtifacts = MockLocalEngineArtifacts();
279    });
280
281    testUsingContext('Prints message when HttpException is thrown - 1', () async {
282      final MockDevice mockDevice = MockDevice();
283      when(mockDevice.supportsHotReload).thenReturn(true);
284      when(mockDevice.supportsHotRestart).thenReturn(false);
285      when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester);
286      when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation _) async => 'Android 10');
287
288      final List<FlutterDevice> devices = <FlutterDevice>[
289        TestFlutterDevice(
290          device: mockDevice,
291          generator: residentCompiler,
292          exception: const HttpException('Connection closed before full header was received, '
293              'uri = http://127.0.0.1:63394/5ZmLv8A59xY=/ws')
294        ),
295      ];
296
297      final int exitCode = await HotRunner(devices,
298        debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
299      ).attach();
300      expect(exitCode, 2);
301      expect(mockLogger.statusText, contains('If you are using an emulator running Android Q Beta, '
302          'consider using an emulator running API level 29 or lower.'));
303      expect(mockLogger.statusText, contains('Learn more about the status of this issue on '
304          'https://issuetracker.google.com/issues/132325318'));
305    }, overrides: <Type, Generator>{
306      Artifacts: () => mockArtifacts,
307      Logger: () => mockLogger,
308      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
309    });
310
311    testUsingContext('Prints message when HttpException is thrown - 2', () async {
312      final MockDevice mockDevice = MockDevice();
313      when(mockDevice.supportsHotReload).thenReturn(true);
314      when(mockDevice.supportsHotRestart).thenReturn(false);
315      when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester);
316      when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation _) async => 'Android 10');
317
318      final List<FlutterDevice> devices = <FlutterDevice>[
319        TestFlutterDevice(
320          device: mockDevice,
321          generator: residentCompiler,
322          exception: const HttpException(', uri = http://127.0.0.1:63394/5ZmLv8A59xY=/ws')
323        ),
324      ];
325
326      final int exitCode = await HotRunner(devices,
327        debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
328      ).attach();
329      expect(exitCode, 2);
330      expect(mockLogger.statusText, contains('If you are using an emulator running Android Q Beta, '
331          'consider using an emulator running API level 29 or lower.'));
332      expect(mockLogger.statusText, contains('Learn more about the status of this issue on '
333          'https://issuetracker.google.com/issues/132325318'));
334    }, overrides: <Type, Generator>{
335      Artifacts: () => mockArtifacts,
336      Logger: () => mockLogger,
337      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
338    });
339  });
340}
341
342class MockDevFs extends Mock implements DevFS {}
343
344class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {}
345
346class MockDevice extends Mock implements Device {
347  MockDevice() {
348    when(isSupported()).thenReturn(true);
349  }
350}
351
352class TestFlutterDevice extends FlutterDevice {
353  TestFlutterDevice({
354    @required Device device,
355    @required this.exception,
356    @required ResidentCompiler generator
357  })  : assert(exception != null),
358        super(device, buildMode: BuildMode.debug, generator: generator, trackWidgetCreation: false);
359
360  /// The exception to throw when the connect method is called.
361  final Exception exception;
362
363  @override
364  Future<void> connect({
365    ReloadSources reloadSources,
366    Restart restart,
367    CompileExpression compileExpression,
368  }) async {
369    throw exception;
370  }
371}
372
373class TestHotRunnerConfig extends HotRunnerConfig {
374  TestHotRunnerConfig({@required this.successfulSetup});
375  bool successfulSetup;
376  bool shutdownHookCalled = false;
377
378  @override
379  Future<bool> setupHotRestart() async {
380    return successfulSetup;
381  }
382
383  @override
384  Future<void> runPreShutdownOperations() async {
385    shutdownHookCalled = true;
386  }
387}
388