[release] src/util.ts: attempt to fix version parsing again

https://go-review.googlesource.com/c/vscode-go/+/245397 attempted
to correct the use of regex matching result. But didn't notice that
the regexp for released version does not capture the patch version
part. Use of semver.coerce on the full go version output previously
worked by accident because coercing recognized the version string
and built the SemVer out of it.

Adjust the regexp so that it captures the whole version string part
including the patch and the prerelease tags.

semver.coerce drops the prerelease tags from Go's version string
(e.g. go1.15rc1, etc), and modifies incomplete semver string to
`major.minor.patch` format (e.g. go1.14 -> go1.14.0).
In certain cases, we want the exact version string as `go version`
outputs. So, store the original version string and if
`includePrerelease` is set, `GoVersion.format` returns the original
version string instead of the result of `semver.format`.

This CL adds tests for Go version parsing and formatting.

Change-Id: I8986d4d5b03d1735d707c4c3b2b38895314dd497
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/245438
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
(cherry picked from commit dc9de1c6a5ce81a79979cda70723bec37ad3a6f6)
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/245600
diff --git a/src/util.ts b/src/util.ts
index 9684d9f..e9434a6 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -82,16 +82,24 @@
 
 export class GoVersion {
 	public sv?: semver.SemVer;
+	// Go version tags are not following the strict semver format
+	// so semver drops the prerelease tags used in Go version.
+	// If sv is valid, let's keep the original version string 
+	// including the prerelease tag parts.
+	public svString?: string;
+	
 	public isDevel?: boolean;
 	private commit?: string;
-
-	constructor(public binaryPath: string, version: string) {
-		const matchesRelease = /go version go(\d.\d+).*/.exec(version);
+	
+	constructor(public binaryPath: string, public version: string) {
+		const matchesRelease = /^go version go(\d\.\d+\S*)\s+/.exec(version);
 		const matchesDevel = /go version devel \+(.[a-zA-Z0-9]+).*/.exec(version);
 		if (matchesRelease) {
+			// note: semver.parse does not work with Go version string like go1.14.
 			const sv = semver.coerce(matchesRelease[1]);
 			if (sv) {
 				this.sv = sv;
+				this.svString = matchesRelease[1];
 			}
 		} else if (matchesDevel) {
 			this.isDevel = true;
@@ -103,11 +111,17 @@
 		return !!this.sv || !!this.isDevel;
 	}
 
-	public format(): string {
+	public format(includePrerelease?: boolean): string {
 		if (this.sv) {
+			if (includePrerelease && this.svString) {
+				return this.svString;
+			}
 			return this.sv.format();
 		}
-		return `devel +${this.commit}`;
+		if (this.isDevel) {
+			return `devel +${this.commit}`;
+		}
+		return `unknown`;
 	}
 
 	public lt(version: string): boolean {
@@ -300,9 +314,14 @@
 
 /**
  * Gets version of Go based on the output of the command `go version`.
- * Returns null if go is being used from source/tip in which case `go version` will not return release tag like go1.6.3
+ * Returns undefined if go version can't be determined because
+ * go is not available or `go version` fails.
  */
 export async function getGoVersion(): Promise<GoVersion | undefined> {
+	// TODO(hyangah): limit the number of concurrent getGoVersion call.
+	// When the extension starts, at least 4 concurrent calls race
+	// and end up calling `go version`.
+
 	const goRuntimePath = getBinPath('go');
 
 	const warn = (msg: string) => {
diff --git a/test/integration/utils.test.ts b/test/integration/utils.test.ts
index 78fa3c3..5e9a8a2 100644
--- a/test/integration/utils.test.ts
+++ b/test/integration/utils.test.ts
@@ -4,7 +4,7 @@
  *--------------------------------------------------------*/
 
 import * as assert from 'assert';
-import { guessPackageNameFromFile, substituteEnv } from '../../src/util';
+import { GoVersion, guessPackageNameFromFile, substituteEnv } from '../../src/util';
 
 suite('utils Tests', () => {
 	test('substituteEnv: default', () => {
@@ -21,6 +21,79 @@
 		// test completed
 		process.env = env;
 	});
+
+	test('build GoVersion', () => {
+		// [input, wantFormat, wantFormatIncludePrerelease, wantIsValid]
+		const testCases: [string|undefined, string, string, boolean][] = [
+			[
+				'go version devel +a295d59d Fri Jun 26 19:00:25 2020 +0000 darwin/amd64',
+				'devel +a295d59d',
+				'devel +a295d59d',
+				true,
+			],
+			[
+				'go version go1.14 darwin/amd64',
+				'1.14.0',
+				'1.14',
+				true,
+			],
+			[
+				'go version go1.14.1 linux/amd64',
+				'1.14.1',
+				'1.14.1',
+				true,
+			],
+			[
+				'go version go1.15rc1 darwin/amd64',
+				'1.15.0',
+				'1.15rc1',
+				true,
+			],
+			[
+				'go version go1.15.1rc2 windows/amd64',
+				'1.15.1',
+				'1.15.1rc2',
+				true,
+			],
+			[
+				'go version go1.15.3-beta.1 darwin/amd64',
+				'1.15.3',
+				'1.15.3-beta.1',
+				true,
+			],
+			[
+				'go version go1.15.3-beta.1.2.3 foobar/amd64',
+				'1.15.3',
+				'1.15.3-beta.1.2.3',
+				true,
+			],
+			[
+				'go version go10.0.1 js/amd64',
+				'unknown',
+				'unknown',
+				false,
+			],
+			[
+				undefined,
+				'unknown',
+				'unknown',
+				false,
+			],
+			[
+				'something wrong',
+				'unknown',
+				'unknown',
+				false,
+			]
+		];
+		for (const [input, wantFormat, wantFormatIncludePrerelease, wantIsValid] of testCases) {
+			const go = new GoVersion('/path/to/go', input);
+
+			assert.equal(go.isValid(), wantIsValid, `GoVersion(${input}) = ${JSON.stringify(go)}`);
+			assert.equal(go.format(), wantFormat, `GoVersion(${input}) = ${JSON.stringify(go)}`);
+			assert.equal(go.format(true), wantFormatIncludePrerelease, `GoVersion(${input}) = ${JSON.stringify(go)}`);
+		}
+	});
 });
 
 suite('GuessPackageNameFromFile Tests', () => {