| // 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). |
| final verboseLogging = Platform.environment['DAP_TEST_VERBOSE'] == '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 "Observatory listening on" banner that is sent |
| /// by the VM when not using --write-service-info. |
| final vmServiceBannerPattern = RegExp(r'Observatory 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. |
| _testDir.deleteSync(recursive: true); |
| } |
| |
| 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, |
| ); |
| } |
| } |