blob: 31a581ff2ff716c082f66988cdddc7d9244b8c3d [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 sinon = require('sinon');
import { TextDocument, TestItem, TestItemCollection, TextDocumentChangeEvent, workspace, Uri } from 'vscode';
import { GoTestExplorer } from '../../src/goTest/explore';
import { MockTestController, MockTestWorkspace } from '../mocks/MockTest';
import { forceDidOpenTextDocument, getSymbols_Regex, populateModulePathCache } from './goTest.utils';
import { MockExtensionContext } from '../mocks/MockContext';
import { MockMemento } from '../mocks/MockMemento';
import * as config from '../../src/config';
import { GoTestResolver } from '../../src/goTest/resolve';
import * as testUtils from '../../src/testUtils';
import { GoTest } from '../../src/goTest/utils';
type Files = Record<string, string | { contents: string; language: string }>;
interface TestCase {
workspace: string[];
files: Files;
}
function newExplorer<T extends GoTestExplorer>(
folders: string[],
files: Files,
ctor: new (...args: ConstructorParameters<typeof GoTestExplorer>) => T
) {
const ws = MockTestWorkspace.from(folders, files);
const ctrl = new MockTestController();
const expl = new ctor(ws, ctrl, new MockMemento(), getSymbols_Regex);
populateModulePathCache(ws);
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);
}
async function forceResolve(resolver: GoTestResolver, item?: TestItem) {
await resolver.resolve(item);
const items: TestItem[] = [];
(item?.children || resolver.items).forEach((x) => items.push(x));
await Promise.all(items.map((x) => forceResolve(resolver, x)));
}
suite('Go Test Explorer', () => {
suite('Document opened', () => {
class DUT extends GoTestExplorer {
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 } = newExplorer(workspace, files, DUT);
await expl._didOpen(ws.fs.files.get(open));
assertTestItems(ctrl.items, expect);
});
}
});
suite('Document edited', async () => {
class DUT extends GoTestExplorer {
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 } = newExplorer(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);
});
}
});
suite('stretchr', () => {
const fixtureDir = path.join(__dirname, '..', '..', '..', 'test', 'testdata', 'stretchrTestSuite');
const ctx = MockExtensionContext.new();
let document: TextDocument;
let testExplorer: GoTestExplorer;
suiteSetup(async () => {
testExplorer = GoTestExplorer.setup(ctx);
const uri = Uri.file(path.join(fixtureDir, 'suite_test.go'));
document = await forceDidOpenTextDocument(workspace, testExplorer, uri);
});
suiteTeardown(() => {
ctx.teardown();
});
test('discovery', () => {
const tests = testExplorer.resolver.find(document.uri).map((x) => x.id);
assert.deepStrictEqual(tests.sort(), [
document.uri.with({ query: 'file' }).toString(),
document.uri.with({ query: 'test', fragment: '(*ExampleTestSuite).TestExample' }).toString(),
document.uri.with({ query: 'test', fragment: 'TestExampleTestSuite' }).toString()
]);
});
});
suite('settings', () => {
const sandbox = sinon.createSandbox();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let goConfig: any;
setup(() => {
goConfig = Object.create(config.getGoConfig());
sandbox.stub(config, 'getGoConfig').returns(goConfig);
});
teardown(() => {
sandbox.restore();
});
suite('packageDisplayMode', () => {
let resolver: GoTestResolver;
setup(() => {
const { expl } = newExplorer(
['/src/proj'],
{
'/src/proj/go.mod': 'module test',
'/src/proj/pkg/main_test.go': 'package main\nfunc TestFoo(t *testing.T) {}',
'/src/proj/pkg/sub/main_test.go': 'package main\nfunc TestBar(t *testing.T) {}'
},
GoTestExplorer
);
resolver = expl.resolver;
});
test('flat', async () => {
// Expect:
// - module
// - package pkg
// - package pkg/sub
sinon.stub(goConfig, 'get').withArgs('testExplorer.packageDisplayMode').returns('flat');
await forceResolve(resolver);
const mod = resolver.items.get('file:///src/proj?module');
assert(mod, 'Module is missing or is not at the root');
const pkg = mod.children.get('file:///src/proj/pkg?package');
assert(pkg, 'Package pkg is missing or not a child of the module');
const sub = mod.children.get('file:///src/proj/pkg/sub?package');
assert(sub, 'Package pkg/sub is missing or not a child of the module');
});
test('nested', async () => {
// Expect:
// - module
// - package pkg
// - package pkg/sub
sinon.stub(goConfig, 'get').withArgs('testExplorer.packageDisplayMode').returns('nested');
await forceResolve(resolver);
const mod = resolver.items.get('file:///src/proj?module');
assert(mod, 'Module is missing or is not at the root');
const pkg = mod.children.get('file:///src/proj/pkg?package');
assert(pkg, 'Package pkg is missing or not a child of the module');
const sub = pkg.children.get('file:///src/proj/pkg/sub?package');
assert(sub, 'Package pkg/sub is missing or not a child of package pkg');
});
});
suite('alwaysRunBenchmarks', () => {
let explorer: GoTestExplorer;
let runStub: sinon.SinonStub<[testUtils.TestConfig], Promise<boolean>>;
const expectedTests = [
'file:///src/proj/pkg/main_test.go?file',
'file:///src/proj/pkg/main_test.go?test#TestFoo',
'file:///src/proj/pkg/main_test.go?benchmark#BenchmarkBar'
];
setup(() => {
runStub = sandbox.stub(testUtils, 'goTest');
runStub.callsFake(() => Promise.resolve(true));
explorer = newExplorer(
['/src/proj'],
{
'/src/proj/go.mod': 'module test',
'/src/proj/pkg/main_test.go': `
package main
func TestFoo(t *testing.T) {}
func BenchmarkBar(b *testing.B) {}
`
},
GoTestExplorer
).expl;
});
test('false', async () => {
// Running the file should only run the test
sinon.stub(goConfig, 'get').withArgs('testExplorer.alwaysRunBenchmarks').returns(false);
await forceResolve(explorer.resolver);
const tests = explorer.resolver.find(Uri.parse('file:///src/proj/pkg/main_test.go'));
assert.deepStrictEqual(
tests.map((x) => x.id),
expectedTests
);
await explorer.runner.run({ include: [tests[0]] });
assert.strictEqual(runStub.callCount, 1, 'Expected goTest to be called once');
assert.deepStrictEqual(runStub.lastCall.args[0].functions, ['TestFoo']);
});
test('true', async () => {
// Running the file should run the test and the benchmark
sinon.stub(goConfig, 'get').withArgs('testExplorer.alwaysRunBenchmarks').returns(true);
await forceResolve(explorer.resolver);
const tests = explorer.resolver.find(Uri.parse('file:///src/proj/pkg/main_test.go'));
assert.deepStrictEqual(
tests.map((x) => x.id),
expectedTests
);
await explorer.runner.run({ include: [tests[0]] });
assert.strictEqual(runStub.callCount, 2, 'Expected goTest to be called twice');
assert.deepStrictEqual(runStub.firstCall.args[0].functions, ['TestFoo']);
assert.deepStrictEqual(runStub.secondCall.args[0].functions, ['BenchmarkBar']);
});
});
suite('showDynamicSubtestsInEditor', () => {
let explorer: GoTestExplorer;
let runStub: sinon.SinonStub<[testUtils.TestConfig], Promise<boolean>>;
setup(() => {
runStub = sandbox.stub(testUtils, 'goTest');
runStub.callsFake((cfg) => {
// Trigger creation of dynamic subtest
cfg.goTestOutputConsumer({
Test: 'TestFoo/Bar',
Action: 'run'
});
return Promise.resolve(true);
});
explorer = newExplorer(
['/src/proj'],
{
'/src/proj/go.mod': 'module test',
'/src/proj/main_test.go': 'package main\nfunc TestFoo(t *testing.T) {}'
},
GoTestExplorer
).expl;
});
test('false', async () => {
// Dynamic subtests should have no location
sinon.stub(goConfig, 'get').withArgs('testExplorer.showDynamicSubtestsInEditor').returns(false);
await forceResolve(explorer.resolver);
const test = explorer.resolver
.find(Uri.parse('file:///src/proj/main_test.go'))
.filter((x) => GoTest.parseId(x.id).name)[0];
assert(test, 'Could not find test');
await explorer.runner.run({ include: [test] });
assert.strictEqual(runStub.callCount, 1, 'Expected goTest to be called once');
const subTest = test.children.get('file:///src/proj/main_test.go?test#TestFoo%2FBar');
assert(subTest, 'Could not find subtest');
assert(!subTest.range, 'Subtest should not have a range');
});
test('true', async () => {
// Dynamic subtests should have the same location as their parents
sinon.stub(goConfig, 'get').withArgs('testExplorer.showDynamicSubtestsInEditor').returns(true);
await forceResolve(explorer.resolver);
const test = explorer.resolver
.find(Uri.parse('file:///src/proj/main_test.go'))
.filter((x) => GoTest.parseId(x.id).name)[0];
assert(test, 'Could not find test');
await explorer.runner.run({ include: [test] });
assert.strictEqual(runStub.callCount, 1, 'Expected goTest to be called once');
const subTest = test.children.get('file:///src/proj/main_test.go?test#TestFoo%2FBar');
assert(subTest, 'Could not find subtest');
assert.deepStrictEqual(subTest.range, test.range, 'Subtest range should match parent range');
});
});
});
});