Skip to content

Commit 248e943

Browse files
DanTupbkonyi
authored andcommitted
[dds] Expand truncated strings from dart:developer log() calls in DAP output
Change-Id: I31338b00daff33a5428e4b2229273ee8289f1c5e Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/206540 Reviewed-by: Ben Konyi <[email protected]>
1 parent 33c3cfc commit 248e943

File tree

5 files changed

+128
-46
lines changed

5 files changed

+128
-46
lines changed

pkg/dds/lib/src/dap/adapters/dart.dart

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
139139
/// processed its initial paused state).
140140
Future<void> get debuggerInitialized => _debuggerInitializedCompleter.future;
141141

142+
bool get evaluateToStringInDebugViews =>
143+
args.evaluateToStringInDebugViews ?? false;
144+
142145
/// [attachRequest] is called by the client when it wants us to to attach to
143146
/// an existing app. This will only be called once (and only one of this or
144147
/// launchRequest will be called).
@@ -430,7 +433,7 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
430433
final resultString = await _converter.convertVmInstanceRefToDisplayString(
431434
thread,
432435
result,
433-
allowCallingToString: true,
436+
allowCallingToString: evaluateToStringInDebugViews,
434437
);
435438
// TODO(dantup): We may need to store `expression` with this data
436439
// to allow building nested evaluateNames.
@@ -854,7 +857,8 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
854857
thread,
855858
variable.value,
856859
name: variable.name,
857-
allowCallingToString: index <= maxToStringsPerEvaluation,
860+
allowCallingToString: evaluateToStringInDebugViews &&
861+
index <= maxToStringsPerEvaluation,
858862
);
859863
}
860864

@@ -880,6 +884,7 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
880884
variables.addAll(await _converter.convertVmInstanceToVariablesList(
881885
thread,
882886
object,
887+
allowCallingToString: evaluateToStringInDebugViews,
883888
startItem: childStart,
884889
numItems: childCount,
885890
));
@@ -981,8 +986,15 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
981986
if (ref == null || ref.kind == vm.InstanceKind.kNull) {
982987
return null;
983988
}
984-
// TODO(dantup): This should handle truncation and complex types.
985-
return ref.valueAsString;
989+
return _converter.convertVmInstanceRefToDisplayString(
990+
thread,
991+
ref,
992+
// Always allow calling toString() here as the user expects the full
993+
// string they logged regardless of the evaluateToStringInDebugViews
994+
// setting.
995+
allowCallingToString: true,
996+
includeQuotesAroundString: false,
997+
);
986998
}
987999

9881000
var loggerName = await asString(record.loggerName);
@@ -996,13 +1008,13 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
9961008
final prefix = '[$loggerName] ';
9971009

9981010
if (message != null) {
999-
sendPrefixedOutput('stdout', prefix, '$message\n');
1011+
sendPrefixedOutput('console', prefix, '$message\n');
10001012
}
10011013
if (error != null) {
1002-
sendPrefixedOutput('stderr', prefix, '$error\n');
1014+
sendPrefixedOutput('console', prefix, '$error\n');
10031015
}
10041016
if (stack != null) {
1005-
sendPrefixedOutput('stderr', prefix, '$stack\n');
1017+
sendPrefixedOutput('console', prefix, '$stack\n');
10061018
}
10071019
}
10081020

