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