src/goTest: adjust file matching to handle interleaved stdout/stderr
Fixes golang/vscode-go#2549
Change-Id: Ib74397cc45ba8a37f8d3a8b579c745d5327d9401
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/455156
Reviewed-by: Jamal Carvalho <jamal@golang.org>
Auto-Submit: Hyang-Ah Hana Kim <hyangah@gmail.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/src/goTest/run.ts b/src/goTest/run.ts
index 4147da9..7f32f4b 100644
--- a/src/goTest/run.ts
+++ b/src/goTest/run.ts
@@ -27,6 +27,7 @@
import { GoTestProfiler, ProfilingOptions } from './profile';
import { debugTestAtCursor } from '../goTest';
import { GoExtensionContext } from '../context';
+import path = require('path');
let debugSessionID = 0;
@@ -634,7 +635,7 @@
for (const { message, location } of messages) {
const loc = `${location?.uri}:${location?.range.start.line}`;
if (merged.has(loc)) {
- merged.get(loc)!.message += '\n' + message;
+ merged.get(loc)!.message += '' + message;
} else {
merged.set(loc, { message, location });
}
@@ -660,6 +661,9 @@
}
}
+ // parseOutput returns build/test error messages associated with source locations.
+ // Location info is inferred heuristically by applying a simple pattern matching
+ // over the output strings from `go test -json` `output` type action events.
parseOutput(test: TestItem, output: string[]): TestMessage[] {
const messages: TestMessage[] = [];
@@ -679,11 +683,27 @@
let current: Location | undefined;
if (!test.uri) return messages;
- const dir = Uri.joinPath(test.uri, '..');
+ const dir = Uri.joinPath(test.uri, '..').fsPath;
+ // TODO(hyangah): handle panic messages specially.
+
+ // Extract the location info from output message.
+ // This is not trivial since both the test output and any output/print
+ // from the tested program are reported as `output` type test events
+ // and not distinguishable. stdout/stderr output from the tested program
+ // makes this more trickier.
+ //
+ // Here we assume that test output messages are line-oriented, precede
+ // with a file name and line number, and end with new lines.
for (const line of output) {
- const m = line.match(/^\s*(?<file>.*\.go):(?<line>\d+): ?(?<message>.*\n)$/);
+ // ^(?:.*\s+|\s*) - non-greedy match of any chars followed by a space or, a space.
+ // (?<file>\S+\.go):(?<line>\d+): - gofile:line: followed by a space.
+ // (?<message>.\n)$ - all remaining message up to $.
+ const m = line.match(/^.*\s+(?<file>\S+\.go):(?<line>\d+): (?<message>.*\n)$/);
if (m?.groups) {
- const file = Uri.joinPath(dir, m.groups.file);
+ const file =
+ m.groups.file && path.isAbsolute(m.groups.file)
+ ? Uri.file(m.groups.file)
+ : Uri.file(path.join(dir, m.groups.file));
const ln = Number(m.groups.line) - 1; // VSCode uses 0-based line numbering (internally)
current = new Location(file, new Position(ln, 0));
messages.push({ message: m.groups.message, location: current });
diff --git a/test/integration/goTest.run.test.ts b/test/integration/goTest.run.test.ts
index 24b192a..7c8028a 100644
--- a/test/integration/goTest.run.test.ts
+++ b/test/integration/goTest.run.test.ts
@@ -1,7 +1,7 @@
import assert = require('assert');
import path = require('path');
import sinon = require('sinon');
-import { Uri, workspace } from 'vscode';
+import { Range, TestItem, Uri, workspace } from 'vscode';
import * as testUtils from '../../src/testUtils';
import { forceDidOpenTextDocument } from './goTest.utils';
import { GoTestExplorer } from '../../src/goTest/explore';
@@ -13,6 +13,46 @@
let testExplorer: GoTestExplorer;
+ suite('parseOutput', () => {
+ const ctx = MockExtensionContext.new();
+ suiteSetup(async () => {
+ testExplorer = GoTestExplorer.setup(ctx, {});
+ });
+ suiteTeardown(() => ctx.teardown());
+
+ function testParseOutput(output: string, expected: { file: string; line: number; msg: string }[]) {
+ const uri = Uri.parse('file:///path/to/mod/file.go');
+ const id = GoTest.id(uri, 'test', 'TestXXX');
+ const ti = { id, uri, range: new Range(1, 0, 100, 0) } as TestItem;
+ const testMsgs = testExplorer.runner.parseOutput(ti, [output]);
+ const got = testMsgs.map((m) => {
+ return {
+ file: m.location?.uri.fsPath,
+ line: m.location?.range.start.line,
+ msg: m.message
+ };
+ });
+ assert.strictEqual(JSON.stringify(got), JSON.stringify(expected));
+ }
+ test('no line info ', () => testParseOutput(' foo \n', []));
+ test('file path without preceding space', () => testParseOutput('file.go:7: foo\n', [])); // valid test message starts with a space.
+ test('valid test message format', () =>
+ testParseOutput(' file.go:7: foo\n', [{ file: '/path/to/mod/file.go', line: 6, msg: 'foo\n' }]));
+ test('message without ending newline', () =>
+ testParseOutput(
+ ' file.go:7: foo ', // valid test message contains a new line.
+ []
+ ));
+ test('user print message before test message', () =>
+ testParseOutput('random print file.go:8: foo\n', [
+ { file: '/path/to/mod/file.go', line: 7, msg: 'foo\n' }
+ ]));
+ test('multiple file locs in one line', () =>
+ testParseOutput('file.go:1: line1 . file.go:2: line2 \n', [
+ { file: '/path/to/mod/file.go', line: 1, msg: 'line2 \n' }
+ ]));
+ });
+
suite('Profile', () => {
const sandbox = sinon.createSandbox();
const ctx = MockExtensionContext.new();