Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/amazonq/src/lsp/lspInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<LspResult> {
Expand Down
122 changes: 122 additions & 0 deletions packages/amazonq/test/e2e/lsp/lspInstaller.test.ts
Original file line number Diff line number Diff line change
@@ -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))
})
})
})
1 change: 1 addition & 0 deletions packages/core/src/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
19 changes: 9 additions & 10 deletions packages/core/src/shared/languageServer/lspResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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) {
Expand All @@ -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)))
Expand Down Expand Up @@ -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)
}

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}`
}
Expand Down
Loading