Implement hot reload using the DDC library bundle format (#162498)
https://github.com/dart-lang/webdev/issues/2516 - Updates restart/reload code to accept a resetCompiler boolean to disambiguate between whether this is a full restart and whether to reset the resident compiler. - Adds code to call reloadSources in DWDS and handle the response (including any errors). - Adds code to invoke reassemble. - Adds code to emit a script that DWDS can later consume that contains the changed sources and their associated libraries. This is used to hot reload. The bootstrapper puts this in the global window. DWDS should be updated to accept it in the provider itself. See https://github.com/dart-lang/webdev/issues/2584. - Adds code to parse module metadata from the frontend server. This is identical to the implementation in DWDS % addressing type-related lints. - Adds tests that run the existing hot reload tests but with web. Some modifications are mode, including waiting for Flutter runs to finish executing, and skipping a test that's not possible on the web. Needs DWDS 24.3.4 to be published first and used before we can land. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing.
This commit is contained in:
parent
4d08217f8d
commit
8e2a6fc3fd
@ -551,6 +551,10 @@ class DevFS {
|
||||
/// Updates files on the device.
|
||||
///
|
||||
/// Returns the number of bytes synced.
|
||||
///
|
||||
/// If [fullRestart] is true, assumes this is a hot restart instead of a hot
|
||||
/// reload. If [resetCompiler] is true, sends a `reset` instruction to the
|
||||
/// frontend server.
|
||||
Future<UpdateFSReport> update({
|
||||
required Uri mainUri,
|
||||
required ResidentCompiler generator,
|
||||
@ -566,6 +570,7 @@ class DevFS {
|
||||
AssetBundle? bundle,
|
||||
bool bundleFirstUpload = false,
|
||||
bool fullRestart = false,
|
||||
bool resetCompiler = false,
|
||||
File? dartPluginRegistrant,
|
||||
}) async {
|
||||
final DateTime candidateCompileTime = DateTime.now();
|
||||
@ -577,7 +582,7 @@ class DevFS {
|
||||
final List<Future<void>> pendingAssetBuilds = <Future<void>>[];
|
||||
bool assetBuildFailed = false;
|
||||
int syncedBytes = 0;
|
||||
if (fullRestart) {
|
||||
if (resetCompiler) {
|
||||
generator.reset();
|
||||
}
|
||||
// On a full restart, or on an initial compile for the attach based workflow,
|
||||
|
@ -40,6 +40,7 @@ import '../web/bootstrap.dart';
|
||||
import '../web/chrome.dart';
|
||||
import '../web/compile.dart';
|
||||
import '../web/memory_fs.dart';
|
||||
import '../web/module_metadata.dart';
|
||||
import '../web_template.dart';
|
||||
|
||||
typedef DwdsLauncher =
|
||||
@ -158,6 +159,16 @@ class WebAssetServer implements AssetReader {
|
||||
/// If [writeRestartScripts] is true, writes a list of sources mapped to their
|
||||
/// ids to the file system that can then be consumed by the hot restart
|
||||
/// callback.
|
||||
///
|
||||
/// For example:
|
||||
/// ```json
|
||||
/// [
|
||||
/// {
|
||||
/// "src": "<file_name>",
|
||||
/// "id": "<id>",
|
||||
/// },
|
||||
/// ]
|
||||
/// ```
|
||||
void performRestart(List<String> modules, {required bool writeRestartScripts}) {
|
||||
for (final String module in modules) {
|
||||
// We skip computing the digest by using the hashCode of the underlying buffer.
|
||||
@ -174,11 +185,44 @@ class WebAssetServer implements AssetReader {
|
||||
for (final String src in modules) {
|
||||
srcIdsList.add(<String, String>{'src': '$src?gen=$_hotRestartGeneration', 'id': src});
|
||||
}
|
||||
writeFile('main.dart.js.restartScripts', json.encode(srcIdsList));
|
||||
writeFile('restart_scripts.json', json.encode(srcIdsList));
|
||||
}
|
||||
_hotRestartGeneration++;
|
||||
}
|
||||
|
||||
/// Given a list of [modules] that need to be reloaded, writes a file that
|
||||
/// contains a list of objects each with two fields:
|
||||
///
|
||||
/// `src`: A string that corresponds to the file path containing a DDC library
|
||||
/// bundle.
|
||||
/// `libraries`: An array of strings containing the libraries that were
|
||||
/// compiled in `src`.
|
||||
///
|
||||
/// For example:
|
||||
/// ```json
|
||||
/// [
|
||||
/// {
|
||||
/// "src": "<file_name>",
|
||||
/// "libraries": ["<lib1>", "<lib2>"],
|
||||
/// },
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// The path of the output file should stay consistent across the lifetime of
|
||||
/// the app.
|
||||
void performReload(List<String> modules) {
|
||||
final List<Map<String, Object>> moduleToLibrary = <Map<String, Object>>[];
|
||||
for (final String module in modules) {
|
||||
final ModuleMetadata metadata = ModuleMetadata.fromJson(
|
||||
json.decode(utf8.decode(_webMemoryFS.metadataFiles['$module.metadata']!.toList()))
|
||||
as Map<String, dynamic>,
|
||||
);
|
||||
final List<String> libraries = metadata.libraries.keys.toList();
|
||||
moduleToLibrary.add(<String, Object>{'src': module, 'libraries': libraries});
|
||||
}
|
||||
writeFile('reload_scripts.json', json.encode(moduleToLibrary));
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
List<String> write(File codeFile, File manifestFile, File sourcemapFile, File metadataFile) {
|
||||
return _webMemoryFS.write(codeFile, manifestFile, sourcemapFile, metadataFile);
|
||||
@ -1001,6 +1045,7 @@ class WebDevFS implements DevFS {
|
||||
AssetBundle? bundle,
|
||||
bool bundleFirstUpload = false,
|
||||
bool fullRestart = false,
|
||||
bool resetCompiler = false,
|
||||
String? projectRootPath,
|
||||
File? dartPluginRegistrant,
|
||||
}) async {
|
||||
@ -1077,7 +1122,7 @@ class WebDevFS implements DevFS {
|
||||
await _validateTemplateFile('index.html');
|
||||
await _validateTemplateFile('flutter_bootstrap.js');
|
||||
final DateTime candidateCompileTime = DateTime.now();
|
||||
if (fullRestart) {
|
||||
if (resetCompiler) {
|
||||
generator.reset();
|
||||
}
|
||||
|
||||
@ -1122,8 +1167,11 @@ class WebDevFS implements DevFS {
|
||||
} on FileSystemException catch (err) {
|
||||
throwToolExit('Failed to load recompiled sources:\n$err');
|
||||
}
|
||||
|
||||
webAssetServer.performRestart(modules, writeRestartScripts: ddcModuleSystem);
|
||||
if (fullRestart) {
|
||||
webAssetServer.performRestart(modules, writeRestartScripts: ddcModuleSystem);
|
||||
} else {
|
||||
webAssetServer.performReload(modules);
|
||||
}
|
||||
return UpdateFSReport(
|
||||
success: true,
|
||||
syncedBytes: codeFile.lengthSync(),
|
||||
|
@ -322,7 +322,7 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
|
||||
}
|
||||
if (debuggingOptions.buildInfo.isDebug && !debuggingOptions.webUseWasm) {
|
||||
await runSourceGenerators();
|
||||
final UpdateFSReport report = await _updateDevFS(fullRestart: true);
|
||||
final UpdateFSReport report = await _updateDevFS(fullRestart: true, resetCompiler: true);
|
||||
if (!report.success) {
|
||||
_logger.printError('Failed to compile application.');
|
||||
appFailedToStart();
|
||||
@ -406,15 +406,28 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
|
||||
bool benchmarkMode = false,
|
||||
}) async {
|
||||
final DateTime start = _systemClock.now();
|
||||
final Status status = _logger.startProgress(
|
||||
'Performing hot restart...',
|
||||
progressId: 'hot.restart',
|
||||
);
|
||||
final Status status;
|
||||
if (debuggingOptions.buildInfo.ddcModuleFormat != DdcModuleFormat.ddc ||
|
||||
debuggingOptions.buildInfo.canaryFeatures == false) {
|
||||
// Triggering hot reload performed hot restart for the old module formats
|
||||
// historically. Keep that behavior and only perform hot reload when the
|
||||
// new module format is used.
|
||||
fullRestart = true;
|
||||
}
|
||||
if (fullRestart) {
|
||||
status = _logger.startProgress('Performing hot restart...', progressId: 'hot.restart');
|
||||
} else {
|
||||
status = _logger.startProgress('Performing hot reload...', progressId: 'hot.reload');
|
||||
}
|
||||
|
||||
if (debuggingOptions.buildInfo.isDebug && !debuggingOptions.webUseWasm) {
|
||||
await runSourceGenerators();
|
||||
// Full restart is always false for web, since the extra recompile is wasteful.
|
||||
final UpdateFSReport report = await _updateDevFS();
|
||||
// Don't reset the resident compiler for web, since the extra recompile is
|
||||
// wasteful.
|
||||
final UpdateFSReport report = await _updateDevFS(
|
||||
fullRestart: fullRestart,
|
||||
resetCompiler: false,
|
||||
);
|
||||
if (report.success) {
|
||||
device!.generator!.accept();
|
||||
} else {
|
||||
@ -448,10 +461,32 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
|
||||
if (!deviceIsDebuggable) {
|
||||
_logger.printStatus('Recompile complete. Page requires refresh.');
|
||||
} else if (isRunningDebug) {
|
||||
// If the hot-restart service extension method is registered, then use
|
||||
// it. Otherwise, default to calling "hotRestart" without a namespace.
|
||||
final String hotRestartMethod = _registeredMethodsForService['hotRestart'] ?? 'hotRestart';
|
||||
await _vmService.service.callMethod(hotRestartMethod);
|
||||
if (fullRestart) {
|
||||
// If the hot-restart service extension method is registered, then use
|
||||
// it. Otherwise, default to calling "hotRestart" without a namespace.
|
||||
final String hotRestartMethod =
|
||||
_registeredMethodsForService['hotRestart'] ?? 'hotRestart';
|
||||
await _vmService.service.callMethod(hotRestartMethod);
|
||||
} else {
|
||||
// Isolates don't work on web. For lack of a better value, pass an
|
||||
// empty string for the isolate id.
|
||||
final vmservice.ReloadReport report = await _vmService.service.reloadSources('');
|
||||
final ReloadReportContents contents = ReloadReportContents.fromReloadReport(report);
|
||||
final bool success = contents.success ?? false;
|
||||
if (!success) {
|
||||
// Rejections happen at compile-time for the web, so in theory,
|
||||
// nothing should go wrong here. However, if DWDS or the DDC runtime
|
||||
// has some internal error, we should still surface it to make
|
||||
// debugging easier.
|
||||
String reloadFailedMessage = 'Hot reload failed:';
|
||||
globals.printError(reloadFailedMessage);
|
||||
for (final ReasonForCancelling reason in contents.notices) {
|
||||
reloadFailedMessage += reason.toString();
|
||||
globals.printError(reason.toString());
|
||||
}
|
||||
return OperationResult(1, reloadFailedMessage);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// On non-debug builds, a hard refresh is required to ensure the
|
||||
// up to date sources are loaded.
|
||||
@ -467,40 +502,76 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
|
||||
|
||||
final Duration elapsed = _systemClock.now().difference(start);
|
||||
final String elapsedMS = getElapsedAsMilliseconds(elapsed);
|
||||
_logger.printStatus('Restarted application in $elapsedMS.');
|
||||
_logger.printStatus('${fullRestart ? 'Restarted' : 'Reloaded'} application in $elapsedMS.');
|
||||
|
||||
unawaited(residentDevtoolsHandler!.hotRestart(flutterDevices));
|
||||
if (fullRestart) {
|
||||
unawaited(residentDevtoolsHandler!.hotRestart(flutterDevices));
|
||||
}
|
||||
|
||||
// Don't track restart times for dart2js builds or web-server devices.
|
||||
if (debuggingOptions.buildInfo.isDebug && deviceIsDebuggable) {
|
||||
_analytics.send(
|
||||
Event.timing(
|
||||
workflow: 'hot',
|
||||
variableName: 'web-incremental-restart',
|
||||
elapsedMilliseconds: elapsed.inMilliseconds,
|
||||
),
|
||||
);
|
||||
// TODO(srujzs): There are a number of fields that the VM tracks in the
|
||||
// analytics that we do not for both hot restart and reload. We should
|
||||
// unify that.
|
||||
final String targetPlatform = getNameForTargetPlatform(TargetPlatform.web_javascript);
|
||||
final String sdkName = await device!.device!.sdkNameAndVersion;
|
||||
HotEvent(
|
||||
'restart',
|
||||
targetPlatform: getNameForTargetPlatform(TargetPlatform.web_javascript),
|
||||
sdkName: sdkName,
|
||||
emulator: false,
|
||||
fullRestart: true,
|
||||
reason: reason,
|
||||
overallTimeInMs: elapsed.inMilliseconds,
|
||||
).send();
|
||||
_analytics.send(
|
||||
Event.hotRunnerInfo(
|
||||
label: 'restart',
|
||||
targetPlatform: getNameForTargetPlatform(TargetPlatform.web_javascript),
|
||||
if (fullRestart) {
|
||||
_analytics.send(
|
||||
Event.timing(
|
||||
workflow: 'hot',
|
||||
variableName: 'web-incremental-restart',
|
||||
elapsedMilliseconds: elapsed.inMilliseconds,
|
||||
),
|
||||
);
|
||||
HotEvent(
|
||||
'restart',
|
||||
targetPlatform: targetPlatform,
|
||||
sdkName: sdkName,
|
||||
emulator: false,
|
||||
fullRestart: true,
|
||||
reason: reason,
|
||||
overallTimeInMs: elapsed.inMilliseconds,
|
||||
),
|
||||
);
|
||||
).send();
|
||||
_analytics.send(
|
||||
Event.hotRunnerInfo(
|
||||
label: 'restart',
|
||||
targetPlatform: targetPlatform,
|
||||
sdkName: sdkName,
|
||||
emulator: false,
|
||||
fullRestart: true,
|
||||
reason: reason,
|
||||
overallTimeInMs: elapsed.inMilliseconds,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
_analytics.send(
|
||||
Event.timing(
|
||||
workflow: 'hot',
|
||||
variableName: 'reload',
|
||||
elapsedMilliseconds: elapsed.inMilliseconds,
|
||||
),
|
||||
);
|
||||
HotEvent(
|
||||
'reload',
|
||||
targetPlatform: targetPlatform,
|
||||
sdkName: sdkName,
|
||||
emulator: false,
|
||||
fullRestart: false,
|
||||
reason: reason,
|
||||
overallTimeInMs: elapsed.inMilliseconds,
|
||||
).send();
|
||||
_analytics.send(
|
||||
Event.hotRunnerInfo(
|
||||
label: 'reload',
|
||||
targetPlatform: targetPlatform,
|
||||
sdkName: sdkName,
|
||||
emulator: false,
|
||||
fullRestart: false,
|
||||
reason: reason,
|
||||
overallTimeInMs: elapsed.inMilliseconds,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return OperationResult.ok;
|
||||
}
|
||||
@ -551,7 +622,10 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
|
||||
return result!.absolute.uri;
|
||||
}
|
||||
|
||||
Future<UpdateFSReport> _updateDevFS({bool fullRestart = false}) async {
|
||||
Future<UpdateFSReport> _updateDevFS({
|
||||
required bool fullRestart,
|
||||
required bool resetCompiler,
|
||||
}) async {
|
||||
final bool isFirstUpload = !assetBundle.wasBuiltOnce();
|
||||
final bool rebuildBundle = assetBundle.needsBuild();
|
||||
if (rebuildBundle) {
|
||||
@ -584,8 +658,9 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
|
||||
bundleFirstUpload: isFirstUpload,
|
||||
generator: device!.generator!,
|
||||
fullRestart: fullRestart,
|
||||
resetCompiler: resetCompiler,
|
||||
dillOutputPath: dillOutputPath,
|
||||
pathToReload: getReloadPath(fullRestart: fullRestart, swap: false),
|
||||
pathToReload: getReloadPath(resetCompiler: resetCompiler, swap: false),
|
||||
invalidatedFiles: invalidationResult.uris!,
|
||||
packageConfig: invalidationResult.packageConfig!,
|
||||
trackWidgetCreation: debuggingOptions.buildInfo.trackWidgetCreation,
|
||||
|
@ -570,6 +570,7 @@ class FlutterDevice {
|
||||
bundleFirstUpload: bundleFirstUpload,
|
||||
generator: generator!,
|
||||
fullRestart: fullRestart,
|
||||
resetCompiler: fullRestart,
|
||||
dillOutputPath: dillOutputPath,
|
||||
trackWidgetCreation: buildInfo.trackWidgetCreation,
|
||||
pathToReload: pathToReload,
|
||||
@ -1112,8 +1113,8 @@ abstract class ResidentRunner extends ResidentHandlers {
|
||||
|
||||
String get dillOutputPath =>
|
||||
_dillOutputPath ?? globals.fs.path.join(artifactDirectory.path, 'app.dill');
|
||||
String getReloadPath({bool fullRestart = false, required bool swap}) {
|
||||
if (!fullRestart) {
|
||||
String getReloadPath({bool resetCompiler = false, required bool swap}) {
|
||||
if (!resetCompiler) {
|
||||
return 'main.dart.incremental.dill';
|
||||
}
|
||||
return 'main.dart${swap ? '.swap' : ''}.dill';
|
||||
|
@ -536,7 +536,7 @@ class HotRunner extends ResidentRunner {
|
||||
bundleFirstUpload: isFirstUpload,
|
||||
bundleDirty: !isFirstUpload && rebuildBundle,
|
||||
fullRestart: fullRestart,
|
||||
pathToReload: getReloadPath(fullRestart: fullRestart, swap: _swap),
|
||||
pathToReload: getReloadPath(resetCompiler: fullRestart, swap: _swap),
|
||||
invalidatedFiles: invalidationResult.uris!,
|
||||
packageConfig: invalidationResult.packageConfig!,
|
||||
dillOutputPath: dillOutputPath,
|
||||
|
@ -252,7 +252,15 @@ $_simpleLoaderScript
|
||||
// We should have written a file containing all the scripts that need to be
|
||||
// reloaded into the page. This is then read when a hot restart is triggered
|
||||
// in DDC via the `\$dartReloadModifiedModules` callback.
|
||||
let restartScripts = currentUri + '.restartScripts';
|
||||
let restartScripts = _currentDirectory + 'restart_scripts.json';
|
||||
// Flutter tools should write a file containing the scripts and libraries
|
||||
// that need to be hot reloaded. This is read in DWDS when a hot reload is
|
||||
// triggered.
|
||||
// TODO(srujzs): Ideally, this should be passed to the
|
||||
// `FrontendServerDdcLibraryBundleStrategyProvider` instead. See
|
||||
// https://github.com/dart-lang/webdev/issues/2584 for more details.
|
||||
let reloadScripts = _currentDirectory + 'reload_scripts.json';
|
||||
window.\$reloadScriptsPath = reloadScripts;
|
||||
|
||||
if (!window.\$dartReloadModifiedModules) {
|
||||
window.\$dartReloadModifiedModules = (function(appName, callback) {
|
||||
|
203
packages/flutter_tools/lib/src/web/module_metadata.dart
Normal file
203
packages/flutter_tools/lib/src/web/module_metadata.dart
Normal file
@ -0,0 +1,203 @@
|
||||
// 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.
|
||||
|
||||
// Taken from https://github.com/dart-lang/webdev/blob/616da45582e008efa114728927eabb498c71f1b7/dwds/lib/src/debugging/metadata/module_metadata.dart.
|
||||
// Prefer to keep the implementations consistent.
|
||||
|
||||
/// Module metadata format version
|
||||
///
|
||||
/// Module reader always creates the current version but is able to read
|
||||
/// metadata files with later versions as long as the changes are backward
|
||||
/// compatible, i.e. only minor or patch versions have changed.
|
||||
class ModuleMetadataVersion {
|
||||
const ModuleMetadataVersion(this.majorVersion, this.minorVersion, this.patchVersion);
|
||||
|
||||
final int majorVersion;
|
||||
final int minorVersion;
|
||||
final int patchVersion;
|
||||
|
||||
/// Current metadata version
|
||||
///
|
||||
/// Version follows simple semantic versioning format 'major.minor.patch'
|
||||
/// See https://semver.org
|
||||
static const ModuleMetadataVersion current = ModuleMetadataVersion(2, 0, 0);
|
||||
|
||||
/// Previous version supported by the metadata reader
|
||||
static const ModuleMetadataVersion previous = ModuleMetadataVersion(1, 0, 0);
|
||||
|
||||
/// Current metadata version created by the reader
|
||||
String get version => '$majorVersion.$minorVersion.$patchVersion';
|
||||
|
||||
/// Is this metadata version compatible with the given version
|
||||
///
|
||||
/// The minor and patch version changes never remove any fields that current
|
||||
/// version supports, so the reader can create current metadata version from
|
||||
/// any file created with a later writer, as long as the major version does
|
||||
/// not change.
|
||||
bool isCompatibleWith(String version) {
|
||||
final List<String> parts = version.split('.');
|
||||
if (parts.length != 3) {
|
||||
throw FormatException(
|
||||
'Version: $version'
|
||||
'does not follow simple semantic versioning format',
|
||||
);
|
||||
}
|
||||
final int major = int.parse(parts[0]);
|
||||
final int minor = int.parse(parts[1]);
|
||||
final int patch = int.parse(parts[2]);
|
||||
return major == majorVersion && minor >= minorVersion && patch >= patchVersion;
|
||||
}
|
||||
}
|
||||
|
||||
/// Library metadata
|
||||
///
|
||||
/// Represents library metadata used in the debugger,
|
||||
/// supports reading from and writing to json.
|
||||
class LibraryMetadata {
|
||||
LibraryMetadata(this.name, this.importUri, this.partUris);
|
||||
|
||||
LibraryMetadata.fromJson(Map<String, Object?> json)
|
||||
: name = _readRequiredField(json, nameField),
|
||||
importUri = _readRequiredField(json, importUriField),
|
||||
partUris = _readOptionalList(json, partUrisField) ?? <String>[];
|
||||
|
||||
static const String nameField = 'name';
|
||||
static const String importUriField = 'importUri';
|
||||
static const String partUrisField = 'partUris';
|
||||
|
||||
/// Library name as defined in pubspec.yaml
|
||||
final String name;
|
||||
|
||||
/// Library importUri
|
||||
///
|
||||
/// Example package:path/path.dart
|
||||
final String importUri;
|
||||
|
||||
/// All file uris from the library
|
||||
///
|
||||
/// Can be relative paths to the directory of the fileUri
|
||||
final List<String> partUris;
|
||||
|
||||
Map<String, Object?> toJson() {
|
||||
return <String, Object?>{
|
||||
nameField: name,
|
||||
importUriField: importUri,
|
||||
partUrisField: <String>[...partUris],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Module metadata
|
||||
///
|
||||
/// Represents module metadata used in the debugger,
|
||||
/// supports reading from and writing to json.
|
||||
class ModuleMetadata {
|
||||
ModuleMetadata(this.name, this.closureName, this.sourceMapUri, this.moduleUri, {String? ver}) {
|
||||
version = ver ?? ModuleMetadataVersion.current.version;
|
||||
}
|
||||
|
||||
ModuleMetadata.fromJson(Map<String, Object?> json)
|
||||
: version = _readRequiredField(json, versionField),
|
||||
name = _readRequiredField(json, nameField),
|
||||
closureName = _readRequiredField(json, closureNameField),
|
||||
sourceMapUri = _readRequiredField(json, sourceMapUriField),
|
||||
moduleUri = _readRequiredField(json, moduleUriField) {
|
||||
if (!ModuleMetadataVersion.current.isCompatibleWith(version) &&
|
||||
!ModuleMetadataVersion.previous.isCompatibleWith(version)) {
|
||||
throw Exception(
|
||||
'Unsupported metadata version $version. '
|
||||
'\n Supported versions: '
|
||||
'\n ${ModuleMetadataVersion.current.version} '
|
||||
'\n ${ModuleMetadataVersion.previous.version}',
|
||||
);
|
||||
}
|
||||
|
||||
for (final Map<String, Object?> l in _readRequiredList<Map<String, Object?>>(
|
||||
json,
|
||||
librariesField,
|
||||
)) {
|
||||
addLibrary(LibraryMetadata.fromJson(l));
|
||||
}
|
||||
}
|
||||
|
||||
static const String versionField = 'version';
|
||||
static const String nameField = 'name';
|
||||
static const String closureNameField = 'closureName';
|
||||
static const String sourceMapUriField = 'sourceMapUri';
|
||||
static const String moduleUriField = 'moduleUri';
|
||||
static const String librariesField = 'libraries';
|
||||
|
||||
/// Metadata format version
|
||||
late final String version;
|
||||
|
||||
/// Module name
|
||||
///
|
||||
/// Used as a name of the js module created by the compiler and
|
||||
/// as key to store and load modules in the debugger and the browser
|
||||
// TODO(srujzs): Remove once https://github.com/dart-lang/sdk/issues/59618 is
|
||||
// resolved.
|
||||
final String name;
|
||||
|
||||
/// Name of the function enclosing the module
|
||||
///
|
||||
/// Used by debugger to determine the top dart scope
|
||||
final String closureName;
|
||||
|
||||
/// Source map uri
|
||||
final String sourceMapUri;
|
||||
|
||||
/// Module uri
|
||||
final String moduleUri;
|
||||
|
||||
final Map<String, LibraryMetadata> libraries = <String, LibraryMetadata>{};
|
||||
|
||||
/// Add [library] to metadata
|
||||
///
|
||||
/// Used for filling the metadata in the compiler or for reading from
|
||||
/// stored metadata files.
|
||||
void addLibrary(LibraryMetadata library) {
|
||||
if (!libraries.containsKey(library.importUri)) {
|
||||
libraries[library.importUri] = library;
|
||||
} else {
|
||||
throw Exception(
|
||||
'Metadata creation error: '
|
||||
'Cannot add library $library with uri ${library.importUri}: '
|
||||
'another library "${libraries[library.importUri]}" is found '
|
||||
'with the same uri',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object?> toJson() {
|
||||
return <String, Object?>{
|
||||
versionField: version,
|
||||
nameField: name,
|
||||
closureNameField: closureName,
|
||||
sourceMapUriField: sourceMapUri,
|
||||
moduleUriField: moduleUri,
|
||||
librariesField: <Map<String, Object?>>[
|
||||
for (final LibraryMetadata lib in libraries.values) lib.toJson(),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
T _readRequiredField<T>(Map<String, Object?> json, String field) {
|
||||
if (!json.containsKey(field)) {
|
||||
throw FormatException('Required field $field is not set in $json');
|
||||
}
|
||||
return json[field]! as T;
|
||||
}
|
||||
|
||||
T? _readOptionalField<T>(Map<String, Object?> json, String field) => json[field] as T?;
|
||||
|
||||
List<T> _readRequiredList<T>(Map<String, Object?> json, String field) {
|
||||
final List<Object?> list = _readRequiredField<List<Object?>>(json, field);
|
||||
return List.castFrom<Object?, T>(list);
|
||||
}
|
||||
|
||||
List<T>? _readOptionalList<T>(Map<String, Object?> json, String field) {
|
||||
final List<Object?>? list = _readOptionalField<List<Object?>>(json, field);
|
||||
return list == null ? null : List.castFrom<Object?, T>(list);
|
||||
}
|
@ -488,6 +488,7 @@ class FakeDevFS extends Fake implements DevFS {
|
||||
AssetBundle? bundle,
|
||||
bool bundleFirstUpload = false,
|
||||
bool fullRestart = false,
|
||||
bool resetCompiler = false,
|
||||
String? projectRootPath,
|
||||
File? dartPluginRegistrant,
|
||||
}) async {
|
||||
|
@ -1663,6 +1663,7 @@ class FakeWebDevFS extends Fake implements WebDevFS {
|
||||
AssetBundle? bundle,
|
||||
bool bundleFirstUpload = false,
|
||||
bool fullRestart = false,
|
||||
bool resetCompiler = false,
|
||||
String? projectRootPath,
|
||||
File? dartPluginRegistrant,
|
||||
}) async {
|
||||
|
@ -5,204 +5,9 @@
|
||||
@Tags(<String>['flutter-test-driver'])
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:vm_service/vm_service.dart';
|
||||
|
||||
import '../src/common.dart';
|
||||
import 'test_data/hot_reload_project.dart';
|
||||
import 'test_driver.dart';
|
||||
import 'test_utils.dart';
|
||||
import 'test_data/hot_reload_test_common.dart';
|
||||
|
||||
void main() {
|
||||
late Directory tempDir;
|
||||
final HotReloadProject project = HotReloadProject();
|
||||
late 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 works without error', () async {
|
||||
await flutter.run();
|
||||
await flutter.hotReload();
|
||||
});
|
||||
|
||||
testWithoutContext('multiple overlapping hot reload are debounced and queued', () async {
|
||||
await flutter.run();
|
||||
// Capture how many *real* hot reloads occur.
|
||||
int numReloads = 0;
|
||||
final StreamSubscription<void> subscription = flutter.stdout
|
||||
.map(parseFlutterResponse)
|
||||
.where(_isHotReloadCompletionEvent)
|
||||
.listen((_) => numReloads++);
|
||||
|
||||
// To reduce tests flaking, override the debounce timer to something higher than
|
||||
// the default to ensure the hot reloads that are supposed to arrive within the
|
||||
// debounce period will even on slower CI machines.
|
||||
const int hotReloadDebounceOverrideMs = 250;
|
||||
const Duration delay = Duration(milliseconds: hotReloadDebounceOverrideMs * 2);
|
||||
|
||||
Future<void> doReload([void _]) =>
|
||||
flutter.hotReload(debounce: true, debounceDurationOverrideMs: hotReloadDebounceOverrideMs);
|
||||
|
||||
try {
|
||||
await Future.wait<void>(<Future<void>>[
|
||||
doReload(),
|
||||
doReload(),
|
||||
Future<void>.delayed(delay).then(doReload),
|
||||
Future<void>.delayed(delay).then(doReload),
|
||||
]);
|
||||
|
||||
// We should only get two reloads, as the first two will have been
|
||||
// merged together by the debounce, and the second two also.
|
||||
expect(numReloads, equals(2));
|
||||
} finally {
|
||||
await subscription.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
testWithoutContext('newly added code executes during hot reload', () async {
|
||||
final StringBuffer stdout = StringBuffer();
|
||||
final Completer<void> completer = Completer<void>();
|
||||
final StreamSubscription<String> subscription = flutter.stdout.listen((String e) {
|
||||
stdout.writeln(e);
|
||||
// If hot reload properly executes newly added code, the 'RELOAD WORKED' message should
|
||||
// be printed before 'TICK 2'. If we don't wait for some signal that the build method
|
||||
// has executed after the reload, this test can encounter a race.
|
||||
if (e.contains('((((TICK 2))))')) {
|
||||
completer.complete();
|
||||
}
|
||||
});
|
||||
await flutter.run();
|
||||
project.uncommentHotReloadPrint();
|
||||
try {
|
||||
await flutter.hotReload();
|
||||
await completer.future;
|
||||
expect(stdout.toString(), contains('(((((RELOAD WORKED)))))'));
|
||||
} finally {
|
||||
await subscription.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
testWithoutContext('hot restart works without error', () async {
|
||||
await flutter.run(verbose: true);
|
||||
await flutter.hotRestart();
|
||||
});
|
||||
|
||||
testWithoutContext('breakpoints are hit after hot reload', () async {
|
||||
Isolate isolate;
|
||||
final Completer<void> sawTick1 = Completer<void>();
|
||||
final Completer<void> sawDebuggerPausedMessage = Completer<void>();
|
||||
final StreamSubscription<String> subscription = flutter.stdout.listen((String line) {
|
||||
if (line.contains('((((TICK 1))))')) {
|
||||
expect(sawTick1.isCompleted, isFalse);
|
||||
sawTick1.complete();
|
||||
}
|
||||
if (line.contains('The application is paused in the debugger on a breakpoint.')) {
|
||||
expect(sawDebuggerPausedMessage.isCompleted, isFalse);
|
||||
sawDebuggerPausedMessage.complete();
|
||||
}
|
||||
});
|
||||
await flutter.run(withDebugger: true, startPaused: true);
|
||||
await flutter
|
||||
.resume(); // we start paused so we can set up our TICK 1 listener before the app starts
|
||||
unawaited(
|
||||
sawTick1.future.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () {
|
||||
// This print is useful for people debugging this test. Normally we would avoid printing in
|
||||
// a test but this is an exception because it's useful ambient information.
|
||||
// ignore: avoid_print
|
||||
print('The test app is taking longer than expected to print its synchronization line...');
|
||||
},
|
||||
),
|
||||
);
|
||||
printOnFailure('waiting for synchronization line...');
|
||||
await sawTick1.future; // after this, app is in steady state
|
||||
await flutter.addBreakpoint(project.scheduledBreakpointUri, project.scheduledBreakpointLine);
|
||||
await Future<void>.delayed(const Duration(seconds: 2));
|
||||
await flutter.hotReload(); // reload triggers code which eventually hits the breakpoint
|
||||
isolate = await flutter.waitForPause();
|
||||
expect(isolate.pauseEvent?.kind, equals(EventKind.kPauseBreakpoint));
|
||||
await flutter.resume();
|
||||
await flutter.addBreakpoint(project.buildBreakpointUri, project.buildBreakpointLine);
|
||||
bool reloaded = false;
|
||||
final Future<void> reloadFuture = flutter.hotReload().then((void value) {
|
||||
reloaded = true;
|
||||
});
|
||||
printOnFailure('waiting for pause...');
|
||||
isolate = await flutter.waitForPause();
|
||||
expect(isolate.pauseEvent?.kind, equals(EventKind.kPauseBreakpoint));
|
||||
printOnFailure('waiting for debugger message...');
|
||||
await sawDebuggerPausedMessage.future;
|
||||
expect(reloaded, isFalse);
|
||||
printOnFailure('waiting for resume...');
|
||||
await flutter.resume();
|
||||
printOnFailure('waiting for reload future...');
|
||||
await reloadFuture;
|
||||
expect(reloaded, isTrue);
|
||||
reloaded = false;
|
||||
printOnFailure('subscription cancel...');
|
||||
await subscription.cancel();
|
||||
});
|
||||
|
||||
testWithoutContext("hot reload doesn't reassemble if paused", () async {
|
||||
final Completer<void> sawTick1 = Completer<void>();
|
||||
final Completer<void> sawDebuggerPausedMessage1 = Completer<void>();
|
||||
final Completer<void> sawDebuggerPausedMessage2 = Completer<void>();
|
||||
final StreamSubscription<String> subscription = flutter.stdout.listen((String line) {
|
||||
printOnFailure('[LOG]:"$line"');
|
||||
if (line.contains('(((TICK 1)))')) {
|
||||
expect(sawTick1.isCompleted, isFalse);
|
||||
sawTick1.complete();
|
||||
}
|
||||
if (line.contains('The application is paused in the debugger on a breakpoint.')) {
|
||||
expect(sawDebuggerPausedMessage1.isCompleted, isFalse);
|
||||
sawDebuggerPausedMessage1.complete();
|
||||
}
|
||||
if (line.contains(
|
||||
'The application is paused in the debugger on a breakpoint; interface might not update.',
|
||||
)) {
|
||||
expect(sawDebuggerPausedMessage2.isCompleted, isFalse);
|
||||
sawDebuggerPausedMessage2.complete();
|
||||
}
|
||||
});
|
||||
await flutter.run(withDebugger: true);
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
await sawTick1.future;
|
||||
await flutter.addBreakpoint(project.buildBreakpointUri, project.buildBreakpointLine);
|
||||
bool reloaded = false;
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
final Future<void> reloadFuture = flutter.hotReload().then((void value) {
|
||||
reloaded = true;
|
||||
});
|
||||
final Isolate isolate = await flutter.waitForPause();
|
||||
expect(isolate.pauseEvent?.kind, equals(EventKind.kPauseBreakpoint));
|
||||
expect(reloaded, isFalse);
|
||||
await sawDebuggerPausedMessage1
|
||||
.future; // this is the one where it say "uh, you broke into the debugger while reloading"
|
||||
await reloadFuture; // this is the one where it times out because you're in the debugger
|
||||
expect(reloaded, isTrue);
|
||||
await flutter.hotReload(); // now we're already paused
|
||||
await sawDebuggerPausedMessage2.future; // so we just get told that nothing is going to happen
|
||||
await flutter.resume();
|
||||
await subscription.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
bool _isHotReloadCompletionEvent(Map<String, Object?>? event) {
|
||||
return event != null &&
|
||||
event['event'] == 'app.progress' &&
|
||||
event['params'] != null &&
|
||||
(event['params']! as Map<String, Object?>)['progressId'] == 'hot.reload' &&
|
||||
(event['params']! as Map<String, Object?>)['finished'] == true;
|
||||
testAll();
|
||||
}
|
||||
|
@ -5,82 +5,9 @@
|
||||
@Tags(<String>['flutter-test-driver'])
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:file/file.dart';
|
||||
|
||||
import '../src/common.dart';
|
||||
import 'test_data/hot_reload_with_asset.dart';
|
||||
import 'test_driver.dart';
|
||||
import 'test_utils.dart';
|
||||
import 'test_data/hot_reload_with_asset_test_common.dart';
|
||||
|
||||
void main() {
|
||||
late Directory tempDir;
|
||||
final HotReloadWithAssetProject project = HotReloadWithAssetProject();
|
||||
late 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 does not need to sync assets on the first reload', () async {
|
||||
final Completer<void> onFirstLoad = Completer<void>();
|
||||
final Completer<void> onSecondLoad = Completer<void>();
|
||||
|
||||
flutter.stdout.listen((String line) {
|
||||
// If the asset fails to load, this message will be printed instead.
|
||||
// this indicates that the devFS was not able to locate the asset
|
||||
// after the hot reload.
|
||||
if (line.contains('FAILED TO LOAD')) {
|
||||
fail('Did not load asset: $line');
|
||||
}
|
||||
if (line.contains('LOADED DATA')) {
|
||||
onFirstLoad.complete();
|
||||
}
|
||||
if (line.contains('SECOND DATA')) {
|
||||
onSecondLoad.complete();
|
||||
}
|
||||
});
|
||||
flutter.stdout.listen(printOnFailure);
|
||||
await flutter.run();
|
||||
await onFirstLoad.future;
|
||||
|
||||
project.uncommentHotReloadPrint();
|
||||
await flutter.hotReload();
|
||||
await onSecondLoad.future;
|
||||
});
|
||||
|
||||
testWithoutContext('hot restart does not need to sync assets on the first reload', () async {
|
||||
final Completer<void> onFirstLoad = Completer<void>();
|
||||
final Completer<void> onSecondLoad = Completer<void>();
|
||||
|
||||
flutter.stdout.listen((String line) {
|
||||
// If the asset fails to load, this message will be printed instead.
|
||||
// this indicates that the devFS was not able to locate the asset
|
||||
// after the hot reload.
|
||||
if (line.contains('FAILED TO LOAD')) {
|
||||
fail('Did not load asset: $line');
|
||||
}
|
||||
if (line.contains('LOADED DATA')) {
|
||||
onFirstLoad.complete();
|
||||
}
|
||||
if (line.contains('SECOND DATA')) {
|
||||
onSecondLoad.complete();
|
||||
}
|
||||
});
|
||||
flutter.stdout.listen(printOnFailure);
|
||||
await flutter.run();
|
||||
await onFirstLoad.future;
|
||||
|
||||
project.uncommentHotReloadPrint();
|
||||
await flutter.hotRestart();
|
||||
await onSecondLoad.future;
|
||||
});
|
||||
testAll();
|
||||
}
|
||||
|
@ -5,46 +5,9 @@
|
||||
@Tags(<String>['flutter-test-driver'])
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:file/file.dart';
|
||||
|
||||
import '../src/common.dart';
|
||||
import 'test_data/stateless_stateful_project.dart';
|
||||
import 'test_driver.dart';
|
||||
import 'test_utils.dart';
|
||||
import 'test_data/stateless_stateful_hot_reload_test_common.dart';
|
||||
|
||||
// This test verifies that we can hot reload a stateless widget into a
|
||||
// stateful one and back.
|
||||
void main() {
|
||||
late Directory tempDir;
|
||||
final HotReloadProject project = HotReloadProject();
|
||||
late FlutterRunTestDriver flutter;
|
||||
|
||||
setUp(() async {
|
||||
tempDir = createResolvedTempDirectorySync('hot_reload_test.');
|
||||
await project.setUpIn(tempDir);
|
||||
flutter = FlutterRunTestDriver(tempDir);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await flutter.stop();
|
||||
tryToDelete(tempDir);
|
||||
});
|
||||
|
||||
testWithoutContext('Can switch from stateless to stateful', () async {
|
||||
await flutter.run();
|
||||
await flutter.hotReload();
|
||||
final StringBuffer stdout = StringBuffer();
|
||||
final StreamSubscription<String> subscription = flutter.stdout.listen(stdout.writeln);
|
||||
|
||||
// switch to stateful.
|
||||
project.toggleState();
|
||||
await flutter.hotReload();
|
||||
|
||||
final String logs = stdout.toString();
|
||||
|
||||
expect(logs, contains('STATEFUL'));
|
||||
await subscription.cancel();
|
||||
});
|
||||
testAll();
|
||||
}
|
||||
|
@ -0,0 +1,247 @@
|
||||
// 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 'dart:async';
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:vm_service/vm_service.dart';
|
||||
|
||||
import '../../src/common.dart';
|
||||
import '../test_driver.dart';
|
||||
import '../test_utils.dart';
|
||||
import 'hot_reload_project.dart';
|
||||
|
||||
void testAll({
|
||||
bool chrome = false,
|
||||
List<String> additionalCommandArgs = const <String>[],
|
||||
Object? skip = false,
|
||||
}) {
|
||||
group('chrome: $chrome'
|
||||
'${additionalCommandArgs.isEmpty ? '' : ' with args: $additionalCommandArgs'}', () {
|
||||
late Directory tempDir;
|
||||
final HotReloadProject project = HotReloadProject();
|
||||
late 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 works without error', () async {
|
||||
await flutter.run(chrome: chrome, additionalCommandArgs: additionalCommandArgs);
|
||||
await flutter.hotReload();
|
||||
});
|
||||
|
||||
testWithoutContext('multiple overlapping hot reload are debounced and queued', () async {
|
||||
await flutter.run(chrome: chrome, additionalCommandArgs: additionalCommandArgs);
|
||||
// Capture how many *real* hot reloads occur.
|
||||
int numReloads = 0;
|
||||
final StreamSubscription<void> subscription = flutter.stdout
|
||||
.map(parseFlutterResponse)
|
||||
.where(_isHotReloadCompletionEvent)
|
||||
.listen((_) => numReloads++);
|
||||
|
||||
// To reduce tests flaking, override the debounce timer to something higher than
|
||||
// the default to ensure the hot reloads that are supposed to arrive within the
|
||||
// debounce period will even on slower CI machines.
|
||||
const int hotReloadDebounceOverrideMs = 250;
|
||||
const Duration delay = Duration(milliseconds: hotReloadDebounceOverrideMs * 2);
|
||||
|
||||
Future<void> doReload([void _]) => flutter.hotReload(
|
||||
debounce: true,
|
||||
debounceDurationOverrideMs: hotReloadDebounceOverrideMs,
|
||||
);
|
||||
|
||||
try {
|
||||
await Future.wait<void>(<Future<void>>[
|
||||
doReload(),
|
||||
doReload(),
|
||||
Future<void>.delayed(delay).then(doReload),
|
||||
Future<void>.delayed(delay).then(doReload),
|
||||
]);
|
||||
|
||||
// We should only get two reloads, as the first two will have been
|
||||
// merged together by the debounce, and the second two also.
|
||||
expect(numReloads, equals(2));
|
||||
} finally {
|
||||
await subscription.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
testWithoutContext('newly added code executes during hot reload', () async {
|
||||
final StringBuffer stdout = StringBuffer();
|
||||
final Completer<void> sawTick1 = Completer<void>();
|
||||
final Completer<void> sawTick2 = Completer<void>();
|
||||
final StreamSubscription<String> subscription = flutter.stdout.listen((String e) {
|
||||
stdout.writeln(e);
|
||||
// Initial run should run the build method before we try and hot reload.
|
||||
if (e.contains('(((TICK 1)))')) {
|
||||
sawTick1.complete();
|
||||
}
|
||||
// If hot reload properly executes newly added code, the 'RELOAD WORKED' message should
|
||||
// be printed before 'TICK 2'. If we don't wait for some signal that the build method
|
||||
// has executed after the reload, this test can encounter a race.
|
||||
if (e.contains('((((TICK 2))))')) {
|
||||
sawTick2.complete();
|
||||
}
|
||||
});
|
||||
await flutter.run(chrome: chrome, additionalCommandArgs: additionalCommandArgs);
|
||||
await sawTick1.future;
|
||||
project.uncommentHotReloadPrint();
|
||||
try {
|
||||
await flutter.hotReload();
|
||||
await sawTick2.future;
|
||||
expect(stdout.toString(), contains('(((((RELOAD WORKED)))))'));
|
||||
} finally {
|
||||
await subscription.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
testWithoutContext('hot restart works without error', () async {
|
||||
await flutter.run(
|
||||
verbose: true,
|
||||
chrome: chrome,
|
||||
additionalCommandArgs: additionalCommandArgs,
|
||||
);
|
||||
await flutter.hotRestart();
|
||||
});
|
||||
|
||||
testWithoutContext('breakpoints are hit after hot reload', () async {
|
||||
Isolate isolate;
|
||||
final Completer<void> sawTick1 = Completer<void>();
|
||||
final Completer<void> sawDebuggerPausedMessage = Completer<void>();
|
||||
final StreamSubscription<String> subscription = flutter.stdout.listen((String line) {
|
||||
if (line.contains('((((TICK 1))))')) {
|
||||
expect(sawTick1.isCompleted, isFalse);
|
||||
sawTick1.complete();
|
||||
}
|
||||
if (line.contains('The application is paused in the debugger on a breakpoint.')) {
|
||||
expect(sawDebuggerPausedMessage.isCompleted, isFalse);
|
||||
sawDebuggerPausedMessage.complete();
|
||||
}
|
||||
});
|
||||
await flutter.run(
|
||||
withDebugger: true,
|
||||
startPaused: true,
|
||||
chrome: chrome,
|
||||
additionalCommandArgs: additionalCommandArgs,
|
||||
);
|
||||
await flutter
|
||||
.resume(); // we start paused so we can set up our TICK 1 listener before the app starts
|
||||
unawaited(
|
||||
sawTick1.future.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () {
|
||||
// This print is useful for people debugging this test. Normally we would avoid printing
|
||||
// in a test but this is an exception because it's useful ambient information.
|
||||
// ignore: avoid_print
|
||||
print(
|
||||
'The test app is taking longer than expected to print its synchronization line...',
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
printOnFailure('waiting for synchronization line...');
|
||||
await sawTick1.future; // after this, app is in steady state
|
||||
await flutter.addBreakpoint(project.scheduledBreakpointUri, project.scheduledBreakpointLine);
|
||||
await Future<void>.delayed(const Duration(seconds: 2));
|
||||
await flutter.hotReload(); // reload triggers code which eventually hits the breakpoint
|
||||
isolate = await flutter.waitForPause();
|
||||
expect(isolate.pauseEvent?.kind, equals(EventKind.kPauseBreakpoint));
|
||||
await flutter.resume();
|
||||
await flutter.addBreakpoint(project.buildBreakpointUri, project.buildBreakpointLine);
|
||||
bool reloaded = false;
|
||||
final Future<void> reloadFuture = flutter.hotReload().then((void value) {
|
||||
reloaded = true;
|
||||
});
|
||||
printOnFailure('waiting for pause...');
|
||||
isolate = await flutter.waitForPause();
|
||||
expect(isolate.pauseEvent?.kind, equals(EventKind.kPauseBreakpoint));
|
||||
if (!chrome) {
|
||||
// TODO(srujzs): Implement paused event messages for the web.
|
||||
// https://github.com/flutter/flutter/issues/162500
|
||||
printOnFailure('waiting for debugger message...');
|
||||
await sawDebuggerPausedMessage.future;
|
||||
}
|
||||
expect(reloaded, isFalse);
|
||||
printOnFailure('waiting for resume...');
|
||||
await flutter.resume();
|
||||
printOnFailure('waiting for reload future...');
|
||||
await reloadFuture;
|
||||
expect(reloaded, isTrue);
|
||||
reloaded = false;
|
||||
printOnFailure('subscription cancel...');
|
||||
await subscription.cancel();
|
||||
});
|
||||
|
||||
testWithoutContext(
|
||||
"hot reload doesn't reassemble if paused",
|
||||
() async {
|
||||
final Completer<void> sawTick1 = Completer<void>();
|
||||
final Completer<void> sawDebuggerPausedMessage1 = Completer<void>();
|
||||
final Completer<void> sawDebuggerPausedMessage2 = Completer<void>();
|
||||
final StreamSubscription<String> subscription = flutter.stdout.listen((String line) {
|
||||
printOnFailure('[LOG]:"$line"');
|
||||
if (line.contains('(((TICK 1)))')) {
|
||||
expect(sawTick1.isCompleted, isFalse);
|
||||
sawTick1.complete();
|
||||
}
|
||||
if (line.contains('The application is paused in the debugger on a breakpoint.')) {
|
||||
expect(sawDebuggerPausedMessage1.isCompleted, isFalse);
|
||||
sawDebuggerPausedMessage1.complete();
|
||||
}
|
||||
if (line.contains(
|
||||
'The application is paused in the debugger on a breakpoint; interface might not '
|
||||
'update.',
|
||||
)) {
|
||||
expect(sawDebuggerPausedMessage2.isCompleted, isFalse);
|
||||
sawDebuggerPausedMessage2.complete();
|
||||
}
|
||||
});
|
||||
await flutter.run(
|
||||
withDebugger: true,
|
||||
chrome: chrome,
|
||||
additionalCommandArgs: additionalCommandArgs,
|
||||
);
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
await sawTick1.future;
|
||||
await flutter.addBreakpoint(project.buildBreakpointUri, project.buildBreakpointLine);
|
||||
bool reloaded = false;
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
final Future<void> reloadFuture = flutter.hotReload().then((void value) {
|
||||
reloaded = true;
|
||||
});
|
||||
final Isolate isolate = await flutter.waitForPause();
|
||||
expect(isolate.pauseEvent?.kind, equals(EventKind.kPauseBreakpoint));
|
||||
expect(reloaded, isFalse);
|
||||
// this is the one where it say "uh, you broke into the debugger while reloading"
|
||||
await sawDebuggerPausedMessage1.future;
|
||||
await reloadFuture; // this is the one where it times out because you're in the debugger
|
||||
expect(reloaded, isTrue);
|
||||
await flutter.hotReload(); // now we're already paused
|
||||
await sawDebuggerPausedMessage2
|
||||
.future; // so we just get told that nothing is going to happen
|
||||
await flutter.resume();
|
||||
await subscription.cancel();
|
||||
},
|
||||
// On the web, hot reload cannot continue as the browser is paused and there are no multiple
|
||||
// isolates, so this test will wait forever.
|
||||
skip: chrome,
|
||||
);
|
||||
}, skip: skip);
|
||||
}
|
||||
|
||||
bool _isHotReloadCompletionEvent(Map<String, Object?>? event) {
|
||||
return event != null &&
|
||||
event['event'] == 'app.progress' &&
|
||||
event['params'] != null &&
|
||||
(event['params']! as Map<String, Object?>)['progressId'] == 'hot.reload' &&
|
||||
(event['params']! as Map<String, Object?>)['finished'] == true;
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
// 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 'dart:async';
|
||||
|
||||
import 'package:file/file.dart';
|
||||
|
||||
import '../../src/common.dart';
|
||||
import '../test_driver.dart';
|
||||
import '../test_utils.dart';
|
||||
import 'hot_reload_with_asset.dart';
|
||||
|
||||
void testAll({
|
||||
bool chrome = false,
|
||||
List<String> additionalCommandArgs = const <String>[],
|
||||
Object? skip = false,
|
||||
}) {
|
||||
group('chrome: $chrome'
|
||||
'${additionalCommandArgs.isEmpty ? '' : ' with args: $additionalCommandArgs'}', () {
|
||||
late Directory tempDir;
|
||||
final HotReloadWithAssetProject project = HotReloadWithAssetProject();
|
||||
late 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 does not need to sync assets on the first reload', () async {
|
||||
final Completer<void> onFirstLoad = Completer<void>();
|
||||
final Completer<void> onSecondLoad = Completer<void>();
|
||||
|
||||
flutter.stdout.listen((String line) {
|
||||
// If the asset fails to load, this message will be printed instead.
|
||||
// this indicates that the devFS was not able to locate the asset
|
||||
// after the hot reload.
|
||||
if (line.contains('FAILED TO LOAD')) {
|
||||
fail('Did not load asset: $line');
|
||||
}
|
||||
if (line.contains('LOADED DATA')) {
|
||||
onFirstLoad.complete();
|
||||
}
|
||||
if (line.contains('SECOND DATA')) {
|
||||
onSecondLoad.complete();
|
||||
}
|
||||
});
|
||||
flutter.stdout.listen(printOnFailure);
|
||||
await flutter.run(chrome: chrome, additionalCommandArgs: additionalCommandArgs);
|
||||
await onFirstLoad.future;
|
||||
|
||||
project.uncommentHotReloadPrint();
|
||||
await flutter.hotReload();
|
||||
await onSecondLoad.future;
|
||||
});
|
||||
|
||||
testWithoutContext('hot restart does not need to sync assets on the first reload', () async {
|
||||
final Completer<void> onFirstLoad = Completer<void>();
|
||||
final Completer<void> onSecondLoad = Completer<void>();
|
||||
|
||||
flutter.stdout.listen((String line) {
|
||||
// If the asset fails to load, this message will be printed instead.
|
||||
// this indicates that the devFS was not able to locate the asset
|
||||
// after the hot reload.
|
||||
if (line.contains('FAILED TO LOAD')) {
|
||||
fail('Did not load asset: $line');
|
||||
}
|
||||
if (line.contains('LOADED DATA')) {
|
||||
onFirstLoad.complete();
|
||||
}
|
||||
if (line.contains('SECOND DATA')) {
|
||||
onSecondLoad.complete();
|
||||
}
|
||||
});
|
||||
flutter.stdout.listen(printOnFailure);
|
||||
await flutter.run(chrome: chrome, additionalCommandArgs: additionalCommandArgs);
|
||||
await onFirstLoad.future;
|
||||
|
||||
project.uncommentHotReloadPrint();
|
||||
await flutter.hotRestart();
|
||||
await onSecondLoad.future;
|
||||
});
|
||||
}, skip: skip);
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
// 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 'dart:async';
|
||||
|
||||
import 'package:file/file.dart';
|
||||
|
||||
import '../../src/common.dart';
|
||||
import '../test_data/stateless_stateful_project.dart';
|
||||
import '../test_driver.dart';
|
||||
import '../test_utils.dart';
|
||||
|
||||
// This test verifies that we can hot reload a stateless widget into a
|
||||
// stateful one and back.
|
||||
void testAll({
|
||||
bool chrome = false,
|
||||
List<String> additionalCommandArgs = const <String>[],
|
||||
Object? skip = false,
|
||||
}) {
|
||||
group('chrome: $chrome'
|
||||
'${additionalCommandArgs.isEmpty ? '' : ' with args: $additionalCommandArgs'}', () {
|
||||
late Directory tempDir;
|
||||
final HotReloadProject project = HotReloadProject();
|
||||
late FlutterRunTestDriver flutter;
|
||||
|
||||
setUp(() async {
|
||||
tempDir = createResolvedTempDirectorySync('hot_reload_test.');
|
||||
await project.setUpIn(tempDir);
|
||||
flutter = FlutterRunTestDriver(tempDir);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await flutter.stop();
|
||||
tryToDelete(tempDir);
|
||||
});
|
||||
|
||||
testWithoutContext('Can switch from stateless to stateful', () async {
|
||||
final Completer<void> completer = Completer<void>();
|
||||
StreamSubscription<String> subscription = flutter.stdout.listen((String line) {
|
||||
if (line.contains('STATELESS')) {
|
||||
completer.complete();
|
||||
}
|
||||
});
|
||||
await flutter.run(chrome: chrome, additionalCommandArgs: additionalCommandArgs);
|
||||
// Wait for run to finish.
|
||||
await completer.future;
|
||||
await subscription.cancel();
|
||||
|
||||
await flutter.hotReload();
|
||||
final StringBuffer stdout = StringBuffer();
|
||||
subscription = flutter.stdout.listen(stdout.writeln);
|
||||
|
||||
// switch to stateful.
|
||||
project.toggleState();
|
||||
await flutter.hotReload();
|
||||
|
||||
final String logs = stdout.toString();
|
||||
|
||||
expect(logs, contains('STATEFUL'));
|
||||
await subscription.cancel();
|
||||
});
|
||||
}, skip: skip);
|
||||
}
|
@ -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.
|
||||
|
||||
@Tags(<String>['flutter-test-driver'])
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import '../integration.shard/test_data/hot_reload_test_common.dart';
|
||||
import '../src/common.dart';
|
||||
|
||||
void main() {
|
||||
testAll(
|
||||
chrome: true,
|
||||
additionalCommandArgs: <String>[
|
||||
'--extra-front-end-options=--dartdevc-canary,--dartdevc-module-format=ddc',
|
||||
],
|
||||
// https://github.com/flutter/flutter/issues/162567
|
||||
skip: Platform.isWindows,
|
||||
);
|
||||
}
|
@ -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.
|
||||
|
||||
@Tags(<String>['flutter-test-driver'])
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import '../integration.shard/test_data/hot_reload_with_asset_test_common.dart';
|
||||
import '../src/common.dart';
|
||||
|
||||
void main() {
|
||||
testAll(
|
||||
chrome: true,
|
||||
additionalCommandArgs: <String>[
|
||||
'--extra-front-end-options=--dartdevc-canary,--dartdevc-module-format=ddc',
|
||||
],
|
||||
// https://github.com/flutter/flutter/issues/162567
|
||||
skip: Platform.isWindows,
|
||||
);
|
||||
}
|
@ -7,7 +7,7 @@ library;
|
||||
|
||||
import '../src/common.dart';
|
||||
|
||||
import 'test_data/hot_restart_web_utils.dart';
|
||||
import 'test_data/hot_restart_web_test_common.dart';
|
||||
|
||||
void main() async {
|
||||
await testAll(useDDCLibraryBundleFormat: false);
|
||||
|
@ -7,7 +7,7 @@ library;
|
||||
|
||||
import '../src/common.dart';
|
||||
|
||||
import 'test_data/hot_restart_web_utils.dart';
|
||||
import 'test_data/hot_restart_web_test_common.dart';
|
||||
|
||||
void main() async {
|
||||
await testAll(useDDCLibraryBundleFormat: true);
|
||||
|
@ -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.
|
||||
|
||||
@Tags(<String>['flutter-test-driver'])
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import '../integration.shard/test_data/stateless_stateful_hot_reload_test_common.dart';
|
||||
import '../src/common.dart';
|
||||
|
||||
void main() {
|
||||
testAll(
|
||||
chrome: true,
|
||||
additionalCommandArgs: <String>[
|
||||
'--extra-front-end-options=--dartdevc-canary,--dartdevc-module-format=ddc',
|
||||
],
|
||||
// https://github.com/flutter/flutter/issues/162567
|
||||
skip: Platform.isWindows,
|
||||
);
|
||||
}
|
@ -72,11 +72,13 @@ Future<void> _testProject(
|
||||
late Directory tempDir;
|
||||
late FlutterRunTestDriver flutter;
|
||||
|
||||
final String testName = 'Hot restart (index.html: $name)';
|
||||
final List<String> additionalCommandArgs =
|
||||
useDDCLibraryBundleFormat
|
||||
? <String>['--extra-front-end-options=--dartdevc-canary,--dartdevc-module-format=ddc']
|
||||
: <String>[];
|
||||
final String testName =
|
||||
'Hot restart (index.html: $name)'
|
||||
'${additionalCommandArgs.isEmpty ? '' : ' with args: $additionalCommandArgs'}';
|
||||
|
||||
setUp(() async {
|
||||
tempDir = createResolvedTempDirectorySync('hot_restart_test.');
|
Loading…
x
Reference in New Issue
Block a user