From 797e793ad29a51e3bd05ef2f3152f4e896fb3052 Mon Sep 17 00:00:00 2001 From: James Pogran Date: Wed, 10 Apr 2024 16:53:43 -0400 Subject: [PATCH] End to end testing for completion, references and definitions --- .gitignore | 1 + .vscode-test.mjs | 2 +- src/test/helper.ts | 129 +++++++++++++++---- src/test/integration/codeAction.test.ts | 19 ++- src/test/integration/completion.test.ts | 159 ++++++++++++++++++------ src/test/integration/definition.test.ts | 92 ++++++++++++++ src/test/integration/hover.test.ts | 37 ++++++ src/test/integration/references.test.ts | 39 ++++++ src/test/integration/symbols.test.ts | 36 +++--- test/fixtures/main.tf | 1 + 10 files changed, 426 insertions(+), 89 deletions(-) create mode 100644 src/test/integration/definition.test.ts create mode 100644 src/test/integration/hover.test.ts create mode 100644 src/test/integration/references.test.ts diff --git a/.gitignore b/.gitignore index c1e430d582..f134e75344 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ npm-debug.log lsp/ testFixture/.terraform.lock.hcl testFixture/.vscode +test/fixtures/.vscode .terraform/ .vscode-test-web .wdio-vscode-service diff --git a/.vscode-test.mjs b/.vscode-test.mjs index 4e29feffd3..d927409255 100644 --- a/.vscode-test.mjs +++ b/.vscode-test.mjs @@ -9,7 +9,7 @@ import { defineConfig } from '@vscode/test-cli'; const config = defineConfig({ version: process.env['VSCODE_VERSION'] ?? 'stable', - workspaceFolder: process.env['VSCODE_WORKSPACE_FOLDER'] ?? './testFixture', + workspaceFolder: process.env['VSCODE_WORKSPACE_FOLDER'] ?? './test/fixtures', launchArgs: ['--disable-extensions', '--disable-workspace-trust'], files: 'out/test/**/*.test.js', mocha: { diff --git a/src/test/helper.ts b/src/test/helper.ts index 6024209e98..c2f66681be 100644 --- a/src/test/helper.ts +++ b/src/test/helper.ts @@ -5,38 +5,125 @@ import * as vscode from 'vscode'; import * as path from 'path'; - -export let doc: vscode.TextDocument; -export let editor: vscode.TextEditor; -export let documentEol: string; -export let platformEol: string; +import * as assert from 'assert'; export async function open(docUri: vscode.Uri): Promise { try { - doc = await vscode.workspace.openTextDocument(docUri); - editor = await vscode.window.showTextDocument(doc); + const doc = await vscode.workspace.openTextDocument(docUri); + await vscode.window.showTextDocument(doc); } catch (e) { console.error(e); throw e; } } -export function getExtensionId(): string { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const pjson = require('../../package.json'); - return `${pjson.publisher}.${pjson.name}`; +export const getDocUri = (p: string): vscode.Uri => { + const documentPath = path.resolve(__dirname, '../../test/fixtures', p); + return vscode.Uri.file(documentPath); +}; + +export async function testCompletion( + docUri: vscode.Uri, + position: vscode.Position, + expectedCompletionList: vscode.CompletionList, +) { + const actualCompletionList = (await vscode.commands.executeCommand( + 'vscode.executeCompletionItemProvider', + docUri, + position, + )) as vscode.CompletionList; + + assert.equal(actualCompletionList.items.length, expectedCompletionList.items.length); + expectedCompletionList.items.forEach((expectedItem, i) => { + const actualItem = actualCompletionList.items[i]; + assert.deepStrictEqual(actualItem.label, expectedItem.label); + assert.deepStrictEqual(actualItem.kind, expectedItem.kind); + }); } -export const testFolderPath = path.resolve(__dirname, '..', '..', 'testFixture'); +export async function testHover(docUri: vscode.Uri, position: vscode.Position, expectedCompletionList: vscode.Hover[]) { + const actualhover = (await vscode.commands.executeCommand( + 'vscode.executeHoverProvider', + docUri, + position, + )) as vscode.Hover[]; -export const getDocPath = (p: string): string => { - return path.resolve(__dirname, '../../testFixture', p); -}; -export const getDocUri = (p: string): vscode.Uri => { - return vscode.Uri.file(getDocPath(p)); -}; + assert.equal(actualhover.length, expectedCompletionList.length); + expectedCompletionList.forEach((expectedItem, i) => { + const actualItem = actualhover[i]; + + assert.deepStrictEqual(actualItem.contents, expectedItem.contents); + }); +} + +export async function testDefinitions( + docUri: vscode.Uri, + position: vscode.Position, + expectedDefinitions: vscode.Location[], +) { + const actualDefinitions = await vscode.commands.executeCommand( + 'vscode.executeDefinitionProvider', + docUri, + position, + ); + + assert.equal(actualDefinitions.length, expectedDefinitions.length); + expectedDefinitions.forEach((expectedItem, i) => { + const actualItem = actualDefinitions[i]; + if (actualItem instanceof vscode.Location) { + assert.deepStrictEqual(actualItem.uri.path, expectedItem.uri.path); + assert.deepStrictEqual(actualItem.range.start, expectedItem.range.start); + assert.deepStrictEqual(actualItem.range.end, expectedItem.range.end); + return; + } else { + // } else if (actualItem instanceof vscode.LocationLink) { + assert.deepStrictEqual(actualItem.targetUri.path, expectedItem.uri.path); + assert.deepStrictEqual(actualItem.targetRange.start, expectedItem.range.start); + assert.deepStrictEqual(actualItem.targetRange.end, expectedItem.range.end); + } + }); +} + +export async function testReferences( + docUri: vscode.Uri, + position: vscode.Position, + expectedDefinitions: vscode.Location[], +) { + const actualDefinitions = await vscode.commands.executeCommand( + 'vscode.executeReferenceProvider', + docUri, + position, + ); + + assert.equal(actualDefinitions.length, expectedDefinitions.length); + expectedDefinitions.forEach((expectedItem, i) => { + const actualItem = actualDefinitions[i]; + assert.deepStrictEqual(actualItem.uri.path, expectedItem.uri.path); + assert.deepStrictEqual(actualItem.range.start, expectedItem.range.start); + assert.deepStrictEqual(actualItem.range.end, expectedItem.range.end); + }); +} + +export async function testSymbols(docUri: vscode.Uri, symbolNames: string[]) { + const symbols = (await vscode.commands.executeCommand( + 'vscode.executeDocumentSymbolProvider', + docUri, + )) as vscode.SymbolInformation[]; + + assert.strictEqual(symbols.length, symbolNames.length); + symbols.forEach((symbol, i) => { + assert.strictEqual(symbol.name, symbolNames[i]); + }); +} + +export async function activateExtension() { + const ext = vscode.extensions.getExtension('hashicorp.terraform'); + if (!ext?.isActive) { + await ext?.activate(); + sleep(1000); + } +} -export async function setTestContent(content: string): Promise { - const all = new vscode.Range(doc.positionAt(0), doc.positionAt(doc.getText().length)); - return editor.edit((eb) => eb.replace(all, content)); +export async function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/src/test/integration/codeAction.test.ts b/src/test/integration/codeAction.test.ts index 235e5d4f17..94ea04bf5d 100644 --- a/src/test/integration/codeAction.test.ts +++ b/src/test/integration/codeAction.test.ts @@ -6,13 +6,25 @@ import * as vscode from 'vscode'; import * as assert from 'assert'; import { expect } from 'chai'; -import { getDocUri, open } from '../helper'; +import { activateExtension, getDocUri, open } from '../helper'; + +suite('code actions', function suite() { + const docUri = getDocUri('actions.tf'); + + this.beforeAll(async () => { + await open(docUri); + await activateExtension(); + }); -suite('code actions', () => { teardown(async () => { await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); + test('language is registered', async () => { + const doc = await vscode.workspace.openTextDocument(docUri); + assert.equal(doc.languageId, 'terraform', 'document language should be `terraform`'); + }); + test('supported actions', async () => { await vscode.workspace .getConfiguration('terraform') @@ -22,9 +34,6 @@ suite('code actions', () => { // eslint-disable-next-line @typescript-eslint/naming-convention .update('codeActionsOnSave', { 'source.formatAll.terraform': true }, vscode.ConfigurationTarget.Workspace); - const docUri = getDocUri('actions.tf'); - await open(docUri); - const supported = [ new vscode.CodeAction('Format Document', vscode.CodeActionKind.Source.append('formatAll').append('terraform')), ]; diff --git a/src/test/integration/completion.test.ts b/src/test/integration/completion.test.ts index 6d4bf90594..e4bfb40715 100644 --- a/src/test/integration/completion.test.ts +++ b/src/test/integration/completion.test.ts @@ -4,17 +4,37 @@ */ import * as vscode from 'vscode'; -import * as assert from 'assert'; -import { expect } from 'chai'; -import { getDocUri, open } from '../helper'; +import { assert } from 'chai'; +import { activateExtension, getDocUri, open, testCompletion } from '../helper'; + +const snippets = [ + new vscode.CompletionItem({ label: 'fore', description: 'For Each' }, vscode.CompletionItemKind.Snippet), + new vscode.CompletionItem({ label: 'module', description: 'Module' }, vscode.CompletionItemKind.Snippet), + new vscode.CompletionItem({ label: 'output', description: 'Output' }, vscode.CompletionItemKind.Snippet), + new vscode.CompletionItem({ label: 'provisioner', description: 'Provisioner' }, vscode.CompletionItemKind.Snippet), + new vscode.CompletionItem({ label: 'vare', description: 'Empty variable' }, vscode.CompletionItemKind.Snippet), + new vscode.CompletionItem({ label: 'varm', description: 'Map variable' }, vscode.CompletionItemKind.Snippet), +]; + +suite('Language Server Completion', function suite() { + const docUri = getDocUri('actions.tf'); + + this.beforeAll(async () => { + await open(docUri); + await activateExtension(); + }); -suite('completion', () => { teardown(async () => { await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); + test('language is registered', async () => { + const doc = await vscode.workspace.openTextDocument(docUri); + assert.equal(doc.languageId, 'terraform', 'document language should be `terraform`'); + }); + test('simple completion', async () => { - const wanted = new vscode.CompletionList([ + const expected = [ new vscode.CompletionItem('check', vscode.CompletionItemKind.Class), new vscode.CompletionItem('data', vscode.CompletionItemKind.Class), new vscode.CompletionItem('import', vscode.CompletionItemKind.Class), @@ -23,45 +43,104 @@ suite('completion', () => { new vscode.CompletionItem('moved', vscode.CompletionItemKind.Class), new vscode.CompletionItem('output', vscode.CompletionItemKind.Class), new vscode.CompletionItem('provider', vscode.CompletionItemKind.Class), + new vscode.CompletionItem('removed', vscode.CompletionItemKind.Class), new vscode.CompletionItem('resource', vscode.CompletionItemKind.Class), new vscode.CompletionItem('terraform', vscode.CompletionItemKind.Class), new vscode.CompletionItem('variable', vscode.CompletionItemKind.Class), - new vscode.CompletionItem({ label: 'fore', description: 'For Each' }, vscode.CompletionItemKind.Snippet), - new vscode.CompletionItem({ label: 'module', description: 'Module' }, vscode.CompletionItemKind.Snippet), - new vscode.CompletionItem({ label: 'output', description: 'Output' }, vscode.CompletionItemKind.Snippet), - new vscode.CompletionItem( - { label: 'provisioner', description: 'Provisioner' }, - vscode.CompletionItemKind.Snippet, - ), - new vscode.CompletionItem({ label: 'vare', description: 'Empty variable' }, vscode.CompletionItemKind.Snippet), - new vscode.CompletionItem({ label: 'varm', description: 'Map variable' }, vscode.CompletionItemKind.Snippet), - ]); - - const docUri = getDocUri('actions.tf'); + ]; + // expected.push(...snippets); + await testCompletion(docUri, new vscode.Position(0, 0), { + items: expected, + }); + }); +}); + +suite('Module Completion', function suite() { + const docUri = getDocUri('main.tf'); + + this.beforeAll(async () => { await open(docUri); + await activateExtension(); + }); + + teardown(async () => { + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + test('language is registered', async () => { + const doc = await vscode.workspace.openTextDocument(docUri); + assert.equal(doc.languageId, 'terraform', 'document language should be `terraform`'); + }); + + // Completion for inputs of a local module + test('inputs of a local module', async () => { + const expected = [ + new vscode.CompletionItem('count', vscode.CompletionItemKind.Property), + new vscode.CompletionItem('depends_on', vscode.CompletionItemKind.Property), + new vscode.CompletionItem('for_each', vscode.CompletionItemKind.Property), + new vscode.CompletionItem('machine_type', vscode.CompletionItemKind.Property), + new vscode.CompletionItem('providers', vscode.CompletionItemKind.Property), + new vscode.CompletionItem('version', vscode.CompletionItemKind.Property), + ]; + expected.push(...snippets); + + await testCompletion(docUri, new vscode.Position(21, 0), { + items: expected, + }); + }); + + // Completion for a local module sources (prefix ./) + test('local module sources', async () => { + const expected = [ + new vscode.CompletionItem('"./ai"', vscode.CompletionItemKind.Text), + new vscode.CompletionItem('"./compute"', vscode.CompletionItemKind.Text), + new vscode.CompletionItem('"cloudposse/label/null"', vscode.CompletionItemKind.Text), + new vscode.CompletionItem('"terraform-aws-modules/eks/aws"', vscode.CompletionItemKind.Text), + new vscode.CompletionItem('"terraform-aws-modules/iam/aws"', vscode.CompletionItemKind.Text), + new vscode.CompletionItem('"terraform-aws-modules/kms/aws"', vscode.CompletionItemKind.Text), + new vscode.CompletionItem('"terraform-aws-modules/lambda/aws"', vscode.CompletionItemKind.Text), + new vscode.CompletionItem('"terraform-aws-modules/rds/aws"', vscode.CompletionItemKind.Text), + new vscode.CompletionItem('"terraform-aws-modules/s3-bucket/aws"', vscode.CompletionItemKind.Text), + new vscode.CompletionItem('"terraform-aws-modules/security-group/aws"', vscode.CompletionItemKind.Text), + new vscode.CompletionItem('"terraform-aws-modules/vpc/aws"', vscode.CompletionItemKind.Text), + new vscode.CompletionItem('"terraform-google-modules/project-factory/google"', vscode.CompletionItemKind.Text), + ]; + expected.push(...snippets); + + // module "compute" { + // source = "./compute" + await testCompletion(docUri, new vscode.Position(18, 11), { + items: expected, + }); + }); +}); + +suite('TFVars Completion', function suite() { + const docUri = getDocUri('terraform.tfvars'); + + this.beforeAll(async () => { + await open(docUri); + await activateExtension(); + }); + + teardown(async () => { + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + test('language is registered', async () => { + const doc = await vscode.workspace.openTextDocument(docUri); + assert.equal(doc.languageId, 'terraform-vars', 'document language should be `terraform-vars`'); + }); - const list = await vscode.commands.executeCommand( - 'vscode.executeCompletionItemProvider', - docUri, - new vscode.Position(0, 0), - ); - - assert.ok(list); - expect(list).not.to.be.undefined; - expect(list.items).not.to.be.undefined; - expect(list.items.length).to.be.greaterThanOrEqual(1); - - for (let index = 0; index < list.items.length; index++) { - const element: vscode.CompletionItem = list.items[index]; - assert.ok(element); - expect(element).not.to.be.undefined; - - const w = wanted.items[index]; - assert.ok(w); - expect(w).not.to.be.undefined; - assert.strictEqual(element.kind, w.kind); - // this can either be a string or a vscode.CompletionItemLabel, so use deep - assert.deepStrictEqual(element.label, w.label); - } + test('simple variable completion', async () => { + const expected = [ + new vscode.CompletionItem('credentials_file', vscode.CompletionItemKind.Property), + new vscode.CompletionItem('project', vscode.CompletionItemKind.Property), + new vscode.CompletionItem('region', vscode.CompletionItemKind.Property), + ]; + expected.push(...snippets); + await testCompletion(docUri, new vscode.Position(1, 0), { + items: expected, + }); }); }); diff --git a/src/test/integration/definition.test.ts b/src/test/integration/definition.test.ts new file mode 100644 index 0000000000..2b639d91bc --- /dev/null +++ b/src/test/integration/definition.test.ts @@ -0,0 +1,92 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import * as vscode from 'vscode'; +import * as assert from 'assert'; +import { activateExtension, getDocUri, open, testDefinitions } from '../helper'; + +suite('go to module definition', function suite() { + const docUri = getDocUri('main.tf'); + + this.beforeAll(async () => { + await open(docUri); + await activateExtension(); + }); + + teardown(async () => { + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + test('language is registered', async () => { + const doc = await vscode.workspace.openTextDocument(docUri); + assert.equal(doc.languageId, 'terraform', 'document language should be `terraform`'); + }); + + test('returns definition for module source', async () => { + const location = new vscode.Location( + getDocUri('compute/main.tf'), + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), + ); + + // module "compute" { + // source = "./compute" + await testDefinitions(docUri, new vscode.Position(18, 11), [location]); + }); + + test('returns definition for module attribute', async () => { + const location = new vscode.Location( + getDocUri('compute/variables.tf'), + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(3, 1)), + ); + + // module "compute" { + // source = "./compute" + // instance_name = "terraform-machine" + await testDefinitions(docUri, new vscode.Position(20, 2), [location]); + }); + + test('returns definition for variable', async () => { + // provider "google" { + // credentials = file(var.credentials_file) + // project = var.project + // region = var.region + // zone = var.zone + const location = new vscode.Location( + getDocUri('variables.tf'), + new vscode.Range(new vscode.Position(4, 0), new vscode.Position(6, 1)), + ); + + await testDefinitions(docUri, new vscode.Position(10, 36), [location]); + }); +}); + +suite('go to variable definition', function suite() { + const docUri = getDocUri('terraform.tfvars'); + + this.beforeAll(async () => { + await open(docUri); + await activateExtension(); + }); + + teardown(async () => { + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + test('language is registered', async () => { + const doc = await vscode.workspace.openTextDocument(docUri); + assert.equal(doc.languageId, 'terraform-vars', 'document language should be `terraform-vars`'); + }); + + test('returns definition for module source', async () => { + const location = new vscode.Location( + getDocUri('variables.tf'), + new vscode.Range(new vscode.Position(12, 0), new vscode.Position(14, 1)), + ); + + // module "compute" { + // source = "./compute" + await testDefinitions(docUri, new vscode.Position(0, 1), [location]); + }); +}); diff --git a/src/test/integration/hover.test.ts b/src/test/integration/hover.test.ts new file mode 100644 index 0000000000..4b20a77218 --- /dev/null +++ b/src/test/integration/hover.test.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import * as vscode from 'vscode'; +import * as assert from 'assert'; +import { activateExtension, getDocUri, open, testHover } from '../helper'; + +suite('hover', function suite() { + const docUri = getDocUri('main.tf'); + + this.beforeAll(async () => { + await open(docUri); + await activateExtension(); + }); + + teardown(async () => { + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + test('language is registered', async () => { + const doc = await vscode.workspace.openTextDocument(docUri); + assert.equal(doc.languageId, 'terraform', 'document language should be `terraform`'); + }); + + test('returns definition for module source', async () => { + await testHover(docUri, new vscode.Position(0, 1), [ + new vscode.Hover( + new vscode.MarkdownString( + '**terraform** _Block_\n\nTerraform block used to configure some high-level behaviors of Terraform', + ), + new vscode.Range(new vscode.Position(14, 12), new vscode.Position(14, 20)), + ), + ]); + }); +}); diff --git a/src/test/integration/references.test.ts b/src/test/integration/references.test.ts new file mode 100644 index 0000000000..ad4db2fa0e --- /dev/null +++ b/src/test/integration/references.test.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import * as vscode from 'vscode'; +import * as assert from 'assert'; +import { activateExtension, getDocUri, open, testReferences } from '../helper'; + +suite('references', function suite() { + const docUri = getDocUri('variables.tf'); + + this.beforeAll(async () => { + await open(docUri); + await activateExtension(); + }); + + teardown(async () => { + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + test('language is registered', async () => { + const doc = await vscode.workspace.openTextDocument(docUri); + assert.equal(doc.languageId, 'terraform', 'document language should be `terraform`'); + }); + + test('returns definition for module source', async () => { + await testReferences(docUri, new vscode.Position(12, 10), [ + new vscode.Location( + getDocUri('main.tf'), + new vscode.Range(new vscode.Position(14, 12), new vscode.Position(14, 20)), + ), + new vscode.Location( + getDocUri('terraform.tfvars'), + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 4)), + ), + ]); + }); +}); diff --git a/src/test/integration/symbols.test.ts b/src/test/integration/symbols.test.ts index ed5356ce2e..912dff5aaf 100644 --- a/src/test/integration/symbols.test.ts +++ b/src/test/integration/symbols.test.ts @@ -5,34 +5,26 @@ import * as vscode from 'vscode'; import * as assert from 'assert'; -import { expect } from 'chai'; -import { getDocUri, open } from '../helper'; +import { activateExtension, getDocUri, open, testSymbols } from '../helper'; + +suite('document symbols', function suite() { + const docUri = getDocUri('sample.tf'); + + this.beforeAll(async () => { + await open(docUri); + await activateExtension(); + }); -suite('document symbols', () => { teardown(async () => { - return await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); - const docUri = getDocUri('sample.tf'); + test('language is registered', async () => { + const doc = await vscode.workspace.openTextDocument(docUri); + assert.equal(doc.languageId, 'terraform', 'document language should be `terraform`'); + }); test('returns symbols', async () => { await testSymbols(docUri, ['provider "vault"', 'resource "vault_auth_backend" "b"', 'module "local"']); }); }); - -async function testSymbols(docUri: vscode.Uri, symbolNames: string[]) { - await open(docUri); - // Executing the command `vscode.executeDocumentSymbolProvider` to simulate requesting doc symbols - const symbols = (await vscode.commands.executeCommand( - 'vscode.executeDocumentSymbolProvider', - docUri, - )) as vscode.SymbolInformation[]; - - assert.ok(symbols); - expect(symbols).not.to.be.undefined; - - assert.strictEqual(symbols.length, symbolNames.length); - symbols.forEach((symbol, i) => { - assert.strictEqual(symbol.name, symbolNames[i]); - }); -} diff --git a/test/fixtures/main.tf b/test/fixtures/main.tf index 34fcab64cb..f50b96e7b6 100644 --- a/test/fixtures/main.tf +++ b/test/fixtures/main.tf @@ -19,4 +19,5 @@ module "compute" { source = "./compute" instance_name = "terraform-machine" + }