pkg/dds/lib/src/dap/protocol_converter.dart

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,32 @@ class ProtocolConverter {
5757
required bool allowCallingToString,
5858
bool includeQuotesAroundString = true,
5959
}) async {
60-
final canCallToString = allowCallingToString &&
61-
(_adapter.args.evaluateToStringInDebugViews ?? false);
62-
63-
if (ref.kind == 'String' || ref.valueAsString != null) {
64-
var stringValue = ref.valueAsString.toString();
65-
if (ref.valueAsStringIsTruncated ?? false) {
66-
stringValue = '$stringValue…';
67-
}
68-
if (ref.kind == 'String' && includeQuotesAroundString) {
69-
stringValue = '"$stringValue"';
70-
}
71-
return stringValue;
60+
final isTruncated = ref.valueAsStringIsTruncated ?? false;
61+
if (ref.kind == vm.InstanceKind.kString && isTruncated) {
62+
// Call toString() if allowed, otherwise (or if it returns null) fall back
63+
// to the truncated value with "…" suffix.
64+
var stringValue = allowCallingToString
65+
? await _callToString(
66+
thread,
67+
ref,
68+
includeQuotesAroundString: includeQuotesAroundString,
69+
)
70+
: null;
71+
stringValue ??= '${ref.valueAsString}…';
72+
73+
return includeQuotesAroundString ? '"$stringValue"' : stringValue;
74+
} else if (ref.kind == vm.InstanceKind.kString) {
75+
// Untruncated strings.
76+
return includeQuotesAroundString
77+
? '"${ref.valueAsString}"'
78+
: ref.valueAsString.toString();
79+
} else if (ref.valueAsString != null) {
80+
return isTruncated
81+
? '${ref.valueAsString}…'
82+
: ref.valueAsString.toString();
7283
} else if (ref.kind == 'PlainInstance') {
7384
var stringValue = ref.classRef?.name ?? '<unknown instance>';
74-
if (canCallToString) {
85+
if (allowCallingToString) {
7586
final toStringValue = await _callToString(
7687
thread,
7788
ref,
@@ -99,6 +110,7 @@ class ProtocolConverter {
99110
Future<List<dap.Variable>> convertVmInstanceToVariablesList(
100111
ThreadInfo thread,
101112
vm.Instance instance, {
113+
required bool allowCallingToString,
102114
int? startItem = 0,
103115
int? numItems,
104116
}) async {
@@ -112,7 +124,7 @@ class ProtocolConverter {
112124
await convertVmResponseToVariable(
113125
thread,
114126
instance,
115-
allowCallingToString: true,
127+
allowCallingToString: allowCallingToString,
116128
)
117129
];
118130
} else if (elements != null) {
@@ -121,10 +133,15 @@ class ProtocolConverter {
121133
return Future.wait(elements
122134
.cast<vm.Response>()
123135
.sublist(start, numItems != null ? start + numItems : null)
124-
.mapIndexed((index, response) async => convertVmResponseToVariable(
125-
thread, response,
136+
.mapIndexed(
137+
(index, response) => convertVmResponseToVariable(
138+
thread,
139+
response,
126140
name: '${start + index}',
127-
allowCallingToString: index <= maxToStringsPerEvaluation)));
141+
allowCallingToString:
142+
allowCallingToString && index <= maxToStringsPerEvaluation,
143+
),
144+
));
128145
} else if (associations != null) {
129146
// For maps, create a variable for each entry (in the requested subset).
130147
// Use the keys and values to create a display string in the form
@@ -135,13 +152,14 @@ class ProtocolConverter {
135152
return Future.wait(associations
136153
.sublist(start, numItems != null ? start + numItems : null)
137154
.mapIndexed((index, mapEntry) async {
138-
final allowCallingToString = index <= maxToStringsPerEvaluation;
155+
final callToString =
156+
allowCallingToString && index <= maxToStringsPerEvaluation;
139157
final keyDisplay = await convertVmResponseToDisplayString(
140158
thread, mapEntry.key,
141-
allowCallingToString: allowCallingToString);
159+
allowCallingToString: callToString);
142160
final valueDisplay = await convertVmResponseToDisplayString(
143161
thread, mapEntry.value,
144-
allowCallingToString: allowCallingToString);
162+
allowCallingToString: callToString);
145163
return dap.Variable(
146164
name: '${start + index}',
147165
value: '$keyDisplay -> $valueDisplay',
@@ -154,7 +172,8 @@ class ProtocolConverter {
154172
(index, field) async => convertVmResponseToVariable(
155173
thread, field.value,
156174
name: field.decl?.name ?? '<unnamed field>',
157-
allowCallingToString: index <= maxToStringsPerEvaluation)));
175+
allowCallingToString:
176+
allowCallingToString && index <= maxToStringsPerEvaluation)));
158177

159178
// Also evaluate the getters if evaluateGettersInDebugViews=true enabled.
160179
final service = _adapter.vmService;
@@ -177,7 +196,8 @@ class ProtocolConverter {
177196
thread,
178197
response,
179198
name: getterName,
180-
allowCallingToString: index <= maxToStringsPerEvaluation,
199+
allowCallingToString:
200+
allowCallingToString && index <= maxToStringsPerEvaluation,
181201
);
182202
}
183203

@@ -397,18 +417,26 @@ class ProtocolConverter {
397417
if (service == null) {
398418
return null;
399419
}
400-
final result = await service.invoke(
420+
var result = await service.invoke(
401421
thread.isolate.id!,
402422
ref.id!,
403423
'toString',
404424
[],
405425
disableBreakpoints: true,
406426
);
407427

428+
// If the response is a string and is truncated, use getObject() to get the
429+
// full value.
430+
if (result is vm.InstanceRef &&
431+
result.kind == 'String' &&
432+
(result.valueAsStringIsTruncated ?? false)) {
433+
result = await service.getObject(thread.isolate.id!, result.id!);
434+
}
435+
408436
return convertVmResponseToDisplayString(
409437
thread,
410438
result,
411-
allowCallingToString: false,
439+
allowCallingToString: false, // Don't allow recursing.
412440
includeQuotesAroundString: includeQuotesAroundString,
413441
);
414442
}

pkg/dds/test/dap/integration/debug_logging_test.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'package:test/test.dart';
66

7+
import 'test_client.dart';
78
import 'test_support.dart';
89

910
main() {
@@ -36,6 +37,33 @@ void main(List<String> args) async {
3637
'Exited.',
3738
]);
3839
});
40+
41+
test('prints long messages from dart:developer log()', () async {
42+
// Make a long message that's more than 255 chars (where the VM truncates
43+
// log strings by default).
44+
final longMessage = 'this is a test' * 20;
45+
final testFile = dap.createTestFile('''
46+
import 'dart:developer';
47+
48+
void main(List<String> args) async {
49+
log('$longMessage');
50+
// Prevent us exiting before the async log messages may have completed.
51+
// The test will terminate the script early once the expectations are met.
52+
await Future.delayed(const Duration(seconds: 30));
53+
}
54+
''');
55+
final expectedLogMessage = '[log] $longMessage\n';
56+
57+
final consoleOutputs = dap.client.outputEvents
58+
.where((event) => event.category == 'console')
59+
.map((event) => event.output);
60+
61+
await Future.wait([
62+
expectLater(consoleOutputs, emitsThrough(expectedLogMessage)),
63+
dap.client.start(file: testFile),
64+
]);
65+
await dap.client.terminate();
66+
});
3967
// These tests can be slow due to starting up the external server process.
4068
}, timeout: Timeout.none);
4169
}

pkg/dds/test/dap/integration/no_debug_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'package:test/test.dart';
66

7+
import 'test_client.dart';
78
import 'test_support.dart';
89

910
main() {

pkg/dds/test/dap/integration/test_client.dart

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,6 @@ class DapTestClient {
5454
Stream<OutputEventBody> get outputEvents => events('output')
5555
.map((e) => OutputEventBody.fromJson(e.body as Map<String, Object?>));
5656

57-
/// Collects all output events until the program terminates.
58-
Future<List<OutputEventBody>> collectOutput(
59-
{File? file, Future<Response> Function()? launch}) async {
60-
final outputEventsFuture = outputEvents.toList();
61-
62-
// Launch script and wait for termination.
63-
await Future.wait([
64-
event('terminated'),
65-
initialize(),
66-
launch?.call() ?? this.launch(file!.path),
67-
], eagerError: true);
68-
69-
return outputEventsFuture;
70-
}
71-
7257
/// Sends a continue request for the given thread.
7358
///
7459
/// Returns a Future that completes when the server returns a corresponding
@@ -207,6 +192,19 @@ class DapTestClient {
207192
sendRequest(StackTraceArguments(
208193
threadId: threadId, startFrame: startFrame, levels: numFrames));
209194

195+
/// Initializes the debug adapter and launches [file] or calls the custom
196+
/// [launch] method.
197+
Future<void> start({
198+
File? file,
199+
Future<Response> Function()? launch,
200+
}) {
201+
// Launch script and wait for termination.
202+
return Future.wait([
203+
initialize(),
204+
launch?.call() ?? this.launch(file!.path),
205+
], eagerError: true);
206+
}
207+
210208
/// Sends a stepIn request for the given thread.
211209
///
212210
/// Returns a Future that completes when the server returns a corresponding
@@ -433,6 +431,21 @@ extension DapTestClientExtension on DapTestClient {
433431
return ThreadsResponseBody.fromJson(response.body as Map<String, Object?>);
434432
}
435433

434+
/// Collects all output events until the program terminates.
435+
///
436+
/// These results include all events in the order they are recieved, including
437+
/// console, stdout and stderr.
438+
Future<List<OutputEventBody>> collectOutput({
439+
File? file,
440+
Future<Response> Function()? launch,
441+
}) async {
442+
final outputEventsFuture = outputEvents.toList();
443+
444+
await start(file: file, launch: launch);
445+
446+
return outputEventsFuture;
447+
}
448+
436449
/// A helper that fetches scopes for a frame, checks for one with the name
437450
/// [expectedName] and verifies its variables.
438451
Future<Scope> expectScopeVariables(

0 commit comments

Comments
 (0)