| // 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: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', () { |
| test('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); |
| }); |
| |
| 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('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); |
| }); |
| |
| test('stops at a line breakpoint and can be resumed', () 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 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( |
| '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); |
| }); |
| }, 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); |
| } |