diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index 9447f173b..f960e1bff 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -6,7 +6,11 @@ - Use default constant port for debug service. - If we fail binding to the port, fall back to previous strategy of finding unbound ports. -- Add metrics measuring DevTools Initial Page Load time. +- Add metrics measuring + - DevTools Initial Page Load time + - Various VM API + - Hot restart + - Http request handling exceptions - Add `ext.dwds.sendEvent` service extension to dwds so other tools can send events to the debugger. Event format: diff --git a/dwds/lib/dwds.dart b/dwds/lib/dwds.dart index 0a0c3fd9a..f3c3d58f3 100644 --- a/dwds/lib/dwds.dart +++ b/dwds/lib/dwds.dart @@ -86,7 +86,9 @@ class Dwds { Future debugConnection(AppConnection appConnection) async { if (!_enableDebugging) throw StateError('Debugging is not enabled.'); - var appDebugServices = await _devHandler.loadAppServices(appConnection); + final dwdsStats = DwdsStats(DateTime.now()); + var appDebugServices = + await _devHandler.loadAppServices(appConnection, dwdsStats); await appDebugServices.chromeProxyService.isInitialized; return DebugConnection(appDebugServices); } diff --git a/dwds/lib/src/dwds_vm_client.dart b/dwds/lib/src/dwds_vm_client.dart index 1c82609b6..8aaa165db 100644 --- a/dwds/lib/src/dwds_vm_client.dart +++ b/dwds/lib/src/dwds_vm_client.dart @@ -80,71 +80,19 @@ class DwdsVmClient { } }; }); - await client.registerService('_flutter.listViews', 'DWDS listViews'); - - client.registerServiceCallback('hotRestart', (request) async { - _logger.info('Attempting a hot restart'); - - chromeProxyService.terminatingIsolates = true; - await _disableBreakpointsAndResume(client, chromeProxyService); - int context; - try { - _logger.info('Attempting to get execution context ID.'); - context = await chromeProxyService.executionContext.id; - _logger.info('Got execution context ID.'); - } on StateError catch (e) { - // We couldn't find the execution context. `hotRestart` may have been - // triggered in the middle of a full reload. - return { - 'error': { - 'code': RPCError.kInternalError, - 'message': e.message, - } - }; - } - // Start listening for isolate create events before issuing a hot - // restart. Only return success after the isolate has fully started. - var stream = chromeProxyService.onEvent('Isolate'); - try { - _logger.info('Issuing \$dartHotRestart request.'); - await chromeProxyService.remoteDebugger - .sendCommand('Runtime.evaluate', params: { - 'expression': r'$dartHotRestart();', - 'awaitPromise': true, - 'contextId': context, - }); - _logger.info('\$dartHotRestart request complete.'); - } on WipError catch (exception) { - var code = exception.error['code']; - // This corresponds to `Execution context was destroyed` which can - // occur during a hot restart that must fall back to a full reload. - if (code != RPCError.kServerError) { - return { - 'error': { - 'code': exception.error['code'], - 'message': exception.error['message'], - 'data': exception, - } - }; - } - } + await client.registerService('_flutter.listViews', 'DWDS'); - _logger.info('Waiting for Isolate Start event.'); - await stream.firstWhere((event) => event.kind == EventKind.kIsolateStart); - chromeProxyService.terminatingIsolates = false; - - _logger.info('Successful hot restart'); - return {'result': Success().toJson()}; - }); - await client.registerService('hotRestart', 'DWDS fullReload'); + client.registerServiceCallback( + 'hotRestart', + (request) => captureElapsedTime( + () => _hotRestart(chromeProxyService, client), + (_) => DwdsEvent.hotRestart())); + await client.registerService('hotRestart', 'DWDS'); - client.registerServiceCallback('fullReload', (_) async { - _logger.info('Attempting a full reload'); - await chromeProxyService.remoteDebugger.enablePage(); - await chromeProxyService.remoteDebugger.pageReload(); - _logger.info('Successful full reload'); - return {'result': Success().toJson()}; - }); + client.registerServiceCallback( + 'fullReload', + (request) => captureElapsedTime(() => _fullReload(chromeProxyService), + (_) => DwdsEvent.fullReload())); await client.registerService('fullReload', 'DWDS'); client.registerServiceCallback('ext.dwds.screenshot', (_) async { @@ -205,6 +153,9 @@ void _processSendEvent(Map event, var action = payload == null ? null : payload['action']; if (screen == 'debugger' && action == 'pageReady') { if (dwdsStats.isFirstDebuggerReady()) { + emitEvent(DwdsEvent.devToolsLoad(DateTime.now() + .difference(dwdsStats.devToolsStart) + .inMilliseconds)); emitEvent(DwdsEvent.debuggerReady(DateTime.now() .difference(dwdsStats.debuggerStart) .inMilliseconds)); @@ -218,6 +169,71 @@ void _processSendEvent(Map event, } } +Future> _hotRestart( + ChromeProxyService chromeProxyService, VmService client) async { + _logger.info('Attempting a hot restart'); + + chromeProxyService.terminatingIsolates = true; + await _disableBreakpointsAndResume(client, chromeProxyService); + int context; + try { + _logger.info('Attempting to get execution context ID.'); + context = await chromeProxyService.executionContext.id; + _logger.info('Got execution context ID.'); + } on StateError catch (e) { + // We couldn't find the execution context. `hotRestart` may have been + // triggered in the middle of a full reload. + return { + 'error': { + 'code': RPCError.kInternalError, + 'message': e.message, + } + }; + } + // Start listening for isolate create events before issuing a hot + // restart. Only return success after the isolate has fully started. + var stream = chromeProxyService.onEvent('Isolate'); + try { + _logger.info('Issuing \$dartHotRestart request.'); + await chromeProxyService.remoteDebugger + .sendCommand('Runtime.evaluate', params: { + 'expression': r'$dartHotRestart();', + 'awaitPromise': true, + 'contextId': context, + }); + _logger.info('\$dartHotRestart request complete.'); + } on WipError catch (exception) { + var code = exception.error['code']; + // This corresponds to `Execution context was destroyed` which can + // occur during a hot restart that must fall back to a full reload. + if (code != RPCError.kServerError) { + return { + 'error': { + 'code': exception.error['code'], + 'message': exception.error['message'], + 'data': exception, + } + }; + } + } + + _logger.info('Waiting for Isolate Start event.'); + await stream.firstWhere((event) => event.kind == EventKind.kIsolateStart); + chromeProxyService.terminatingIsolates = false; + + _logger.info('Successful hot restart'); + return {'result': Success().toJson()}; +} + +Future> _fullReload( + ChromeProxyService chromeProxyService) async { + _logger.info('Attempting a full reload'); + await chromeProxyService.remoteDebugger.enablePage(); + await chromeProxyService.remoteDebugger.pageReload(); + _logger.info('Successful full reload'); + return {'result': Success().toJson()}; +} + Future _disableBreakpointsAndResume( VmService client, ChromeProxyService chromeProxyService) async { _logger.info('Attempting to disable breakpoints and resume the isolate'); diff --git a/dwds/lib/src/events.dart b/dwds/lib/src/events.dart index 36e5ad693..f7bb86d45 100644 --- a/dwds/lib/src/events.dart +++ b/dwds/lib/src/events.dart @@ -12,6 +12,9 @@ class DwdsStats { /// The time when the user starts the debugger. final DateTime debuggerStart; + /// The time when dwds launches DevTools. + DateTime devToolsStart; + var _isDebuggerReady = false; /// Records and returns whether the debugger became ready. @@ -28,13 +31,17 @@ class DwdsEventKind { static const String compilerUpdateDependencies = 'COMPILER_UPDATE_DEPENDENCIES'; static const String devtoolsLaunch = 'DEVTOOLS_LAUNCH'; + static const String devToolsLoad = 'DEVTOOLS_LOAD'; + static const String debuggerReady = 'DEBUGGER_READY'; static const String evaluate = 'EVALUATE'; static const String evaluateInFrame = 'EVALUATE_IN_FRAME'; + static const String fullReload = 'FULL_RELOAD'; static const String getIsolate = 'GET_ISOLATE'; static const String getScripts = 'GET_SCRIPTS'; static const String getSourceReport = 'GET_SOURCE_REPORT'; - static const String debuggerReady = 'DEBUGGER_READY'; static const String getVM = 'GET_VM'; + static const String hotRestart = 'HOT_RESTART'; + static const String httpRequestException = 'HTTP_REQUEST_EXCEPTION'; static const String resume = 'RESUME'; DwdsEventKind._(); @@ -77,11 +84,26 @@ class DwdsEvent { DwdsEvent.getSourceReport() : this(DwdsEventKind.getSourceReport, {}); + DwdsEvent.hotRestart() : this(DwdsEventKind.hotRestart, {}); + + DwdsEvent.fullReload() : this(DwdsEventKind.fullReload, {}); + DwdsEvent.debuggerReady(int elapsedMilliseconds) : this(DwdsEventKind.debuggerReady, { 'elapsedMilliseconds': elapsedMilliseconds, }); + DwdsEvent.devToolsLoad(int elapsedMilliseconds) + : this(DwdsEventKind.devToolsLoad, { + 'elapsedMilliseconds': elapsedMilliseconds, + }); + + DwdsEvent.httpRequestException(String server, String exception) + : this(DwdsEventKind.httpRequestException, { + 'server': server, + 'exception': exception, + }); + void addException(dynamic exception) { payload['exception'] = exception; } @@ -103,3 +125,24 @@ void emitEvent(DwdsEvent event) => _eventController.sink.add(event); /// A global stream of [DwdsEvent]s. Stream get eventStream => _eventController.stream; + +/// Call [function] and record its execution time. +/// +/// Calls [event] to create the event to be recorded, +/// and appends time and exception details to it if +/// available. +Future captureElapsedTime( + Future Function() function, DwdsEvent Function(T result) event) async { + var stopwatch = Stopwatch()..start(); + T result; + try { + return result = await function(); + } catch (e) { + emitEvent(event(result) + ..addException(e) + ..addElapsedTime(stopwatch.elapsedMilliseconds)); + rethrow; + } finally { + emitEvent(event(result)..addElapsedTime(stopwatch.elapsedMilliseconds)); + } +} diff --git a/dwds/lib/src/handlers/dev_handler.dart b/dwds/lib/src/handlers/dev_handler.dart index 39324bf72..922bf2331 100644 --- a/dwds/lib/src/handlers/dev_handler.dart +++ b/dwds/lib/src/handlers/dev_handler.dart @@ -221,8 +221,8 @@ class DevHandler { ); } - Future loadAppServices(AppConnection appConnection) async { - var dwdsStats = DwdsStats(DateTime.now()); + Future loadAppServices( + AppConnection appConnection, DwdsStats dwdsStats) async { var appId = appConnection.request.appId; if (_servicesByAppId[appId] == null) { var debugService = await _startLocalDebugService( @@ -321,9 +321,10 @@ class DevHandler { return; } + var dwdsStats = DwdsStats(DateTime.now()); AppDebugServices appServices; try { - appServices = await loadAppServices(appConnection); + appServices = await loadAppServices(appConnection, dwdsStats); } catch (_) { var error = 'Unable to connect debug services to your ' 'application. Most likely this means you are trying to ' @@ -364,6 +365,7 @@ class DevHandler { ..promptExtension = false)))); appServices.connectedInstanceId = appConnection.request.instanceId; + dwdsStats.devToolsStart = DateTime.now(); await _launchDevTools(appServices.chromeProxyService.remoteDebugger, appServices.debugService.uri); } @@ -511,6 +513,7 @@ class DevHandler { extensionDebugConnections.add(DebugConnection(appServices)); _servicesByAppId[appId] = appServices; } + dwdsStats.devToolsStart = DateTime.now(); await _launchDevTools(extensionDebugger, await _servicesByAppId[appId].debugService.encodedUri); }); diff --git a/dwds/lib/src/servers/extension_backend.dart b/dwds/lib/src/servers/extension_backend.dart index b6aa03a8b..85aba7668 100644 --- a/dwds/lib/src/servers/extension_backend.dart +++ b/dwds/lib/src/servers/extension_backend.dart @@ -8,12 +8,12 @@ import 'dart:async'; import 'dart:io'; import 'package:async/async.dart'; - import 'package:http_multi_server/http_multi_server.dart'; import 'package:logging/logging.dart'; import 'package:shelf/shelf.dart'; import '../../data/extension_request.dart'; +import '../events.dart'; import '../handlers/socket_connections.dart'; import '../utilities/shared.dart'; import 'extension_debugger.dart'; @@ -21,7 +21,7 @@ import 'extension_debugger.dart'; const authenticationResponse = 'Dart Debug Authentication Success!\n\n' 'You can close this tab and launch the Dart Debug Extension again.'; -Logger _logger = Logger('ExtensiobBackend'); +Logger _logger = Logger('ExtensionBackend'); /// A backend for the Dart Debug Extension. /// @@ -56,7 +56,8 @@ class ExtensionBackend { }).add(_socketHandler.handler); var server = await HttpMultiServer.bind(hostname, 0); serveHttpRequests(server, cascade.handler, (e, s) { - _logger.warning('Error serving requests', e, s); + _logger.warning('Error serving requests', e); + emitEvent(DwdsEvent.httpRequestException('ExtensionBackend', '$e:$s')); }); return ExtensionBackend._( _socketHandler, server.address.host, server.port, server); diff --git a/dwds/lib/src/services/chrome_proxy_service.dart b/dwds/lib/src/services/chrome_proxy_service.dart index 3a7165132..7f2231a40 100644 --- a/dwds/lib/src/services/chrome_proxy_service.dart +++ b/dwds/lib/src/services/chrome_proxy_service.dart @@ -184,7 +184,7 @@ class ChromeProxyService implements VmServiceInterface { moduleFormat: moduleFormat, soundNullSafety: soundNullSafety); var dependencies = await globalLoadStrategy.moduleInfoForEntrypoint(entrypoint); - await _captureElapsedTime(() async { + await captureElapsedTime(() async { var result = await _compiler.updateDependencies(dependencies); // Expression evaluation is ready after dependencies are updated. if (!_compilerCompleter.isCompleted) _compilerCompleter.complete(); @@ -422,7 +422,7 @@ ${globalLoadStrategy.loadModuleSnippet}("dart_sdk").developer.invokeExtension( bool disableBreakpoints, }) async { // TODO(798) - respect disableBreakpoints. - return _captureElapsedTime(() async { + return captureElapsedTime(() async { await isInitialized; if (_expressionEvaluator != null) { await isCompilerInitialized; @@ -447,7 +447,7 @@ ${globalLoadStrategy.loadModuleSnippet}("dart_sdk").developer.invokeExtension( {Map scope, bool disableBreakpoints}) async { // TODO(798) - respect disableBreakpoints. - return _captureElapsedTime(() async { + return captureElapsedTime(() async { await isInitialized; if (_expressionEvaluator != null) { await isCompilerInitialized; @@ -509,7 +509,7 @@ ${globalLoadStrategy.loadModuleSnippet}("dart_sdk").developer.invokeExtension( @override Future getIsolate(String isolateId) async { - return _captureElapsedTime(() async { + return captureElapsedTime(() async { await isInitialized; return _getIsolate(isolateId); }, (result) => DwdsEvent.getIsolate()); @@ -531,7 +531,7 @@ ${globalLoadStrategy.loadModuleSnippet}("dart_sdk").developer.invokeExtension( @override Future getScripts(String isolateId) async { - return await _captureElapsedTime(() async { + return await captureElapsedTime(() async { await isInitialized; return _inspector?.getScripts(isolateId); }, (result) => DwdsEvent.getScripts()); @@ -544,7 +544,7 @@ ${globalLoadStrategy.loadModuleSnippet}("dart_sdk").developer.invokeExtension( int endTokenPos, bool forceCompile, bool reportLines}) async { - return await _captureElapsedTime(() async { + return await captureElapsedTime(() async { await isInitialized; return await _inspector?.getSourceReport(isolateId, reports, scriptId: scriptId, @@ -568,7 +568,7 @@ ${globalLoadStrategy.loadModuleSnippet}("dart_sdk").developer.invokeExtension( @override Future getVM() async { - return _captureElapsedTime(() async { + return captureElapsedTime(() async { await isInitialized; return _vm; }, (result) => DwdsEvent.getVM()); @@ -685,7 +685,7 @@ ${globalLoadStrategy.loadModuleSnippet}("dart_sdk").developer.invokeExtension( {String step, int frameIndex}) async { if (_inspector == null) throw StateError('No running isolate.'); if (_inspector.appConnection.isStarted) { - return _captureElapsedTime(() async { + return captureElapsedTime(() async { await isInitialized; return await (await _debugger) .resume(isolateId, step: step, frameIndex: frameIndex); @@ -1037,27 +1037,6 @@ ${globalLoadStrategy.loadModuleSnippet}("dart_sdk").developer.invokeExtension( Future setBreakpointState( String isolateId, String breakpointId, bool enable) => throw UnimplementedError(); - - /// Call [function] and record its execution time. - /// - /// Calls [event] to create the event to be recorded, - /// and appends time and exception details to it if - /// available. - Future _captureElapsedTime( - Future Function() function, DwdsEvent Function(T result) event) async { - var stopwatch = Stopwatch()..start(); - T result; - try { - return result = await function(); - } catch (e) { - emitEvent(event(result) - ..addException(e) - ..addElapsedTime(stopwatch.elapsedMilliseconds)); - rethrow; - } finally { - emitEvent(event(result)..addElapsedTime(stopwatch.elapsedMilliseconds)); - } - } } /// The `type`s of [ConsoleAPIEvent]s that are treated as `stderr` logs. diff --git a/dwds/lib/src/services/debug_service.dart b/dwds/lib/src/services/debug_service.dart index 3b06d6e2c..9987c5e51 100644 --- a/dwds/lib/src/services/debug_service.dart +++ b/dwds/lib/src/services/debug_service.dart @@ -23,6 +23,7 @@ import 'package:web_socket_channel/web_socket_channel.dart'; import '../../dwds.dart'; import '../debugging/execution_context.dart'; import '../debugging/remote_debugger.dart'; +import '../events.dart'; import '../utilities/shared.dart'; import 'chrome_proxy_service.dart'; @@ -256,7 +257,8 @@ class DebugService { } var server = await startHttpServer(hostname, port: 44456); serveHttpRequests(server, handler, (e, s) { - _logger.warning('Error serving requests', e, s); + _logger.warning('Error serving requests', e); + emitEvent(DwdsEvent.httpRequestException('DebugService', '$e:$s')); }); return DebugService._( chromeProxyService, diff --git a/dwds/test/events_test.dart b/dwds/test/events_test.dart index 61bcce61e..7ad33ddc9 100644 --- a/dwds/test/events_test.dart +++ b/dwds/test/events_test.dart @@ -5,10 +5,13 @@ // @dart = 2.9 import 'dart:async'; +import 'dart:io'; import 'package:dwds/src/connections/debug_connection.dart'; import 'package:dwds/src/events.dart'; import 'package:dwds/src/services/chrome_proxy_service.dart'; +import 'package:dwds/src/utilities/shared.dart'; +import 'package:http_multi_server/http_multi_server.dart'; import 'package:test/test.dart'; import 'package:vm_service/vm_service.dart'; import 'package:webdriver/async_core.dart'; @@ -25,323 +28,417 @@ WipConnection get tabConnection => context.tabConnection; final context = TestContext(); void main() { - Future initialEvents; - VmService vmService; - Keyboard keyboard; - Stream events; - - /// Runs [action] and waits for an event matching [eventMatcher]. - Future expectEventDuring( - Matcher eventMatcher, Future Function() action, - {Timeout timeout}) async { - // The events stream is a broadcast stream so start listening - // before the action. - final events = expectLater( - pipe(context.testServer.dwds.events, timeout: timeout), - emitsThrough(eventMatcher)); - final result = await action(); - await events; - return result; - } - - setUpAll(() async { - setCurrentLogWriter(); - initialEvents = expectLater( - pipe(eventStream, timeout: const Timeout.factor(5)), - emitsThrough(matchesEvent(DwdsEventKind.compilerUpdateDependencies, { - 'entrypoint': 'hello_world/main.dart.bootstrap.js', - 'elapsedMilliseconds': isNotNull - }))); - await context.setUp( - serveDevTools: true, - enableExpressionEvaluation: true, - ); - vmService = context.debugConnection.vmService; - keyboard = context.webDriver.driver.keyboard; - events = context.testServer.dwds.events; - }); - - tearDownAll(() async { - await context.tearDown(); - }); - - test('emits DEVTOOLS_LAUNCH event', () async { - await expectEventDuring( - matchesEvent(DwdsEventKind.devtoolsLaunch, {}), - () => keyboard.sendChord([Keyboard.alt, 'd']), - ); - }); - - test('emits DEBUGGER_READY event', () async { - await expectEventDuring( - matchesEvent(DwdsEventKind.debuggerReady, { - 'elapsedMilliseconds': isNotNull, - }), - () => keyboard.sendChord([Keyboard.alt, 'd']), - ); - }, - skip: 'Enable after publishing of ' - 'https://github.com/flutter/devtools/pull/3346'); - - test('events can be listened to multiple times', () async { - events.listen((_) {}); - events.listen((_) {}); - }); - - test('can emit event through service extension', () async { - final response = await expectEventDuring( - matchesEvent('foo-event', {'data': 1234}), - () => vmService.callServiceExtension('ext.dwds.emitEvent', args: { - 'type': 'foo-event', - 'payload': {'data': 1234}, - })); - expect(response.type, 'Success'); - }); - - group('evaluate', () { - Isolate isolate; - LibraryRef bootstrap; - - setUpAll(() async { - setCurrentLogWriter(); - final vm = await service.getVM(); - isolate = await service.getIsolate(vm.isolates.first.id); - bootstrap = isolate.rootLib; - }); + group('serve requests', () { + HttpServer server; setUp(() async { setCurrentLogWriter(); + server = await HttpMultiServer.bind('localhost', 0); }); - test('emits EVALUATE events on evaluation success', () async { - final expression = "helloString('world')"; - await expectEventDuring( - matchesEvent(DwdsEventKind.evaluate, { - 'expression': expression, - 'success': isTrue, - 'elapsedMilliseconds': isNotNull, - }), - () => service.evaluate(isolate.id, bootstrap.id, expression)); - }); - - test('emits COMPILER_UPDATE_DEPENDENCIES event', () async { - await initialEvents; + tearDown(() async { + await server?.close(); }); - test('emits EVALUATE events on evaluation failure', () async { - final expression = 'some-bad-expression'; - await expectEventDuring( - matchesEvent(DwdsEventKind.evaluate, { - 'expression': expression, - 'success': isFalse, - 'error': isA(), - 'elapsedMilliseconds': isNotNull, - }), - () => service.evaluate(isolate.id, bootstrap.id, expression)); + test('emits HTTP_REQUEST_EXCEPTION event', () async { + final throwAsyncException = () async { + await Future.delayed(const Duration(milliseconds: 100)); + throw Exception('async error'); + }; + + // The events stream is a broadcast stream so start listening + // before the action. + final events = expectLater( + pipe(eventStream), + emitsThrough(matchesEvent(DwdsEventKind.httpRequestException, { + 'server': 'FakeServer', + 'exception': startsWith('Exception: async error'), + }))); + + // Start serving requests with a failing handler in an error zone. + serveHttpRequests(server, (request) async { + unawaited(throwAsyncException()); + return null; + }, (e, s) { + emitEvent(DwdsEvent.httpRequestException('FakeServer', '$e:$s')); + }); + + // Send a request. + final client = HttpClient(); + var request = + await client.getUrl(Uri.parse('http://localhost:${server.port}/foo')); + + // Ignore the response. + var response = await request.close(); + await response.drain(); + + // Wait for expected events. + await events; }); }); - group('evaluateInFrame', () { - String isolateId; - Stream stream; - ScriptList scripts; - ScriptRef mainScript; + group('with dwds', () { + Future initialEvents; + VmService vmService; + Keyboard keyboard; + Stream events; + + /// Runs [action] and waits for an event matching [eventMatcher]. + Future expectEventDuring( + Matcher eventMatcher, Future Function() action, + {Timeout timeout}) async { + // The events stream is a broadcast stream so start listening + // before the action. + final events = expectLater( + pipe(context.testServer.dwds.events, timeout: timeout), + emitsThrough(eventMatcher)); + final result = await action(); + await events; + return result; + } setUpAll(() async { setCurrentLogWriter(); - final vm = await service.getVM(); - - isolateId = vm.isolates.first.id; - scripts = await service.getScripts(isolateId); - await service.streamListen('Debug'); - stream = service.onEvent('Debug'); - mainScript = scripts.scripts - .firstWhere((script) => script.uri.contains('main.dart')); + initialEvents = expectLater( + pipe(eventStream, timeout: const Timeout.factor(5)), + emitsThrough(matchesEvent(DwdsEventKind.compilerUpdateDependencies, { + 'entrypoint': 'hello_world/main.dart.bootstrap.js', + 'elapsedMilliseconds': isNotNull + }))); + await context.setUp( + serveDevTools: true, + enableExpressionEvaluation: true, + ); + vmService = context.debugConnection.vmService; + keyboard = context.webDriver.driver.keyboard; + events = context.testServer.dwds.events; }); - setUp(() async { - setCurrentLogWriter(); + tearDownAll(() async { + await context.tearDown(); }); - test('emits EVALUATE_IN_FRAME events on RPC error', () async { - final expression = 'some-bad-expression'; + test('emits DEVTOOLS_LAUNCH event', () async { await expectEventDuring( - matchesEvent(DwdsEventKind.evaluateInFrame, { - 'expression': expression, - 'success': isFalse, - 'exception': isA().having( - (e) => e.message, 'message', contains('program is not paused')), - 'elapsedMilliseconds': isNotNull, - }), - () => service - .evaluateInFrame(isolateId, 0, expression) - .catchError((_) {})); + matchesEvent(DwdsEventKind.devtoolsLaunch, {}), + () => keyboard.sendChord([Keyboard.alt, 'd']), + ); }); - test('emits EVALUATE_IN_FRAME events on evaluation error', () async { - final line = await context.findBreakpointLine( - 'callPrintCount', isolateId, mainScript); - final bp = await service.addBreakpoint(isolateId, mainScript.id, line); - // Wait for breakpoint to trigger. - await stream - .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); - - // Evaluation succeeds and return ErrorRef containing compilation error, - // so event is marked as success. - final expression = 'some-bad-expression'; + test('emits DEBUGGER_READY event', () async { await expectEventDuring( - matchesEvent(DwdsEventKind.evaluateInFrame, { - 'expression': expression, - 'success': isFalse, - 'error': isA(), - 'elapsedMilliseconds': isNotNull, - }), - () => service - .evaluateInFrame(isolateId, 0, expression) - .catchError((_) {})); - - await service.removeBreakpoint(isolateId, bp.id); - await service.resume(isolateId); - }); - - test('emits EVALUATE_IN_FRAME events on evaluation success', () async { - final line = await context.findBreakpointLine( - 'callPrintCount', isolateId, mainScript); - final bp = await service.addBreakpoint(isolateId, mainScript.id, line); - // Wait for breakpoint to trigger. - await stream - .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); - - // Evaluation succeeds and return InstanceRef, - // so event is marked as success. - final expression = 'true'; + matchesEvent(DwdsEventKind.debuggerReady, { + 'elapsedMilliseconds': isNotNull, + }), + () => keyboard.sendChord([Keyboard.alt, 'd']), + ); + }, + skip: 'Enable after publishing of ' + 'https://github.com/flutter/devtools/pull/3346'); + + test('emits DEVTOOLS_LOAD events', () async { await expectEventDuring( - matchesEvent(DwdsEventKind.evaluateInFrame, { - 'expression': expression, - 'success': isTrue, - 'elapsedMilliseconds': isNotNull, - }), - () => service - .evaluateInFrame(isolateId, 0, expression) - .catchError((_) {})); - - await service.removeBreakpoint(isolateId, bp.id); - await service.resume(isolateId); + matchesEvent(DwdsEventKind.devToolsLoad, { + 'elapsedMilliseconds': isNotNull, + }), + () => keyboard.sendChord([Keyboard.alt, 'd']), + ); + }, + skip: 'Enable after publishing of ' + 'https://github.com/flutter/devtools/pull/3346'); + + test('events can be listened to multiple times', () async { + events.listen((_) {}); + events.listen((_) {}); }); - }); - - group('getSourceReport', () { - String isolateId; - ScriptList scripts; - ScriptRef mainScript; - setUp(() async { - setCurrentLogWriter(); - final vm = await service.getVM(); - isolateId = vm.isolates.first.id; - scripts = await service.getScripts(isolateId); - - mainScript = scripts.scripts - .firstWhere((script) => script.uri.contains('main.dart')); + test('can emit event through service extension', () async { + final response = await expectEventDuring( + matchesEvent('foo-event', {'data': 1234}), + () => vmService.callServiceExtension('ext.dwds.emitEvent', args: { + 'type': 'foo-event', + 'payload': {'data': 1234}, + })); + expect(response.type, 'Success'); }); - test('emits GET_SOURCE_REPORT events', () async { - await expectEventDuring( - matchesEvent(DwdsEventKind.getSourceReport, { - 'elapsedMilliseconds': isNotNull, - }), - () => service.getSourceReport( - isolateId, [SourceReportKind.kPossibleBreakpoints], - scriptId: mainScript.id)); + group('evaluate', () { + Isolate isolate; + LibraryRef bootstrap; + + setUpAll(() async { + setCurrentLogWriter(); + final vm = await service.getVM(); + isolate = await service.getIsolate(vm.isolates.first.id); + bootstrap = isolate.rootLib; + }); + + setUp(() async { + setCurrentLogWriter(); + }); + + test('emits EVALUATE events on evaluation success', () async { + final expression = "helloString('world')"; + await expectEventDuring( + matchesEvent(DwdsEventKind.evaluate, { + 'expression': expression, + 'success': isTrue, + 'elapsedMilliseconds': isNotNull, + }), + () => service.evaluate(isolate.id, bootstrap.id, expression)); + }); + + test('emits COMPILER_UPDATE_DEPENDENCIES event', () async { + await initialEvents; + }); + + test('emits EVALUATE events on evaluation failure', () async { + final expression = 'some-bad-expression'; + await expectEventDuring( + matchesEvent(DwdsEventKind.evaluate, { + 'expression': expression, + 'success': isFalse, + 'error': isA(), + 'elapsedMilliseconds': isNotNull, + }), + () => service.evaluate(isolate.id, bootstrap.id, expression)); + }); }); - }); - group('getSripts', () { - String isolateId; - - setUp(() async { - setCurrentLogWriter(); - final vm = await service.getVM(); - isolateId = vm.isolates.first.id; + group('evaluateInFrame', () { + String isolateId; + Stream stream; + ScriptList scripts; + ScriptRef mainScript; + + setUpAll(() async { + setCurrentLogWriter(); + final vm = await service.getVM(); + + isolateId = vm.isolates.first.id; + scripts = await service.getScripts(isolateId); + await service.streamListen('Debug'); + stream = service.onEvent('Debug'); + mainScript = scripts.scripts + .firstWhere((script) => script.uri.contains('main.dart')); + }); + + setUp(() async { + setCurrentLogWriter(); + }); + + test('emits EVALUATE_IN_FRAME events on RPC error', () async { + final expression = 'some-bad-expression'; + await expectEventDuring( + matchesEvent(DwdsEventKind.evaluateInFrame, { + 'expression': expression, + 'success': isFalse, + 'exception': isA().having((e) => e.message, 'message', + contains('program is not paused')), + 'elapsedMilliseconds': isNotNull, + }), + () => service + .evaluateInFrame(isolateId, 0, expression) + .catchError((_) {})); + }); + + test('emits EVALUATE_IN_FRAME events on evaluation error', () async { + final line = await context.findBreakpointLine( + 'callPrintCount', isolateId, mainScript); + final bp = await service.addBreakpoint(isolateId, mainScript.id, line); + // Wait for breakpoint to trigger. + await stream + .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); + + // Evaluation succeeds and return ErrorRef containing compilation error, + // so event is marked as success. + final expression = 'some-bad-expression'; + await expectEventDuring( + matchesEvent(DwdsEventKind.evaluateInFrame, { + 'expression': expression, + 'success': isFalse, + 'error': isA(), + 'elapsedMilliseconds': isNotNull, + }), + () => service + .evaluateInFrame(isolateId, 0, expression) + .catchError((_) {})); + + await service.removeBreakpoint(isolateId, bp.id); + await service.resume(isolateId); + }); + + test('emits EVALUATE_IN_FRAME events on evaluation success', () async { + final line = await context.findBreakpointLine( + 'callPrintCount', isolateId, mainScript); + final bp = await service.addBreakpoint(isolateId, mainScript.id, line); + // Wait for breakpoint to trigger. + await stream + .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); + + // Evaluation succeeds and return InstanceRef, + // so event is marked as success. + final expression = 'true'; + await expectEventDuring( + matchesEvent(DwdsEventKind.evaluateInFrame, { + 'expression': expression, + 'success': isTrue, + 'elapsedMilliseconds': isNotNull, + }), + () => service + .evaluateInFrame(isolateId, 0, expression) + .catchError((_) {})); + + await service.removeBreakpoint(isolateId, bp.id); + await service.resume(isolateId); + }); }); - test('emits GET_SCRIPTS events', () async { - await expectEventDuring( - matchesEvent(DwdsEventKind.getScripts, { - 'elapsedMilliseconds': isNotNull, - }), - () => service.getScripts(isolateId)); + group('getSourceReport', () { + String isolateId; + ScriptList scripts; + ScriptRef mainScript; + + setUp(() async { + setCurrentLogWriter(); + final vm = await service.getVM(); + isolateId = vm.isolates.first.id; + scripts = await service.getScripts(isolateId); + + mainScript = scripts.scripts + .firstWhere((script) => script.uri.contains('main.dart')); + }); + + test('emits GET_SOURCE_REPORT events', () async { + await expectEventDuring( + matchesEvent(DwdsEventKind.getSourceReport, { + 'elapsedMilliseconds': isNotNull, + }), + () => service.getSourceReport( + isolateId, [SourceReportKind.kPossibleBreakpoints], + scriptId: mainScript.id)); + }); }); - }); - - group('getIsolate', () { - String isolateId; - setUp(() async { - setCurrentLogWriter(); - final vm = await service.getVM(); - isolateId = vm.isolates.first.id; + group('getSripts', () { + String isolateId; + + setUp(() async { + setCurrentLogWriter(); + final vm = await service.getVM(); + isolateId = vm.isolates.first.id; + }); + + test('emits GET_SCRIPTS events', () async { + await expectEventDuring( + matchesEvent(DwdsEventKind.getScripts, { + 'elapsedMilliseconds': isNotNull, + }), + () => service.getScripts(isolateId)); + }); }); - test('emits GET_ISOLATE events', () async { - await expectEventDuring( - matchesEvent(DwdsEventKind.getIsolate, { - 'elapsedMilliseconds': isNotNull, - }), - () => service.getIsolate(isolateId)); + group('getIsolate', () { + String isolateId; + + setUp(() async { + setCurrentLogWriter(); + final vm = await service.getVM(); + isolateId = vm.isolates.first.id; + }); + + test('emits GET_ISOLATE events', () async { + await expectEventDuring( + matchesEvent(DwdsEventKind.getIsolate, { + 'elapsedMilliseconds': isNotNull, + }), + () => service.getIsolate(isolateId)); + }); }); - }); - group('getVM', () { - setUp(() async { - setCurrentLogWriter(); + group('getVM', () { + setUp(() async { + setCurrentLogWriter(); + }); + + test('emits GET_VM events', () async { + await expectEventDuring( + matchesEvent(DwdsEventKind.getVM, { + 'elapsedMilliseconds': isNotNull, + }), + () => service.getVM()); + }); }); - test('emits GET_VM events', () async { - await expectEventDuring( - matchesEvent(DwdsEventKind.getVM, { - 'elapsedMilliseconds': isNotNull, - }), - () => service.getVM()); - }); - }); + group('hotRestart', () { + setUp(() async { + setCurrentLogWriter(); + }); - group('resume', () { - String isolateId; - Stream stream; - ScriptList scripts; - ScriptRef mainScript; + test('emits HOT_RESTART event', () async { + var client = context.debugConnection.vmService; - setUp(() async { - setCurrentLogWriter(); - final vm = await service.getVM(); - isolateId = vm.isolates.first.id; - scripts = await service.getScripts(isolateId); - await service.streamListen('Debug'); - stream = service.onEvent('Debug'); - mainScript = scripts.scripts - .firstWhere((script) => script.uri.contains('main.dart')); - final line = await context.findBreakpointLine( - 'callPrintCount', isolateId, mainScript); - final bp = await service.addBreakpoint(isolateId, mainScript.id, line); - // Wait for breakpoint to trigger. - await stream - .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); - await service.removeBreakpoint(isolateId, bp.id); + await expectEventDuring( + matchesEvent(DwdsEventKind.hotRestart, { + 'elapsedMilliseconds': isNotNull, + }), + () => client.callServiceExtension('hotRestart')); + }); }); - tearDown(() async { - // Resume execution to not impact other tests. - await service.resume(isolateId); + group('resume', () { + String isolateId; + Stream stream; + ScriptList scripts; + ScriptRef mainScript; + + setUp(() async { + setCurrentLogWriter(); + final vm = await service.getVM(); + isolateId = vm.isolates.first.id; + scripts = await service.getScripts(isolateId); + await service.streamListen('Debug'); + stream = service.onEvent('Debug'); + mainScript = scripts.scripts + .firstWhere((script) => script.uri.contains('main.dart')); + final line = await context.findBreakpointLine( + 'callPrintCount', isolateId, mainScript); + final bp = await service.addBreakpoint(isolateId, mainScript.id, line); + // Wait for breakpoint to trigger. + await stream + .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); + await service.removeBreakpoint(isolateId, bp.id); + }); + + tearDown(() async { + // Resume execution to not impact other tests. + await service.resume(isolateId); + }); + + test('emits RESUME events', () async { + await expectEventDuring( + matchesEvent(DwdsEventKind.resume, { + 'step': 'Into', + 'elapsedMilliseconds': isNotNull, + }), + () => service.resume(isolateId, step: 'Into')); + }); }); - test('emits RESUME events', () async { - await expectEventDuring( - matchesEvent(DwdsEventKind.resume, { - 'step': 'Into', - 'elapsedMilliseconds': isNotNull, - }), - () => service.resume(isolateId, step: 'Into')); + group('fullReload', () { + setUp(() async { + setCurrentLogWriter(); + }); + + test('emits FULL_RELOAD event', () async { + var client = context.debugConnection.vmService; + + await expectEventDuring( + matchesEvent(DwdsEventKind.fullReload, { + 'elapsedMilliseconds': isNotNull, + }), + () => client.callServiceExtension('fullReload')); + }); }); }); }