// Copyright 2014 The Flutter Authors. 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:io'; import 'package:dds/dap.dart'; import 'package:file/file.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:test/test.dart'; import 'test_client.dart'; import 'test_server.dart'; /// 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 bool 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 bool verboseLogging = Platform.environment['DAP_TEST_VERBOSE'] == 'true'; const String endOfErrorOutputMarker = '════════════════════════════════════════════════════════════════════════════════'; /// 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, { bool allowExtras = false, }) { if (allowExtras) { expect( actual.replaceAll('\r\n', '\n').trim().split('\n'), containsAllInOrder(expected), ); } else { expect( actual.replaceAll('\r\n', '\n').trim().split('\n'), equals(expected), ); } } /// Manages running a simple Flutter app to be used in tests that need to attach /// to an existing process. class SimpleFlutterRunner { SimpleFlutterRunner(this.process) { process.stdout.transform(ByteToLineTransformer()).listen(_handleStdout); process.stderr.transform(utf8.decoder).listen(_handleStderr); unawaited(process.exitCode.then(_handleExitCode)); } final StreamController<String> _output = StreamController<String>.broadcast(); /// A broadcast stream of any non-JSON output from the process. Stream<String> get output => _output.stream; void _handleExitCode(int code) { if (!_vmServiceUriCompleter.isCompleted) { _vmServiceUriCompleter.completeError('Flutter process ended without producing a VM Service URI'); } } void _handleStderr(String err) { if (!_vmServiceUriCompleter.isCompleted) { _vmServiceUriCompleter.completeError(err); } } void _handleStdout(String outputLine) { try { final Object? json = jsonDecode(outputLine); // Flutter --machine output is wrapped in [brackets] so will deserialize // as a list with one item. if (json is List && json.length == 1) { final Object? message = json.single; // Parse the add.debugPort event which contains our VM Service URI. if (message is Map<String, Object?> && message['event'] == 'app.debugPort') { final String vmServiceUri = (message['params']! as Map<String, Object?>)['wsUri']! as String; if (!_vmServiceUriCompleter.isCompleted) { _vmServiceUriCompleter.complete(Uri.parse(vmServiceUri)); } } } } on FormatException { // `flutter run` writes a lot of text to stdout that isn't daemon messages // (not valid JSON), so just pass that one for tests that may want it. _output.add(outputLine); } } final Process process; final Completer<Uri> _vmServiceUriCompleter = Completer<Uri>(); Future<Uri> get vmServiceUri => _vmServiceUriCompleter.future; static Future<SimpleFlutterRunner> start(Directory projectDirectory) async { final String flutterToolPath = globals.fs.path.join(Cache.flutterRoot!, 'bin', globals.platform.isWindows ? 'flutter.bat' : 'flutter'); final List<String> args = <String>[ 'run', '--machine', '-d', 'flutter-tester', ]; final Process process = await Process.start( flutterToolPath, args, workingDirectory: projectDirectory.path, ); return SimpleFlutterRunner(process); } } /// A helper class containing the DAP server/client for DAP integration tests. class DapTestSession { DapTestSession._(this.server, this.client); DapTestServer server; DapTestClient client; Future<void> tearDown() async { await client.stop(); await server.stop(); } static Future<DapTestSession> setUp({List<String>? additionalArgs}) async { final DapTestServer server = await _startServer(additionalArgs: additionalArgs); final DapTestClient 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, ); } }