blob: a8afc94d65f5e8c89f6ba9e154acb3fe454cdabe [file] [log] [blame] [edit]
// 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 StreamSubscription 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();
expect(contents, contains('DevTools'));
// 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',
if (vmServiceUri != null) vmServiceUri,
]);
process.stderr.transform(utf8.decoder).listen(print);
bool startedDds = false;
bool startedDtd = false;
final devToolsServedCompleter = Completer<void>();
late StreamSubscription 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(
[
'--disable-dart-dev',
'--observe=0',
if (disableServiceAuthCodes) '--disable-service-auth-codes',
targetProject.relativeFilePath,
],
);
final serviceUriCompleter = Completer<String>();
late 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);
});
}
});
}