src/goDebug: respect user cwd debug configuration in noDebug

If the launch configuration contain the current working directory,
run the program in that directory.

Build the program in the directory containing the program to be built
and then run the program in the given directory.

Fixes golang/vscode-go#918

Change-Id: I2d20d00a68cb1999245c41c3c5b2cdf0e746a4ba
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/270437
Trust: Suzy Mueller <suzmue@golang.org>
Run-TryBot: Suzy Mueller <suzmue@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/src/debugAdapter/goDebug.ts b/src/debugAdapter/goDebug.ts
index 61d7e0d..2d52b32 100644
--- a/src/debugAdapter/goDebug.ts
+++ b/src/debugAdapter/goDebug.ts
@@ -3,7 +3,7 @@
  * Licensed under the MIT License. See LICENSE in the project root for license information.
  *--------------------------------------------------------*/
 
-import { ChildProcess, execFile, spawn } from 'child_process';
+import { ChildProcess, execFile, spawn, spawnSync } from 'child_process';
 import { EventEmitter } from 'events';
 import * as fs from 'fs';
 import { existsSync, lstatSync } from 'fs';
@@ -481,25 +481,68 @@
 				if (!!launchArgs.noDebug) {
 					if (mode === 'debug') {
 						this.noDebug = true;
-						const runArgs = ['run'];
-						const runOptions: { [key: string]: any } = { cwd: dirname, env };
+						const build = ['build'];
+
+						const output = path.join(os.tmpdir(), 'out');
+						build.push(`-o=${output}`);
+
+						const buildOptions: { [key: string]: any } = { cwd: dirname, env };
 						if (launchArgs.buildFlags) {
-							runArgs.push(launchArgs.buildFlags);
+							build.push(launchArgs.buildFlags);
 						}
+
 						if (isProgramDirectory) {
-							runArgs.push('.');
+							build.push('.');
 						} else {
-							runArgs.push(program);
-						}
-						if (launchArgs.args) {
-							runArgs.push(...launchArgs.args);
+							build.push(program);
 						}
 
 						const goExe = getBinPathWithPreferredGopathGoroot('go', []);
 						log(`Current working directory: ${dirname}`);
-						log(`Running: ${goExe} ${runArgs.join(' ')}`);
+						log(`Building: ${goExe} ${build.join(' ')}`);
 
-						this.debugProcess = spawn(goExe, runArgs, runOptions);
+						// Use spawnSync to ensure that the binary exists before running it.
+						const buffer = spawnSync(goExe, build, buildOptions);
+						if (buffer.stderr  && buffer.stderr.length > 0) {
+							const str = buffer.stderr.toString();
+							if (this.onstderr) {
+								this.onstderr(str);
+							}
+						}
+						if (buffer.stdout && buffer.stdout.length > 0) {
+							const str = buffer.stdout.toString();
+							if (this.onstdout) {
+								this.onstdout(str);
+							}
+						}
+						if (buffer.status) {
+							logError(`Build process exiting with code: ${buffer.status} signal: ${buffer.signal}`);
+							if (this.onclose) {
+								this.onclose(buffer.status);
+							}
+						} else {
+							log(`Build process exiting normally ${buffer.signal}`);
+						}
+						if (buffer.error) {
+							reject(buffer.error);
+						}
+
+						// Run the built binary
+						let wd = dirname;
+						if (!!launchArgs.cwd) {
+							wd = launchArgs.cwd;
+						}
+						const runOptions: { [key: string]: any } = { cwd: wd, env };
+
+						const run = [];
+						if (launchArgs.args) {
+							run.push(...launchArgs.args);
+						}
+
+						log(`Current working directory: ${wd}`);
+						log(`Running: ${output} ${run.join(' ')}`);
+
+						this.debugProcess = spawn(output, run, runOptions);
 						this.debugProcess.stderr.on('data', (chunk) => {
 							const str = chunk.toString();
 							if (this.onstderr) {
@@ -525,6 +568,7 @@
 						this.debugProcess.on('error', (err) => {
 							reject(err);
 						});
+
 						resolve();
 						return;
 					}
diff --git a/test/integration/goDebug.test.ts b/test/integration/goDebug.test.ts
index a2b862a..08cc650 100644
--- a/test/integration/goDebug.test.ts
+++ b/test/integration/goDebug.test.ts
@@ -1,5 +1,6 @@
 import * as assert from 'assert';
 import { ChildProcess, spawn } from 'child_process';
+import { debug } from 'console';
 import * as fs from 'fs';
 import getPort = require('get-port');
 import * as http from 'http';
@@ -568,6 +569,181 @@
 		});
 	});
 
