src/goTest: fix bugs in subtest handling
This change addresses two issues
1. When the subtest name includes regexp metacharacters,
they need to be escaped before passing as `-run` flag value
that assumes regexp. We split around '/' and then escape meta
characters using escapeRegExp.
2. A bug in getOrCreateSubTest's child search prevented
looking up a node for the nested subtest. We should've compared
label with label. Added comments to clarify the difference
between label and name.
Fixes golang/vscode-go#3070
Fixes golang/vscode-go#2624
For golang/vscode-go#3022 (even though the issue is for the codelens)
Change-Id: I250d1188e91679aee5704e1bccfcd2344cf1ae90
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/546260
Reviewed-by: Suzy Mueller <suzmue@golang.org>
Commit-Queue: Hyang-Ah Hana Kim <hyangah@gmail.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
diff --git a/src/goTest/resolve.ts b/src/goTest/resolve.ts
index 48c38bf..5bcc91c 100644
--- a/src/goTest/resolve.ts
+++ b/src/goTest/resolve.ts
@@ -169,27 +169,34 @@
return it(this.items);
}
- // Create or Retrieve a sub test or benchmark. The ID will be of the form:
+ // Create or Retrieve a sub test or benchmark. This is always dynamically
+ // called while processing test run output.
+ // The parent's uri is of the form:
// file:///path/to/mod/file.go?test#TestXxx%2fA%2fB%2fC
- getOrCreateSubTest(item: TestItem, label: string, name: string, dynamic?: boolean): TestItem | undefined {
- if (!item.uri) return;
- const { kind } = GoTest.parseId(item.id);
+ // The name is the name of the subtest to get or create.
+ // The label is the title presented in the UI.
+ // We expect the caller to call getOrCreateSubTest with the
+ // right parent.
+ // For example, if TestXxx has subtests and we are processing
+ // TestXxx/Sub1/Sub2, the parent item should be a node corresponding
+ // to TestXxx/Sub1, the label is Sub2 while the name is TestXxx/Sub1/Sub2.
+ getOrCreateSubTest(parent: TestItem, label: string, name: string): TestItem | undefined {
+ if (!parent.uri) return;
+ const { kind } = GoTest.parseId(parent.id);
let existing: TestItem | undefined;
- item.children.forEach((child) => {
- if (child.label === name) existing = child;
+ parent.children.forEach((child) => {
+ if (child.label === label) existing = child;
});
if (existing) return existing;
- item.canResolveChildren = true;
- const sub = this.createItem(label, item.uri, kind, name);
- item.children.add(sub);
+ parent.canResolveChildren = true;
+ const sub = this.createItem(label, parent.uri, kind, name);
+ parent.children.add(sub);
- if (dynamic) {
- this.isDynamicSubtest.add(sub);
- if (this.shouldSetRange(item)) {
- sub.range = item.range;
- }
+ this.isDynamicSubtest.add(sub);
+ if (this.shouldSetRange(parent)) {
+ sub.range = parent.range;
}
return sub;
diff --git a/src/goTest/run.ts b/src/goTest/run.ts
index 7f32f4b..697a481 100644
--- a/src/goTest/run.ts
+++ b/src/goTest/run.ts
@@ -28,6 +28,7 @@
import { debugTestAtCursor } from '../goTest';
import { GoExtensionContext } from '../context';
import path = require('path');
+import { escapeRegExp } from '../subTestUtils';
let debugSessionID = 0;
@@ -190,7 +191,7 @@
const run = this.ctrl.createTestRun(request, `Debug ${name}`);
if (!testFunctions) return;
- const started = await debugTestAtCursor(doc, name, testFunctions, goConfig, id);
+ const started = await debugTestAtCursor(doc, escapeSubTestName(name), testFunctions, goConfig, id);
if (!started) {
subs.forEach((s) => s.dispose());
run.end();
@@ -463,7 +464,7 @@
...rest,
outputChannel,
dir: pkg.uri?.fsPath ?? '',
- functions: Object.keys(functions),
+ functions: Object.keys(functions)?.map((v) => escapeSubTestName(v)),
goTestOutputConsumer: rest.isBenchmark
? (e) => this.consumeGoBenchmarkEvent(run, functions, complete, e)
: (e) => this.consumeGoTestEvent(run, functions, record, complete, concat, e)
@@ -503,12 +504,12 @@
const m = name.substring(pos).match(re);
if (!m) {
if (!parent) return tests[name];
- return this.resolver.getOrCreateSubTest(parent, name.substring(pos), name, true);
+ return this.resolver.getOrCreateSubTest(parent, name.substring(pos), name);
}
const subName = name.substring(0, pos + (m.index ?? 0));
const test = parent
- ? this.resolver.getOrCreateSubTest(parent, name.substring(pos, pos + (m.index ?? 0)), subName, true)
+ ? this.resolver.getOrCreateSubTest(parent, name.substring(pos, pos + (m.index ?? 0)), subName)
: tests[subName];
return resolve(test, pos + (m.index ?? 0), m[0].length);
};
@@ -722,3 +723,17 @@
return output.some((x) => rePkg.test(x));
}
}
+
+// escapeSubTestName escapes regexp-like metacharacters. Unlike
+// escapeSubTestName in subTestUtils.ts, this assumes the input are
+// coming from the test explorer test items whose names are computed from
+// the actual test run, not from a hacky source code analysis so escaping
+// empty unprintable characters is not necessary here.
+function escapeSubTestName(v: string) {
+ return v?.includes('/')
+ ? v
+ .split('/')
+ .map((part) => escapeRegExp(part), '')
+ .join('/')
+ : v;
+}
diff --git a/src/subTestUtils.ts b/src/subTestUtils.ts
index 76d8748..a81a6e4 100644
--- a/src/subTestUtils.ts
+++ b/src/subTestUtils.ts
@@ -18,6 +18,12 @@
return `${testFuncName}/${subTestName}`
.replace(/\s/g, '_')
.split('/')
- .map((part) => `\\Q${part}\\E`, '')
+ .map((part) => escapeRegExp(part), '')
.join('$/^');
}
+
+// escapeRegExp escapes regex metacharacters.
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
+export function escapeRegExp(v: string) {
+ return v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
diff --git a/test/gopls/goTest.run.test.ts b/test/gopls/goTest.run.test.ts
index f1d2efe..112959a 100644
--- a/test/gopls/goTest.run.test.ts
+++ b/test/gopls/goTest.run.test.ts
@@ -230,15 +230,21 @@
// Locate subtest
console.log('Locate subtest');
- const tSub = tMain.children.get(GoTest.id(uri, 'test', 'TestMain/Sub'));
+ const tSub = tMain.children.get(GoTest.id(uri, 'test', 'TestMain/Sub|Test'));
assert(tSub, 'Subtest was not created');
console.log('Locate subtests with conflicting names');
- const tSub2 = tMain.children.get(GoTest.id(uri, 'test', 'TestMain/Sub#01'));
+ const tSub2 = tMain.children.get(GoTest.id(uri, 'test', 'TestMain/Sub|Test#01'));
assert(tSub2, 'Subtest #01 was not created');
- const tSub3 = tMain.children.get(GoTest.id(uri, 'test', 'TestMain/Sub#01#01'));
+ const tSub3 = tMain.children.get(GoTest.id(uri, 'test', 'TestMain/Sub|Test#01#01'));
assert(tSub3, 'Subtest #01#01 was not created');
+ const tSub4 = tMain.children.get(GoTest.id(uri, 'test', 'TestMain/1_+_1'));
+ assert(tSub4, 'Subtest 1_+_1 was not created');
+
+ const tSub5 = tSub4.children.get(GoTest.id(uri, 'test', 'TestMain/1_+_1/Nested'));
+ assert(tSub5, 'Subtest 1_+_1/Nested was not created');
+
// Run subtest by itself
console.log('Run subtest by itself');
assert(
@@ -255,7 +261,7 @@
console.log('Verify TestMain/Sub was run');
call = spy.lastCall.args[0];
assert.strictEqual(call.dir, subTestDir);
- assert.deepStrictEqual(call.functions, ['TestMain/Sub']);
+ assert.deepStrictEqual(call.functions, ['TestMain/Sub\\|Test']); // | is escaped.
spy.resetHistory();
// Ensure the subtest hasn't been disposed
diff --git a/test/testdata/subTest/sub_test.go b/test/testdata/subTest/sub_test.go
index ce66fe9..1aad473 100644
--- a/test/testdata/subTest/sub_test.go
+++ b/test/testdata/subTest/sub_test.go
@@ -4,9 +4,13 @@
func TestMain(t *testing.T) {
t.Log("Main")
- t.Run("Sub", func(t *testing.T) { t.Log("Sub") })
- t.Run("Sub", func(t *testing.T) { t.Log("Sub#01") })
- t.Run("Sub#01", func(t *testing.T) { t.Log("Sub#01#01") })
+ t.Run("Sub|Test", func(t *testing.T) { t.Log("Sub") })
+ t.Run("Sub|Test", func(t *testing.T) { t.Log("Sub#01") })
+ t.Run("Sub|Test#01", func(t *testing.T) { t.Log("Sub#01#01") })
+
+ t.Run("1 + 1", func(t *testing.T) {
+ t.Run("Nested", func(t *testing.T) { t.Log("1 + 1 = 2") })
+ })
}
func TestOther(t *testing.T) {
diff --git a/test/unit/subTestUtils.test.ts b/test/unit/subTestUtils.test.ts
index 7043cb4..4eb19cf 100644
--- a/test/unit/subTestUtils.test.ts
+++ b/test/unit/subTestUtils.test.ts
@@ -12,12 +12,17 @@
{
test: 'TestFunction',
subtest: 'GET /path/with/slashes',
- want: '\\QTestFunction\\E$/^\\QGET_\\E$/^\\Qpath\\E$/^\\Qwith\\E$/^\\Qslashes\\E'
+ want: 'TestFunction$/^GET_$/^path$/^with$/^slashes'
},
{
test: 'TestMain',
subtest: 'All{0,1} tests [run]+ (fine)',
- want: '\\QTestMain\\E$/^\\QAll{0,1}_tests_[run]+_(fine)\\E'
+ want: 'TestMain$/^All\\{0,1\\}_tests_\\[run\\]\\+_\\(fine\\)'
+ },
+ {
+ test: 'TestMain',
+ subtest: 'Foo|Bar+',
+ want: 'TestMain$/^Foo\\|Bar\\+'
}
];