| // 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<void>(); | |
| final dtdServedCompleter = Completer<void>(); | |
| late final StreamSubscription<String> sub; | |
| sub = process!.stdout | |
| .transform<String>(utf8.decoder) | |
| .transform<String>(const LineSplitter()) | |
| .listen((line) async { | |
| final json = jsonDecode(line); | |
| final eventName = json['event'] as String?; | |
| final params = (json['params'] as Map?)?.cast<String, Object?>(); | |
| switch (eventName) { | |
| case 'server.dtdStarted': | |
| // {"event":"server.dtdStarted","params":{ | |
| // "uri":"ws://127.0.0.1:50882/nQf49D0YcbONeKVq" | |
| // }} | |
| expect(params!['uri'], isA<String>()); | |
| 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<String>()); | |
| expect(params['port'], isA<int>()); | |
| 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(); | |
| // This is a sanity check to make sure we're actually getting the content | |
| // we expect. This String is in the DevTools index.html file: | |
| // https://github.com/flutter/devtools/blob/master/packages/devtools_app/web/index.html#L14. | |
| expect( | |
| contents, | |
| contains('Note: This tag is replaced when served through DDS!'), | |
| ); | |
| // kill the process | |
| process!.kill(); | |
| process = null; | |
| }); | |
| }); | |
| Future<void> startDevTools({ | |
| String? vmServiceUri, | |
| bool shouldStartDds = false, | |
| bool shouldPrintDtd = false, | |
| }) async { | |
| final process = await p.start([ | |
| 'devtools', | |
| '--no-launch-browser', | |
| if (shouldPrintDtd) '--print-dtd', | |
| ?vmServiceUri, | |
| ]); | |
| process.stderr.transform(utf8.decoder).listen(print); | |
| bool startedDds = false; | |
| bool startedDtd = false; | |
| final devToolsServedCompleter = Completer<void>(); | |
| late final StreamSubscription<String> sub; | |
| sub = process.stdout | |
| .transform<String>(utf8.decoder) | |
| .transform<String>(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<void> main() async { | |
| while (true) { | |
| await Future.delayed(const Duration(seconds: 1)); | |
| } | |
| } | |
| ''', | |
| ); | |
| p = TestProject(); | |
| }); | |
| tearDown(() { | |
| targetProjectInstance?.kill(); | |
| targetProjectInstance = null; | |
| targetProject.dispose(); | |
| p.dispose(); | |
| }); | |
| Future<String> startTargetProject({ | |
| required bool disableServiceAuthCodes, | |
| }) async { | |
| targetProjectInstance = await targetProject.start([ | |
| '--no-dds', | |
| '--observe=0', | |
| if (disableServiceAuthCodes) '--disable-service-auth-codes', | |
| targetProject.relativeFilePath, | |
| ]); | |
| final serviceUriCompleter = Completer<String>(); | |
| late final StreamSubscription<String> 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)![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); | |
| }); | |
| } | |
| }); | |
| } |