1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// 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:math' as math;
import 'package:dds/dap.dart' hide PidTracker;
import '../base/io.dart';
import '../cache.dart';
import '../convert.dart';
import 'flutter_adapter_args.dart';
import 'flutter_base_adapter.dart';
/// A DAP Debug Adapter for running and debugging Flutter tests.
class FlutterTestDebugAdapter extends FlutterBaseDebugAdapter with TestAdapter {
FlutterTestDebugAdapter(
super.channel, {
required super.fileSystem,
required super.platform,
super.ipv6,
super.enableFlutterDds = true,
super.enableAuthCodes,
super.logger,
super.onError,
});
/// Called by [attachRequest] to request that we actually connect to the app to be debugged.
@override
Future<void> attachImpl() async {
sendOutput('console', '\nAttach is not currently supported');
handleSessionTerminate();
}
/// Called by [launchRequest] to request that we actually start the tests to be run/debugged.
///
/// For debugging, this should start paused, connect to the VM Service, set
/// breakpoints, and resume.
@override
Future<void> launchImpl() async {
final FlutterLaunchRequestArguments args = this.args as FlutterLaunchRequestArguments;
final bool debug = enableDebugger;
final String? program = args.program;
final List<String> toolArgs = <String>[
'test',
'--machine',
if (!enableFlutterDds) '--no-dds',
if (debug) '--start-paused',
];
// Handle customTool and deletion of any arguments for it.
final String executable = args.customTool ?? fileSystem.path.join(Cache.flutterRoot!, 'bin', platform.isWindows ? 'flutter.bat' : 'flutter');
final int? removeArgs = args.customToolReplacesArgs;
if (args.customTool != null && removeArgs != null) {
toolArgs.removeRange(0, math.min(removeArgs, toolArgs.length));
}
final List<String> processArgs = <String>[
...toolArgs,
...?args.toolArgs,
if (program != null) program,
...?args.args,
];
await launchAsProcess(
executable: executable,
processArgs: processArgs,
env: args.env,
);
// Delay responding until the debugger is connected.
if (debug) {
await debuggerInitialized;
}
}
/// Called by [terminateRequest] to request that we gracefully shut down the app being run (or in the case of an attach, disconnect).
@override
Future<void> terminateImpl() async {
terminatePids(ProcessSignal.sigterm);
await process?.exitCode;
}
/// Handles the Flutter process exiting, terminating the debug session if it has not already begun terminating.
@override
void handleExitCode(int code) {
final String codeSuffix = code == 0 ? '' : ' ($code)';
logger?.call('Process exited ($code)');
handleSessionTerminate(codeSuffix);
}
/// Handles incoming JSON events from `flutter test --machine`.
bool _handleJsonEvent(String event, Map<String, Object?>? params) {
params ??= <String, Object?>{};
switch (event) {
case 'test.startedProcess':
_handleTestStartedProcess(params);
return true;
}
return false;
}
@override
void handleStderr(List<int> data) {
logger?.call('stderr: $data');
sendOutput('stderr', utf8.decode(data));
}
/// Handles stdout from the `flutter test --machine` process, decoding the JSON and calling the appropriate handlers.
@override
void handleStdout(String data) {
// Output to stdout from `flutter test --machine` is either:
// 1. JSON output from flutter_tools (eg. "test.startedProcess") which is
// wrapped in [] brackets and has an event/params.
// 2. JSON output from package:test (not wrapped in brackets).
// 3. Non-JSON output (user messages, or flutter_tools printing things like
// call stacks/error information).
logger?.call('stdout: $data');
Object? jsonData;
try {
jsonData = jsonDecode(data);
} on FormatException {
// If the output wasn't valid JSON, it was standard stdout that should
// be passed through to the user.
sendOutput('stdout', data);
return;
}
// Check for valid flutter_tools JSON output (1) first.
final Map<String, Object?>? flutterPayload = jsonData is List &&
jsonData.length == 1 &&
jsonData.first is Map<String, Object?>
? jsonData.first as Map<String, Object?>
: null;
final Object? event = flutterPayload?['event'];
final Object? params = flutterPayload?['params'];
if (event is String && params is Map<String, Object?>?) {
_handleJsonEvent(event, params);
} else if (jsonData != null) {
// Handle package:test output (2).
sendTestEvents(jsonData);
} else {
// Other output should just be passed straight through.
sendOutput('stdout', data);
}
}
/// Handles the test.processStarted event from Flutter that provides the VM Service URL.
void _handleTestStartedProcess(Map<String, Object?> params) {
final String? vmServiceUriString = params['vmServiceUri'] as String?;
// For no-debug mode, this event may be still sent so ignore it if we know
// we're not debugging, or its URI is null.
if (!enableDebugger || vmServiceUriString == null) {
return;
}
final Uri vmServiceUri = Uri.parse(vmServiceUriString);
connectDebugger(vmServiceUri);
}
}