blob: ca76d4d4ac01abf2d76d77b4adca3662e6084f1d [file] [log] [blame]
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable eqeqeq */
/* eslint-disable node/no-unpublished-import */
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------*/
import assert from 'assert';
import * as fs from 'fs-extra';
import * as path from 'path';
import * as sinon from 'sinon';
import * as vscode from 'vscode';
import { getGoConfig, getGoplsConfig } from '../../src/config';
import { FilePatch, getEdits, getEditsFromUnifiedDiffStr } from '../../src/diffUtils';
import { check } from '../../src/goCheck';
import {
generateTestCurrentFile,
generateTestCurrentFunction,
generateTestCurrentPackage
} from '../../src/goGenerateTests';
import { updateGoVarsFromConfig } from '../../src/goInstallTools';
import { buildLanguageServerConfig } from '../../src/language/goLanguageServer';
import { goLint } from '../../src/goLint';
import { goPlay } from '../../src/goPlayground';
import { testCurrentFile } from '../../src/commands';
import {
getBinPath,
getCurrentGoPath,
getGoVersion,
getImportPath,
GoVersion,
handleDiagnosticErrors,
ICheckResult
} from '../../src/util';
import cp = require('child_process');
import os = require('os');
import { MockExtensionContext } from '../mocks/MockContext';
const testAll = (isModuleMode: boolean) => {
const dummyCancellationSource = new vscode.CancellationTokenSource();
// suiteSetup will initialize the following vars.
let gopath: string;
let repoPath: string;
let fixturePath: string;
let fixtureSourcePath: string;
let generateTestsSourcePath: string;
let generateFunctionTestSourcePath: string;
let generatePackageTestSourcePath: string;
let previousEnv: any;
let goVersion: GoVersion;
suiteSetup(async () => {
previousEnv = Object.assign({}, process.env);
process.env.GO111MODULE = isModuleMode ? 'on' : 'off';
await updateGoVarsFromConfig({});
gopath = getCurrentGoPath();
if (!gopath) {
assert.ok(gopath, 'Cannot run tests if GOPATH is not set as environment variable');
return;
}
goVersion = await getGoVersion();
console.log(`Using GOPATH: ${gopath}`);
repoPath = isModuleMode ? fs.mkdtempSync(path.join(os.tmpdir(), 'legacy')) : path.join(gopath, 'src', 'test');
fixturePath = path.join(repoPath, 'testfixture');
fixtureSourcePath = path.join(__dirname, '..', '..', '..', 'test', 'testdata');
generateTestsSourcePath = path.join(repoPath, 'generatetests');
generateFunctionTestSourcePath = path.join(repoPath, 'generatefunctiontest');
generatePackageTestSourcePath = path.join(repoPath, 'generatePackagetest');
fs.removeSync(repoPath);
fs.copySync(fixtureSourcePath, fixturePath, {
recursive: true
// TODO(hyangah): should we enable GOPATH mode
});
fs.copySync(
path.join(fixtureSourcePath, 'generatetests', 'generatetests.go'),
path.join(generateTestsSourcePath, 'generatetests.go')
);
fs.copySync(
path.join(fixtureSourcePath, 'generatetests', 'generatetests.go'),
path.join(generateFunctionTestSourcePath, 'generatetests.go')
);
fs.copySync(
path.join(fixtureSourcePath, 'generatetests', 'generatetests.go'),
path.join(generatePackageTestSourcePath, 'generatetests.go')
);
fs.copySync(
path.join(fixtureSourcePath, 'diffTestData', 'file1.go'),
path.join(fixturePath, 'diffTest1Data', 'file1.go')
);
fs.copySync(
path.join(fixtureSourcePath, 'diffTestData', 'file2.go'),
path.join(fixturePath, 'diffTest1Data', 'file2.go')
);
fs.copySync(
path.join(fixtureSourcePath, 'diffTestData', 'file1.go'),
path.join(fixturePath, 'diffTest2Data', 'file1.go')
);
fs.copySync(
path.join(fixtureSourcePath, 'diffTestData', 'file2.go'),
path.join(fixturePath, 'diffTest2Data', 'file2.go')
);
});
suiteTeardown(() => {
fs.removeSync(repoPath);
process.env = previousEnv;
});
teardown(() => {
sinon.restore();
});
test('Linting - concurrent process cancelation', async () => {
const util = require('../../src/util');
const processutil = require('../../src/utils/processUtils');
sinon.spy(util, 'runTool');
sinon.spy(processutil, 'killProcessTree');
const config = Object.create(getGoConfig(), {
vetOnSave: { value: 'package' },
vetFlags: { value: ['-all'] },
buildOnSave: { value: 'package' },
lintOnSave: { value: 'package' },
// simulate a long running lint process by sleeping for a couple seconds
lintTool: { value: process.platform !== 'win32' ? 'sleep' : 'timeout' },
lintFlags: { value: process.platform !== 'win32' ? ['2'] : ['/t', '2'] }
});
const goplsConfig = Object.create(getGoplsConfig(), {});
const results = await Promise.all([
goLint(vscode.Uri.file(path.join(fixturePath, 'linterTest', 'linter_1.go')), config, goplsConfig),
goLint(vscode.Uri.file(path.join(fixturePath, 'linterTest', 'linter_2.go')), config, goplsConfig)
]);
assert.equal(util.runTool.callCount, 2, 'should have launched 2 lint jobs');
assert.equal(
processutil.killProcessTree.callCount,
1,
'should have killed 1 lint job before launching the next'
);
});
test('Linting - lint errors with multiple open files', async () => {
try {
// handleDiagnosticErrors may adjust the lint errors' ranges to make the error more visible.
// This adjustment applies only to the text documents known to vscode. This test checks
// the adjustment is made consistently across multiple open text documents.
const file1 = await vscode.workspace.openTextDocument(
vscode.Uri.file(path.join(fixturePath, 'linterTest', 'linter_1.go'))
);
const file2 = await vscode.workspace.openTextDocument(
vscode.Uri.file(path.join(fixturePath, 'linterTest', 'linter_2.go'))
);
console.log('start linting');
const warnings = await goLint(
file2.uri,
Object.create(getGoConfig(), {
lintTool: { value: 'staticcheck' },
lintFlags: { value: ['-checks', 'all,-ST1000,-ST1016'] }
// staticcheck skips debatable checks such as ST1003 by default,
// but this test depends on ST1003 (MixedCaps package name) presented in both files
// in the same package. So, enable that.
}),
Object.create(getGoplsConfig(), {}),
'package'
);
const diagnosticCollection = vscode.languages.createDiagnosticCollection('linttest');
handleDiagnosticErrors({}, file2, warnings, diagnosticCollection);
// The first diagnostic message for each file should be about the use of MixedCaps in package name.
// Both files belong to the same package name, and we want them to be identical.
const file1Diagnostics = diagnosticCollection.get(file1.uri);
const file2Diagnostics = diagnosticCollection.get(file2.uri);
assert(file1Diagnostics);
assert(file2Diagnostics);
assert(file1Diagnostics.length > 0);
assert(file2Diagnostics.length > 0);
assert.deepStrictEqual(file1Diagnostics[0], file2Diagnostics[0]);
} catch (e) {
assert.fail(`failed to lint: ${e}`);
}
});
test('Error checking', async () => {
const config = Object.create(getGoConfig(), {
vetOnSave: { value: 'package' },
vetFlags: { value: ['-all'] },
lintOnSave: { value: 'package' },
lintTool: { value: 'staticcheck' },
lintFlags: { value: [] },
buildOnSave: { value: 'package' }
});
const expectedLintErrors = [
// Unlike golint, staticcheck will report only those compile errors,
// but not lint errors when the program is broken.
{
line: 11,
severity: 'warning',
// From v0.4.0, staticcheck uses 'undefined:' as the prefix of this error.
msg: /(?:undeclared name|undefined): prin \(compile\)/
}
];
// If a user has enabled diagnostics via a language server,
// then we disable running build or vet to avoid duplicate errors and warnings.
const lspConfig = await buildLanguageServerConfig(getGoConfig());
const expectedBuildVetErrors = lspConfig.enabled
? []
: [{ line: 11, severity: 'error', msg: 'undefined: prin' }];
// `check` itself doesn't run deDupeDiagnostics, so we expect all vet/lint errors.
const expected = [...expectedLintErrors, ...expectedBuildVetErrors];
const diagnostics = await check(
{
buildDiagnosticCollection: vscode.languages.createDiagnosticCollection('buildtest'),
lintDiagnosticCollection: vscode.languages.createDiagnosticCollection('linttest'),
vetDiagnosticCollection: vscode.languages.createDiagnosticCollection('vettest')
},
vscode.Uri.file(path.join(fixturePath, 'errorsTest', 'errors.go')),
config
);
const sortedDiagnostics = ([] as ICheckResult[]).concat
.apply(
[],
diagnostics.map((x) => x.errors)
)
.sort((a: any, b: any) => a.line - b.line);
assert.equal(sortedDiagnostics.length > 0, true, 'Failed to get linter results');
const matchCount = expected.filter((expectedItem) => {
return sortedDiagnostics.some((diag: any) => {
return (
expectedItem.line === diag.line &&
expectedItem.severity === diag.severity &&
diag.msg.match(expectedItem.msg)
);
});
});
assert.equal(
matchCount.length >= expected.length,
true,
`Failed to match expected errors \n${JSON.stringify(sortedDiagnostics)} \n VS\n ${JSON.stringify(expected)}`
);
});
test('Test Generate unit tests skeleton for file', async () => {
const gotestsPath = getBinPath('gotests');
const uri = vscode.Uri.file(path.join(generateTestsSourcePath, 'generatetests.go'));
const document = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(document);
const ctx = new MockExtensionContext() as any;
await generateTestCurrentFile(ctx, {})();
const testFileGenerated = fs.existsSync(path.join(generateTestsSourcePath, 'generatetests_test.go'));
assert.equal(testFileGenerated, true, 'Test file not generated.');
});
test('Test Generate unit tests skeleton for a function', async () => {
const gotestsPath = getBinPath('gotests');
const uri = vscode.Uri.file(path.join(generateFunctionTestSourcePath, 'generatetests.go'));
const document = await vscode.workspace.openTextDocument(uri);
const editor = await vscode.window.showTextDocument(document);
editor.selection = new vscode.Selection(5, 0, 6, 0);
const ctx = new MockExtensionContext() as any;
await generateTestCurrentFunction(ctx, {})();
const testFileGenerated = fs.existsSync(path.join(generateTestsSourcePath, 'generatetests_test.go'));
assert.equal(testFileGenerated, true, 'Test file not generated.');
});
test('Test Generate unit tests skeleton for package', async () => {
const gotestsPath = getBinPath('gotests');
const uri = vscode.Uri.file(path.join(generatePackageTestSourcePath, 'generatetests.go'));
const document = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(document);
const ctx = new MockExtensionContext() as any;
await generateTestCurrentPackage(ctx, {})();
const testFileGenerated = fs.existsSync(path.join(generateTestsSourcePath, 'generatetests_test.go'));
assert.equal(testFileGenerated, true, 'Test file not generated.');
});
test('Test diffUtils.getEditsFromUnifiedDiffStr', async function () {
// Run this test only in module mode.
if (!isModuleMode) {
this.skip();
}
if (process.platform === 'win32') {
// This test requires diff tool that's not available on windows
this.skip();
}
const file1path = path.join(fixturePath, 'diffTest1Data', 'file1.go');
const file2path = path.join(fixturePath, 'diffTest1Data', 'file2.go');
const file1uri = vscode.Uri.file(file1path);
const file2contents = fs.readFileSync(file2path, 'utf8');
const fileEditPatches: any | FilePatch[] = await new Promise((resolve) => {
cp.exec(`diff -u ${file1path} ${file2path}`, (err, stdout, stderr) => {
const filePatches: FilePatch[] = getEditsFromUnifiedDiffStr(stdout);
if (!filePatches || filePatches.length !== 1) {
assert.fail(null, null, 'Failed to get patches for the test file', '');
}
if (!filePatches[0].fileName) {
assert.fail(null, null, 'Failed to parse the file path from the diff output', '');
}
if (!filePatches[0].edits) {
assert.fail(null, null, 'Failed to parse edits from the diff output', '');
}
resolve(filePatches);
});
});
const textDocument = await vscode.workspace.openTextDocument(file1uri);
const editor = await vscode.window.showTextDocument(textDocument);
await editor.edit((editBuilder) => {
fileEditPatches[0].edits.forEach((edit: any) => {
edit.applyUsingTextEditorEdit(editBuilder);
});
});
assert.equal(editor.document.getText(), file2contents);
});
test('Test diffUtils.getEdits', async function () {
if (!isModuleMode) {
this.skip();
} // Run this test only in module mode.
const file1path = path.join(fixturePath, 'diffTest2Data', 'file1.go');
const file2path = path.join(fixturePath, 'diffTest2Data', 'file2.go');
const file1uri = vscode.Uri.file(file1path);
const file1contents = fs.readFileSync(file1path, 'utf8');
const file2contents = fs.readFileSync(file2path, 'utf8');
const fileEdits = getEdits(file1path, file1contents, file2contents);
if (!fileEdits) {
assert.fail(null, null, 'Failed to get patches for the test file', '');
}
if (!fileEdits.fileName) {
assert.fail(null, null, 'Failed to parse the file path from the diff output', '');
}
if (!fileEdits.edits) {
assert.fail(null, null, 'Failed to parse edits from the diff output', '');
}
const textDocument = await vscode.workspace.openTextDocument(file1uri);
const editor = await vscode.window.showTextDocument(textDocument);
await editor.edit((editBuilder) => {
fileEdits.edits.forEach((edit) => {
edit.applyUsingTextEditorEdit(editBuilder);
});
});
assert.equal(editor.document.getText(), file2contents);
});
test('Test Env Variables are passed to Tests', async () => {
const config = Object.create(getGoConfig(), {
testEnvVars: { value: { dummyEnvVar: 'dummyEnvValue', dummyNonString: 1 } }
});
const uri = vscode.Uri.file(path.join(fixturePath, 'baseTest', 'sample_test.go'));
const document = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(document);
const ctx = new MockExtensionContext() as any;
const result = await testCurrentFile(false, () => config)(ctx, {})([]);
assert.equal(result, true);
});
test('getImportPath()', () => {
const testCases: [string, string][] = [
['import "github.com/sirupsen/logrus"', 'github.com/sirupsen/logrus'],
['import "net/http"', 'net/http'],
['"github.com/sirupsen/logrus"', 'github.com/sirupsen/logrus'],
['', ''],
['func foo(bar int) (int, error) {', ''],
['// This is a comment, complete with punctuation.', '']
];
testCases.forEach((run) => {
assert.equal(run[1], getImportPath(run[0]));
});
});
test('goPlay - success run', async () => {
const goplayPath = getBinPath('goplay');
if (goplayPath === 'goplay') {
// goplay is not installed, so skip the test
return;
}
const validCode = `
package main
import (
"fmt"
)
func main() {
for i := 1; i < 4; i++ {
fmt.Printf("%v ", i)
}
fmt.Print("Go!")
}`;
const goConfig = Object.create(getGoConfig(), {
playground: { value: { run: true, openbrowser: false, share: false } }
});
await goPlay(validCode, goConfig['playground']).then(
(result) => {
assert(result.includes('1 2 3 Go!'));
},
(e) => {
assert.ifError(e);
}
);
});
test('goPlay - success run & share', async () => {
const goplayPath = getBinPath('goplay');
if (goplayPath === 'goplay') {
// goplay is not installed, so skip the test
return;
}
const validCode = `
package main
import (
"fmt"
)
func main() {
for i := 1; i < 4; i++ {
fmt.Printf("%v ", i)
}
fmt.Print("Go!")
}`;
const goConfig = Object.create(getGoConfig(), {
playground: { value: { run: true, openbrowser: false, share: true } }
});
await goPlay(validCode, goConfig['playground']).then(
(result) => {
assert(result.includes('1 2 3 Go!'));
assert(result.includes('https://play.golang.org/'));
},
(e) => {
assert.ifError(e);
}
);
});
test('goPlay - fail', async () => {
const goplayPath = getBinPath('goplay');
if (goplayPath === 'goplay') {
// goplay is not installed, so skip the test
return;
}
const invalidCode = `
package main
import (
"fmt"
)
func fantasy() {
fmt.Print("not a main package, sorry")
}`;
const goConfig = Object.create(getGoConfig(), {
playground: { value: { run: true, openbrowser: false, share: false } }
});
await goPlay(invalidCode, goConfig['playground']).then(
(result) => {
assert.ifError(result);
},
(e) => {
assert.ok(e);
}
);
});
test('Build Tags checking', async () => {
const goplsConfig = await buildLanguageServerConfig(getGoConfig());
if (goplsConfig.enabled) {
// Skip this test if gopls is enabled. Build/Vet checks this test depend on are
// disabled when the language server is enabled, and gopls is not handling tags yet.
return;
}
// Note: The following checks can't be parallelized because the underlying go build command
// runner (goBuild) will cancel any outstanding go build commands.
const checkWithTags = async (tags: string) => {
const fileUri = vscode.Uri.file(path.join(fixturePath, 'buildTags', 'hello.go'));
const defaultGoCfg = getGoConfig(fileUri);
const cfg = Object.create(defaultGoCfg, {
vetOnSave: { value: 'off' },
lintOnSave: { value: 'off' },
buildOnSave: { value: 'package' },
buildTags: { value: tags }
}) as vscode.WorkspaceConfiguration;
const diagnostics = await check({}, fileUri, cfg);
return ([] as string[]).concat(
...diagnostics.map<string[]>((d) => {
return d.errors.map((e) => e.msg) as string[];
})
);
};
const errors1 = await checkWithTags('randomtag');
assert.deepEqual(
errors1,
['undefined: fmt.Prinln'],
'check with buildtag "randomtag" failed. Unexpected errors found.'
);
// TODO(hyangah): after go1.13, -tags expects a comma-separated tag list.
// For backwards compatibility, space-separated tag lists are still recognized,
// but change to a space-separated list once we stop testing with go1.12.
const errors2 = await checkWithTags('randomtag other');
assert.deepEqual(
errors2,
['undefined: fmt.Prinln'],
'check with multiple buildtags "randomtag,other" failed. Unexpected errors found.'
);
const errors3 = await checkWithTags('');
assert.equal(
errors3.length,
1,
'check without buildtag failed. Unexpected number of errors found' + JSON.stringify(errors3)
);
const errMsg = errors3[0];
assert.ok(
errMsg.includes("can't load package: package test/testfixture/buildTags") ||
errMsg.includes('build constraints exclude all Go files'),
`check without buildtags failed. Go files not excluded. ${errMsg}`
);
});
test('Test Tags checking', async () => {
const config1 = Object.create(getGoConfig(), {
vetOnSave: { value: 'off' },
lintOnSave: { value: 'off' },
buildOnSave: { value: 'package' },
testTags: { value: null },
buildTags: { value: 'randomtag' }
});
const config2 = Object.create(getGoConfig(), {
vetOnSave: { value: 'off' },
lintOnSave: { value: 'off' },
buildOnSave: { value: 'package' },
testTags: { value: 'randomtag' }
});
const config3 = Object.create(getGoConfig(), {
vetOnSave: { value: 'off' },
lintOnSave: { value: 'off' },
buildOnSave: { value: 'package' },
testTags: { value: 'randomtag othertag' }
});
const config4 = Object.create(getGoConfig(), {
vetOnSave: { value: 'off' },
lintOnSave: { value: 'off' },
buildOnSave: { value: 'package' },
testTags: { value: '' }
});
const uri = vscode.Uri.file(path.join(fixturePath, 'testTags', 'hello_test.go'));
const document = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(document);
const ctx = new MockExtensionContext() as any;
const result1 = await testCurrentFile(false, () => config1)(ctx, {})([]);
assert.equal(result1, true);
const result2 = await testCurrentFile(false, () => config2)(ctx, {})([]);
assert.equal(result2, true);
const result3 = await testCurrentFile(false, () => config3)(ctx, {})([]);
assert.equal(result3, true);
const result4 = await testCurrentFile(false, () => config4)(ctx, {})([]);
assert.equal(result4, false);
});
};
suite('Go Extension Tests (GOPATH mode)', function () {
this.timeout(20000);
testAll(false);
});
suite('Go Extension Tests (Module mode)', function () {
this.timeout(20000);
testAll(true);
});