Unverified Commit bdb830a8 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] pretty print hot reload rejection error (#66701)

If the vm of an attached device rejects a hot reload, pretty print the reason. Suggest a hot restart so that users are aware that they do not have to detach and rebuild. Also resets the last compilation time, so a subsequent restart would still apply the last change. Adds an integration test for the const field removal.

Fixes #64027
parent 5fa80171
...@@ -207,6 +207,9 @@ class DevFSException implements Exception { ...@@ -207,6 +207,9 @@ class DevFSException implements Exception {
final String message; final String message;
final dynamic error; final dynamic error;
final StackTrace stackTrace; final StackTrace stackTrace;
@override
String toString() => 'DevFSException($message, $error, $stackTrace)';
} }
/// Interface responsible for syncing asset files to a development device. /// Interface responsible for syncing asset files to a development device.
...@@ -399,6 +402,7 @@ class DevFS { ...@@ -399,6 +402,7 @@ class DevFS {
List<Uri> sources = <Uri>[]; List<Uri> sources = <Uri>[];
DateTime lastCompiled; DateTime lastCompiled;
DateTime _previousCompiled;
PackageConfig lastPackageConfig; PackageConfig lastPackageConfig;
File _widgetCacheOutputFile; File _widgetCacheOutputFile;
...@@ -440,6 +444,20 @@ class DevFS { ...@@ -440,6 +444,20 @@ class DevFS {
_logger.printTrace('DevFS: Deleted filesystem on the device ($_baseUri)'); _logger.printTrace('DevFS: Deleted filesystem on the device ($_baseUri)');
} }
/// Mark the [lastCompiled] time to the previous successful compile.
///
/// Sometimes a hot reload will be rejected by the VM due to a change in the
/// structure of the code not supporting the hot reload. In these cases,
/// the best resolution is a hot restart. However, the resident runner
/// will not recognize this file as having been changed since the delta
/// will already have been accepted. Instead, reset the compile time so
/// that the last updated files are included in subsequent compilations until
/// a reload is accepted.
void resetLastCompiled() {
lastCompiled = _previousCompiled;
}
/// If the build method of a single widget was modified, return the widget name. /// 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, /// If any other changes were made, or there is an error scanning the file,
...@@ -520,6 +538,7 @@ class DevFS { ...@@ -520,6 +538,7 @@ class DevFS {
return UpdateFSReport(success: false); return UpdateFSReport(success: false);
} }
// Only update the last compiled time if we successfully compiled. // Only update the last compiled time if we successfully compiled.
_previousCompiled = lastCompiled;
lastCompiled = candidateCompileTime; lastCompiled = candidateCompileTime;
// list of sources that needs to be monitored are in [compilerOutput.sources] // list of sources that needs to be monitored are in [compilerOutput.sources]
sources = compilerOutput.sources; sources = compilerOutput.sources;
......
...@@ -926,6 +926,11 @@ class WebDevFS implements DevFS { ...@@ -926,6 +926,11 @@ class WebDevFS implements DevFS {
'web', 'web',
'dart_stack_trace_mapper.js', 'dart_stack_trace_mapper.js',
)); ));
@override
void resetLastCompiled() {
// Not used for web compilation.
}
} }
class ReleaseAssetServer { class ReleaseAssetServer {
......
...@@ -358,37 +358,6 @@ class FlutterDevice { ...@@ -358,37 +358,6 @@ class FlutterDevice {
return devFS.create(); return devFS.create();
} }
Future<List<Future<vm_service.ReloadReport>>> reloadSources(
String entryPath, {
bool pause = false,
}) async {
final String deviceEntryUri = devFS.baseUri
.resolveUri(globals.fs.path.toUri(entryPath)).toString();
final vm_service.VM vm = await vmService.getVM();
return <Future<vm_service.ReloadReport>>[
for (final vm_service.IsolateRef isolateRef in vm.isolates)
vmService.reloadSources(
isolateRef.id,
pause: pause,
rootLibUri: deviceEntryUri,
)
];
}
Future<void> resetAssetDirectory() async {
final Uri deviceAssetsDirectoryUri = devFS.baseUri.resolveUri(
globals.fs.path.toUri(getAssetBuildDirectory()));
assert(deviceAssetsDirectoryUri != null);
final List<FlutterView> views = await vmService.getFlutterViews();
await Future.wait<void>(views.map<Future<void>>(
(FlutterView view) => vmService.setAssetDirectory(
assetsDirectory: deviceAssetsDirectoryUri,
uiIsolateId: view.uiIsolate.id,
viewId: view.id,
)
));
}
Future<void> debugDumpApp() async { Future<void> debugDumpApp() async {
final List<FlutterView> views = await vmService.getFlutterViews(); final List<FlutterView> views = await vmService.getFlutterViews();
for (final FlutterView view in views) { for (final FlutterView view in views) {
......
...@@ -406,6 +406,12 @@ class HotRunner extends ResidentRunner { ...@@ -406,6 +406,12 @@ class HotRunner extends ResidentRunner {
return results; return results;
} }
void _resetDevFSCompileTime() {
for (final FlutterDevice device in flutterDevices) {
device.devFS.resetLastCompiled();
}
}
void _resetDirtyAssets() { void _resetDirtyAssets() {
for (final FlutterDevice device in flutterDevices) { for (final FlutterDevice device in flutterDevices) {
device.devFS.assetPathsToEvict.clear(); device.devFS.assetPathsToEvict.clear();
...@@ -593,7 +599,7 @@ class HotRunner extends ResidentRunner { ...@@ -593,7 +599,7 @@ class HotRunner extends ResidentRunner {
/// Returns [true] if the reload was successful. /// Returns [true] if the reload was successful.
/// Prints errors if [printErrors] is [true]. /// Prints errors if [printErrors] is [true].
static bool validateReloadReport( static bool validateReloadReport(
Map<String, dynamic> reloadReport, { vm_service.ReloadReport reloadReport, {
bool printErrors = true, bool printErrors = true,
}) { }) {
if (reloadReport == null) { if (reloadReport == null) {
...@@ -602,29 +608,12 @@ class HotRunner extends ResidentRunner { ...@@ -602,29 +608,12 @@ class HotRunner extends ResidentRunner {
} }
return false; return false;
} }
if (!(reloadReport['type'] == 'ReloadReport' && final ReloadReportContents contents = ReloadReportContents.fromReloadReport(reloadReport);
(reloadReport['success'] == true || if (!reloadReport.success) {
(reloadReport['success'] == false &&
(reloadReport['details'] is Map<String, dynamic> &&
reloadReport['details']['notices'] is List<dynamic> &&
(reloadReport['details']['notices'] as List<dynamic>).isNotEmpty &&
(reloadReport['details']['notices'] as List<dynamic>).every(
(dynamic item) => item is Map<String, dynamic> && item['message'] is String
)
)
)
)
)) {
if (printErrors) {
globals.printError('Hot reload received invalid response: $reloadReport');
}
return false;
}
if (!(reloadReport['success'] as bool)) {
if (printErrors) { if (printErrors) {
globals.printError('Hot reload was rejected:'); globals.printError('Hot reload was rejected:');
for (final Map<String, dynamic> notice in (reloadReport['details']['notices'] as List<dynamic>).cast<Map<String, dynamic>>()) { for (final ReasonForCancelling reason in contents.notices) {
globals.printError('${notice['message']}'); globals.printError(reason.toString());
} }
} }
return false; return false;
...@@ -798,6 +787,38 @@ class HotRunner extends ResidentRunner { ...@@ -798,6 +787,38 @@ class HotRunner extends ResidentRunner {
return result; return result;
} }
Future<List<Future<vm_service.ReloadReport>>> _reloadDeviceSources(
FlutterDevice device,
String entryPath, {
bool pause = false,
}) async {
final String deviceEntryUri = device.devFS.baseUri
.resolveUri(globals.fs.path.toUri(entryPath)).toString();
final vm_service.VM vm = await device.vmService.getVM();
return <Future<vm_service.ReloadReport>>[
for (final vm_service.IsolateRef isolateRef in vm.isolates)
device.vmService.reloadSources(
isolateRef.id,
pause: pause,
rootLibUri: deviceEntryUri,
)
];
}
Future<void> _resetAssetDirectory(FlutterDevice device) async {
final Uri deviceAssetsDirectoryUri = device.devFS.baseUri.resolveUri(
globals.fs.path.toUri(getAssetBuildDirectory()));
assert(deviceAssetsDirectoryUri != null);
final List<FlutterView> views = await device.vmService.getFlutterViews();
await Future.wait<void>(views.map<Future<void>>(
(FlutterView view) => device.vmService.setAssetDirectory(
assetsDirectory: deviceAssetsDirectoryUri,
uiIsolateId: view.uiIsolate.id,
viewId: view.id,
)
));
}
Future<OperationResult> _reloadSources({ Future<OperationResult> _reloadSources({
String targetPlatform, String targetPlatform,
String sdkName, String sdkName,
...@@ -835,12 +856,13 @@ class HotRunner extends ResidentRunner { ...@@ -835,12 +856,13 @@ class HotRunner extends ResidentRunner {
final List<Future<DeviceReloadReport>> allReportsFutures = <Future<DeviceReloadReport>>[]; final List<Future<DeviceReloadReport>> allReportsFutures = <Future<DeviceReloadReport>>[];
for (final FlutterDevice device in flutterDevices) { for (final FlutterDevice device in flutterDevices) {
if (_shouldResetAssetDirectory) { if (_shouldResetAssetDirectory) {
// Asset directory has to be set only once when we switch from // Asset directory has to be set only once when the engine switches from
// running from bundle to uploaded files. // running from bundle to uploaded files.
await device.resetAssetDirectory(); await _resetAssetDirectory(device);
_shouldResetAssetDirectory = false; _shouldResetAssetDirectory = false;
} }
final List<Future<vm_service.ReloadReport>> reportFutures = await device.reloadSources( final List<Future<vm_service.ReloadReport>> reportFutures = await _reloadDeviceSources(
device,
entryPath, pause: pause, entryPath, pause: pause,
); );
allReportsFutures.add(Future.wait(reportFutures).then( allReportsFutures.add(Future.wait(reportFutures).then(
...@@ -851,7 +873,7 @@ class HotRunner extends ResidentRunner { ...@@ -851,7 +873,7 @@ class HotRunner extends ResidentRunner {
// Don't print errors because they will be printed further down when // Don't print errors because they will be printed further down when
// `validateReloadReport` is called again. // `validateReloadReport` is called again.
await device.updateReloadStatus( await device.updateReloadStatus(
validateReloadReport(firstReport.json, printErrors: false), validateReloadReport(firstReport, printErrors: false),
); );
return DeviceReloadReport(device, reports); return DeviceReloadReport(device, reports);
}, },
...@@ -860,7 +882,7 @@ class HotRunner extends ResidentRunner { ...@@ -860,7 +882,7 @@ class HotRunner extends ResidentRunner {
final List<DeviceReloadReport> reports = await Future.wait(allReportsFutures); final List<DeviceReloadReport> reports = await Future.wait(allReportsFutures);
for (final DeviceReloadReport report in reports) { for (final DeviceReloadReport report in reports) {
final vm_service.ReloadReport reloadReport = report.reports[0]; final vm_service.ReloadReport reloadReport = report.reports[0];
if (!validateReloadReport(reloadReport.json)) { if (!validateReloadReport(reloadReport)) {
// Reload failed. // Reload failed.
HotEvent('reload-reject', HotEvent('reload-reject',
targetPlatform: targetPlatform, targetPlatform: targetPlatform,
...@@ -871,7 +893,11 @@ class HotRunner extends ResidentRunner { ...@@ -871,7 +893,11 @@ class HotRunner extends ResidentRunner {
nullSafety: usageNullSafety, nullSafety: usageNullSafety,
fastReassemble: null, fastReassemble: null,
).send(); ).send();
return OperationResult(1, 'Reload rejected'); // Reset devFS lastCompileTime to ensure the file will still be marked
// as dirty on subsequent reloads.
_resetDevFSCompileTime();
final ReloadReportContents contents = ReloadReportContents.fromReloadReport(reloadReport);
return OperationResult(1, 'Reload rejected: ${contents.notices.join("\n")}');
} }
// Collect stats only from the first device. If/when run -d all is // Collect stats only from the first device. If/when run -d all is
// refactored, we'll probably need to send one hot reload/restart event // refactored, we'll probably need to send one hot reload/restart event
...@@ -1307,3 +1333,54 @@ class ProjectFileInvalidator { ...@@ -1307,3 +1333,54 @@ class ProjectFileInvalidator {
); );
} }
} }
/// Additional serialization logic for a hot reload response.
class ReloadReportContents {
factory ReloadReportContents.fromReloadReport(vm_service.ReloadReport report) {
final List<ReasonForCancelling> reasons = <ReasonForCancelling>[];
final Object notices = report.json['notices'];
if (notices is! List<dynamic>) {
return ReloadReportContents._(report.success, reasons, report);
}
for (final Object obj in notices as List<dynamic>) {
if (obj is! Map<String, dynamic>) {
continue;
}
final Map<String, dynamic> notice = obj as Map<String, dynamic>;
reasons.add(ReasonForCancelling(
message: notice['message'] is String
? notice['message'] as String
: 'Unknown Error',
));
}
return ReloadReportContents._(report.success, reasons, report);
}
ReloadReportContents._(
this.success,
this.notices,
this.report,
);
final bool success;
final List<ReasonForCancelling> notices;
final vm_service.ReloadReport report;
}
/// A serialization class for hot reload rejection reasons.
///
/// Injects an additional error message that a hot restart will
/// resolve the issue.
class ReasonForCancelling {
ReasonForCancelling({
this.message,
});
final String message;
@override
String toString() {
return '$message.\nTry performing a hot restart instead.';
}
}
...@@ -17,7 +17,6 @@ import 'package:flutter_tools/src/devfs.dart'; ...@@ -17,7 +17,6 @@ import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/vmservice.dart'; import 'package:flutter_tools/src/vmservice.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:package_config/package_config.dart'; import 'package:package_config/package_config.dart';
import 'package:fake_async/fake_async.dart';
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart'; import '../src/context.dart';
...@@ -142,25 +141,22 @@ void main() { ...@@ -142,25 +141,22 @@ void main() {
); );
await devFS.create(); await devFS.create();
await FakeAsync().run((FakeAsync time) async { final UpdateFSReport report = await devFS.update(
final UpdateFSReport report = await devFS.update( mainUri: Uri.parse('lib/foo.txt'),
mainUri: Uri.parse('lib/foo.txt'), dillOutputPath: 'lib/foo.dill',
dillOutputPath: 'lib/foo.dill', generator: residentCompiler,
generator: residentCompiler, pathToReload: 'lib/foo.txt.dill',
pathToReload: 'lib/foo.txt.dill', trackWidgetCreation: false,
trackWidgetCreation: false, invalidatedFiles: <Uri>[],
invalidatedFiles: <Uri>[], packageConfig: PackageConfig.empty,
packageConfig: PackageConfig.empty, );
);
time.elapse(const Duration(seconds: 2)); expect(report.syncedBytes, 5);
expect(report.success, isTrue);
expect(report.syncedBytes, 5); verify(httpClient.putUrl(any)).called(kFailedAttempts + 1);
expect(report.success, isTrue); verify(httpRequest.close()).called(kFailedAttempts + 1);
verify(httpClient.putUrl(any)).called(kFailedAttempts + 1); verify(osUtils.gzipLevel1Stream(any)).called(kFailedAttempts + 1);
verify(httpRequest.close()).called(kFailedAttempts + 1); });
verify(osUtils.gzipLevel1Stream(any)).called(kFailedAttempts + 1);
});
}, skip: true); // TODO(jonahwilliams): clean up with https://github.com/flutter/flutter/issues/60675
testWithoutContext('DevFS reports unsuccessful compile when errors are returned', () async { testWithoutContext('DevFS reports unsuccessful compile when errors are returned', () async {
final FileSystem fileSystem = MemoryFileSystem.test(); final FileSystem fileSystem = MemoryFileSystem.test();
...@@ -259,6 +255,60 @@ void main() { ...@@ -259,6 +255,60 @@ void main() {
expect(devFS.lastCompiled, isNot(previousCompile)); expect(devFS.lastCompiled, isNot(previousCompile));
}); });
testWithoutContext('DevFS can reset compilation time', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[createDevFSRequest],
);
final LocalDevFSWriter localDevFSWriter = LocalDevFSWriter(fileSystem: fileSystem);
fileSystem.directory('test').createSync();
final DevFS devFS = DevFS(
fakeVmServiceHost.vmService,
'test',
fileSystem.currentDirectory,
fileSystem: fileSystem,
logger: BufferLogger.test(),
osUtils: FakeOperatingSystemUtils(),
httpClient: HttpClient(),
);
await devFS.create();
final DateTime previousCompile = devFS.lastCompiled;
final MockResidentCompiler residentCompiler = MockResidentCompiler();
when(residentCompiler.recompile(
any,
any,
outputPath: anyNamed('outputPath'),
packageConfig: anyNamed('packageConfig'),
)).thenAnswer((Invocation invocation) async {
fileSystem.file('lib/foo.txt.dill').createSync(recursive: true);
return const CompilerOutput('lib/foo.txt.dill', 0, <Uri>[]);
});
final UpdateFSReport report = await devFS.update(
mainUri: Uri.parse('lib/main.dart'),
generator: residentCompiler,
dillOutputPath: 'lib/foo.dill',
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
invalidatedFiles: <Uri>[],
packageConfig: PackageConfig.empty,
devFSWriter: localDevFSWriter,
);
expect(report.success, true);
expect(devFS.lastCompiled, isNot(previousCompile));
devFS.resetLastCompiled();
expect(devFS.lastCompiled, previousCompile);
// Does not reset to report compile time.
devFS.resetLastCompiled();
expect(devFS.lastCompiled, previousCompile);
});
testWithoutContext('DevFS uses provided DevFSWriter instead of default HTTP writer', () async { testWithoutContext('DevFS uses provided DevFSWriter instead of default HTTP writer', () async {
final FileSystem fileSystem = MemoryFileSystem.test(); final FileSystem fileSystem = MemoryFileSystem.test();
final FakeDevFSWriter writer = FakeDevFSWriter(); final FakeDevFSWriter writer = FakeDevFSWriter();
......
...@@ -55,21 +55,20 @@ final FakeVmServiceRequest listViews = FakeVmServiceRequest( ...@@ -55,21 +55,20 @@ final FakeVmServiceRequest listViews = FakeVmServiceRequest(
void main() { void main() {
group('validateReloadReport', () { group('validateReloadReport', () {
testUsingContext('invalid', () async { testUsingContext('invalid', () async {
expect(HotRunner.validateReloadReport(<String, dynamic>{}), false); expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
expect(HotRunner.validateReloadReport(<String, dynamic>{
'type': 'ReloadReport', 'type': 'ReloadReport',
'success': false, 'success': false,
'details': <String, dynamic>{}, 'details': <String, dynamic>{},
}), false); })), false);
expect(HotRunner.validateReloadReport(<String, dynamic>{ expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport', 'type': 'ReloadReport',
'success': false, 'success': false,
'details': <String, dynamic>{ 'details': <String, dynamic>{
'notices': <Map<String, dynamic>>[ 'notices': <Map<String, dynamic>>[
], ],
}, },
}), false); })), false);
expect(HotRunner.validateReloadReport(<String, dynamic>{ expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport', 'type': 'ReloadReport',
'success': false, 'success': false,
'details': <String, dynamic>{ 'details': <String, dynamic>{
...@@ -77,15 +76,15 @@ void main() { ...@@ -77,15 +76,15 @@ void main() {
'message': 'error', 'message': 'error',
}, },
}, },
}), false); })), false);
expect(HotRunner.validateReloadReport(<String, dynamic>{ expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport', 'type': 'ReloadReport',
'success': false, 'success': false,
'details': <String, dynamic>{ 'details': <String, dynamic>{
'notices': <Map<String, dynamic>>[], 'notices': <Map<String, dynamic>>[],
}, },
}), false); })), false);
expect(HotRunner.validateReloadReport(<String, dynamic>{ expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport', 'type': 'ReloadReport',
'success': false, 'success': false,
'details': <String, dynamic>{ 'details': <String, dynamic>{
...@@ -93,8 +92,8 @@ void main() { ...@@ -93,8 +92,8 @@ void main() {
<String, dynamic>{'message': false}, <String, dynamic>{'message': false},
], ],
}, },
}), false); })), false);
expect(HotRunner.validateReloadReport(<String, dynamic>{ expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport', 'type': 'ReloadReport',
'success': false, 'success': false,
'details': <String, dynamic>{ 'details': <String, dynamic>{
...@@ -102,8 +101,8 @@ void main() { ...@@ -102,8 +101,8 @@ void main() {
<String, dynamic>{'message': <String>['error']}, <String, dynamic>{'message': <String>['error']},
], ],
}, },
}), false); })), false);
expect(HotRunner.validateReloadReport(<String, dynamic>{ expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport', 'type': 'ReloadReport',
'success': false, 'success': false,
'details': <String, dynamic>{ 'details': <String, dynamic>{
...@@ -112,8 +111,8 @@ void main() { ...@@ -112,8 +111,8 @@ void main() {
<String, dynamic>{'message': <String>['error']}, <String, dynamic>{'message': <String>['error']},
], ],
}, },
}), false); })), false);
expect(HotRunner.validateReloadReport(<String, dynamic>{ expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport', 'type': 'ReloadReport',
'success': false, 'success': false,
'details': <String, dynamic>{ 'details': <String, dynamic>{
...@@ -121,11 +120,19 @@ void main() { ...@@ -121,11 +120,19 @@ void main() {
<String, dynamic>{'message': 'error'}, <String, dynamic>{'message': 'error'},
], ],
}, },
}), false); })), false);
expect(HotRunner.validateReloadReport(<String, dynamic>{ expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport', 'type': 'ReloadReport',
'success': true, 'success': true,
}), true); })), 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.'));
}); });
}); });
......
...@@ -189,21 +189,6 @@ void main() { ...@@ -189,21 +189,6 @@ void main() {
when(mockFlutterDevice.vmService).thenAnswer((Invocation invocation) { when(mockFlutterDevice.vmService).thenAnswer((Invocation invocation) {
return fakeVmServiceHost?.vmService; return fakeVmServiceHost?.vmService;
}); });
when(mockFlutterDevice.reloadSources(any, pause: anyNamed('pause'))).thenAnswer((Invocation invocation) async {
return <Future<vm_service.ReloadReport>>[
Future<vm_service.ReloadReport>.value(vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': true,
'details': <String, dynamic>{
'loadedLibraryCount': 1,
'finalLibraryCount': 1,
'receivedLibraryCount': 1,
'receivedClassesCount': 1,
'receivedProceduresCount': 1,
},
})),
];
});
}); });
testUsingContext('ResidentRunner can attach to device successfully', () => testbed.run(() async { testUsingContext('ResidentRunner can attach to device successfully', () => testbed.run(() async {
...@@ -600,6 +585,38 @@ void main() { ...@@ -600,6 +585,38 @@ void main() {
listViews, listViews,
listViews, listViews,
listViews, listViews,
const FakeVmServiceRequest(
method: '_flutter.setAssetBundlePath',
args: <String, Object>{
'viewId': 'a',
'assetDirectory': 'build/flutter_assets',
'isolateId': '1',
}
),
FakeVmServiceRequest(
method: 'getVM',
jsonResponse: vm_service.VM.parse(<String, Object>{
'isolates': <Object>[
fakeUnpausedIsolate.toJson(),
],
}).toJson(),
),
const FakeVmServiceRequest(
method: 'reloadSources',
args: <String, Object>{
'isolateId': '1',
'pause': false,
'rootLibUri': 'lib/main.dart.incremental.dill'
},
jsonResponse: <String, Object>{
'type': 'ReloadReport',
'success': true,
'details': <String, Object>{
'loadedLibraryCount': 1,
},
},
),
listViews,
FakeVmServiceRequest( FakeVmServiceRequest(
method: 'getIsolate', method: 'getIsolate',
args: <String, Object>{ args: <String, Object>{
...@@ -655,6 +672,101 @@ void main() { ...@@ -655,6 +672,101 @@ void main() {
expect(result.code, 0); expect(result.code, 0);
})); }));
testUsingContext('ResidentRunner resets compilation time on reload reject', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
listViews,
listViews,
listViews,
listViews,
const FakeVmServiceRequest(
method: '_flutter.setAssetBundlePath',
args: <String, Object>{
'viewId': 'a',
'assetDirectory': 'build/flutter_assets',
'isolateId': '1',
}
),
FakeVmServiceRequest(
method: 'getVM',
jsonResponse: vm_service.VM.parse(<String, Object>{
'isolates': <Object>[
fakeUnpausedIsolate.toJson(),
],
}).toJson(),
),
const FakeVmServiceRequest(
method: 'reloadSources',
args: <String, Object>{
'isolateId': '1',
'pause': false,
'rootLibUri': 'lib/main.dart.incremental.dill'
},
jsonResponse: <String, Object>{
'type': 'ReloadReport',
'success': false,
'notices': <Object>[
<String, Object>{
'message': 'Failed to hot reload'
}
],
'details': <String, Object>{},
},
),
listViews,
FakeVmServiceRequest(
method: 'getIsolate',
args: <String, Object>{
'isolateId': '1',
},
jsonResponse: fakeUnpausedIsolate.toJson(),
),
FakeVmServiceRequest(
method: 'ext.flutter.reassemble',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
},
),
]);
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;
});
final Completer<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync();
final Completer<void> onAppStart = Completer<void>.sync();
unawaited(residentRunner.attach(
appStartedCompleter: onAppStart,
connectionInfoCompleter: onConnectionInfo,
));
await onAppStart.future;
when(mockFlutterDevice.updateDevFS(
mainUri: anyNamed('mainUri'),
target: anyNamed('target'),
bundle: anyNamed('bundle'),
firstBuildTime: anyNamed('firstBuildTime'),
bundleFirstUpload: anyNamed('bundleFirstUpload'),
bundleDirty: anyNamed('bundleDirty'),
fullRestart: anyNamed('fullRestart'),
projectRootPath: anyNamed('projectRootPath'),
pathToReload: anyNamed('pathToReload'),
invalidatedFiles: anyNamed('invalidatedFiles'),
dillOutputPath: anyNamed('dillOutputPath'),
packageConfig: anyNamed('packageConfig'),
)).thenAnswer((Invocation invocation) async {
return UpdateFSReport(success: true);
});
final OperationResult result = await residentRunner.restart(fullRestart: false);
expect(result.fatal, false);
expect(result.message, contains('Reload rejected: Failed to hot reload')); // contains error message from reload report.
expect(result.code, 1);
verify(mockDevFS.resetLastCompiled()).called(1); // compilation time is reset.
}));
testUsingContext('ResidentRunner can send target platform to analytics from hot reload', () => testbed.run(() async { testUsingContext('ResidentRunner can send target platform to analytics from hot reload', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[ fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
...@@ -662,6 +774,38 @@ void main() { ...@@ -662,6 +774,38 @@ void main() {
listViews, listViews,
listViews, listViews,
listViews, listViews,
const FakeVmServiceRequest(
method: '_flutter.setAssetBundlePath',
args: <String, Object>{
'viewId': 'a',
'assetDirectory': 'build/flutter_assets',
'isolateId': '1',
}
),
FakeVmServiceRequest(
method: 'getVM',
jsonResponse: vm_service.VM.parse(<String, Object>{
'isolates': <Object>[
fakeUnpausedIsolate.toJson(),
],
}).toJson(),
),
const FakeVmServiceRequest(
method: 'reloadSources',
args: <String, Object>{
'isolateId': '1',
'pause': false,
'rootLibUri': 'lib/main.dart.incremental.dill'
},
jsonResponse: <String, Object>{
'type': 'ReloadReport',
'success': true,
'details': <String, Object>{
'loadedLibraryCount': 1,
},
},
),
listViews,
FakeVmServiceRequest( FakeVmServiceRequest(
method: 'getIsolate', method: 'getIsolate',
args: <String, Object>{ args: <String, Object>{
...@@ -2464,3 +2608,36 @@ class FakeProjectFileInvalidator extends Fake implements ProjectFileInvalidator ...@@ -2464,3 +2608,36 @@ class FakeProjectFileInvalidator extends Fake implements ProjectFileInvalidator
]); ]);
} }
} }
class FakeDevice extends Fake implements Device {
FakeDevice({
String sdkNameAndVersion = 'Android',
TargetPlatform targetPlatform = TargetPlatform.android_arm,
bool isLocalEmulator = false,
this.supportsHotRestart = true,
}) : _isLocalEmulator = isLocalEmulator,
_targetPlatform = targetPlatform,
_sdkNameAndVersion = sdkNameAndVersion;
final bool _isLocalEmulator;
final TargetPlatform _targetPlatform;
final String _sdkNameAndVersion;
@override
final bool supportsHotRestart;
@override
Future<String> get sdkNameAndVersion async => _sdkNameAndVersion;
@override
Future<TargetPlatform> get targetPlatform async => _targetPlatform;
@override
Future<bool> get isLocalEmulator async => _isLocalEmulator;
@override
String get name => 'FakeDevice';
@override
Future<void> dispose() async { }
}
// 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:file/file.dart';
import '../src/common.dart';
import 'test_data/hot_reload_const_project.dart';
import 'test_driver.dart';
import 'test_utils.dart';
void main() {
Directory tempDir;
final HotReloadConstProject project = HotReloadConstProject();
FlutterRunTestDriver flutter;
setUp(() async {
tempDir = createResolvedTempDirectorySync('hot_reload_test.');
await project.setUpIn(tempDir);
flutter = FlutterRunTestDriver(tempDir);
});
tearDown(() async {
await flutter?.stop();
tryToDelete(tempDir);
});
testWithoutContext('hot reload displays a formatted error message when removing a field from a const class', () async {
await flutter.run();
project.removeFieldFromConstClass();
expect(flutter.hotReload(), throwsA(contains('Try performing a hot restart instead.')));
});
}
// 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 '../test_utils.dart';
import 'project.dart';
class HotReloadConstProject extends Project {
@override
final String pubspec = '''
name: test
environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
''';
@override
final String main = r'''
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final ByteData message = const StringCodec().encodeMessage('AppLifecycleState.resumed');
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/lifecycle', message, (_) { });
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
final int field = 2;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Container(),
);
}
}
''';
void removeFieldFromConstClass() {
final String newMainContents = main.replaceAll(
'final int field = 2;',
'// final int field = 2;',
);
writeFile(fileSystem.path.join(dir.path, 'lib', 'main.dart'), newMainContents);
}
}
...@@ -588,19 +588,6 @@ class FlutterRunTestDriver extends FlutterTestDriver { ...@@ -588,19 +588,6 @@ class FlutterRunTestDriver extends FlutterTestDriver {
); );
} }
Future<void> reloadMethod({ String libraryId, String classId }) async {
if (_currentRunningAppId == null) {
throw Exception('App has not started yet');
}
final dynamic reloadMethodResponse = await _sendRequest(
'app.reloadMethod',
<String, dynamic>{'appId': _currentRunningAppId, 'class': classId, 'library': libraryId},
);
if (reloadMethodResponse == null || reloadMethodResponse['code'] != 0) {
_throwErrorResponse('reloadMethodResponse request failed');
}
}
Future<void> _restart({ bool fullRestart = false, bool pause = false, bool debounce = false, int debounceDurationOverrideMs }) async { Future<void> _restart({ bool fullRestart = false, bool pause = false, bool debounce = false, int debounceDurationOverrideMs }) async {
if (_currentRunningAppId == null) { if (_currentRunningAppId == null) {
throw Exception('App has not started yet'); throw Exception('App has not started yet');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment