Skip to content

Commit d6fb202

Browse files
committed
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 #3070 Fixes #2624 For #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 <[email protected]> Commit-Queue: Hyang-Ah Hana Kim <[email protected]> TryBot-Result: kokoro <[email protected]>
1 parent fae8cb9 commit d6fb202

File tree

6 files changed

+71
-28
lines changed

6 files changed

+71
-28
lines changed

src/goTest/resolve.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -169,27 +169,34 @@ export class GoTestResolver {
169169
return it(this.items);
170170
}
171171

172-
// Create or Retrieve a sub test or benchmark. The ID will be of the form:
172+
// Create or Retrieve a sub test or benchmark. This is always dynamically
173+
// called while processing test run output.
174+
// The parent's uri is of the form:
173175
// file:///path/to/mod/file.go?test#TestXxx%2fA%2fB%2fC
174-
getOrCreateSubTest(item: TestItem, label: string, name: string, dynamic?: boolean): TestItem | undefined {
175-
if (!item.uri) return;
176-
const { kind } = GoTest.parseId(item.id);
176+
// The name is the name of the subtest to get or create.
177+
// The label is the title presented in the UI.
178+
// We expect the caller to call getOrCreateSubTest with the
179+
// right parent.
180+
// For example, if TestXxx has subtests and we are processing
181+
// TestXxx/Sub1/Sub2, the parent item should be a node corresponding
182+
// to TestXxx/Sub1, the label is Sub2 while the name is TestXxx/Sub1/Sub2.
183+
getOrCreateSubTest(parent: TestItem, label: string, name: string): TestItem | undefined {
184+
if (!parent.uri) return;
185+
const { kind } = GoTest.parseId(parent.id);
177186

178187
let existing: TestItem | undefined;
179-
item.children.forEach((child) => {
180-
if (child.label === name) existing = child;
188+
parent.children.forEach((child) => {
189+
if (child.label === label) existing = child;
181190
});
182191
if (existing) return existing;
183192

184-
item.canResolveChildren = true;
185-
const sub = this.createItem(label, item.uri, kind, name);
186-
item.children.add(sub);
193+
parent.canResolveChildren = true;
194+
const sub = this.createItem(label, parent.uri, kind, name);
195+
parent.children.add(sub);
187196

188-
if (dynamic) {
189-
this.isDynamicSubtest.add(sub);
190-
if (this.shouldSetRange(item)) {
191-
sub.range = item.range;
192-
}
197+
this.isDynamicSubtest.add(sub);
198+
if (this.shouldSetRange(parent)) {
199+
sub.range = parent.range;
193200
}
194201

195202
return sub;

src/goTest/run.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { GoTestProfiler, ProfilingOptions } from './profile';
2828
import { debugTestAtCursor } from '../goTest';
2929
import { GoExtensionContext } from '../context';
3030
import path = require('path');
31+
import { escapeRegExp } from '../subTestUtils';
3132

3233
let debugSessionID = 0;
3334

@@ -190,7 +191,7 @@ export class GoTestRunner {
190191

191192
const run = this.ctrl.createTestRun(request, `Debug ${name}`);
192193
if (!testFunctions) return;
193-
const started = await debugTestAtCursor(doc, name, testFunctions, goConfig, id);
194+
const started = await debugTestAtCursor(doc, escapeSubTestName(name), testFunctions, goConfig, id);
194195
if (!started) {
195196
subs.forEach((s) => s.dispose());
196197
run.end();
@@ -463,7 +464,7 @@ export class GoTestRunner {
463464
...rest,
464465
outputChannel,
465466
dir: pkg.uri?.fsPath ?? '',
466-
functions: Object.keys(functions),
467+
functions: Object.keys(functions)?.map((v) => escapeSubTestName(v)),
467468
goTestOutputConsumer: rest.isBenchmark
468469
? (e) => this.consumeGoBenchmarkEvent(run, functions, complete, e)
469470
: (e) => this.consumeGoTestEvent(run, functions, record, complete, concat, e)
@@ -503,12 +504,12 @@ export class GoTestRunner {
503504
const m = name.substring(pos).match(re);
504505
if (!m) {
505506
if (!parent) return tests[name];
506-
return this.resolver.getOrCreateSubTest(parent, name.substring(pos), name, true);
507+
return this.resolver.getOrCreateSubTest(parent, name.substring(pos), name);
507508
}
508509

509510
const subName = name.substring(0, pos + (m.index ?? 0));
510511
const test = parent
511-
? this.resolver.getOrCreateSubTest(parent, name.substring(pos, pos + (m.index ?? 0)), subName, true)
512+
? this.resolver.getOrCreateSubTest(parent, name.substring(pos, pos + (m.index ?? 0)), subName)
512513
: tests[subName];
513514
return resolve(test, pos + (m.index ?? 0), m[0].length);
514515
};
@@ -722,3 +723,17 @@ export class GoTestRunner {
722723
return output.some((x) => rePkg.test(x));
723724
}
724725
}
726+
727+
// escapeSubTestName escapes regexp-like metacharacters. Unlike
728+
// escapeSubTestName in subTestUtils.ts, this assumes the input are
729+
// coming from the test explorer test items whose names are computed from
730+
// the actual test run, not from a hacky source code analysis so escaping
731+
// empty unprintable characters is not necessary here.
732+
function escapeSubTestName(v: string) {
733+
return v?.includes('/')
734+
? v
735+
.split('/')
736+
.map((part) => escapeRegExp(part), '')
737+
.join('/')
738+
: v;
739+
}

src/subTestUtils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ export function escapeSubTestName(testFuncName: string, subTestName: string): st
1818
return `${testFuncName}/${subTestName}`
1919
.replace(/\s/g, '_')
2020
.split('/')
21-
.map((part) => `\\Q${part}\\E`, '')
21+
.map((part) => escapeRegExp(part), '')
2222
.join('$/^');
2323
}
24+
25+
// escapeRegExp escapes regex metacharacters.
26+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
27+
export function escapeRegExp(v: string) {
28+
return v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
29+
}

test/gopls/goTest.run.test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,15 +230,21 @@ suite('Go Test Runner', () => {
230230

231231
// Locate subtest
232232
console.log('Locate subtest');
233-
const tSub = tMain.children.get(GoTest.id(uri, 'test', 'TestMain/Sub'));
233+
const tSub = tMain.children.get(GoTest.id(uri, 'test', 'TestMain/Sub|Test'));
234234
assert(tSub, 'Subtest was not created');
235235

236236
console.log('Locate subtests with conflicting names');
237-
const tSub2 = tMain.children.get(GoTest.id(uri, 'test', 'TestMain/Sub#01'));
237+
const tSub2 = tMain.children.get(GoTest.id(uri, 'test', 'TestMain/Sub|Test#01'));
238238
assert(tSub2, 'Subtest #01 was not created');
239-
const tSub3 = tMain.children.get(GoTest.id(uri, 'test', 'TestMain/Sub#01#01'));
239+
const tSub3 = tMain.children.get(GoTest.id(uri, 'test', 'TestMain/Sub|Test#01#01'));
240240
assert(tSub3, 'Subtest #01#01 was not created');
241241

242+
const tSub4 = tMain.children.get(GoTest.id(uri, 'test', 'TestMain/1_+_1'));
243+
assert(tSub4, 'Subtest 1_+_1 was not created');
244+
245+
const tSub5 = tSub4.children.get(GoTest.id(uri, 'test', 'TestMain/1_+_1/Nested'));
246+
assert(tSub5, 'Subtest 1_+_1/Nested was not created');
247+
242248
// Run subtest by itself
243249
console.log('Run subtest by itself');
244250
assert(
@@ -255,7 +261,7 @@ suite('Go Test Runner', () => {
255261
console.log('Verify TestMain/Sub was run');
256262
call = spy.lastCall.args[0];
257263
assert.strictEqual(call.dir, subTestDir);
258-
assert.deepStrictEqual(call.functions, ['TestMain/Sub']);
264+
assert.deepStrictEqual(call.functions, ['TestMain/Sub\\|Test']); // | is escaped.
259265
spy.resetHistory();
260266

261267
// Ensure the subtest hasn't been disposed

test/testdata/subTest/sub_test.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ import "testing"
44

55
func TestMain(t *testing.T) {
66
t.Log("Main")
7-
t.Run("Sub", func(t *testing.T) { t.Log("Sub") })
8-
t.Run("Sub", func(t *testing.T) { t.Log("Sub#01") })
9-
t.Run("Sub#01", func(t *testing.T) { t.Log("Sub#01#01") })
7+
t.Run("Sub|Test", func(t *testing.T) { t.Log("Sub") })
8+
t.Run("Sub|Test", func(t *testing.T) { t.Log("Sub#01") })
9+
t.Run("Sub|Test#01", func(t *testing.T) { t.Log("Sub#01#01") })
10+
11+
t.Run("1 + 1", func(t *testing.T) {
12+
t.Run("Nested", func(t *testing.T) { t.Log("1 + 1 = 2") })
13+
})
1014
}
1115

1216
func TestOther(t *testing.T) {

test/unit/subTestUtils.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,17 @@ suite('escapeSubTestName Tests', () => {
1212
{
1313
test: 'TestFunction',
1414
subtest: 'GET /path/with/slashes',
15-
want: '\\QTestFunction\\E$/^\\QGET_\\E$/^\\Qpath\\E$/^\\Qwith\\E$/^\\Qslashes\\E'
15+
want: 'TestFunction$/^GET_$/^path$/^with$/^slashes'
1616
},
1717
{
1818
test: 'TestMain',
1919
subtest: 'All{0,1} tests [run]+ (fine)',
20-
want: '\\QTestMain\\E$/^\\QAll{0,1}_tests_[run]+_(fine)\\E'
20+
want: 'TestMain$/^All\\{0,1\\}_tests_\\[run\\]\\+_\\(fine\\)'
21+
},
22+
{
23+
test: 'TestMain',
24+
subtest: 'Foo|Bar+',
25+
want: 'TestMain$/^Foo\\|Bar\\+'
2126
}
2227
];
2328

0 commit comments

Comments
 (0)