| // 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, |
| ); |
| } |
| } |