| // 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:dap/dap.dart'; |
| import 'package:dds/src/dap/adapters/dart.dart'; |
| 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 evaluation', () { |
| test('evaluates expressions with simple results', () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(''' |
| void main(List<String> args) { |
| var a = 1; |
| var b = 2; |
| var c = 'test'; |
| print('Hello!'); $breakpointMarker |
| }'''); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| await client.expectEvalResult(topFrameId, 'a', '1'); |
| await client.expectEvalResult(topFrameId, 'a * b', '2'); |
| await client.expectEvalResult(topFrameId, 'c', '"test"'); |
| }); |
| |
| test('evaluates expressions with complex results', () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(simpleBreakpointProgram); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| final result = await client.expectEvalResult( |
| topFrameId, |
| 'DateTime(2000, 1, 1)', |
| 'DateTime', |
| ); |
| |
| // Check we got a variablesReference that maps on to the fields. |
| expect(result.variablesReference, isPositive); |
| await client.expectVariables( |
| result.variablesReference, |
| ''' |
| isUtc: false, eval: DateTime(2000, 1, 1).isUtc |
| ''', |
| ); |
| }); |
| |
| test('evaluates expressions ending with semicolons', () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(''' |
| void main(List<String> args) { |
| var a = 1; |
| var b = 2; |
| print('Hello!'); $breakpointMarker |
| }'''); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| await client.expectEvalResult(topFrameId, 'a + b;', '3'); |
| }); |
| |
| test('evaluates complex expressions with evaluateToStringInDebugViews=true', |
| () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(simpleBreakpointProgram); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| |
| final stop = await client.hitBreakpoint( |
| testFile, |
| breakpointLine, |
| launch: () => |
| client.launch(testFile.path, evaluateToStringInDebugViews: true), |
| ); |
| |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| await client.expectEvalResult( |
| topFrameId, |
| 'DateTime(2000, 1, 1)', |
| 'DateTime (2000-01-01 00:00:00.000)', |
| ); |
| }); |
| |
| test( |
| 'evaluates $threadExceptionExpression to the threads exception (simple type)', |
| () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(r''' |
| void main(List<String> args) { |
| throw 'my error'; |
| }'''); |
| |
| final stop = await client.hitException(testFile); |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| final result = await client.expectEvalResult( |
| topFrameId, |
| threadExceptionExpression, |
| '"my error"', |
| ); |
| expect(result.variablesReference, equals(0)); |
| }); |
| |
| test( |
| 'evaluates $threadExceptionExpression to the threads exception (complex type)', |
| () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(r''' |
| void main(List<String> args) { |
| throw Exception('my error'); |
| }'''); |
| |
| final stop = await client.hitException(testFile); |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| final result = await client.expectEvalResult( |
| topFrameId, |
| threadExceptionExpression, |
| '_Exception', |
| ); |
| expect(result.variablesReference, isPositive); |
| }); |
| |
| test( |
| 'evaluates $threadExceptionExpression.x.y to x.y on the threads exception', |
| () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(r''' |
| void main(List<String> args) { |
| throw Exception('12345'); |
| } |
| '''); |
| |
| final stop = await client.hitException(testFile); |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| await client.expectEvalResult( |
| topFrameId, |
| '$threadExceptionExpression.message.length', |
| '5', |
| ); |
| }); |
| |
| test('can evaluate expressions in non-top frames', () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(''' |
| void main(List<String> args) { |
| var a = 999; |
| foo(); |
| } |
| |
| void foo() { |
| var a = 111; $breakpointMarker |
| }'''); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| final stack = await client.getValidStack(stop.threadId!, |
| startFrame: 0, numFrames: 2); |
| final secondFrameId = stack.stackFrames[1].id; |
| |
| await client.expectEvalResult(secondFrameId, 'a', '999'); |
| }); |
| |
| test('returns the full message for evaluation errors', () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(simpleBreakpointProgram); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| expectResponseError( |
| client.evaluate( |
| '1 + "a"', |
| frameId: topFrameId, |
| ), |
| allOf([ |
| contains('evaluateInFrame: (113) Expression compilation error'), |
| contains("'String' can't be assigned to a variable of type 'num'."), |
| contains( |
| '1 + "a"\n' |
| ' ^', |
| ) |
| ]), |
| ); |
| }); |
| |
| test('returns short errors for evaluation in "watch" context', () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(simpleBreakpointProgram); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| expectResponseError( |
| client.evaluate( |
| '1 + "a"', |
| frameId: topFrameId, |
| context: 'watch', |
| ), |
| equals( |
| "A value of type 'String' can't be assigned " |
| "to a variable of type 'num'.", |
| ), |
| ); |
| }); |
| |
| test('returns truncated strings by default', () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(simpleBreakpointProgram); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| |
| final expectedTruncatedString = 'a' * 128; |
| await client.expectEvalResult( |
| topFrameId, |
| '"a" * 200', |
| '"$expectedTruncatedString…"', |
| ); |
| }); |
| |
| test('returns whole string without quotes when context is "clipboard"', |
| () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(simpleBreakpointProgram); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| |
| final expectedStringValue = 'a' * 200; |
| await client.expectEvalResult( |
| topFrameId, |
| '"a" * 200', |
| expectedStringValue, |
| context: 'clipboard', |
| ); |
| }); |
| |
| test('returns whole string with quotes when context is "repl"', () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(simpleBreakpointProgram); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| |
| final expectedStringValue = 'a' * 200; |
| await client.expectEvalResult( |
| topFrameId, |
| '"a" * 200', |
| '"$expectedStringValue"', |
| context: 'repl', |
| ); |
| }); |
| |
| test('returns whole string with quotes when context is a script', () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(simpleBreakpointProgram); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| |
| final expectedStringValue = 'a' * 200; |
| await client.expectEvalResult( |
| topFrameId, |
| '"a" * 200', |
| '"$expectedStringValue"', |
| context: Uri.file(testFile.path).toString(), |
| ); |
| }); |
| |
| test('variableReferences remain valid while an isolate is paused', |
| () async { |
| final client = dap.client; |
| final testFile = |
| dap.createTestFile(simpleBreakpointProgramWith50ExtraLines); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| |
| // We're paused at a breakpoint. Our evaluate results should continue to |
| // work even after GCs. |
| final threadId = stop.threadId!; |
| final topFrameId = await client.getTopFrameId(threadId); |
| |
| // Evaluate something and get back a variablesReference. |
| final result = await client.expectEvalResult( |
| topFrameId, |
| 'DateTime(2000, 1, 1)', |
| 'DateTime', |
| ); |
| |
| // Ensure it remains valid even after GCs. This ensures both DAP preserves |
| // the variablesReference, and also that the VM preserved the instance |
| // reference. |
| for (var i = 0; i < 5; i++) { |
| await client.expectVariables( |
| result.variablesReference, |
| 'isUtc: false, eval: DateTime(2000, 1, 1).isUtc', |
| ); |
| // Force GC. |
| await client.forceGc(threadId); |
| } |
| }); |
| |
| test('variableReferences become invalid after a resume', () async { |
| final client = dap.client; |
| final testFile = |
| dap.createTestFile(simpleBreakpointProgramWith50ExtraLines); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| final stop = await client.hitBreakpoint( |
| testFile, |
| breakpointLine, |
| additionalBreakpoints: [breakpointLine + 1], |
| ); |
| |
| // We're paused at a breakpoint. Our evaluate results should only continue |
| // to work until we resume. |
| final threadId = stop.threadId!; |
| final topFrameId = await client.getTopFrameId(threadId); |
| |
| // Evaluate something and get back a variablesReference. |
| final evalResult = await client.expectEvalResult( |
| topFrameId, |
| 'DateTime(2000, 1, 1)', |
| 'DateTime', |
| ); |
| |
| // Resume, which should invalidate the variablesReference. |
| await client.continue_(threadId); |
| |
| // Verify the reference is no longer valid. This is because we clear |
| // the threads variable data on resume, and not because of VM Service |
| // Zone IDs. We have no way to validate the VM behaviour because we |
| // don't have the reference (it is abstracted from the DAP client) to |
| // verify. |
| expectResponseError( |
| client.variables(evalResult.variablesReference), |
| equals('Bad state: variablesReference is no longer valid'), |
| ); |
| }); |
| |
| test('variableReferences become invalid after a step', () async { |
| final client = dap.client; |
| final testFile = |
| dap.createTestFile(simpleBreakpointProgramWith50ExtraLines); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| |
| // We're paused at a breakpoint. Our evaluate results should only continue |
| // to work until we resume. |
| final threadId = stop.threadId!; |
| final topFrameId = await client.getTopFrameId(threadId); |
| |
| // Evaluate something and get back a variablesReference. |
| final evalResult = await client.expectEvalResult( |
| topFrameId, |
| 'DateTime(2000, 1, 1)', |
| 'DateTime', |
| ); |
| |
| // Step, which should also invalidate because we're basically unpausing |
| // and then pausing again. |
| await client.next(threadId); |
| |
| // Verify the reference is no longer valid. This is because we clear |
| // the threads variable data on resume/step, and not because of VM Service |
| // Zone IDs. We have no way to validate the VM behaviour because we |
| // don't have the reference (it is abstracted from the DAP client) to |
| // verify. |
| expectResponseError( |
| client.variables(evalResult.variablesReference), |
| equals('Bad state: variablesReference is no longer valid'), |
| ); |
| }); |
| |
| test('evaluation service zones are invalidated on resume', () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(simpleBreakpointProgram); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| |
| // Enable logging and capture any requests to the VM that are calling |
| // invalidateIdZone. |
| final loggedEvaluateInFrameRequests = client |
| .events('dart.log') |
| .map((event) => event.body as Map<String, Object?>) |
| .map((body) => body['message'] as String) |
| .where((message) => message.contains('"method":"invalidateIdZone"')) |
| .toList(); |
| await client.custom('updateSendLogsToClient', {'enabled': true}); |
| |
| // Trigger an evaluation because evaluation zones are created lazily |
| // and we won't invalidate anything if we haven't created one. |
| await client.evaluate('0', frameId: stop.threadId!); |
| |
| // Resume and wait for the app to terminate. |
| await Future.wait([ |
| client.continue_(stop.threadId!), |
| client.event('terminated'), |
| ]); |
| |
| // Verify that invalidate had been called. |
| expect( |
| await loggedEvaluateInFrameRequests, |
| isNotEmpty, |
| ); |
| }); |
| |
| test('evaluation service zones are invalidated on step', () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(simpleBreakpointProgram); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| |
| // Enable logging and capture any requests to the VM that are calling |
| // invalidateIdZone. |
| final loggedEvaluateInFrameRequests = client |
| .events('dart.log') |
| .map((event) => event.body as Map<String, Object?>) |
| .map((body) => body['message'] as String) |
| .where((message) => message.contains('"method":"invalidateIdZone"')) |
| .toList(); |
| await client.custom('updateSendLogsToClient', {'enabled': true}); |
| |
| // Trigger an evaluation because evaluation zones are created lazily |
| // and we won't invalidate anything if we haven't created one. |
| await client.evaluate('0', frameId: stop.threadId!); |
| |
| // Step, which should also invalidate because we're basically unpausing |
| // and then pausing again. |
| await client.next(stop.threadId!); |
| |
| // Then disable logging (so we don't get false positives from shutdown) |
| // and terminate. |
| await client.custom('updateSendLogsToClient', {'enabled': false}); |
| await Future.wait([ |
| client.terminate(), |
| client.event('terminated'), |
| ]); |
| |
| // Verify that invalidate had been called. |
| expect( |
| await loggedEvaluateInFrameRequests, |
| isNotEmpty, |
| ); |
| }); |
| |
| group('global evaluation', () { |
| test('can evaluate in a bin/ file when not paused given a bin/ URI', |
| () async { |
| final client = dap.client; |
| await dap.createFooPackage(); |
| final testFile = dap.createTestFile(globalEvaluationProgram); |
| |
| await Future.wait([ |
| client.initialize(), |
| client.launch(testFile.path), |
| ], eagerError: true); |
| |
| // Wait for a '.' to be printed to know the script is up and running. |
| await dap.client.outputEvents |
| .firstWhere((event) => event.output.trim() == '.'); |
| |
| await client.expectGlobalEvalResult( |
| 'myGlobal', |
| '"Hello, world!"', |
| context: Uri.file(testFile.path).toString(), |
| ); |
| }); |
| |
| test('can evaluate when not paused given a lib/ URI', () async { |
| final client = dap.client; |
| final (_, libFile) = await dap.createFooPackage(); |
| final binFile = dap.createTestFile(globalEvaluationProgram); |
| |
| await Future.wait([ |
| client.initialize(), |
| client.launch(binFile.path), |
| ], eagerError: true); |
| |
| // Wait for a '.' to be printed to know the script is up and running. |
| await dap.client.outputEvents |
| .firstWhere((event) => event.output.trim() == '.'); |
| |
| await client.expectGlobalEvalResult( |
| 'fooGlobal', |
| '"Hello, foo!"', |
| context: Uri.file(libFile.path).toString(), |
| ); |
| }); |
| |
| test('returns a suitable error with no context', () async { |
| const expectedErrorMessage = 'Evaluation is only supported when the ' |
| 'debugger is paused unless you have a Dart file active in the ' |
| 'editor'; |
| |
| final client = dap.client; |
| await dap.createFooPackage(); |
| final testFile = dap.createTestFile(globalEvaluationProgram); |
| |
| await Future.wait([ |
| client.initialize(), |
| client.launch(testFile.path), |
| ], eagerError: true); |
| |
| // Wait for a '.' to be printed to know the script is up and running. |
| await dap.client.outputEvents |
| .firstWhere((event) => event.output.trim() == '.'); |
| |
| final response = await client.sendRequest( |
| EvaluateArguments( |
| expression: 'myGlobal', |
| ), |
| allowFailure: true, |
| ); |
| expect(response.success, isFalse); |
| expect(response.message, expectedErrorMessage); |
| |
| // Also verify the structured error body. |
| final body = response.body as Map<String, Object?>; |
| final error = body['error'] as Map<String, Object?>; |
| final variables = error['variables'] as Map<String, Object?>; |
| expect(error['format'], '{message}'); |
| expect(error['showUser'], false); |
| expect(variables['message'], expectedErrorMessage); |
| expect(variables['stack'], isNotNull); |
| }); |
| |
| test('returns a suitable error with an unknown script context', () async { |
| final client = dap.client; |
| await dap.createFooPackage(); |
| final testFile = dap.createTestFile(globalEvaluationProgram); |
| |
| await Future.wait([ |
| client.initialize(), |
| client.launch(testFile.path), |
| ], eagerError: true); |
| |
| // Wait for a '.' to be printed to know the script is up and running. |
| await dap.client.outputEvents |
| .firstWhere((event) => event.output.trim() == '.'); |
| |
| final context = |
| Uri.file(testFile.path.replaceAll('.dart', '_invalid.dart')) |
| .toString(); |
| final response = await client.sendRequest( |
| EvaluateArguments( |
| expression: 'myGlobal', |
| context: context, |
| ), |
| allowFailure: true, |
| ); |
| expect(response.success, isFalse); |
| expect( |
| response.message, |
| contains('Unable to find the library for file:'), |
| ); |
| }); |
| }); |
| |
| group('format specifiers', () { |
| test('",nq" suppresses quotes on strings', () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(''' |
| void main(List<String> args) { |
| var myString = 'test'; |
| print('Hello!'); $breakpointMarker |
| }'''); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| await client.expectEvalResult(topFrameId, 'myString', '"test"'); |
| await client.expectEvalResult(topFrameId, 'myString,nq', 'test'); |
| }); |
| |
| test('",h" renders numbers in hex', () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(''' |
| void main(List<String> args) { |
| var i = 12345; |
| print('Hello!'); $breakpointMarker |
| }'''); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| await client.expectEvalResult(topFrameId, 'i', '12345'); |
| await client.expectEvalResult(topFrameId, 'i,h', '0x3039'); |
| }); |
| |
| test('",d" renders numbers in decimal', () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(''' |
| void main(List<String> args) { |
| var i = 12345; |
| print('Hello!'); $breakpointMarker |
| }'''); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| await client.expectEvalResult(topFrameId, 'i', '12345'); |
| await client.expectEvalResult(topFrameId, 'i,d', '12345'); |
| }); |
| |
| test('apply to child values', () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(''' |
| void main(List<String> args) { |
| var myItems = [12345, 34567]; |
| print('Hello!'); $breakpointMarker |
| }'''); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| final result = await client.expectEvalResult( |
| topFrameId, 'myItems,h', 'List (2 items)'); |
| |
| // Check we got a variablesReference and fetching the child items uses |
| // the requested formatting. |
| expect(result.variablesReference, isPositive); |
| await client.expectVariables( |
| result.variablesReference, |
| ''' |
| [0]: 0x3039, eval: myItems[0] |
| [1]: 0x8707, eval: myItems[1] |
| ''', |
| ); |
| }); |
| |
| test('multiple can be applied in any order', () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(''' |
| void main(List<String> args) { |
| var myItems = [12345, 'test']; |
| print('Hello!'); $breakpointMarker |
| }'''); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| for (final expression in ['myItems,h,nq', 'myItems,nq,h']) { |
| final result = await client.expectEvalResult( |
| topFrameId, expression, 'List (2 items)'); |
| |
| // Check we got a variablesReference and fetching the child items uses |
| // the requested formatting. |
| expect(result.variablesReference, isPositive); |
| await client.expectVariables( |
| result.variablesReference, |
| ''' |
| [0]: 0x3039, eval: myItems[0] |
| [1]: test, eval: myItems[1] |
| ''', |
| ); |
| } |
| }); |
| }); |
| |
| group('value formats', () { |
| test('supports format.hex in evaluation arguments', () async { |
| final client = dap.client; |
| final testFile = dap.createTestFile(''' |
| void main(List<String> args) { |
| var i = 12345; |
| print('Hello!'); $breakpointMarker |
| }'''); |
| final breakpointLine = lineWith(testFile, breakpointMarker); |
| |
| final stop = await client.hitBreakpoint(testFile, breakpointLine); |
| final topFrameId = await client.getTopFrameId(stop.threadId!); |
| await client.expectEvalResult( |
| topFrameId, |
| 'i', |
| '0x3039', |
| format: ValueFormat(hex: true), |
| ); |
| }); |
| }); |
| // These tests can be slow due to starting up the external server process. |
| }, timeout: Timeout.none); |
| } |