Skip to content

Support hot reload testing #2611

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions dwds/test/fixtures/context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -371,6 +372,9 @@ class TestContext {
packageUriMapper,
() async => {},
buildSettings,
hotReloadSourcesUri: Uri.parse(
'http://localhost:$port/${WebDevFS.reloadScriptsFileName}',
),
).strategy
: FrontendServerDdcStrategyProvider(
testSettings.reloadConfiguration,
Expand Down
8 changes: 8 additions & 0 deletions dwds/test/fixtures/project.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
97 changes: 97 additions & 0 deletions dwds/test/hot_reload_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// 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<void> makeEditAndRecompile() async {
context.makeEditToDartLibFile(
libFileName: 'library1.dart',
toReplace: originalString,
replaceWith: newString,
);
await context.recompile(fullRestart: false);
}

void undoEdit() {
context.makeEditToDartLibFile(
libFileName: 'library1.dart',
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 {
undoEdit();
await context.tearDown();
});

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));
}
5 changes: 5 additions & 0 deletions fixtures/_testHotReload/lib/library1.dart
Original file line number Diff line number Diff line change
@@ -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 => 'Hello World!';
9 changes: 9 additions & 0 deletions fixtures/_testHotReload/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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

7 changes: 7 additions & 0 deletions fixtures/_testHotReload/web/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<html>

<head>
<script defer src="main.dart.js"></script>
</head>

</html>
23 changes: 23 additions & 0 deletions fixtures/_testHotReload/web/main.dart
Original file line number Diff line number Diff line change
@@ -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();
}
65 changes: 63 additions & 2 deletions frontend_server_common/lib/src/devfs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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": "<file_name>",
/// "libraries": ["<lib1>", "<lib2>"],
/// },
/// ]
/// ```
///
/// 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<String> modules, String entrypointDirectory) {
final moduleToLibrary = <Map<String, Object>>[];
for (final module in modules) {
final metadata = ModuleMetadata.fromJson(
json.decode(utf8
.decode(assetServer.getMetadata('$module.metadata').toList()))
as Map<String, dynamic>,
);
final libraries = metadata.libraries.keys.toList();
moduleToLibrary.add(<String, Object>{
'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);
Expand Down
Loading