blob: 645d73ef0c0617aec75aaf610456d971192a6299 [file] [log] [blame]
/*---------------------------------------------------------
* Copyright 2021 The Go Authors. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------*/
import assert = require('assert');
import path = require('path');
import {
DocumentSymbol,
FileType,
TestItem,
Uri,
TextDocument,
SymbolKind,
Range,
Position,
TestItemCollection,
TextDocumentChangeEvent
} from 'vscode';
import { packagePathToGoModPathMap as pkg2mod } from '../../src/goModules';
import { TestExplorer, testID } from '../../src/goTestExplorer';
import { MockTestController, MockTestWorkspace } from '../mocks/MockTest';
type Files = Record<string, string | { contents: string; language: string }>;
interface TestCase {
workspace: string[];
files: Files;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function symbols(doc: TextDocument, token: unknown): Thenable<DocumentSymbol[]> {
const syms: DocumentSymbol[] = [];
const range = new Range(new Position(0, 0), new Position(0, 0));
doc.getText().replace(/^func (Test|Benchmark|Example)([A-Z]\w+)(\(.*\))/gm, (m, type, name, details) => {
syms.push(new DocumentSymbol(type + name, details, SymbolKind.Function, range, range));
return m;
});
return Promise.resolve(syms);
}
function setup(folders: string[], files: Files) {
return setupCtor(folders, files, TestExplorer);
}
function setupCtor<T extends TestExplorer>(
folders: string[],
files: Files,
ctor: new (...args: ConstructorParameters<typeof TestExplorer>) => T
) {
const ws = MockTestWorkspace.from(folders, files);
const ctrl = new MockTestController();
const expl = new ctor(ctrl, ws, symbols);
function walk(dir: Uri, modpath?: string) {
const dirs: Uri[] = [];
for (const [name, type] of ws.fs.dirs.get(dir.toString())) {
const uri = dir.with({ path: path.join(dir.path, name) });
if (type === FileType.Directory) {
dirs.push(uri);
} else if (name === 'go.mod') {
modpath = dir.path;
}
}
pkg2mod[dir.path] = modpath || '';
for (const dir of dirs) {
walk(dir, modpath);
}
}
// prevent getModFolderPath from actually doing anything;
for (const pkg in pkg2mod) delete pkg2mod[pkg];
walk(Uri.file('/'));
return { ctrl, expl, ws };
}
function assertTestItems(items: TestItemCollection, expect: string[]) {
const actual: string[] = [];
function walk(items: TestItemCollection) {
items.forEach((item) => {
actual.push(item.id);
walk(item.children);
});
}
walk(items);
assert.deepStrictEqual(actual, expect);
}
suite('Test Explorer', () => {
suite('Items', () => {
interface TC extends TestCase {
item?: ([string, string, string] | [string, string, string, string])[];
expect: string[];
}
const cases: Record<string, Record<string, TC>> = {
Root: {
'Basic module': {
workspace: ['/src/proj'],
files: {
'/src/proj/go.mod': 'module test',
'/src/proj/main.go': 'package main'
},
expect: ['file:///src/proj?module']
},
'Basic workspace': {
workspace: ['/src/proj'],
files: {
'/src/proj/main.go': 'package main'
},
expect: ['file:///src/proj?workspace']
},
'Module and workspace': {
workspace: ['/src/proj1', '/src/proj2'],
files: {
'/src/proj1/go.mod': 'module test',
'/src/proj2/main.go': 'package main'
},
expect: ['file:///src/proj1?module', 'file:///src/proj2?workspace']
},
'Module in workspace': {
workspace: ['/src/proj'],
files: {
'/src/proj/mod/go.mod': 'module test',
'/src/proj/main.go': 'package main'
},
expect: ['file:///src/proj/mod?module', 'file:///src/proj?workspace']
}
},
Module: {
'Empty': {
workspace: ['/src/proj'],
files: {
'/src/proj/go.mod': 'module test',
'/src/proj/main.go': 'package main'
},
item: [['test', '/src/proj', 'module']],
expect: []
},
'Root package': {
workspace: ['/src/proj'],
files: {
'/src/proj/go.mod': 'module test',
'/src/proj/main_test.go': 'package main'
},
item: [['test', '/src/proj', 'module']],
expect: ['file:///src/proj/main_test.go?file']
},
'Sub packages': {
workspace: ['/src/proj'],
files: {
'/src/proj/go.mod': 'module test',
'/src/proj/foo/main_test.go': 'package main',
'/src/proj/bar/main_test.go': 'package main'
},
item: [['test', '/src/proj', 'module']],
expect: ['file:///src/proj/foo?package', 'file:///src/proj/bar?package']
},
'Nested packages': {
workspace: ['/src/proj'],
files: {
'/src/proj/go.mod': 'module test',
'/src/proj/main_test.go': 'package main',
'/src/proj/foo/main_test.go': 'package main',
'/src/proj/foo/bar/main_test.go': 'package main'
},
item: [['test', '/src/proj', 'module']],
expect: [
'file:///src/proj/foo?package',
'file:///src/proj/foo/bar?package',
'file:///src/proj/main_test.go?file'
]
}
},
Package: {
'Empty': {
workspace: ['/src/proj'],
files: {
'/src/proj/go.mod': 'module test',
'/src/proj/pkg/main.go': 'package main'
},
item: [
['test', '/src/proj', 'module'],
['pkg', '/src/proj/pkg', 'package']
],
expect: []
},
'Flat': {
workspace: ['/src/proj'],
files: {
'/src/proj/go.mod': 'module test',
'/src/proj/pkg/main_test.go': 'package main',
'/src/proj/pkg/sub/main_test.go': 'package main'
},
item: [
['test', '/src/proj', 'module'],
['pkg', '/src/proj/pkg', 'package']
],
expect: ['file:///src/proj/pkg/main_test.go?file']
},
'Sub package': {
workspace: ['/src/proj'],
files: {
'/src/proj/go.mod': 'module test',
'/src/proj/pkg/sub/main_test.go': 'package main'
},
item: [
['test', '/src/proj', 'module'],
['pkg', '/src/proj/pkg', 'package']
],
expect: []
}
},
File: {
'Empty': {
workspace: ['/src/proj'],
files: {
'/src/proj/go.mod': 'module test',
'/src/proj/main_test.go': 'package main'
},
item: [
['test', '/src/proj', 'module'],
['main_test.go', '/src/proj/main_test.go', 'file']
],
expect: []
},
'One of each': {
workspace: ['/src/proj'],
files: {
'/src/proj/go.mod': 'module test',
'/src/proj/main_test.go': `
package main
func TestMain(*testing.M) {}
func TestFoo(*testing.T) {}
func BenchmarkBar(*testing.B) {}
func ExampleBaz() {}
`
},
item: [
['test', '/src/proj', 'module'],
['main_test.go', '/src/proj/main_test.go', 'file']
],
expect: [
'file:///src/proj/main_test.go?test#TestFoo',
'file:///src/proj/main_test.go?benchmark#BenchmarkBar',
'file:///src/proj/main_test.go?example#ExampleBaz'
]
}
}
};
for (const n in cases) {
suite(n, () => {
for (const m in cases[n]) {
test(m, async () => {
const { workspace, files, expect, item: itemData = [] } = cases[n][m];
const { ctrl } = setup(workspace, files);
let item: TestItem | undefined;
for (const [label, uri, kind, name] of itemData) {
const u = Uri.parse(uri);
const child = ctrl.createTestItem(testID(u, kind, name), label, u);
(item?.children || ctrl.items).add(child);
item = child;
}
await ctrl.resolveHandler(item);
const actual: string[] = [];
(item?.children || ctrl.items).forEach((x) => actual.push(x.id));
assert.deepStrictEqual(actual, expect);
});
}
});
}
});
suite('Events', () => {
suite('Document opened', () => {
class DUT extends TestExplorer {
async _didOpen(doc: TextDocument) {
await this.didOpenTextDocument(doc);
}
}
interface TC extends TestCase {
open: string;
expect: string[];
}
const cases: Record<string, TC> = {
'In workspace': {
workspace: ['/src/proj'],
files: {
'/src/proj/go.mod': 'module test',
'/src/proj/foo_test.go': 'package main\nfunc TestFoo(*testing.T) {}',
'/src/proj/bar_test.go': 'package main\nfunc TestBar(*testing.T) {}',
'/src/proj/baz/main_test.go': 'package main\nfunc TestBaz(*testing.T) {}'
},
open: 'file:///src/proj/foo_test.go',
expect: [
'file:///src/proj?module',
'file:///src/proj/foo_test.go?file',
'file:///src/proj/foo_test.go?test#TestFoo'
]
},
'Outside workspace': {
workspace: [],
files: {
'/src/proj/go.mod': 'module test',
'/src/proj/foo_test.go': 'package main\nfunc TestFoo(*testing.T) {}'
},
open: 'file:///src/proj/foo_test.go',
expect: [
'file:///src/proj?module',
'file:///src/proj/foo_test.go?file',
'file:///src/proj/foo_test.go?test#TestFoo'
]
}
};
for (const name in cases) {
test(name, async () => {
const { workspace, files, open, expect } = cases[name];
const { ctrl, expl, ws } = setupCtor(workspace, files, DUT);
await expl._didOpen(ws.fs.files.get(open));
assertTestItems(ctrl.items, expect);
});
}
});
suite('Document edited', async () => {
class DUT extends TestExplorer {
async _didOpen(doc: TextDocument) {
await this.didOpenTextDocument(doc);
}
async _didChange(e: TextDocumentChangeEvent) {
await this.didChangeTextDocument(e);
}
}
interface TC extends TestCase {
open: string;
changes: [string, string][];
expect: {
before: string[];
after: string[];
};
}
const cases: Record<string, TC> = {
'Add test': {
workspace: ['/src/proj'],
files: {
'/src/proj/go.mod': 'module test',
'/src/proj/foo_test.go': 'package main'
},
open: 'file:///src/proj/foo_test.go',
changes: [['file:///src/proj/foo_test.go', 'package main\nfunc TestFoo(*testing.T) {}']],
expect: {
before: ['file:///src/proj?module'],
after: [
'file:///src/proj?module',
'file:///src/proj/foo_test.go?file',
'file:///src/proj/foo_test.go?test#TestFoo'
]
}
},
'Remove test': {
workspace: ['/src/proj'],
files: {
'/src/proj/go.mod': 'module test',
'/src/proj/foo_test.go': 'package main\nfunc TestFoo(*testing.T) {}'
},
open: 'file:///src/proj/foo_test.go',
changes: [['file:///src/proj/foo_test.go', 'package main']],
expect: {
before: [
'file:///src/proj?module',
'file:///src/proj/foo_test.go?file',
'file:///src/proj/foo_test.go?test#TestFoo'
],
after: ['file:///src/proj?module']
}
}
};
for (const name in cases) {
test(name, async () => {
const { workspace, files, open, changes, expect } = cases[name];
const { ctrl, expl, ws } = setupCtor(workspace, files, DUT);
await expl._didOpen(ws.fs.files.get(open));
assertTestItems(ctrl.items, expect.before);
for (const [file, contents] of changes) {
const doc = ws.fs.files.get(file);
doc.contents = contents;
await expl._didChange({
document: doc,
contentChanges: []
});
}
assertTestItems(ctrl.items, expect.after);
});
}
});
});
});