// 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:async';
import 'dart:io';

import 'package:dap/dap.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';

import 'test_client.dart';
import 'test_scripts.dart';
import 'test_server.dart';
import 'test_support.dart';

main() {
  group('debug mode', () {
    late DapTestSession dap;
    setUp(() async {
      // Temporarily enable verbose logging to debug some flakes on the bots
      // https://github.com/dart-lang/sdk/issues/60187
      dap = await DapTestSession.setUp(forceVerboseLogging: true);
    });
    tearDown(() => dap.tearDown());

    test('runs a simple script', () async {
      final testFile = dap.createTestFile(simpleArgPrintingProgram);

      final outputEvents = await dap.client.collectOutput(
        launch: () => dap.client.launch(
          testFile.path,
          args: ['one', 'two'],
        ),
      );

      // Expect a "console" output event that prints the URI of the VM Service
      // the debugger connects to.
      final vmConnection = outputEvents.first;
      expect(vmConnection.output,
          startsWith('Connecting to VM Service at ws://127.0.0.1:'));
      expect(vmConnection.category, anyOf('console', isNull));

      // Expect the normal applications output.
      final output = outputEvents.skip(2).map((e) => e.output).join();
      expectLines(output, [
        'Hello!',
        'World!',
        'args: [one, two]',
        '',
        'Exited.',
      ]);
    });

    test('does not include empty output events when output ends with a newline',
        () async {
      final testFile = dap.createTestFile(simpleArgPrintingProgram);

      final outputEvents = await dap.client.collectOutput(
        launch: () => dap.client.launch(
          testFile.path,
          args: ['one', 'two'],
        ),
      );

      // The sample application uses `print()` which includes newlines on
      // each output. Output is split by `\n` when scanning for stack frames
      // and previously would include empty output events at the end if the
      // content ended with a newline.
      // https://github.com/flutter/flutter/pull/147250#issuecomment-2075128834
      for (var output in outputEvents) {
        expect(output.output, isNotEmpty);
      }
    });

    test('runs a simple script using runInTerminal request', () async {
      final testFile = dap.createTestFile(emptyProgram);

      // Set up a handler to handle the server calling the clients runInTerminal
      // request and capture the args.
      RunInTerminalRequestArguments? runInTerminalArgs;
      Process? proc;
      dap.client.handleRequest(
        'runInTerminal',
        (args) async {
          runInTerminalArgs = RunInTerminalRequestArguments.fromJson(
            args as Map<String, Object?>,
          );

          // Run the requested process (emulating what the editor would do) so
          // that the DA will pick up the service info file, connect to the VM,
          // resume, and then detect its termination.
          final runArgs = runInTerminalArgs!;
          proc = await Process.start(
            runArgs.args.first,
            runArgs.args.skip(1).toList(),
            workingDirectory: runArgs.cwd,
          );

          return RunInTerminalResponseBody(processId: proc!.pid);
        },
      );

      // Run the script until we get a TerminatedEvent.
      await Future.wait([
        dap.client.event('terminated'),
        dap.client.initialize(supportsRunInTerminalRequest: true),
        dap.client.launch(testFile.path, console: "terminal"),
      ], eagerError: true);

      expect(runInTerminalArgs, isNotNull);
      expect(proc, isNotNull);
      expect(
        runInTerminalArgs!.args,
        containsAllInOrder([
          Platform.resolvedExecutable,
          uppercaseDriveLetter(testFile.path),
        ]),
      );
      expect(proc!.pid, isPositive);
      expect(proc!.exitCode, completes);
    });

    test('runs a simple script with commas in the filename', () async {
      final (packageUri, _) = await dap.createFooPackage('foo,foo.dart');
      final testFile = dap.createTestFile(
        '''
          import '$packageUri';
          void main() {
            foo();
          }
        ''',
      );

      final outputEvents = await dap.client.collectOutput(
        launch: () => dap.client.launch(
          testFile.path,
          args: ['one', 'two'],
        ),
      );

      // Expect the normal applications output. This means we set up the
      // debugger without crashing, even though we imported files with commas
      // in the name.
      final output = outputEvents.skip(2).map((e) => e.output).join();
      expectLines(output, [
        'Hello!',
        'World!',
        'args: [one, two]',
        '',
        'Exited.',
      ]);
    }, skip: 'Fails because of https://github.com/dart-lang/sdk/issues/52632');

    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('exit');
      await Future.wait([
        stop,
        dap.client.initialize(),
        dap.client
            .launch(testFile.path, toolArgs: ["--pause-isolates-on-exit"]),
      ], eagerError: true);

      // Resume and expect termination.
      await Future.wait([
        dap.client.event('terminated'),
        dap.client.continue_((await stop).threadId!),
      ], eagerError: true);
    });

    test('does not resume isolates if user passes --pause-isolates-on-start',
        () async {
      // Internally we always pass --pause-isolates-on-start and resume the
      // isolates after setting any breakpoints.
      //
      // 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('entry');
      await Future.wait([
        stop,
        dap.client.initialize(),
        dap.client.launch(
          testFile.path,
          toolArgs: ["--pause-isolates-on-start"],
        ),
      ], eagerError: true);

      // Resume and expect termination.
      await Future.wait([
        dap.client.event('terminated'),
        dap.client.continue_((await stop).threadId!),
      ], eagerError: true);
    });

    test('receives thread, stopped, continued events during pause/resume',
        () async {
      final client = dap.client;
      final testFile = dap.createTestFile(debuggerPauseAndPrintManyProgram);

      // Collect interesting events that we want to verify exist and in the
      // right order.
      final interestingEvents = const {
        'thread',
        'stopped',
        'continued',
        'terminated'
      };
      final eventsFuture = client.allEvents
          .where((e) => interestingEvents.contains(e.event))
          .map((e) {
        // Create a descriptive string for verifying later.
        final body = e.body as Map<String, Object?>;
        const interestingFields = [
          'reason',
          'threadId',
          'allThreadsContinued',
          'allThreadsStopped'
        ];
        final description = interestingFields
            .where(body.containsKey)
            .map((field) => '$field: ${body[field]}')
            .join(', ');
        return description.isNotEmpty ? '${e.event} ($description)' : e.event;
      }).toList();

      // Start the program and wait to pause on `debugger()`.
      final stoppedFuture = client.expectStop('step');
      await client.start(file: testFile);
      final threadId = (await stoppedFuture).threadId!;

      // Step 3 times and wait for the corresponding stop.
      for (var i = 0; i < 3; i++) {
        client.next(threadId);
        await client.stoppedEvents.first;
      }

      // Resume to run to end.
      client.continue_(threadId);

      // Verify we had the expected events.
      expect(
        await eventsFuture,
        [
          'thread (reason: started, threadId: $threadId)',
          'stopped (reason: entry, threadId: $threadId, allThreadsStopped: false)',
          'continued (threadId: $threadId, allThreadsContinued: false)',
          // stop on debugger()
          'stopped (reason: step, threadId: $threadId, allThreadsStopped: false)',
          // step 1
          'continued (threadId: $threadId, allThreadsContinued: false)',
          'stopped (reason: step, threadId: $threadId, allThreadsStopped: false)',
          // step 2
          'continued (threadId: $threadId, allThreadsContinued: false)',
          'stopped (reason: step, threadId: $threadId, allThreadsStopped: false)',
          // step 3
          'continued (threadId: $threadId, allThreadsContinued: false)',
          'stopped (reason: step, threadId: $threadId, allThreadsStopped: false)',
          // continue
          'continued (threadId: $threadId, allThreadsContinued: false)',
          // pause-on-exit to drain stdout and handle looking up URIs
          'stopped (reason: exit, threadId: $threadId, allThreadsStopped: false)',
          // finished
          'thread (reason: exited, threadId: $threadId)',
          'terminated'
        ],
      );
    });

    for (final outputKind in ['stdout', 'stderr']) {
      test('sends $outputKind 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)
        // - stack frames with dart URIs (that need asynchronously resolving)
        final fileUri = Uri.file(dap.createTestFile('').path);
        final (packageUri, _) = await dap.createFooPackage();
        final dartUri = Uri.parse('dart:isolate-patch/isolate_patch.dart');
        final testFile = dap.createTestFile(
          stackPrintingProgram(outputKind, fileUri, packageUri, dartUri),
        );

        var outputEvents = await dap.client.collectOutput(
          launch: () => dap.client.launch(testFile.path),
        );
        outputEvents =
            outputEvents.where((e) => e.category == outputKind).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:3:4)',
          '#2      main3 ($dartUri:5:6)',
          '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(
          'fades $outputKind stack frames that are not part of our project when allowAnsiColorOutput=true',
          () async {
        // 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)
        // - stack frames with dart URIs (that need asynchronously resolving)
        final fileUri = Uri.file(dap.createTestFile('').path);
        final (packageUri, _) = await dap.createFooPackage();
        final dartUri = Uri.parse('dart:isolate-patch/isolate_patch.dart');
        final testFile = dap.createTestFile(
          stackPrintingProgram(outputKind, fileUri, packageUri, dartUri),
        );

        var outputEvents = await dap.client.collectOutput(
          launch: () => dap.client.launch(testFile.path,
              allowAnsiColorOutput: true,
              // Include package:foo as being user-code, to ensure it's not faded.
              additionalProjectPaths: [
                path.join(dap.testPackagesDir.path, 'foo'),
              ]),
        );
        outputEvents =
            outputEvents.where((e) => e.category == outputKind).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:3:4)',
          '\u001B[2m#2      main3 ($dartUri:5:6)\u001B[0m',
          'End',
        ]);
      });

      test(
          'includes correct Source.name for SDK and package sources in $outputKind output',
          () async {
        // 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)
        // - stack frames with dart URIs (that need asynchronously resolving)
        final fileUri = Uri.file(dap.createTestFile('').path);
        final (packageUri, _) = await dap.createFooPackage();
        final dartUri = Uri.parse('dart:isolate-patch/isolate_patch.dart');
        final testFile = dap.createTestFile(
          stackPrintingProgram(outputKind, fileUri, packageUri, dartUri),
        );

        final outputEvents = await dap.client.collectOutput(file: testFile);
        final outputSourceNames = outputEvents
            .where((e) => e.category == outputKind)
            .map((output) => output.source?.name)
            .where((sourceName) => (sourceName?.isNotEmpty ?? false))
            .toList();

        expect(
          outputSourceNames,
          [
            fileUri.toFilePath(),
            packageUri.toString(),
            dartUri.toString(),
          ],
        );
      });
    }

    group('progress notifications', () {
      /// Helper to verify [events] are the expected start/update/end events
      /// in-order for a debug session starting.
      void verifyLaunchProgressEvents(List<Event> events) {
        final bodies =
            events.map((e) => e.body as Map<String, Object?>).toList();
        final start = ProgressStartEventBody.fromMap(bodies[0]);
        final update = ProgressUpdateEventBody.fromMap(bodies[1]);
        final end = ProgressEndEventBody.fromMap(bodies[2]);

        expect(start.progressId, isNotNull);
        expect(start.title, 'Debugger');
        expect(start.message, 'Starting…');
        expect(update.progressId, start.progressId);
        expect(update.message, 'Connecting…');
        expect(end.progressId, start.progressId);
        expect(end.message, isNull);
      }

      test('sends no events by default', () async {
        final testFile = dap.createTestFile(simpleArgPrintingProgram);

        final standardEvents = dap.client.standardProgressEvents().toList();
        final customEvents = dap.client.customProgressEvents().toList();

        // Run the script to completion.
        await Future.wait([
          dap.client.event('terminated'),
          dap.client.initialize(),
          dap.client.launch(testFile.path),
        ], eagerError: true);

        expect(await standardEvents, isEmpty);
        expect(await customEvents, isEmpty);
      });

      test('sends standard events when supported', () async {
        final testFile = dap.createTestFile(simpleArgPrintingProgram);

        final standardEventsFuture =
            dap.client.standardProgressEvents().toList();
        final customEventsFuture = dap.client.customProgressEvents().toList();

        // Run the script to completion.
        await Future.wait([
          dap.client.event('terminated'),
          dap.client.initialize(
            supportsProgressReporting: true,
          ),
          dap.client.launch(testFile.path),
        ], eagerError: true);

        final standardEvents = await standardEventsFuture;
        final customEvents = await customEventsFuture;

        // Verify the standard launch events.
        expect(
          standardEvents.map((e) => e.event),
          ['progressStart', 'progressUpdate', 'progressEnd'],
        );
        verifyLaunchProgressEvents(standardEvents);
        // And no custom events.
        expect(customEvents, isEmpty);
      });

      test('sends custom events when requested', () async {
        final testFile = dap.createTestFile(simpleArgPrintingProgram);

        final standardEventsFuture =
            dap.client.standardProgressEvents().toList();
        final customEventsFuture = dap.client.customProgressEvents().toList();

        // Run the script to completion.
        await Future.wait([
          dap.client.event('terminated'),
          dap.client.initialize(),
          dap.client.launch(
            testFile.path,
            sendCustomProgressEvents: true,
          ),
        ], eagerError: true);

        final standardEvents = await standardEventsFuture;
        final customEvents = await customEventsFuture;

        // Verify no standard events.
        expect(standardEvents, isEmpty);
        // But custom events are sent.
        expect(
          customEvents.map((e) => e.event),
          ['dart.progressStart', 'dart.progressUpdate', 'dart.progressEnd'],
        );
        verifyLaunchProgressEvents(customEvents);
      });
    });

    test('provides a list of threads', () async {
      final client = dap.client;
      final testFile = dap.createTestFile(simpleBreakpointProgram);
      final breakpointLine = lineWith(testFile, breakpointMarker);

      await client.hitBreakpoint(testFile, breakpointLine);
      final response = await client.getValidThreads();

      expect(response.threads, hasLength(1));
      expect(response.threads.first.name, equals('main'));
    });

    test('runs with DDS by default', () async {
      final client = dap.client;
      final testFile = dap.createTestFile(simpleBreakpointProgram);
      final breakpointLine = lineWith(testFile, breakpointMarker);

      await client.hitBreakpoint(testFile, breakpointLine);
      expect(await client.ddsAvailable, isTrue);
    });

    test('runs with auth codes enabled by default', () async {
      final testFile = dap.createTestFile(emptyProgram);

      final outputEvents = await dap.client.collectOutput(file: testFile);
      final vmServiceUri = _extractVmServiceUri(outputEvents.first);
      expect(vmServiceUri.path, matches(vmServiceAuthCodePathPattern));
    });

    test('can map SDK source code to a local path', () async {
      final client = dap.client;
      final testFile = dap.createTestFile(simpleBreakpointProgram);
      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 to go into print.
      final responses = await Future.wait([
        client.expectStop('step', sourceName: 'dart:core/print.dart'),
        client.stepIn(stop.threadId!),
      ], eagerError: true);
      final stopResponse = responses.first as StoppedEventBody;

      // Fetch the top stack frame (which should be inside print).
      final stack = await client.getValidStack(
        stopResponse.threadId!,
        startFrame: 0,
        numFrames: 1,
      );
      final topFrame = stack.stackFrames.first;

      // SDK sources that have been mapped have no sourceReference but a path.
      expect(
        topFrame.source!.path,
        equals(path.join(sdkRoot, 'lib', 'core', 'print.dart')),
      );
      expect(topFrame.source!.sourceReference, isNull);
    });

    test('can shutdown during startup', () async {
      final testFile = dap.createTestFile(simpleArgPrintingProgram);

      // Request termination immediately upon receiving the first Thread event.
      // The DAP is also responding to this event to configure the isolate (eg.
      // set breakpoints and exception pause behaviour) and will cause it to
      // receive "Service has disappeared" responses if these are in-flight as
      // the process terminates. These should be silently discarded since they
      // are normal during shutdown.
      unawaited(dap.client.event('thread').then((_) => dap.client.terminate()));

      // Start the program and expect termination.
      await Future.wait([
        dap.client.event('terminated'),
        dap.client.start(file: testFile),
      ], eagerError: true);
    });

    test('can hot reload', () async {
      const originalText = 'ORIGINAL TEXT';
      const newText = 'NEW TEXT';

      // Create a script that prints 'ORIGINAL TEXT'.
      final testFile = dap.createTestFile(stringPrintingProgram(originalText));

      // Start the program and wait for 'ORIGINAL TEXT' to be printed.
      await Future.wait([
        dap.client.initialize(),
        dap.client.launch(testFile.path),
      ], eagerError: true);

      // Expect the original text.
      await dap.client.outputEvents
          .firstWhere((event) => event.output.trim() == originalText);

      // Update the file and hot reload.
      testFile.writeAsStringSync(stringPrintingProgram(newText), flush: true);
      // Set a future date to ensure hot reload detects it as being modified.
      testFile.setLastModifiedSync(
        DateTime.now().add(const Duration(seconds: 2)),
      );
      await dap.client.hotReload();

      // Expect the new text.
      await dap.client.outputEvents
          .firstWhere((event) => event.output.trim() == newText);

      await dap.client.terminate();

      // If we're running out of process, ensure the server process terminates.
      final server = dap.server;
      if (server is OutOfProcessDapTestServer) {
        await server.exitCode;
      }
    });

    test('can pause', () async {
      final testFile = dap.createTestFile(infiniteRunningProgram);

      // Start a program and hit a breakpoint.
      final client = dap.client;
      final threadFuture = client.threadEvents.first;

      // Start the app and wait for it to start printing output.
      await Future.wait([
        client.initialize(),
        client.launch(testFile.path),
        dap.client.outputEvents
            .firstWhere((event) => event.output.contains('Looping'))
      ]);

      // Ensure we can pause.
      final thread = await threadFuture;
      await Future.wait([
        client.expectStop('pause'),
        client.pause(thread.threadId),
      ], eagerError: true);
    });

    test('can restart frame', () async {
      final client = dap.client;
      final testFile = dap.createTestFile(restartFrameProgram);
      final breakpointLine = lineWith(testFile, breakpointMarker);
      final outputEventsFuture = dap.client.outputEvents.toList();

      // Stop at the breakpoint in the printMessage function.
      final stop = await client.hitBreakpoint(testFile, breakpointLine);
      final threadId = stop.threadId!;

      // Restart back to the main function and expect a new stop event.
      final stack =
          await client.getValidStack(threadId, startFrame: 0, numFrames: 2);
      final mainFunctionFrame = stack.stackFrames[1];
      await Future.wait([
        client.expectStop('step'),
        client.restartFrame(mainFunctionFrame.id),
      ], eagerError: true);

      // Resume, hit breakpoint again, resume to end.
      await Future.wait([
        client.expectStop('breakpoint'),
        client.continue_(threadId),
      ], eagerError: true);
      await Future.wait([
        client.event('terminated'),
        client.continue_(threadId),
      ], eagerError: true);

      // Finally, verify we got output that shows we restarted and re-ran the
      // code before the breakpoint.
      final outputEvents = await outputEventsFuture;
      final outputMessages = outputEvents.map((e) => e.output.trim());

      expect(
        outputMessages,
        containsAll(['Hello', 'Hello', 'World']),
      );
    });

    test('forwards tool events to client', () async {
      final testFile = dap.createTestFile(simpleToolEventProgram);

      // Capture any `dart.toolEvent` events.
      final toolEventsFuture = dap.client.events('dart.toolEvent').toList();

      // Run the script to completion.
      await Future.wait([
        dap.client.event('terminated'),
        dap.client.initialize(),
        dap.client.launch(testFile.path),
      ], eagerError: true);

      // Verify we got exactly the event in the sample program.
      final toolEvents = await toolEventsFuture;
      expect(toolEvents, hasLength(1));
      final toolEvent = toolEvents.single;
      expect(toolEvent.body, {
        'kind': 'navigate',
        'data': {
          'uri': 'file:///file.dart',
        },
      });
    });

    test('resolves URIs in tool events to file:///', () async {
      final client = dap.client;
      final testFile =
          dap.createTestFile(simpleToolEventWithDartCoreUriProgram);

      // Capture the `dart.toolEvent` event.
      final toolEventsFuture = client.events('dart.toolEvent').first;

      // Run the script until we get the event (which means mapping has
      // completed).
      await Future.wait([
        toolEventsFuture,
        client.initialize(),
        client.launch(testFile.path),
      ], eagerError: true);

      // Terminate the app (since the test script has a delay to ensure it
      // doesn't terminate before the async mapping code completes).
      await client.terminate();

      // Verify we got the right fileUri.
      final toolEvent = await toolEventsFuture;
      final body = toolEvent.body as Map<String, Object?>;
      final data = body['data'] as Map<String, Object?>;
      final uri = data['uri'] as String;
      final resolvedUri = data['resolvedUri'] as String;
      expect(uri, 'dart:core');
      expect(resolvedUri, startsWith('file:///'));
      expect(resolvedUri, endsWith('lib/core/core.dart'));
    });
    // These tests can be slow due to starting up the external server process.
  }, timeout: Timeout.none);

  group('debug mode', () {
    test('can be run without DDS using vmAdditionalArgs', () async {
      final dap = await DapTestSession.setUp();
      addTearDown(dap.tearDown);

      final client = dap.client;
      final testFile = dap.createTestFile(simpleBreakpointProgram);
      final breakpointLine = lineWith(testFile, breakpointMarker);

      await client.hitBreakpoint(testFile, breakpointLine,
          vmAdditionalArgs: ['--no-dds']);

      expect(await client.ddsAvailable, isFalse);
    });

    test('can run without auth codes using vmAdditionalArgs', () async {
      final dap = await DapTestSession.setUp();
      addTearDown(dap.tearDown);

      final testFile = dap.createTestFile(emptyProgram);
      final outputEvents = await dap.client.collectOutput(
        launch: () => dap.client.launch(testFile.path,
            vmAdditionalArgs: ['--disable-service-auth-codes']),
      );
      final vmServiceUri = _extractVmServiceUri(outputEvents.first);
      expect(vmServiceUri.path, isNot(matches(vmServiceAuthCodePathPattern)));
    });

    test('can run with ipv6 with a DAP flag', () async {
      final dap = await DapTestSession.setUp(additionalArgs: ['--ipv6']);
      addTearDown(dap.tearDown);

      final testFile = dap.createTestFile(emptyProgram);
      final outputEvents = await dap.client.collectOutput(file: testFile);
      final vmServiceUri = _extractVmServiceUri(outputEvents.first);

      expect(vmServiceUri.host, equals('::1'));
    });
    // These tests can be slow due to starting up the external server process.
  }, timeout: Timeout.none);
}

/// Extracts the VM Service URI from the "Connecting to ..." banner output by
/// the DAP server upon connection.
Uri _extractVmServiceUri(OutputEventBody vmConnectionBanner) {
  // TODO(dantup): Change this to use the dart.debuggerUris custom event
  //   if implemented (which VS Code also needs).
  final match = dapVmServiceBannerPattern.firstMatch(vmConnectionBanner.output);
  return Uri.parse(match!.group(1)!);
}
