diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart index d1b80a90c2..05fffc59eb 100644 --- a/packages/flutter_tools/lib/src/devfs.dart +++ b/packages/flutter_tools/lib/src/devfs.dart @@ -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 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> pendingAssetBuilds = >[]; 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, diff --git a/packages/flutter_tools/lib/src/isolated/devfs_web.dart b/packages/flutter_tools/lib/src/isolated/devfs_web.dart index c3d06240b1..4423a59d07 100644 --- a/packages/flutter_tools/lib/src/isolated/devfs_web.dart +++ b/packages/flutter_tools/lib/src/isolated/devfs_web.dart @@ -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": "", + /// "id": "", + /// }, + /// ] + /// ``` void performRestart(List 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({'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": "", + /// "libraries": ["", ""], + /// }, + /// ] + /// ``` + /// + /// The path of the output file should stay consistent across the lifetime of + /// the app. + void performReload(List modules) { + final List> moduleToLibrary = >[]; + for (final String module in modules) { + final ModuleMetadata metadata = ModuleMetadata.fromJson( + json.decode(utf8.decode(_webMemoryFS.metadataFiles['$module.metadata']!.toList())) + as Map, + ); + final List libraries = metadata.libraries.keys.toList(); + moduleToLibrary.add({'src': module, 'libraries': libraries}); + } + writeFile('reload_scripts.json', json.encode(moduleToLibrary)); + } + @visibleForTesting List 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(), diff --git a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart index a1c7faf23a..f3ccd0636f 100644 --- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart @@ -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 _updateDevFS({bool fullRestart = false}) async { + Future _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, diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 2cd801fd7a..08f68a3091 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -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'; diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index b1b8df1b09..1ba9d5cb94 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -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, diff --git a/packages/flutter_tools/lib/src/web/bootstrap.dart b/packages/flutter_tools/lib/src/web/bootstrap.dart index 63e43fcd1c..95383ff086 100644 --- a/packages/flutter_tools/lib/src/web/bootstrap.dart +++ b/packages/flutter_tools/lib/src/web/bootstrap.dart @@ -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) { diff --git a/packages/flutter_tools/lib/src/web/module_metadata.dart b/packages/flutter_tools/lib/src/web/module_metadata.dart new file mode 100644 index 0000000000..6b78662372 --- /dev/null +++ b/packages/flutter_tools/lib/src/web/module_metadata.dart @@ -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 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 json) + : name = _readRequiredField(json, nameField), + importUri = _readRequiredField(json, importUriField), + partUris = _readOptionalList(json, partUrisField) ?? []; + + 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 partUris; + + Map toJson() { + return { + nameField: name, + importUriField: importUri, + partUrisField: [...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 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 l in _readRequiredList>( + 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 libraries = {}; + + /// 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 toJson() { + return { + versionField: version, + nameField: name, + closureNameField: closureName, + sourceMapUriField: sourceMapUri, + moduleUriField: moduleUri, + librariesField: >[ + for (final LibraryMetadata lib in libraries.values) lib.toJson(), + ], + }; + } +} + +T _readRequiredField(Map json, String field) { + if (!json.containsKey(field)) { + throw FormatException('Required field $field is not set in $json'); + } + return json[field]! as T; +} + +T? _readOptionalField(Map json, String field) => json[field] as T?; + +List _readRequiredList(Map json, String field) { + final List list = _readRequiredField>(json, field); + return List.castFrom(list); +} + +List? _readOptionalList(Map json, String field) { + final List? list = _readOptionalField>(json, field); + return list == null ? null : List.castFrom(list); +} diff --git a/packages/flutter_tools/test/general.shard/resident_runner_helpers.dart b/packages/flutter_tools/test/general.shard/resident_runner_helpers.dart index 3062386a53..e8c47c86ab 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_helpers.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_helpers.dart @@ -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 { diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index a7bbb0c737..9cc275e4aa 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -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 { diff --git a/packages/flutter_tools/test/integration.shard/hot_reload_test.dart b/packages/flutter_tools/test/integration.shard/hot_reload_test.dart index fe68c102f5..f89f2db72a 100644 --- a/packages/flutter_tools/test/integration.shard/hot_reload_test.dart +++ b/packages/flutter_tools/test/integration.shard/hot_reload_test.dart @@ -5,204 +5,9 @@ @Tags(['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 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 doReload([void _]) => - flutter.hotReload(debounce: true, debounceDurationOverrideMs: hotReloadDebounceOverrideMs); - - try { - await Future.wait(>[ - doReload(), - doReload(), - Future.delayed(delay).then(doReload), - Future.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 completer = Completer(); - final StreamSubscription 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 sawTick1 = Completer(); - final Completer sawDebuggerPausedMessage = Completer(); - final StreamSubscription 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.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 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 sawTick1 = Completer(); - final Completer sawDebuggerPausedMessage1 = Completer(); - final Completer sawDebuggerPausedMessage2 = Completer(); - final StreamSubscription 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.delayed(const Duration(seconds: 1)); - await sawTick1.future; - await flutter.addBreakpoint(project.buildBreakpointUri, project.buildBreakpointLine); - bool reloaded = false; - await Future.delayed(const Duration(seconds: 1)); - final Future 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? event) { - return event != null && - event['event'] == 'app.progress' && - event['params'] != null && - (event['params']! as Map)['progressId'] == 'hot.reload' && - (event['params']! as Map)['finished'] == true; + testAll(); } diff --git a/packages/flutter_tools/test/integration.shard/hot_reload_with_asset_test.dart b/packages/flutter_tools/test/integration.shard/hot_reload_with_asset_test.dart index aca0f0e41b..14c59fb84a 100644 --- a/packages/flutter_tools/test/integration.shard/hot_reload_with_asset_test.dart +++ b/packages/flutter_tools/test/integration.shard/hot_reload_with_asset_test.dart @@ -5,82 +5,9 @@ @Tags(['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 onFirstLoad = Completer(); - final Completer onSecondLoad = Completer(); - - 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 onFirstLoad = Completer(); - final Completer onSecondLoad = Completer(); - - 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(); } diff --git a/packages/flutter_tools/test/integration.shard/stateless_stateful_hot_reload_test.dart b/packages/flutter_tools/test/integration.shard/stateless_stateful_hot_reload_test.dart index 5b7de6ad32..49aef21824 100644 --- a/packages/flutter_tools/test/integration.shard/stateless_stateful_hot_reload_test.dart +++ b/packages/flutter_tools/test/integration.shard/stateless_stateful_hot_reload_test.dart @@ -5,46 +5,9 @@ @Tags(['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 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(); } diff --git a/packages/flutter_tools/test/integration.shard/test_data/hot_reload_test_common.dart b/packages/flutter_tools/test/integration.shard/test_data/hot_reload_test_common.dart new file mode 100644 index 0000000000..ef64309068 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/test_data/hot_reload_test_common.dart @@ -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 additionalCommandArgs = const [], + 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 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 doReload([void _]) => flutter.hotReload( + debounce: true, + debounceDurationOverrideMs: hotReloadDebounceOverrideMs, + ); + + try { + await Future.wait(>[ + doReload(), + doReload(), + Future.delayed(delay).then(doReload), + Future.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 sawTick1 = Completer(); + final Completer sawTick2 = Completer(); + final StreamSubscription 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 sawTick1 = Completer(); + final Completer sawDebuggerPausedMessage = Completer(); + final StreamSubscription 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.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 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 sawTick1 = Completer(); + final Completer sawDebuggerPausedMessage1 = Completer(); + final Completer sawDebuggerPausedMessage2 = Completer(); + final StreamSubscription 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.delayed(const Duration(seconds: 1)); + await sawTick1.future; + await flutter.addBreakpoint(project.buildBreakpointUri, project.buildBreakpointLine); + bool reloaded = false; + await Future.delayed(const Duration(seconds: 1)); + final Future 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? event) { + return event != null && + event['event'] == 'app.progress' && + event['params'] != null && + (event['params']! as Map)['progressId'] == 'hot.reload' && + (event['params']! as Map)['finished'] == true; +} diff --git a/packages/flutter_tools/test/integration.shard/test_data/hot_reload_with_asset_test_common.dart b/packages/flutter_tools/test/integration.shard/test_data/hot_reload_with_asset_test_common.dart new file mode 100644 index 0000000000..ba2006ec02 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/test_data/hot_reload_with_asset_test_common.dart @@ -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 additionalCommandArgs = const [], + 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 onFirstLoad = Completer(); + final Completer onSecondLoad = Completer(); + + 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 onFirstLoad = Completer(); + final Completer onSecondLoad = Completer(); + + 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); +} diff --git a/packages/flutter_tools/test/integration.shard/test_data/stateless_stateful_hot_reload_test_common.dart b/packages/flutter_tools/test/integration.shard/test_data/stateless_stateful_hot_reload_test_common.dart new file mode 100644 index 0000000000..9acd6a86fc --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/test_data/stateless_stateful_hot_reload_test_common.dart @@ -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 additionalCommandArgs = const [], + 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 completer = Completer(); + StreamSubscription 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); +} diff --git a/packages/flutter_tools/test/web.shard/hot_reload_web_test.dart b/packages/flutter_tools/test/web.shard/hot_reload_web_test.dart new file mode 100644 index 0000000000..d57ba36c1d --- /dev/null +++ b/packages/flutter_tools/test/web.shard/hot_reload_web_test.dart @@ -0,0 +1,22 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@Tags(['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: [ + '--extra-front-end-options=--dartdevc-canary,--dartdevc-module-format=ddc', + ], + // https://github.com/flutter/flutter/issues/162567 + skip: Platform.isWindows, + ); +} diff --git a/packages/flutter_tools/test/web.shard/hot_reload_with_asset_web_test.dart b/packages/flutter_tools/test/web.shard/hot_reload_with_asset_web_test.dart new file mode 100644 index 0000000000..d447f0ee34 --- /dev/null +++ b/packages/flutter_tools/test/web.shard/hot_reload_with_asset_web_test.dart @@ -0,0 +1,22 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@Tags(['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: [ + '--extra-front-end-options=--dartdevc-canary,--dartdevc-module-format=ddc', + ], + // https://github.com/flutter/flutter/issues/162567 + skip: Platform.isWindows, + ); +} diff --git a/packages/flutter_tools/test/web.shard/hot_restart_web_amd_test.dart b/packages/flutter_tools/test/web.shard/hot_restart_web_amd_test.dart index b148ae7129..48e5db9236 100644 --- a/packages/flutter_tools/test/web.shard/hot_restart_web_amd_test.dart +++ b/packages/flutter_tools/test/web.shard/hot_restart_web_amd_test.dart @@ -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); diff --git a/packages/flutter_tools/test/web.shard/hot_restart_web_ddc_library_bundle_test.dart b/packages/flutter_tools/test/web.shard/hot_restart_web_ddc_library_bundle_test.dart index 5799596e58..542e728e3b 100644 --- a/packages/flutter_tools/test/web.shard/hot_restart_web_ddc_library_bundle_test.dart +++ b/packages/flutter_tools/test/web.shard/hot_restart_web_ddc_library_bundle_test.dart @@ -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); diff --git a/packages/flutter_tools/test/web.shard/stateless_stateful_hot_reload_web_test.dart b/packages/flutter_tools/test/web.shard/stateless_stateful_hot_reload_web_test.dart new file mode 100644 index 0000000000..e5101085f7 --- /dev/null +++ b/packages/flutter_tools/test/web.shard/stateless_stateful_hot_reload_web_test.dart @@ -0,0 +1,22 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@Tags(['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: [ + '--extra-front-end-options=--dartdevc-canary,--dartdevc-module-format=ddc', + ], + // https://github.com/flutter/flutter/issues/162567 + skip: Platform.isWindows, + ); +} diff --git a/packages/flutter_tools/test/web.shard/test_data/hot_restart_web_utils.dart b/packages/flutter_tools/test/web.shard/test_data/hot_restart_web_test_common.dart similarity index 96% rename from packages/flutter_tools/test/web.shard/test_data/hot_restart_web_utils.dart rename to packages/flutter_tools/test/web.shard/test_data/hot_restart_web_test_common.dart index 7e43de0139..91aa3a1692 100644 --- a/packages/flutter_tools/test/web.shard/test_data/hot_restart_web_utils.dart +++ b/packages/flutter_tools/test/web.shard/test_data/hot_restart_web_test_common.dart @@ -72,11 +72,13 @@ Future _testProject( late Directory tempDir; late FlutterRunTestDriver flutter; - final String testName = 'Hot restart (index.html: $name)'; final List additionalCommandArgs = useDDCLibraryBundleFormat ? ['--extra-front-end-options=--dartdevc-canary,--dartdevc-module-format=ddc'] : []; + final String testName = + 'Hot restart (index.html: $name)' + '${additionalCommandArgs.isEmpty ? '' : ' with args: $additionalCommandArgs'}'; setUp(() async { tempDir = createResolvedTempDirectorySync('hot_restart_test.');