src/debugAdapter: add substitutePath config for debugging

This change adds a new configuration option to both launch and
attach requests. substituePath takes an array that maps from string to string
that is used to translate paths passed to the debugger and then
back to the client.

This allows users to translate their symlinked directories to the
files that were actually used to build the binary. In addition this
can also be used for remote debugging, and when the location of the
files has moved since the program was built.

Update golang/vscode-go#622

Change-Id: I71b081d17a29655c14cd20093dc9f88867fbcc69
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/270017
Trust: Suzy Mueller <suzmue@golang.org>
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Run-TryBot: Suzy Mueller <suzmue@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Polina Sokolova <polina@google.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/docs/debugging.md b/docs/debugging.md
index 48c1bfe..5ae9ddb 100644
--- a/docs/debugging.md
+++ b/docs/debugging.md
@@ -110,7 +110,8 @@
 showLog    | If `true`, Delve logs will be printed in the Debug Console panel.
 logOutput  | Comma-separated list of Delve components (`debugger`, `gdbwire`, `lldbout`, `debuglineerr`, `rpc`) that should produce debug output when `showLog` is `true`.
 buildFlags | Build flags to pass to the Go compiler.
-remotePath | If remote debugging (`mode`: `remote`), this should be the absolute path to the package being debugged on the remote machine. See the section on [Remote Debugging](#remote-debugging) for further details. [golang/vscode-go#45](https://github.com/golang/vscode-go/issues/45) is also relevant.
+remotePath | If remote debugging (`mode`: `remote`), this should be the absolute path to the package being debugged on the remote machine. See the section on [Remote Debugging](#remote-debugging) for further details. [golang/vscode-go#45](https://github.com/golang/vscode-go/issues/45) is also relevant. Becomes the first mapping in substitutePath.
+substitutePath | An array of mappings from an absolute local path to an absolute remote path that is used by the debuggee. The debug adapter will replace the local path with the remote path in all of the calls. The mappings are applied in order, and the first matching mapping is used. This can be used to map files that have moved since the program was built, different remote paths, and symlinked files or directories. This is intended to be equivalent to the [substitute-path]((https://github.com/go-delve/delve/tree/master/Documentation/cli#config)(https://github.com/go-delve/delve/tree/master/Documentation/cli#config)) configuration, and will eventually configure substitute-path in Delve directly.
 cwd | The working directory to be used in running the program. If remote debugging (`mode`: `remote`), this should be the absolute path to the working directory being debugged on the local machine. See the section on [Remote Debugging](#remote-debugging) for further details. [golang/vscode-go#45](https://github.com/golang/vscode-go/issues/45) is also relevant.
 processId  | This is the process ID of the executable you want to debug. Applicable only when using the `attach` request in `local` mode.
 
@@ -308,7 +309,27 @@
 
 In the example, the VS Code debugger will run on the same machine as the headless `dlv` server. Make sure to update the `port` and `host` settings to point to your remote machine.
 
-`remotePath` should point to the absolute path of the program being debugged in the remote machine. `cwd` should point to the absolute path of the working directory of the program being debugged on your local machine. This should be the counterpart of the folder in `remotePath`. See [golang/vscode-go#45](https://github.com/golang/vscode-go/issues/45) for updates regarding `remotePath` and `cwd`.
+`remotePath` should point to the absolute path of the program being debugged in the remote machine. `cwd` should point to the absolute path of the working directory of the program being debugged on your local machine. This should be the counterpart of the folder in `remotePath`. See [golang/vscode-go#45](https://github.com/golang/vscode-go/issues/45) for updates regarding `remotePath` and `cwd`. You can also use the equivalent `substitutePath` configuration.
+
+```json5
+{
+    "name": "Launch remote",
+    "type": "go",
+    "request": "attach",
+    "mode": "remote",
+    "substitutePath": [
+		{
+			"from": "/absolute/path/dir/on/local/machine",
+			"to": "/absolute/path/dir/on/remote/machine",
+		},
+	],
+    "port": 2345,
+    "host": "127.0.0.1",
+    "cwd": "/absolute/path/dir/on/local/machine",
+}
+```
+
+If you do not set, `remotePath` or `substitutePath`, then the debug adapter will attempt to infer the path mappings. See [golang/vscode-go#45](https://github.com/golang/vscode-go/issues/45) for more information.
 
 When you run the `Launch remote` target, VS Code will send debugging commands to the `dlv` server you started, instead of launching it's own `dlv` instance against your program.
 
@@ -405,9 +426,25 @@
 
 ### Debugging symlink directories
 
-This extension does not provide support for debugging projects containing symlinks. Make sure that you are setting breakpoints in the files that Go will use to compile your program.
+Since the debugger and go compiler use the actual filenames, extra configuration is required to debug symlinked directories. Use the `substitutePath` property to tell the debugAdapter how to properly translate the paths. For example, if your project lives in `/path/to/actual/helloWorld`, but the project is open in vscode under the linked folder `/path/to/hello`, you can add the following to your config to set breakpoints in the files in `/path/to/hello`:
 
-For updates to symlink support reference [golang/vscode-go#622](https://github.com/golang/vscode-go/issues/622).
+```json5
+{
+    "name": "Launch remote",
+    "type": "go",
+    "request": "launch",
+    "mode": "auto",
+    "program": "/path/to/hello",
+    "substitutePath": [
+		{
+			"from": "/path/to/hello",
+			"to": "/path/to/actual/helloWorld",
+		},
+	],
+}
+```
+
+This extension does not provide general support for debugging projects containing symlinks. If `substitutePath` does not meet your needs, please consider commenting on this issue that contains updates to symlink support reference [golang/vscode-go#622](https://github.com/golang/vscode-go/issues/622).
 
 [Delve]: https://github.com/go-delve/delve
 [VS Code variables]: https://code.visualstudio.com/docs/editor/variables-reference
diff --git a/package.json b/package.json
index c3663b7..6c4809c 100644
--- a/package.json
+++ b/package.json
@@ -522,6 +522,26 @@
                 "description": "Environment variables passed to the program.",
                 "default": {}
               },
+              "substitutePath": {
+                "type": "array",
+                "items": {
+                  "type": "object",
+                  "properties": {
+                    "from": {
+                      "type": "string",
+                      "description": "The absolute local path to be replaced when passing paths to the debugger",
+                      "default": ""
+                    },
+                    "to": {
+                      "type": "string",
+                      "description": "The absolute remote path to be replaced when passing paths back to the client",
+                      "default": ""
+                    }
+                  }
+                },
+                "description": "An array of mappings from a local path to the remote path that is used by the debuggee. The debug adapter will replace the local path with the remote path in all of the calls. Overriden by remotePath.",
+                "default": []
+              },
               "buildFlags": {
                 "type": "string",
                 "description": "Build flags, to be passed to the Go compiler.",
@@ -534,7 +554,7 @@
               },
               "remotePath": {
                 "type": "string",
-                "description": "Absolute path to the file being debugged on the remote machine in case of remote debugging.",
+                "description": "Absolute path to the file being debugged on the remote machine in case of remote debugging. If specified, becomes the first entry in substitutePath.",
                 "default": ""
               },
               "port": {
@@ -680,7 +700,7 @@
               },
               "remotePath": {
                 "type": "string",
-                "description": "If remote debugging, the path to the source code on the remote machine, if different from the local machine.",
+                "description": "If remote debugging, the path to the source code on the remote machine, if different from the local machine. If specified, becomes the first entry in substitutePath.",
                 "default": ""
               },
               "port": {
@@ -693,6 +713,26 @@
                 "description": "The host name of the machine the delve debugger will be listening on.",
                 "default": "127.0.0.1"
               },
+              "substitutePath": {
+                "type": "array",
+                "items": {
+                  "type": "object",
+                  "properties": {
+                    "from": {
+                      "type": "string",
+                      "description": "The absolute local path to be replaced when passing paths to the debugger",
+                      "default": ""
+                    },
+                    "to": {
+                      "type": "string",
+                      "description": "The absolute remote path to be replaced when passing paths back to the client",
+                      "default": ""
+                    }
+                  }
+                },
+                "description": "An array of mappings from a local path to the remote path that is used by the debuggee. The debug adapter will replace the local path with the remote path in all of the calls.",
+                "default": []
+              },
               "trace": {
                 "type": "string",
                 "enum": [
@@ -1063,7 +1103,7 @@
               },
               "remotePath": {
                 "type": "string",
-                "description": "If remote debugging, the path to the source code on the remote machine, if different from the local machine.",
+                "description": "If remote debugging, the path to the source code on the remote machine, if different from the local machine. If specified, becomes the first entry in substitutePath.",
                 "default": ""
               },
               "port": {
diff --git a/src/debugAdapter/goDebug.ts b/src/debugAdapter/goDebug.ts
index 84e36d7..788af3d 100644
--- a/src/debugAdapter/goDebug.ts
+++ b/src/debugAdapter/goDebug.ts
@@ -276,6 +276,7 @@
 	trace?: 'verbose' | 'log' | 'error';
 	backend?: string;
 	output?: string;
+	substitutePath?: {from: string, to: string}[];
 	/** Delve LoadConfig parameters */
 	dlvLoadConfig?: LoadConfig;
 	dlvToolPath: string;
@@ -307,6 +308,7 @@
 	host?: string;
 	trace?: 'verbose' | 'log' | 'error';
 	backend?: string;
+	substitutePath?: {from: string, to: string}[];
 	/** Delve LoadConfig parameters */
 	dlvLoadConfig?: LoadConfig;
 	dlvToolPath: string;
@@ -369,6 +371,10 @@
 	return filePath;
 }
 
+function normalizeSeparators(filePath: string): string {
+	return filePath.replace(/\/|\\/g, '/');
+}
+
 function getBaseName(filePath: string) {
 	return filePath.includes('/') ? path.basename(filePath) : path.win32.basename(filePath);
 }
@@ -853,6 +859,9 @@
 	private localToRemotePathMapping = new Map<string, string>();
 	private remoteToLocalPathMapping = new Map<string, string>();
 
+	// TODO(suzmue): Use delve's implementation of substitute-path.
+	private substitutePath: {from: string, to: string}[];
+
 	private showGlobalVariables: boolean = false;
 
 	private continueEpoch = 0;
@@ -1038,7 +1047,7 @@
 	}
 
 	protected async toDebuggerPath(filePath: string): Promise<string> {
-		if (this.delve.remotePath.length === 0) {
+		if (this.substitutePath.length === 0) {
 			if (this.delve.isRemoteDebugging) {
 				// The user trusts us to infer the remote path mapping!
 				await this.initializeRemotePackagesAndSources();
@@ -1047,14 +1056,28 @@
 					return matchedRemoteFile;
 				}
 			}
+
 			return this.convertClientPathToDebugger(filePath);
 		}
 
 		// The filePath may have a different path separator than the localPath
-		// So, update it to use the same separator as the remote path to ease
-		// in replacing the local path in it with remote path
-		filePath = filePath.replace(/\/|\\/g, this.remotePathSeparator);
-		return filePath.replace(this.delve.program.replace(/\/|\\/g, this.remotePathSeparator), this.delve.remotePath);
+		// So, update it to use the same separator for ease in path replacement.
+		filePath = normalizeSeparators(filePath);
+		let substitutedPath = filePath;
+		let substituteRule: {from: string, to: string};
+		this.substitutePath.forEach((value) => {
+			if (filePath.startsWith(value.from)) {
+				if (!!substituteRule) {
+					log(`Substitutition rule ${value.from}:${value.to} applies to local path ${filePath} but it was already mapped to debugger path using rule ${substituteRule.from}:${substituteRule.to}`);
+					return;
+				}
+				substitutedPath = filePath.replace(value.from, value.to);
+				substituteRule = {from: value.from, to: value.to};
+			}
+		});
+		filePath = substitutedPath;
+
+		return filePath = filePath.replace(/\/|\\/g, this.remotePathSeparator);
 	}
 
 	/**
@@ -1203,7 +1226,7 @@
 	 * have been initialized.
 	 */
 	protected toLocalPath(pathToConvert: string): string {
-		if (this.delve.remotePath.length === 0) {
+		if (this.substitutePath.length === 0) {
 			// User trusts use to infer the path
 			if (this.delve.isRemoteDebugging) {
 				const inferredPath = this.inferLocalPathFromRemotePath(pathToConvert);
@@ -1211,11 +1234,28 @@
 					return inferredPath;
 				}
 			}
+
 			return this.convertDebuggerPathToClient(pathToConvert);
 		}
 
+		// If there is a substitutePath mapping, then we replace the path.
+		pathToConvert = normalizeSeparators(pathToConvert);
+		let substitutedPath = pathToConvert;
+		let substituteRule: {from: string, to: string};
+		this.substitutePath.forEach((value) => {
+			if (pathToConvert.startsWith(value.to)) {
+				if (!!substituteRule) {
+					log(`Substitutition rule ${value.from}:${value.to} applies to debugger path ${pathToConvert} but it was already mapped to local path using rule ${substituteRule.from}:${substituteRule.to}`);
+					return;
+				}
+				substitutedPath = pathToConvert.replace(value.to, value.from);
+				substituteRule = {from: value.from, to: value.to};
+			}
+		});
+		pathToConvert = substitutedPath;
+
 		// When the pathToConvert is under GOROOT or Go module cache, replace path appropriately
-		if (!pathToConvert.startsWith(this.delve.remotePath)) {
+		if (!substituteRule) {
 			// Fix for https://github.com/Microsoft/vscode-go/issues/1178
 			const index = pathToConvert.indexOf(`${this.remotePathSeparator}src${this.remotePathSeparator}`);
 			const goroot = this.getGOROOT();
@@ -1239,7 +1279,6 @@
 			}
 		}
 		return pathToConvert
-			.replace(this.delve.remotePath, this.delve.program)
 			.split(this.remotePathSeparator)
 			.join(this.localPathSeparator);
 	}
@@ -1835,6 +1874,7 @@
 		}
 
 		this.localPathSeparator = findPathSeparator(localPath);
+		this.substitutePath = [];
 		if (args.remotePath.length > 0) {
 			this.remotePathSeparator = findPathSeparator(args.remotePath);
 
@@ -1857,6 +1897,25 @@
 			) {
 				args.remotePath = args.remotePath.substring(0, args.remotePath.length - 1);
 			}
+
+			// Make the remotePath mapping the first one in substitutePath
+			// so that it will take precedence over the other mappings.
+			this.substitutePath.push({
+				from: normalizeSeparators(localPath),
+				to: normalizeSeparators(args.remotePath)
+			});
+		}
+
+		if (!!args.substitutePath) {
+			args.substitutePath.forEach((value) => {
+				if (!this.remotePathSeparator) {
+					this.remotePathSeparator = findPathSeparator(value.to);
+				}
+				this.substitutePath.push({
+					from: normalizeSeparators(value.from),
+					to: normalizeSeparators(value.to)
+				});
+			});
 		}
 
 		// Launch the Delve debugger on the program
diff --git a/test/integration/goDebug.test.ts b/test/integration/goDebug.test.ts
index 04e4b82..92a8fb8 100644
--- a/test/integration/goDebug.test.ts
+++ b/test/integration/goDebug.test.ts
@@ -1,11 +1,12 @@
 import * as assert from 'assert';
-import { ChildProcess, spawn } from 'child_process';
-import { debug } from 'console';
+import * as cp from 'child_process';
 import * as fs from 'fs';
 import getPort = require('get-port');
 import * as http from 'http';
+import { tmpdir } from 'os';
 import * as path from 'path';
 import * as sinon from 'sinon';
+import util = require('util');
 import { DebugConfiguration } from 'vscode';
 import { DebugClient } from 'vscode-debugadapter-testsupport';
 import { ILocation } from 'vscode-debugadapter-testsupport/lib/debugClient';
@@ -18,7 +19,7 @@
 	RemoteSourcesAndPackages,
 } from '../../src/debugAdapter/goDebug';
 import { GoDebugConfigurationProvider } from '../../src/goDebugConfiguration';
-import { getBinPath } from '../../src/util';
+import { getBinPath, getGoVersion, rmdirRecursive } from '../../src/util';
 import { killProcessTree } from '../../src/utils/processUtils';
 
 suite('Path Manipulation Tests', () => {
@@ -323,7 +324,7 @@
 	 */
 	async function setUpRemoteProgram(
 			dlvPort: number, serverPort: number,
-			acceptMultiClient = true, continueOnStart = true): Promise<ChildProcess> {
+			acceptMultiClient = true, continueOnStart = true): Promise<cp.ChildProcess> {
 		const serverFolder = path.join(DATA_ROOT, 'helloWorldServer');
 		const toolPath = getBinPath('dlv');
 		const args = ['debug', '--api-version=2', '--headless', `--listen=127.0.0.1:${dlvPort}`];
@@ -333,7 +334,7 @@
 		if (continueOnStart) {
 			args.push('--continue');
 		}
-		const childProcess = spawn(toolPath, args,
+		const childProcess = cp.spawn(toolPath, args,
 			{cwd: serverFolder,  env: { PORT: `${serverPort}`, ...process.env}});
 
 		// Give dlv a few seconds to start.
@@ -514,7 +515,6 @@
 				request: 'launch',
 				mode: 'auto',
 				program: PROGRAM,
-				trace: 'verbose'
 			};
 
 			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
@@ -745,7 +745,7 @@
 	});
 
 	suite('remote attach', () => {
-		let childProcess: ChildProcess;
+		let childProcess: cp.ChildProcess;
 		let server: number;
 		let debugConfig: DebugConfiguration;
 		setup(async () => {
@@ -1284,4 +1284,213 @@
 			]);
 		});
 	});
+
+	suite('substitute path', () => {
+		// TODO(suzmue): add unit tests for substitutePath.
+		let tmpDir: string;
+
+		suiteSetup(() => {
+			tmpDir = fs.mkdtempSync(path.join(DATA_ROOT, 'substitutePathTest'));
+		});
+
+		suiteTeardown(() => {
+			rmdirRecursive(tmpDir);
+		});
+
+		suite('substitutePath with symlinks', () => {
+			let linkedDir: string;
+			setup(() => {
+				linkedDir = path.join(tmpDir, 'src');
+				fs.symlinkSync(DATA_ROOT, linkedDir);
+			});
+
+			teardown(() => {
+				fs.unlinkSync(linkedDir);
+			});
+
+			test('should stop on a breakpoint set in file with substituted path', () => {
+				const PROGRAM_TEMP = path.join(linkedDir, 'baseTest');
+				const PROGRAM = path.join(DATA_ROOT, 'baseTest');
+
+				const FILE = path.join(PROGRAM_TEMP, 'test.go');
+				const BREAKPOINT_LINE = 11;
+
+				const config = {
+					name: 'Launch',
+					type: 'go',
+					request: 'launch',
+					mode: 'auto',
+					program: PROGRAM_TEMP,
+					substitutePath: [
+						{
+							from: PROGRAM_TEMP,
+							to: PROGRAM
+						}
+					]
+				};
+				const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+				return dc.hitBreakpoint(debugConfig, getBreakpointLocation(FILE, BREAKPOINT_LINE));
+			});
+
+			test('should stop on a breakpoint in test file with substituted path', () => {
+				const PROGRAM_TEMP = path.join(linkedDir, 'baseTest');
+				const PROGRAM = path.join(DATA_ROOT, 'baseTest');
+
+				const FILE = path.join(PROGRAM_TEMP, 'sample_test.go');
+				const BREAKPOINT_LINE = 15;
+
+				const config = {
+					name: 'Launch file',
+					type: 'go',
+					request: 'launch',
+					mode: 'test',
+					program: PROGRAM_TEMP,
+					substitutePath: [
+						{
+							from: PROGRAM_TEMP,
+							to: PROGRAM
+						}
+					]
+				};
+				const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+				return dc.hitBreakpoint(debugConfig, getBreakpointLocation(FILE, BREAKPOINT_LINE));
+			});
+		});
+
+		function copyDirectory(name: string) {
+			const from = path.join(DATA_ROOT, name);
+			const to = path.join(tmpDir, name);
+			fs.mkdirSync(to);
+			fs.readdirSync(from).forEach((file) => {
+				fs.copyFileSync(path.join(from, file), path.join(to, file));
+			});
+			return to;
+		}
+
+		async function buildGoProgram(cwd: string, outputFile: string): Promise<string> {
+			const goRuntimePath = getBinPath('go');
+			const execFile = util.promisify(cp.execFile);
+			const child = await execFile(goRuntimePath,
+				['build', '-o', outputFile, `--gcflags='all=-N -l'`, '.'],
+				{cwd});
+			if (child.stderr.length > 0) {
+				throw Error(child.stderr);
+			}
+			return outputFile;
+		}
+
+		suite('substitutePath with missing files', () => {
+			let goBuildOutput: string;
+			suiteSetup(() => {
+				goBuildOutput = fs.mkdtempSync(path.join(tmpdir(), 'output'));
+			});
+
+			suiteTeardown(() => {
+				rmdirRecursive(goBuildOutput);
+			});
+
+			async function copyBuildDelete(program: string): Promise<{program: string, output: string}> {
+				const wd = copyDirectory(program);
+				const output = await buildGoProgram(wd, path.join(goBuildOutput, program));
+				rmdirRecursive(wd);
+				return {program: wd, output};
+			}
+
+			test('should stop on a breakpoint set in file with substituted path', async () => {
+				const {program, output} = await copyBuildDelete('baseTest');
+				const FILE = path.join(DATA_ROOT, 'baseTest', 'test.go');
+				const BREAKPOINT_LINE = 11;
+
+				const config = {
+					name: 'Launch',
+					type: 'go',
+					request: 'launch',
+					mode: 'exec',
+					program: output,
+					substitutePath: [
+						{
+							from: path.join(DATA_ROOT, 'baseTest'),
+							to: program
+						}
+					]
+				};
+				const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+				return dc.hitBreakpoint(debugConfig, getBreakpointLocation(FILE, BREAKPOINT_LINE));
+			});
+		});
+
+		suite('substitutePath with remote program', () => {
+			let server: number;
+			let remoteAttachDebugConfig: DebugConfiguration;
+			let helloWorldLocal: string;
+			let helloWorldRemote: string;
+			setup(async () => {
+				server = await getPort();
+				remoteAttachConfig.port = await getPort();
+				remoteAttachDebugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, remoteAttachConfig);
+			});
+
+			suiteSetup(() => {
+				helloWorldLocal = copyDirectory('helloWorldServer');
+				helloWorldRemote = path.join(DATA_ROOT, 'helloWorldServer');
+			});
+
+			suiteTeardown(() => {
+				rmdirRecursive(helloWorldLocal);
+			});
+
+			test('stopped for a breakpoint set during initialization using substitutePath (remote attach)', async () => {
+				const FILE = path.join(helloWorldLocal, 'main.go');
+				const BREAKPOINT_LINE = 29;
+				const remoteProgram = await setUpRemoteProgram(remoteAttachConfig.port, server);
+
+				const breakpointLocation = getBreakpointLocation(FILE, BREAKPOINT_LINE, false);
+				// Setup attach with a breakpoint.
+				remoteAttachDebugConfig.cwd = tmpDir;
+				remoteAttachDebugConfig.remotePath = '';
+				remoteAttachDebugConfig.substitutePath = [
+					{from: helloWorldLocal, to: helloWorldRemote}
+				];
+				await setUpRemoteAttach(remoteAttachDebugConfig, [breakpointLocation]);
+
+				// Calls the helloworld server to make the breakpoint hit.
+				await waitForBreakpoint(
+					() => http.get(`http://localhost:${server}`).on('error', (data) => console.log(data)),
+					breakpointLocation);
+
+				await dc.disconnectRequest({restart: false});
+				await killProcessTree(remoteProgram);
+				await new Promise((resolve) => setTimeout(resolve, 2_000));
+			});
+
+			test('stopped for a breakpoint set during initialization using remotePath (remote attach)', async () => {
+				const FILE = path.join(helloWorldLocal, 'main.go');
+				const BREAKPOINT_LINE = 29;
+				const remoteProgram = await setUpRemoteProgram(remoteAttachConfig.port, server);
+
+				const breakpointLocation = getBreakpointLocation(FILE, BREAKPOINT_LINE, false);
+				// Setup attach with a breakpoint.
+				remoteAttachDebugConfig.cwd = helloWorldLocal;
+				remoteAttachDebugConfig.remotePath = helloWorldRemote;
+				// This is a bad mapping, make sure that the remotePath config is used first.
+				remoteAttachDebugConfig.substitutePath = [
+					{from: helloWorldLocal, to: helloWorldLocal}
+				];
+				await setUpRemoteAttach(remoteAttachDebugConfig, [breakpointLocation]);
+
+				// Calls the helloworld server to make the breakpoint hit.
+				await waitForBreakpoint(
+					() => http.get(`http://localhost:${server}`).on('error', (data) => console.log(data)),
+					breakpointLocation);
+
+				await dc.disconnectRequest({restart: false});
+				await killProcessTree(remoteProgram);
+				await new Promise((resolve) => setTimeout(resolve, 2_000));
+			});
+		});
+	});
+
 });