// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. @Timeout(Duration(minutes: 2)) library; import 'dart:async'; import 'dart:io'; import 'package:dwds/src/events.dart'; import 'package:dwds/src/utilities/server.dart'; import 'package:test/test.dart'; import 'package:test_common/logging.dart'; import 'package:test_common/test_sdk_configuration.dart'; import 'package:vm_service/vm_service.dart'; import 'package:vm_service_interface/vm_service_interface.dart'; import 'package:webdriver/async_core.dart'; import 'fixtures/context.dart'; import 'fixtures/project.dart'; import 'fixtures/utilities.dart'; void main() { final provider = TestSdkConfigurationProvider(); tearDownAll(provider.dispose); final context = TestContext(TestProject.test, provider); group('serve requests', () { late HttpServer server; setUp(() async { setCurrentLogWriter(); server = await startHttpServer('localhost', port: 0); }); tearDown(() async { await server.close(); }); test('emits HTTP_REQUEST_EXCEPTION event', () async { Future 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 Future.error('error'); }, (e, s) { emitEvent(DwdsEvent.httpRequestException('FakeServer', '$e:$s')); }, ); // Send a request. final client = HttpClient(); final request = await client.getUrl( Uri.parse('http://localhost:${server.port}/foo'), ); // Ignore the response. final response = await request.close(); await response.drain(); // Wait for expected events. await events; }); }); group( 'with dwds', () { Future? initialEvents; late Keyboard keyboard; late Stream events; late VmService fakeClient; /// 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; } /// Runs [action] and waits for an event matching [eventMatcher]. Future expectEventsDuring( List eventMatchers, Future Function() action, { Timeout? timeout, }) async { // The events stream is a broadcast stream so start listening // before the action. final events = eventMatchers.map( (matcher) => expectLater( pipe(context.testServer.dwds.events, timeout: timeout), emitsThrough(matcher), ), ); final result = await action(); await Future.wait(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( testSettings: TestSettings(enableExpressionEvaluation: true), debugSettings: TestDebugSettings.withDevToolsLaunch(context), ); keyboard = context.webDriver.driver.keyboard; events = context.testServer.dwds.events; fakeClient = await context.connectFakeClient(); }); tearDownAll(() async { await context.tearDown(); }); test( 'emits DEBUGGER_READY and DEVTOOLS_LOAD events', () async { await expectEventsDuring([ matchesEvent(DwdsEventKind.debuggerReady, { 'elapsedMilliseconds': isNotNull, 'screen': equals('debugger'), }), matchesEvent(DwdsEventKind.devToolsLoad, { 'elapsedMilliseconds': isNotNull, 'screen': equals('debugger'), }), ], () => keyboard.sendChord([Keyboard.alt, 'd'])); }, skip: 'https://github.com/dart-lang/webdev/issues/2394', ); test('emits DEVTOOLS_LAUNCH event', () async { await expectEventDuring( matchesEvent(DwdsEventKind.devtoolsLaunch, {}), () => keyboard.sendChord([Keyboard.alt, 'd']), ); }); 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}), () => fakeClient.callServiceExtension( 'ext.dwds.emitEvent', args: { 'type': 'foo-event', 'payload': {'data': 1234}, }, ), ); expect(response.type, 'Success'); }); group('evaluate', () { late VmServiceInterface service; late String isolateId; late String bootstrapId; setUpAll(() async { setCurrentLogWriter(); service = context.service; final vm = await service.getVM(); final isolate = await service.getIsolate(vm.isolates!.first.id!); isolateId = isolate.id!; bootstrapId = isolate.rootLib!.id!; }); 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(isolateId, bootstrapId, 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(isolateId, bootstrapId, expression), ); }); }); group('evaluateInFrame', () { late VmServiceInterface service; late String isolateId; late Stream stream; late ScriptRef mainScript; setUpAll(() async { setCurrentLogWriter(); service = context.service; final vm = await service.getVM(); isolateId = vm.isolates!.first.id!; await service.streamListen('Debug'); stream = service.onEvent('Debug'); final scriptList = await service.getScripts(isolateId); mainScript = scriptList.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((_) => Future.value(Response())), ); }); 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((_) => Future.value(Response())), ); 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((_) => Future.value(Response())), ); await service.removeBreakpoint(isolateId, bp.id!); await service.resume(isolateId); }); }); group('getSourceReport', () { late VmServiceInterface service; late String isolateId; late ScriptRef mainScript; setUp(() async { setCurrentLogWriter(); service = context.service; final vm = await service.getVM(); isolateId = vm.isolates!.first.id!; final scriptList = await service.getScripts(isolateId); mainScript = scriptList.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('getScripts', () { late VmServiceInterface service; late String isolateId; setUp(() async { setCurrentLogWriter(); service = context.service; 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), ); }); }); group('getIsolate', () { late VmServiceInterface service; late String isolateId; setUp(() async { setCurrentLogWriter(); service = context.service; 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(); }); test('emits GET_VM events', () async { await expectEventDuring( matchesEvent(DwdsEventKind.getVM, { 'elapsedMilliseconds': isNotNull, }), () => context.service.getVM(), ); }); }); group('hotRestart', () { setUp(() async { setCurrentLogWriter(); }); test('emits HOT_RESTART event', () async { final hotRestart = context.getRegisteredServiceExtension( 'hotRestart', ); await expectEventDuring( matchesEvent(DwdsEventKind.hotRestart, { 'elapsedMilliseconds': isNotNull, }), () => fakeClient.callServiceExtension(hotRestart!), ); }); }); group('resume', () { late VmServiceInterface service; late String isolateId; setUp(() async { setCurrentLogWriter(); service = context.service; final vm = await service.getVM(); isolateId = vm.isolates!.first.id!; await service.streamListen('Debug'); final stream = service.onEvent('Debug'); final scriptList = await service.getScripts(isolateId); final mainScript = scriptList.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'), ); }); }); group('fullReload', () { setUp(() async { setCurrentLogWriter(); }); test('emits FULL_RELOAD event', () async { final fullReload = context.getRegisteredServiceExtension( 'fullReload', ); await expectEventDuring( matchesEvent(DwdsEventKind.fullReload, { 'elapsedMilliseconds': isNotNull, }), () => fakeClient.callServiceExtension(fullReload!), ); }); }); }, // TODO(elliette): Re-enable (https://github.com/dart-lang/webdev/issues/1852). skip: Platform.isWindows, timeout: Timeout.factor(2), ); } /// Matches event recursively. Matcher matchesEvent(String type, Map payload) { return isA() .having((e) => e.type, 'type', type) .having((e) => e.payload.keys, 'payload.keys', payload.keys) .having((e) => e.payload.values, 'payload.values', payload.values); } /// Pipes the [stream] into a newly created stream. /// Returns the new stream which is closed on [timeout]. Stream pipe(Stream stream, {Timeout? timeout}) { final controller = StreamController(); final defaultTimeout = const Timeout(Duration(seconds: 20)); timeout ??= defaultTimeout; unawaited( stream .forEach(controller.add) .timeout(defaultTimeout.merge(timeout).duration!) .catchError((_) {}) .then((value) => controller.close()), ); return controller.stream; }