// 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. import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:dartdev/src/commands/devtools.dart'; import 'package:dds/devtools_server.dart'; import 'package:test/test.dart'; import '../utils.dart'; final dartVMServiceRegExp = RegExp( r'The Dart VM service is listening on (http://127.0.0.1:.*)', ); final ddsStartedRegExp = RegExp( r'Started the Dart Development Service \(DDS\) at (http://127.0.0.1:.*)', ); final dtdStartedRegExp = RegExp( r'Serving the Dart Tooling Daemon at (ws://127.0.0.1:.*)', ); final servingDevToolsRegExp = RegExp( r'Serving DevTools at (http://127.0.0.1:.*)', ); void main() { group('devtools', devtools, timeout: longTimeout); } void devtools() { late TestProject p; test('--help', () async { p = project(); var result = await p.run(['devtools', '--help']); expect(result.exitCode, 0); expect(result.stderr, isEmpty); expect(result.stdout, contains('Open DevTools')); expect(result.stdout, contains('Usage: dart devtools [arguments] [service protocol uri]')); // Does not show verbose help. expect(result.stdout.contains('--try-ports'), isFalse); }); test('--help --verbose', () async { p = project(); var result = await p.run(['devtools', '--help', '--verbose']); expect(result.exitCode, 0); expect(result.stderr, isEmpty); expect(result.stdout, contains('Open DevTools')); expect( result.stdout, contains( 'Usage: dart [vm-options] devtools [arguments] [service protocol uri]')); // Shows verbose help. expect(result.stdout, contains('--try-ports')); }); group('integration', () { Process? process; tearDown(() { process?.kill(); }); test('serves resources', () async { p = project(); // start the devtools server process = await p.start(['devtools', '--no-launch-browser', '--machine']); process!.stderr.transform(utf8.decoder).listen(print); String? devToolsHost; int? devToolsPort; final devToolsServedCompleter = Completer(); final dtdServedCompleter = Completer(); late final StreamSubscription sub; sub = process!.stdout .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) async { final json = jsonDecode(line); final eventName = json['event'] as String?; final params = (json['params'] as Map?)?.cast(); switch (eventName) { case 'server.dtdStarted': // {"event":"server.dtdStarted","params":{ // "uri":"ws://127.0.0.1:50882/nQf49D0YcbONeKVq" // }} expect(params!['uri'], isA()); dtdServedCompleter.complete(); case 'server.started': // {"event":"server.started","method":"server.started","params":{ // "host":"127.0.0.1","port":9100,"pid":93508,"protocolVersion":"1.1.0" // }} expect(params!['host'], isA()); expect(params['port'], isA()); devToolsHost = params['host'] as String; devToolsPort = params['port'] as int; // We can cancel the subscription because the 'server.started' event // is expected after the 'server.dtdStarted' event. await sub.cancel(); devToolsServedCompleter.complete(); default: } }); await Future.wait([ dtdServedCompleter.future, devToolsServedCompleter.future, ]).timeout( const Duration(seconds: 5), onTimeout: () => throw Exception( 'Expected DTD and DevTools to be served, but one or both were not.', ), ); // Connect to the port and confirm we can load a devtools resource. HttpClient client = HttpClient(); expect(devToolsHost, isNotNull); expect(devToolsPort, isNotNull); final httpRequest = await client.get(devToolsHost!, devToolsPort!, 'index.html'); final httpResponse = await httpRequest.close(); final contents = (await httpResponse.transform(utf8.decoder).toList()).join(); client.close(); expect(contents, contains('DevTools')); // kill the process process!.kill(); process = null; }); }); Future startDevTools({ String? vmServiceUri, bool shouldStartDds = false, bool shouldPrintDtd = false, }) async { final process = await p.start([ 'devtools', '--no-launch-browser', if (shouldPrintDtd) '--print-dtd', if (vmServiceUri != null) vmServiceUri, ]); process.stderr.transform(utf8.decoder).listen(print); bool startedDds = false; bool startedDtd = false; final devToolsServedCompleter = Completer(); late final StreamSubscription sub; sub = process.stdout .transform(utf8.decoder) .transform(const LineSplitter()) .listen((event) async { print(event); if (event.contains(ddsStartedRegExp)) { startedDds = true; } else if (event.contains(dtdStartedRegExp)) { startedDtd = true; } else if (event.contains(servingDevToolsRegExp)) { await sub.cancel(); devToolsServedCompleter.complete(); } }); await devToolsServedCompleter.future; expect(startedDds, shouldStartDds); expect(startedDtd, shouldPrintDtd); // kill the process process.kill(); } test('prints DTD URI', () async { p = project(); await startDevTools(shouldPrintDtd: true); }); group('spawns DDS integration', () { late TestProject targetProject; Process? targetProjectInstance; setUp(() { // NOTE: we don't use `project()` here since it registers a tear-down // which can be called before the target process is killed. This can be // problematic on Windows, which won't let us delete directories while a // process is actively accessing it. Manually disposing the projects is // the easiest way to work around this. targetProject = TestProject( mainSrc: ''' Future main() async { while (true) { await Future.delayed(const Duration(seconds: 1)); } } ''', ); p = TestProject(); }); tearDown(() { targetProjectInstance?.kill(); targetProjectInstance = null; targetProject.dispose(); p.dispose(); }); Future startTargetProject({ required bool disableServiceAuthCodes, }) async { targetProjectInstance = await targetProject.start( [ '--no-dds', '--observe=0', if (disableServiceAuthCodes) '--disable-service-auth-codes', targetProject.relativeFilePath, ], ); final serviceUriCompleter = Completer(); late final StreamSubscription sub; sub = targetProjectInstance!.stdout .transform(utf8.decoder) .transform(const LineSplitter()) .listen((event) async { if (event.contains(dartVMServiceRegExp)) { await sub.cancel(); serviceUriCompleter.complete( dartVMServiceRegExp.firstMatch(event)!.group(1), ); } }); return await serviceUriCompleter.future; } for (final disableAuthCodes in const [true, false]) { final authCodesEnabledStr = disableAuthCodes ? 'disabled' : 'enabled'; test('with auth codes $authCodesEnabledStr', () async { final vmServiceUri = await startTargetProject( disableServiceAuthCodes: disableAuthCodes, ); // The first run should cause DDS to be started. await startDevTools(vmServiceUri: vmServiceUri, shouldStartDds: true); // The second run should not since DDS is already running. await startDevTools(vmServiceUri: vmServiceUri, shouldStartDds: false); }); test('check for redirect with auth codes $authCodesEnabledStr', () async { final vmServiceUri = Uri.parse( await startTargetProject( disableServiceAuthCodes: disableAuthCodes, ), ); var updatedUri = await DevToolsCommand.checkForRedirectToExistingDDSInstance( vmServiceUri, ); // We should not have followed a redirect since DDS isn't running. expect(vmServiceUri, updatedUri); // Start DDS for this VM service instance. final ddsUri = await DevToolsCommand.maybeStartDDS( uri: vmServiceUri, ddsHost: DevToolsServer.defaultDdsHost, ddsPort: DevToolsServer.defaultDdsPort.toString(), ); // Ensure that navigating to the VM service URI will redirect us to the // DDS URI. updatedUri = await DevToolsCommand.checkForRedirectToExistingDDSInstance( vmServiceUri, ); expect(updatedUri, ddsUri); }); } }); }