diff --git a/.gitignore b/.gitignore index 41dcb232c82..5b3693c327d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .gradle .intellijPlatform +.kotlin out/ target/ .idea diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index a8ffb0067fb..45238ea7b2f 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -34,8 +34,6 @@ dependencies { testImplementation(libs.junit4) testImplementation(libs.bundles.mockito) testImplementation(gradleTestKit()) - - testRuntimeOnly(libs.junit5.jupiterVintage) } tasks.test { diff --git a/buildSrc/src/main/kotlin/toolkit-publishing-conventions.gradle.kts b/buildSrc/src/main/kotlin/toolkit-publishing-conventions.gradle.kts index 9eeb990c92d..7b5994ab3e4 100644 --- a/buildSrc/src/main/kotlin/toolkit-publishing-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/toolkit-publishing-conventions.gradle.kts @@ -66,3 +66,8 @@ tasks.verifyPlugin { // give each instance its own home dir systemProperty("plugin.verifier.home.dir", temporaryDir) } + +val pluginZip by configurations.creating +artifacts { + add("pluginZip", tasks.buildPlugin) +} diff --git a/buildSrc/src/main/kotlin/toolkit-testing.gradle.kts b/buildSrc/src/main/kotlin/toolkit-testing.gradle.kts index 9c83f195e5d..ca37a88d9c1 100644 --- a/buildSrc/src/main/kotlin/toolkit-testing.gradle.kts +++ b/buildSrc/src/main/kotlin/toolkit-testing.gradle.kts @@ -20,10 +20,9 @@ dependencies { // Everything uses junit4/5 except rider, which uses TestNG testFixturesApi(platform(versionCatalog.findLibrary("junit5-bom").get())) - testFixturesApi(versionCatalog.findLibrary("junit5-jupiterApi").get()) - testFixturesApi(versionCatalog.findLibrary("junit5-jupiterParams").get()) + testFixturesApi(versionCatalog.findLibrary("junit5-jupiter").get()) - testRuntimeOnly(versionCatalog.findLibrary("junit5-jupiterEngine").get()) + testRuntimeOnly(versionCatalog.findLibrary("junit-platform-launcher").get()) testRuntimeOnly(versionCatalog.findLibrary("junit5-jupiterVintage").get()) } diff --git a/buildspec/linuxUiTests.yml b/buildspec/linuxUiTests.yml index c200f631ccd..607736f9f46 100644 --- a/buildspec/linuxUiTests.yml +++ b/buildspec/linuxUiTests.yml @@ -19,7 +19,7 @@ env: phases: install: commands: - - dnf install -y marco mate-media + - dnf install -y marco mate-media e2fsprogs - startDesktop.sh # login to DockerHub so we don't get throttled @@ -45,10 +45,9 @@ phases: credential_source=EcsContainer" - chmod +x gradlew - - ./gradlew -PideProfileName=$ALTERNATIVE_IDE_PROFILE_NAME :sandbox-all:prepareTestIdeUiSandbox --console plain --info - ffmpeg -loglevel quiet -nostdin -f x11grab -video_size ${SCREEN_WIDTH}x${SCREEN_HEIGHT} -i ${DISPLAY} -codec:v libx264 -pix_fmt yuv420p -vf drawtext="fontsize=48:box=1:boxcolor=black@0.75:boxborderw=5:fontcolor=white:x=0:y=h-text_h:text='%{gmtime\:%H\\\\\:%M\\\\\:%S}'" -framerate 12 -g 12 /tmp/screen_recording.mp4 & - - ./gradlew -PideProfileName=$ALTERNATIVE_IDE_PROFILE_NAME uiTestCore coverageReport --console plain --info + - ./gradlew -PideProfileName=$ALTERNATIVE_IDE_PROFILE_NAME :ui-tests-starter:test coverageReport --console plain --info post_build: commands: diff --git a/detekt-rules/build.gradle.kts b/detekt-rules/build.gradle.kts index aa42eb403d7..045281b95aa 100644 --- a/detekt-rules/build.gradle.kts +++ b/detekt-rules/build.gradle.kts @@ -15,7 +15,6 @@ dependencies { // only used to make test work testRuntimeOnly(libs.slf4j.api) - testRuntimeOnly(libs.junit5.jupiterVintage) } tasks.test { diff --git a/gradle.properties b/gradle.properties index 8c527a59a30..93f2e9d68a9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ toolkitVersion=3.45-SNAPSHOT publishToken= publishChannel= -ideProfileName=2024.1 +ideProfileName=2024.2 remoteRobotPort=8080 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 49574a7802d..b6a0855a88d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -91,11 +91,13 @@ jackson-xml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-xm jackson-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson" } jacoco = { module = "org.jacoco:org.jacoco.core", version.ref = "jacoco" } jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" } + +# platfom launcher version selected by BOM +junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } + junit4 = { module = "junit:junit", version.ref = "junit4" } junit5-bom = { module = "org.junit:junit-bom", version.ref = "junit5" } -junit5-jupiterApi = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit5" } -junit5-jupiterEngine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" } -junit5-jupiterParams = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit5" } +junit5-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit5" } junit5-jupiterVintage = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit5" } kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinCoroutines" } kotlin-coroutinesDebug = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-debug", version.ref = "kotlinCoroutines" } diff --git a/noop/build.gradle.kts b/noop/build.gradle.kts new file mode 100644 index 00000000000..54de3e72a44 --- /dev/null +++ b/noop/build.gradle.kts @@ -0,0 +1,5 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// project that does nothing +tasks.register("test") diff --git a/plugins/core/core/build.gradle.kts b/plugins/core/core/build.gradle.kts index 32464f6cb60..00bc6beedc6 100644 --- a/plugins/core/core/build.gradle.kts +++ b/plugins/core/core/build.gradle.kts @@ -24,7 +24,6 @@ dependencies { implementation(libs.commonmark) testImplementation(libs.junit4) - testRuntimeOnly(libs.junit5.jupiterVintage) testRuntimeOnly(project(":plugin-core:resources")) testRuntimeOnly(project(":plugin-core:sdk-codegen")) } diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/sso/bearer/BearerTokenProvider.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/sso/bearer/BearerTokenProvider.kt index 8cdecbc64a2..420613c2d6a 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/sso/bearer/BearerTokenProvider.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/sso/bearer/BearerTokenProvider.kt @@ -48,6 +48,9 @@ interface BearerTokenProvider : SdkTokenProvider, SdkAutoCloseable, ToolkitBeare */ fun currentToken(): AccessToken? + /** + * Not meant to be invoked outside the implementation + */ fun refresh(): AccessToken /** @@ -58,13 +61,13 @@ interface BearerTokenProvider : SdkTokenProvider, SdkAutoCloseable, ToolkitBeare /** * Request provider to interactively request user input to obtain a new [AccessToken] */ - open fun reauthenticate() { + fun reauthenticate() { throw UnsupportedOperationException("Provider is not interactive and cannot reauthenticate") } - open fun supportsLogout() = this is BearerTokenLogoutSupport + fun supportsLogout() = this is BearerTokenLogoutSupport - open fun invalidate() { + fun invalidate() { throw UnsupportedOperationException("Provider is not interactive and cannot be invalidated") } @@ -90,10 +93,9 @@ class InteractiveBearerTokenProvider( val startUrl: String, val region: String, val scopes: List, - id: String, + override val id: String, cache: DiskCache = diskCache, ) : BearerTokenProvider, BearerTokenLogoutSupport, Disposable { - override val id = id override val displayName = ToolkitBearerTokenProvider.ssoDisplayName(startUrl) private val ssoOidcClient: SsoOidcClient = buildUnmanagedSsoOidcClient(region) @@ -107,7 +109,7 @@ class InteractiveBearerTokenProvider( ) private val supplier = CachedSupplier.builder { refreshToken() }.prefetchStrategy(NonBlocking("AWS SSO bearer token refresher")).build() - private val lastToken = AtomicReference() + internal val lastToken = AtomicReference() val pendingAuthorization: PendingAuthorization? get() = accessTokenProvider.authorization @@ -134,6 +136,7 @@ class InteractiveBearerTokenProvider( ) } + // we need to seed CachedSupplier with an initial value, then subsequent calls need to hit the network private fun refreshToken(): RefreshResult { val lastToken = lastToken.get() ?: throw NoTokenInitializedException("Token refresh started before session initialized") val token = if (Duration.between(Instant.now(), lastToken.expiresAt) > Duration.ofMinutes(30)) { @@ -148,6 +151,7 @@ class InteractiveBearerTokenProvider( .build() } + // how we expect consumers to obtain a token override fun resolveToken() = supplier.get() override fun close() { @@ -159,6 +163,7 @@ class InteractiveBearerTokenProvider( close() } + // internal nonsense so we can query the token without triggering a refresh override fun currentToken() = lastToken.get() /** @@ -189,7 +194,7 @@ class InteractiveBearerTokenProvider( class NoTokenInitializedException(message: String) : Exception(message) -public enum class BearerTokenAuthState { +enum class BearerTokenAuthState { AUTHORIZED, NEEDS_REFRESH, NOT_AUTHENTICATED, diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/webview/LoginBrowser.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/webview/LoginBrowser.kt index dc308c1d513..e3c6e81fbab 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/webview/LoginBrowser.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/webview/LoginBrowser.kt @@ -130,6 +130,7 @@ abstract class LoginBrowser( protected val onPendingToken: (InteractiveBearerTokenProvider) -> Unit = { provider -> startBrowserOpenTimer(provider.startUrl, provider.region, provider.scopes) + projectCoroutineScope(project).launch { val authorization = pollForAuthorization(provider) if (authorization != null) { diff --git a/plugins/core/webview/src/q-ui/components/loginOptions.vue b/plugins/core/webview/src/q-ui/components/loginOptions.vue index 9d55100d222..bce0f2a469b 100644 --- a/plugins/core/webview/src/q-ui/components/loginOptions.vue +++ b/plugins/core/webview/src/q-ui/components/loginOptions.vue @@ -37,7 +37,7 @@ export default defineComponent({ login(type: LoginOption) { this.$emit('login', type) }, - emitUiClickTelemetry(elementId: String) { + emitUiClickTelemetry(elementId: string) { this.$emit('emitUiClickTelemetry', elementId) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 21396263a70..c6872181877 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -97,8 +97,15 @@ rootProject.name = "aws-toolkit-jetbrains" include("detekt-rules") include("ui-tests") include("sandbox-all") +include("ui-tests-starter") when (providers.gradleProperty("ideProfileName").get()) { - "2023.3", "2024.1" -> include("tmp-all") + // FIX_WHEN_MIN_IS_242: `tmp-all` test module no longer needed in 242+ + "2023.3", "2024.1" -> { + include("tmp-all") + + // only available 242+ + project(":ui-tests-starter").projectDir = file("noop") + } } /* diff --git a/ui-tests-starter/.gitignore b/ui-tests-starter/.gitignore new file mode 100644 index 00000000000..0fb363385bd --- /dev/null +++ b/ui-tests-starter/.gitignore @@ -0,0 +1 @@ +allure-results/ diff --git a/ui-tests-starter/build.gradle.kts b/ui-tests-starter/build.gradle.kts new file mode 100644 index 00000000000..a3cc765ef05 --- /dev/null +++ b/ui-tests-starter/build.gradle.kts @@ -0,0 +1,60 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import org.jetbrains.intellij.platform.gradle.TestFrameworkType +import software.aws.toolkits.gradle.findFolders +import software.aws.toolkits.gradle.intellij.IdeVersions + +plugins { + id("toolkit-kotlin-conventions") + id("toolkit-intellij-plugin") + + id("org.jetbrains.intellij.platform") +} + +val ideProfile = IdeVersions.ideProfile(project) + +// Add our source sets per IDE profile version (i.e. src-211) +sourceSets { + test { + java.srcDirs(findFolders(project, "tst", ideProfile)) + resources.srcDirs(findFolders(project, "tst-resources", ideProfile)) + } +} + +intellijPlatform { + buildSearchableOptions = false + instrumentCode = false +} + +val testPlugins by configurations.registering + +dependencies { + testImplementation(platform("com.jetbrains.intellij.tools:ide-starter-squashed")) + // should really be set by the BOM, but too much work to figure out right now + testImplementation("org.kodein.di:kodein-di-jvm:7.20.2") + intellijPlatform { + intellijIdeaCommunity(IdeVersions.ideProfile(providers).map { it.name }) + + testFramework(TestFrameworkType.Starter) + } + + testPlugins(project(":plugin-amazonq", "pluginZip")) + testPlugins(project(":plugin-core", "pluginZip")) +} + +tasks.test { + dependsOn(testPlugins) + + useJUnitPlatform() + + systemProperty("ui.test.plugins", testPlugins.get().asPath) +} + +// hack to disable ui tests in ./gradlew check +val action = Action { + if (hasTask(tasks.test.get())) { + tasks.test.get().enabled = false + } +} +gradle.taskGraph.whenReady(action) diff --git a/ui-tests-starter/tst-241-242/software/aws/toolkits/jetbrains/uitests/TestCIServer.kt b/ui-tests-starter/tst-241-242/software/aws/toolkits/jetbrains/uitests/TestCIServer.kt new file mode 100644 index 00000000000..625e02311d7 --- /dev/null +++ b/ui-tests-starter/tst-241-242/software/aws/toolkits/jetbrains/uitests/TestCIServer.kt @@ -0,0 +1,29 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.uitests + +import com.intellij.ide.starter.ci.CIServer +import java.nio.file.Path + +object TestCIServer : CIServer { + override val isBuildRunningOnCI: Boolean = System.getenv("CI").toBoolean() == true + override val buildNumber: String = "" + override val branchName: String = "" + override val buildParams: Map = mapOf() + + override fun publishArtifact(source: Path, artifactPath: String, artifactName: String) { + } + + override fun reportTestFailure(testName: String, message: String, details: String) { + println("test: $testName") + println("message: $message") + println("details: $details") + error(message) + } + + override fun ignoreTestFailure(testName: String, message: String, details: String) { + } + + override fun isTestFailureShouldBeIgnored(message: String) = false +} diff --git a/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/TestCIServer.kt b/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/TestCIServer.kt new file mode 100644 index 00000000000..557f4059254 --- /dev/null +++ b/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/TestCIServer.kt @@ -0,0 +1,29 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.uitests + +import com.intellij.ide.starter.ci.CIServer +import java.nio.file.Path + +object TestCIServer : CIServer { + override val isBuildRunningOnCI: Boolean = System.getenv("CI").toBoolean() == true + override val buildNumber: String = "" + override val branchName: String = "" + override val buildParams: Map = mapOf() + + override fun publishArtifact(source: Path, artifactPath: String, artifactName: String) { + } + + override fun reportTestFailure(testName: String, message: String, details: String, linkToLogs: String?) { + println("test: $testName") + println("message: $message") + println("details: $details") + error(message) + } + + override fun ignoreTestFailure(testName: String, message: String) { + } + + override fun isTestFailureShouldBeIgnored(message: String) = false +} diff --git a/ui-tests-starter/tst/software/aws/toolkits/jetbrains/uitests/OfflineAmazonQInlineCompletionTest.kt b/ui-tests-starter/tst/software/aws/toolkits/jetbrains/uitests/OfflineAmazonQInlineCompletionTest.kt new file mode 100644 index 00000000000..669a781deb6 --- /dev/null +++ b/ui-tests-starter/tst/software/aws/toolkits/jetbrains/uitests/OfflineAmazonQInlineCompletionTest.kt @@ -0,0 +1,99 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.aws.toolkits.jetbrains.uitests + +import com.intellij.driver.sdk.openFile +import com.intellij.driver.sdk.ui.ui +import com.intellij.driver.sdk.waitForProjectOpen +import com.intellij.ide.starter.ci.CIServer +import com.intellij.ide.starter.di.di +import com.intellij.ide.starter.driver.engine.runIdeWithDriver +import com.intellij.ide.starter.ide.IdeProductProvider +import com.intellij.ide.starter.junit5.hyphenateWithClass +import com.intellij.ide.starter.models.TestCase +import com.intellij.ide.starter.project.LocalProjectInfo +import com.intellij.ide.starter.runner.CurrentTestMethod +import com.intellij.ide.starter.runner.Starter +import org.junit.jupiter.api.Test +import org.kodein.di.DI +import org.kodein.di.bindSingleton +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.io.path.createParentDirectories +import kotlin.io.path.writeText + +class OfflineAmazonQInlineCompletionTest { + init { + di = DI { + extend(di) + bindSingleton(overrides = true) { TestCIServer } + } + } + + @Test + fun `completion request with expired credentials does not freeze EDT`() { + val testCase = TestCase( + IdeProductProvider.IC, + LocalProjectInfo( + Paths.get("tstData", "Hello") + ) + ).useRelease("2024.2") + Paths.get(System.getProperty("user.home"), ".aws", "sso", "cache", "ee1d2538cb8d358377d7661466c866af747a8a3f.json") + .createParentDirectories() + .writeText( + """ + { + "clientId": "DummyId", + "clientSecret": "DummySecret", + "expiresAt": "3070-01-01T00:00:00Z", + "scopes": [ + "scope1", + "scope2" + ], + "issuerUrl": "1", + "region": "2", + "clientType": "public", + "grantTypes": [ + "authorization_code", + "refresh_token" + ], + "redirectUris": [ + "http://127.0.0.1/oauth/callback" + ] + } + """.trimIndent() + ) + Paths.get(System.getProperty("user.home"), ".aws", "sso", "cache", "d3b447f809607422aac1470dd17fbb32e358cdb3.json") + .writeText( + """ + { + "issuerUrl": "https://example.awsapps.com/start", + "region": "us-east-1", + "accessToken": "DummyAccessToken", + "refreshToken": "RefreshToken", + "createdAt": "1970-01-01T00:00:00Z", + "expiresAt": "1970-01-01T00:00:00Z" + } + """.trimIndent() + ) + Starter.newContext(CurrentTestMethod.hyphenateWithClass(), testCase).apply { + System.getProperty("ui.test.plugins").split(File.pathSeparator).forEach { path -> + pluginConfigurator.installPluginFromPath( + Path.of(path) + ) + } + + copyExistingConfig(Paths.get("tstData", "config")) + updateGeneralSettings() + }.runIdeWithDriver() + .useDriverAndCloseIde { + waitForProjectOpen() + openFile("Example.java") + ui.keyboard { + // left meta + c + repeat(5) { hotKey(18, 67) } + } + } + } +} diff --git a/ui-tests-starter/tstData/Hello/Example.java b/ui-tests-starter/tstData/Hello/Example.java new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ui-tests-starter/tstData/config/options/aws.xml b/ui-tests-starter/tstData/config/options/aws.xml new file mode 100644 index 00000000000..66330aa8bb8 --- /dev/null +++ b/ui-tests-starter/tstData/config/options/aws.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/ui-tests-starter/tstData/config/options/ide.general.xml b/ui-tests-starter/tstData/config/options/ide.general.xml new file mode 100644 index 00000000000..28da7b923dd --- /dev/null +++ b/ui-tests-starter/tstData/config/options/ide.general.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/ui-tests-starter/tstData/config/options/proxy.settings.xml b/ui-tests-starter/tstData/config/options/proxy.settings.xml new file mode 100644 index 00000000000..f7b1482215e --- /dev/null +++ b/ui-tests-starter/tstData/config/options/proxy.settings.xml @@ -0,0 +1,8 @@ + + + + diff --git a/ui-tests/build.gradle.kts b/ui-tests/build.gradle.kts index dcbf681de03..4888618c140 100644 --- a/ui-tests/build.gradle.kts +++ b/ui-tests/build.gradle.kts @@ -17,7 +17,6 @@ dependencies { testImplementation(project(":plugin-core:core")) testImplementation(project(path = ":plugin-core:core", configuration = "testArtifacts")) testImplementation(libs.kotlin.coroutines) - testImplementation(libs.junit5.jupiterApi) testImplementation(libs.intellijRemoteFixtures) testImplementation(libs.intellijRemoteRobot) testImplementation(libs.aws.cloudformation) @@ -29,8 +28,6 @@ dependencies { testImplementation(libs.commons.io) // match version declared by intellijRemoteRobot testImplementation("com.squareup.okhttp3:okhttp:4.12.0") - - testRuntimeOnly(libs.junit5.jupiterEngine) } // don't run gui tests as part of check