blob: 365e60d9d5e91eefca652e06d8a2538d1511edd6 [file] [log] [blame]
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
// for details. 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:path/path.dart' as path;
import 'package:test/test.dart';
import 'test_client.dart';
import 'test_scripts.dart';
import 'test_support.dart';
main() {
late DapTestSession dap;
setUp(() async {
dap = await DapTestSession.setUp();
});
tearDown(() => dap.tearDown());
group('debug mode breakpoints', () {
testWithUriConfigurations(() => dap, 'stops at a line breakpoint',
() async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
await client.hitBreakpoint(testFile, breakpointLine);
});
testWithUriConfigurations(() => dap, 'resolves modified breakpoints',
() async {
final client = dap.client;
final testFile = dap.createTestFile(simpleMultiBreakpointProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
// Start the app and hit the initial breakpoint.
await client.hitBreakpoint(testFile, breakpointLine);
// Collect IDs of all breakpoints that get resolved.
final resolvedBreakpoints = <int>{};
final breakpointResolveSubscription =
client.breakpointChangeEvents.listen((event) {
if (event.breakpoint.verified) {
resolvedBreakpoints.add(event.breakpoint.id!);
} else {
resolvedBreakpoints.remove(event.breakpoint.id!);
}
});
// Add breakpoints to the 4 lines after the current one, one at a time.
// Capture the IDs of all breakpoints added.
final breakpointLinesToSend = <int>[breakpointLine];
final addedBreakpoints = <int>{};
for (var i = 1; i <= 4; i++) {
breakpointLinesToSend.add(breakpointLine + i);
final response =
await client.setBreakpoints(testFile, breakpointLinesToSend);
for (final breakpoint in response.breakpoints) {
addedBreakpoints.add(breakpoint.id!);
}
}
// Wait up to a few seconds for the resolved events to come through to
// allow for slow CI bots, but exit early if they all arrived.
final testUntil = DateTime.now().toUtc().add(const Duration(seconds: 5));
while (DateTime.now().toUtc().isBefore(testUntil) &&
resolvedBreakpoints.length < addedBreakpoints.length) {
await pumpEventQueue(times: 5000);
}
await breakpointResolveSubscription.cancel();
// Ensure every breakpoint that was added was also resolved.
expect(resolvedBreakpoints, addedBreakpoints);
});
testWithUriConfigurations(
() => dap, 'provides reason for failed breakpoints', () async {
final client = dap.client;
final testFile = dap.createTestFile(debuggerPauseProgram);
final invalidBreakpointLine = 9999;
// Start running the app.
await Future.wait([
client.initialize(),
client.launch(testFile.path),
], eagerError: true);
// Collect reasons from all breakpoint events.
final breakpointReasons = <String?>[];
final breakpointChangedSubscription =
client.breakpointChangeEvents.listen((event) {
breakpointReasons
.add('${event.breakpoint.reason}: ${event.breakpoint.message}');
});
// Set the breakpoint and also collect the original reason.
var bps = await client.setBreakpoint(testFile, invalidBreakpointLine);
var bp = bps.breakpoints.single;
breakpointReasons.add('${bp.reason}: ${bp.message}');
// Wait up to a few seconds for the change events to come through to
// allow for slow CI bots, but exit early if they all arrived.
final testUntil = DateTime.now().toUtc().add(const Duration(seconds: 5));
while (DateTime.now().toUtc().isBefore(testUntil) &&
breakpointReasons.length < 2) {
await pumpEventQueue(times: 5000);
}
expect(
breakpointReasons,
equals([
'pending: Breakpoint has not yet been resolved',
'failed: No debuggable code where breakpoint was requested'
]));
await breakpointChangedSubscription.cancel();
});
testWithUriConfigurations(
() => dap, 'provides reason for not-yet-resolved breakpoints',
() async {
final client = dap.client;
final testFile = dap.createTestFile(debuggerPauseProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
// Start running the app.
await Future.wait([
client.initialize(),
client.launch(testFile.path),
], eagerError: true);
// Set a breakpoint and verify the result.
var bps = await client.setBreakpoint(testFile, breakpointLine);
expect(bps.breakpoints.single.reason, 'pending');
expect(bps.breakpoints.single.message,
'Breakpoint has not yet been resolved');
});
test('responds to setBreakpoints before any breakpoint events', () async {
final client = dap.client;
final testFile =
dap.createTestFile(simpleBreakpointProgramWith50ExtraLines);
final setBreakpointLine = lineWith(testFile, breakpointMarker);
// Run the app and get to a breakpoint. This will allow us to add new
// breakpoints in the same file that are _immediately_ resolved.
await client.hitBreakpoint(testFile, setBreakpointLine);
// Call setBreakpoint again, and ensure it response before we get any
// breakpoint change events because we require their IDs before the change
// events are sent.
var setBreakpointsResponded = false;
await Future.wait([
client.breakpointChangeEvents.first.then((_) {
if (!setBreakpointsResponded) {
throw 'breakpoint change event arrived before '
'setBreakpoints completed';
}
}),
client
// Send 50 breakpoints for the next 50 lines to ensure we spend some
// time sending requests to the VM to allow events to start coming
// back from the VM before we complete. Without this, the test can
// pass even without the fix.
.setBreakpoints(testFile,
List.generate(50, (index) => setBreakpointLine + index))
.then((_) => setBreakpointsResponded = true),
]);
});
test('does not stop at a removed breakpoint', () async {
final testFile = dap.createTestFile('''
void main(List<String> args) async {
print('Hello!'); $breakpointMarker
print('Hello!'); $breakpointMarker
}
''');
final client = dap.client;
final breakpoint1Line = lineWith(testFile, breakpointMarker);
final breakpoint2Line = breakpoint1Line + 1;
// Hit the first breakpoint.
final stop = await client.hitBreakpoint(testFile, breakpoint1Line,
additionalBreakpoints: [breakpoint2Line]);
// Remove all breakpoints.
await client.setBreakpoints(testFile, []);
// Resume and expect termination (should not hit the second breakpoint).
await Future.wait([
client.event('terminated'),
client.continue_(stop.threadId!),
], eagerError: true);
});
test('does not fail updating breakpoints after a removal', () async {
// https://github.com/flutter/flutter/issues/106369 was caused by us not
// tracking removals correctly, meaning we could try to remove a removed
// breakpoint a second time.
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
await client.hitBreakpoint(testFile, breakpointLine);
// Remove the breakpoint.
await client.setBreakpoints(testFile, []);
// Send another breakpoint update to ensure it doesn't try to re-remove
// the previously removed breakpoint.
await client.setBreakpoints(testFile, []);
});
test(
'does not fail updating breakpoints after a removal '
'if two breakpoints resolved to the same location', () async {
final client = dap.client;
final testFile =
dap.createTestFile(simpleBreakpointWithLeadingBlankLineProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
final beforeBreakpointLine = breakpointLine - 1;
// Hit the breakpoint line first to ensure the function is compiled. This
// is required to ensure the VM gives us back the same breakpoint ID for
// the two locations.
await client.hitBreakpoint(testFile, breakpointLine);
// Add breakpoints to both lines.
await client.setBreakpoints(
testFile,
[breakpointLine, beforeBreakpointLine],
);
// Remove all breakpoints (which in reality, is just one).
await client.setBreakpoints(testFile, []);
});
test('stops at a line breakpoint in the SDK set via local sources',
() async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
// Add the breakpoint to the first line inside the SDK's print function.
final sdkFile = File(path.join(sdkRoot, 'lib', 'core', 'print.dart'));
final breakpointLine = lineWith(sdkFile, 'print(Object? object) {') + 1;
await client.hitBreakpoint(sdkFile, breakpointLine, entryFile: testFile);
});
/// Tests hitting a simple breakpoint and resuming.
Future<void> testHitBreakpointAndResume() async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(testFile, breakpointLine);
// Resume and expect termination (as the script will get to the end).
await Future.wait([
client.event('terminated'),
client.continue_(stop.threadId!),
], eagerError: true);
}
test('stops at a line breakpoint and can be resumed', () async {
await testHitBreakpointAndResume();
});
test('stops at a line breakpoint and can step over (next)', () async {
final testFile = dap.createTestFile('''
void main(List<String> args) async {
print('Hello!'); $breakpointMarker
print('Hello!'); $stepMarker
}
''');
final breakpointLine = lineWith(testFile, breakpointMarker);
final stepLine = lineWith(testFile, stepMarker);
// Hit the initial breakpoint.
final stop = await dap.client.hitBreakpoint(testFile, breakpointLine);
// Step and expect stopping on the next line with a 'step' stop type.
await Future.wait([
dap.client.expectStop('step', file: testFile, line: stepLine),
dap.client.next(stop.threadId!),
], eagerError: true);
});
test(
'stops at a line breakpoint and can step over (next) '
'when stepping granularity was included', () async {
final testFile = dap.createTestFile('''
void main(List<String> args) async {
print('Hello!'); $breakpointMarker
print('Hello!'); $stepMarker
}
''');
final breakpointLine = lineWith(testFile, breakpointMarker);
final stepLine = lineWith(testFile, stepMarker);
// Hit the initial breakpoint.
final stop = await dap.client.hitBreakpoint(testFile, breakpointLine);
// Step and expect stopping on the next line with a 'step' stop type.
await Future.wait([
dap.client.expectStop('step', file: testFile, line: stepLine),
dap.client.next(stop.threadId!, granularity: 'statement'),
], eagerError: true);
});
test('ignores resume request for an exited isolate', () async {
final client = dap.client;
final testFile = dap.createTestFile(isolateSpawningProgram);
// Run the script and wait for the isolate to exit.
final threadExitFuture =
client.threadEvents.where((event) => event.reason == 'exited').first;
await Future.wait([
threadExitFuture,
client.initialize(),
client.launch(testFile.path),
], eagerError: true);
final exitedThreadId = (await threadExitFuture).threadId;
// Try to resume the already-exited thread. It should not fail.
await client.continue_(exitedThreadId);
});
test(
'stops at a line breakpoint and can step over (next) an async boundary',
() async {
final client = dap.client;
final testFile = dap.createTestFile('''
Future<void> main(List<String> args) async {
await asyncPrint('Hello!'); $breakpointMarker
await asyncPrint('Hello!'); $stepMarker
}
Future<void> asyncPrint(String message) async {
await Future.delayed(const Duration(milliseconds: 1));
}
''');
final breakpointLine = lineWith(testFile, breakpointMarker);
final stepLine = lineWith(testFile, stepMarker);
// Hit the initial breakpoint.
final stop = await dap.client.hitBreakpoint(testFile, breakpointLine);
// The first step will move from `asyncPrint` to the `await`.
await Future.wait([
client.expectStop('step', file: testFile, line: breakpointLine),
client.next(stop.threadId!),
], eagerError: true);
// The next step should go over the async boundary and to stepLine (if
// we did not correctly send kOverAsyncSuspension we would end up in
// the asyncPrint method).
await Future.wait([
client.expectStop('step', file: testFile, line: stepLine),
client.next(stop.threadId!),
], eagerError: true);
});
test('stops at a line breakpoint and can step in', () async {
final client = dap.client;
final testFile = dap.createTestFile('''
void main(List<String> args) async {
log('Hello!'); $breakpointMarker
}
void log(String message) { $stepMarker
print(message);
}
''');
final breakpointLine = lineWith(testFile, breakpointMarker);
final stepLine = lineWith(testFile, stepMarker);
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(testFile, breakpointLine);
// Step and expect stopping in the inner function with a 'step' stop type.
await Future.wait([
client.expectStop('step', file: testFile, line: stepLine),
client.stepIn(stop.threadId!),
], eagerError: true);
});
test('stops at a line breakpoint and can step out', () async {
final client = dap.client;
final testFile = dap.createTestFile('''
void main(List<String> args) async {
log('Hello!');
log('Hello!'); $stepMarker
}
void log(String message) {
print(message); $breakpointMarker
}
''');
final breakpointLine = lineWith(testFile, breakpointMarker);
final stepLine = lineWith(testFile, stepMarker);
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(testFile, breakpointLine);
// Step and expect stopping in the inner function with a 'step' stop type.
await Future.wait([
client.expectStop('step', file: testFile, line: stepLine),
client.stepOut(stop.threadId!),
], eagerError: true);
});
test('does not step into SDK code with debugSdkLibraries=false', () async {
final client = dap.client;
final testFile = dap.createTestFile('''
void main(List<String> args) async {
print('Hello!'); $breakpointMarker
print('Hello!'); $stepMarker
}
''');
final breakpointLine = lineWith(testFile, breakpointMarker);
final stepLine = lineWith(testFile, stepMarker);
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(
testFile,
breakpointLine,
launch: () => client.launch(
testFile.path,
debugSdkLibraries: false,
),
);
// Step in and expect stopping on the next line (don't go into print).
await Future.wait([
client.expectStop('step', file: testFile, line: stepLine),
client.stepIn(stop.threadId!),
], eagerError: true);
});
test('steps into SDK code with debugSdkLibraries=true', () async {
final client = dap.client;
final testFile = dap.createTestFile('''
void main(List<String> args) async {
print('Hello!'); $breakpointMarker
print('Hello!');
}
''');
final breakpointLine = lineWith(testFile, breakpointMarker);
// Hit the initial breakpoint.
final stop = await dap.client.hitBreakpoint(
testFile,
breakpointLine,
launch: () => client.launch(
testFile.path,
debugSdkLibraries: true,
),
);
// Step in and expect to go into print.
await Future.wait([
client.expectStop('step', sourceName: 'dart:core/print.dart'),
client.stepIn(stop.threadId!),
], eagerError: true);
});
test(
'does not step into external package code with debugExternalPackageLibraries=false',
() async {
final client = dap.client;
final (otherPackageUri, _) = await dap.createFooPackage();
final testFile = dap.createTestFile('''
import '$otherPackageUri';
void main(List<String> args) async {
foo(); $breakpointMarker
foo(); $stepMarker
}
''');
final breakpointLine = lineWith(testFile, breakpointMarker);
final stepLine = lineWith(testFile, stepMarker);
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(
testFile,
breakpointLine,
launch: () => client.launch(
testFile.path,
debugExternalPackageLibraries: false,
),
);
// Step in and expect stopping on the next line (don't go into the package).
await Future.wait([
client.expectStop('step', file: testFile, line: stepLine),
client.stepIn(stop.threadId!),
], eagerError: true);
});
test(
'steps into external package code with debugExternalPackageLibraries=true',
() async {
final client = dap.client;
final (otherPackageUri, _) = await dap.createFooPackage();
final testFile = dap.createTestFile('''
import '$otherPackageUri';
void main(List<String> args) async {
foo(); $breakpointMarker
foo();
}
''');
final breakpointLine = lineWith(testFile, breakpointMarker);
// Hit the initial breakpoint.
final stop = await dap.client.hitBreakpoint(
testFile,
breakpointLine,
launch: () => client.launch(
testFile.path,
debugExternalPackageLibraries: true,
),
);
// Step in and expect to go into the package.
await Future.wait([
client.expectStop('step', sourceName: '$otherPackageUri'),
client.stepIn(stop.threadId!),
], eagerError: true);
});
test(
'steps into other-project package code with debugExternalPackageLibraries=false',
() async {
final client = dap.client;
final (otherPackageUri, _) = await dap.createFooPackage();
final testFile = dap.createTestFile('''
import '$otherPackageUri';
void main(List<String> args) async {
foo(); $breakpointMarker
foo();
}
''');
final breakpointLine = lineWith(testFile, breakpointMarker);
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(
testFile,
breakpointLine,
launch: () => client.launch(
testFile.path,
debugExternalPackageLibraries: false,
// Include the packages folder as an additional project path so that
// it will be treated as local code.
additionalProjectPaths: [dap.testPackagesDir.path],
),
);
// Step in and expect stopping in the other package.
await Future.wait([
client.expectStop('step', sourceName: '$otherPackageUri'),
client.stepIn(stop.threadId!),
], eagerError: true);
});
test('allows changing debug settings during session', () async {
final client = dap.client;
final testFile = dap.createTestFile('''
void main(List<String> args) async {
print('Hello!'); $breakpointMarker
print('Hello!'); $stepMarker
}
''');
final breakpointLine = lineWith(testFile, breakpointMarker);
final stepLine = lineWith(testFile, stepMarker);
// Start with debugSdkLibraries _enabled_ and hit the breakpoint.
final stop = await client.hitBreakpoint(
testFile,
breakpointLine,
launch: () => client.launch(
testFile.path,
debugSdkLibraries: true,
),
);
// Turn off debugSdkLibraries.
await client.custom('updateDebugOptions', {
'debugSdkLibraries': false,
});
// Step in and expect stopping on the next line (don't go into print
// because we turned off SDK debugging).
await Future.wait([
client.expectStop('step', file: testFile, line: stepLine),
client.stepIn(stop.threadId!),
], eagerError: true);
});
test('handles breakpoints correctly in newly spawned isolates', () async {
// When calling debugger(), the stop reason is "step" because we can't
// tell the difference between a pause from debugger() and one from
// stepping.
const debuggerStopReason = 'step';
final client = dap.client;
final testFile =
dap.createTestFile(multiIsolateBreakpointResolutionProgram);
final breakpoint1Line = lineWith(testFile, '$breakpointMarker 1');
final breakpoint2Line = lineWith(testFile, '$breakpointMarker 2');
// Start the app and wait for it to pause.
unawaited(client.start(file: testFile));
final mainIsolateStop = await client.expectStop(debuggerStopReason);
// Add and remove a breakpoint to consume "breakpoints/1" in the main
// isolate.
await client.setBreakpoints(testFile, [breakpoint1Line]);
await client.setBreakpoints(testFile, []);
// Resume so that the new isolate spawns.
client.continue_(mainIsolateStop.threadId!);
final otherIsolateStop = await client.expectStop(debuggerStopReason);
// Make sure the stop we got was a new isolate.
expect(otherIsolateStop.threadId!, isNot(mainIsolateStop.threadId!));
// Send the other breakpoint and verify it resolves to the expected line.
client.setBreakpoints(testFile, [breakpoint2Line]);
final bpResolved = await client.breakpointChangeEvents
.firstWhere((e) => e.reason == 'changed');
expect(bpResolved.breakpoint.line, breakpoint2Line);
});
test('does not fail if two debug clients resume the same thread', () async {
final testFile = dap.createTestFile(infiniteRunningProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
// Start a program and hit a breakpoint.
final client1 = dap.client;
final stop1 = await client1.hitBreakpoint(testFile, breakpointLine);
final vmServiceUri = (await client1.vmServiceUri)!;
final thread1 = stop1.threadId!;
// Attach a second debug adapter to it.
final dap2 = await DapTestSession.setUp(logPrefix: '(CLIENT2) ');
final client2 = dap2.client;
await Future.wait([
// We'll still get event for existing pause.
client2.expectStop('breakpoint'),
client2.start(
launch: () => client2.attach(
vmServiceUri: vmServiceUri.toString(),
autoResumeOnEntry: false,
autoResumeOnExit: false,
cwd: dap.testAppDir.path,
),
),
]);
final thread2 = (await client2.getValidThreads()).threads.single.id;
// Send resumes to both and ensure they complete without errors.
await Future.wait([
client1.continue_(thread1),
client2.continue_(thread2),
], eagerError: true);
await dap2.tearDown();
});
}, timeout: Timeout.none);
group('debug mode conditional breakpoints', () {
test('stops with condition evaluating to true', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
await client.hitBreakpoint(
testFile,
breakpointLine,
condition: '1 == 1',
);
});
test('does not stop with condition evaluating to false', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
await client.doNotHitBreakpoint(
testFile,
breakpointLine,
condition: '1 == 2',
);
});
test('stops with condition evaluating to non-zero', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
await client.hitBreakpoint(
testFile,
breakpointLine,
condition: '1 + 1',
);
});
test('does not stop with condition evaluating to zero', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
await client.doNotHitBreakpoint(
testFile,
breakpointLine,
condition: '1 - 1',
);
});
test('reports evaluation errors for conditions', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
final outputEventsFuture = client.outputEvents.toList();
await client.doNotHitBreakpoint(
testFile,
breakpointLine,
condition: "1 + 'a'",
);
final outputEvents = await outputEventsFuture;
final outputMessages = outputEvents.map((e) => e.output);
final hasPrefix = startsWith(
'Debugger failed to evaluate breakpoint condition "1 + \'a\'": '
'evaluateInFrame: (113) Expression compilation error');
final hasDescriptiveMessage = contains(
"A value of type 'String' can't be assigned to a variable of type 'num'");
expect(
outputMessages,
containsAll([allOf(hasPrefix, hasDescriptiveMessage)]),
);
});
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
group('debug mode logpoints', () {
/// A helper that tests a LogPoint using [logMessage] and expecting the
/// script not to pause and [expectedMessage] to show up in the output.
Future<void> testLogPoint(
DapTestSession dap,
String logMessage,
String expectedMessage,
) async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
final outputEventsFuture = client.outputEvents.toList();
await client.doNotHitBreakpoint(
testFile,
breakpointLine,
logMessage: logMessage,
);
final outputEvents = await outputEventsFuture;
final outputMessages = outputEvents.map((e) => e.output.trim());
expect(
outputMessages,
contains(expectedMessage),
);
}
test('print simple messages', () async {
await testLogPoint(
dap,
r'This is a test message',
'This is a test message',
);
});
test('print messages with Dart interpolation', () async {
await testLogPoint(
dap,
r'This is a test message in ${DateTime(2000, 1, 1).year}',
'This is a test message in ${DateTime(2000, 1, 1).year}',
);
});
test('print messages with just {braces}', () async {
await testLogPoint(
dap,
// The DAP spec says "Expressions within {} are interpolated" so in the DA
// we just prefix them with $ and treat them like other Dart interpolation
// expressions.
r'This is a test message in {DateTime(2000, 1, 1).year}',
'This is a test message in ${DateTime(2000, 1, 1).year}',
);
});
test('allows \\{escaped braces}', () async {
await testLogPoint(
dap,
// Since we treat things in {braces} as expressions, we need to support
// escaping them.
r'This is a test message with \{escaped braces}',
r'This is a test message with {escaped braces}',
);
});
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
}