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:
Srujan Gaddam 2025-02-02 22:10:24 -08:00 committed by GitHub
parent 4d08217f8d
commit 8e2a6fc3fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 866 additions and 360 deletions

View File

@ -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,

View File

@ -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(),

View File

@ -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,

View File

@ -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';

View File

@ -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,

View File

@ -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) {

View 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);
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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,
);
}

View File

@ -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,
);
}

View File

@ -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);

View File

@ -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);

View File

@ -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,
);
}

View File

@ -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.');