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