From c60669670db1931af2efca79ed83d3344ce64ac8 Mon Sep 17 00:00:00 2001 From: MarkZ Date: Mon, 18 Dec 2023 14:28:36 -0800 Subject: [PATCH] Keeping the first instance of an app connection and extending timeouts --- dwds/test/devtools_test.dart | 196 ++-- dwds/test/fixtures/context.dart | 12 +- dwds/test/reload_test.dart | 835 +++++++++--------- dwds/test/run_request_test.dart | 2 +- .../test/test_sdk_configuration_test.dart | 46 +- 5 files changed, 577 insertions(+), 514 deletions(-) diff --git a/dwds/test/devtools_test.dart b/dwds/test/devtools_test.dart index b7e1ddf1b..8e9fb4868 100644 --- a/dwds/test/devtools_test.dart +++ b/dwds/test/devtools_test.dart @@ -32,110 +32,115 @@ void main() { final context = TestContext(TestProject.testWithSoundNullSafety, provider); - group('Injected client', () { - setUp(() async { - await context.setUp( - debugSettings: TestDebugSettings.withDevTools(context), - ); - await context.webDriver.driver.keyboard.sendChord([Keyboard.alt, 'd']); - // Wait for DevTools to actually open. - await Future.delayed(const Duration(seconds: 2)); - }); - - tearDown(() async { - await context.tearDown(); - }); - - test( - 'can launch devtools', - () async { - final windows = await context.webDriver.windows.toList(); - await context.webDriver.driver.switchTo.window(windows.last); - expect(await context.webDriver.pageSource, contains('DevTools')); - expect(await context.webDriver.currentUrl, contains('ide=Dwds')); - // TODO(https://github.com/dart-lang/webdev/issues/1888): Re-enable. - }, - skip: Platform.isWindows, - ); - - test('can not launch devtools for the same app in multiple tabs', () async { - final appUrl = await context.webDriver.currentUrl; - // Open a new tab, select it, and navigate to the app - await context.webDriver.driver - .execute("window.open('$appUrl', '_blank');", []); - await Future.delayed(const Duration(seconds: 2)); - final newAppWindow = await context.webDriver.windows.last; - await newAppWindow.setAsActive(); + group( + 'Injected client', + () { + setUp(() async { + await context.setUp( + debugSettings: TestDebugSettings.withDevTools(context), + ); + await context.webDriver.driver.keyboard.sendChord([Keyboard.alt, 'd']); + // Wait for DevTools to actually open. + await Future.delayed(const Duration(seconds: 2)); + }); - // Wait for the page to be ready before trying to open DevTools again. - await _waitForPageReady(context); + tearDown(() async { + await context.tearDown(); + }); - // Try to open devtools and check for the alert. - await context.webDriver.driver.keyboard.sendChord([Keyboard.alt, 'd']); - await Future.delayed(const Duration(seconds: 2)); - final alert = context.webDriver.driver.switchTo.alert; - expect(alert, isNotNull); - expect( - await alert.text, - contains('This app is already being debugged in a different tab'), + test( + 'can launch devtools', + () async { + final windows = await context.webDriver.windows.toList(); + await context.webDriver.driver.switchTo.window(windows.last); + expect(await context.webDriver.pageSource, contains('DevTools')); + expect(await context.webDriver.currentUrl, contains('ide=Dwds')); + // TODO(https://github.com/dart-lang/webdev/issues/1888): Re-enable. + }, + skip: Platform.isWindows, ); - await alert.accept(); - var windows = await context.webDriver.windows.toList(); - for (final window in windows) { - if (window.id != newAppWindow.id) { - await window.setAsActive(); - await window.close(); - } - } + test('can not launch devtools for the same app in multiple tabs', + () async { + final appUrl = await context.webDriver.currentUrl; + // Open a new tab, select it, and navigate to the app + await context.webDriver.driver + .execute("window.open('$appUrl', '_blank');", []); + await Future.delayed(const Duration(seconds: 2)); + final newAppWindow = await context.webDriver.windows.last; + await newAppWindow.setAsActive(); - await newAppWindow.setAsActive(); - await context.webDriver.driver.keyboard.sendChord([Keyboard.alt, 'd']); - await Future.delayed(const Duration(seconds: 2)); - windows = await context.webDriver.windows.toList(); - final devToolsWindow = - windows.firstWhere((window) => window != newAppWindow); - await devToolsWindow.setAsActive(); - expect(await context.webDriver.pageSource, contains('DevTools')); - }); + // Wait for the page to be ready before trying to open DevTools again. + await _waitForPageReady(context); - test( - 'destroys and recreates the isolate during a page refresh', - () async { - // This test is the same as one in reload_test, but runs here when there - // is a connected client (DevTools) since it can behave differently. - // 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.waitForSuccessfulBuild(propagateToBrowser: true); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), + // Try to open devtools and check for the alert. + await context.webDriver.driver.keyboard.sendChord([Keyboard.alt, 'd']); + await Future.delayed(const Duration(seconds: 2)); + final alert = context.webDriver.driver.switchTo.alert; + expect(alert, isNotNull); + expect( + await alert.text, + contains('This app is already being debugged in a different tab'), ); + await alert.accept(); - await context.webDriver.driver.refresh(); + var windows = await context.webDriver.windows.toList(); + for (final window in windows) { + if (window.id != newAppWindow.id) { + await window.setAsActive(); + await window.close(); + } + } - await eventsDone; - // Re-set the edited file: - context.makeEditToDartEntryFile( - toReplace: 'Bonjour le monde!', - replaceWith: 'Hello World!', - ); - }, - skip: 'https://github.com/dart-lang/webdev/issues/1888', - ); - }); + await newAppWindow.setAsActive(); + await context.webDriver.driver.keyboard.sendChord([Keyboard.alt, 'd']); + await Future.delayed(const Duration(seconds: 2)); + windows = await context.webDriver.windows.toList(); + final devToolsWindow = + windows.firstWhere((window) => window != newAppWindow); + await devToolsWindow.setAsActive(); + expect(await context.webDriver.pageSource, contains('DevTools')); + }); + + test( + 'destroys and recreates the isolate during a page refresh', + () async { + // This test is the same as one in reload_test, but runs here when there + // is a connected client (DevTools) since it can behave differently. + // 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.waitForSuccessfulBuild(propagateToBrowser: true); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + await context.webDriver.driver.refresh(); + + await eventsDone; + // Re-set the edited file: + context.makeEditToDartEntryFile( + toReplace: 'Bonjour le monde!', + replaceWith: 'Hello World!', + ); + }, + skip: 'https://github.com/dart-lang/webdev/issues/1888', + ); + }, + timeout: Timeout.factor(2), + ); group('Injected client without a DevTools server', () { setUp(() async { @@ -193,6 +198,7 @@ void main() { }, tags: ['extension'], skip: 'https://github.com/dart-lang/webdev/issues/2114', + timeout: Timeout.factor(2), ); } diff --git a/dwds/test/fixtures/context.dart b/dwds/test/fixtures/context.dart index f2adf7634..34a45e76e 100644 --- a/dwds/test/fixtures/context.dart +++ b/dwds/test/fixtures/context.dart @@ -420,8 +420,13 @@ class TestContext { if (testSettings.autoRun) { connection.runMain(); } - appConnection = connection; - appConnectionCompleter.complete(); + + // We may reuse the app connection, so only save it the first time + // it's encountered. + if (!appConnectionCompleter.isCompleted) { + appConnection = connection; + appConnectionCompleter.complete(); + } }); _appUrl = basePath.isEmpty @@ -451,6 +456,9 @@ class TestContext { if (debugSettings.enableDebugging && !testSettings.waitToDebug) { await startDebugging(); } + } else { + // No tab needs to be dicovered, so fulfill the relevant completer. + tabConnectionCompleter.complete(); } } catch (e, s) { _logger.severe('Failed to setup the service, $e:$s'); diff --git a/dwds/test/reload_test.dart b/dwds/test/reload_test.dart index 6c425eaff..8a0d8eb10 100644 --- a/dwds/test/reload_test.dart +++ b/dwds/test/reload_test.dart @@ -42,438 +42,290 @@ void main() { ); } - group('Injected client with live reload', () { - group('and with debugging', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.liveReload, - ), - ); + 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); + }); }); - tearDown(() async { - undoEdit(); - await context.tearDown(); + 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); + }); }); - 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('and without debugging', () { + group( + 'Injected client', + () { setUp(() async { setCurrentLogWriter(debug: debug); await context.setUp( testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.liveReload, - ), - debugSettings: TestDebugSettings.noDevTools().copyWith( - enableDebugging: false, + enableExpressionEvaluation: true, ), ); }); tearDown(() async { - undoEdit(); await context.tearDown(); + undoEdit(); }); - test('can live reload changes ', () async { + test('destroys and recreates the isolate during a hot restart', () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); await makeEditAndWaitForRebuild(); - final source = await context.webDriver.pageSource; + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); - // A full reload should clear the state. - expect(source.contains(originalString), isFalse); - expect(source.contains(newString), isTrue); + expect( + await client.callServiceExtension('hotRestart'), + const TypeMatcher(), + ); + + await eventsDone; }); - }); - group('and without debugging using WebSockets', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.liveReload, + 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), + ]), ), - debugSettings: TestDebugSettings.noDevTools().copyWith( - enableDebugging: false, - useSse: false, + ); + + // Execute two hot restart calls in parallel. + final done = Future.wait([ + client.callServiceExtension('hotRestart'), + client.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', ), ); - }); - tearDown(() async { - await context.tearDown(); - undoEdit(); + await eventsDone; }); - test('can live reload changes ', () async { + test('destroys and recreates the isolate during a page refresh', + () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); 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('Injected client', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - enableExpressionEvaluation: true, - ), - ); - }); - - 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), - ]), - ), - ); - - expect( - await client.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 done = Future.wait([ - client.callServiceExtension('hotRestart'), - client.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), - ]), - ), - ); - - expect( - await client.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 String callback = - '(_, __) async => ServiceExtensionResponse.result("")'; - - await client.evaluate( - isolateId, - library, - "registerExtension('ext.foo', $callback)", - ); - - expect( - await client.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), - ]), - ), - ); - - expect(await client.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(); - await client.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 still exist. - expect(isolate.breakpoints!.isNotEmpty, isTrue); - final bp = isolate.breakpoints!.first; - - // Should pause eventually. - await stream - .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); - - expect( - await client.removeBreakpoint(isolate.id!, bp.id!), - isA(), - ); - expect(await client.resume(isolate.id!), isA()); - }); - - test('can evaluate expressions after hot restart ', () 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 client.callServiceExtension('hotRestart'); - - vm = await client.getVM(); - isolateId = vm.isolates!.first.id!; - final isolate = await client.getIsolate(isolateId); - final library = isolate.rootLib!.uri!; - final bp = isolate.breakpoints!.first; - - // Should pause eventually. - final event = await stream - .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); - - // Expression evaluation while paused on a breakpoint should work. - var result = await client.evaluateInFrame( - isolate.id!, - event.topFrame!.index!, - 'count', - ); - expect( - result, - isA().having( - (instance) => instance.valueAsString, - 'valueAsString', - greaterThanOrEqualTo('0'), - ), - ); - - await client.removeBreakpoint(isolateId, bp.id!); - await client.resume(isolateId); - - // Expression evaluation while running should work. - result = await client.evaluate(isolateId, library, 'true'); - expect( - result, - isA().having( - (instance) => instance.valueAsString, - 'valueAsString', - 'true', - ), - ); - }); - }); - - group('Injected client with hot restart', () { - group('and with debugging', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.hotRestart, + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), ), ); - }); - tearDown(() async { - await context.tearDown(); - undoEdit(); + await context.webDriver.driver.refresh(); + + await eventsDone; }); - test('can hot restart changes ', () async { + test('can hot restart via the service extension', () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); await makeEditAndWaitForRebuild(); - final source = await context.webDriver.pageSource; + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + expect( + await client.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), isTrue); - expect(source.contains(newString), isTrue); - // The ext.flutter.disassemble callback is invoked and waited for. + 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 String callback = + '(_, __) async => ServiceExtensionResponse.result("")'; + + await client.evaluate( + isolateId, + library, + "registerExtension('ext.foo', $callback)", + ); + expect( - source, - contains('start disassemble end disassemble $newString'), + await client.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('fires isolate create/destroy events during hot restart', () async { + 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, @@ -486,45 +338,208 @@ void main() { ), ); - await makeEditAndWaitForRebuild(); + expect(await client.callServiceExtension('fullReload'), isA()); 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), - ); + final source = await context.webDriver.pageSource; + // Should see only the new text + expect(source.contains(originalString), isFalse); + expect(source.contains(newString), isTrue); }); - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); + 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); - test('can hot restart changes ', () async { await makeEditAndWaitForRebuild(); - + await client.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); - // The ext.flutter.disassemble callback is invoked and waited for. + + vm = await client.getVM(); + isolateId = vm.isolates!.first.id!; + final isolate = await client.getIsolate(isolateId); + + // Previous breakpoint should still exist. + expect(isolate.breakpoints!.isNotEmpty, isTrue); + final bp = isolate.breakpoints!.first; + + // Should pause eventually. + await stream + .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); + expect( - source, - contains('start disassemble end disassemble $newString'), + await client.removeBreakpoint(isolate.id!, bp.id!), + isA(), ); + expect(await client.resume(isolate.id!), isA()); + }); + + test('can evaluate expressions after hot restart ', () 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 client.callServiceExtension('hotRestart'); + + vm = await client.getVM(); + isolateId = vm.isolates!.first.id!; + final isolate = await client.getIsolate(isolateId); + final library = isolate.rootLib!.uri!; + final bp = isolate.breakpoints!.first; + + // Should pause eventually. + final event = await stream + .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); + + // Expression evaluation while paused on a breakpoint should work. + var result = await client.evaluateInFrame( + isolate.id!, + event.topFrame!.index!, + 'count', + ); + expect( + result, + isA().having( + (instance) => instance.valueAsString, + 'valueAsString', + greaterThanOrEqualTo('0'), + ), + ); + + await client.removeBreakpoint(isolateId, bp.id!); + await client.resume(isolateId); + + // Expression evaluation while running should work. + 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), + ); } TypeMatcher _hasKind(String kind) => diff --git a/dwds/test/run_request_test.dart b/dwds/test/run_request_test.dart index 89db646a5..1ce04425b 100644 --- a/dwds/test/run_request_test.dart +++ b/dwds/test/run_request_test.dart @@ -68,7 +68,7 @@ void main() { await stream.firstWhere((event) => event.kind == EventKind.kResume); expect(isolate.pauseEvent!.kind, EventKind.kResume); }); - }); + }, timeout: Timeout.factor(2),); group('while debugger is not attached', () { setUp(() async { diff --git a/test_common/test/test_sdk_configuration_test.dart b/test_common/test/test_sdk_configuration_test.dart index 5b01d0613..2e5febef5 100644 --- a/test_common/test/test_sdk_configuration_test.dart +++ b/test_common/test/test_sdk_configuration_test.dart @@ -7,6 +7,7 @@ import 'dart:io'; +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'; @@ -40,7 +41,45 @@ void main() { }); }); - group('Test SDK configuration |', () { + group('Test SDK configuration | DDC with DDC modules |', () { + setCurrentLogWriter(debug: debug); + final provider = TestSdkConfigurationProvider( + verbose: debug, ddcModuleFormat: ModuleFormat.ddc); + tearDownAll(provider.dispose); + + test('Can validate configuration with generated assets', () async { + final sdkConfiguration = await provider.configuration; + sdkConfiguration.validateSdkDir(); + sdkConfiguration.validate(); + }); + + test('SDK layout exists', () async { + await provider.configuration; + final sdkLayout = provider.sdkLayout; + + expect(sdkLayout.sdkDirectory, _directoryExists); + expect(sdkLayout.soundDdcJsPath, _fileExists); + expect(sdkLayout.soundDdcJsMapPath, _fileExists); + expect(sdkLayout.soundSummaryPath, _fileExists); + expect(sdkLayout.soundFullDillPath, _fileExists); + + expect(sdkLayout.weakDdcJsPath, _fileExists); + expect(sdkLayout.weakDdcJsMapPath, _fileExists); + expect(sdkLayout.weakSummaryPath, _fileExists); + expect(sdkLayout.weakFullDillPath, _fileExists); + + expect(sdkLayout.ddcModuleLoaderJsPath, _fileExists); + expect(sdkLayout.stackTraceMapperPath, _fileExists); + + expect(sdkLayout.dartPath, _fileExists); + expect(sdkLayout.frontendServerSnapshotPath, _fileExists); + expect(sdkLayout.dartdevcSnapshotPath, _fileExists); + expect(sdkLayout.kernelWorkerSnapshotPath, _fileExists); + expect(sdkLayout.devToolsDirectory, _directoryExists); + }); + }); + + group('Test SDK configuration | DDC with AMD modules |', () { setCurrentLogWriter(debug: debug); final provider = TestSdkConfigurationProvider(verbose: debug); tearDownAll(provider.dispose); @@ -58,20 +97,15 @@ void main() { expect(sdkLayout.sdkDirectory, _directoryExists); expect(sdkLayout.soundAmdJsPath, _fileExists); expect(sdkLayout.soundAmdJsMapPath, _fileExists); - expect(sdkLayout.soundDdcJsPath, _fileExists); - expect(sdkLayout.soundDdcJsMapPath, _fileExists); expect(sdkLayout.soundSummaryPath, _fileExists); expect(sdkLayout.soundFullDillPath, _fileExists); expect(sdkLayout.weakAmdJsPath, _fileExists); expect(sdkLayout.weakAmdJsMapPath, _fileExists); - expect(sdkLayout.weakDdcJsPath, _fileExists); - expect(sdkLayout.weakDdcJsMapPath, _fileExists); expect(sdkLayout.weakSummaryPath, _fileExists); expect(sdkLayout.weakFullDillPath, _fileExists); expect(sdkLayout.requireJsPath, _fileExists); - expect(sdkLayout.ddcModuleLoaderJsPath, _fileExists); expect(sdkLayout.stackTraceMapperPath, _fileExists); expect(sdkLayout.dartPath, _fileExists);