Skip to content

Commit 7ba424d

Browse files
authored
test(amazonq): Add e2e tests for lsp auto updating (#6326)
## Problem We don't have any e2e test for auto updating the language server ## Solution Add a test that verifies downloading, caching, and fallbacks work as expected --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent ec5d5af commit 7ba424d

File tree

4 files changed

+133
-11
lines changed

4 files changed

+133
-11
lines changed

packages/amazonq/src/lsp/lspInstaller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Range } from 'semver'
88
import { ManifestResolver, LanguageServerResolver, LspResolver, LspResult } from 'aws-core-vscode/shared'
99

1010
const manifestURL = 'https://aws-toolkit-language-servers.amazonaws.com/codewhisperer/0/manifest.json'
11-
const supportedLspServerVersions = '^2.3.1'
11+
export const supportedLspServerVersions = '^2.3.0'
1212

1313
export class AmazonQLSPResolver implements LspResolver {
1414
async resolve(): Promise<LspResult> {
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import assert from 'assert'
7+
import sinon from 'sinon'
8+
import { AmazonQLSPResolver, supportedLspServerVersions } from '../../../src/lsp/lspInstaller'
9+
import {
10+
fs,
11+
LanguageServerResolver,
12+
makeTemporaryToolkitFolder,
13+
ManifestResolver,
14+
request,
15+
} from 'aws-core-vscode/shared'
16+
import * as semver from 'semver'
17+
18+
function createVersion(version: string) {
19+
return {
20+
isDelisted: false,
21+
serverVersion: version,
22+
targets: [
23+
{
24+
arch: process.arch,
25+
platform: process.platform,
26+
contents: [
27+
{
28+
bytes: 0,
29+
filename: 'servers.zip',
30+
hashes: [],
31+
url: 'http://fakeurl',
32+
},
33+
],
34+
},
35+
],
36+
}
37+
}
38+
39+
describe('AmazonQLSPInstaller', () => {
40+
let resolver: AmazonQLSPResolver
41+
let sandbox: sinon.SinonSandbox
42+
let tempDir: string
43+
44+
beforeEach(async () => {
45+
sandbox = sinon.createSandbox()
46+
resolver = new AmazonQLSPResolver()
47+
tempDir = await makeTemporaryToolkitFolder()
48+
sandbox.stub(LanguageServerResolver.prototype, 'defaultDownloadFolder').returns(tempDir)
49+
})
50+
51+
afterEach(async () => {
52+
delete process.env.AWS_LANGUAGE_SERVER_OVERRIDE
53+
sandbox.restore()
54+
await fs.delete(tempDir, {
55+
recursive: true,
56+
})
57+
})
58+
59+
describe('resolve()', () => {
60+
it('uses AWS_LANGUAGE_SERVER_OVERRIDE', async () => {
61+
const overridePath = '/custom/path/to/lsp'
62+
process.env.AWS_LANGUAGE_SERVER_OVERRIDE = overridePath
63+
64+
const result = await resolver.resolve()
65+
66+
assert.strictEqual(result.assetDirectory, overridePath)
67+
assert.strictEqual(result.location, 'override')
68+
assert.strictEqual(result.version, '0.0.0')
69+
})
70+
71+
it('resolves', async () => {
72+
// First try - should download the file
73+
const download = await resolver.resolve()
74+
75+
assert.ok(download.assetDirectory.startsWith(tempDir))
76+
assert.deepStrictEqual(download.location, 'remote')
77+
assert.ok(semver.satisfies(download.version, supportedLspServerVersions))
78+
79+
// Second try - Should see the contents in the cache
80+
const cache = await resolver.resolve()
81+
82+
assert.ok(cache.assetDirectory.startsWith(tempDir))
83+
assert.deepStrictEqual(cache.location, 'cache')
84+
assert.ok(semver.satisfies(cache.version, supportedLspServerVersions))
85+
86+
/**
87+
* Always make sure the latest version is one patch higher. This stops a problem
88+
* where the fallback can't be used because the latest compatible version
89+
* is equal to the min version, so if the cache isn't valid, then there
90+
* would be no fallback location
91+
*
92+
* Instead, increasing the latest compatible lsp version means we can just
93+
* use the one we downloaded earlier in the test as the fallback
94+
*/
95+
const nextVer = semver.inc(cache.version, 'patch', true)
96+
if (!nextVer) {
97+
throw new Error('Could not increment version')
98+
}
99+
sandbox.stub(ManifestResolver.prototype, 'resolve').resolves({
100+
manifestSchemaVersion: '0.0.0',
101+
artifactId: 'foo',
102+
artifactDescription: 'foo',
103+
isManifestDeprecated: false,
104+
versions: [createVersion(nextVer), createVersion(cache.version)],
105+
})
106+
107+
// fail the next http request for the language server
108+
sandbox.stub(request, 'fetch').returns({
109+
response: Promise.resolve({
110+
ok: false,
111+
}),
112+
} as any)
113+
114+
// Third try - Cache doesn't exist and we couldn't download from the internet, fallback to a local version
115+
const fallback = await resolver.resolve()
116+
117+
assert.ok(fallback.assetDirectory.startsWith(tempDir))
118+
assert.deepStrictEqual(fallback.location, 'fallback')
119+
assert.ok(semver.satisfies(fallback.version, supportedLspServerVersions))
120+
})
121+
})
122+
})

packages/core/src/shared/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,4 @@ export { TabTypeDataMap } from '../amazonq/webview/ui/tabs/constants'
6262
export * from './languageServer/manifestResolver'
6363
export * from './languageServer/lspResolver'
6464
export * from './languageServer/types'
65+
export { default as request } from './request'

packages/core/src/shared/languageServer/lspResolver.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ export class LanguageServerResolver {
7777
throw new ToolkitError('Unable to find a compatible version of the Language Server')
7878
}
7979

80-
const version = path.basename(cacheDirectory)
80+
const version = path.basename(fallbackDirectory)
8181
logger.info(
82-
`Unable to install language server ${latestVersion.serverVersion}. Launching a previous version from ${fallbackDirectory}`
82+
`Unable to install ${this.lsName} language server v${latestVersion.serverVersion}. Launching a previous version from ${fallbackDirectory}`
8383
)
8484

8585
result.location = 'fallback'
@@ -107,6 +107,7 @@ export class LanguageServerResolver {
107107
.filter(([_, filetype]) => filetype === FileType.Directory)
108108
.map(([pathName, _]) => semver.parse(pathName))
109109
.filter((ver): ver is semver.SemVer => ver !== null)
110+
.map((x) => x.version)
110111

111112
const expectedVersion = semver.parse(version)
112113
if (!expectedVersion) {
@@ -115,7 +116,7 @@ export class LanguageServerResolver {
115116

116117
const sortedCachedLspVersions = compatibleLspVersions
117118
.filter((v) => this.isValidCachedVersion(v, cachedVersions, expectedVersion))
118-
.sort((a, b) => semver.compare(a.serverVersion, b.serverVersion))
119+
.sort((a, b) => semver.compare(b.serverVersion, a.serverVersion))
119120

120121
const fallbackDir = (
121122
await Promise.all(sortedCachedLspVersions.map((ver) => this.getValidLocalCacheDirectory(ver)))
@@ -144,9 +145,9 @@ export class LanguageServerResolver {
144145
* A version is considered valid if it exists in the cache and is less than
145146
* or equal to the expected version.
146147
*/
147-
private isValidCachedVersion(version: LspVersion, cachedVersions: semver.SemVer[], expectedVersion: semver.SemVer) {
148+
private isValidCachedVersion(version: LspVersion, cachedVersions: string[], expectedVersion: semver.SemVer) {
148149
const serverVersion = semver.parse(version.serverVersion) as semver.SemVer
149-
return cachedVersions.find((x) => x === serverVersion) && serverVersion <= expectedVersion
150+
return cachedVersions.includes(serverVersion.version) && semver.lte(serverVersion, expectedVersion)
150151
}
151152

152153
/**
@@ -283,9 +284,7 @@ export class LanguageServerResolver {
283284
const latestCompatibleVersion =
284285
this.manifest.versions
285286
.filter((ver) => this.isCompatibleVersion(ver) && this.hasRequiredTargetContent(ver))
286-
.sort((a, b) => {
287-
return a.serverVersion.localeCompare(b.serverVersion)
288-
})[0] ?? undefined
287+
.sort((a, b) => semver.compare(b.serverVersion, a.serverVersion))[0] ?? undefined
289288

290289
if (latestCompatibleVersion === undefined) {
291290
// TODO fix these error range names
@@ -340,12 +339,12 @@ export class LanguageServerResolver {
340339
return version.targets.find((x) => x.arch === arch && x.platform === platform)
341340
}
342341

343-
private defaultDownloadFolder() {
342+
defaultDownloadFolder() {
344343
const applicationSupportFolder = getApplicationSupportFolder()
345344
return path.join(applicationSupportFolder, `aws/toolkits/language-servers/${this.lsName}`)
346345
}
347346

348-
getDownloadDirectory(version: string) {
347+
private getDownloadDirectory(version: string) {
349348
const directory = this._defaultDownloadFolder ?? this.defaultDownloadFolder()
350349
return `${directory}/${version}`
351350
}

0 commit comments

Comments
 (0)