From 07b8c73e163ba980ca29a3bccd22c7f793cc7784 Mon Sep 17 00:00:00 2001 From: Josh Pinkney Date: Thu, 9 Jan 2025 11:24:53 -0500 Subject: [PATCH] test(amazonq): Add e2e tests for lsp auto updating --- packages/amazonq/src/lsp/lspInstaller.ts | 2 +- .../amazonq/test/e2e/lsp/lspInstaller.test.ts | 122 ++++++++++++++++++ packages/core/src/shared/index.ts | 1 + .../src/shared/languageServer/lspResolver.ts | 19 ++- 4 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 packages/amazonq/test/e2e/lsp/lspInstaller.test.ts diff --git a/packages/amazonq/src/lsp/lspInstaller.ts b/packages/amazonq/src/lsp/lspInstaller.ts index 120cdebe39c..5148d895466 100644 --- a/packages/amazonq/src/lsp/lspInstaller.ts +++ b/packages/amazonq/src/lsp/lspInstaller.ts @@ -8,7 +8,7 @@ import { Range } from 'semver' import { ManifestResolver, LanguageServerResolver, LspResolver, LspResult } from 'aws-core-vscode/shared' const manifestURL = 'https://aws-toolkit-language-servers.amazonaws.com/codewhisperer/0/manifest.json' -const supportedLspServerVersions = '^2.3.1' +export const supportedLspServerVersions = '^2.3.0' export class AmazonQLSPResolver implements LspResolver { async resolve(): Promise { diff --git a/packages/amazonq/test/e2e/lsp/lspInstaller.test.ts b/packages/amazonq/test/e2e/lsp/lspInstaller.test.ts new file mode 100644 index 00000000000..0bf2edafa3f --- /dev/null +++ b/packages/amazonq/test/e2e/lsp/lspInstaller.test.ts @@ -0,0 +1,122 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import sinon from 'sinon' +import { AmazonQLSPResolver, supportedLspServerVersions } from '../../../src/lsp/lspInstaller' +import { + fs, + LanguageServerResolver, + makeTemporaryToolkitFolder, + ManifestResolver, + request, +} from 'aws-core-vscode/shared' +import * as semver from 'semver' + +function createVersion(version: string) { + return { + isDelisted: false, + serverVersion: version, + targets: [ + { + arch: process.arch, + platform: process.platform, + contents: [ + { + bytes: 0, + filename: 'servers.zip', + hashes: [], + url: 'http://fakeurl', + }, + ], + }, + ], + } +} + +describe('AmazonQLSPInstaller', () => { + let resolver: AmazonQLSPResolver + let sandbox: sinon.SinonSandbox + let tempDir: string + + beforeEach(async () => { + sandbox = sinon.createSandbox() + resolver = new AmazonQLSPResolver() + tempDir = await makeTemporaryToolkitFolder() + sandbox.stub(LanguageServerResolver.prototype, 'defaultDownloadFolder').returns(tempDir) + }) + + afterEach(async () => { + delete process.env.AWS_LANGUAGE_SERVER_OVERRIDE + sandbox.restore() + await fs.delete(tempDir, { + recursive: true, + }) + }) + + describe('resolve()', () => { + it('uses AWS_LANGUAGE_SERVER_OVERRIDE', async () => { + const overridePath = '/custom/path/to/lsp' + process.env.AWS_LANGUAGE_SERVER_OVERRIDE = overridePath + + const result = await resolver.resolve() + + assert.strictEqual(result.assetDirectory, overridePath) + assert.strictEqual(result.location, 'override') + assert.strictEqual(result.version, '0.0.0') + }) + + it('resolves', async () => { + // First try - should download the file + const download = await resolver.resolve() + + assert.ok(download.assetDirectory.startsWith(tempDir)) + assert.deepStrictEqual(download.location, 'remote') + assert.ok(semver.satisfies(download.version, supportedLspServerVersions)) + + // Second try - Should see the contents in the cache + const cache = await resolver.resolve() + + assert.ok(cache.assetDirectory.startsWith(tempDir)) + assert.deepStrictEqual(cache.location, 'cache') + assert.ok(semver.satisfies(cache.version, supportedLspServerVersions)) + + /** + * Always make sure the latest version is one patch higher. This stops a problem + * where the fallback can't be used because the latest compatible version + * is equal to the min version, so if the cache isn't valid, then there + * would be no fallback location + * + * Instead, increasing the latest compatible lsp version means we can just + * use the one we downloaded earlier in the test as the fallback + */ + const nextVer = semver.inc(cache.version, 'patch', true) + if (!nextVer) { + throw new Error('Could not increment version') + } + sandbox.stub(ManifestResolver.prototype, 'resolve').resolves({ + manifestSchemaVersion: '0.0.0', + artifactId: 'foo', + artifactDescription: 'foo', + isManifestDeprecated: false, + versions: [createVersion(nextVer), createVersion(cache.version)], + }) + + // fail the next http request for the language server + sandbox.stub(request, 'fetch').returns({ + response: Promise.resolve({ + ok: false, + }), + } as any) + + // Third try - Cache doesn't exist and we couldn't download from the internet, fallback to a local version + const fallback = await resolver.resolve() + + assert.ok(fallback.assetDirectory.startsWith(tempDir)) + assert.deepStrictEqual(fallback.location, 'fallback') + assert.ok(semver.satisfies(fallback.version, supportedLspServerVersions)) + }) + }) +}) diff --git a/packages/core/src/shared/index.ts b/packages/core/src/shared/index.ts index a77f45d9a91..d081bfc2bde 100644 --- a/packages/core/src/shared/index.ts +++ b/packages/core/src/shared/index.ts @@ -62,3 +62,4 @@ export { TabTypeDataMap } from '../amazonq/webview/ui/tabs/constants' export * from './languageServer/manifestResolver' export * from './languageServer/lspResolver' export * from './languageServer/types' +export { default as request } from './request' diff --git a/packages/core/src/shared/languageServer/lspResolver.ts b/packages/core/src/shared/languageServer/lspResolver.ts index 636163327ab..7a6fc4102b0 100644 --- a/packages/core/src/shared/languageServer/lspResolver.ts +++ b/packages/core/src/shared/languageServer/lspResolver.ts @@ -77,9 +77,9 @@ export class LanguageServerResolver { throw new ToolkitError('Unable to find a compatible version of the Language Server') } - const version = path.basename(cacheDirectory) + const version = path.basename(fallbackDirectory) logger.info( - `Unable to install language server ${latestVersion.serverVersion}. Launching a previous version from ${fallbackDirectory}` + `Unable to install ${this.lsName} language server v${latestVersion.serverVersion}. Launching a previous version from ${fallbackDirectory}` ) result.location = 'fallback' @@ -107,6 +107,7 @@ export class LanguageServerResolver { .filter(([_, filetype]) => filetype === FileType.Directory) .map(([pathName, _]) => semver.parse(pathName)) .filter((ver): ver is semver.SemVer => ver !== null) + .map((x) => x.version) const expectedVersion = semver.parse(version) if (!expectedVersion) { @@ -115,7 +116,7 @@ export class LanguageServerResolver { const sortedCachedLspVersions = compatibleLspVersions .filter((v) => this.isValidCachedVersion(v, cachedVersions, expectedVersion)) - .sort((a, b) => semver.compare(a.serverVersion, b.serverVersion)) + .sort((a, b) => semver.compare(b.serverVersion, a.serverVersion)) const fallbackDir = ( await Promise.all(sortedCachedLspVersions.map((ver) => this.getValidLocalCacheDirectory(ver))) @@ -144,9 +145,9 @@ export class LanguageServerResolver { * A version is considered valid if it exists in the cache and is less than * or equal to the expected version. */ - private isValidCachedVersion(version: LspVersion, cachedVersions: semver.SemVer[], expectedVersion: semver.SemVer) { + private isValidCachedVersion(version: LspVersion, cachedVersions: string[], expectedVersion: semver.SemVer) { const serverVersion = semver.parse(version.serverVersion) as semver.SemVer - return cachedVersions.find((x) => x === serverVersion) && serverVersion <= expectedVersion + return cachedVersions.includes(serverVersion.version) && semver.lte(serverVersion, expectedVersion) } /** @@ -283,9 +284,7 @@ export class LanguageServerResolver { const latestCompatibleVersion = this.manifest.versions .filter((ver) => this.isCompatibleVersion(ver) && this.hasRequiredTargetContent(ver)) - .sort((a, b) => { - return a.serverVersion.localeCompare(b.serverVersion) - })[0] ?? undefined + .sort((a, b) => semver.compare(b.serverVersion, a.serverVersion))[0] ?? undefined if (latestCompatibleVersion === undefined) { // TODO fix these error range names @@ -340,12 +339,12 @@ export class LanguageServerResolver { return version.targets.find((x) => x.arch === arch && x.platform === platform) } - private defaultDownloadFolder() { + defaultDownloadFolder() { const applicationSupportFolder = getApplicationSupportFolder() return path.join(applicationSupportFolder, `aws/toolkits/language-servers/${this.lsName}`) } - getDownloadDirectory(version: string) { + private getDownloadDirectory(version: string) { const directory = this._defaultDownloadFolder ?? this.defaultDownloadFolder() return `${directory}/${version}` }