From 5b391a5acadcc4a8551a9a99c69595297b6a95a4 Mon Sep 17 00:00:00 2001 From: Srujan Gaddam Date: Thu, 10 Apr 2025 17:48:25 -0700 Subject: [PATCH 1/2] Support hot reload testing - Adds code from flutter_tools to pipe a list of modules and their corresponding libraries to the embedder. Includes some path modifications not in flutter_tools to handle the test project. - Adds a test and test project to modify a variable, and check that the new value is read on reload only when reevaluated. --- dwds/test/fixtures/context.dart | 4 + dwds/test/fixtures/project.dart | 8 ++ dwds/test/hot_reload_test.dart | 96 +++++++++++++++++++++++ fixtures/_testHotReload/lib/library1.dart | 5 ++ fixtures/_testHotReload/pubspec.yaml | 9 +++ fixtures/_testHotReload/web/index.html | 7 ++ fixtures/_testHotReload/web/main.dart | 23 ++++++ frontend_server_common/lib/src/devfs.dart | 65 ++++++++++++++- 8 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 dwds/test/hot_reload_test.dart create mode 100644 fixtures/_testHotReload/lib/library1.dart create mode 100644 fixtures/_testHotReload/pubspec.yaml create mode 100644 fixtures/_testHotReload/web/index.html create mode 100644 fixtures/_testHotReload/web/main.dart diff --git a/dwds/test/fixtures/context.dart b/dwds/test/fixtures/context.dart index 1d9f497fb..abe8f8f1d 100644 --- a/dwds/test/fixtures/context.dart +++ b/dwds/test/fixtures/context.dart @@ -24,6 +24,7 @@ import 'package:dwds/src/services/expression_compiler_service.dart'; import 'package:dwds/src/utilities/dart_uri.dart'; import 'package:dwds/src/utilities/server.dart'; import 'package:file/local.dart'; +import 'package:frontend_server_common/src/devfs.dart'; import 'package:frontend_server_common/src/resident_runner.dart'; import 'package:http/http.dart'; import 'package:http/io_client.dart'; @@ -371,6 +372,9 @@ class TestContext { packageUriMapper, () async => {}, buildSettings, + hotReloadSourcesUri: Uri.parse( + 'http://localhost:$port/${WebDevFS.reloadScriptsFileName}', + ), ).strategy : FrontendServerDdcStrategyProvider( testSettings.reloadConfiguration, diff --git a/dwds/test/fixtures/project.dart b/dwds/test/fixtures/project.dart index 652fd5db2..25f58e017 100644 --- a/dwds/test/fixtures/project.dart +++ b/dwds/test/fixtures/project.dart @@ -135,6 +135,14 @@ class TestProject { htmlEntryFileName: 'index.html', ); + static const testHotReload = TestProject._( + packageName: '_test_hot_reload', + packageDirectory: '_testHotReload', + webAssetsPath: 'web', + dartEntryFileName: 'main.dart', + htmlEntryFileName: 'index.html', + ); + const TestProject._({ required this.packageName, required this.packageDirectory, diff --git a/dwds/test/hot_reload_test.dart b/dwds/test/hot_reload_test.dart new file mode 100644 index 000000000..6952462ac --- /dev/null +++ b/dwds/test/hot_reload_test.dart @@ -0,0 +1,96 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@Tags(['daily']) +@TestOn('vm') +@Timeout(Duration(minutes: 5)) +library; + +import 'package:dwds/expression_compiler.dart'; +import 'package:test/test.dart'; +import 'package:test_common/logging.dart'; +import 'package:test_common/test_sdk_configuration.dart'; +import 'package:vm_service/vm_service.dart'; + +import 'fixtures/context.dart'; +import 'fixtures/project.dart'; +import 'fixtures/utilities.dart'; + +const originalString = 'Hello World!'; +const newString = 'Bonjour le monde!'; + +void main() { + // Enable verbose logging for debugging. + final debug = false; + final provider = TestSdkConfigurationProvider( + verbose: debug, + canaryFeatures: true, + ddcModuleFormat: ModuleFormat.ddc, + ); + final project = TestProject.testHotReload; + final context = TestContext(project, provider); + + tearDownAll(provider.dispose); + + Future makeEditAndRecompile() async { + context.makeEditToDartLibFile( + libFileName: 'library1.dart', + toReplace: originalString, + replaceWith: newString, + ); + await context.recompile(fullRestart: false); + } + + void undoEdit() { + context.makeEditToDartEntryFile( + toReplace: newString, + replaceWith: originalString, + ); + } + + group('Injected client', () { + late VmService fakeClient; + + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + enableExpressionEvaluation: true, + compilationMode: CompilationMode.frontendServer, + moduleFormat: ModuleFormat.ddc, + canaryFeatures: true, + ), + ); + fakeClient = await context.connectFakeClient(); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test('can hot reload', () async { + final client = context.debugConnection.vmService; + await makeEditAndRecompile(); + + final vm = await client.getVM(); + final isolate = await client.getIsolate(vm.isolates!.first.id!); + + final report = await fakeClient.reloadSources(isolate.id!); + expect(report.success, true); + + var source = await context.webDriver.pageSource; + // Should not contain the change until the function that updates the page + // is evaluated in a hot reload. + expect(source, contains(originalString)); + expect(source.contains(newString), false); + + final rootLib = isolate.rootLib; + await client.evaluate(isolate.id!, rootLib!.id!, 'evaluate()'); + source = await context.webDriver.pageSource; + expect(source, contains(newString)); + expect(source.contains(originalString), false); + }); + }, timeout: Timeout.factor(2)); +} diff --git a/fixtures/_testHotReload/lib/library1.dart b/fixtures/_testHotReload/lib/library1.dart new file mode 100644 index 000000000..5d09d63ed --- /dev/null +++ b/fixtures/_testHotReload/lib/library1.dart @@ -0,0 +1,5 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +String get reloadValue => 'Bonjour le monde!'; diff --git a/fixtures/_testHotReload/pubspec.yaml b/fixtures/_testHotReload/pubspec.yaml new file mode 100644 index 000000000..3354b80f9 --- /dev/null +++ b/fixtures/_testHotReload/pubspec.yaml @@ -0,0 +1,9 @@ +name: _test_hot_reload +version: 1.0.0 +description: >- + A fake package used for testing hot reload. +publish_to: none + +environment: + sdk: ^3.7.0 + diff --git a/fixtures/_testHotReload/web/index.html b/fixtures/_testHotReload/web/index.html new file mode 100644 index 000000000..d93440a94 --- /dev/null +++ b/fixtures/_testHotReload/web/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/fixtures/_testHotReload/web/main.dart b/fixtures/_testHotReload/web/main.dart new file mode 100644 index 000000000..03fe05d3b --- /dev/null +++ b/fixtures/_testHotReload/web/main.dart @@ -0,0 +1,23 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. 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:core'; +import 'dart:js_interop'; + +import 'package:_test_hot_reload/library1.dart'; + +@JS('document.body.innerHTML') +external set innerHtml(String html); + +@JS('console.log') +external void log(String s); + +void evaluate() { + log('evaluate called $reloadValue'); + innerHtml = 'Program is running!\n $reloadValue}\n'; +} + +void main() { + evaluate(); +} diff --git a/frontend_server_common/lib/src/devfs.dart b/frontend_server_common/lib/src/devfs.dart index 2fb7bb96a..02a24839e 100644 --- a/frontend_server_common/lib/src/devfs.dart +++ b/frontend_server_common/lib/src/devfs.dart @@ -10,6 +10,8 @@ import 'dart:io'; import 'package:dwds/asset_reader.dart'; import 'package:dwds/config.dart'; import 'package:dwds/expression_compiler.dart'; +// ignore: implementation_imports +import 'package:dwds/src/debugging/metadata/module_metadata.dart'; import 'package:dwds/utilities.dart'; import 'package:file/file.dart'; import 'package:path/path.dart' as p; @@ -217,8 +219,7 @@ class WebDevFS { if (fullRestart) { performRestart(modules); } else { - // TODO(srujzs): Support hot reload testing. - throw Exception('Hot reload is not supported yet.'); + performReload(modules, prefix); } } return UpdateFSReport( @@ -249,6 +250,66 @@ class WebDevFS { assetServer.writeFile('restart_scripts.json', json.encode(srcIdsList)); } + static const String reloadScriptsFileName = 'reload_scripts.json'; + + /// 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. + /// + /// [entrypointDirectory] is used to make the module paths relative to the + /// entrypoint, which is needed in order to load `src`s correctly. + void performReload(List modules, String entrypointDirectory) { + final moduleToLibrary = >[]; + for (final module in modules) { + final metadata = ModuleMetadata.fromJson( + json.decode(utf8 + .decode(assetServer.getMetadata('$module.metadata').toList())) + as Map, + ); + final libraries = metadata.libraries.keys.toList(); + moduleToLibrary.add({ + 'src': _findModuleToLoad(module, entrypointDirectory), + 'libraries': libraries + }); + } + assetServer.writeFile(reloadScriptsFileName, json.encode(moduleToLibrary)); + } + + /// Given a [module] location from the [ModuleMetadata], return its path in + /// the server relative to the entrypoint in [entrypointDirectory]. + /// + /// This is needed in cases where the entrypoint is in a subdirectory in the + /// package. + String _findModuleToLoad(String module, String entrypointDirectory) { + if (entrypointDirectory.isEmpty) return module; + assert(entrypointDirectory.endsWith('/')); + if (module.startsWith(entrypointDirectory)) { + return module.substring(entrypointDirectory.length); + } + var numDirs = entrypointDirectory.split('/').length - 1; + while (numDirs-- > 0) { + module = '../$module'; + } + return module; + } + File get ddcModuleLoaderJS => fileSystem.file(sdkLayout.ddcModuleLoaderJsPath); File get requireJS => fileSystem.file(sdkLayout.requireJsPath); From 4a0c080f258d6e8b8f39d2a265a70952a57d8f15 Mon Sep 17 00:00:00 2001 From: Srujan Gaddam Date: Mon, 14 Apr 2025 20:31:22 -0700 Subject: [PATCH 2/2] Fix undo edit step --- dwds/test/hot_reload_test.dart | 5 +++-- fixtures/_testHotReload/lib/library1.dart | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dwds/test/hot_reload_test.dart b/dwds/test/hot_reload_test.dart index 6952462ac..a836426c8 100644 --- a/dwds/test/hot_reload_test.dart +++ b/dwds/test/hot_reload_test.dart @@ -43,7 +43,8 @@ void main() { } void undoEdit() { - context.makeEditToDartEntryFile( + context.makeEditToDartLibFile( + libFileName: 'library1.dart', toReplace: newString, replaceWith: originalString, ); @@ -66,8 +67,8 @@ void main() { }); tearDown(() async { - await context.tearDown(); undoEdit(); + await context.tearDown(); }); test('can hot reload', () async { diff --git a/fixtures/_testHotReload/lib/library1.dart b/fixtures/_testHotReload/lib/library1.dart index 5d09d63ed..1daf3d4c3 100644 --- a/fixtures/_testHotReload/lib/library1.dart +++ b/fixtures/_testHotReload/lib/library1.dart @@ -2,4 +2,4 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -String get reloadValue => 'Bonjour le monde!'; +String get reloadValue => 'Hello World!';