From 36e22129b082297421bf1f3fc087ae5950bceca5 Mon Sep 17 00:00:00 2001 From: Srujan Gaddam Date: Tue, 1 Apr 2025 14:37:19 -0700 Subject: [PATCH 1/3] Add support for hot restart tests in DWDS with the frontend server - Aligns the bootstrap script with Flutter tools for the DDC library bundle format. This includes adding the dartReloadModifiedModules callback and the script to wait until all sources are loaded before calling main. - Aligns the recompile code with Flutter tools. This includes adding support for detecting invalidated files, using recompile-restart, and writing a script that DWDS can use to detect files for a hot restart. - Refactors hot restart tests into shared test code that is now tested both with the build daemon and the frontend server. Tests that rely on build events are skipped in the frontend server by design and tests that rely on deferring main are skipped with the new DDC library bundle format until https://github.com/dart-lang/sdk/issues/60528 is resolved. Tests are also renamed to use "restart" instead of "reload" to distinguish from hot reload tests, which will be added in a future CL. --- ...d_daemon_hot_restart_correctness_test.dart | 37 + dwds/test/build_daemon_hot_restart_test.dart | 37 + dwds/test/common/hot_restart_common.dart | 647 ++++++++++++++++++ .../hot_restart_correctness_common.dart | 225 ++++++ dwds/test/fixtures/context.dart | 34 +- ...d_server_hot_restart_correctness_test.dart | 37 + .../frontend_server_hot_restart_test.dart | 39 ++ dwds/test/reload_correctness_test.dart | 196 ------ dwds/test/reload_test.dart | 594 ---------------- frontend_server_common/lib/src/bootstrap.dart | 145 +++- frontend_server_common/lib/src/devfs.dart | 262 +++++-- .../lib/src/frontend_server_client.dart | 18 +- .../lib/src/resident_runner.dart | 37 +- 13 files changed, 1403 insertions(+), 905 deletions(-) create mode 100644 dwds/test/build_daemon_hot_restart_correctness_test.dart create mode 100644 dwds/test/build_daemon_hot_restart_test.dart create mode 100644 dwds/test/common/hot_restart_common.dart create mode 100644 dwds/test/common/hot_restart_correctness_common.dart create mode 100644 dwds/test/frontend_server_hot_restart_correctness_test.dart create mode 100644 dwds/test/frontend_server_hot_restart_test.dart delete mode 100644 dwds/test/reload_correctness_test.dart delete mode 100644 dwds/test/reload_test.dart diff --git a/dwds/test/build_daemon_hot_restart_correctness_test.dart b/dwds/test/build_daemon_hot_restart_correctness_test.dart new file mode 100644 index 000000000..66042b6b6 --- /dev/null +++ b/dwds/test/build_daemon_hot_restart_correctness_test.dart @@ -0,0 +1,37 @@ +// 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. + +@TestOn('vm') +@Tags(['daily']) +@Timeout(Duration(minutes: 2)) +library; + +import 'package:dwds/expression_compiler.dart'; +import 'package:test/test.dart'; +import 'package:test_common/test_sdk_configuration.dart'; + +import 'common/hot_restart_correctness_common.dart'; +import 'fixtures/context.dart'; + +void main() { + // Enable verbose logging for debugging. + final debug = false; + final canaryFeatures = false; + final moduleFormat = ModuleFormat.amd; + final compilationMode = CompilationMode.buildDaemon; + + final provider = TestSdkConfigurationProvider( + verbose: debug, + canaryFeatures: canaryFeatures, + ddcModuleFormat: moduleFormat, + ); + + runTests( + provider: provider, + moduleFormat: moduleFormat, + compilationMode: compilationMode, + canaryFeatures: canaryFeatures, + debug: debug, + ); +} diff --git a/dwds/test/build_daemon_hot_restart_test.dart b/dwds/test/build_daemon_hot_restart_test.dart new file mode 100644 index 000000000..12600703a --- /dev/null +++ b/dwds/test/build_daemon_hot_restart_test.dart @@ -0,0 +1,37 @@ +// 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. + +@TestOn('vm') +@Tags(['daily']) +@Timeout(Duration(minutes: 2)) +library; + +import 'package:dwds/expression_compiler.dart'; +import 'package:test/test.dart'; +import 'package:test_common/test_sdk_configuration.dart'; + +import 'common/hot_restart_common.dart'; +import 'fixtures/context.dart'; + +void main() { + // Enable verbose logging for debugging. + final debug = false; + final canaryFeatures = false; + final moduleFormat = ModuleFormat.amd; + final compilationMode = CompilationMode.buildDaemon; + + final provider = TestSdkConfigurationProvider( + verbose: debug, + canaryFeatures: canaryFeatures, + ddcModuleFormat: moduleFormat, + ); + + runTests( + provider: provider, + moduleFormat: moduleFormat, + compilationMode: compilationMode, + canaryFeatures: canaryFeatures, + debug: debug, + ); +} diff --git a/dwds/test/common/hot_restart_common.dart b/dwds/test/common/hot_restart_common.dart new file mode 100644 index 000000000..deeb791b8 --- /dev/null +++ b/dwds/test/common/hot_restart_common.dart @@ -0,0 +1,647 @@ +// 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/dwds.dart'; +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 runTests({ + required TestSdkConfigurationProvider provider, + required ModuleFormat moduleFormat, + required CompilationMode compilationMode, + required bool canaryFeatures, + required bool debug, +}) { + final context = TestContext(TestProject.testAppendBody, provider); + + tearDownAll(provider.dispose); + + Future makeEditAndRecompile() async { + context.makeEditToDartEntryFile( + toReplace: originalString, + replaceWith: newString, + ); + if (compilationMode == CompilationMode.frontendServer) { + await context.recompile(fullRestart: true); + } else { + assert(compilationMode == CompilationMode.buildDaemon); + await context.waitForSuccessfulBuild(propagateToBrowser: true); + } + } + + void undoEdit() { + context.makeEditToDartEntryFile( + toReplace: newString, + replaceWith: originalString, + ); + } + + group( + 'Injected client with live reload', + () { + group('and with debugging', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.liveReload, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + ); + }); + + tearDown(() async { + undoEdit(); + await context.tearDown(); + }); + + test('can live reload changes ', () async { + await makeEditAndRecompile(); + final source = await context.webDriver.pageSource; + + // A full reload should clear the state. + expect(source.contains(originalString), isFalse); + expect(source.contains(newString), isTrue); + }); + }); + + group('and without debugging', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.liveReload, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + debugSettings: TestDebugSettings.noDevTools().copyWith( + enableDebugging: false, + ), + ); + }); + + tearDown(() async { + undoEdit(); + await context.tearDown(); + }); + + test('can live reload changes ', () async { + await makeEditAndRecompile(); + + final source = await context.webDriver.pageSource; + + // A full reload should clear the state. + expect(source.contains(originalString), isFalse); + expect(source.contains(newString), isTrue); + }); + }); + + group('and without debugging using WebSockets', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.liveReload, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + debugSettings: TestDebugSettings.noDevTools().copyWith( + enableDebugging: false, + useSse: false, + ), + ); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test('can live reload changes ', () async { + await makeEditAndRecompile(); + + final source = await context.webDriver.pageSource; + + // A full reload should clear the state. + expect(source.contains(originalString), isFalse); + expect(source.contains(newString), isTrue); + }); + }); + }, + // `BuildResult`s are only ever emitted when using the build daemon. + skip: compilationMode != CompilationMode.buildDaemon, + timeout: Timeout.factor(2), + ); + + group('Injected client', () { + late VmService fakeClient; + + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + enableExpressionEvaluation: true, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + ); + fakeClient = await context.connectFakeClient(); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test('destroys and recreates the isolate during a hot restart', () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + await makeEditAndRecompile(); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); + expect( + await fakeClient.callServiceExtension(hotRestart!), + const TypeMatcher(), + ); + + await eventsDone; + }); + + test('can execute simultaneous hot restarts', () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + await makeEditAndRecompile(); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + // Execute two hot restart calls in parallel. + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); + final done = Future.wait([ + fakeClient.callServiceExtension(hotRestart!), + fakeClient.callServiceExtension(hotRestart), + ]); + expect(await done, [ + const TypeMatcher(), + const TypeMatcher(), + ]); + + // The debugger is still working. + final vm = await client.getVM(); + final isolateId = vm.isolates!.first.id!; + final isolate = await client.getIsolate(isolateId); + final library = isolate.rootLib!.uri!; + + final result = await client.evaluate(isolateId, library, 'true'); + expect( + result, + isA().having( + (instance) => instance.valueAsString, + 'valueAsString', + 'true', + ), + ); + + await eventsDone; + }); + + test('destroys and recreates the isolate during a page refresh', () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + await makeEditAndRecompile(); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + await context.webDriver.driver.refresh(); + + await eventsDone; + }); + + test('can hot restart via the service extension', () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + await makeEditAndRecompile(); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); + expect( + await fakeClient.callServiceExtension(hotRestart!), + const TypeMatcher(), + ); + + await eventsDone; + + final source = await context.webDriver.pageSource; + // Main is re-invoked which shouldn't clear the state. + expect(source, contains(originalString)); + expect(source, contains(newString)); + }); + + test('can send events before and after hot restart', () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + + // The event just before hot restart might never be received, + // but the injected client continues to work and send events + // after hot restart. + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + _hasKind( + EventKind.kServiceExtensionAdded, + ).having((e) => e.extensionRPC, 'service', 'ext.bar'), + ), + ); + + var vm = await client.getVM(); + var isolateId = vm.isolates!.first.id!; + var isolate = await client.getIsolate(isolateId); + var library = isolate.rootLib!.uri!; + + final callback = '(_, __) async => ServiceExtensionResponse.result("")'; + + await client.evaluate( + isolateId, + library, + "registerExtension('ext.foo', $callback)", + ); + + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); + expect( + await fakeClient.callServiceExtension(hotRestart!), + const TypeMatcher(), + ); + + vm = await client.getVM(); + isolateId = vm.isolates!.first.id!; + isolate = await client.getIsolate(isolateId); + library = isolate.rootLib!.uri!; + + await client.evaluate( + isolateId, + library, + "registerExtension('ext.bar', $callback)", + ); + + await eventsDone; + + final source = await context.webDriver.pageSource; + // Main is re-invoked which shouldn't clear the state. + expect(source, contains('Hello World!')); + }); + + test('can refresh the page via the fullReload service extension', () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + await makeEditAndRecompile(); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + final fullReload = context.getRegisteredServiceExtension('fullReload'); + expect( + await fakeClient.callServiceExtension(fullReload!), + isA(), + ); + + await eventsDone; + + final source = await context.webDriver.pageSource; + // Should see only the new text + expect(source.contains(originalString), isFalse); + expect(source.contains(newString), isTrue); + }); + + test('can hot restart while paused', () async { + final client = context.debugConnection.vmService; + var vm = await client.getVM(); + var isolateId = vm.isolates!.first.id!; + await client.streamListen('Debug'); + final stream = client.onEvent('Debug'); + final scriptList = await client.getScripts(isolateId); + final main = scriptList.scripts!.firstWhere( + (script) => script.uri!.contains('main.dart'), + ); + final bpLine = await context.findBreakpointLine( + 'printCount', + isolateId, + main, + ); + await client.addBreakpoint(isolateId, main.id!, bpLine); + await stream.firstWhere( + (event) => event.kind == EventKind.kPauseBreakpoint, + ); + + await makeEditAndRecompile(); + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); + await fakeClient.callServiceExtension(hotRestart!); + final source = await context.webDriver.pageSource; + + // Main is re-invoked which shouldn't clear the state. + expect(source.contains(originalString), isTrue); + expect(source.contains(newString), isTrue); + + vm = await client.getVM(); + isolateId = vm.isolates!.first.id!; + final isolate = await client.getIsolate(isolateId); + + // Previous breakpoint should be cleared. + expect(isolate.breakpoints!.isEmpty, isTrue); + }); + + test('can evaluate expressions after hot restart', () async { + final client = context.debugConnection.vmService; + + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); + await fakeClient.callServiceExtension(hotRestart!); + + final vm = await client.getVM(); + final isolateId = vm.isolates!.first.id!; + final isolate = await client.getIsolate(isolateId); + final library = isolate.rootLib!.uri!; + + // Expression evaluation while running should work. + final result = await client.evaluate(isolateId, library, 'true'); + expect( + result, + isA().having( + (instance) => instance.valueAsString, + 'valueAsString', + 'true', + ), + ); + }); + }, timeout: Timeout.factor(2)); + + group( + 'Injected client with hot restart', + () { + group('and with debugging', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.hotRestart, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + ); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test('can hot restart changes ', () async { + await makeEditAndRecompile(); + + final source = await context.webDriver.pageSource; + + // Main is re-invoked which shouldn't clear the state. + expect(source.contains(originalString), isTrue); + expect(source.contains(newString), isTrue); + // The ext.flutter.disassemble callback is invoked and waited for. + expect( + source, + contains('start disassemble end disassemble $newString'), + ); + }); + + test( + 'fires isolate create/destroy events during hot restart', + () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + await makeEditAndRecompile(); + + await eventsDone; + }, + ); + }); + + group('and without debugging', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.hotRestart, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + debugSettings: TestDebugSettings.noDevTools().copyWith( + enableDebugging: false, + ), + ); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test('can hot restart changes ', () async { + await makeEditAndRecompile(); + + final source = await context.webDriver.pageSource; + + // Main is re-invoked which shouldn't clear the state. + expect(source.contains(originalString), isTrue); + expect(source.contains(newString), isTrue); + // The ext.flutter.disassemble callback is invoked and waited for. + expect( + source, + contains('start disassemble end disassemble $newString'), + ); + }); + }); + }, + // `BuildResult`s are only ever emitted when using the build daemon. + skip: compilationMode != CompilationMode.buildDaemon, + timeout: Timeout.factor(2), + ); + + group( + 'when isolates_paused_on_start is true', + () { + late VmService client; + late VmService fakeClient; + + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + enableExpressionEvaluation: true, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + ); + client = context.debugConnection.vmService; + fakeClient = await context.connectFakeClient(); + await client.setFlag('pause_isolates_on_start', 'true'); + await client.streamListen('Isolate'); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test( + 'after hot-restart, does not run app until there is a resume event', + () async { + await makeEditAndRecompile(); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + final hotRestart = context.getRegisteredServiceExtension( + 'hotRestart', + ); + expect( + await fakeClient.callServiceExtension(hotRestart!), + const TypeMatcher(), + ); + + await eventsDone; + + final sourceBeforeResume = await context.webDriver.pageSource; + expect(sourceBeforeResume.contains(newString), isFalse); + + final vm = await client.getVM(); + final isolateId = vm.isolates!.first.id!; + await client.resume(isolateId); + + final sourceAfterResume = await context.webDriver.pageSource; + expect(sourceAfterResume.contains(newString), isTrue); + }, + ); + + test( + 'after page refresh, does not run app until there is a resume event', + () async { + await makeEditAndRecompile(); + + await context.webDriver.driver.refresh(); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + await eventsDone; + + final sourceBeforeResume = await context.webDriver.pageSource; + expect(sourceBeforeResume.contains(newString), isFalse); + + final vm = await client.getVM(); + final isolateId = vm.isolates!.first.id!; + await client.resume(isolateId); + + final sourceAfterResume = await context.webDriver.pageSource; + expect(sourceAfterResume.contains(newString), isTrue); + }, + ); + }, + // https://github.com/dart-lang/sdk/issues/60528 + skip: moduleFormat == ModuleFormat.ddc && canaryFeatures == true, + ); +} + +TypeMatcher _hasKind(String kind) => + isA().having((e) => e.kind, 'kind', kind); diff --git a/dwds/test/common/hot_restart_correctness_common.dart b/dwds/test/common/hot_restart_correctness_common.dart new file mode 100644 index 000000000..5cdda1000 --- /dev/null +++ b/dwds/test/common/hot_restart_correctness_common.dart @@ -0,0 +1,225 @@ +// 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/dwds.dart'; +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:test_common/utilities.dart'; +import 'package:vm_service/vm_service.dart'; + +import '../fixtures/context.dart'; +import '../fixtures/project.dart'; +import '../fixtures/utilities.dart'; + +const originalString = 'variableToModifyToForceRecompile = 23'; +const newString = 'variableToModifyToForceRecompile = 45'; + +const constantSuccessString = 'ConstantEqualitySuccess'; +const constantFailureString = 'ConstantEqualityFailure'; + +void runTests({ + required TestSdkConfigurationProvider provider, + required ModuleFormat moduleFormat, + required CompilationMode compilationMode, + required bool canaryFeatures, + required bool debug, +}) { + tearDownAll(provider.dispose); + + final testHotRestart2 = TestProject.testHotRestart2; + final context = TestContext(testHotRestart2, provider); + + Future makeEditAndRecompile() async { + context.makeEditToDartLibFile( + libFileName: 'library2.dart', + toReplace: originalString, + replaceWith: newString, + ); + if (compilationMode == CompilationMode.frontendServer) { + await context.recompile(fullRestart: true); + } else { + assert(compilationMode == CompilationMode.buildDaemon); + await context.waitForSuccessfulBuild(propagateToBrowser: true); + } + } + + void undoEdit() { + context.makeEditToDartLibFile( + libFileName: 'library2.dart', + toReplace: newString, + replaceWith: originalString, + ); + } + + group('Injected client', () { + VmService? fakeClient; + + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + enableExpressionEvaluation: true, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + ); + + fakeClient = await context.connectFakeClient(); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test( + 'properly compares constants after hot restart via the service extension', + () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + + var source = await context.webDriver.pageSource; + expect( + source, + contains('ConstObject(reloadVariable: 23, ConstantEqualitySuccess)'), + ); + + await makeEditAndRecompile(); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); + expect( + await fakeClient!.callServiceExtension(hotRestart!), + const TypeMatcher(), + ); + + await eventsDone; + + source = await context.webDriver.pageSource; + if (dartSdkIsAtLeast('3.4.0-61.0.dev')) { + expect( + source, + contains( + 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', + ), + ); + } + }, + ); + }, timeout: Timeout.factor(2)); + + group( + 'Injected client with hot restart', + () { + group('and with debugging', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.hotRestart, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + ); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test('properly compares constants after hot restart', () async { + var source = await context.webDriver.pageSource; + expect( + source, + contains( + 'ConstObject(reloadVariable: 23, ConstantEqualitySuccess)', + ), + ); + + await makeEditAndRecompile(); + + source = await context.webDriver.pageSource; + if (dartSdkIsAtLeast('3.4.0-61.0.dev')) { + expect( + source, + contains( + 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', + ), + ); + } + }); + }); + + group('and without debugging', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.hotRestart, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + debugSettings: TestDebugSettings.noDevTools().copyWith( + enableDebugging: false, + ), + ); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test('properly compares constants after hot restart', () async { + var source = await context.webDriver.pageSource; + expect( + source, + contains( + 'ConstObject(reloadVariable: 23, ConstantEqualitySuccess)', + ), + ); + + await makeEditAndRecompile(); + + source = await context.webDriver.pageSource; + if (dartSdkIsAtLeast('3.4.0-61.0.dev')) { + expect( + source, + contains( + 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', + ), + ); + } + }); + }); + }, + // `BuildResult`s are only ever emitted when using the build daemon. + skip: compilationMode != CompilationMode.buildDaemon, + timeout: Timeout.factor(2), + ); +} + +TypeMatcher _hasKind(String kind) => + isA().having((e) => e.kind, 'kind', kind); diff --git a/dwds/test/fixtures/context.dart b/dwds/test/fixtures/context.dart index 220301050..273f76ffd 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'; @@ -115,6 +116,10 @@ class TestContext { final _serviceNameToMethod = {}; + late LocalFileSystem frontendServerFileSystem; + + late String _hostname; + /// Internal VM service. /// /// Prefer using [vmService] instead in tests when possible, to include testing @@ -305,9 +310,9 @@ class TestContext { final entry = p.toUri( p.join(project.webAssetsPath, project.dartEntryFileName), ); - final fileSystem = LocalFileSystem(); + frontendServerFileSystem = LocalFileSystem(); final packageUriMapper = await PackageUriMapper.create( - fileSystem, + frontendServerFileSystem, project.packageConfigFile, useDebuggerModuleNames: testSettings.useDebuggerModuleNames, ); @@ -333,20 +338,23 @@ class TestContext { ); final assetServerPort = await findUnusedPort(); + _hostname = appMetadata.hostname; await webRunner.run( - fileSystem, - appMetadata.hostname, + frontendServerFileSystem, + _hostname, assetServerPort, filePathToServe, + initialCompile: true, + fullRestart: false, ); if (testSettings.enableExpressionEvaluation) { expressionCompiler = webRunner.expressionCompiler; } - basePath = webRunner.devFS.assetServer.basePath; - assetReader = webRunner.devFS.assetServer; - _assetHandler = webRunner.devFS.assetServer.handleRequest; + basePath = webRunner.devFS!.assetServer.basePath; + assetReader = webRunner.devFS!.assetServer; + _assetHandler = webRunner.devFS!.assetServer.handleRequest; loadStrategy = switch (testSettings.moduleFormat) { ModuleFormat.amd => FrontendServerRequireStrategyProvider( @@ -572,6 +580,18 @@ class TestContext { file.writeAsStringSync(fileContents.replaceAll(toReplace, replaceWith)); } + Future recompile({required bool fullRestart}) async { + await webRunner.run( + frontendServerFileSystem, + _hostname, + await findUnusedPort(), + webCompatiblePath([project.directoryToServe, project.filePathToServe]), + initialCompile: false, + fullRestart: fullRestart, + ); + return; + } + Future waitForSuccessfulBuild({ Duration? timeout, bool propagateToBrowser = false, diff --git a/dwds/test/frontend_server_hot_restart_correctness_test.dart b/dwds/test/frontend_server_hot_restart_correctness_test.dart new file mode 100644 index 000000000..198e6546c --- /dev/null +++ b/dwds/test/frontend_server_hot_restart_correctness_test.dart @@ -0,0 +1,37 @@ +// 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. + +@TestOn('vm') +@Tags(['daily']) +@Timeout(Duration(minutes: 2)) +library; + +import 'package:dwds/expression_compiler.dart'; +import 'package:test/test.dart'; +import 'package:test_common/test_sdk_configuration.dart'; + +import 'common/hot_restart_correctness_common.dart'; +import 'fixtures/context.dart'; + +void main() { + // Enable verbose logging for debugging. + final debug = false; + final canaryFeatures = true; + final moduleFormat = ModuleFormat.ddc; + final compilationMode = CompilationMode.frontendServer; + + final provider = TestSdkConfigurationProvider( + verbose: debug, + canaryFeatures: canaryFeatures, + ddcModuleFormat: moduleFormat, + ); + + runTests( + provider: provider, + moduleFormat: moduleFormat, + compilationMode: compilationMode, + canaryFeatures: canaryFeatures, + debug: debug, + ); +} diff --git a/dwds/test/frontend_server_hot_restart_test.dart b/dwds/test/frontend_server_hot_restart_test.dart new file mode 100644 index 000000000..95d98bd28 --- /dev/null +++ b/dwds/test/frontend_server_hot_restart_test.dart @@ -0,0 +1,39 @@ +// 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. + +@TestOn('vm') +@Tags(['daily']) +@Timeout(Duration(minutes: 2)) +library; + +import 'package:dwds/expression_compiler.dart'; +import 'package:test/test.dart'; +import 'package:test_common/test_sdk_configuration.dart'; + +import 'common/hot_restart_common.dart'; +import 'fixtures/context.dart'; + +void main() { + // Enable verbose logging for debugging. + final debug = false; + final canaryFeatures = true; + final moduleFormat = ModuleFormat.ddc; + final compilationMode = CompilationMode.frontendServer; + + group('canary: $canaryFeatures |', () { + final provider = TestSdkConfigurationProvider( + verbose: debug, + canaryFeatures: canaryFeatures, + ddcModuleFormat: moduleFormat, + ); + + runTests( + provider: provider, + moduleFormat: moduleFormat, + compilationMode: compilationMode, + canaryFeatures: canaryFeatures, + debug: debug, + ); + }); +} diff --git a/dwds/test/reload_correctness_test.dart b/dwds/test/reload_correctness_test.dart deleted file mode 100644 index ffffc6a6f..000000000 --- a/dwds/test/reload_correctness_test.dart +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright (c) 2024, 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/dwds.dart'; -import 'package:test/test.dart'; -import 'package:test_common/logging.dart'; -import 'package:test_common/test_sdk_configuration.dart'; -import 'package:test_common/utilities.dart'; -import 'package:vm_service/vm_service.dart'; - -import 'fixtures/context.dart'; -import 'fixtures/project.dart'; -import 'fixtures/utilities.dart'; - -const originalString = 'variableToModifyToForceRecompile = 23'; -const newString = 'variableToModifyToForceRecompile = 45'; - -const constantSuccessString = 'ConstantEqualitySuccess'; -const constantFailureString = 'ConstantEqualityFailure'; - -void main() { - // set to true for debug logging. - final debug = false; - - final provider = TestSdkConfigurationProvider(verbose: debug); - tearDownAll(provider.dispose); - - final testHotRestart2 = TestProject.testHotRestart2; - final context = TestContext(testHotRestart2, provider); - - Future makeEditAndWaitForRebuild() async { - context.makeEditToDartLibFile( - libFileName: 'library2.dart', - toReplace: originalString, - replaceWith: newString, - ); - await context.waitForSuccessfulBuild(propagateToBrowser: true); - } - - void undoEdit() { - context.makeEditToDartLibFile( - libFileName: 'library2.dart', - toReplace: newString, - replaceWith: originalString, - ); - } - - group('Injected client', () { - VmService? fakeClient; - - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings(enableExpressionEvaluation: true), - ); - - fakeClient = await context.connectFakeClient(); - }); - - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); - - test( - 'properly compares constants after hot restart via the service extension', - () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - - var source = await context.webDriver.pageSource; - expect( - source, - contains('ConstObject(reloadVariable: 23, ConstantEqualitySuccess)'), - ); - - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - final hotRestart = context.getRegisteredServiceExtension('hotRestart'); - expect( - await fakeClient!.callServiceExtension(hotRestart!), - const TypeMatcher(), - ); - - await eventsDone; - - source = await context.webDriver.pageSource; - if (dartSdkIsAtLeast('3.4.0-61.0.dev')) { - expect( - source, - contains( - 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', - ), - ); - } - }, - ); - }, timeout: Timeout.factor(2)); - - group('Injected client with hot restart', () { - group('and with debugging', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.hotRestart, - ), - ); - }); - - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); - - test('properly compares constants after hot restart', () async { - var source = await context.webDriver.pageSource; - expect( - source, - contains('ConstObject(reloadVariable: 23, ConstantEqualitySuccess)'), - ); - - await makeEditAndWaitForRebuild(); - - source = await context.webDriver.pageSource; - if (dartSdkIsAtLeast('3.4.0-61.0.dev')) { - expect( - source, - contains( - 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', - ), - ); - } - }); - }); - - group('and without debugging', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.hotRestart, - ), - debugSettings: TestDebugSettings.noDevTools().copyWith( - enableDebugging: false, - ), - ); - }); - - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); - - test('properly compares constants after hot restart', () async { - var source = await context.webDriver.pageSource; - expect( - source, - contains('ConstObject(reloadVariable: 23, ConstantEqualitySuccess)'), - ); - - await makeEditAndWaitForRebuild(); - - source = await context.webDriver.pageSource; - if (dartSdkIsAtLeast('3.4.0-61.0.dev')) { - expect( - source, - contains( - 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', - ), - ); - } - }); - }); - }, timeout: Timeout.factor(2)); -} - -TypeMatcher _hasKind(String kind) => - isA().having((e) => e.kind, 'kind', kind); diff --git a/dwds/test/reload_test.dart b/dwds/test/reload_test.dart deleted file mode 100644 index b0451104a..000000000 --- a/dwds/test/reload_test.dart +++ /dev/null @@ -1,594 +0,0 @@ -// Copyright (c) 2019, 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/dwds.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() { - // set to true for debug logging. - final debug = false; - - final provider = TestSdkConfigurationProvider(verbose: debug); - tearDownAll(provider.dispose); - - final context = TestContext(TestProject.testAppendBody, provider); - - Future makeEditAndWaitForRebuild() async { - context.makeEditToDartEntryFile( - toReplace: originalString, - replaceWith: newString, - ); - await context.waitForSuccessfulBuild(propagateToBrowser: true); - } - - void undoEdit() { - context.makeEditToDartEntryFile( - toReplace: newString, - replaceWith: originalString, - ); - } - - group('Injected client with live reload', () { - group('and with debugging', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.liveReload, - ), - ); - }); - - tearDown(() async { - undoEdit(); - await context.tearDown(); - }); - - test('can live reload changes ', () async { - await makeEditAndWaitForRebuild(); - final source = await context.webDriver.pageSource; - - // A full reload should clear the state. - expect(source.contains(originalString), isFalse); - expect(source.contains(newString), isTrue); - }); - }); - - group('and without debugging', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.liveReload, - ), - debugSettings: TestDebugSettings.noDevTools().copyWith( - enableDebugging: false, - ), - ); - }); - - tearDown(() async { - undoEdit(); - await context.tearDown(); - }); - - test('can live reload changes ', () async { - await makeEditAndWaitForRebuild(); - - final source = await context.webDriver.pageSource; - - // A full reload should clear the state. - expect(source.contains(originalString), isFalse); - expect(source.contains(newString), isTrue); - }); - }); - - group('and without debugging using WebSockets', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.liveReload, - ), - debugSettings: TestDebugSettings.noDevTools().copyWith( - enableDebugging: false, - useSse: false, - ), - ); - }); - - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); - - test('can live reload changes ', () async { - await makeEditAndWaitForRebuild(); - - final source = await context.webDriver.pageSource; - - // A full reload should clear the state. - expect(source.contains(originalString), isFalse); - expect(source.contains(newString), isTrue); - }); - }); - }, timeout: Timeout.factor(2)); - - group('Injected client', () { - late VmService fakeClient; - - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings(enableExpressionEvaluation: true), - ); - fakeClient = await context.connectFakeClient(); - }); - - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); - - test('destroys and recreates the isolate during a hot restart', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - final hotRestart = context.getRegisteredServiceExtension('hotRestart'); - expect( - await fakeClient.callServiceExtension(hotRestart!), - const TypeMatcher(), - ); - - await eventsDone; - }); - - test('can execute simultaneous hot restarts', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - // Execute two hot restart calls in parallel. - final hotRestart = context.getRegisteredServiceExtension('hotRestart'); - final done = Future.wait([ - fakeClient.callServiceExtension(hotRestart!), - fakeClient.callServiceExtension(hotRestart), - ]); - expect(await done, [ - const TypeMatcher(), - const TypeMatcher(), - ]); - - // The debugger is still working. - final vm = await client.getVM(); - final isolateId = vm.isolates!.first.id!; - final isolate = await client.getIsolate(isolateId); - final library = isolate.rootLib!.uri!; - - final result = await client.evaluate(isolateId, library, 'true'); - expect( - result, - isA().having( - (instance) => instance.valueAsString, - 'valueAsString', - 'true', - ), - ); - - await eventsDone; - }); - - test('destroys and recreates the isolate during a page refresh', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - await context.webDriver.driver.refresh(); - - await eventsDone; - }); - - test('can hot restart via the service extension', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - final hotRestart = context.getRegisteredServiceExtension('hotRestart'); - expect( - await fakeClient.callServiceExtension(hotRestart!), - const TypeMatcher(), - ); - - await eventsDone; - - final source = await context.webDriver.pageSource; - // Main is re-invoked which shouldn't clear the state. - expect(source, contains(originalString)); - expect(source, contains(newString)); - }); - - test('can send events before and after hot restart', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - - // The event just before hot restart might never be received, - // but the injected client continues to work and send events - // after hot restart. - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - _hasKind( - EventKind.kServiceExtensionAdded, - ).having((e) => e.extensionRPC, 'service', 'ext.bar'), - ), - ); - - var vm = await client.getVM(); - var isolateId = vm.isolates!.first.id!; - var isolate = await client.getIsolate(isolateId); - var library = isolate.rootLib!.uri!; - - final callback = '(_, __) async => ServiceExtensionResponse.result("")'; - - await client.evaluate( - isolateId, - library, - "registerExtension('ext.foo', $callback)", - ); - - final hotRestart = context.getRegisteredServiceExtension('hotRestart'); - expect( - await fakeClient.callServiceExtension(hotRestart!), - const TypeMatcher(), - ); - - vm = await client.getVM(); - isolateId = vm.isolates!.first.id!; - isolate = await client.getIsolate(isolateId); - library = isolate.rootLib!.uri!; - - await client.evaluate( - isolateId, - library, - "registerExtension('ext.bar', $callback)", - ); - - await eventsDone; - - final source = await context.webDriver.pageSource; - // Main is re-invoked which shouldn't clear the state. - expect(source, contains('Hello World!')); - }); - - test('can refresh the page via the fullReload service extension', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - final fullReload = context.getRegisteredServiceExtension('fullReload'); - expect( - await fakeClient.callServiceExtension(fullReload!), - isA(), - ); - - await eventsDone; - - final source = await context.webDriver.pageSource; - // Should see only the new text - expect(source.contains(originalString), isFalse); - expect(source.contains(newString), isTrue); - }); - - test('can hot restart while paused', () async { - final client = context.debugConnection.vmService; - var vm = await client.getVM(); - var isolateId = vm.isolates!.first.id!; - await client.streamListen('Debug'); - final stream = client.onEvent('Debug'); - final scriptList = await client.getScripts(isolateId); - final main = scriptList.scripts!.firstWhere( - (script) => script.uri!.contains('main.dart'), - ); - final bpLine = await context.findBreakpointLine( - 'printCount', - isolateId, - main, - ); - await client.addBreakpoint(isolateId, main.id!, bpLine); - await stream.firstWhere( - (event) => event.kind == EventKind.kPauseBreakpoint, - ); - - await makeEditAndWaitForRebuild(); - final hotRestart = context.getRegisteredServiceExtension('hotRestart'); - await fakeClient.callServiceExtension(hotRestart!); - final source = await context.webDriver.pageSource; - - // Main is re-invoked which shouldn't clear the state. - expect(source.contains(originalString), isTrue); - expect(source.contains(newString), isTrue); - - vm = await client.getVM(); - isolateId = vm.isolates!.first.id!; - final isolate = await client.getIsolate(isolateId); - - // Previous breakpoint should be cleared. - expect(isolate.breakpoints!.isEmpty, isTrue); - }); - - test('can evaluate expressions after hot restart', () async { - final client = context.debugConnection.vmService; - - final hotRestart = context.getRegisteredServiceExtension('hotRestart'); - await fakeClient.callServiceExtension(hotRestart!); - - final vm = await client.getVM(); - final isolateId = vm.isolates!.first.id!; - final isolate = await client.getIsolate(isolateId); - final library = isolate.rootLib!.uri!; - - // Expression evaluation while running should work. - final result = await client.evaluate(isolateId, library, 'true'); - expect( - result, - isA().having( - (instance) => instance.valueAsString, - 'valueAsString', - 'true', - ), - ); - }); - }, timeout: Timeout.factor(2)); - - group('Injected client with hot restart', () { - group('and with debugging', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.hotRestart, - ), - ); - }); - - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); - - test('can hot restart changes ', () async { - await makeEditAndWaitForRebuild(); - - final source = await context.webDriver.pageSource; - - // Main is re-invoked which shouldn't clear the state. - expect(source.contains(originalString), isTrue); - expect(source.contains(newString), isTrue); - // The ext.flutter.disassemble callback is invoked and waited for. - expect( - source, - contains('start disassemble end disassemble $newString'), - ); - }); - - test('fires isolate create/destroy events during hot restart', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - await makeEditAndWaitForRebuild(); - - await eventsDone; - }); - }); - - group('and without debugging', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.hotRestart, - ), - debugSettings: TestDebugSettings.noDevTools().copyWith( - enableDebugging: false, - ), - ); - }); - - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); - - test('can hot restart changes ', () async { - await makeEditAndWaitForRebuild(); - - final source = await context.webDriver.pageSource; - - // Main is re-invoked which shouldn't clear the state. - expect(source.contains(originalString), isTrue); - expect(source.contains(newString), isTrue); - // The ext.flutter.disassemble callback is invoked and waited for. - expect( - source, - contains('start disassemble end disassemble $newString'), - ); - }); - }); - }, timeout: Timeout.factor(2)); - - // TODO(https://github.com/dart-lang/webdev/issues/2380): Run these tests with - // the FrontendServer as well. - group('when isolates_paused_on_start is true', () { - late VmService client; - late VmService fakeClient; - - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings(enableExpressionEvaluation: true), - ); - client = context.debugConnection.vmService; - fakeClient = await context.connectFakeClient(); - await client.setFlag('pause_isolates_on_start', 'true'); - await client.streamListen('Isolate'); - }); - - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); - - test( - 'after hot-restart, does not run app until there is a resume event', - () async { - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - final hotRestart = context.getRegisteredServiceExtension('hotRestart'); - expect( - await fakeClient.callServiceExtension(hotRestart!), - const TypeMatcher(), - ); - - await eventsDone; - - final sourceBeforeResume = await context.webDriver.pageSource; - expect(sourceBeforeResume.contains(newString), isFalse); - - final vm = await client.getVM(); - final isolateId = vm.isolates!.first.id!; - await client.resume(isolateId); - - final sourceAfterResume = await context.webDriver.pageSource; - expect(sourceAfterResume.contains(newString), isTrue); - }, - ); - - test( - 'after page refresh, does not run app until there is a resume event', - () async { - await makeEditAndWaitForRebuild(); - - await context.webDriver.driver.refresh(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - await eventsDone; - - final sourceBeforeResume = await context.webDriver.pageSource; - expect(sourceBeforeResume.contains(newString), isFalse); - - final vm = await client.getVM(); - final isolateId = vm.isolates!.first.id!; - await client.resume(isolateId); - - final sourceAfterResume = await context.webDriver.pageSource; - expect(sourceAfterResume.contains(newString), isTrue); - }, - ); - }); -} - -TypeMatcher _hasKind(String kind) => - isA().having((e) => e.kind, 'kind', kind); diff --git a/frontend_server_common/lib/src/bootstrap.dart b/frontend_server_common/lib/src/bootstrap.dart index faf2fa420..b45ff45c2 100644 --- a/frontend_server_common/lib/src/bootstrap.dart +++ b/frontend_server_common/lib/src/bootstrap.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io' show Platform; + // Note: this is a copy from flutter tools, updated to work with dwds tests /// JavaScript snippet to determine the base URL of the current path. @@ -393,6 +395,8 @@ $_baseUrlScript $_simpleLoaderScript (function() { + let appName = "org-dartlang-app:/$entrypoint"; + // Load pre-requisite DDC scripts. We intentionally use invalid names to avoid // namespace clashes. let prerequisiteScripts = [ @@ -413,26 +417,45 @@ $_simpleLoaderScript } Promise.all(prerequisiteLoads).then((_) => afterPrerequisiteLogic()); + // Save the current script so we can access it in a closure. + var _currentScript = document.currentScript; + + // Create a policy if needed to load the files during a hot restart. + let policy = { + createScriptURL: function(src) {return src;} + }; + if (self.trustedTypes && self.trustedTypes.createPolicy) { + policy = self.trustedTypes.createPolicy('dartDdcModuleUrl', policy); + } + var afterPrerequisiteLogic = function() { window.\$dartLoader.rootDirectories.push(_currentDirectory); let scripts = [ { "src": "dart_sdk.js", - "id": "dart_sdk \\0" + "id": "dart_sdk" }, + { + "src": "$bootstrapUrl", + "id": "data-main" + } ]; let loadConfig = new window.\$dartLoader.LoadConfiguration(); loadConfig.root = _currentDirectory; - loadConfig.bootstrapScript = { - "src": "$bootstrapUrl", - "id": "data-main" - }; - scripts.push(loadConfig.bootstrapScript); + + // TODO(srujzs): Verify this is sufficient for Windows. + loadConfig.isWindows = ${Platform.isWindows}; + loadConfig.bootstrapScript = scripts[scripts.length - 1]; + loadConfig.loadScriptFn = function(loader) { loader.addScriptsToQueue(scripts, null); loader.loadEnqueuedModules(); } + loadConfig.ddcEventForLoadStart = /* LOAD_ALL_MODULES_START */ 1; + loadConfig.ddcEventForLoadedOk = /* LOAD_ALL_MODULES_END_OK */ 2; + loadConfig.ddcEventForLoadedError = /* LOAD_ALL_MODULES_END_ERROR */ 3; + let loader = new window.\$dartLoader.DDCLoader(loadConfig); // Record prerequisite scripts' fully resolved URLs. @@ -443,30 +466,118 @@ $_simpleLoaderScript window.\$dartLoader.loadConfig = loadConfig; window.\$dartLoader.loader = loader; - // TODO(srujzs): Support hot restart. - // Begin loading libraries loader.nextAttempt(); - } + + // Set up stack trace mapper. + if (window.\$dartStackTraceUtility && + !window.\$dartStackTraceUtility.ready) { + window.\$dartStackTraceUtility.ready = true; + window.\$dartStackTraceUtility.setSourceMapProvider(function(url) { + var baseUrl = window.location.protocol + '//' + window.location.host; + url = url.replace(baseUrl + '/', ''); + if (url == 'dart_sdk.js') { + return dartDevEmbedder.debugger.getSourceMap('dart_sdk'); + } + url = url.replace(".lib.js", ""); + return dartDevEmbedder.debugger.getSourceMap(url); + }); + } + + let currentUri = _currentScript.src; + // 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 = _currentDirectory + '/restart_scripts.json'; + + if (!window.\$dartReloadModifiedModules) { + window.\$dartReloadModifiedModules = (function(appName, callback) { + var xhttp = new XMLHttpRequest(); + xhttp.withCredentials = true; + xhttp.onreadystatechange = function() { + // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState + if (this.readyState == 4 && this.status == 200 || this.status == 304) { + var scripts = JSON.parse(this.responseText); + var numToLoad = 0; + var numLoaded = 0; + for (var i = 0; i < scripts.length; i++) { + var script = scripts[i]; + if (script.id == null) continue; + var src = _currentDirectory + '/' + script.src.toString(); + var oldSrc = window.\$dartLoader.moduleIdToUrl.get(script.id); + + // We might actually load from a different uri, delete the old one + // just to be sure. + window.\$dartLoader.urlToModuleId.delete(oldSrc); + + window.\$dartLoader.moduleIdToUrl.set(script.id, src); + window.\$dartLoader.urlToModuleId.set(src, script.id); + + numToLoad++; + + var el = document.getElementById(script.id); + if (el) el.remove(); + el = window.\$dartCreateScript(); + el.src = policy.createScriptURL(src); + el.async = false; + el.defer = true; + el.id = script.id; + el.onload = function() { + numLoaded++; + if (numToLoad == numLoaded) callback(); + }; + document.head.appendChild(el); + } + // Call `callback` right away if we found no updated scripts. + if (numToLoad == 0) callback(); + } + }; + xhttp.open("GET", restartScripts, true); + xhttp.send(); + }); + } + }; })(); '''; } -String generateDDCLibraryBundleMainModule({required String entrypoint}) { - return '''/* ENTRYPOINT_EXTENTION_MARKER */ +const String _onLoadEndCallback = r'$onLoadEndCallback'; + +String generateDDCLibraryBundleMainModule({ + required String entrypoint, + required String onLoadEndBootstrap, +}) { + // The typo below in "EXTENTION" is load-bearing, package:build depends on it. + return ''' +/* ENTRYPOINT_EXTENTION_MARKER */ (function() { let appName = "org-dartlang-app:///$entrypoint"; dartDevEmbedder.debugger.registerDevtoolsFormatter(); - let child = {}; - child.main = function() { - dartDevEmbedder.runMain(appName, {}); + // Set up a final script that lets us know when all scripts have been loaded. + // Only then can we call the main method. + let onLoadEndSrc = '$onLoadEndBootstrap'; + window.\$dartLoader.loadConfig.bootstrapScript = { + src: onLoadEndSrc, + id: onLoadEndSrc, + }; + window.\$dartLoader.loadConfig.tryLoadBootstrapScript = true; + // Should be called by $onLoadEndBootstrap once all the scripts have been + // loaded. + window.$_onLoadEndCallback = function() { + let child = {}; + child.main = function() { + dartDevEmbedder.runMain(appName, {}); + } + /* MAIN_EXTENSION_MARKER */ + child.main(); } - - /* MAIN_EXTENSION_MARKER */ - child.main(); })(); '''; } + +String generateDDCLibraryBundleOnLoadEndBootstrap() { + return '''window.$_onLoadEndCallback();'''; +} diff --git a/frontend_server_common/lib/src/devfs.dart b/frontend_server_common/lib/src/devfs.dart index e7d5e67ba..2fb7bb96a 100644 --- a/frontend_server_common/lib/src/devfs.dart +++ b/frontend_server_common/lib/src/devfs.dart @@ -4,6 +4,9 @@ // Note: this is a copy from flutter tools, updated to work with dwds tests +import 'dart:convert'; +import 'dart:io'; + import 'package:dwds/asset_reader.dart'; import 'package:dwds/config.dart'; import 'package:dwds/expression_compiler.dart'; @@ -38,6 +41,8 @@ class WebDevFS { final PackageUriMapper packageUriMapper; final String index; final UrlEncoder? urlTunneler; + List sources = []; + DateTime? lastCompiled; @Deprecated('Only sound null safety is supported as of Dart 3.0') final bool soundNullSafety; @@ -73,109 +78,118 @@ class WebDevFS { required String dillOutputPath, required ResidentCompiler generator, required List invalidatedFiles, + required bool initialCompile, + required bool fullRestart, }) async { final mainPath = mainUri.toFilePath(); final outputDirectoryPath = fileSystem.file(mainPath).parent.path; final entryPoint = mainUri.toString(); - var ddcModuleLoader = 'ddc_module_loader.js'; - var require = 'require.js'; - var stackMapper = 'stack_trace_mapper.js'; - var main = 'main.dart.js'; - var bootstrap = 'main_module.bootstrap.js'; - + var prefix = ''; // If base path is not overwritten, use main's subdirectory // to store all files, so the paths match the requests. if (assetServer.basePath.isEmpty) { final directory = p.dirname(entryPoint); - ddcModuleLoader = '$directory/ddc_module_loader.js'; - require = '$directory/require.js'; - stackMapper = '$directory/stack_trace_mapper.js'; - main = '$directory/main.dart.js'; - bootstrap = '$directory/main_module.bootstrap.js'; + prefix = '$directory/'; } - assetServer.writeFile( - entryPoint, fileSystem.file(mainPath).readAsStringSync()); - assetServer.writeFile(stackMapper, stackTraceMapper.readAsStringSync()); - - switch (ddcModuleFormat) { - case ModuleFormat.amd: - assetServer.writeFile(require, requireJS.readAsStringSync()); - assetServer.writeFile( - main, - generateBootstrapScript( - requireUrl: 'require.js', - mapperUrl: 'stack_trace_mapper.js', - entrypoint: entryPoint, - ), - ); - assetServer.writeFile( - bootstrap, - generateMainModule( - entrypoint: entryPoint, - ), - ); - break; - case ModuleFormat.ddc: - assetServer.writeFile( - ddcModuleLoader, ddcModuleLoaderJS.readAsStringSync()); - String bootstrapper; - String mainModule; - if (compilerOptions.canaryFeatures) { - bootstrapper = generateDDCLibraryBundleBootstrapScript( + if (initialCompile) { + final ddcModuleLoader = '${prefix}ddc_module_loader.js'; + final require = '${prefix}require.js'; + final stackMapper = '${prefix}stack_trace_mapper.js'; + final main = '${prefix}main.dart.js'; + final bootstrap = '${prefix}main_module.bootstrap.js'; + + assetServer.writeFile( + entryPoint, fileSystem.file(mainPath).readAsStringSync()); + assetServer.writeFile(stackMapper, stackTraceMapper.readAsStringSync()); + + switch (ddcModuleFormat) { + case ModuleFormat.amd: + assetServer.writeFile(require, requireJS.readAsStringSync()); + assetServer.writeFile( + main, + generateBootstrapScript( + requireUrl: 'require.js', + mapperUrl: 'stack_trace_mapper.js', + entrypoint: entryPoint, + ), + ); + assetServer.writeFile( + bootstrap, + generateMainModule( + entrypoint: entryPoint, + ), + ); + break; + case ModuleFormat.ddc: + assetServer.writeFile( + ddcModuleLoader, ddcModuleLoaderJS.readAsStringSync()); + String bootstrapper; + String mainModule; + if (compilerOptions.canaryFeatures) { + bootstrapper = generateDDCLibraryBundleBootstrapScript( + ddcModuleLoaderUrl: ddcModuleLoader, + mapperUrl: stackMapper, + entrypoint: entryPoint, + bootstrapUrl: bootstrap); + const onLoadEndBootstrap = 'on_load_end_bootstrap.js'; + assetServer.writeFile(onLoadEndBootstrap, + generateDDCLibraryBundleOnLoadEndBootstrap()); + mainModule = generateDDCLibraryBundleMainModule( + entrypoint: entryPoint, onLoadEndBootstrap: onLoadEndBootstrap); + } else { + bootstrapper = generateDDCBootstrapScript( ddcModuleLoaderUrl: ddcModuleLoader, mapperUrl: stackMapper, entrypoint: entryPoint, - bootstrapUrl: bootstrap); - mainModule = - generateDDCLibraryBundleMainModule(entrypoint: entryPoint); - } else { - bootstrapper = generateDDCBootstrapScript( - ddcModuleLoaderUrl: ddcModuleLoader, - mapperUrl: stackMapper, - entrypoint: entryPoint, - bootstrapUrl: bootstrap, - ); + bootstrapUrl: bootstrap, + ); - // DDC uses a simple heuristic to determine exported identifier names. - // The module name (entrypoint name here) has its extension removed, - // and special path elements like '/', '\', and '..' are replaced with - // '__'. - final exportedMainName = pathToJSIdentifier(entryPoint.split('.')[0]); - mainModule = generateDDCMainModule( - entrypoint: entryPoint, exportedMain: exportedMainName); - } - assetServer.writeFile( - main, - bootstrapper, - ); - assetServer.writeFile( - bootstrap, - mainModule, - ); - break; - default: - throw Exception('Unsupported DDC module format $ddcModuleFormat.'); - } + // DDC uses a simple heuristic to determine exported identifier + // names. The module name (entrypoint name here) has its extension + // removed, and special path elements like '/', '\', and '..' are + // replaced with + // '__'. + final exportedMainName = + pathToJSIdentifier(entryPoint.split('.')[0]); + mainModule = generateDDCMainModule( + entrypoint: entryPoint, exportedMain: exportedMainName); + } + assetServer.writeFile( + main, + bootstrapper, + ); + assetServer.writeFile( + bootstrap, + mainModule, + ); + break; + default: + throw Exception('Unsupported DDC module format $ddcModuleFormat.'); + } - assetServer.writeFile('main_module.digests', '{}'); + assetServer.writeFile('main_module.digests', '{}'); - final sdk = dartSdk; - final sdkSourceMap = dartSdkSourcemap; - assetServer.writeFile('dart_sdk.js', sdk.readAsStringSync()); - assetServer.writeFile('dart_sdk.js.map', sdkSourceMap.readAsStringSync()); + final sdk = dartSdk; + final sdkSourceMap = dartSdkSourcemap; + assetServer.writeFile('dart_sdk.js', sdk.readAsStringSync()); + assetServer.writeFile('dart_sdk.js.map', sdkSourceMap.readAsStringSync()); + generator.reset(); + } - generator.reset(); final compilerOutput = await generator.recompile( Uri.parse('org-dartlang-app:///$mainUri'), invalidatedFiles, outputPath: p.join(dillOutputPath, 'app.dill'), packageConfig: packageUriMapper.packageConfig, + recompileRestart: fullRestart, ); if (compilerOutput == null || compilerOutput.errorCount > 0) { return UpdateFSReport(success: false); } + sources = compilerOutput.sources; + lastCompiled = DateTime.now(); File codeFile; File manifestFile; @@ -197,6 +211,16 @@ class WebDevFS { } on FileSystemException catch (err) { throw Exception('Failed to load recompiled sources:\n$err'); } + if (ddcModuleFormat == ModuleFormat.ddc && + compilerOptions.canaryFeatures && + !initialCompile) { + if (fullRestart) { + performRestart(modules); + } else { + // TODO(srujzs): Support hot reload testing. + throw Exception('Hot reload is not supported yet.'); + } + } return UpdateFSReport( success: true, syncedBytes: codeFile.lengthSync(), @@ -204,6 +228,27 @@ class WebDevFS { )..invalidatedModules = modules; } + /// Given a list of [modules] that need to be loaded, 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) { + final srcIdsList = >[]; + for (final src in modules) { + srcIdsList.add({'src': src, 'id': src}); + } + assetServer.writeFile('restart_scripts.json', json.encode(srcIdsList)); + } + File get ddcModuleLoaderJS => fileSystem.file(sdkLayout.ddcModuleLoaderJsPath); File get requireJS => fileSystem.file(sdkLayout.requireJsPath); @@ -244,3 +289,68 @@ class UpdateFSReport { /// Only used for JavaScript compilation. List? invalidatedModules; } + +/// The result of an invalidation check from [ProjectFileInvalidator]. +class InvalidationResult { + const InvalidationResult({this.uris}); + + final List? uris; +} + +/// The [ProjectFileInvalidator] track the dependencies for a running +/// application to determine when they are dirty. +class ProjectFileInvalidator { + ProjectFileInvalidator({required FileSystem fileSystem}) + : _fileSystem = fileSystem; + + final FileSystem _fileSystem; + + static const String _pubCachePathLinuxAndMac = '.pub-cache'; + static const String _pubCachePathWindows = 'Pub/Cache'; + + Future findInvalidated({ + required DateTime? lastCompiled, + required List urisToMonitor, + required String packagesPath, + }) async { + if (lastCompiled == null) { + // Initial load. + assert(urisToMonitor.isEmpty); + return InvalidationResult(uris: []); + } + + final urisToScan = [ + // Don't watch pub cache directories to speed things up a little. + for (final Uri uri in urisToMonitor) + if (_isNotInPubCache(uri)) uri, + ]; + final invalidatedFiles = []; + for (final uri in urisToScan) { + // Calling fs.statSync() is more performant than fs.file().statSync(), + // but uri.toFilePath() does not work with MultiRootFileSystem. + final updatedAt = uri.hasScheme && uri.scheme != 'file' + ? _fileSystem.file(uri).statSync().modified + : _fileSystem + .statSync(uri.toFilePath(windows: Platform.isWindows)) + .modified; + if (updatedAt.isAfter(lastCompiled)) { + invalidatedFiles.add(uri); + } + } + // We need to check the .dart_tool/package_config.json file too since it is + // not used in compilation. + final packageFile = _fileSystem.file(packagesPath); + final packageUri = packageFile.uri; + final updatedAt = packageFile.statSync().modified; + if (updatedAt.isAfter(lastCompiled)) { + invalidatedFiles.add(packageUri); + } + + return InvalidationResult(uris: invalidatedFiles); + } + + bool _isNotInPubCache(Uri uri) { + return !(Platform.isWindows && uri.path.contains(_pubCachePathWindows)) && + !uri.path.contains(_pubCachePathLinuxAndMac); + } +} diff --git a/frontend_server_common/lib/src/frontend_server_client.dart b/frontend_server_common/lib/src/frontend_server_client.dart index 9151cdda8..e500646b4 100644 --- a/frontend_server_common/lib/src/frontend_server_client.dart +++ b/frontend_server_common/lib/src/frontend_server_client.dart @@ -173,13 +173,15 @@ class _RecompileRequest extends _CompilationRequest { this.mainUri, this.invalidatedFiles, this.outputPath, - this.packageConfig, - ); + this.packageConfig, { + required this.recompileRestart, + }); Uri mainUri; List invalidatedFiles; String outputPath; PackageConfig packageConfig; + bool recompileRestart; @override Future _run(ResidentCompiler compiler) async => @@ -288,11 +290,14 @@ class ResidentCompiler { /// point that is used for recompilation. /// Binary file name is returned if compilation was successful, otherwise /// null is returned. + /// If [recompileRestart] is true, uses the `recompile-restart` instruction + /// instead of `recompile`. Future recompile( Uri mainUri, List invalidatedFiles, { required String outputPath, required PackageConfig packageConfig, + required bool recompileRestart, }) async { if (!_controller.hasListener) { _controller.stream.listen(_handleCompilationRequest); @@ -300,7 +305,8 @@ class ResidentCompiler { final completer = Completer(); _controller.add(_RecompileRequest( - completer, mainUri, invalidatedFiles, outputPath, packageConfig)); + completer, mainUri, invalidatedFiles, outputPath, packageConfig, + recompileRestart: recompileRestart)); return completer.future; } @@ -320,8 +326,10 @@ class ResidentCompiler { final server = _server!; final inputKey = generateV4UUID(); - server.stdin.writeln('recompile $mainUri$inputKey'); - _logger.info('<- recompile $mainUri$inputKey'); + final instruction = + request.recompileRestart ? 'recompile-restart' : 'recompile'; + server.stdin.writeln('$instruction $mainUri $inputKey'); + _logger.info('<- $instruction $mainUri $inputKey'); for (final fileUri in request.invalidatedFiles) { String message; if (fileUri.scheme == 'package') { diff --git a/frontend_server_common/lib/src/resident_runner.dart b/frontend_server_common/lib/src/resident_runner.dart index fafbf345a..604b33035 100644 --- a/frontend_server_common/lib/src/resident_runner.dart +++ b/frontend_server_common/lib/src/resident_runner.dart @@ -63,13 +63,21 @@ class ResidentWebRunner { late ResidentCompiler generator; late ExpressionCompiler expressionCompiler; - late WebDevFS devFS; - late Uri uri; + ProjectFileInvalidator? _projectFileInvalidator; + WebDevFS? devFS; + Uri? uri; late Iterable modules; Future run( - FileSystem fileSystem, String? hostname, int port, String index) async { - devFS = WebDevFS( + FileSystem fileSystem, + String? hostname, + int port, + String index, { + required bool initialCompile, + required bool fullRestart, + }) async { + _projectFileInvalidator ??= ProjectFileInvalidator(fileSystem: fileSystem); + devFS ??= WebDevFS( fileSystem: fileSystem, hostname: hostname ?? 'localhost', port: port, @@ -80,9 +88,10 @@ class ResidentWebRunner { sdkLayout: sdkLayout, compilerOptions: compilerOptions, ); - uri = await devFS.create(); + uri ??= await devFS!.create(); - final report = await _updateDevFS(); + final report = await _updateDevFS( + initialCompile: initialCompile, fullRestart: fullRestart); if (!report.success) { _logger.severe('Failed to compile application.'); return 1; @@ -94,17 +103,25 @@ class ResidentWebRunner { return 0; } - Future _updateDevFS() async { - final report = await devFS.update( + Future _updateDevFS( + {required bool initialCompile, required bool fullRestart}) async { + final invalidationResult = await _projectFileInvalidator!.findInvalidated( + lastCompiled: devFS!.lastCompiled, + urisToMonitor: devFS!.sources, + packagesPath: packageConfigFile.toFilePath(), + ); + final report = await devFS!.update( mainUri: mainUri, dillOutputPath: outputPath, generator: generator, - invalidatedFiles: []); + invalidatedFiles: invalidationResult.uris!, + initialCompile: initialCompile, + fullRestart: fullRestart); return report; } Future stop() async { await generator.shutdown(); - await devFS.dispose(); + await devFS!.dispose(); } } From bfbbeb35a85104af6d8bd1e56c35311e2c635b3b Mon Sep 17 00:00:00 2001 From: Srujan Gaddam Date: Fri, 11 Apr 2025 12:28:10 -0700 Subject: [PATCH 2/3] Remove unused import --- dwds/test/fixtures/context.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/dwds/test/fixtures/context.dart b/dwds/test/fixtures/context.dart index 273f76ffd..1d9f497fb 100644 --- a/dwds/test/fixtures/context.dart +++ b/dwds/test/fixtures/context.dart @@ -24,7 +24,6 @@ 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'; From 32dce82a8bb758959c502491a5b24cfbab827e8d Mon Sep 17 00:00:00 2001 From: Srujan Gaddam Date: Fri, 11 Apr 2025 13:41:11 -0700 Subject: [PATCH 3/3] Update version --- dwds/CHANGELOG.md | 2 ++ dwds/lib/src/version.dart | 2 +- dwds/pubspec.yaml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index fc8f3e7e6..ce0745325 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,3 +1,5 @@ +## 24.3.11-wip + ## 24.3.10 - Disabled breakpoints on changed files in a hot reload. They currently do not diff --git a/dwds/lib/src/version.dart b/dwds/lib/src/version.dart index 0380cfc8b..a107a046c 100644 --- a/dwds/lib/src/version.dart +++ b/dwds/lib/src/version.dart @@ -1,2 +1,2 @@ // Generated code. Do not modify. -const packageVersion = '24.3.10'; +const packageVersion = '24.3.11-wip'; diff --git a/dwds/pubspec.yaml b/dwds/pubspec.yaml index e22f02ea0..18f4ee4c3 100644 --- a/dwds/pubspec.yaml +++ b/dwds/pubspec.yaml @@ -1,6 +1,6 @@ name: dwds # Every time this changes you need to run `dart run build_runner build`. -version: 24.3.10 +version: 24.3.11-wip description: >- A service that proxies between the Chrome debug protocol and the Dart VM service protocol.