This commit is contained in:
Heng Lu 2022-03-14 20:18:07 +08:00 коммит произвёл Heng Lu
Родитель 10cdbcaf73
Коммит d02f7c37d0
23 изменённых файлов: 841 добавлений и 8980 удалений

69
.github/workflows/publish.yml поставляемый
Просмотреть файл

@ -1,69 +0,0 @@
name: Publish release
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*'
jobs:
build:
name: Package
strategy:
matrix:
include:
- vsce_target: win32-x64
ls_target: windows_amd64
npm_config_arch: x64
- vsce_target: win32-ia32
ls_target: windows_386
npm_config_arch: ia32
- vsce_target: win32-arm64
ls_target: windows_arm64
npm_config_arch: arm
- vsce_target: linux-x64
ls_target: linux_amd64
npm_config_arch: x64
- vsce_target: linux-arm64
ls_target: linux_arm64
npm_config_arch: arm64
- vsce_target: linux-armhf
ls_target: linux_arm
npm_config_arch: arm
- vsce_target: darwin-x64
ls_target: darwin_amd64
npm_config_arch: x64
- vsce_target: darwin-arm64
ls_target: darwin_arm64
npm_config_arch: arm64
runs-on: "ubuntu-latest"
steps:
- name: Check out repository
uses: actions/checkout@v2
- name: Read extension version
id: ext-version
run: |
content=`cat ./package.json | jq -r .version`
echo "::set-output name=content::$content"
- name: Ensure version matches with tag
if: ${{ github.ref != format('refs/tags/v{0}', steps.ext-version.outputs.content) }}
run: |
echo "Version mismatch!"
echo "Received ref: ${{ github.ref }}"
echo "Expected ref: refs/tags/v${{ steps.ext-version.outputs.content }}"
exit 1
- uses: actions/setup-node@v2
with:
node-version-file: '.nvmrc'
- name: Install dependencies
run: npm ci
env:
npm_config_arch: ${{ matrix.npm_config_arch }}
- name: Package VSIX
run: npm run package -- --target=${{ matrix.vsce_target }}
env:
ls_target: ${{ matrix.ls_target }}
- name: Upload vsix as artifact
uses: actions/upload-artifact@v2
with:
name: ${{ matrix.vsce_target }}.vsix
path: "*.vsix"

22
.github/workflows/test.yml поставляемый
Просмотреть файл

@ -23,6 +23,28 @@ jobs:
- name: lint
run: npm run lint
unit:
strategy:
fail-fast: false
matrix:
os:
- windows-latest
- macos-latest
- ubuntu-latest
runs-on: ${{ matrix.os }}
timeout-minutes: 3
steps:
- name: Checkout Repo
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version-file: '.nvmrc'
- name: npm install
run: npm ci
- name: unit test
run: npm run test:unit
test:
strategy:
fail-fast: false

2
.nvmrc
Просмотреть файл

@ -1 +1 @@
16.13.2
14.15.1

2
.vscode/extensions.json поставляемый
Просмотреть файл

@ -1,3 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "connor4312.esbuild-problem-matchers"]
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
}

10
.vscode/launch.json поставляемый
Просмотреть файл

