blob: 1c0dc21ef483d9f0f316c1ed01504435a2f2c781 [file] [log] [blame]
// 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:dds/src/dap/logging.dart';
import 'package:dds/src/dap/protocol_generated.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'test_client.dart';
import 'test_server.dart';
/// A [RegExp] that matches the "Connecting to VM Service" banner that is sent
/// by the DAP adapter as the first output event for a debug session.
final dapVmServiceBannerPattern =
RegExp(r'Connecting to VM Service at ([^\s]+)\s');
/// Whether to run the DAP server in-process with the tests, or externally in
/// another process.
///
/// By default tests will run the DAP server out-of-process to match the real
/// use from editors, but this complicates debugging the adapter. Set this env
/// variables to run the server in-process for easier debugging (this can be
/// simplified in VS Code by using a launch config with custom CodeLens links).
final useInProcessDap = Platform.environment['DAP_TEST_INTERNAL'] == 'true';
/// Whether to print all protocol traffic to stdout while running tests.
///
/// This is useful for debugging locally or on the bots and will include both
/// DAP traffic (between the test DAP client and the DAP server) and the VM
/// Service traffic (wrapped in a custom 'dart.log' event).
///
/// Verbose logging is temporarily enabled for all test runs to try and
/// understand failures noted at https://github.com/dart-lang/sdk/issues/48274.
/// Once resolved, this variable can be set back to the result of:
/// Platform.environment['DAP_TEST_VERBOSE'] == 'true'
final verboseLogging = true;
/// A [RegExp] that matches the `path` part of a VM Service URI that contains
/// an authentication token.
final vmServiceAuthCodePathPattern = RegExp(r'^/[\w_\-=]{5,15}/ws$');
/// A [RegExp] that matches the "The Dart VM service is listening on" banner that is sent
/// by the VM when not using --write-service-info.
final vmServiceBannerPattern =
RegExp(r'The Dart VM service is listening on ([^\s]+)\s');
/// The root of the SDK containing the current running VM.
final sdkRoot = path.dirname(path.dirname(Platform.resolvedExecutable));
/// Expects the lines in [actual] to match the relevant matcher in [expected],
/// ignoring differences in line endings and trailing whitespace.
void expectLines(String actual, List<Object> expected) {
expect(
actual.replaceAll('\r\n', '\n').trim().split('\n'),
equals(expected),
);
}
/// Expects [actual] starts with [expected], ignoring differences in line
/// endings and trailing whitespace.
void expectLinesStartWith(String actual, List<String> expected) {
expect(
actual.replaceAll('\r\n', '\n').trim(),
startsWith(expected.join('\n').trim()),
);
}
/// Expects [response] to fail with a `message` matching [messageMatcher].
expectResponseError<T>(Future<T> response, Matcher messageMatcher) {
expect(
response,
throwsA(
const TypeMatcher<Response>()
.having((r) => r.success, 'success', isFalse)
.having((r) => r.message, 'message', messageMatcher),
),
);
}
/// Returns the 1-base line in [file] that contains [searchText].
int lineWith(File file, String searchText) =>
file.readAsLinesSync().indexWhere((line) => line.contains(searchText)) + 1;
Future<Process> startDartProcessPaused(
String script,
List<String> args, {
required String cwd,
List<String>? vmArgs,
}) async {
final vmPath = Platform.resolvedExecutable;
vmArgs ??= [];
vmArgs.addAll([
'--enable-vm-service=0',
'--pause_isolates_on_start',
]);
final processArgs = [
...vmArgs,
script,
...args,
];
return Process.start(
vmPath,
processArgs,
workingDirectory: cwd,
);
}
/// Monitors [process] for the Observatory/VM Service banner and extracts the
/// VM Service URI.
Future<Uri> waitForStdoutVmServiceBanner(Process process) {
final _vmServiceUriCompleter = Completer<Uri>();
late StreamSubscription<String> vmServiceBannerSub;
vmServiceBannerSub = process.stdout.transform(utf8.decoder).listen(
(line) {
final match = vmServiceBannerPattern.firstMatch(line);
if (match != null) {
_vmServiceUriCompleter.complete(Uri.parse(match.group(1)!));
vmServiceBannerSub.cancel();
}
},
onDone: () {
if (!_vmServiceUriCompleter.isCompleted) {
_vmServiceUriCompleter.completeError('Stream ended');
}
},
);
return _vmServiceUriCompleter.future;
}
/// A helper class containing the DAP server/client for DAP integration tests.
class DapTestSession {
DapTestServer server;
DapTestClient client;
final Directory testDir =
Directory.systemTemp.createTempSync('dart-sdk-dap-test');
late final Directory testAppDir;
late final Directory testPackagesDir;
DapTestSession._(this.server, this.client) {
testAppDir = testDir.createTempSync('app');
createPubspec(testAppDir, 'my_test_project');
testPackagesDir = testDir.createTempSync('packages');
}
/// Adds package with [name] (optionally at [packageFolderUri]) to the
/// project in [dir].
///
/// If [packageFolderUri] is not supplied, will use [Isolate.resolvePackageUri]
/// assuming the package is available to the tests.
Future<void> addPackageDependency(
Directory dir,
String name, [
Uri? packageFolderUri,
]) async {
final proc = await Process.run(
Platform.resolvedExecutable,
[
'pub',
'add',
name,
if (packageFolderUri != null) ...[
'--path',
packageFolderUri.toFilePath(),
],
],
workingDirectory: dir.path,
);
expect(
proc.exitCode,
isZero,
reason: '${proc.stdout}\n${proc.stderr}'.trim(),
);
}
/// Create a simple package named `foo` that has an empty `foo` function.
Future<Uri> createFooPackage() {
return createSimplePackage(
'foo',
'''
foo() {
// Does nothing.
}
''',
);
}
void createPubspec(Directory dir, String projectName) {
final pubspecFile = File(path.join(dir.path, 'pubspec.yaml'));
pubspecFile
..createSync()
..writeAsStringSync('''
name: $projectName
version: 1.0.0
environment:
sdk: '>=2.13.0 <3.0.0'
''');
}
/// Creates a simple package script and adds the package to
/// .dart_tool/package_config.json
Future<Uri> createSimplePackage(
String name,
String content,
) async {
final packageDir = Directory(path.join(testPackagesDir.path, name))
..createSync(recursive: true);
final packageLibDir = Directory(path.join(packageDir.path, 'lib'))
..createSync(recursive: true);
// Create a pubspec and a implementation file in the lib folder.
createPubspec(packageDir, name);
final testFile = File(path.join(packageLibDir.path, '$name.dart'));
testFile.writeAsStringSync(content);
// Add this new package as a dependency for the app.
final fileUri = Uri.file('${packageDir.path}/');
await addPackageDependency(testAppDir, name, fileUri);
return Uri.parse('package:$name/$name.dart');
}
/// Creates a file in a temporary folder to be used as an application for testing.
///
/// The file will be deleted at the end of the test run.
File createTestFile(String content) {
final testFile = File(path.join(testAppDir.path, 'test_file.dart'));
testFile.writeAsStringSync(content);
return testFile;
}
Future<void> tearDown() async {
await client.stop();
await server.stop();
// Clean up any temp folders created during the test runs.
await tryDelete(testDir);
}
/// Tries to delete [dir] multiple times before printing a warning and giving up.
///
/// This avoids "The process cannot access the file because it is being
/// used by another process" errors on Windows trying to delete folders that
/// have only very recently been unlocked.
Future<void> tryDelete(Directory dir) async {
const maxAttempts = 10;
const delay = Duration(milliseconds: 100);
var attempt = 0;
while (++attempt <= maxAttempts) {
try {
testDir.deleteSync(recursive: true);
break;
} catch (e) {
if (attempt == maxAttempts) {
print('Failed to delete $testDir after $maxAttempts attempts.\n$e');
break;
}
await Future.delayed(delay);
}
}
}
static Future<DapTestSession> setUp({List<String>? additionalArgs}) async {
final server = await _startServer(additionalArgs: additionalArgs);
final client = await DapTestClient.connect(
server,
captureVmServiceTraffic: verboseLogging,
logger: verboseLogging ? print : null,
);
return DapTestSession._(server, client);
}
/// Starts a DAP server that can be shared across tests.
static Future<DapTestServer> _startServer({
Logger? logger,
List<String>? additionalArgs,
}) async {
return useInProcessDap
? await InProcessDapTestServer.create(
logger: logger,
additionalArgs: additionalArgs,
)
: await OutOfProcessDapTestServer.create(
logger: logger,
additionalArgs: additionalArgs,
);
}
}