// 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 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' || // Enable verbose logging on CI bots. // TODO(bkonyi): remove this once https://github.com/flutter/flutter/issues/172636 is resolved. Platform.environment.containsKey('SWARMING_TASK_ID'); const 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 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 _output = StreamController.broadcast(); /// A broadcast stream of any non-JSON output from the process. Stream 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 && message['event'] == 'app.debugPort') { final vmServiceUri = (message['params']! as Map)['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 _vmServiceUriCompleter = Completer(); Future get vmServiceUri => _vmServiceUriCompleter.future; static Future start(Directory projectDirectory) async { final String flutterToolPath = globals.fs.path.join( Cache.flutterRoot!, 'bin', globals.platform.isWindows ? 'flutter.bat' : 'flutter', ); final args = ['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 tearDown() async { await client.stop(); await server.stop(); } static Future setUp({List? 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 _startServer({Logger? logger, List? additionalArgs}) async { return useInProcessDap ? await InProcessDapTestServer.create(logger: logger, additionalArgs: additionalArgs) : await OutOfProcessDapTestServer.create(logger: logger, additionalArgs: additionalArgs); } }