src/testUtils: use -json in goTest

log messages in `go test` use only the base name of the file.
When `go test` is invoked against multiple packages, we cannot tell
which package each file belongs to until the test for the package
completes and the final output including the package path is printed.
Thus, goTest buffered the test output until the final line of the package
test result becomes available and used the package path info embedded
in the last line in order to infer the full path of each log
message's file name.

This hack is no longer necessary if -json flag is used. Each entry
in the output JSON stream contains the package path.

-json flag exists since go1.9. We will no longer supports go1.8 or
older versions.

Since 1.14, go streams the test output if the `-v` flag is given.
But because of this bufferring in goTest, users couldn't benefit from
this new go1.14 feature. By eliminating this bufferring, test output
will be streamed as soon as it becomes available.

This change also fixes the file name expansion pattern because with `-v`
the log message format is different (i.e. prefixed with the test function).

Fixes golang/vscode-go#316

Note: if the log message comes from a different package (e.g. some
assertion package is used), this filename rewrite logic does not work.
That's an existing bug and without the go command's help, we cannot
fix it. A better approach would be to teach `go test` to use the full
file path in its output when requested, but it's beyond the scope of
this work.

Change-Id: I0a38bdb340e6c242ccb6eaf16a0b5af8425168fc
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/242540
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/src/testUtils.ts b/src/testUtils.ts
index fe7acfc..f84a9e2 100644
--- a/src/testUtils.ts
+++ b/src/testUtils.ts
@@ -228,6 +228,17 @@
 }
 
 /**
+ * go test -json output format.
+ * which is a subset of https://golang.org/cmd/test2json/#hdr-Output_Format
+ * and includes only the fields that we are using.
+ */
+interface GoTestOutput {
+	Action: string;
+	Output?: string;
+	Package?: string;
+}
+
+/**
  * Runs go test and presents the output in the 'Go' channel.
  *
  * @param goConfig Configuration for the Go extension.
@@ -309,6 +320,7 @@
 					pkgMap = new Map<string, string>();
 				}
 				// Use the package name to be in the args to enable running tests in symlinked directories
+				// TODO(hyangah): check why modules mode didn't set currentPackage.
 				if (!testconfig.includeSubDirectories && currentPackage) {
 					targets.splice(0, 0, currentPackage);
 				}
@@ -321,6 +333,7 @@
 				}
 
 				args.push(...targets);
+				args.push('-json');
 
 				// ensure that user provided flags are appended last (allow use of -args ...)
 				// ignore user provided -run flag if we are already using it
@@ -337,30 +350,29 @@
 				const outBuf = new LineBuffer();
 				const errBuf = new LineBuffer();
 
-				// 1=ok/FAIL, 2=package, 3=time/(cached)
-				const packageResultLineRE = /^(ok|FAIL)\s+(\S+)\s+([0-9\.]+s|\(cached\))/;
-				const lineWithErrorRE = /^(\t|\s\s\s\s)\S/;
 				const testResultLines: string[] = [];
-
 				const processTestResultLine = (line: string) => {
-					testResultLines.push(line);
-					const result = line.match(packageResultLineRE);
-					if (result && (pkgMap.has(result[2]) || currentGoWorkspace)) {
-						const hasTestFailed = line.startsWith('FAIL');
-						const packageNameArr = result[2].split('/');
-						const baseDir = pkgMap.get(result[2]) || path.join(currentGoWorkspace, ...packageNameArr);
-						testResultLines.forEach((testResultLine) => {
-							if (hasTestFailed && lineWithErrorRE.test(testResultLine)) {
-								outputChannel.appendLine(expandFilePathInOutput(testResultLine, baseDir));
-							} else {
-								outputChannel.appendLine(testResultLine);
-							}
-						});
-						testResultLines.splice(0);
+					try {
+						const m = <GoTestOutput>(JSON.parse(line));
+						if (m.Action !== 'output' || !m.Output) {
+							return;
+						}
+						const out = m.Output;
+						const pkg = m.Package;
+						if (pkg && (pkgMap.has(pkg) || currentGoWorkspace)) {
+							const pkgNameArr = pkg.split('/');
+							const baseDir = pkgMap.get(pkg) || path.join(currentGoWorkspace, ...pkgNameArr);
+							// go test emits test results on stdout, which contain file names relative to the package under test
+							outputChannel.appendLine(expandFilePathInOutput(out, baseDir).trimRight());
+						} else {
+							outputChannel.appendLine(out.trimRight());
+						}
+					} catch (e) {
+						console.log(`failed to parse JSON: ${e}`);
+						outputChannel.appendLine(line);
 					}
 				};
 
-				// go test emits test results on stdout, which contain file names relative to the package under test
 				outBuf.onLine((line) => processTestResultLine(line));
 				outBuf.onDone((last) => {
 					if (last) {
@@ -437,7 +449,7 @@
 function expandFilePathInOutput(output: string, cwd: string): string {
 	const lines = output.split('\n');
 	for (let i = 0; i < lines.length; i++) {
-		const matches = lines[i].match(/^\s*(.+.go):(\d+):/);
+		const matches = lines[i].match(/\s+(\S+.go):(\d+):\s+/);
 		if (matches && matches[1] && !path.isAbsolute(matches[1])) {
 			lines[i] = lines[i].replace(matches[1], path.join(cwd, matches[1]));
 		}
diff --git a/test/integration/test.test.ts b/test/integration/test.test.ts
index c2ef69f..6287f7a 100644
--- a/test/integration/test.test.ts
+++ b/test/integration/test.test.ts
@@ -50,7 +50,7 @@
 	}
 
 	async function runTest(
-		input: { isMod: boolean, includeSubDirectories: boolean },
+		input: { isMod: boolean, includeSubDirectories: boolean, testFlags?: string[] },
 		wantFiles: string[]) {
 
 		fs.copySync(sourcePath, repoPath, { recursive: true });
@@ -62,7 +62,7 @@
 			goConfig: config,
 			outputChannel,
 			dir: repoPath,
-			flags: getTestFlags(config),
+			flags: input.testFlags ? input.testFlags : getTestFlags(config),
 			isMod: input.isMod,
 			includeSubDirectories: input.includeSubDirectories,
 		};
@@ -87,6 +87,12 @@
 		await runTest(
 			{ isMod: true, includeSubDirectories: false },
 			[path.join(repoPath, 'a_test.go')]);
+		await runTest(
+			{ isMod: true, includeSubDirectories: true, testFlags: ['-v'] },
+			[path.join(repoPath, 'a_test.go'), path.join(repoPath, 'b', 'b_test.go')]);
+		await runTest(
+			{ isMod: true, includeSubDirectories: false, testFlags: ['-v'] },
+			[path.join(repoPath, 'a_test.go')]);
 	});
 
 	test('resolves file names in logs (GOPATH)', async () => {
@@ -97,6 +103,12 @@
 		await runTest(
 			{ isMod: true, includeSubDirectories: false },
 			[path.join(repoPath, 'a_test.go')]);
+		await runTest(
+			{ isMod: true, includeSubDirectories: true, testFlags: ['-v'] },
+			[path.join(repoPath, 'a_test.go'), path.join(repoPath, 'b', 'b_test.go')]);
+		await runTest(
+			{ isMod: true, includeSubDirectories: false, testFlags: ['-v'] },
+			[path.join(repoPath, 'a_test.go')]);
 	});
 });