[dds] Attach file/line/col metadata to DAP OutputEvents for detected call stacks
Change-Id: Ia91f40aaf244d892d2ff5ffb0e4a1e47f8ad9068
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/228040
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Ben Konyi <bkonyi@google.com>
diff --git a/pkg/dds/analysis_options.yaml b/pkg/dds/analysis_options.yaml
index 1f83658..6b1919a 100644
--- a/pkg/dds/analysis_options.yaml
+++ b/pkg/dds/analysis_options.yaml
@@ -5,3 +5,4 @@
- depend_on_referenced_packages
- directives_ordering
- prefer_generic_function_type_aliases
+ - prefer_relative_imports
diff --git a/pkg/dds/lib/src/cpu_samples_manager.dart b/pkg/dds/lib/src/cpu_samples_manager.dart
index 14b5c22..aafb15d 100644
--- a/pkg/dds/lib/src/cpu_samples_manager.dart
+++ b/pkg/dds/lib/src/cpu_samples_manager.dart
@@ -2,9 +2,9 @@
// 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:dds/src/common/ring_buffer.dart';
import 'package:vm_service/vm_service.dart';
+import 'common/ring_buffer.dart';
import 'dds_impl.dart';
/// Manages CPU sample caches for an individual [Isolate].
diff --git a/pkg/dds/lib/src/dap/adapters/dart.dart b/pkg/dds/lib/src/dap/adapters/dart.dart
index 5e1cda7..83c9d9d 100644
--- a/pkg/dds/lib/src/dap/adapters/dart.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart.dart
@@ -21,6 +21,7 @@
import '../protocol_converter.dart';
import '../protocol_generated.dart';
import '../protocol_stream.dart';
+import '../utils.dart';
/// The mime type to send with source responses to the client.
///
@@ -382,6 +383,20 @@
/// VM Service disconnects.
bool isTerminating = false;
+ /// Whether isolates that pause in the PauseExit state should be automatically
+ /// resumed after any in-process log events have completed.
+ ///
+ /// Normally this will be true, but it may be set to false if the user
+ /// also manually passes pause-isolates-on-exit.
+ bool resumeIsolatesAfterPauseExit = true;
+
+ /// A [Future] that completes when the last queued OutputEvent has been sent.
+ ///
+ /// Calls to [SendOutput] will reserve their place in this queue and
+ /// subsequent calls will chain their own sends onto this (and replace it) to
+ /// preserve order.
+ Future? _lastOutputEvent;
+
/// Removes any breakpoints or pause behaviour and resumes any paused
/// isolates.
///
@@ -903,7 +918,11 @@
}
/// Sends a [TerminatedEvent] if one has not already been sent.
- void handleSessionTerminate([String exitSuffix = '']) {
+ ///
+ /// Waits for any in-progress output events to complete first.
+ void handleSessionTerminate([String exitSuffix = '']) async {
+ await _waitForPendingOutputEvents();
+
if (_hasSentTerminatedEvent) {
return;
}
@@ -911,8 +930,9 @@
isTerminating = true;
_hasSentTerminatedEvent = true;
// Always add a leading newline since the last written text might not have
- // had one.
- sendOutput('console', '\nExited$exitSuffix.');
+ // had one. Send directly via sendEvent and not sendOutput to ensure no
+ // async since we're about to terminate.
+ sendEvent(OutputEventBody(output: '\nExited$exitSuffix.'));
sendEvent(TerminatedEventBody());
}
@@ -1100,9 +1120,32 @@
}
/// Sends an OutputEvent (without a newline, since calls to this method
- /// may be used by buffered data).
- void sendOutput(String category, String message) {
- sendEvent(OutputEventBody(category: category, output: message));
+ /// may be using buffered data that is not split cleanly on newlines).
+ ///
+ /// If [category] is `stderr`, will also look for stack traces and extract
+ /// file/line information to add to the metadata of the event.
+ ///
+ /// To ensure output is sent to the client in the correct order even if
+ /// processing stack frames requires async calls, this function will insert
+ /// output events into a queue and only send them when previous calls have
+ /// been completed.
+ void sendOutput(String category, String message) async {
+ // Reserve our place in the queue be inserting a future that we can complete
+ // after we have sent the output event.
+ final completer = Completer<void>();
+ final _previousEvent = _lastOutputEvent ?? Future.value();
+ _lastOutputEvent = completer.future;
+
+ try {
+ final outputEvents = await _buildOutputEvents(category, message);
+
+ // Chain our sends onto the end of the previous one, and complete our Future
+ // once done so that the next one can go.
+ await _previousEvent;
+ outputEvents.forEach(sendEvent);
+ } finally {
+ completer.complete();
+ }
}
/// Sends an OutputEvent for [message], prefixed with [prefix] and with [message]
@@ -1577,6 +1620,108 @@
return uri.replace(path: newPath);
}
+ /// Creates one or more OutputEvents for the provided [message].
+ ///
+ /// Messages that contain stack traces may be split up into seperate events
+ /// for each frame to allow location metadata to be attached.
+ Future<List<OutputEventBody>> _buildOutputEvents(
+ String category,
+ String message,
+ ) async {
+ try {
+ if (category == 'stderr') {
+ return await _buildStdErrOutputEvents(message);
+ } else {
+ return [OutputEventBody(category: category, output: message)];
+ }
+ } catch (e, s) {
+ // Since callers of [sendOutput] may not await it, don't allow unhandled
+ // errors (for example if the VM Service quits while we were trying to
+ // map URIs), just log and return the event without metadata.
+ logger?.call('Failed to build OutputEvent: $e, $s');
+ return [OutputEventBody(category: category, output: message)];
+ }
+ }
+
+ /// Builds OutputEvents for stderr.
+ ///
+ /// If a stack trace can be parsed from [message], file/line information will
+ /// be included in the metadata of the event.
+ Future<List<OutputEventBody>> _buildStdErrOutputEvents(String message) async {
+ final events = <OutputEventBody>[];
+
+ // Extract all the URIs so we can send a batch request for resolving them.
+ final lines = message.split('\n');
+ final frames = lines.map(parseStackFrame).toList();
+ final uris = frames.whereNotNull().map((f) => f.uri).toList();
+
+ // We need an Isolate to resolve package URIs. Since we don't know what
+ // isolate printed an error to stderr, we just have to use the first one and
+ // hope the packages are available. If one is not available (which should
+ // never be the case), we will just skip resolution.
+ final thread = _isolateManager.threads.firstOrNull;
+
+ // Send a batch request. This will cache the results so we can easily use
+ // them in the loop below by calling the method again.
+ if (uris.isNotEmpty) {
+ try {
+ await thread?.resolveUrisToPathsBatch(uris);
+ } catch (e, s) {
+ // Ignore errors that may occur if the VM is shutting down before we got
+ // this request out. In most cases we will have pre-cached the results
+ // when the libraries were loaded (in order to check if they're user code)
+ // so it's likely this won't cause any issues (dart:isolate-patch is an
+ // exception seen that appears in the stack traces but was not previously
+ // seen/cached).
+ logger?.call('Failed to resolve URIs: $e\n$s');
+ }
+ }
+
+ // Convert any URIs to paths.
+ final paths = await Future.wait(frames.map((frame) async {
+ final uri = frame?.uri;
+ if (uri == null) return null;
+ if (uri.isScheme('file')) return uri.toFilePath();
+ if (isResolvableUri(uri)) {
+ try {
+ return await thread?.resolveUriToPath(uri);
+ } catch (e, s) {
+ // Swallow errors for the same reason noted above.
+ logger?.call('Failed to resolve URIs: $e\n$s');
+ }
+ }
+ return null;
+ }));
+
+ for (var i = 0; i < lines.length; i++) {
+ final line = lines[i];
+ final frame = frames[i];
+ final uri = frame?.uri;
+ final path = paths[i];
+ // For the name, we usually use the package URI, but if we only ended up
+ // with a file URI, try to make it relative to cwd so it's not so long.
+ final name = uri != null && path != null
+ ? (uri.isScheme('file')
+ ? _converter.convertToRelativePath(path)
+ : uri.toString())
+ : null;
+ // Because we split on newlines, all items exept the last one need to
+ // have their trailing newlines added back.
+ final output = i == lines.length - 1 ? line : '$line\n';
+ events.add(
+ OutputEventBody(
+ category: 'stderr',
+ output: output,
+ source: path != null ? Source(name: name, path: path) : null,
+ line: frame?.line,
+ column: frame?.column,
+ ),
+ );
+ }
+
+ return events;
+ }
+
/// Handles evaluation of an expression that is (or begins with)
/// `threadExceptionExpression` which corresponds to the exception at the top
/// of [thread].
@@ -1618,6 +1763,18 @@
await debuggerInitialized;
await _isolateManager.handleEvent(event);
+
+ final eventKind = event.kind;
+ final isolate = event.isolate;
+ // We pause isolates on exit to allow requests for resolving URIs in
+ // stderr call stacks, so when we see an isolate pause, wait for any
+ // pending logs and then resume it (so it exits).
+ if (resumeIsolatesAfterPauseExit &&
+ eventKind == vm.EventKind.kPauseExit &&
+ isolate != null) {
+ await _waitForPendingOutputEvents();
+ await _isolateManager.resumeIsolate(isolate);
+ }
}
@protected
@@ -1857,6 +2014,20 @@
return (data) => _withErrorHandling(() => handler(data));
}
+ /// Waits for any pending async output events that might be in progress.
+ ///
+ /// If another output event is queued while waiting, the new event will be
+ /// waited for, until there are no more.
+ Future<void> _waitForPendingOutputEvents() async {
+ // Keep awaiting it as long as it's changing to allow for other
+ // events being queued up while it runs.
+ var lastEvent = _lastOutputEvent;
+ do {
+ lastEvent = _lastOutputEvent;
+ await lastEvent;
+ } while (lastEvent != _lastOutputEvent);
+ }
+
/// Calls a function with an error handler that handles errors that occur when
/// the VM Service/DDS shuts down.
///
diff --git a/pkg/dds/lib/src/dap/adapters/dart_cli_adapter.dart b/pkg/dds/lib/src/dap/adapters/dart_cli_adapter.dart
index 928f94a..06def69 100644
--- a/pkg/dds/lib/src/dap/adapters/dart_cli_adapter.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart_cli_adapter.dart
@@ -73,6 +73,14 @@
terminatePids(ProcessSignal.sigkill);
}
+ /// Checks whether [flag] is in [args], allowing for both underscore and
+ /// dash format.
+ bool _containsVmFlag(List<String> args, String flag) {
+ final flagUnderscores = flag.replaceAll('-', '_');
+ final flagDashes = flag.replaceAll('_', '-');
+ return args.contains(flagUnderscores) || args.contains(flagDashes);
+ }
+
/// Called by [launchRequest] to request that we actually start the app to be
/// run/debugged.
///
@@ -103,6 +111,17 @@
],
];
+ final toolArgs = args.toolArgs ?? [];
+ if (debug) {
+ // If the user has explicitly set pause-isolates-on-exit we need to
+ // not add it ourselves, and disable auto-resuming.
+ if (_containsVmFlag(toolArgs, '--pause_isolates_on_exit')) {
+ resumeIsolatesAfterPauseExit = false;
+ } else {
+ vmArgs.add('--pause_isolates_on_exit');
+ }
+ }
+
// Handle customTool and deletion of any arguments for it.
final executable = args.customTool ?? Platform.resolvedExecutable;
final removeArgs = args.customToolReplacesArgs;
@@ -112,7 +131,7 @@
final processArgs = [
...vmArgs,
- ...?args.toolArgs,
+ ...toolArgs,
args.program,
...?args.args,
];
diff --git a/pkg/dds/lib/src/dap/isolate_manager.dart b/pkg/dds/lib/src/dap/isolate_manager.dart
index b3fac35..ee48794 100644
--- a/pkg/dds/lib/src/dap/isolate_manager.dart
+++ b/pkg/dds/lib/src/dap/isolate_manager.dart
@@ -13,6 +13,7 @@
import 'adapters/dart.dart';
import 'exceptions.dart';
import 'protocol_generated.dart';
+import 'utils.dart';
/// Manages state of Isolates (called Threads by the DAP protocol).
///
@@ -215,8 +216,7 @@
));
}
- Future<void> resumeIsolate(vm.IsolateRef isolateRef,
- [String? resumeType]) async {
+ Future<void> resumeIsolate(vm.IsolateRef isolateRef) async {
final isolateId = isolateRef.id!;
final thread = _threadsByIsolateId[isolateId];
@@ -823,7 +823,7 @@
Future<List<String?>> resolveUrisToPathsBatch(List<Uri> uris) async {
// First find the set of URIs we don't already have results for.
final requiredUris = uris
- .where((uri) => !uri.isScheme('file'))
+ .where(isResolvableUri)
.where((uri) => !_resolvedPaths.containsKey(uri.toString()))
.toSet() // Take only distinct values.
.toList();
@@ -837,17 +837,24 @@
completers.forEach(
(uri, completer) => _resolvedPaths[uri] = completer.future,
);
- final results =
- await _manager._lookupResolvedPackageUris(isolate, requiredUris);
- if (results == null) {
- // If no result, all of the results are null.
- completers.forEach((uri, completer) => completer.complete(null));
- } else {
- // Otherwise, complete each one by index with the corresponding value.
- results.map(_convertUriToFilePath).forEachIndexed((i, result) {
- final uri = requiredUris[i].toString();
- completers[uri]!.complete(result);
- });
+ try {
+ final results =
+ await _manager._lookupResolvedPackageUris(isolate, requiredUris);
+ if (results == null) {
+ // If no result, all of the results are null.
+ completers.forEach((uri, completer) => completer.complete(null));
+ } else {
+ // Otherwise, complete each one by index with the corresponding value.
+ results.map(_convertUriToFilePath).forEachIndexed((i, result) {
+ final uri = requiredUris[i].toString();
+ completers[uri]!.complete(result);
+ });
+ }
+ } catch (e) {
+ // We can't leave dangling completers here because others may already
+ // be waiting on them, so propogate the error to them.
+ completers.forEach((uri, completer) => completer.completeError(e));
+ rethrow;
}
}
diff --git a/pkg/dds/lib/src/dap/utils.dart b/pkg/dds/lib/src/dap/utils.dart
new file mode 100644
index 0000000..fa47866
--- /dev/null
+++ b/pkg/dds/lib/src/dap/utils.dart
@@ -0,0 +1,42 @@
+// Copyright (c) 2022, 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:stack_trace/stack_trace.dart' as stack;
+
+/// Returns whether this URI has a scheme that can be resolved to a file path
+/// via the VM Service.
+bool isResolvableUri(Uri uri) {
+ return !uri.isScheme('file') &&
+ // Parsed stack frames may have URIs with no scheme and the text
+ // "unparsed" if they looked like stack frames but had no file
+ // information.
+ !uri.isScheme('');
+}
+
+/// Attempts to parse a line as a stack frame in order to read path/line/col
+/// information.
+///
+/// It should not be assumed that if a [stack.Frame] is returned that the input
+/// was necessarily a stack frame or that calling `toString` will return the
+/// original input text.
+stack.Frame? parseStackFrame(String line) {
+ /// Helper to try parsing a frame with [parser], returning `null` if it
+ /// fails to parse.
+ stack.Frame? tryParseFrame(stack.Frame Function(String) parser) {
+ final frame = parser(line);
+ return frame is stack.UnparsedFrame ? null : frame;
+ }
+
+ // Try different formats of stack frames.
+ // pkg:stack_trace does not have a generic Frame.parse() and Trace.parse()
+ // doesn't work well when the content includes non-stack-frame lines
+ // (https://github.com/dart-lang/stack_trace/issues/115).
+ return tryParseFrame((line) => stack.Frame.parseVM(line)) ??
+ // TODO(dantup): Tidy up when constructor tear-offs are available.
+ tryParseFrame((line) => stack.Frame.parseV8(line)) ??
+ tryParseFrame((line) => stack.Frame.parseSafari(line)) ??
+ tryParseFrame((line) => stack.Frame.parseFirefox(line)) ??
+ tryParseFrame((line) => stack.Frame.parseIE(line)) ??
+ tryParseFrame((line) => stack.Frame.parseFriendly(line));
+}
diff --git a/pkg/dds/lib/src/devtools/devtools_handler.dart b/pkg/dds/lib/src/devtools/devtools_handler.dart
index 3b55bfb..ad7067d 100644
--- a/pkg/dds/lib/src/devtools/devtools_handler.dart
+++ b/pkg/dds/lib/src/devtools/devtools_handler.dart
@@ -4,12 +4,12 @@
import 'dart:async';
-import 'package:dds/src/constants.dart';
import 'package:devtools_shared/devtools_server.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_static/shelf_static.dart';
import 'package:sse/server/sse_handler.dart';
+import '../constants.dart';
import '../dds_impl.dart';
import 'devtools_client.dart';
diff --git a/pkg/dds/pubspec.yaml b/pkg/dds/pubspec.yaml
index 92adad1..51e3d8d 100644
--- a/pkg/dds/pubspec.yaml
+++ b/pkg/dds/pubspec.yaml
@@ -23,6 +23,7 @@
shelf_proxy: ^1.0.0
shelf_static: ^1.0.0
shelf_web_socket: ^1.0.0
+ stack_trace: ^1.10.0
sse: ^4.0.0
stream_channel: ^2.0.0
vm_service: ^8.1.0
diff --git a/pkg/dds/test/dap/integration/debug_exceptions_test.dart b/pkg/dds/test/dap/integration/debug_exceptions_test.dart
index 752ce5f..157f136 100644
--- a/pkg/dds/test/dap/integration/debug_exceptions_test.dart
+++ b/pkg/dds/test/dap/integration/debug_exceptions_test.dart
@@ -70,6 +70,25 @@
exceptionPauseMode: 'All',
);
});
+
+ test('parses line/column information from stack traces', () async {
+ final client = dap.client;
+ final testFile = dap.createTestFile(simpleThrowingProgram);
+ final exceptionLine = lineWith(testFile, 'throw');
+ final outputEvents = await client.collectOutput(file: testFile);
+
+ // Find the output event for the top of the printed stack trace.
+ // It should look something like:
+ // #0 main (file:///var/folders/[...]/app3JZLvu/test_file.dart:2:5)
+ final mainStackFrameEvent = outputEvents
+ .firstWhere((event) => event.output.startsWith('#0 main'));
+
+ // Expect that there is metadata attached that matches the file/location we
+ // expect.
+ expect(mainStackFrameEvent.source?.path, testFile.path);
+ expect(mainStackFrameEvent.line, exceptionLine);
+ expect(mainStackFrameEvent.column, 5);
+ });
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
}
diff --git a/pkg/dds/test/dap/integration/debug_test.dart b/pkg/dds/test/dap/integration/debug_test.dart
index 6655b58..974692f 100644
--- a/pkg/dds/test/dap/integration/debug_test.dart
+++ b/pkg/dds/test/dap/integration/debug_test.dart
@@ -94,6 +94,76 @@
expect(proc!.exitCode, completes);
});
+ test('does not resume isolates if user passes --pause-isolates-on-exit',
+ () async {
+ // Internally we always pass --pause-isolates-on-exit and resume the
+ // isolates after waiting for any output events to complete (in case they
+ // need to resolve URIs that involve API calls on an Isolate).
+ //
+ // However if a user passes this flag explicitly, we should not
+ // auto-resume because they might be trying to debug something.
+ final testFile = dap.createTestFile(simpleArgPrintingProgram);
+
+ // Run the script, expecting a Stopped event.
+ final stop = dap.client.expectStop('pause');
+ await Future.wait([
+ stop,
+ dap.client.initialize(),
+ dap.client
+ .launch(testFile.path, toolArgs: ["--pause-isolates-on-exit"]),
+ ], eagerError: true);
+
+ // Resume and expect termination.
+ await await Future.wait([
+ dap.client.event('terminated'),
+ dap.client.continue_((await stop).threadId!),
+ ], eagerError: true);
+ });
+
+ test('sends output events in the correct order', () async {
+ // Output events that have their URIs mapped will be processed slowly due
+ // the async requests for resolving the package URI. This should not cause
+ // them to appear out-of-order with other lines that do not require this
+ // work.
+ //
+ // Use a sample program that prints output to stderr that includes:
+ // - non stack frame lines
+ // - stack frames with file:// URIs
+ // - stack frames with package URIs (that need asynchronously resolving)
+ final fileUri = Uri.file(dap.createTestFile('').path);
+ final packageUri = await dap.createFooPackage();
+ final testFile =
+ dap.createTestFile(stderrPrintingProgram(fileUri, packageUri));
+
+ var outputEvents = await dap.client.collectOutput(
+ launch: () => dap.client.launch(testFile.path),
+ );
+ outputEvents = outputEvents.where((e) => e.category == 'stderr').toList();
+
+ // Verify the order of the stderr output events.
+ final output = outputEvents
+ .map((e) => '${e.output.trim()}')
+ .where((output) => output.isNotEmpty)
+ .join('\n');
+ expectLines(output, [
+ 'Start',
+ '#0 main ($fileUri:1:2)',
+ '#1 main2 ($packageUri:1:2)',
+ 'End',
+ ]);
+
+ // As a sanity check, verify we did actually do the async path mapping and
+ // got both frames with paths in our test folder.
+ final stackFramesWithPaths = outputEvents.where((e) =>
+ e.source?.path != null &&
+ path.isWithin(dap.testDir.path, e.source!.path!));
+ expect(
+ stackFramesWithPaths,
+ hasLength(2),
+ reason: 'Expected two frames within path ${dap.testDir.path}',
+ );
+ });
+
test('provides a list of threads', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
diff --git a/pkg/dds/test/dap/integration/test_client.dart b/pkg/dds/test/dap/integration/test_client.dart
index ec3c7c0..8ce631d 100644
--- a/pkg/dds/test/dap/integration/test_client.dart
+++ b/pkg/dds/test/dap/integration/test_client.dart
@@ -224,6 +224,7 @@
Future<Response> launch(
String program, {
List<String>? args,
+ List<String>? toolArgs,
String? cwd,
bool? noDebug,
List<String>? additionalProjectPaths,
@@ -240,6 +241,7 @@
program: program,
cwd: cwd,
args: args,
+ toolArgs: toolArgs,
additionalProjectPaths: additionalProjectPaths,
console: console,
debugSdkLibraries: debugSdkLibraries,
diff --git a/pkg/dds/test/dap/integration/test_scripts.dart b/pkg/dds/test/dap/integration/test_scripts.dart
index 429018b..84eea06 100644
--- a/pkg/dds/test/dap/integration/test_scripts.dart
+++ b/pkg/dds/test/dap/integration/test_scripts.dart
@@ -55,6 +55,24 @@
}
''';
+/// A simple Dart script that prints to stderr without throwing/terminating.
+///
+/// The output will contain stack traces include both the supplied file and
+/// package URIs.
+String stderrPrintingProgram(Uri fileUri, Uri packageUri) {
+ return '''
+ import 'dart:io';
+ import '$packageUri';
+
+ void main(List<String> args) async {
+ stderr.writeln('Start');
+ stderr.writeln('#0 main ($fileUri:1:2)');
+ stderr.writeln('#1 main2 ($packageUri:1:2)');
+ stderr.write('End');
+ }
+''';
+}
+
/// Returns a simple Dart script that prints the provided string repeatedly.
String stringPrintingProgram(String text) {
// jsonEncode the string to get it into a quoted/escaped form that can be
diff --git a/pkg/dds/test/dap/integration/test_support.dart b/pkg/dds/test/dap/integration/test_support.dart
index d4c2a8b..ebabce8 100644
--- a/pkg/dds/test/dap/integration/test_support.dart
+++ b/pkg/dds/test/dap/integration/test_support.dart
@@ -133,15 +133,15 @@
class DapTestSession {
DapTestServer server;
DapTestClient client;
- final Directory _testDir =
+ final Directory testDir =
Directory.systemTemp.createTempSync('dart-sdk-dap-test');
late final Directory testAppDir;
late final Directory testPackagesDir;
DapTestSession._(this.server, this.client) {
- testAppDir = _testDir.createTempSync('app');
+ testAppDir = testDir.createTempSync('app');
createPubspec(testAppDir, 'my_test_project');
- testPackagesDir = _testDir.createTempSync('packages');
+ testPackagesDir = testDir.createTempSync('packages');
}
/// Adds package with [name] (optionally at [packageFolderUri]) to the
@@ -236,7 +236,7 @@
await server.stop();
// Clean up any temp folders created during the test runs.
- _testDir.deleteSync(recursive: true);
+ testDir.deleteSync(recursive: true);
}
static Future<DapTestSession> setUp({List<String>? additionalArgs}) async {