+	suite('set current working directory', () => {
+		test('should debug program with cwd set', async () => {
+			const WD = path.join(DATA_ROOT, 'cwdTest');
+			const PROGRAM = path.join(WD, 'cwdTest');
+			const FILE = path.join(PROGRAM, 'main.go');
+			const BREAKPOINT_LINE = 11;
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+				cwd: WD,
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			await dc.hitBreakpoint(debugConfig, getBreakpointLocation(FILE, BREAKPOINT_LINE));
+
+			await assertVariableValue('strdat', '"Hello, World!"');
+		});
+
+		test('should debug program without cwd set', async () => {
+			const WD = path.join(DATA_ROOT, 'cwdTest');
+			const PROGRAM = path.join(WD, 'cwdTest');
+			const FILE = path.join(PROGRAM, 'main.go');
+			const BREAKPOINT_LINE = 11;
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			await dc.hitBreakpoint(debugConfig, getBreakpointLocation(FILE, BREAKPOINT_LINE));
+
+			await assertVariableValue('strdat', '"Goodbye, World."');
+		});
+
+		test('should debug file program with cwd set', async () => {
+			const WD = path.join(DATA_ROOT, 'cwdTest');
+			const PROGRAM = path.join(WD, 'cwdTest', 'main.go');
+			const FILE = PROGRAM;
+			const BREAKPOINT_LINE = 11;
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+				cwd: WD,
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			await dc.hitBreakpoint(debugConfig, getBreakpointLocation(FILE, BREAKPOINT_LINE));
+
+			await assertVariableValue('strdat', '"Hello, World!"');
+		});
+
+		test('should debug file program without cwd set', async () => {
+			const WD = path.join(DATA_ROOT, 'cwdTest');
+			const PROGRAM = path.join(WD, 'cwdTest', 'main.go');
+			const FILE = PROGRAM;
+			const BREAKPOINT_LINE = 11;
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			await dc.hitBreakpoint(debugConfig, getBreakpointLocation(FILE, BREAKPOINT_LINE));
+
+			await assertVariableValue('strdat', '"Goodbye, World."');
+		});
+
+		test('should run program with cwd set (noDebug)', () => {
+			const WD = path.join(DATA_ROOT, 'cwdTest');
+			const PROGRAM = path.join(WD, 'cwdTest');
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+				cwd: WD,
+				noDebug: true
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			return Promise.all([
+				dc.launch(debugConfig),
+				dc.waitForEvent('output').then((event) => {
+					assert.strictEqual(event.body.output, 'Hello, World!\n');
+				})
+			]);
+		});
+
+		test('should run program without cwd set (noDebug)', () => {
+			const WD = path.join(DATA_ROOT, 'cwdTest');
+			const PROGRAM = path.join(WD, 'cwdTest');
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+				noDebug: true
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			return Promise.all([
+				dc.launch(debugConfig),
+				dc.waitForEvent('output').then((event) => {
+					assert.strictEqual(event.body.output, 'Goodbye, World.\n');
+				})
+			]);
+		});
+
+		test('should run file program with cwd set (noDebug)', () => {
+			const WD = path.join(DATA_ROOT, 'cwdTest');
+			const PROGRAM = path.join(WD, 'cwdTest', 'main.go');
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+				cwd: WD,
+				noDebug: true
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			return Promise.all([
+				dc.launch(debugConfig),
+				dc.waitForEvent('output').then((event) => {
+					assert.strictEqual(event.body.output, 'Hello, World!\n');
+				})
+			]);
+		});
+
+		test('should run file program without cwd set (noDebug)', () => {
+			const WD = path.join(DATA_ROOT, 'cwdTest');
+			const PROGRAM = path.join(WD, 'cwdTest', 'main.go');
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+				noDebug: true
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			return Promise.all([
+				dc.launch(debugConfig),
+				dc.waitForEvent('output').then((event) => {
+					assert.strictEqual(event.body.output, 'Goodbye, World.\n');
+				})
+			]);
+		});
+
+	});
+
 	suite('remote attach', () => {
 		let childProcess: ChildProcess;
 		let server: number;
diff --git a/test/testdata/cwdTest/cwdTest/hello.txt b/test/testdata/cwdTest/cwdTest/hello.txt
new file mode 100644
index 0000000..d29c4ae
--- /dev/null
+++ b/test/testdata/cwdTest/cwdTest/hello.txt
@@ -0,0 +1 @@
+Goodbye, World.
\ No newline at end of file
diff --git a/test/testdata/cwdTest/cwdTest/main.go b/test/testdata/cwdTest/cwdTest/main.go
new file mode 100644
index 0000000..041ce74
--- /dev/null
+++ b/test/testdata/cwdTest/cwdTest/main.go
@@ -0,0 +1,12 @@
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+)
+
+func main() {
+	dat, _ := ioutil.ReadFile("hello.txt")
+	strdat := string(dat)
+	fmt.Println(strdat)
+}
diff --git a/test/testdata/cwdTest/hello.txt b/test/testdata/cwdTest/hello.txt
new file mode 100644
index 0000000..b45ef6f
--- /dev/null
+++ b/test/testdata/cwdTest/hello.txt
@@ -0,0 +1 @@
+Hello, World!
\ No newline at end of file