@ -5,9 +5,13 @@
"name": "Launch Client",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "npm: watch"
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}"],
"outFiles": ["${workspaceRoot}/out/**/*.js"],
"preLaunchTask": {
"type": "npm",
"script": "watch"
}
},
{
"name": "Run Extension Tests",

16
.vscode/tasks.json поставляемый
Просмотреть файл

@ -4,17 +4,15 @@
{
"type": "npm",
"script": "watch",
"group": "build",
"problemMatcher": "$esbuild-watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"label": "npm: watch"
"presentation": {
"reveal": "never"
},
{
"type": "npm",
"script": "build",
"group": "build",
"problemMatcher": "$esbuild",
"label": "npm: build"
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "terraformInitAndWatch",

Просмотреть файл

@ -3,7 +3,6 @@
.vscode-test
build/
lsp/
node_modules/
src/
**/__mocks__
out/*.test.*

Просмотреть файл

@ -33,7 +33,7 @@ exteral lanuage server by configuring the following:
```
### Run in local development
0. Prerequisites: golang >1.16, node 16.X, npm 8.X
0. Prerequisites: golang >1.16, node 14.X, npm 6.X
1. Clone [Terraform AzApi Provider Language Server](https://github.com/Azure/azapi-lsp) to local
2. Run `go install` under project folder
3. Add the following configuration to vscode setting file.

8869
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -10,12 +10,9 @@
"preview": false,
"private": true,
"engines": {
"npm": "~8.X",
"node": "~16.X",
"vscode": "^1.61.0"
},
"langServer": {
"version": "0.25.2"
"npm": "~6.X",
"node": "~14.X",
"vscode": "^1.55.0"
},
"qna": "https://github.com/Azure/azapi-vscode/issues",
"bugs": {
@ -102,6 +99,10 @@
],
"default": "off",
"description": "Traces the communication between VS Code and the language server."
},
"requiredVersion": {
"type": "string",
"description": "The required version of the Language Server described as a semantic version string, for example '^2.0.1' or '> 1.0'. Defaults to latest available version."
}
},
"default": {
@ -130,50 +131,47 @@
"viewsWelcome": []
},
"scripts": {
"esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node",
"esbuild": "npm run esbuild-base -- --sourcemap",
"esbuild-watch": "npm run esbuild-base -- --sourcemap --watch",
"compile": "npm run esbuild",
"watch": "npm run download:ls && npm run esbuild-watch",
"download:ls": "ts-node ./build/downloader.ts",
"vscode:prepublish": "npm run download:ls && npm run esbuild-base -- --minify",
"package": "vsce package",
"test-compile": "tsc -p ./",
"pretest": "npm run download:ls && npm run test-compile && npm run lint",
"test": "node ./out/test/runTest.js",
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"lint": "eslint src --ext ts",
"pretest": "npm run compile && npm run lint",
"test": "node ./out/test/runTest.js",
"test:unit": "jest --silent=false",
"package": "vsce package",
"prettier": "prettier \"**/*.+(js|json|ts)\"",
"format": "npm run prettier -- --write",
"check-format": "npm run prettier -- --check",
"preview": "ts-node ./build/preview.ts"
"check-format": "npm run prettier -- --check"
},
"dependencies": {
"@types/axios": "^0.14.0",
"@types/semver": "^7.3.4",
"@types/unzip-stream": "^0.3.1",
"@vscode/extension-telemetry": "^0.4.9",
"axios": "^0.24.0",
"fs": "0.0.1-security",
"openpgp": "^4.10.10",
"semver": "^7.3.5",
"short-unique-id": "^3.2.3",
"vscode-extension-telemetry": "^0.4.2",
"unzip-stream": "^0.3.1",
"vscode-languageclient": "^7.0.0",
"vscode-uri": "^3.0.2",
"which": "^2.0.2"
"which": "^2.0.2",
"yauzl": "^2.10.0"
},
"devDependencies": {
"@types/chai": "^4.2.22",
"@types/glob": "^7.1.3",
"@types/jest": "^27.0.3",
"@types/mocha": "^9.0.0",
"@types/node": "^16.11.7",
"@types/node": "^12.12.54",
"@types/openpgp": "^4.4.15",
"@types/semver": "^7.3.4",
"@types/unzip-stream": "^0.3.1",
"@types/vscode": "^1.61.1",
"@types/vscode": "^1.52.0",
"@types/which": "^2.0.1",
"@types/yauzl": "^2.9.1",
"@typescript-eslint/eslint-plugin": "^3.9.0",
"@typescript-eslint/parser": "^3.9.0",
"axios": "^0.24.0",
"chai": "^4.3.4",
"esbuild": "^0.14.11",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.1",
@ -181,13 +179,9 @@
"jest": "^27.4.3",
"mocha": "^9.1.3",
"prettier": "^2.3.2",
"semver": "^7.3.5",
"temp": "^0.9.4",
"ts-jest": "^27.1.0",
"ts-node": "^10.4.0",
"typescript": "^4.5.4",
"unzip-stream": "^0.3.1",
"vsce": "^2.6.3",
"typescript": "^3.9.7",
"vscode-test": "^1.5.2"
}
}

Просмотреть файл

@ -1,6 +1,6 @@
import ShortUniqueId from 'short-unique-id';
import * as vscode from 'vscode';
import TelemetryReporter from 'vscode-extension-telemetry';
import TelemetryReporter from '@vscode/extension-telemetry';
import {
DocumentSelector,
Executable,

Просмотреть файл

@ -1,7 +1,10 @@
import * as vscode from 'vscode';
import TelemetryReporter from 'vscode-extension-telemetry';
import TelemetryReporter from '@vscode/extension-telemetry';
import { ClientHandler } from './clientHandler';
import { DEFAULT_LS_VERSION, isValidVersionString } from './installer/detector';
import { updateOrInstall } from './installer/updater';
import { ServerPath } from './serverPath';
import { SingleInstanceTimeout } from './utils';
import { config } from './vscodeUtils';
const brand = `Terraform AzApi Provider`;
@ -10,6 +13,7 @@ export let terraformStatus: vscode.StatusBarItem;
let reporter: TelemetryReporter;
let clientHandler: ClientHandler;
const languageServerUpdater = new SingleInstanceTimeout();
export async function activate(context: vscode.ExtensionContext): Promise<void> {
const manifest = context.extension.packageJSON;
@ -20,6 +24,15 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
const lsPath = new ServerPath(context);
clientHandler = new ClientHandler(lsPath, outputChannel, reporter);
if (config('azapi').has('languageServer.requiredVersion')) {
const langServerVer = config('azapi').get('languageServer.requiredVersion', DEFAULT_LS_VERSION);
if (!isValidVersionString(langServerVer)) {
vscode.window.showWarningMessage(
`The Terraform Language Server Version string '${langServerVer}' is not a valid semantic version and will be ignored.`,
);
}
}
// Subscriptions
context.subscriptions.push(
vscode.commands.registerCommand('azapi.enableLanguageServer', async () => {
@ -31,7 +44,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
vscode.ConfigurationTarget.Global,
);
}
startLanguageServer();
await updateLanguageServer(manifest.version, lsPath);
}),
vscode.commands.registerCommand('azapi.disableLanguageServer', async () => {
if (enabled()) {
@ -42,10 +55,11 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
vscode.ConfigurationTarget.Global,
);
}
stopLanguageServer();
languageServerUpdater.clear();
return clientHandler.stopClient();
}),
vscode.workspace.onDidChangeConfiguration(async (event: vscode.ConfigurationChangeEvent) => {
if (event.affectsConfiguration('terraform') || event.affectsConfiguration('azapi-lsp')) {
if (event.affectsConfiguration('azapi')) {
const reloadMsg = 'Reload VSCode window to apply language server changes';
const selected = await vscode.window.showInformationMessage(reloadMsg, 'Reload');
if (selected === 'Reload') {
@ -56,7 +70,13 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
);
if (enabled()) {
startLanguageServer();
try {
await updateLanguageServer(manifest.version, lsPath);
} catch (error) {
if (error instanceof Error) {
reporter.sendTelemetryException(error);
}
}
}
}
@ -68,24 +88,33 @@ export async function deactivate(): Promise<void> {
return clientHandler.stopClient();
}
async function startLanguageServer() {
try {
await clientHandler.startClient();
vscode.commands.executeCommand('setContext', 'terraform.showTreeViews', true);
} catch (error) {
console.log(error); // for test failure reporting
if (error instanceof Error) {
vscode.window.showErrorMessage(error instanceof Error ? error.message : error);
} else if (typeof error === 'string') {
vscode.window.showErrorMessage(error);
async function updateLanguageServer(extVersion: string, lsPath: ServerPath, scheduled = false) {
if (config('extensions').get<boolean>('autoCheckUpdates', true) === true) {
console.log('Scheduling check for language server updates...');
const hour = 1000 * 60 * 60;
languageServerUpdater.timeout(function () {
updateLanguageServer(extVersion, lsPath, true);
}, 24 * hour);
}
if (lsPath.hasCustomBinPath()) {
// skip install check if user has specified a custom path to the LS
// with custom paths we *need* to start the lang client always
await clientHandler.startClient();
return;
}
}
}
async function stopLanguageServer() {
try {
await clientHandler.stopClient();
vscode.commands.executeCommand('setContext', 'terraform.showTreeViews', false);
await updateOrInstall(config('azapi').get('languageServer.requiredVersion', DEFAULT_LS_VERSION), lsPath, reporter);
// On scheduled checks, we download to stg and do not replace prod path
// So we *do not* need to stop or start the LS
if (scheduled) {
return;
}
// On fresh starts we *need* to start the lang client always
await clientHandler.startClient();
} catch (error) {
console.log(error); // for test failure reporting
if (error instanceof Error) {
@ -97,5 +126,5 @@ async function stopLanguageServer() {
}
function enabled(): boolean {
return config('azapi').get('languageServer.external', false);
return config('azapi').get('languageServer.external', true);
}

81
src/installer/detector.ts Normal file
Просмотреть файл

@ -0,0 +1,81 @@
import * as semver from 'semver';
import * as vscode from 'vscode';
import { exec } from '../utils';
import axios from 'axios';
import { Build, Release } from '../types';
export const DEFAULT_LS_VERSION = 'latest';
export function isValidVersionString(value: string): boolean {
return semver.validRange(value, { includePrerelease: true, loose: true }) !== null;
}
export async function getLsVersion(binPath: string): Promise<string | undefined> {
try {
const jsonCmd: { stdout: string } = await exec(binPath, ['version', '-json']);
const jsonOutput = JSON.parse(jsonCmd.stdout);
return jsonOutput.version.replace('-dev', '');
} catch (err) {
// assume older version of LS which didn't have json flag
// return undefined as regex matching isn't useful here
// if it's old enough to not have the json version, we would be updating anyway
return undefined;
}
}
export async function getRequiredVersionRelease(versionString: string): Promise<Release> {
try {
const response = await axios.get('https://api.github.com/repos/Azure/azapi-lsp/releases', {
headers: {
Authorization: 'token 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',
},
});
if (response.status == 200 && response.data.length != 0) {
if (versionString == 'latest') {
return toRelease(response.data[0]);
} else {
const versions = [];
for (const i in response.data) {
versions.push(response.data[i].tag_name);
}
const matchedVersion = semver.maxSatisfying(versions, versionString);
for (const i in response.data) {
if (response.data[i].tag_name == matchedVersion) {
return toRelease(response.data[i]);
}
}
console.log(`Found no matched release of azapi-lsp, version: ${versionString}`);
vscode.window.showWarningMessage(`Found no matched release of azapi-lsp, use latest`);
return toRelease(response.data[0]);
}
}
vscode.window.showWarningMessage(`Found no releases of azapi-lsp`);
} catch (err) {
vscode.window.showErrorMessage(`Error loading releases of azapi-lsp`);
throw err;
}
throw new Error('no valid release');
}
function toRelease(data: any): Release {
const assets: Build[] = [];
for (const i in data.assets) {
assets.push({
name: data.assets[i].name,
downloadUrl: data.assets[i].browser_download_url,
});
}
return {
version: data.name,
assets: assets,
};
}
export async function pathExists(filePath: string): Promise<boolean> {
try {
await vscode.workspace.fs.stat(vscode.Uri.file(filePath));
return true;
} catch (error) {
return false;
}
}

121
src/installer/installer.ts Normal file
Просмотреть файл

@ -0,0 +1,121 @@
import axios from 'axios';
import * as path from 'path';
import * as vscode from 'vscode';
import TelemetryReporter from '@vscode/extension-telemetry';
import { pathExists } from './detector';
import * as fs from 'fs';
import * as unzip from 'unzip-stream';
import { Build, Release } from '../types';
export async function installTerraformLS(
installPath: string,
release: Release,
reporter: TelemetryReporter,
): Promise<void> {
reporter.sendTelemetryEvent('installingLs', { terraformLsVersion: release.version });
const zipfile = path.resolve(installPath, `azapi-lsp_${release.version}.zip`);
const os = getPlatform();
const arch = getArch();
let build: Build | undefined;
for (const i in release.assets) {
if (release.assets[i].name.endsWith(`${os}_${arch}.zip`)) {
build = release.assets[i];
break;
}
}
if (!build) {
throw new Error(`Install error: no matching azapi-lsp binary for ${os}/${arch}`);
}
// On brand new extension installs, there isn't a directory until we execute here
// Create it if it doesn't exist so the downloader can unpack
if ((await pathExists(installPath)) === false) {
await vscode.workspace.fs.createDirectory(vscode.Uri.file(installPath));
}
console.log(build);
// Download and unpack async inside the VS Code notification window
// This will show in the statusbar for the duration of the download and unpack
// This was the most non-distuptive choice that still provided some status to the user
return vscode.window.withProgress(
{
cancellable: false,
location: vscode.ProgressLocation.Window,
title: 'Installing azapi-lsp',
},
async (progress) => {
// download zip
progress.report({ increment: 30 });
await axios.get(build!.downloadUrl, { responseType: 'stream' }).then(function (response) {
const fileWritePipe = fs.createWriteStream(zipfile);
response.data.pipe(fileWritePipe);
return new Promise<void>((resolve, reject) => {
fileWritePipe.on('close', () => resolve());
response.data.on('error', reject);
});
});
// verify
progress.report({ increment: 30 });
// unzip
const fileExtension = os === 'windows' ? '.exe' : '';
const unversionedName = path.resolve(installPath, `azapi-lsp${fileExtension}`);
progress.report({ increment: 20 });
const fileReadStream = fs.createReadStream(zipfile);
const unzipPipe = unzip.Extract({ path: installPath });
fileReadStream.pipe(unzipPipe);
await new Promise<void>((resolve, reject) => {
unzipPipe.on('close', () => {
fs.chmodSync(unversionedName, '755');
return resolve();
});
fileReadStream.on('error', reject);
});
progress.report({ increment: 10 });
return vscode.workspace.fs.delete(vscode.Uri.file(zipfile));
},
);
}
function getPlatform(): string {
const platform = process.platform.toString();
if (platform === 'win32') {
return 'windows';
}
if (platform === 'sunos') {
return 'solaris';
}
return platform;
}
function getArch(): string {
// platform | terraform-ls | extension platform | vs code editor
// -- | -- | -- | --
// macOS | darwin_amd64 | darwin_x64 | ✅
// macOS | darwin_arm64 | darwin_arm64 | ✅
// Linux | linux_amd64 | linux_x64 | ✅
// Linux | linux_arm | linux_armhf | ✅
// Linux | linux_arm64 | linux_arm64 | ✅
// Windows | windows_386 | win32_ia32 | ✅
// Windows | windows_amd64 | win32_x64 | ✅
// Windows | windows_arm64 | win32_arm64 | ✅
const arch = process.arch;
if (arch === 'ia32') {
return '386';
}
if (arch === 'x64') {
return 'amd64';
}
if (arch === 'armhf') {
return 'arm';
}
return arch;
}

97
src/installer/updater.ts Normal file
Просмотреть файл

@ -0,0 +1,97 @@
import * as semver from 'semver';
import * as vscode from 'vscode';
import TelemetryReporter from '@vscode/extension-telemetry';
import { ServerPath } from '../serverPath';
import { Release } from '../types';
import { config } from '../vscodeUtils';
import {
DEFAULT_LS_VERSION,
getLsVersion,
getRequiredVersionRelease,
isValidVersionString,
pathExists,
} from './detector';
import { installTerraformLS } from './installer';
export async function updateOrInstall(
lsVersion: string,
lsPath: ServerPath,
reporter: TelemetryReporter,
): Promise<void> {
const stgingExists = await pathExists(lsPath.stgBinPath());
if (stgingExists) {
// LS was updated during the last run while user was using the extension
// Do not check for updates here, as normal execution flow will handle decision logic later
// Need to move stg path to prod path now and return normal execution
await vscode.workspace.fs.rename(vscode.Uri.file(lsPath.stgBinPath()), vscode.Uri.file(lsPath.binPath()), {
overwrite: true,
});
return;
}
// Silently default to latest if an invalid version string is passed.
// Actually telling the user about a bad string is left to the main extension code instead of here
const versionString = isValidVersionString(lsVersion) ? lsVersion : DEFAULT_LS_VERSION;
const lsPresent = await pathExists(lsPath.binPath());
const autoUpdate = config('extensions').get<boolean>('autoUpdate', true);
if (lsPresent === true && autoUpdate === false) {
// LS is present in prod path, but user does not want automatic updates
// Return normal execution
return;
}
// Get LS release information from github release
// Fall back to latest if not requested version not available
let release: Release;
try {
release = await getRequiredVersionRelease(versionString);
} catch (err) {
console.log(
`Error while finding Terraform language server release which satisfies range '${versionString}': ${err}`,
);
// if the releases site is inaccessible, report it and skip the install
if (err instanceof Error) {
reporter.sendTelemetryException(err);
}
return;
}
if (lsPresent === false) {
// LS is not present, need to download now in order to function
// Install directly to production path and return normal execution
return installTerraformLS(lsPath.installPath(), release, reporter);
}
// We know there is an LS Present at this point, find out version if possible
const installedVersion = await getLsVersion(lsPath.binPath());
if (installedVersion === undefined) {
console.log(`Currently installed Terraform language server is version '${installedVersion}`);
// ls is present but too old to tell us the version, so need to update now
return installTerraformLS(lsPath.installPath(), release, reporter);
}
// We know there is an LS present and know the version, so decide whether to update or not
console.log(`Currently installed Terraform language server is version '${installedVersion}`);
reporter.sendTelemetryEvent('foundLsInstalled', { terraformLsVersion: installedVersion });
// Already at the latest or specified version, no update needed
// return to normal execution flow
if (semver.eq(release.version, installedVersion, { includePrerelease: true })) {
console.log(`Language server release is current: ${release.version}`);
return;
}
// We used to prompt for decision here, but effectively downgrading or upgrading
// are the same operation so log decision and update
if (semver.gt(release.version, installedVersion, { includePrerelease: true })) {
// Upgrade
console.log(`A newer language server release is available: ${release.version}`);
} else if (semver.lt(release.version, installedVersion, { includePrerelease: true })) {
// Downgrade
console.log(`An older language server release is available: ${release.version}`);
}
// Update indicated and user wants autoupdates, so update to latest or specified version
return installTerraformLS(lsPath.stgInstallPath(), release, reporter);
}

Просмотреть файл

@ -13,7 +13,11 @@ export class ServerPath {
}
public installPath(): string {
return path.join(this.context.extensionPath, INSTALL_FOLDER_NAME);
return path.join(this.context.globalStorageUri.fsPath, INSTALL_FOLDER_NAME);
}
public stgInstallPath(): string {
return path.join(this.context.globalStorageUri.fsPath, 'stg');
}
public hasCustomBinPath(): boolean {
@ -28,6 +32,14 @@ export class ServerPath {
return path.resolve(this.installPath(), this.binName());
}
public stgBinPath(): string {
if (this.customBinPath) {
return this.customBinPath;
}
return path.resolve(this.stgInstallPath(), this.binName());
}
public binName(): string {
if (this.customBinPath) {
return path.basename(this.customBinPath);

Просмотреть файл

@ -1,4 +1,4 @@
import TelemetryReporter from 'vscode-extension-telemetry';
import TelemetryReporter from '@vscode/extension-telemetry';
import { BaseLanguageClient, ClientCapabilities, StaticFeature } from 'vscode-languageclient';
import { ExperimentalClientCapabilities } from './types';

Просмотреть файл

@ -0,0 +1,62 @@
import { mocked } from 'ts-jest/utils';
import { exec as execOrg } from '../../utils';
import { getLsVersion, isValidVersionString, getRequiredVersionRelease } from '../../installer/detector';
jest.mock('../../utils');
const exec = mocked(execOrg);
describe('terraform release detector', () => {
test('returns valid release', async () => {
const version = 'v0.0.1';
const result = await getRequiredVersionRelease(version);
expect(result.version).toMatch(version);
expect(result.assets.length).toBeGreaterThan(0);
});
test('returns latest if invalid version', async () => {
const resultWithInvalidVersion = await getRequiredVersionRelease('v10000.24.0');
const resultLatest = await getRequiredVersionRelease('latest');
expect(resultLatest.version.length).toBeGreaterThan(0);
expect(resultWithInvalidVersion).toMatchObject(resultLatest);
});
});
describe('terraform detector', () => {
test('returns valid version with valid path', async () => {
exec.mockImplementationOnce(async () => {
return {
stdout: '{"version": "1.2.3"}',
stderr: '',
};
});
const result = await getLsVersion('installPath');
expect(result).toBe('1.2.3');
});
test('returns undefined with invalid path', async () => {
const result = await getLsVersion('installPath');
expect(result).toBe(undefined);
});
});
describe('version detector', () => {
test('detect valid version', async () => {
const result = isValidVersionString('1.2.3');
expect(result).toBeTruthy();
});
test('detect invalid version', async () => {
const result = isValidVersionString('1f');
expect(result).toBeFalsy();
});
test('detect valid semver version', async () => {
const result = isValidVersionString('1.2.3-alpha');
expect(result).toBeTruthy();
});
test('detect invalid semver version', async () => {
const result = isValidVersionString('1.23-alpha');
expect(result).toBeFalsy();
});
});

Просмотреть файл

@ -0,0 +1,34 @@
import { installTerraformLS } from '../../installer/installer';
import { reporter } from './mocks/reporter';
import { Build, Release } from '../../types';
jest.mock('../../installer/detector');
describe('azapi-lsp installer', () => {
describe('should install', () => {
test('when valid version is passed', async () => {
const expectedRelease: Release = getRelease('v0.0.1');
await installTerraformLS('installPath', expectedRelease, reporter);
});
});
});
function getRelease(version: string): Release {
return {
version: version,
assets: [
{
downloadUrl: `https://github.com/Azure/azapi-lsp/releases/download/${version}/azapi-lsp_${version}_windows_amd64.zip`,
name: `azapi-lsp_${version}_windows_amd64.zip`,
},
{
downloadUrl: `https://github.com/Azure/azapi-lsp/releases/download/${version}/azapi-lsp_${version}_darwin_amd64.zip`,
name: `azapi-lsp_${version}_darwin_amd64.zip`,
},
{
downloadUrl: `https://github.com/Azure/azapi-lsp/releases/download/${version}/azapi-lsp_${version}_linux_amd64.zip`,
name: `azapi-lsp_${version}_linux_amd64.zip`,
},
],
};
}

Просмотреть файл

@ -4,4 +4,5 @@ export const reporter = {
sendTelemetryErrorEvent: jest.fn(),
sendTelemetryException: jest.fn(),
dispose: jest.fn(),
telemetryLevel: 'all' as any,
};

Просмотреть файл

@ -0,0 +1,245 @@
import * as vscode from 'vscode';
import { mocked } from 'ts-jest/utils';
import { updateOrInstall } from '../../installer/updater';
import { reporter } from './mocks/reporter';
import { installTerraformLS } from '../../installer/installer';
import {
getRequiredVersionRelease as getRequiredVersionReleaseOrig,
isValidVersionString as isValidVersionStringOrig,
pathExists as pathExistsOrig,
getLsVersion as getLsVersionOrig,
} from '../../installer/detector';
import { ServerPath } from '../../serverPath';
import { lsPathMock } from './mocks/serverPath';
import { Release } from '../../types';
jest.mock('../../installer/detector');
jest.mock('../../installer/installer');
const getConfiguration = mocked(vscode.workspace.getConfiguration);
const pathExists = mocked(pathExistsOrig);
const isValidVersionString = mocked(isValidVersionStringOrig);
const getRequiredVersionRelease = mocked(getRequiredVersionReleaseOrig);
const getLsVersion = mocked(getLsVersionOrig);
// @ts-ignore
const lsPath: ServerPath & typeof lsPathMock = lsPathMock;
describe('azapi-lsp updater', () => {
describe('should install', () => {
test('on fresh install', async () => {
getConfiguration.mockImplementationOnce(() => ({
get: jest.fn(() => {
// config('extensions').get<boolean>('autoUpdate', true);
return true;
}),
has: jest.fn(),
inspect: jest.fn(),
update: jest.fn(),
}));
pathExists
.mockImplementationOnce(async () => false) // stg not present
.mockImplementationOnce(async () => false); // prod not present
isValidVersionString.mockImplementationOnce(() => true);
getRequiredVersionRelease.mockImplementationOnce(async () => {
return getRelease('v0.0.1');
});
await updateOrInstall('v0.0.1', lsPath, reporter);
expect(pathExists).toBeCalledTimes(2);
expect(installTerraformLS).toBeCalledTimes(1);
expect(vscode.workspace.getConfiguration).toBeCalledTimes(1);
expect(lsPath.stgBinPath).toBeCalledTimes(1);
expect(lsPath.installPath).toBeCalledTimes(1);
});
test('ls version not found', async () => {
getConfiguration.mockImplementationOnce(() => ({
get: jest.fn(() => {
// config('extensions').get<boolean>('autoUpdate', true);
return true;
}),
has: jest.fn(),
inspect: jest.fn(),
then: jest.fn(),
update: jest.fn(),
}));
pathExists
.mockImplementationOnce(async () => false) // stg not present
.mockImplementationOnce(async () => true); // prod present
isValidVersionString.mockImplementationOnce(() => true);
getLsVersion.mockImplementationOnce(async () => undefined);
getRequiredVersionRelease.mockImplementationOnce(async () => {
return getRelease('v0.0.1');
});
await updateOrInstall('v0.0.1', lsPath, reporter);
expect(pathExists).toBeCalledTimes(2);
expect(installTerraformLS).toBeCalledTimes(1);
});
test('with out of date ls', async () => {
getConfiguration.mockImplementationOnce(() => ({
get: jest.fn(() => {
// config('extensions').get<boolean>('autoUpdate', true);
return true;
}),
has: jest.fn(),
inspect: jest.fn(),
then: jest.fn(),
update: jest.fn(),
}));
pathExists
.mockImplementationOnce(async () => false) // stg not present
.mockImplementationOnce(async () => true); // prod present
isValidVersionString.mockImplementationOnce(() => {
return true;
});
getLsVersion.mockImplementationOnce(async () => 'v0.0.1');
getRequiredVersionRelease.mockImplementationOnce(async () => {
return getRelease('v0.0.2');
});
await updateOrInstall('v0.0.2', lsPath, reporter);
expect(pathExists).toBeCalledTimes(2);
expect(installTerraformLS).toBeCalledTimes(1);
});
});
describe('should not install', () => {
test('instead move staging to prod', async () => {
// this mimics the stging path being present, which should trigger a rename
pathExists.mockImplementationOnce(async () => true);
getRequiredVersionRelease.mockImplementationOnce(async () => {
return getRelease('v0.0.1');
});
await updateOrInstall('v0.0.1', lsPath, reporter);
// expect stg to be renamed to prod
expect(vscode.workspace.fs.rename).toBeCalledTimes(1);
expect(vscode.workspace.getConfiguration).toBeCalledTimes(0);
});
test('ls present and autoupdate is false', async () => {
getConfiguration.mockImplementationOnce(() => ({
get: jest.fn(() => {
// config('extensions').get<boolean>('autoUpdate', true);
return false;
}),
has: jest.fn(),
inspect: jest.fn(),
then: jest.fn(),
update: jest.fn(),
}));
lsPath.installPath.mockImplementationOnce(() => 'installPath');
lsPath.stgBinPath.mockImplementationOnce(() => 'stgbinpath');
isValidVersionString.mockImplementationOnce(() => {
return true;
});
pathExists
.mockImplementationOnce(async () => false) // stg
.mockImplementationOnce(async () => true); // prod
getRequiredVersionRelease.mockImplementationOnce(async () => {
return getRelease('v0.0.1');
});
await updateOrInstall('v0.0.1', lsPath, reporter);
expect(pathExists).toBeCalledTimes(2);
expect(vscode.workspace.getConfiguration).toBeCalledTimes(1);
expect(vscode.workspace.fs.rename).toBeCalledTimes(0);
expect(getRequiredVersionRelease).toBeCalledTimes(0);
});
test('invlaid azapi-lsp verison', async () => {
getConfiguration.mockImplementationOnce(() => ({
get: jest.fn(() => {
// config('extensions').get<boolean>('autoUpdate', true);
return true;
}),
has: jest.fn(),
inspect: jest.fn(),
then: jest.fn(),
update: jest.fn(),
}));
lsPath.installPath.mockImplementationOnce(() => 'installPath');
lsPath.stgBinPath.mockImplementationOnce(() => 'stgbinpath');
isValidVersionString.mockImplementationOnce(() => {
return true;
});
pathExists
.mockImplementationOnce(async () => false) // stg
.mockImplementationOnce(async () => false); // prod
getRequiredVersionRelease.mockImplementationOnce(() => {
throw new Error('wahtever');
});
await updateOrInstall('v0.0.1', lsPath, reporter);
expect(pathExists).toBeCalledTimes(2);
expect(getRequiredVersionRelease).toBeCalledTimes(1);
expect(reporter.sendTelemetryException).toBeCalledTimes(1);
expect(vscode.workspace.getConfiguration).toBeCalledTimes(1);
expect(vscode.workspace.fs.rename).toBeCalledTimes(0);
});
test('with current ls version', async () => {
getConfiguration.mockImplementationOnce(() => ({
get: jest.fn(() => {
// config('extensions').get<boolean>('autoUpdate', true);
return true;
}),
has: jest.fn(),
inspect: jest.fn(),
then: jest.fn(),
update: jest.fn(),
}));
pathExists
.mockImplementationOnce(async () => false) // stg not present
.mockImplementationOnce(async () => true); // prod present
isValidVersionString.mockImplementationOnce(() => true);
getLsVersion.mockImplementationOnce(async () => 'v0.0.1');
getRequiredVersionRelease.mockImplementationOnce(async () => {
return getRelease('v0.0.1');
});
await updateOrInstall('v0.0.1', lsPath, reporter);
expect(pathExists).toBeCalledTimes(2);
expect(installTerraformLS).toBeCalledTimes(0);
});
});
});
function getRelease(version: string): Release {
return {
version: version,
assets: [
{
downloadUrl: `https://github.com/Azure/azapi-lsp/releases/download/${version}/azapi-lsp_${version}_windows_amd64.zip`,
name: `azapi-lsp_${version}_windows_amd64.zip`,
},
{
downloadUrl: `https://github.com/Azure/azapi-lsp/releases/download/${version}/azapi-lsp_${version}_darwin_amd64.zip`,
name: `azapi-lsp_${version}_darwin_amd64.zip`,
},
{
downloadUrl: `https://github.com/Azure/azapi-lsp/releases/download/${version}/azapi-lsp_${version}_linux_amd64.zip`,
name: `azapi-lsp_${version}_linux_amd64.zip`,
},
],
};
}

Просмотреть файл

@ -6,3 +6,13 @@ export interface ExperimentalClientCapabilities {
telemetryVersion?: number;
};
}
export interface Build {
name: string;
downloadUrl: string;
}
export interface Release {
version: string;
assets: Build[];
}

Просмотреть файл

@ -14,3 +14,31 @@ export function exec(cmd: string, args: readonly string[]): Promise<{ stdout: st
export async function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// A small wrapper around setTimeout which ensures that only a single timeout
// timer can be running at a time. Attempts to add a new timeout silently fail.
export class SingleInstanceTimeout {
private timerLock = false;
private timerId: NodeJS.Timeout | null = null;
public timeout(fn: (...args: any[]) => void, delay: number, ...args: any[]): void {
if (!this.timerLock) {
this.timerLock = true;
this.timerId = setTimeout(
() => {
this.timerLock = false;
fn();
},
delay,
args,
);
}
}
public clear(): void {
if (this.timerId) {
clearTimeout(this.timerId);
}
this.timerLock = false;
}
}