blob: 401182462a76f7e7f5ce2b6915ada8f7ab0cba66 [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 '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(additionalArgs: ['--test']);
// For "dart run test:test" to work we must always have a cwd set.
dap.client.defaultCwd = dap.testAppDir.path;
await dap.addPackageDependency(dap.testAppDir, 'test');
});
tearDown(() => dap.tearDown());
group('dart test', () {
/// A helper that verifies a full set of expected test results for the
/// [simpleTestProgram] script.
void expectStandardSimpleTestResults(TestEvents events) {
// Check we received all expected test events passed through from
// package:test.
final eventNames =
events.testNotifications.map((e) => e['type']).toList();
// start/done should always be first/last.
expect(eventNames.first, equals('start'));
expect(eventNames.last, equals('done'));
// allSuites should have occurred after start.
expect(
eventNames,
containsAllInOrder(['start', 'allSuites']),
);
// Expect two tests, with the failing one emitting an error.
expect(
eventNames,
containsAllInOrder([
'testStart',
'testDone',
'testStart',
'error',
'testDone',
]),
);
}
test('can run without debugging', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleTestProgram);
// Collect output and test events while running the script.
final outputEvents = await client.collectTestOutput(
launch: () => client.launch(
testFile.path,
noDebug: true,
args: ['--chain-stack-traces'], // to suppress warnings in the output
),
);
// Check the printed output shows that the run finished, and it's exit
// code (which is 1 due to the failing test).
final output = outputEvents.output.map((e) => e.output).join();
expectLines(output, simpleTestProgramExpectedOutput);
expectStandardSimpleTestResults(outputEvents);
});
test('can run a single test', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleTestProgram);
// Collect output and test events while running the script.
final outputEvents = await client.collectTestOutput(
launch: () => client.launch(
testFile.path,
noDebug: true,
// It's up to the calling IDE to pass the correct args for 'dart test'
// if it wants to run a subset of tests.
args: [
'--plain-name',
'passing test',
],
),
);
final testsNames = outputEvents.testNotifications
.where((e) => e['type'] == 'testStart')
.map((e) => (e['test'] as Map<String, Object?>)['name'])
.toList();
expect(testsNames, contains('group 1 passing test'));
expect(testsNames, isNot(contains('group 1 failing test')));
});
test('includes absolute paths in OutputEvent metadata', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleFailingTestProgram);
// Collect output and test events while running the script.
final outputEvents = await client.collectTestOutput(
// Set noDebug:true because we shouldn't require a VM Service connection
// to map file paths.
launch: () => client.launch(testFile.path, noDebug: true),
);
// Collect paths from any OutputEvents that had them.
final stackFramePaths = outputEvents.output
.map((event) => event.source?.path)
.nonNulls
.toList();
// Ensure we had a frame with the absolute path of the test script.
expect(stackFramePaths, contains(uppercaseDriveLetter(testFile.path)));
});
test('can hit and resume from a breakpoint', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleTestProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
// Collect output and test events while running the script.
final outputEvents = await client.collectTestOutput(
// When launching, hit a breakpoint and resume.
start: () => client.hitBreakpoint(
testFile,
breakpointLine,
args: ['--chain-stack-traces'], // to suppress warnings in the output
).then((stop) => client.continue_(stop.threadId!)),
);
// Check the usual output and test events to ensure breaking/resuming did
// not affect the results.
final output = outputEvents.output
.map((e) => e.output)
.skipWhile(dapVmServiceBannerPattern.hasMatch)
.join();
expectLines(output, simpleTestProgramExpectedOutput);
expectStandardSimpleTestResults(outputEvents);
});
test('has locations for all function stack frames', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleTestProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
final stop = await client.hitBreakpoint(
testFile,
breakpointLine,
args: ['--chain-stack-traces'], // to suppress warnings in the output
);
// Check the top few frames all have locations.
final stack = await client.getValidStack(
stop.threadId!,
startFrame: 0,
numFrames: 5,
);
for (final frame in stack.stackFrames) {
// Skip labels frames (eg. "<async gap>").
if (frame.presentationHint == "label") continue;
expect(frame.line, isPositive);
expect(frame.column, isPositive);
}
});
test('can cleanly terminate from a breakpoint', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleTestProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
// Hit the breakpoint inside the test.
await client.hitBreakpoint(testFile, breakpointLine);
// Send a single terminate, and expect a clean exit (with a `terminated`
// event).
await client.terminate();
});
test('resolves modified breakpoints', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleTestMultiBreakpointProgram);
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!);
}
}
await pumpEventQueue(times: 5000);
await breakpointResolveSubscription.cancel();
// Ensure every breakpoint that was added was also resolved.
expect(resolvedBreakpoints, addedBreakpoints);
});
test('responds to setBreakpoints before any breakpoint events', () async {
final client = dap.client;
final testFile =
dap.createTestFile(simpleTestBreakpointProgramWith50ExtraLines);
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 lines 1-50 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('rejects attaching', () async {
final client = dap.client;
final outputEvents = await client.collectTestOutput(
launch: () => client.attach(
vmServiceUri: 'ws://bogus.local/',
autoResumeOnEntry: false,
autoResumeOnExit: false,
),
);
final output = outputEvents.output.map((e) => e.output).join();
expectLines(output, [
'Attach is not supported for test runs',
'',
'Exited.',
]);
});
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
}