| /*--------------------------------------------------------- | |
| * 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 } from '../../src/config'; | |
| import { FilePatch, getEdits, getEditsFromUnifiedDiffStr } from '../../src/diffUtils'; | |
| import { check } from '../../src/diagnostics/goCheck'; | |
| import { | |
| generateTestCurrentFile, | |
| generateTestCurrentFunction, | |
| generateTestCurrentPackage | |
| } from '../../src/goGenerateTests'; | |
| import { updateGoVarsFromConfig } from '../../src/goInstallTools'; | |
| import { buildLanguageServerConfig } from '../../src/language/goLanguageServer'; | |
| import { goPlay } from '../../src/goPlayground'; | |
| import { testCurrentFile } from '../../src/commands'; | |
| import { getBinPath, getCurrentGoPath, getImportPath } from '../../src/util'; | |
| import { ICheckResult } from '../../src/diagnostics/diagnostics'; | |
| import cp = require('child_process'); | |
| import os = require('os'); | |
| import { MockExtensionContext } from '../mocks/MockContext'; | |
| import { MockWorkspaceConfiguration } from './mocks/configuration'; | |
| const testAll = (isModuleMode: boolean) => { | |
| // 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; | |
| 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; | |
| } | |
| 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('Error checking', async () => { | |
| const config = new MockWorkspaceConfiguration( | |
| getGoConfig(), | |
| new Map<string, any>([ | |
| ['vetOnSave', 'package'], | |
| ['vetFlags', ['-all']], | |
| ['lintOnSave', 'package'], | |
| ['lintTool', 'staticcheck'], | |
| ['lintFlags', []], | |
| ['buildOnSave', '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.strictEqual(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.strictEqual( | |
| 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 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.strictEqual(testFileGenerated, true, 'Test file not generated.'); | |
| }); | |
| test('Test Generate unit tests skeleton for a function', async () => { | |
| 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.strictEqual(testFileGenerated, true, 'Test file not generated.'); | |
| }); | |
| test('Test Generate unit tests skeleton for package', async () => { | |
| 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.strictEqual(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) => { | |
| 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.strictEqual(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.strictEqual(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.strictEqual(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.strictEqual(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.strictEqual( | |
| 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.strictEqual(result1, true); | |
| const result2 = await testCurrentFile(false, () => config2)(ctx, {})([]); | |
| assert.strictEqual(result2, true); | |
| const result3 = await testCurrentFile(false, () => config3)(ctx, {})([]); | |
| assert.strictEqual(result3, true); | |
| const result4 = await testCurrentFile(false, () => config4)(ctx, {})([]); | |
| assert.strictEqual(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); | |
| }); |