-
Notifications
You must be signed in to change notification settings - Fork 273
feat(amazonq): Added LSP Manifest manager related changes #5387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 17 commits
c4aa8cb
d1ca7d1
600f91a
cad10ef
2607eec
b0e9648
973b710
2641472
fef3b06
539cb7e
563ca08
8f44478
30f8dc6
eef3e59
bc25fb9
7ac76b0
a28dca7
20e7451
a970509
44f5c7b
60c4b1c
65f6f68
224289d
3e1bc1e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| // Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts | ||
|
|
||
| import com.intellij.openapi.util.SystemInfo | ||
| import com.intellij.openapi.util.text.StringUtil | ||
| import com.intellij.util.io.DigestUtil | ||
| import com.intellij.util.system.CpuArch | ||
| import java.nio.file.Path | ||
| import java.nio.file.Paths | ||
|
|
||
| fun getToolkitsCommonCacheRoot(): Path = when { | ||
|
Check warning on line 13 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt
|
||
| SystemInfo.isWindows -> { | ||
| Paths.get(System.getenv("LOCALAPPDATA")) | ||
|
Check warning on line 15 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt
|
||
| } | ||
| SystemInfo.isMac -> { | ||
| Paths.get(System.getProperty("user.home"), "Library", "Caches") | ||
|
Check warning on line 18 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt
|
||
| } | ||
| else -> { | ||
| Paths.get(System.getProperty("user.home"), ".cache") | ||
|
Check warning on line 21 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt
|
||
| } | ||
| } | ||
|
Check warning on line 23 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt
|
||
|
|
||
| fun getCurrentOS(): String = when { | ||
|
Check warning on line 25 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt
|
||
| SystemInfo.isWindows -> "windows" | ||
| SystemInfo.isMac -> "darwin" | ||
| else -> "linux" | ||
| } | ||
|
Check warning on line 29 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt
|
||
|
|
||
| fun getCurrentArchitecture() = when { | ||
|
Check warning on line 31 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt
|
||
|
||
| CpuArch.CURRENT == CpuArch.X86_64 -> "x64" | ||
| else -> "arm64" | ||
|
||
| } | ||
|
Check warning on line 34 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt
|
||
|
|
||
| fun generateMD5Hash(filePath: Path): String { | ||
| val messageDigest = DigestUtil.md5() | ||
| DigestUtil.updateContentHash(messageDigest, filePath) | ||
| return StringUtil.toHexString(messageDigest.digest()) | ||
|
Check warning on line 39 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt
|
||
| } | ||
LokeshDogga13 marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| // Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts | ||
|
|
||
| import org.assertj.core.util.VisibleForTesting | ||
| import software.aws.toolkits.core.utils.deleteIfExists | ||
| import software.aws.toolkits.core.utils.error | ||
| import software.aws.toolkits.core.utils.exists | ||
| import software.aws.toolkits.core.utils.getLogger | ||
| import software.aws.toolkits.core.utils.info | ||
| import software.aws.toolkits.core.utils.readText | ||
| import software.aws.toolkits.jetbrains.core.getETagFromUrl | ||
| import software.aws.toolkits.jetbrains.core.getTextFromUrl | ||
| import software.aws.toolkits.jetbrains.core.saveFileFromUrl | ||
| import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager | ||
| import java.nio.file.Path | ||
|
|
||
| class ManifestFetcher { | ||
|
Check warning on line 19 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
|
||
|
|
||
| private val lspManifestUrl = "https://aws-toolkit-language-servers.amazonaws.com/codewhisperer/0/manifest.jso" | ||
| private val manifestManager = ManifestManager() | ||
| private val lspManifestFilePath: Path = getToolkitsCommonCacheRoot().resolve("aws").resolve("toolkits").resolve("language-servers") | ||
| .resolve("jetbrains-lsp-manifest.json") | ||
|
Check warning on line 24 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
|
|
||
| companion object { | ||
| private val logger = getLogger<ManifestFetcher>() | ||
|
Check warning on line 27 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
| } | ||
|
|
||
| /** | ||
| * Method which will be used to fetch latest manifest. | ||
| * */ | ||
| fun fetch(): ManifestManager.Manifest? { | ||
| val localManifest = fetchManifestFromLocal() | ||
|
Check warning on line 34 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
| if (localManifest != null) { | ||
| return localManifest | ||
|
Check warning on line 36 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
| } | ||
| return fetchManifestFromRemote() | ||
|
Check warning on line 38 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
| } | ||
|
|
||
| @VisibleForTesting | ||
| internal fun fetchManifestFromRemote(): ManifestManager.Manifest? { | ||
| val manifest: ManifestManager.Manifest? | ||
| try { | ||
| val manifestString = getTextFromUrl(lspManifestUrl) | ||
|
Check warning on line 45 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
| manifest = manifestManager.readManifestFile(manifestString) ?: return null | ||
| } catch (e: Exception) { | ||
| logger.error(e) { "error fetching lsp manifest from remote URL ${e.message}" } | ||
| return null | ||
|
Check warning on line 49 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
| } | ||
| if (manifest.isManifestDeprecated == true) { | ||
| logger.info { "Manifest is deprecated" } | ||
| return null | ||
|
Check warning on line 53 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
| } | ||
| updateManifestCache() | ||
| logger.info { "Using manifest found from remote URL" } | ||
| return manifest | ||
|
Check warning on line 57 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
| } | ||
|
|
||
| private fun updateManifestCache() { | ||
| try { | ||
| saveFileFromUrl(lspManifestUrl, lspManifestFilePath) | ||
| } catch (e: Exception) { | ||
| logger.error(e) { "error occurred while saving lsp manifest to local cache ${e.message}" } | ||
|
Check warning on line 64 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
| } | ||
| } | ||
|
Check warning on line 66 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
|
|
||
| @VisibleForTesting | ||
| internal fun fetchManifestFromLocal(): ManifestManager.Manifest? { | ||
| val localETag = getManifestETagFromLocal() | ||
| val remoteETag = getManifestETagFromUrl() | ||
|
Check warning on line 71 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
| // If local and remote have same ETag, we can re-use the manifest file from local to fetch artifacts. | ||
| // If remote manifest is null or system is offline, re-use localManifest | ||
| if ((localETag != null && remoteETag != null && localETag == remoteETag) or (localETag != null && remoteETag == null)) { | ||
| try { | ||
| val manifestContent = lspManifestFilePath.readText() | ||
| val manifest = manifestManager.readManifestFile(manifestContent) | ||
|
Check warning on line 77 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
| if (manifest != null) return manifest | ||
| lspManifestFilePath.deleteIfExists() // delete manifest if it fails to de-serialize | ||
| } catch (e: Exception) { | ||
| logger.error(e) { "error reading lsp manifest file from local ${e.message}" } | ||
| return null | ||
|
Check warning on line 82 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
| } | ||
| } | ||
| return null | ||
|
Check warning on line 85 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
| } | ||
|
|
||
| private fun getManifestETagFromLocal(): String? { | ||
| if (lspManifestFilePath.exists()) { | ||
| return generateMD5Hash(lspManifestFilePath) | ||
|
Check warning on line 90 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
| } | ||
| return null | ||
|
Check warning on line 92 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
| } | ||
|
|
||
| private fun getManifestETagFromUrl(): String? { | ||
| try { | ||
| val actualETag = getETagFromUrl(lspManifestUrl) | ||
| return actualETag.trim('"') | ||
| } catch (e: Exception) { | ||
| logger.error(e) { "error fetching ETag of lsp manifest from url." } | ||
|
Check warning on line 100 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
| } | ||
| return null | ||
|
Check warning on line 102 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| // Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts | ||
|
|
||
| import io.mockk.every | ||
| import io.mockk.mockkStatic | ||
| import org.assertj.core.api.Assertions.assertThat | ||
| import org.jetbrains.annotations.TestOnly | ||
| import org.junit.jupiter.api.BeforeEach | ||
| import org.junit.jupiter.api.Test | ||
| import org.mockito.kotlin.atLeastOnce | ||
| import org.mockito.kotlin.never | ||
| import org.mockito.kotlin.reset | ||
| import org.mockito.kotlin.spy | ||
| import org.mockito.kotlin.verify | ||
| import org.mockito.kotlin.whenever | ||
| import software.aws.toolkits.jetbrains.core.getTextFromUrl | ||
| import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager | ||
|
|
||
| @TestOnly | ||
| class ManifestFetcherTest { | ||
|
|
||
| private lateinit var manifestFetcher: ManifestFetcher | ||
| private lateinit var manifest: ManifestManager.Manifest | ||
| private lateinit var manifestManager: ManifestManager | ||
|
|
||
| @BeforeEach | ||
| fun setup() { | ||
| manifestFetcher = spy(ManifestFetcher()) | ||
| manifestManager = spy(ManifestManager()) | ||
| manifest = ManifestManager.Manifest() | ||
| } | ||
|
|
||
| @Test | ||
| fun `should return null when both local and remote manifests are null`() { | ||
| whenever(manifestFetcher.fetchManifestFromLocal()).thenReturn(null) | ||
| whenever(manifestFetcher.fetchManifestFromRemote()).thenReturn(null) | ||
|
|
||
| assertThat(manifestFetcher.fetch()).isNull() | ||
| } | ||
|
|
||
| @Test | ||
| fun `should return valid result from local should not execute remote method`() { | ||
| reset(manifestFetcher) | ||
| whenever(manifestFetcher.fetchManifestFromLocal()).thenReturn(manifest) | ||
|
|
||
| assertThat(manifestFetcher.fetch()).isNotNull().isEqualTo(manifest) | ||
| verify(manifestFetcher, atLeastOnce()).fetchManifestFromLocal() | ||
| verify(manifestFetcher, never()).fetchManifestFromRemote() | ||
| } | ||
|
|
||
| @Test | ||
| fun `should return valid result from remote`() { | ||
| whenever(manifestFetcher.fetchManifestFromLocal()).thenReturn(null) | ||
| whenever(manifestFetcher.fetchManifestFromRemote()).thenReturn(manifest) | ||
|
|
||
| assertThat(manifestFetcher.fetch()).isNotNull().isEqualTo(manifest) | ||
| verify(manifestFetcher, atLeastOnce()).fetchManifestFromLocal() | ||
| verify(manifestFetcher, atLeastOnce()).fetchManifestFromRemote() | ||
| } | ||
|
|
||
| @Test | ||
| fun `fetchManifestFromRemote should return null due to invalid manifestString`() { | ||
| mockkStatic("software.aws.toolkits.jetbrains.core.HttpUtilsKt") | ||
| every { getTextFromUrl(any()) } returns "ManifestContent" | ||
|
|
||
| whenever(manifestManager.readManifestFile("")).thenReturn(null) | ||
|
|
||
| assertThat(manifestFetcher.fetchManifestFromRemote()).isNull() | ||
| } | ||
|
|
||
| @Test | ||
| fun `fetchManifestFromRemote should return manifest and update manifest`() { | ||
| val validManifest = ManifestManager.Manifest(manifestSchemaVersion = "1.0") | ||
| mockkStatic("software.aws.toolkits.jetbrains.core.HttpUtilsKt") | ||
|
|
||
| every { getTextFromUrl(any()) } returns "{ \"manifestSchemaVersion\": \"1.0\" }" | ||
|
|
||
| val result = manifestFetcher.fetchManifestFromRemote() | ||
| assertThat(result).isNotNull().isEqualTo(validManifest) | ||
| } | ||
|
|
||
| @Test | ||
| fun `fetchManifestFromRemote should return null if manifest is deprecated`() { | ||
| mockkStatic("software.aws.toolkits.jetbrains.core.HttpUtilsKt") | ||
| every { getTextFromUrl(any()) } returns "ManifestContent" | ||
|
|
||
| val deprecatedManifest = ManifestManager.Manifest(isManifestDeprecated = true) | ||
|
|
||
| whenever(manifestManager.readManifestFile("")).thenReturn(deprecatedManifest) | ||
|
|
||
| assertThat(manifestFetcher.fetchManifestFromRemote()).isNull() | ||
| } | ||
|
|
||
| @Test | ||
| fun `fetchManifestFromLocal should return null`() { | ||
| assertThat(manifestFetcher.fetchManifestFromLocal()).isNull() | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.