From bd1f6a6500f246ab5be694f416bd65e3189c23b9 Mon Sep 17 00:00:00 2001 From: Srujan Gaddam Date: Mon, 18 Aug 2025 14:29:59 -0700 Subject: [PATCH 1/4] Wait a second before making edits to files on Windows dart:io does not have millisecond-level precision on file stats and instead, the modified time is rounded to the nearest second. If edits and recompiles are done within the same second as the previous compile, frontend_server_common can't tell that the files have changed. Instead, wait a second before editing. In order to support this, edits are staged at once. Without this, we'd be waiting multiple seconds for several edits. --- dwds/test/common/hot_restart_common.dart | 11 ++- .../hot_restart_correctness_common.dart | 12 +-- dwds/test/devtools_test.dart | 11 ++- dwds/test/fixtures/context.dart | 43 ++++++----- dwds/test/hot_reload_breakpoints_test.dart | 75 ++++++++++--------- dwds/test/hot_reload_test.dart | 12 +-- dwds/test/hot_restart_breakpoints_test.dart | 53 +++++++------ 7 files changed, 120 insertions(+), 97 deletions(-) diff --git a/dwds/test/common/hot_restart_common.dart b/dwds/test/common/hot_restart_common.dart index 5cea54d1e..944195668 100644 --- a/dwds/test/common/hot_restart_common.dart +++ b/dwds/test/common/hot_restart_common.dart @@ -47,10 +47,13 @@ void runTests({ } Future makeEditAndRecompile() async { - context.makeEditToDartEntryFile( - toReplace: originalString, - replaceWith: newString, - ); + await context.makeEdits([ + ( + file: context.project.dartEntryFileName, + originalString: originalString, + newString: newString, + ), + ]); await recompile(hasEdits: true); } diff --git a/dwds/test/common/hot_restart_correctness_common.dart b/dwds/test/common/hot_restart_correctness_common.dart index 381c41a39..b6353c9cd 100644 --- a/dwds/test/common/hot_restart_correctness_common.dart +++ b/dwds/test/common/hot_restart_correctness_common.dart @@ -38,11 +38,13 @@ void runTests({ final context = TestContext(testHotRestart2, provider); Future makeEditAndRecompile() async { - context.makeEditToDartLibFile( - libFileName: 'library2.dart', - toReplace: originalString, - replaceWith: newString, - ); + await context.makeEdits([ + ( + file: 'library2.dart', + originalString: originalString, + newString: newString, + ), + ]); if (compilationMode == CompilationMode.frontendServer) { await context.recompile(fullRestart: true); } else { diff --git a/dwds/test/devtools_test.dart b/dwds/test/devtools_test.dart index 3c246fe74..94eeb70fd 100644 --- a/dwds/test/devtools_test.dart +++ b/dwds/test/devtools_test.dart @@ -112,10 +112,13 @@ void main() { // https://github.com/dart-lang/webdev/pull/901#issuecomment-586438132 final client = context.debugConnection.vmService; await client.streamListen('Isolate'); - context.makeEditToDartEntryFile( - toReplace: 'Hello World!', - replaceWith: 'Bonjour le monde!', - ); + await context.makeEdits([ + ( + file: context.project.dartEntryFileName, + originalString: 'Hello World!', + newString: 'Bonjour le monde!', + ), + ]); await context.waitForSuccessfulBuild(propagateToBrowser: true); final eventsDone = expectLater( diff --git a/dwds/test/fixtures/context.dart b/dwds/test/fixtures/context.dart index 6a40c8f5a..05b95b696 100644 --- a/dwds/test/fixtures/context.dart +++ b/dwds/test/fixtures/context.dart @@ -565,23 +565,32 @@ class TestContext { _outputDir = null; } - void makeEditToDartEntryFile({ - required String toReplace, - required String replaceWith, - }) { - final file = File(project.dartEntryFilePath); - final fileContents = file.readAsStringSync(); - file.writeAsStringSync(fileContents.replaceAll(toReplace, replaceWith)); - } - - void makeEditToDartLibFile({ - required String libFileName, - required String toReplace, - required String replaceWith, - }) { - final file = File(project.dartLibFilePath(libFileName)); - final fileContents = file.readAsStringSync(); - file.writeAsStringSync(fileContents.replaceAll(toReplace, replaceWith)); + /// Given a list of edits, use file IO to write them to the file system. + /// + /// If `file` has the same name as the project's entry file name, that file + /// will be edited. Otherwise, it's assumed to be a library file. + // TODO(srujzs): It's possible we may want a library file with the same name + // as the entry file, but this function doesn't allow that. Potentially + // support that. + Future makeEdits( + List<({String file, String originalString, String newString})> edits, + ) async { + // `dart:io`'s `stat` on Windows does not have millisecond precision so we + // need to make sure we wait long enough that modifications result in a + // timestamp that is guaranteed to be after the previous compile. + // TODO(https://github.com/dart-lang/sdk/issues/51937): Remove once this bug + // is fixed. + if (Platform.isWindows) await Future.delayed(Duration(seconds: 1)); + for (var (:file, :originalString, :newString) in edits) { + if (file == project.dartEntryFileName) { + file = project.dartEntryFilePath; + } else { + file = project.dartLibFilePath(file); + } + final f = File(project.dartLibFilePath(file)); + final fileContents = f.readAsStringSync(); + f.writeAsStringSync(fileContents.replaceAll(originalString, newString)); + } } void addLibraryFile({required String libFileName, required String contents}) { diff --git a/dwds/test/hot_reload_breakpoints_test.dart b/dwds/test/hot_reload_breakpoints_test.dart index 7849005c7..b71e23bd8 100644 --- a/dwds/test/hot_reload_breakpoints_test.dart +++ b/dwds/test/hot_reload_breakpoints_test.dart @@ -35,28 +35,11 @@ void main() { tearDownAll(provider.dispose); - void makeEdit(String file, String originalString, String newString) { - if (file == project.dartEntryFileName) { - context.makeEditToDartEntryFile( - toReplace: originalString, - replaceWith: newString, - ); - } else { - context.makeEditToDartLibFile( - libFileName: file, - toReplace: originalString, - replaceWith: newString, - ); - } - } - - Future makeEditAndRecompile( - String file, - String originalString, - String newString, + Future makeEditsAndRecompile( + List<({String file, String originalString, String newString})> edits, ) async { - makeEdit(file, originalString, newString); - await context.recompile(fullRestart: false); + await context.makeEdits(edits); + await context.recompile(fullRestart: true); } group('when pause_isolates_on_start is true', () { @@ -255,7 +238,9 @@ void main() { await resumeAndExpectLog(oldString); // Modify the string that gets printed. - await makeEditAndRecompile(mainFile, oldString, newString); + await makeEditsAndRecompile([ + (file: mainFile, originalString: oldString, newString: newString), + ]); await hotReloadAndHandlePausePost([ (file: mainFile, breakpointMarker: callLogMarker, bp: bp), @@ -291,7 +276,9 @@ void main() { final extraLog = 'hot reload'; final oldString = "log('"; final newString = "log('$extraLog');\n$oldString"; - await makeEditAndRecompile(mainFile, oldString, newString); + await makeEditsAndRecompile([ + (file: mainFile, originalString: oldString, newString: newString), + ]); bp = (await hotReloadAndHandlePausePost([ @@ -307,7 +294,9 @@ void main() { await resumeAndExpectLog(genLog); // Remove the line we just added. - await makeEditAndRecompile(mainFile, newString, oldString); + await makeEditsAndRecompile([ + (file: mainFile, originalString: newString, newString: oldString), + ]); await hotReloadAndHandlePausePost([ (file: mainFile, breakpointMarker: callLogMarker, bp: bp), @@ -363,10 +352,13 @@ void main() { final newImports = '$oldImports\n' "import 'package:_test_hot_reload_breakpoints/library.dart';"; - makeEdit(mainFile, oldImports, newImports); + final edits = [ + (file: mainFile, originalString: oldImports, newString: newImports), + ]; final oldLog = "log('\$mainValue');"; final newLog = "log('\$libraryValue');"; - await makeEditAndRecompile(mainFile, oldLog, newLog); + edits.add((file: mainFile, originalString: oldLog, newString: newLog)); + await makeEditsAndRecompile(edits); await hotReloadAndHandlePausePost([ (file: mainFile, breakpointMarker: callLogMarker, bp: bp), @@ -410,6 +402,8 @@ void main() { // Add library files, import them, but only refer to the last one in main. final numFiles = 50; + final edits = + <({String file, String originalString, String newString})>[]; for (var i = 1; i <= numFiles; i++) { final libFile = 'library$i.dart'; context.addLibraryFile( @@ -422,11 +416,16 @@ void main() { final newImports = '$oldImports\n' "import 'package:_test_hot_reload_breakpoints/$libFile';"; - makeEdit(mainFile, oldImports, newImports); + edits.add(( + file: mainFile, + originalString: oldImports, + newString: newImports, + )); } final oldLog = "log('\$mainValue');"; final newLog = "log('\$libraryValue$numFiles');"; - await makeEditAndRecompile(mainFile, oldLog, newLog); + edits.add((file: mainFile, originalString: oldLog, newString: newLog)); + await makeEditsAndRecompile(edits); await hotReloadAndHandlePausePost([ (file: mainFile, breakpointMarker: callLogMarker, bp: bp), @@ -460,7 +459,9 @@ void main() { final oldLog = "log('\$mainValue');"; final newLog = "log('\${closure()}');"; - await makeEditAndRecompile(mainFile, oldLog, newLog); + await makeEditsAndRecompile([ + (file: mainFile, originalString: oldLog, newString: newLog), + ]); bp = (await hotReloadAndHandlePausePost([ @@ -478,11 +479,13 @@ void main() { await resumeAndExpectLog(oldCapturedString); final newCapturedString = 'captured closure gen1'; - await makeEditAndRecompile( - mainFile, - oldCapturedString, - newCapturedString, - ); + await makeEditsAndRecompile([ + ( + file: mainFile, + originalString: oldCapturedString, + newString: newCapturedString, + ), + ]); await hotReloadAndHandlePausePost([ (file: mainFile, breakpointMarker: capturedStringMarker, bp: bp), @@ -539,7 +542,9 @@ void main() { await callEvaluateAndExpectLog(oldString); // Modify the string that gets printed and hot reload. - await makeEditAndRecompile(mainFile, oldString, newString); + await makeEditsAndRecompile([ + (file: mainFile, originalString: oldString, newString: newString), + ]); final vm = await client.getVM(); final isolate = await client.getIsolate(vm.isolates!.first.id!); final report = await client.reloadSources(isolate.id!); diff --git a/dwds/test/hot_reload_test.dart b/dwds/test/hot_reload_test.dart index 27c48fad4..b6d72ba38 100644 --- a/dwds/test/hot_reload_test.dart +++ b/dwds/test/hot_reload_test.dart @@ -40,11 +40,13 @@ void main() { } Future makeEditAndRecompile() async { - context.makeEditToDartLibFile( - libFileName: 'library1.dart', - toReplace: originalString, - replaceWith: newString, - ); + await context.makeEdits([ + ( + file: 'library1.dart', + originalString: originalString, + newString: newString, + ), + ]); await recompile(); } diff --git a/dwds/test/hot_restart_breakpoints_test.dart b/dwds/test/hot_restart_breakpoints_test.dart index 358baa2fb..349af9272 100644 --- a/dwds/test/hot_restart_breakpoints_test.dart +++ b/dwds/test/hot_restart_breakpoints_test.dart @@ -36,27 +36,10 @@ void main() { tearDownAll(provider.dispose); - void makeEdit(String file, String originalString, String newString) { - if (file == project.dartEntryFileName) { - context.makeEditToDartEntryFile( - toReplace: originalString, - replaceWith: newString, - ); - } else { - context.makeEditToDartLibFile( - libFileName: file, - toReplace: originalString, - replaceWith: newString, - ); - } - } - - Future makeEditAndRecompile( - String file, - String originalString, - String newString, + Future makeEditsAndRecompile( + List<({String file, String originalString, String newString})> edits, ) async { - makeEdit(file, originalString, newString); + await context.makeEdits(edits); await context.recompile(fullRestart: true); } @@ -215,7 +198,9 @@ void main() { await addBreakpoint(file: mainFile, breakpointMarker: callLogMarker); - await makeEditAndRecompile(mainFile, oldLog, newLog); + await makeEditsAndRecompile([ + (file: mainFile, originalString: oldLog, newString: newLog), + ]); final breakpointFuture = waitForBreakpoint(); @@ -239,7 +224,9 @@ void main() { final extraLog = 'hot reload'; final oldString = "log('"; final newString = "log('$extraLog');\n$oldString"; - await makeEditAndRecompile(mainFile, oldString, newString); + await makeEditsAndRecompile([ + (file: mainFile, originalString: oldString, newString: newString), + ]); var breakpointFuture = waitForBreakpoint(); @@ -256,7 +243,9 @@ void main() { consoleLogs.clear(); // Remove the line we just added. - await makeEditAndRecompile(mainFile, newString, oldString); + await makeEditsAndRecompile([ + (file: mainFile, originalString: newString, newString: oldString), + ]); breakpointFuture = waitForBreakpoint(); @@ -293,10 +282,13 @@ void main() { final newImports = '$oldImports\n' "import 'package:_test_hot_restart_breakpoints/library.dart';"; - makeEdit(mainFile, oldImports, newImports); + final edits = [ + (file: mainFile, originalString: oldImports, newString: newImports), + ]; final oldLog = "log('$genLog');"; final newLog = "log('\$libraryValue');"; - await makeEditAndRecompile(mainFile, oldLog, newLog); + edits.add((file: mainFile, originalString: oldLog, newString: newLog)); + await makeEditsAndRecompile(edits); var breakpointFuture = waitForBreakpoint(); @@ -329,6 +321,8 @@ void main() { // Add library files, import them, but only refer to the last one in main. final numFiles = 50; + final edits = + <({String file, String originalString, String newString})>[]; for (var i = 1; i <= numFiles; i++) { final libFile = 'library$i.dart'; context.addLibraryFile( @@ -341,11 +335,16 @@ void main() { final newImports = '$oldImports\n' "import 'package:_test_hot_restart_breakpoints/$libFile';"; - makeEdit(mainFile, oldImports, newImports); + edits.add(( + file: mainFile, + originalString: oldImports, + newString: newImports, + )); } final oldLog = "log('$genLog');"; final newLog = "log('\$libraryValue$numFiles');"; - await makeEditAndRecompile(mainFile, oldLog, newLog); + edits.add((file: mainFile, originalString: oldLog, newString: newLog)); + await makeEditsAndRecompile(edits); var breakpointFuture = waitForBreakpoint(); From fea0600a272c0b90dfe4664a4bafe3922ff1bb5f Mon Sep 17 00:00:00 2001 From: Srujan Gaddam Date: Tue, 19 Aug 2025 17:16:53 -0700 Subject: [PATCH 2/4] Change pageSource tests to use console logs instead --- dwds/test/common/hot_restart_common.dart | 183 ++++++------------ .../hot_restart_correctness_common.dart | 88 ++++----- dwds/test/fixtures/context.dart | 4 +- dwds/test/hot_reload_test.dart | 50 ++--- fixtures/_test/example/append_body/main.dart | 22 ++- fixtures/_test_hot_reload/web/main.dart | 8 +- fixtures/_test_hot_restart2/web/main.dart | 14 +- 7 files changed, 136 insertions(+), 233 deletions(-) diff --git a/dwds/test/common/hot_restart_common.dart b/dwds/test/common/hot_restart_common.dart index 944195668..50ce08eed 100644 --- a/dwds/test/common/hot_restart_common.dart +++ b/dwds/test/common/hot_restart_common.dart @@ -57,20 +57,17 @@ void runTests({ await recompile(hasEdits: true); } - /// Wait for main to finish executing before checking expectations by checking - /// for a log output. - /// - /// If [debuggingEnabled] is false, we can't check for Chrome logs and instead - /// wait 1 second. - // TODO(srujzs): We should do something less prone to race conditions when - // debugging is disabled. - Future waitForMainToExecute({bool debuggingEnabled = true}) async { - if (!debuggingEnabled) return Future.delayed(const Duration(seconds: 1)); + /// Wait for `expectedStrings` to be printed in the console. + Future expectLogs(List expectedStrings) async { + final expectations = List.from(expectedStrings); final completer = Completer(); - final expectedString = 'main executed'; final subscription = context.webkitDebugger.onConsoleAPICalled.listen((e) { - if (e.args.first.value == expectedString) { - completer.complete(); + final value = e.args.first.value; + if (expectations.contains(value)) { + expectations.remove(value); + if (expectations.isEmpty) { + completer.complete(); + } } }); await completer.future; @@ -98,14 +95,10 @@ void runTests({ }); test('can live reload changes ', () async { - final mainDone = waitForMainToExecute(); - await makeEditAndRecompile(); - await mainDone; - final source = await context.webDriver.pageSource; - // A full reload should clear the state. - expect(source.contains(originalString), isFalse); - expect(source.contains(newString), isTrue); + final logExpectation = expectLogs([newString]); + await makeEditAndRecompile(); + await logExpectation; }); }); @@ -130,14 +123,10 @@ void runTests({ }); test('can live reload changes ', () async { - final mainDone = waitForMainToExecute(debuggingEnabled: false); - await makeEditAndRecompile(); - await mainDone; - final source = await context.webDriver.pageSource; - // A full reload should clear the state. - expect(source.contains(originalString), isFalse); - expect(source.contains(newString), isTrue); + final logExpectation = expectLogs([newString]); + await makeEditAndRecompile(); + await logExpectation; }); }); @@ -163,14 +152,10 @@ void runTests({ }); test('can live reload changes ', () async { - final mainDone = waitForMainToExecute(debuggingEnabled: false); - await makeEditAndRecompile(); - await mainDone; - final source = await context.webDriver.pageSource; - // A full reload should clear the state. - expect(source.contains(originalString), isFalse); - expect(source.contains(newString), isTrue); + final logExpectation = expectLogs([newString]); + await makeEditAndRecompile(); + await logExpectation; }); }); }, @@ -306,7 +291,8 @@ void runTests({ ]), ), ); - final mainDone = waitForMainToExecute(); + // Main is re-invoked which shouldn't clear the state. + final logExpectation = expectLogs(['$originalString $newString']); final hotRestart = context.getRegisteredServiceExtension('hotRestart'); expect( await fakeClient.callServiceExtension(hotRestart!), @@ -314,12 +300,7 @@ void runTests({ ); await eventsDone; - await mainDone; - - final source = await context.webDriver.pageSource; - // Main is re-invoked which shouldn't clear the state. - expect(source, contains(originalString)); - expect(source, contains(newString)); + await logExpectation; }); test('can send events before and after hot restart', () async { @@ -352,7 +333,8 @@ void runTests({ ); await recompile(); - final mainDone = waitForMainToExecute(); + // Main is re-invoked which shouldn't clear the state. + final logExpectation = expectLogs(['$originalString $originalString']); final hotRestart = context.getRegisteredServiceExtension('hotRestart'); expect( await fakeClient.callServiceExtension(hotRestart!), @@ -371,11 +353,7 @@ void runTests({ ); await eventsDone; - await mainDone; - - final source = await context.webDriver.pageSource; - // Main is re-invoked which shouldn't clear the state. - expect(source, contains('Hello World!')); + await logExpectation; }); test('can refresh the page via the fullReload service extension', () async { @@ -393,7 +371,8 @@ void runTests({ ]), ), ); - final mainDone = waitForMainToExecute(); + // Should see only the new text. + final logExpectation = expectLogs([newString]); final fullReload = context.getRegisteredServiceExtension('fullReload'); expect( await fakeClient.callServiceExtension(fullReload!), @@ -401,12 +380,7 @@ void runTests({ ); await eventsDone; - await mainDone; - - final source = await context.webDriver.pageSource; - // Should see only the new text - expect(source.contains(originalString), isFalse); - expect(source.contains(newString), isTrue); + await logExpectation; }); test('can hot restart while paused', () async { @@ -430,17 +404,11 @@ void runTests({ ); await makeEditAndRecompile(); - final mainDone = waitForMainToExecute(); + // Main is re-invoked which shouldn't clear the state. + final logExpectation = expectLogs(['$originalString $newString']); final hotRestart = context.getRegisteredServiceExtension('hotRestart'); await fakeClient.callServiceExtension(hotRestart!); - - await mainDone; - - 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); + await logExpectation; vm = await client.getVM(); isolateId = vm.isolates!.first.id!; @@ -477,37 +445,27 @@ void runTests({ test('can hot restart with no changes, hot restart with changes, and ' 'hot restart again with no changes', () async { // Empty hot restart. - var mainDone = waitForMainToExecute(); + var logExpectation = expectLogs(['$originalString $originalString']); await recompile(); final hotRestart = context.getRegisteredServiceExtension('hotRestart'); await fakeClient.callServiceExtension(hotRestart!); - - await mainDone; - var source = await context.webDriver.pageSource; - expect(source.contains(originalString), isTrue); - expect(source.contains(newString), isFalse); + await logExpectation; // Hot restart. - mainDone = waitForMainToExecute(); + logExpectation = expectLogs([ + '$originalString $originalString $newString', + ]); await makeEditAndRecompile(); await fakeClient.callServiceExtension(hotRestart); - - await mainDone; - 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); + await logExpectation; // Empty hot restart. - mainDone = waitForMainToExecute(); + logExpectation = expectLogs([ + '$originalString $originalString $newString $newString', + ]); await recompile(); await fakeClient.callServiceExtension(hotRestart); - - await mainDone; - source = await context.webDriver.pageSource; - expect(source.contains(originalString), isTrue); - // `newString` should now exist twice in the source. - expect(source.contains(RegExp('$newString.*$newString')), isTrue); + await logExpectation; }); }, timeout: Timeout.factor(2)); @@ -532,19 +490,15 @@ void runTests({ }); test('can hot restart changes ', () async { - final mainDone = waitForMainToExecute(); - await makeEditAndRecompile(); - await mainDone; - 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'), - ); + final logExpectations = expectLogs([ + '$originalString $newString', + // The ext.flutter.disassemble callback is invoked and waited for. + 'start disassemble', + 'end disassemble', + ]); + await makeEditAndRecompile(); + await logExpectations; }); test( @@ -592,19 +546,15 @@ void runTests({ }); test('can hot restart changes ', () async { - final mainDone = waitForMainToExecute(debuggingEnabled: false); - await makeEditAndRecompile(); - await mainDone; - 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'), - ); + final logExpectations = expectLogs([ + '$originalString $newString', + // The ext.flutter.disassemble callback is invoked and waited for. + 'start disassemble', + 'end disassemble', + ]); + await makeEditAndRecompile(); + await logExpectations; }); }); }, @@ -653,7 +603,8 @@ void runTests({ ), ); - final mainDone = waitForMainToExecute(); + // Main is re-invoked which shouldn't clear the state. + final logExpectation = expectLogs(['$originalString $newString']); final hotRestart = context.getRegisteredServiceExtension('hotRestart'); expect( await fakeClient.callServiceExtension(hotRestart!), @@ -662,24 +613,18 @@ void runTests({ 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); - await mainDone; - - final sourceAfterResume = await context.webDriver.pageSource; - expect(sourceAfterResume.contains(newString), isTrue); + await logExpectation; }, ); test( 'after page refresh, does not run app until there is a resume event', () async { - final mainDone = waitForMainToExecute(); + final logExpectation = expectLogs([newString]); await makeEditAndRecompile(); await context.webDriver.driver.refresh(); @@ -696,17 +641,11 @@ void runTests({ 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); - await mainDone; - - final sourceAfterResume = await context.webDriver.pageSource; - expect(sourceAfterResume.contains(newString), isTrue); + await logExpectation; }, ); }); diff --git a/dwds/test/common/hot_restart_correctness_common.dart b/dwds/test/common/hot_restart_correctness_common.dart index b6353c9cd..302be62dc 100644 --- a/dwds/test/common/hot_restart_correctness_common.dart +++ b/dwds/test/common/hot_restart_correctness_common.dart @@ -7,12 +7,13 @@ @Timeout(Duration(minutes: 5)) library; +import 'dart:async'; + 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'; @@ -53,6 +54,18 @@ void runTests({ } } + /// Wait for `expectedString` to be printed in the console. + Future expectLog(String expectedString) async { + final completer = Completer(); + final subscription = context.webkitDebugger.onConsoleAPICalled.listen((e) { + if (e.args.first.value == expectedString) { + completer.complete(); + } + }); + await completer.future; + await subscription.cancel(); + } + group('Injected client', () { VmService? fakeClient; @@ -74,18 +87,25 @@ void runTests({ await context.tearDown(); }); + test('initial state prints the right log', () async { + final client = context.debugConnection.vmService; + + final logExpectation = expectLog( + 'ConstObject(reloadVariable: 23, ConstantEqualitySuccess)', + ); + final vm = await client.getVM(); + final isolate = await client.getIsolate(vm.isolates!.first.id!); + final rootLib = isolate.rootLib; + await client.evaluate(isolate.id!, rootLib!.id!, 'printConst()'); + await logExpectation; + }); + 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( @@ -99,6 +119,9 @@ void runTests({ ), ); + final logExpectation = expectLog( + 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', + ); final hotRestart = context.getRegisteredServiceExtension('hotRestart'); expect( await fakeClient!.callServiceExtension(hotRestart!), @@ -106,16 +129,7 @@ void runTests({ ); await eventsDone; - - source = await context.webDriver.pageSource; - if (dartSdkIsAtLeast('3.4.0-61.0.dev')) { - expect( - source, - contains( - 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', - ), - ); - } + await logExpectation; }, ); }, timeout: Timeout.factor(2)); @@ -141,25 +155,11 @@ void runTests({ }); test('properly compares constants after hot restart', () async { - var source = await context.webDriver.pageSource; - expect( - source, - contains( - 'ConstObject(reloadVariable: 23, ConstantEqualitySuccess)', - ), + final logExpectation = expectLog( + 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', ); - await makeEditAndRecompile(); - - source = await context.webDriver.pageSource; - if (dartSdkIsAtLeast('3.4.0-61.0.dev')) { - expect( - source, - contains( - 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', - ), - ); - } + await logExpectation; }); }); @@ -184,25 +184,11 @@ void runTests({ }); test('properly compares constants after hot restart', () async { - var source = await context.webDriver.pageSource; - expect( - source, - contains( - 'ConstObject(reloadVariable: 23, ConstantEqualitySuccess)', - ), + final logExpectation = expectLog( + 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', ); - await makeEditAndRecompile(); - - source = await context.webDriver.pageSource; - if (dartSdkIsAtLeast('3.4.0-61.0.dev')) { - expect( - source, - contains( - 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', - ), - ); - } + await logExpectation; }); }); }, diff --git a/dwds/test/fixtures/context.dart b/dwds/test/fixtures/context.dart index 05b95b696..ead0c53f5 100644 --- a/dwds/test/fixtures/context.dart +++ b/dwds/test/fixtures/context.dart @@ -492,8 +492,9 @@ class TestContext { if (debugSettings.enableDebugging && !testSettings.waitToDebug) { await startDebugging(); } + _webkitDebugger = WebkitDebugger(WipDebugger(tabConnection)); } else { - // No tab needs to be dicovered, so fulfill the relevant completer. + // No tab needs to be discovered, so fulfill the relevant completer. tabConnectionCompleter.complete(); } } catch (e, s) { @@ -538,7 +539,6 @@ class TestContext { Future startDebugging() async { debugConnection = await testServer.dwds.debugConnection(appConnection); - _webkitDebugger = WebkitDebugger(WipDebugger(tabConnection)); } Future tearDown() async { diff --git a/dwds/test/hot_reload_test.dart b/dwds/test/hot_reload_test.dart index b6d72ba38..fafcbe749 100644 --- a/dwds/test/hot_reload_test.dart +++ b/dwds/test/hot_reload_test.dart @@ -50,16 +50,20 @@ void main() { await recompile(); } - /// Wait for `evaluate` to finish executing before checking expectations by - /// checking for a log output. - Future waitForEvaluateToExecute() async { + // Call `evaluate` through an expression evaluation and wait for + // `expectedString` to be printed. + Future callEvaluateAndExpectLog(String expectedString) async { + final client = context.debugConnection.vmService; final completer = Completer(); - final expectedString = 'evaluate executed'; final subscription = context.webkitDebugger.onConsoleAPICalled.listen((e) { if (e.args.first.value == expectedString) { completer.complete(); } }); + final vm = await client.getVM(); + final isolate = await client.getIsolate(vm.isolates!.first.id!); + final rootLib = isolate.rootLib; + await client.evaluate(isolate.id!, rootLib!.id!, 'evaluate()'); await completer.future; await subscription.cancel(); } @@ -93,63 +97,35 @@ void main() { 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 evaluateDone = waitForEvaluateToExecute(); - final rootLib = isolate.rootLib; - await client.evaluate(isolate.id!, rootLib!.id!, 'evaluate()'); - await evaluateDone; - source = await context.webDriver.pageSource; - expect(source, contains(newString)); - expect(source.contains(originalString), false); + await callEvaluateAndExpectLog(newString); }); test('can hot reload with no changes, hot reload with changes, and ' 'hot reload again with no changes', () async { final client = context.debugConnection.vmService; - // Empty hot reload, + // Empty hot reload. await recompile(); final vm = await client.getVM(); final isolate = await client.getIsolate(vm.isolates!.first.id!); var report = await fakeClient.reloadSources(isolate.id!); expect(report.success, true); - var evaluateDone = waitForEvaluateToExecute(); - final rootLib = isolate.rootLib; - await client.evaluate(isolate.id!, rootLib!.id!, 'evaluate()'); - await evaluateDone; - var source = await context.webDriver.pageSource; - expect(source, contains(originalString)); - expect(source.contains(newString), false); + await callEvaluateAndExpectLog(originalString); // Hot reload. await makeEditAndRecompile(); report = await fakeClient.reloadSources(isolate.id!); expect(report.success, true); - evaluateDone = waitForEvaluateToExecute(); - await client.evaluate(isolate.id!, rootLib.id!, 'evaluate()'); - await evaluateDone; - source = await context.webDriver.pageSource; - expect(source, contains(newString)); - expect(source.contains(originalString), false); + await callEvaluateAndExpectLog(newString); // Empty hot reload. await recompile(); report = await fakeClient.reloadSources(isolate.id!); expect(report.success, true); - evaluateDone = waitForEvaluateToExecute(); - await client.evaluate(isolate.id!, rootLib.id!, 'evaluate()'); - await evaluateDone; - source = await context.webDriver.pageSource; - expect(source, contains(newString)); - expect(source.contains(originalString), false); + await callEvaluateAndExpectLog(newString); }); }, timeout: Timeout.factor(2)); } diff --git a/fixtures/_test/example/append_body/main.dart b/fixtures/_test/example/append_body/main.dart index c030eac89..8a89205d1 100644 --- a/fixtures/_test/example/append_body/main.dart +++ b/fixtures/_test/example/append_body/main.dart @@ -4,14 +4,16 @@ import 'dart:async'; import 'dart:developer'; -// TODO: https://github.com/dart-lang/webdev/issues/2508 -// ignore: deprecated_member_use -import 'dart:html'; import 'dart:js_interop'; @JS('console.log') external void log(String _); +// We use this to test whether a hot restart or a full reload occurred. In the +// former, we should see the old log, but in the latter, we should not. +@JS('\$previousLog') +external String? previousLog; + void main() { var count = 0; // For setting breakpoints. @@ -19,15 +21,17 @@ void main() { print('Count is: ${++count}'); // Breakpoint: printCount }); - document.body?.appendText('Hello World!'); + var logMessage = 'Hello World!'; + // Note that we concatenate instead of logging each one separately to avoid + // possibly mixing up logs with a previous call to `main`. + if (previousLog != null) logMessage = '$previousLog $logMessage'; + log(logMessage); + previousLog = logMessage; registerExtension('ext.flutter.disassemble', (_, __) async { - document.body?.appendText('start disassemble '); + log('start disassemble'); await Future.delayed(const Duration(seconds: 1)); - document.body?.appendText('end disassemble '); + log('end disassemble'); return ServiceExtensionResponse.result('{}'); }); - - // Wait for this print statement so that we know main is done executing. - log('main executed'); } diff --git a/fixtures/_test_hot_reload/web/main.dart b/fixtures/_test_hot_reload/web/main.dart index e1b1f02f8..a85a43af8 100644 --- a/fixtures/_test_hot_reload/web/main.dart +++ b/fixtures/_test_hot_reload/web/main.dart @@ -10,14 +10,8 @@ import 'package:_test_hot_reload/library1.dart'; @JS('console.log') external void log(String _); -@JS('document.body.innerHTML') -external set innerHtml(String html); - void evaluate() { - innerHtml = 'Program is running!\n $reloadValue}\n'; - - // Wait for this print statement so that we know evaluate is done executing. - log('evaluate executed'); + log('$reloadValue'); } void main() { diff --git a/fixtures/_test_hot_restart2/web/main.dart b/fixtures/_test_hot_restart2/web/main.dart index 090ea673d..92d072a64 100644 --- a/fixtures/_test_hot_restart2/web/main.dart +++ b/fixtures/_test_hot_restart2/web/main.dart @@ -3,9 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:core'; -// TODO: https://github.com/dart-lang/webdev/issues/2508 -// ignore: deprecated_member_use -import 'dart:html'; +import 'dart:js_interop'; import 'package:_test_hot_restart1/library1.dart'; import 'package:_test_hot_restart2/library2.dart'; @@ -23,6 +21,9 @@ import 'package:_test_hot_restart2/library2.dart'; /// Constants in reloaded modules fail to compare with constants in stale /// constant containers, causing 'ConstantEqualityFailure's. +@JS('console.log') +external void log(String _); + class ConstObject { const ConstObject(); String get text => 'ConstObject(' @@ -30,7 +31,10 @@ class ConstObject { '${value1 == value2 ? 'ConstantEqualitySuccess' : 'ConstantEqualityFailure'})'; } +void printConst() { + log('${const ConstObject().text}'); +} + void main() { - document.body!.innerHtml = - 'Program is running!\n${const ConstObject().text}\n'; + printConst(); } From 4f01268132771d85f97de9c884787bc532bf878c Mon Sep 17 00:00:00 2001 From: Srujan Gaddam Date: Tue, 19 Aug 2025 20:07:18 -0700 Subject: [PATCH 3/4] Bump to 25.1.0-wip --- 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 2add9e7f8..be004348e 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,3 +1,5 @@ +## 25.1.0-wip + ## 25.0.1 ### Bug Fixes: diff --git a/dwds/lib/src/version.dart b/dwds/lib/src/version.dart index fa092f758..9c88b04dc 100644 --- a/dwds/lib/src/version.dart +++ b/dwds/lib/src/version.dart @@ -1,2 +1,2 @@ // Generated code. Do not modify. -const packageVersion = '25.0.1'; +const packageVersion = '25.1.0-wip'; diff --git a/dwds/pubspec.yaml b/dwds/pubspec.yaml index 3b9e52480..f45a9267c 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: 25.0.1 +version: 25.1.0-wip description: >- A service that proxies between the Chrome debug protocol and the Dart VM From 59998a0a89653a2d3643c6da07d4ba215565b0b8 Mon Sep 17 00:00:00 2001 From: Srujan Gaddam Date: Fri, 22 Aug 2025 14:25:33 -0700 Subject: [PATCH 4/4] Review comments --- dwds/test/common/hot_restart_common.dart | 71 ++++++++-------- .../hot_restart_correctness_common.dart | 29 ++++--- dwds/test/fixtures/context.dart | 6 +- dwds/test/hot_reload_breakpoints_test.dart | 82 ++++++++++++------- dwds/test/hot_reload_test.dart | 23 ++++-- dwds/test/hot_restart_breakpoints_test.dart | 34 ++++---- 6 files changed, 144 insertions(+), 101 deletions(-) diff --git a/dwds/test/common/hot_restart_common.dart b/dwds/test/common/hot_restart_common.dart index 50ce08eed..de546ba08 100644 --- a/dwds/test/common/hot_restart_common.dart +++ b/dwds/test/common/hot_restart_common.dart @@ -57,8 +57,8 @@ void runTests({ await recompile(hasEdits: true); } - /// Wait for `expectedStrings` to be printed in the console. - Future expectLogs(List expectedStrings) async { + // Wait for `expectedStrings` to be printed to the console. + Future waitForLogs(List expectedStrings) async { final expectations = List.from(expectedStrings); final completer = Completer(); final subscription = context.webkitDebugger.onConsoleAPICalled.listen((e) { @@ -70,7 +70,14 @@ void runTests({ } } }); - await completer.future; + await completer.future.timeout( + const Duration(minutes: 1), + onTimeout: () { + throw TimeoutException( + 'Failed to find logs: $expectedStrings in console.', + ); + }, + ); await subscription.cancel(); } @@ -96,9 +103,9 @@ void runTests({ test('can live reload changes ', () async { // A full reload should clear the state. - final logExpectation = expectLogs([newString]); + final logFuture = waitForLogs([newString]); await makeEditAndRecompile(); - await logExpectation; + await logFuture; }); }); @@ -124,9 +131,9 @@ void runTests({ test('can live reload changes ', () async { // A full reload should clear the state. - final logExpectation = expectLogs([newString]); + final logFuture = waitForLogs([newString]); await makeEditAndRecompile(); - await logExpectation; + await logFuture; }); }); @@ -153,9 +160,9 @@ void runTests({ test('can live reload changes ', () async { // A full reload should clear the state. - final logExpectation = expectLogs([newString]); + final logFuture = waitForLogs([newString]); await makeEditAndRecompile(); - await logExpectation; + await logFuture; }); }); }, @@ -292,7 +299,7 @@ void runTests({ ), ); // Main is re-invoked which shouldn't clear the state. - final logExpectation = expectLogs(['$originalString $newString']); + final logFuture = waitForLogs(['$originalString $newString']); final hotRestart = context.getRegisteredServiceExtension('hotRestart'); expect( await fakeClient.callServiceExtension(hotRestart!), @@ -300,7 +307,7 @@ void runTests({ ); await eventsDone; - await logExpectation; + await logFuture; }); test('can send events before and after hot restart', () async { @@ -334,7 +341,7 @@ void runTests({ await recompile(); // Main is re-invoked which shouldn't clear the state. - final logExpectation = expectLogs(['$originalString $originalString']); + final logFuture = waitForLogs(['$originalString $originalString']); final hotRestart = context.getRegisteredServiceExtension('hotRestart'); expect( await fakeClient.callServiceExtension(hotRestart!), @@ -353,7 +360,7 @@ void runTests({ ); await eventsDone; - await logExpectation; + await logFuture; }); test('can refresh the page via the fullReload service extension', () async { @@ -372,7 +379,7 @@ void runTests({ ), ); // Should see only the new text. - final logExpectation = expectLogs([newString]); + final logFuture = waitForLogs([newString]); final fullReload = context.getRegisteredServiceExtension('fullReload'); expect( await fakeClient.callServiceExtension(fullReload!), @@ -380,7 +387,7 @@ void runTests({ ); await eventsDone; - await logExpectation; + await logFuture; }); test('can hot restart while paused', () async { @@ -405,10 +412,10 @@ void runTests({ await makeEditAndRecompile(); // Main is re-invoked which shouldn't clear the state. - final logExpectation = expectLogs(['$originalString $newString']); + final logFuture = waitForLogs(['$originalString $newString']); final hotRestart = context.getRegisteredServiceExtension('hotRestart'); await fakeClient.callServiceExtension(hotRestart!); - await logExpectation; + await logFuture; vm = await client.getVM(); isolateId = vm.isolates!.first.id!; @@ -445,27 +452,25 @@ void runTests({ test('can hot restart with no changes, hot restart with changes, and ' 'hot restart again with no changes', () async { // Empty hot restart. - var logExpectation = expectLogs(['$originalString $originalString']); + var logFuture = waitForLogs(['$originalString $originalString']); await recompile(); final hotRestart = context.getRegisteredServiceExtension('hotRestart'); await fakeClient.callServiceExtension(hotRestart!); - await logExpectation; + await logFuture; // Hot restart. - logExpectation = expectLogs([ - '$originalString $originalString $newString', - ]); + logFuture = waitForLogs(['$originalString $originalString $newString']); await makeEditAndRecompile(); await fakeClient.callServiceExtension(hotRestart); - await logExpectation; + await logFuture; // Empty hot restart. - logExpectation = expectLogs([ + logFuture = waitForLogs([ '$originalString $originalString $newString $newString', ]); await recompile(); await fakeClient.callServiceExtension(hotRestart); - await logExpectation; + await logFuture; }); }, timeout: Timeout.factor(2)); @@ -491,14 +496,14 @@ void runTests({ test('can hot restart changes ', () async { // Main is re-invoked which shouldn't clear the state. - final logExpectations = expectLogs([ + final logFutures = waitForLogs([ '$originalString $newString', // The ext.flutter.disassemble callback is invoked and waited for. 'start disassemble', 'end disassemble', ]); await makeEditAndRecompile(); - await logExpectations; + await logFutures; }); test( @@ -547,14 +552,14 @@ void runTests({ test('can hot restart changes ', () async { // Main is re-invoked which shouldn't clear the state. - final logExpectations = expectLogs([ + final logFutures = waitForLogs([ '$originalString $newString', // The ext.flutter.disassemble callback is invoked and waited for. 'start disassemble', 'end disassemble', ]); await makeEditAndRecompile(); - await logExpectations; + await logFutures; }); }); }, @@ -604,7 +609,7 @@ void runTests({ ); // Main is re-invoked which shouldn't clear the state. - final logExpectation = expectLogs(['$originalString $newString']); + final logFuture = waitForLogs(['$originalString $newString']); final hotRestart = context.getRegisteredServiceExtension('hotRestart'); expect( await fakeClient.callServiceExtension(hotRestart!), @@ -617,14 +622,14 @@ void runTests({ final isolateId = vm.isolates!.first.id!; await client.resume(isolateId); - await logExpectation; + await logFuture; }, ); test( 'after page refresh, does not run app until there is a resume event', () async { - final logExpectation = expectLogs([newString]); + final logFuture = waitForLogs([newString]); await makeEditAndRecompile(); await context.webDriver.driver.refresh(); @@ -645,7 +650,7 @@ void runTests({ final isolateId = vm.isolates!.first.id!; await client.resume(isolateId); - await logExpectation; + await logFuture; }, ); }); diff --git a/dwds/test/common/hot_restart_correctness_common.dart b/dwds/test/common/hot_restart_correctness_common.dart index 302be62dc..36ac8e942 100644 --- a/dwds/test/common/hot_restart_correctness_common.dart +++ b/dwds/test/common/hot_restart_correctness_common.dart @@ -54,15 +54,22 @@ void runTests({ } } - /// Wait for `expectedString` to be printed in the console. - Future expectLog(String expectedString) async { + // Wait for `expectedString` to be printed to the console. + Future waitForLog(String expectedString) async { final completer = Completer(); final subscription = context.webkitDebugger.onConsoleAPICalled.listen((e) { if (e.args.first.value == expectedString) { completer.complete(); } }); - await completer.future; + await completer.future.timeout( + const Duration(minutes: 1), + onTimeout: () { + throw TimeoutException( + "Failed to find log: '$expectedString' in console.", + ); + }, + ); await subscription.cancel(); } @@ -90,14 +97,14 @@ void runTests({ test('initial state prints the right log', () async { final client = context.debugConnection.vmService; - final logExpectation = expectLog( + final logFuture = waitForLog( 'ConstObject(reloadVariable: 23, ConstantEqualitySuccess)', ); final vm = await client.getVM(); final isolate = await client.getIsolate(vm.isolates!.first.id!); final rootLib = isolate.rootLib; await client.evaluate(isolate.id!, rootLib!.id!, 'printConst()'); - await logExpectation; + await logFuture; }); test( @@ -119,7 +126,7 @@ void runTests({ ), ); - final logExpectation = expectLog( + final logFuture = waitForLog( 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', ); final hotRestart = context.getRegisteredServiceExtension('hotRestart'); @@ -129,7 +136,7 @@ void runTests({ ); await eventsDone; - await logExpectation; + await logFuture; }, ); }, timeout: Timeout.factor(2)); @@ -155,11 +162,11 @@ void runTests({ }); test('properly compares constants after hot restart', () async { - final logExpectation = expectLog( + final logFuture = waitForLog( 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', ); await makeEditAndRecompile(); - await logExpectation; + await logFuture; }); }); @@ -184,11 +191,11 @@ void runTests({ }); test('properly compares constants after hot restart', () async { - final logExpectation = expectLog( + final logFuture = waitForLog( 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', ); await makeEditAndRecompile(); - await logExpectation; + await logFuture; }); }); }, diff --git a/dwds/test/fixtures/context.dart b/dwds/test/fixtures/context.dart index ead0c53f5..e515f277a 100644 --- a/dwds/test/fixtures/context.dart +++ b/dwds/test/fixtures/context.dart @@ -572,9 +572,7 @@ class TestContext { // TODO(srujzs): It's possible we may want a library file with the same name // as the entry file, but this function doesn't allow that. Potentially // support that. - Future makeEdits( - List<({String file, String originalString, String newString})> edits, - ) async { + Future makeEdits(List edits) async { // `dart:io`'s `stat` on Windows does not have millisecond precision so we // need to make sure we wait long enough that modifications result in a // timestamp that is guaranteed to be after the previous compile. @@ -686,3 +684,5 @@ class TestContext { return lineNumber + 1; } } + +typedef Edit = ({String file, String originalString, String newString}); diff --git a/dwds/test/hot_reload_breakpoints_test.dart b/dwds/test/hot_reload_breakpoints_test.dart index b71e23bd8..7bcb781d3 100644 --- a/dwds/test/hot_reload_breakpoints_test.dart +++ b/dwds/test/hot_reload_breakpoints_test.dart @@ -35,9 +35,7 @@ void main() { tearDownAll(provider.dispose); - Future makeEditsAndRecompile( - List<({String file, String originalString, String newString})> edits, - ) async { + Future makeEditsAndRecompile(List edits) async { await context.makeEdits(edits); await context.recompile(fullRestart: true); } @@ -111,9 +109,9 @@ void main() { await client.resume(isolate.id!); } - // When the program is executing, we want to check that at some point it - // will execute code that will emit [expectedString]. - Future resumeAndExpectLog(String expectedString) async { + // Resume the program, and check that at some point it will execute code + // that will print `expectedString` to the console. + Future resumeAndWaitForLog(String expectedString) async { final completer = Completer(); final subscription = context.webkitDebugger.onConsoleAPICalled.listen(( e, @@ -123,7 +121,14 @@ void main() { } }); await resume(); - await completer.future; + await completer.future.timeout( + const Duration(minutes: 1), + onTimeout: () { + throw TimeoutException( + "Failed to find log: '$expectedString' in console.", + ); + }, + ); await subscription.cancel(); } @@ -167,9 +172,9 @@ void main() { await client.evaluate(isolate.id!, rootLib!.id!, 'evaluate()'); } - // Much like `resumeAndExpectLog`, we need a completer to ensure the log - // will eventually occur when code is executing. - Future callEvaluateAndExpectLog(String expectedString) async { + // Call the method `evaluate` in the program and wait for `expectedString` + // to be printed to the console. + Future callEvaluateAndWaitForLog(String expectedString) async { final completer = Completer(); final subscription = context.webkitDebugger.onConsoleAPICalled.listen(( e, @@ -182,7 +187,14 @@ void main() { final isolate = await client.getIsolate(vm.isolates!.first.id!); final rootLib = isolate.rootLib; await client.evaluate(isolate.id!, rootLib!.id!, 'evaluate()'); - await completer.future; + await completer.future.timeout( + const Duration(minutes: 1), + onTimeout: () { + throw TimeoutException( + "Failed to find log: '$expectedString' in console.", + ); + }, + ); await subscription.cancel(); } @@ -203,7 +215,7 @@ void main() { // Should break at `callLog`. await breakpointFuture; - await resumeAndExpectLog(genString); + await resumeAndWaitForLog(genString); await context.recompile(fullRestart: false); @@ -217,7 +229,7 @@ void main() { // Should break at `callLog`. await breakpointFuture; - await resumeAndExpectLog(genString); + await resumeAndWaitForLog(genString); }); test('after edit and hot reload, breakpoint is in new file', () async { @@ -235,7 +247,7 @@ void main() { // Should break at `callLog`. await breakpointFuture; - await resumeAndExpectLog(oldString); + await resumeAndWaitForLog(oldString); // Modify the string that gets printed. await makeEditsAndRecompile([ @@ -252,7 +264,7 @@ void main() { // Should break at `callLog`. await breakpointFuture; - await resumeAndExpectLog(newString); + await resumeAndWaitForLog(newString); }); test('after adding line, hot reload, removing line, and hot reload, ' @@ -270,7 +282,7 @@ void main() { // Should break at `callLog`. await breakpointFuture; - await resumeAndExpectLog(genLog); + await resumeAndWaitForLog(genLog); // Add an extra log before the existing log. final extraLog = 'hot reload'; @@ -287,11 +299,11 @@ void main() { breakpointFuture = waitForBreakpoint(); - await callEvaluateAndExpectLog(extraLog); + await callEvaluateAndWaitForLog(extraLog); // Should break at `callLog`. await breakpointFuture; - await resumeAndExpectLog(genLog); + await resumeAndWaitForLog(genLog); // Remove the line we just added. await makeEditsAndRecompile([ @@ -315,7 +327,7 @@ void main() { // Should break at `callLog`. await breakpointFuture; expect(consoleLogs.contains(extraLog), false); - await resumeAndExpectLog(genLog); + await resumeAndWaitForLog(genLog); await consoleSubscription.cancel(); }); @@ -336,7 +348,7 @@ void main() { // Should break at `callLog`. await breakpointFuture; - await resumeAndExpectLog(genLog); + await resumeAndWaitForLog(genLog); // Add a library file, import it, and then refer to it in the log. final libFile = 'library.dart'; @@ -377,7 +389,7 @@ void main() { await resume(); // Should break at `libValue`. await breakpointFuture; - await resumeAndExpectLog(libGenLog); + await resumeAndWaitForLog(libGenLog); }, ); @@ -398,12 +410,11 @@ void main() { // Should break at `callLog`. await breakpointFuture; - await resumeAndExpectLog(genLog); + await resumeAndWaitForLog(genLog); // Add library files, import them, but only refer to the last one in main. final numFiles = 50; - final edits = - <({String file, String originalString, String newString})>[]; + final edits = []; for (var i = 1; i <= numFiles; i++) { final libFile = 'library$i.dart'; context.addLibraryFile( @@ -448,7 +459,7 @@ void main() { await resume(); // Should break at the breakpoint in the last file. await breakpointFuture; - await resumeAndExpectLog('library$numFiles gen1'); + await resumeAndWaitForLog('library$numFiles gen1'); }); test('breakpoint in captured code is deleted', () async { @@ -476,7 +487,7 @@ void main() { await breakpointFuture; final oldCapturedString = 'captured closure gen0'; // Closure gets evaluated for the first time. - await resumeAndExpectLog(oldCapturedString); + await resumeAndWaitForLog(oldCapturedString); final newCapturedString = 'captured closure gen1'; await makeEditsAndRecompile([ @@ -493,7 +504,7 @@ void main() { // Breakpoint should not be hit as it's now deleted. We should also see // the old string still as the closure has not been reevaluated. - await callEvaluateAndExpectLog(oldCapturedString); + await callEvaluateAndWaitForLog(oldCapturedString); }); }, timeout: Timeout.factor(2)); @@ -518,7 +529,9 @@ void main() { await context.tearDown(); }); - Future callEvaluateAndExpectLog(String expectedString) async { + // Call the method `evaluate` in the program and wait for `expectedString` + // to be printed to the console. + Future callEvaluateAndWaitForLog(String expectedString) async { final completer = Completer(); final subscription = context.webkitDebugger.onConsoleAPICalled.listen(( e, @@ -531,7 +544,14 @@ void main() { final isolate = await client.getIsolate(vm.isolates!.first.id!); final rootLib = isolate.rootLib; await client.evaluate(isolate.id!, rootLib!.id!, 'evaluate()'); - await completer.future; + await completer.future.timeout( + const Duration(minutes: 1), + onTimeout: () { + throw TimeoutException( + "Failed to find log: '$expectedString' in console.", + ); + }, + ); await subscription.cancel(); } @@ -539,7 +559,7 @@ void main() { final oldString = 'main gen0'; final newString = 'main gen1'; - await callEvaluateAndExpectLog(oldString); + await callEvaluateAndWaitForLog(oldString); // Modify the string that gets printed and hot reload. await makeEditsAndRecompile([ @@ -551,7 +571,7 @@ void main() { expect(report.success, true); // Program should not be paused, so this should execute. - await callEvaluateAndExpectLog(newString); + await callEvaluateAndWaitForLog(newString); }); }, timeout: Timeout.factor(2)); } diff --git a/dwds/test/hot_reload_test.dart b/dwds/test/hot_reload_test.dart index fafcbe749..41080f79a 100644 --- a/dwds/test/hot_reload_test.dart +++ b/dwds/test/hot_reload_test.dart @@ -50,9 +50,9 @@ void main() { await recompile(); } - // Call `evaluate` through an expression evaluation and wait for - // `expectedString` to be printed. - Future callEvaluateAndExpectLog(String expectedString) async { + // Call the method `evaluate` in the program and wait for `expectedString` to + // be printed to the console. + Future callEvaluateAndWaitForLog(String expectedString) async { final client = context.debugConnection.vmService; final completer = Completer(); final subscription = context.webkitDebugger.onConsoleAPICalled.listen((e) { @@ -64,7 +64,14 @@ void main() { final isolate = await client.getIsolate(vm.isolates!.first.id!); final rootLib = isolate.rootLib; await client.evaluate(isolate.id!, rootLib!.id!, 'evaluate()'); - await completer.future; + await completer.future.timeout( + const Duration(minutes: 1), + onTimeout: () { + throw TimeoutException( + "Failed to find log: '$expectedString' in console.", + ); + }, + ); await subscription.cancel(); } @@ -97,7 +104,7 @@ void main() { final report = await fakeClient.reloadSources(isolate.id!); expect(report.success, true); - await callEvaluateAndExpectLog(newString); + await callEvaluateAndWaitForLog(newString); }); test('can hot reload with no changes, hot reload with changes, and ' @@ -111,21 +118,21 @@ void main() { var report = await fakeClient.reloadSources(isolate.id!); expect(report.success, true); - await callEvaluateAndExpectLog(originalString); + await callEvaluateAndWaitForLog(originalString); // Hot reload. await makeEditAndRecompile(); report = await fakeClient.reloadSources(isolate.id!); expect(report.success, true); - await callEvaluateAndExpectLog(newString); + await callEvaluateAndWaitForLog(newString); // Empty hot reload. await recompile(); report = await fakeClient.reloadSources(isolate.id!); expect(report.success, true); - await callEvaluateAndExpectLog(newString); + await callEvaluateAndWaitForLog(newString); }); }, timeout: Timeout.factor(2)); } diff --git a/dwds/test/hot_restart_breakpoints_test.dart b/dwds/test/hot_restart_breakpoints_test.dart index 349af9272..1a83b5f67 100644 --- a/dwds/test/hot_restart_breakpoints_test.dart +++ b/dwds/test/hot_restart_breakpoints_test.dart @@ -36,9 +36,7 @@ void main() { tearDownAll(provider.dispose); - Future makeEditsAndRecompile( - List<({String file, String originalString, String newString})> edits, - ) async { + Future makeEditsAndRecompile(List edits) async { await context.makeEdits(edits); await context.recompile(fullRestart: true); } @@ -112,9 +110,9 @@ void main() { await client.resume(isolate.id!); } - // When the program is executing, we want to check that at some point it - // will execute code that will emit [expectedString]. - Future resumeAndExpectLog(String expectedString) async { + // Resume the program, and check that at some point it will execute code + // that will print `expectedString` to the console. + Future resumeAndWaitForLog(String expectedString) async { final completer = Completer(); final subscription = context.webkitDebugger.onConsoleAPICalled.listen(( e, @@ -124,7 +122,14 @@ void main() { } }); await resume(); - await completer.future; + await completer.future.timeout( + const Duration(minutes: 1), + onTimeout: () { + throw TimeoutException( + "Failed to find log: '$expectedString' in console.", + ); + }, + ); await subscription.cancel(); } @@ -189,7 +194,7 @@ void main() { // Should break at `callLog`. await breakpointFuture; - await resumeAndExpectLog(genString); + await resumeAndWaitForLog(genString); }); test('after edit and hot restart, breakpoint is in new file', () async { @@ -211,7 +216,7 @@ void main() { // Should break at `callLog`. await breakpointFuture; expect(consoleLogs.contains(newLog), false); - await resumeAndExpectLog(newLog); + await resumeAndWaitForLog(newLog); }); test('after adding line, hot restart, removing line, and hot restart, ' @@ -238,7 +243,7 @@ void main() { await breakpointFuture; expect(consoleLogs.contains(extraLog), true); expect(consoleLogs.contains(genLog), false); - await resumeAndExpectLog(genLog); + await resumeAndWaitForLog(genLog); consoleLogs.clear(); @@ -257,7 +262,7 @@ void main() { await breakpointFuture; expect(consoleLogs.contains(extraLog), false); expect(consoleLogs.contains(genLog), false); - await resumeAndExpectLog(genLog); + await resumeAndWaitForLog(genLog); }); test( @@ -307,7 +312,7 @@ void main() { // Should break at `libValue`. await breakpointFuture; expect(consoleLogs.contains(libGenLog), false); - await resumeAndExpectLog(libGenLog); + await resumeAndWaitForLog(libGenLog); }, ); @@ -321,8 +326,7 @@ void main() { // Add library files, import them, but only refer to the last one in main. final numFiles = 50; - final edits = - <({String file, String originalString, String newString})>[]; + final edits = []; for (var i = 1; i <= numFiles; i++) { final libFile = 'library$i.dart'; context.addLibraryFile( @@ -365,7 +369,7 @@ void main() { // Should break at the breakpoint in the last file. await breakpointFuture; expect(consoleLogs.contains(newGenLog), false); - await resumeAndExpectLog(newGenLog); + await resumeAndWaitForLog(newGenLog); }); }); }