blob: 35b249cff609e07aafc36cc214be61a08da53403 [file] [log] [blame]
import assert = require('assert');
import path = require('path');
import sinon = require('sinon');
import { Range, TestItem, Uri, workspace } from 'vscode';
import * as testUtils from '../../src/testUtils';
import { forceDidOpenTextDocument } from './goTest.utils';
import { GoTestExplorer } from '../../src/goTest/explore';
import { MockExtensionContext } from '../mocks/MockContext';
import { GoTest } from '../../src/goTest/utils';
import { Env } from './goplsTestEnv.utils';
import { updateGoVarsFromConfig } from '../../src/goInstallTools';
suite('Go Test Runner', () => {
const fixtureDir = path.join(__dirname, '..', '..', '..', 'test', 'testdata');
let testExplorer: GoTestExplorer;
suiteSetup(async () => {
await updateGoVarsFromConfig({});
});
suite('parseOutput', () => {
const ctx = MockExtensionContext.new();
suiteSetup(async () => {
testExplorer = GoTestExplorer.setup(ctx, {});
});
suiteTeardown(() => ctx.teardown());
const fileURI = Uri.parse('file:///path/to/mod/file.go');
const filePath = fileURI.fsPath;
function testParseOutput(output: string, expected: { file: string; line: number; msg: string }[]) {
const id = GoTest.id(fileURI, 'test', 'TestXXX');
const ti = { id, uri: fileURI, 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: filePath, 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: filePath, line: 7, msg: 'foo\n' }]));
test('multiple file locs in one line', () =>
testParseOutput('file.go:1: line1 . file.go:2: line2 \n', [{ file: filePath, line: 1, msg: 'line2 \n' }]));
});
suite('Profile', function () {
const sandbox = sinon.createSandbox();
const ctx = MockExtensionContext.new();
const env = new Env();
let uri: Uri;
let stub: sinon.SinonStub<[testUtils.TestConfig], Promise<boolean>>;
suiteSetup(async () => {
uri = Uri.file(path.join(fixtureDir, 'codelens', 'codelens2_test.go'));
await env.startGopls(uri.fsPath);
testExplorer = GoTestExplorer.setup(ctx, env.goCtx);
await forceDidOpenTextDocument(workspace, testExplorer, uri);
});
setup(() => {
stub = sandbox.stub(testUtils, 'goTest');
stub.callsFake((cfg) => {
const send = cfg.goTestOutputConsumer;
if (send && cfg.functions instanceof Array) {
cfg.functions.forEach((name) => send({ Test: name, Action: 'run' }));
cfg.functions.forEach((name) => send({ Test: name, Action: 'pass' }));
}
return Promise.resolve(true);
});
});
teardown(() => {
sandbox.restore();
});
// suiteTeardown
this.afterEach(async function () {
await env.teardown();
// Note: this shouldn't use () => {...}. Arrow functions do not have 'this'.
// I don't know why but this.currentTest.state does not have the expected value when
// used with teardown.
env.flushTrace(this.currentTest?.state === 'failed');
ctx.teardown();
});
test('creates a profile', async () => {
const test = Array.from(testExplorer.resolver.allItems).filter((x) => GoTest.parseId(x.id).name)[0];
assert(test, 'No tests found');
assert(
await testExplorer.runner.run(
{
include: [test],
exclude: undefined,
profile: undefined
},
undefined,
{ kind: 'cpu' }
),
'Failed to execute `go test`'
);
assert.strictEqual(stub.callCount, 1, 'expected one call to goTest');
assert(stub.lastCall.args[0].flags.some((x) => x === '--cpuprofile'));
assert(testExplorer.profiler.hasProfileFor(test.id), 'Did not create profile for test');
});
test('tests are run together when not profiling', async () => {
const tests = Array.from(testExplorer.resolver.allItems).filter((x) => GoTest.parseId(x.id).name);
assert(tests, 'No tests found');
assert(
await testExplorer.runner.run({
include: tests,
exclude: undefined,
profile: undefined
}),
'Failed to execute `go test`'
);
assert.strictEqual(stub.callCount, 1, 'expected one call to goTest');
assert.deepStrictEqual(
stub.lastCall.args[0].functions,
tests.map((x) => GoTest.parseId(x.id).name)
);
});
test.skip('tests are run individually when profiling', async () => {
const tests = Array.from(testExplorer.resolver.allItems).filter((x) => GoTest.parseId(x.id).name);
assert(tests, 'No tests found');
console.log(`running ${tests.length} tests`);
assert(
await testExplorer.runner.run(
{
include: tests,
exclude: undefined,
profile: undefined
},
undefined,
{ kind: 'cpu' }
),
'Failed to execute `go test`'
);
console.log('verify we got expected calls');
const calls = await stub.getCalls();
assert.strictEqual(calls.length, tests.length, 'expected one call to goTest per test');
calls.forEach((call, i) =>
assert.deepStrictEqual(call.args[0].functions, [GoTest.parseId(tests[i].id).name])
);
tests.forEach((test) =>
assert(testExplorer.profiler.hasProfileFor(test.id), `Missing profile for ${test.id}`)
);
});
});
suite('Subtest', function () {
// WARNING: each call to testExplorer.runner.run triggers one or more
// `go test` command runs (testUtils.goTest is spied, not mocked or replaced).
// Each `go test` command invocation can take seconds on slow machines.
// As we add more cases, the timeout should be increased accordingly.
const sandbox = sinon.createSandbox();
const subTestDir = path.join(fixtureDir, 'subTest');
const ctx = MockExtensionContext.new();
const env = new Env();
let uri: Uri;
let spy: sinon.SinonSpy<[testUtils.TestConfig], Promise<boolean>>;
suiteSetup(async () => {
uri = Uri.file(path.join(subTestDir, 'sub_test.go'));
// TODO(hyangah): I don't know why, but gopls seems to pick up ./test/testdata/codelens as
// the workspace directory when we don't explicitly set the workspace directory
// (so initialize request doesn't include workspace dir info). The codelens directory was
// used in the previous test suite. Figure out why.
await env.startGopls(uri.fsPath, undefined, subTestDir);
testExplorer = GoTestExplorer.setup(ctx, env.goCtx);
await forceDidOpenTextDocument(workspace, testExplorer, uri);
spy = sandbox.spy(testUtils, 'goTest');
});
// suiteTeardown
this.afterEach(async function () {
await env.teardown();
env.flushTrace(this.currentTest?.state === 'failed');
ctx.teardown();
sandbox.restore();
});
test('discover and run', async () => {
console.log('discover and run');
// Locate TestMain and TestOther
const tests = testExplorer.resolver.find(uri).filter((x) => GoTest.parseId(x.id).kind === 'test');
tests.sort((a, b) => a.label.localeCompare(b.label));
assert.deepStrictEqual(
tests.map((x) => x.label),
['TestMain', 'TestOther']
);
const [tMain, tOther] = tests;
// Run TestMain
console.log('Run TestMain');
assert(
await testExplorer.runner.run({
include: [tMain],
exclude: undefined,
profile: undefined
}),
'Failed to execute `go test`'
);
assert.strictEqual(spy.callCount, 1, 'expected one call to goTest');
// Verify TestMain was run
console.log('Verify TestMain was run');
let call = spy.lastCall.args[0];
assert.strictEqual(call.dir, subTestDir);
assert.deepStrictEqual(call.functions, ['TestMain']);
spy.resetHistory();
// Locate subtest
console.log('Locate subtest');
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|Test#01'));
assert(tSub2, 'Subtest #01 was not created');
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(
await testExplorer.runner.run({
include: [tSub],
exclude: undefined,
profile: undefined
}),
'Failed to execute `go test`'
);
assert.strictEqual(spy.callCount, 1, 'expected one call to goTest');
// Verify TestMain/Sub was run
console.log('Verify TestMain/Sub was run');
call = spy.lastCall.args[0];
assert.strictEqual(call.dir, subTestDir);
assert.deepStrictEqual(call.functions, ['TestMain/Sub\\|Test']); // | is escaped.
spy.resetHistory();
// Ensure the subtest hasn't been disposed
console.log('Ensure the subtest has not been disposed');
assert(tSub.parent, 'Subtest was disposed');
// Attempt to run subtest and other test - should not work
console.log('Attempt to run subtest and other test');
assert(
await testExplorer.runner.run({
include: [tSub, tOther],
exclude: undefined,
profile: undefined
}),
'Failed to execute `go test`'
);
assert.strictEqual(spy.callCount, 0, 'expected no calls to goTest');
}).timeout(10000);
});
});