You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

356 lines
14 KiB

/*
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.test.tests.integration
import org.gradle.util.GradleVersion
import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.PreviewLogger
import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.RemoteConnection
import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.receiveConfigFromGradle
import org.jetbrains.compose.experimental.internal.kotlinVersionNumbers
import org.jetbrains.compose.internal.utils.Arch
import org.jetbrains.compose.internal.utils.OS
import org.jetbrains.compose.internal.utils.currentArch
import org.jetbrains.compose.internal.utils.currentOS
import org.jetbrains.compose.test.utils.*
import org.junit.jupiter.api.Assumptions
import java.net.ServerSocket
import java.net.Socket
import java.net.SocketTimeoutException
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.thread
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
import java.io.File
class GradlePluginTest : GradlePluginTestBase() {
private data class IosTestEnv(
val targetBuildDir: File,
val appDir: File,
val envVars: Map<String, String>
)
enum class IosPlatform(val id: String) {
SIMULATOR("iphonesimulator"), IOS("iphoneos")
}
enum class IosArch(val id: String) {
X64("x86_64"), ARM64("arm64")
}
enum class IosBuildConfiguration(val id: String) {
DEBUG("Debug"), RELEASE("Release")
}
private fun iosTestEnv(
platform: IosPlatform = IosPlatform.SIMULATOR,
arch: IosArch = IosArch.X64,
configuration: IosBuildConfiguration = IosBuildConfiguration.DEBUG
): IosTestEnv {
val targetBuildDir = testWorkDir.resolve("build/ios/${configuration.id}-${platform.id}").apply { mkdirs() }
val appDir = targetBuildDir.resolve("App.app").apply { mkdirs() }
val envVars = mapOf(
"PLATFORM_NAME" to platform.id,
"ARCHS" to arch.id,
"BUILT_PRODUCTS_DIR" to targetBuildDir.canonicalPath,
"CONTENTS_FOLDER_PATH" to appDir.name,
)
return IosTestEnv(
targetBuildDir = targetBuildDir,
appDir = appDir,
envVars = envVars
)
}
@Test
fun iosResources() {
Assumptions.assumeTrue(currentOS == OS.MacOS)
val iosTestEnv = iosTestEnv()
val testEnv = defaultTestEnvironment.copy(
additionalEnvVars = iosTestEnv.envVars
)
with(TestProject(TestProjects.iosResources, testEnv)) {
gradle(":embedAndSignAppleFrameworkForXcode", "--dry-run").checks {
// This test is not intended to actually run embedAndSignAppleFrameworkForXcode.
// Instead, it should check that embedAndSign depends on syncComposeResources using dry run
check.taskSkipped(":syncComposeResourcesForIos")
check.taskSkipped(":embedAndSignAppleFrameworkForXcode")
}
gradle(":syncComposeResourcesForIos").checks {
check.taskSuccessful(":syncComposeResourcesForIos")
iosTestEnv.appDir.resolve("compose-resources/compose-multiplatform.xml").checkExists()
}
}
}
@Test
fun iosTestResources() {
Assumptions.assumeTrue(currentOS == OS.MacOS)
with(testProject(TestProjects.iosResources)) {
gradle(":linkDebugTestIosX64", "--dry-run").checks {
check.taskSkipped(":copyTestComposeResourcesForIosX64")
check.taskSkipped(":linkDebugTestIosX64")
}
gradle(":copyTestComposeResourcesForIosX64").checks {
check.taskSuccessful(":copyTestComposeResourcesForIosX64")
file("build/bin/iosX64/debugTest/compose-resources/compose-multiplatform.xml").checkExists()
}
}
}
@Test
fun iosMokoResources() {
Assumptions.assumeTrue(currentOS == OS.MacOS)
val iosTestEnv = iosTestEnv()
val testEnv = defaultTestEnvironment.copy(
additionalEnvVars = iosTestEnv.envVars
)
with(testProject(TestProjects.iosMokoResources, testEnv)) {
gradle(
":embedAndSignAppleFrameworkForXcode",
":copyFrameworkResourcesToApp",
"--dry-run",
"--info"
).checks {
// This test is not intended to actually run embedAndSignAppleFrameworkForXcode.
// Instead, it should check that the sync disables itself.
check.logContains("Compose Multiplatform resource management for iOS is disabled")
check.logDoesntContain(":syncComposeResourcesForIos")
}
}
}
@Test
fun nativeCacheKind() {
Assumptions.assumeTrue(currentOS == OS.MacOS)
val task = if (currentArch == Arch.X64) {
":subproject:linkDebugFrameworkIosX64"
} else {
":subproject:linkDebugFrameworkIosArm64"
}
// Note: we used to test with kotlin version 1.9.0 and 1.9.10 too,
// but since we now use Compose core libs (1.6.0-dev-1340 and newer) built using kotlin 1.9.21,
// the compiler crashed (older k/native doesn't support libs built using newer k/native):
// e: kotlin.NotImplementedError: Generation of stubs for class org.jetbrains.kotlin.ir.symbols.impl.IrTypeParameterPublicSymbolImpl is not supported yet
if (kotlinVersionNumbers(defaultTestEnvironment.kotlinVersion) >= KotlinVersion(1, 9, 20)) {
testWorkDir.deleteRecursively()
testWorkDir.mkdirs()
val project = TestProject(
TestProjects.nativeCacheKind,
defaultTestEnvironment.copy(useGradleConfigurationCache = false)
)
with(project) {
gradle(task, "--info").checks {
check.taskSuccessful(task)
check.logContains("-Xauto-cache-from=")
}
}
}
}
@Test
fun nativeCacheKindError() {
Assumptions.assumeTrue(currentOS == OS.MacOS)
fun withNativeCacheKindErrorProject(kotlinVersion: String, fn: TestProject.() -> Unit) {
with(testProject(
TestProjects.nativeCacheKindError,
defaultTestEnvironment.copy(kotlinVersion = kotlinVersion)
)) {
fn()
testWorkDir.deleteRecursively()
testWorkDir.mkdirs()
}
}
fun testKotlinVersion(kotlinVersion: String) {
Fix reporting configuration problems with configuration cache (#3596) Sometimes we need to report warnings during the configuration phase. For example, when Androidx Compose Compiler is used with non-JVM targets (e.g. iOS/js), we want to warn users that using non-JB compiler with non-JVM targets is not supported. The default way of reporting warnings in Gradle is using a logger. For example we could write something like: ``` abstract class ComposePlugin : Plugin<Project> { override fun apply(project: Project) { if (project.hasNonJvmTargets() && project.usesNonJBComposeCompiler()) { project.logger.warn("...") } } } ``` This approach has a few issues: 1. When the Configuration Cache is enabled, project's configuration might get skipped altogether, and the warning won't be printed. 2. If a project contains multiple Gradle modules (subprojects), the warning might be printed multiple times. That might be OK for some warnings. But repeating exactly the same warning 10s or 100s is unnecessary. The only way to share the state between Gradle modules, while preserving compatibility with the Configuration Cache, is to define Gradle Build Service. In 1.5.0 we used Gradle Build Service mechanism for both warnings. However, I did not know that: * only the service's parameters are persisted in the Configuration Cache. The service itself is not persisted. * if a service instance is materialized during the configuration phase, then all changes made to its parameters will not be visible to that particular instance (but they will be visible to the next instance). So the only way to report diagnostics with configuration cache without repetition is to define a service that is not materialized during the configuration phase (i.e. serviceProvider.get() is not called), add to add warnings to a set property of the service. This change implements that. Resolves #3595
9 months ago
val args = arrayOf("help")
val commonPartOfWarning = "Compose Multiplatform Gradle plugin manages this property automatically"
withNativeCacheKindErrorProject(kotlinVersion = kotlinVersion) {
gradle(*args).checks {
check.logDoesntContain("Error: 'kotlin.native.cacheKind")
check.logDoesntContain(commonPartOfWarning)
}
}
withNativeCacheKindErrorProject(kotlinVersion = kotlinVersion) {
gradleFailure(*args, "-Pkotlin.native.cacheKind=none").checks {
check.logContains("Error: 'kotlin.native.cacheKind' is explicitly set to 'none'")
check.logContains(commonPartOfWarning)
}
Fix reporting configuration problems with configuration cache (#3596) Sometimes we need to report warnings during the configuration phase. For example, when Androidx Compose Compiler is used with non-JVM targets (e.g. iOS/js), we want to warn users that using non-JB compiler with non-JVM targets is not supported. The default way of reporting warnings in Gradle is using a logger. For example we could write something like: ``` abstract class ComposePlugin : Plugin<Project> { override fun apply(project: Project) { if (project.hasNonJvmTargets() && project.usesNonJBComposeCompiler()) { project.logger.warn("...") } } } ``` This approach has a few issues: 1. When the Configuration Cache is enabled, project's configuration might get skipped altogether, and the warning won't be printed. 2. If a project contains multiple Gradle modules (subprojects), the warning might be printed multiple times. That might be OK for some warnings. But repeating exactly the same warning 10s or 100s is unnecessary. The only way to share the state between Gradle modules, while preserving compatibility with the Configuration Cache, is to define Gradle Build Service. In 1.5.0 we used Gradle Build Service mechanism for both warnings. However, I did not know that: * only the service's parameters are persisted in the Configuration Cache. The service itself is not persisted. * if a service instance is materialized during the configuration phase, then all changes made to its parameters will not be visible to that particular instance (but they will be visible to the next instance). So the only way to report diagnostics with configuration cache without repetition is to define a service that is not materialized during the configuration phase (i.e. serviceProvider.get() is not called), add to add warnings to a set property of the service. This change implements that. Resolves #3595
9 months ago
gradleFailure(*args, "-Pkotlin.native.cacheKind=none").checks {
check.logContains("Error: 'kotlin.native.cacheKind' is explicitly set to 'none'")
check.logContains(commonPartOfWarning)
Fix reporting configuration problems with configuration cache (#3596) Sometimes we need to report warnings during the configuration phase. For example, when Androidx Compose Compiler is used with non-JVM targets (e.g. iOS/js), we want to warn users that using non-JB compiler with non-JVM targets is not supported. The default way of reporting warnings in Gradle is using a logger. For example we could write something like: ``` abstract class ComposePlugin : Plugin<Project> { override fun apply(project: Project) { if (project.hasNonJvmTargets() && project.usesNonJBComposeCompiler()) { project.logger.warn("...") } } } ``` This approach has a few issues: 1. When the Configuration Cache is enabled, project's configuration might get skipped altogether, and the warning won't be printed. 2. If a project contains multiple Gradle modules (subprojects), the warning might be printed multiple times. That might be OK for some warnings. But repeating exactly the same warning 10s or 100s is unnecessary. The only way to share the state between Gradle modules, while preserving compatibility with the Configuration Cache, is to define Gradle Build Service. In 1.5.0 we used Gradle Build Service mechanism for both warnings. However, I did not know that: * only the service's parameters are persisted in the Configuration Cache. The service itself is not persisted. * if a service instance is materialized during the configuration phase, then all changes made to its parameters will not be visible to that particular instance (but they will be visible to the next instance). So the only way to report diagnostics with configuration cache without repetition is to define a service that is not materialized during the configuration phase (i.e. serviceProvider.get() is not called), add to add warnings to a set property of the service. This change implements that. Resolves #3595
9 months ago
}
}
withNativeCacheKindErrorProject(kotlinVersion = kotlinVersion) {
gradleFailure(*args, "-Pkotlin.native.cacheKind=static").checks {
check.logContains("Error: 'kotlin.native.cacheKind' is explicitly set to 'static'")
check.logContains(commonPartOfWarning)
}
}
withNativeCacheKindErrorProject(kotlinVersion = kotlinVersion) {
gradleFailure(*args, "-Pkotlin.native.cacheKind.iosX64=none").checks {
check.logContains("Error: 'kotlin.native.cacheKind.iosX64' is explicitly set to 'none'")
check.logContains(commonPartOfWarning)
}
}
withNativeCacheKindErrorProject(kotlinVersion = kotlinVersion) {
gradleFailure(*args, "-Pkotlin.native.cacheKind.iosX64=static").checks {
check.logContains("Error: 'kotlin.native.cacheKind.iosX64' is explicitly set to 'static'")
check.logContains(commonPartOfWarning)
}
}
}
testKotlinVersion("1.9.21")
}
@Test
fun skikoWasm() = with(
testProject(
TestProjects.skikoWasm,
// configuration cache is disabled as a temporary workaround for KT-58057
// todo: enable once KT-58057 is fixed
testEnvironment = defaultTestEnvironment.copy(useGradleConfigurationCache = false)
)
) {
fun jsCanvasEnabled(value: Boolean) {
modifyGradleProperties { put("org.jetbrains.compose.experimental.jscanvas.enabled", value.toString()) }
}
jsCanvasEnabled(false)
gradleFailure(":build").checks {
check.logContains("ERROR: Compose targets '[jscanvas]' are experimental and may have bugs!")
}
jsCanvasEnabled(true)
gradle(":build").checks {
check.taskSuccessful(":unpackSkikoWasmRuntimeJs")
check.taskSuccessful(":compileKotlinJs")
}
}
@Test
fun newAndroidTarget() {
Assumptions.assumeTrue(defaultTestEnvironment.parsedGradleVersion >= GradleVersion.version("8.0.0"))
with(testProject(TestProjects.newAndroidTarget)) {
gradle("build", "--dry-run").checks {
}
}
}
@Test
fun jsMppIsNotBroken() =
with(
testProject(
TestProjects.jsMpp,
testEnvironment = defaultTestEnvironment.copy(
kotlinVersion = TestProperties.composeJsCompilerCompatibleKotlinVersion
)
)
) {
gradle(":compileKotlinJs").checks {
check.taskSuccessful(":compileKotlinJs")
}
}
@Test
fun configurePreview() {
val isAlive = AtomicBoolean(true)
val receivedConfigCount = AtomicInteger(0)
val port = AtomicInteger(-1)
val connectionThread = thread {
val serverSocket = ServerSocket(0).apply {
soTimeout = 10_000
}
port.set(serverSocket.localPort)
serverSocket.use {
while (isAlive.get()) {
try {
val socket = serverSocket.accept()
val connection = RemoteConnectionImpl(socket, TestPreviewLogger("SERVER"))
val previewConfig = connection.receiveConfigFromGradle()
if (previewConfig != null) {
receivedConfigCount.incrementAndGet()
}
} catch (e: Exception) {
if (!isAlive.get()) break
if (e !is SocketTimeoutException) {
e.printStackTrace()
throw e
}
}
}
}
}
val startTimeNs = System.nanoTime()
while (port.get() <= 0) {
val elapsedTimeNs = System.nanoTime() - startTimeNs
val elapsedTimeMs = elapsedTimeNs / 1_000_000L
if (elapsedTimeMs > 10_000) {
error("Server socket initialization timeout!")
}
Thread.sleep(200)
}
try {
testConfigureDesktopPreviewImpl(port.get())
} finally {
isAlive.set(false)
connectionThread.interrupt()
connectionThread.join(5000)
}
val expectedReceivedConfigCount = 2
val actualReceivedConfigCount = receivedConfigCount.get()
check(actualReceivedConfigCount == 2) {
"Expected to receive $expectedReceivedConfigCount preview configs, got $actualReceivedConfigCount"
}
}
private fun testConfigureDesktopPreviewImpl(port: Int) {
check(port > 0) { "Invalid port: $port" }
with(testProject(TestProjects.jvmPreview)) {
val portProperty = "-Pcompose.desktop.preview.ide.port=$port"
val previewTargetProperty = "-Pcompose.desktop.preview.target=PreviewKt.ExamplePreview"
val jvmTask = ":jvm:configureDesktopPreview"
gradle(jvmTask, portProperty, previewTargetProperty).checks {
check.taskSuccessful(jvmTask)
}
val mppTask = ":mpp:configureDesktopPreviewDesktop"
gradle(mppTask, portProperty, previewTargetProperty).checks {
check.taskSuccessful(mppTask)
}
}
}
private class TestPreviewLogger(private val prefix: String) : PreviewLogger() {
override val isEnabled: Boolean
get() = true
override fun log(s: String) {
println("$prefix: $s")
}
}
private fun RemoteConnectionImpl(
socket: Socket, logger: PreviewLogger
): RemoteConnection {
val connectionClass = Class.forName("org.jetbrains.compose.desktop.ui.tooling.preview.rpc.RemoteConnectionImpl")
val constructor = connectionClass.constructors.first {
it.parameterCount == 3
}
return constructor.newInstance(socket, logger, {}) as RemoteConnection
}
}