diff --git a/packages/flutter_tools/lib/src/build_runner/devfs_web.dart b/packages/flutter_tools/lib/src/build_runner/devfs_web.dart index d80e67267d8..1dcaf2d7f29 100644 --- a/packages/flutter_tools/lib/src/build_runner/devfs_web.dart +++ b/packages/flutter_tools/lib/src/build_runner/devfs_web.dart @@ -851,7 +851,7 @@ class WebDevFS implements DevFS { success: true, syncedBytes: codeFile.lengthSync(), invalidatedSourcesCount: invalidatedFiles.length, - )..invalidatedModules = modules; + ); } @visibleForTesting diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart index d327867275e..00cfaac0c49 100644 --- a/packages/flutter_tools/lib/src/commands/attach.dart +++ b/packages/flutter_tools/lib/src/commands/attach.dart @@ -15,6 +15,7 @@ import '../base/io.dart'; import '../commands/daemon.dart'; import '../compile.dart'; import '../device.dart'; +import '../features.dart'; import '../fuchsia/fuchsia_device.dart'; import '../globals.dart' as globals; import '../ios/devices.dart'; @@ -26,6 +27,7 @@ import '../resident_runner.dart'; import '../run_cold.dart'; import '../run_hot.dart'; import '../runner/flutter_command.dart'; +import '../widget_cache.dart'; /// A Flutter-command that attaches to applications that have been launched /// without `flutter run`. @@ -369,6 +371,7 @@ class AttachCommand extends FlutterCommand { targetModel: TargetModel(stringArg('target-model')), buildInfo: getBuildInfo(), userIdentifier: userIdentifier, + widgetCache: WidgetCache(featureFlags: featureFlags), ); flutterDevice.observatoryUris = observatoryUris; final List flutterDevices = [flutterDevice]; diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 987dba08da6..16fed434406 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -19,6 +19,7 @@ import '../build_info.dart'; import '../convert.dart'; import '../device.dart'; import '../emulator.dart'; +import '../features.dart'; import '../globals.dart' as globals; import '../project.dart'; import '../resident_runner.dart'; @@ -26,6 +27,7 @@ import '../run_cold.dart'; import '../run_hot.dart'; import '../runner/flutter_command.dart'; import '../web/web_runner.dart'; +import '../widget_cache.dart'; const String protocolVersion = '0.6.0'; @@ -468,6 +470,7 @@ class AppDomain extends Domain { viewFilter: isolateFilter, target: target, buildInfo: options.buildInfo, + widgetCache: WidgetCache(featureFlags: featureFlags), ); ResidentRunner runner; diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 7dec77488fc..5cc468b3d24 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -23,6 +23,7 @@ import '../run_hot.dart'; import '../runner/flutter_command.dart'; import '../tracing.dart'; import '../web/web_runner.dart'; +import '../widget_cache.dart'; import 'daemon.dart'; abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopmentArtifacts { @@ -519,6 +520,7 @@ class RunCommand extends RunCommandBase { target: stringArg('target'), buildInfo: getBuildInfo(), userIdentifier: userIdentifier, + widgetCache: WidgetCache(featureFlags: featureFlags), ), ]; // Only support "web mode" with a single web device due to resident runner diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart index 78f641339a7..a32b29b3a3d 100644 --- a/packages/flutter_tools/lib/src/devfs.dart +++ b/packages/flutter_tools/lib/src/devfs.dart @@ -302,34 +302,32 @@ class UpdateFSReport { bool success = false, int invalidatedSourcesCount = 0, int syncedBytes = 0, - }) { - _success = success; - _invalidatedSourcesCount = invalidatedSourcesCount; - _syncedBytes = syncedBytes; - } + this.fastReassemble, + }) : _success = success, + _invalidatedSourcesCount = invalidatedSourcesCount, + _syncedBytes = syncedBytes; bool get success => _success; int get invalidatedSourcesCount => _invalidatedSourcesCount; int get syncedBytes => _syncedBytes; - /// JavaScript modules produced by the incremental compiler in `dartdevc` - /// mode. - /// - /// Only used for JavaScript compilation. - List invalidatedModules; + bool _success; + bool fastReassemble; + int _invalidatedSourcesCount; + int _syncedBytes; void incorporateResults(UpdateFSReport report) { if (!report._success) { _success = false; } + if (report.fastReassemble != null && fastReassemble != null) { + fastReassemble &= report.fastReassemble; + } else if (report.fastReassemble != null) { + fastReassemble = report.fastReassemble; + } _invalidatedSourcesCount += report._invalidatedSourcesCount; _syncedBytes += report._syncedBytes; - invalidatedModules ??= report.invalidatedModules; } - - bool _success; - int _invalidatedSourcesCount; - int _syncedBytes; } class DevFS { diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 0e0294ab455..2c24cef852a 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -33,6 +33,7 @@ import 'project.dart'; import 'run_cold.dart'; import 'run_hot.dart'; import 'vmservice.dart'; +import 'widget_cache.dart'; class FlutterDevice { FlutterDevice( @@ -45,6 +46,7 @@ class FlutterDevice { TargetPlatform targetPlatform, ResidentCompiler generator, this.userIdentifier, + this.widgetCache, }) : assert(buildInfo.trackWidgetCreation != null), generator = generator ?? ResidentCompiler( globals.artifacts.getArtifactPath( @@ -78,6 +80,7 @@ class FlutterDevice { List experimentalFlags, ResidentCompiler generator, String userIdentifier, + WidgetCache widgetCache, }) async { ResidentCompiler generator; final TargetPlatform targetPlatform = await device.targetPlatform; @@ -167,6 +170,7 @@ class FlutterDevice { generator: generator, buildInfo: buildInfo, userIdentifier: userIdentifier, + widgetCache: widgetCache, ); } @@ -174,6 +178,7 @@ class FlutterDevice { final ResidentCompiler generator; final BuildInfo buildInfo; final String userIdentifier; + final WidgetCache widgetCache; Stream observatoryUris; vm_service.VmService vmService; DevFS devFS; @@ -641,6 +646,44 @@ class FlutterDevice { return 0; } + /// Validates whether this hot reload is a candidate for a fast reassemble. + Future _attemptFastReassembleCheck(List invalidatedFiles, PackageConfig packageConfig) async { + if (invalidatedFiles.length != 1 || widgetCache == null) { + return false; + } + final List views = await vmService.getFlutterViews(); + final String widgetName = await widgetCache?.validateLibrary(invalidatedFiles.single); + if (widgetName == null) { + return false; + } + final String packageUri = packageConfig.toPackageUri(invalidatedFiles.single)?.toString() + ?? invalidatedFiles.single.toString(); + for (final FlutterView view in views) { + final vm_service.Isolate isolate = await vmService.getIsolateOrNull(view.uiIsolate.id); + final vm_service.LibraryRef targetLibrary = isolate.libraries + .firstWhere( + (vm_service.LibraryRef libraryRef) => libraryRef.uri == packageUri, + orElse: () => null, + ); + if (targetLibrary == null) { + return false; + } + try { + // Evaluate an expression to allow type checking for that invalidated widget + // name. For more information, see `debugFastReassembleMethod` in flutter/src/widgets/binding.dart + await vmService.evaluate( + view.uiIsolate.id, + targetLibrary.id, + '((){debugFastReassembleMethod=(Object _fastReassembleParam) => _fastReassembleParam is $widgetName})()', + ); + } on Exception catch (err) { + globals.printTrace(err.toString()); + return false; + } + } + return true; + } + Future updateDevFS({ Uri mainUri, String target, @@ -660,22 +703,34 @@ class FlutterDevice { timeout: timeoutConfiguration.fastOperation, ); UpdateFSReport report; + bool fastReassemble = false; try { - report = await devFS.update( - mainUri: mainUri, - target: target, - bundle: bundle, - firstBuildTime: firstBuildTime, - bundleFirstUpload: bundleFirstUpload, - generator: generator, - fullRestart: fullRestart, - dillOutputPath: dillOutputPath, - trackWidgetCreation: buildInfo.trackWidgetCreation, - projectRootPath: projectRootPath, - pathToReload: pathToReload, - invalidatedFiles: invalidatedFiles, - packageConfig: packageConfig, - ); + await Future.wait(>[ + devFS.update( + mainUri: mainUri, + target: target, + bundle: bundle, + firstBuildTime: firstBuildTime, + bundleFirstUpload: bundleFirstUpload, + generator: generator, + fullRestart: fullRestart, + dillOutputPath: dillOutputPath, + trackWidgetCreation: buildInfo.trackWidgetCreation, + projectRootPath: projectRootPath, + pathToReload: pathToReload, + invalidatedFiles: invalidatedFiles, + packageConfig: packageConfig, + ).then((UpdateFSReport newReport) => report = newReport), + if (!fullRestart) + _attemptFastReassembleCheck( + invalidatedFiles, + packageConfig, + ).then((bool newFastReassemble) => fastReassemble = newFastReassemble) + ]); + if (fastReassemble) { + globals.logger.printTrace('Attempting fast reassemble.'); + } + report.fastReassemble = fastReassemble; } on DevFSException { devFSStatus.cancel(); return UpdateFSReport(success: false); diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index cc259c1dde6..d217e53d84e 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -154,62 +154,15 @@ class HotRunner extends ResidentRunner { @override Future reloadMethod({ String libraryId, String classId }) async { - final Stopwatch stopwatch = Stopwatch()..start(); - final UpdateFSReport results = UpdateFSReport(success: true); - final List invalidated = [Uri.parse(libraryId)]; - final PackageConfig packageConfig = await loadPackageConfigWithLogging( - globals.fs.file(debuggingOptions.buildInfo.packagesPath), - logger: globals.logger, - ); - for (final FlutterDevice device in flutterDevices) { - results.incorporateResults(await device.updateDevFS( - mainUri: globals.fs.file(mainPath).absolute.uri, - target: target, - bundle: assetBundle, - firstBuildTime: firstBuildTime, - bundleFirstUpload: false, - bundleDirty: false, - fullRestart: false, - projectRootPath: projectRootPath, - pathToReload: getReloadPath(fullRestart: false), - invalidatedFiles: invalidated, - packageConfig: packageConfig, - dillOutputPath: dillOutputPath, - )); - } - if (!results.success) { - return OperationResult(1, 'Failed to compile'); - } - try { - final String entryPath = globals.fs.path.relative( - getReloadPath(fullRestart: false), - from: projectRootPath, + final OperationResult result = await restart(pause: false); + if (!result.isOk) { + throw vm_service.RPCError( + 'Unable to reload sources', + RPCErrorCodes.kInternalError, + '', ); - for (final FlutterDevice device in flutterDevices) { - final List> reportFutures = await device.reloadSources( - entryPath, pause: false, - ); - final List reports = await Future.wait(reportFutures); - final vm_service.ReloadReport firstReport = reports.first; - await device.updateReloadStatus(validateReloadReport(firstReport.json, printErrors: false)); - } - } on Exception catch (error) { - return OperationResult(1, error.toString()); } - - for (final FlutterDevice device in flutterDevices) { - final List views = await device.vmService.getFlutterViews(); - for (final FlutterView view in views) { - await device.vmService.flutterFastReassemble( - classId, - isolateId: view.uiIsolate.id, - ); - } - } - - globals.printStatus('reloadMethod took ${stopwatch.elapsedMilliseconds}'); - globals.flutterUsage.sendTiming('hot', 'ui', stopwatch.elapsed); - return OperationResult.ok; + return result; } // Returns the exit code of the flutter tool process, like [run]. @@ -942,9 +895,19 @@ class HotRunner extends ResidentRunner { } } else { reassembleViews[view] = device.vmService; - reassembleFutures.add(device.vmService.flutterReassemble( - isolateId: view.uiIsolate.id, - ).catchError((dynamic error) { + // If the tool identified a change in a single widget, do a fast instead + // of a full reassemble. + Future reassembleWork; + if (updatedDevFS.fastReassemble == true) { + reassembleWork = device.vmService.flutterFastReassemble( + isolateId: view.uiIsolate.id, + ); + } else { + reassembleWork = device.vmService.flutterReassemble( + isolateId: view.uiIsolate.id, + ); + } + reassembleFutures.add(reassembleWork.catchError((dynamic error) { failedReassemble = true; globals.printError('Reassembling ${view.uiIsolate.name} failed: $error'); }, test: (dynamic error) => error is Exception)); diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index 31a5ede978b..a1581f9bc8a 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart @@ -628,15 +628,13 @@ extension FlutterVmService on vm_service.VmService { ); } - Future> flutterFastReassemble(String classId, { + Future> flutterFastReassemble({ @required String isolateId, }) { return invokeFlutterExtensionRpcRaw( 'ext.flutter.fastReassemble', isolateId: isolateId, - args: { - 'class': classId, - }, + args: {}, ); } diff --git a/packages/flutter_tools/lib/src/widget_cache.dart b/packages/flutter_tools/lib/src/widget_cache.dart new file mode 100644 index 00000000000..a74ae9c42ea --- /dev/null +++ b/packages/flutter_tools/lib/src/widget_cache.dart @@ -0,0 +1,28 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; + +import 'features.dart'; + +/// The widget cache determines if the body of a single widget was modified since +/// the last scan of the token stream. +class WidgetCache { + WidgetCache({ + @required FeatureFlags featureFlags, + }) : _featureFlags = featureFlags; + + final FeatureFlags _featureFlags; + + /// If the build method of a single widget was modified, return the widget name. + /// + /// If any other changes were made, or there is an error scanning the file, + /// return `null`. + Future validateLibrary(Uri libraryUri) async { + if (!_featureFlags.isSingleWidgetReloadEnabled) { + return null; + } + return null; + } +} diff --git a/packages/flutter_tools/test/general.shard/project_file_invalidator_test.dart b/packages/flutter_tools/test/general.shard/project_file_invalidator_test.dart index 5d36d25405e..f5a9041438d 100644 --- a/packages/flutter_tools/test/general.shard/project_file_invalidator_test.dart +++ b/packages/flutter_tools/test/general.shard/project_file_invalidator_test.dart @@ -84,7 +84,7 @@ void main() { ', asyncScanning: $asyncScanning', () async { final DateTime past = DateTime.now().subtract(const Duration(seconds: 1)); final FileSystem fileSystem = MemoryFileSystem.test(); - final PackageConfig packageConfig = PackageConfig.empty; + const PackageConfig packageConfig = PackageConfig.empty; final ProjectFileInvalidator projectFileInvalidator = ProjectFileInvalidator( fileSystem: fileSystem, platform: FakePlatform(), @@ -126,7 +126,7 @@ void main() { testWithoutContext('Picks up changes to the .packages file and updates PackageConfig' ', asyncScanning: $asyncScanning', () async { final FileSystem fileSystem = MemoryFileSystem.test(); - final PackageConfig packageConfig = PackageConfig.empty; + const PackageConfig packageConfig = PackageConfig.empty; final ProjectFileInvalidator projectFileInvalidator = ProjectFileInvalidator( fileSystem: fileSystem, platform: FakePlatform(), diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart index 027de7c7577..c0dc95878d9 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart @@ -4,7 +4,11 @@ import 'dart:async'; +import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/widget_cache.dart'; +import 'package:meta/meta.dart'; +import 'package:package_config/package_config.dart'; import 'package:vm_service/vm_service.dart' as vm_service; import 'package:file/memory.dart'; import 'package:file_testing/file_testing.dart'; @@ -40,7 +44,13 @@ final vm_service.Isolate fakeUnpausedIsolate = vm_service.Isolate( ), breakpoints: [], exceptionPauseMode: null, - libraries: [], + libraries: [ + vm_service.LibraryRef( + id: '1', + uri: 'file:///hello_world/main.dart', + name: '', + ), + ], livePorts: 0, name: 'test', number: '1', @@ -66,6 +76,19 @@ final vm_service.Isolate fakePausedIsolate = vm_service.Isolate( startTime: 0, ); +final vm_service.VM fakeVM = vm_service.VM( + isolates: [fakeUnpausedIsolate], + pid: 1, + hostCPU: '', + isolateGroups: [], + targetCPU: '', + startTime: 0, + name: 'dart', + architectureBits: 64, + operatingSystem: '', + version: '', +); + final FlutterView fakeFlutterView = FlutterView( id: 'a', uiIsolate: fakeUnpausedIsolate, @@ -622,6 +645,260 @@ void main() { Usage: () => MockUsage(), })); + testUsingContext('ResidentRunner can perform fast reassemble', () => testbed.run(() async { + fakeVmServiceHost = FakeVmServiceHost(requests: [ + listViews, + FakeVmServiceRequest( + method: 'getVM', + jsonResponse: fakeVM.toJson(), + ), + listViews, + listViews, + listViews, + FakeVmServiceRequest( + method: 'getIsolate', + args: { + 'isolateId': '1', + }, + jsonResponse: fakeUnpausedIsolate.toJson(), + ), + const FakeVmServiceRequest( + method: 'evaluate', + args: { + 'isolateId': '1', + 'targetId': '1', + 'expression': '((){debugFastReassembleMethod=(Object _fastReassembleParam) => _fastReassembleParam is FakeWidget})()', + } + ), + listViews, + const FakeVmServiceRequest( + method: '_flutter.setAssetBundlePath', + args: { + 'viewId': 'a', + 'assetDirectory': 'build/flutter_assets', + 'isolateId': '1', + } + ), + FakeVmServiceRequest( + method: 'getVM', + jsonResponse: fakeVM.toJson(), + ), + const FakeVmServiceRequest( + method: 'reloadSources', + args: { + 'isolateId': '1', + 'pause': false, + 'rootLibUri': 'lib/main.dart.incremental.dill', + }, + jsonResponse: { + 'type': 'ReloadReport', + 'success': true, + 'details': { + 'loadedLibraryCount': 1, + }, + }, + ), + listViews, + FakeVmServiceRequest( + method: 'getIsolate', + args: { + 'isolateId': '1', + }, + jsonResponse: fakeUnpausedIsolate.toJson(), + ), + FakeVmServiceRequest( + method: 'ext.flutter.fastReassemble', + args: { + 'isolateId': fakeUnpausedIsolate.id, + }, + ), + ]); + final FakeFlutterDevice flutterDevice = FakeFlutterDevice( + mockDevice, + BuildInfo.debug, + FakeWidgetCache(), + FakeResidentCompiler(), + mockDevFS, + )..vmService = fakeVmServiceHost.vmService; + residentRunner = HotRunner( + [ + flutterDevice, + ], + stayResident: false, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + ); + when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async { + return 'Example'; + }); + when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async { + return TargetPlatform.android_arm; + }); + when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async { + return false; + }); + when(mockDevice.getLogReader(app: anyNamed('app'))).thenReturn(NoOpDeviceLogReader('test')); + when(mockDevFS.update( + mainUri: anyNamed('mainUri'), + 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'), + invalidatedFiles: anyNamed('invalidatedFiles'), + packageConfig: anyNamed('packageConfig'), + )).thenAnswer((Invocation invocation) async { + return UpdateFSReport(success: true); + }); + + final Completer onConnectionInfo = Completer.sync(); + final Completer onAppStart = Completer.sync(); + unawaited(residentRunner.attach( + appStartedCompleter: onAppStart, + connectionInfoCompleter: onConnectionInfo, + )); + + final OperationResult result = await residentRunner.restart(fullRestart: false); + expect(result.fatal, false); + expect(result.code, 0); + }, overrides: { + FileSystem: () => MemoryFileSystem.test(), + Platform: () => FakePlatform(operatingSystem: 'linux'), + ProjectFileInvalidator: () => FakeProjectFileInvalidator(), + })); + + testUsingContext('ResidentRunner bails out of fast reassemble if evaluation fails', () => testbed.run(() async { + fakeVmServiceHost = FakeVmServiceHost(requests: [ + listViews, + FakeVmServiceRequest( + method: 'getVM', + jsonResponse: fakeVM.toJson(), + ), + listViews, + listViews, + listViews, + FakeVmServiceRequest( + method: 'getIsolate', + args: { + 'isolateId': '1', + }, + jsonResponse: fakeUnpausedIsolate.toJson(), + ), + const FakeVmServiceRequest( + method: 'evaluate', + args: { + 'isolateId': '1', + 'targetId': '1', + 'expression': '((){debugFastReassembleMethod=(Object _fastReassembleParam) => _fastReassembleParam is FakeWidget})()', + }, + errorCode: 500, + ), + listViews, + const FakeVmServiceRequest( + method: '_flutter.setAssetBundlePath', + args: { + 'viewId': 'a', + 'assetDirectory': 'build/flutter_assets', + 'isolateId': '1', + } + ), + FakeVmServiceRequest( + method: 'getVM', + jsonResponse: fakeVM.toJson(), + ), + const FakeVmServiceRequest( + method: 'reloadSources', + args: { + 'isolateId': '1', + 'pause': false, + 'rootLibUri': 'lib/main.dart.incremental.dill', + }, + jsonResponse: { + 'type': 'ReloadReport', + 'success': true, + 'details': { + 'loadedLibraryCount': 1, + }, + }, + ), + listViews, + FakeVmServiceRequest( + method: 'getIsolate', + args: { + 'isolateId': '1', + }, + jsonResponse: fakeUnpausedIsolate.toJson(), + ), + FakeVmServiceRequest( + method: 'ext.flutter.reassemble', + args: { + 'isolateId': fakeUnpausedIsolate.id, + }, + ), + ]); + final FakeFlutterDevice flutterDevice = FakeFlutterDevice( + mockDevice, + BuildInfo.debug, + FakeWidgetCache(), + FakeResidentCompiler(), + mockDevFS, + )..vmService = fakeVmServiceHost.vmService; + residentRunner = HotRunner( + [ + flutterDevice, + ], + stayResident: false, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + ); + when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async { + return 'Example'; + }); + when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async { + return TargetPlatform.android_arm; + }); + when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async { + return false; + }); + when(mockDevice.getLogReader(app: anyNamed('app'))).thenReturn(NoOpDeviceLogReader('test')); + when(mockDevFS.update( + mainUri: anyNamed('mainUri'), + 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'), + invalidatedFiles: anyNamed('invalidatedFiles'), + packageConfig: anyNamed('packageConfig'), + )).thenAnswer((Invocation invocation) async { + return UpdateFSReport(success: true); + }); + + final Completer onConnectionInfo = Completer.sync(); + final Completer onAppStart = Completer.sync(); + unawaited(residentRunner.attach( + appStartedCompleter: onAppStart, + connectionInfoCompleter: onConnectionInfo, + )); + + final OperationResult result = await residentRunner.restart(fullRestart: false); + expect(result.fatal, false); + expect(result.code, 0); + }, overrides: { + FileSystem: () => MemoryFileSystem.test(), + Platform: () => FakePlatform(operatingSystem: 'linux'), + ProjectFileInvalidator: () => FakeProjectFileInvalidator(), + })); + + testUsingContext('ResidentRunner can send target platform to analytics from full restart', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, @@ -1740,3 +2017,74 @@ class ThrowingForwardingFileSystem extends ForwardingFileSystem { return delegate.file(path); } } + +class FakeWidgetCache implements WidgetCache { + @override + Future validateLibrary(Uri libraryUri) async { + return 'FakeWidget'; + } +} + +class FakeFlutterDevice extends FlutterDevice { + FakeFlutterDevice( + Device device, + BuildInfo buildInfo, + WidgetCache widgetCache, + ResidentCompiler residentCompiler, + this.fakeDevFS, + ) : super(device, buildInfo: buildInfo, widgetCache:widgetCache, generator: residentCompiler); + + @override + Future connect({ + ReloadSources reloadSources, + Restart restart, + CompileExpression compileExpression, + ReloadMethod reloadMethod, + GetSkSLMethod getSkSLMethod, + PrintStructuredErrorLogMethod printStructuredErrorLogMethod, + }) async { } + + + final DevFS fakeDevFS; + + @override + DevFS get devFS => fakeDevFS; + + @override + set devFS(DevFS value) {} +} + +class FakeResidentCompiler extends Fake implements ResidentCompiler { + @override + Future recompile( + Uri mainUri, + List invalidatedFiles, { + @required String outputPath, + @required PackageConfig packageConfig, + bool suppressErrors = false, + }) async { + return const CompilerOutput('foo.dill', 0, []); + } + + @override + void accept() { } + + @override + void reset() { } +} + +class FakeProjectFileInvalidator extends Fake implements ProjectFileInvalidator { + @override + Future findInvalidated({ + @required DateTime lastCompiled, + @required List urisToMonitor, + @required String packagesPath, + @required PackageConfig packageConfig, + bool asyncScanning = false, + }) async { + return InvalidationResult( + packageConfig: packageConfig ?? PackageConfig.empty, + uris: [Uri.parse('file:///hello_world/main.dart'), + ]); + } +} diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index 9f2725cce24..78494a4183b 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -139,7 +139,7 @@ void main() { trackWidgetCreation: anyNamed('trackWidgetCreation'), packageConfig: anyNamed('packageConfig'), )).thenAnswer((Invocation _) async { - return UpdateFSReport(success: true, syncedBytes: 0)..invalidatedModules = []; + return UpdateFSReport(success: true, syncedBytes: 0); }); when(mockDebugConnection.vmService).thenAnswer((Invocation invocation) { return fakeVmServiceHost.vmService; @@ -352,7 +352,7 @@ void main() { trackWidgetCreation: anyNamed('trackWidgetCreation'), packageConfig: anyNamed('packageConfig'), )).thenAnswer((Invocation _) async { - return UpdateFSReport(success: false, syncedBytes: 0)..invalidatedModules = []; + return UpdateFSReport(success: false, syncedBytes: 0); }); expect(await residentWebRunner.run(), 1); @@ -587,8 +587,7 @@ void main() { )).thenAnswer((Invocation invocation) async { // Generated entrypoint file in temp dir. expect(invocation.namedArguments[#mainUri].toString(), contains('entrypoint.dart')); - return UpdateFSReport(success: true) - ..invalidatedModules = ['example']; + return UpdateFSReport(success: true); }); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( @@ -668,8 +667,7 @@ void main() { packageConfig: anyNamed('packageConfig'), )).thenAnswer((Invocation invocation) async { entrypointFileUri = invocation.namedArguments[#mainUri] as Uri; - return UpdateFSReport(success: true) - ..invalidatedModules = ['example']; + return UpdateFSReport(success: true); }); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( @@ -727,8 +725,7 @@ void main() { invalidatedFiles: anyNamed('invalidatedFiles'), packageConfig: anyNamed('packageConfig'), )).thenAnswer((Invocation invocation) async { - return UpdateFSReport(success: true) - ..invalidatedModules = ['example']; + return UpdateFSReport(success: true); }); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( @@ -801,7 +798,7 @@ void main() { packageConfig: anyNamed('packageConfig'), trackWidgetCreation: anyNamed('trackWidgetCreation'), )).thenAnswer((Invocation _) async { - return UpdateFSReport(success: false, syncedBytes: 0)..invalidatedModules = []; + return UpdateFSReport(success: false, syncedBytes: 0); }); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( @@ -875,7 +872,7 @@ void main() { packageConfig: anyNamed('packageConfig'), trackWidgetCreation: anyNamed('trackWidgetCreation'), )).thenAnswer((Invocation _) async { - return UpdateFSReport(success: false, syncedBytes: 0)..invalidatedModules = []; + return UpdateFSReport(success: false, syncedBytes: 0); }); final OperationResult result = await residentWebRunner.restart(fullRestart: true); diff --git a/packages/flutter_tools/test/general.shard/widget_cache_test.dart b/packages/flutter_tools/test/general.shard/widget_cache_test.dart new file mode 100644 index 00000000000..80d1a4d10cb --- /dev/null +++ b/packages/flutter_tools/test/general.shard/widget_cache_test.dart @@ -0,0 +1,22 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_tools/src/widget_cache.dart'; + +import '../src/common.dart'; +import '../src/testbed.dart'; + +void main() { + testWithoutContext('widget cache returns null when experiment is disabled', () async { + final WidgetCache widgetCache = WidgetCache(featureFlags: TestFeatureFlags(isSingleWidgetReloadEnabled: false)); + + expect(await widgetCache.validateLibrary(Uri.parse('package:hello_world/main.dart')), null); + }); + + testWithoutContext('widget cache returns null because functionality is not complete', () async { + final WidgetCache widgetCache = WidgetCache(featureFlags: TestFeatureFlags(isSingleWidgetReloadEnabled: true)); + + expect(await widgetCache.validateLibrary(Uri.parse('package:hello_world/main.dart')), null); + }); +}