Fix configuration cache issues (#2607)

Fixes #2587

* Test Gradle 7.6+ with configuration cache by default

* Make unpacking wasm runtime configuration cache compatible

* Make ProGuard integration configuration cache compatible

* Fix unresolved reference

* Fix configuration cache exception on macOS
This commit is contained in:
Alexey Tsvetkov
2023-01-11 10:46:40 +03:00
committed by GitHub
parent 57348cbde3
commit 472ef34fc3
17 changed files with 299 additions and 151 deletions

View File

@@ -88,6 +88,7 @@ val jar = tasks.named<Jar>("jar") {
val supportedGradleVersions = project.property("compose.tests.gradle.versions")
.toString().split(",")
.map { it.trim() }
.map { GradleVersion.version(it) }
val gradleTestsPattern = "org.jetbrains.compose.test.tests.integration.*"
@@ -107,9 +108,13 @@ tasks.test {
}
for (gradleVersion in supportedGradleVersions) {
tasks.registerVerificationTask<Test>("testGradle-$gradleVersion") {
tasks.registerVerificationTask<Test>("testGradle-${gradleVersion.version}") {
classpath = tasks.test.get().classpath
systemProperty("compose.tests.gradle.version", gradleVersion)
if (gradleVersion >= GradleVersion.version("7.6")) {
systemProperty("compose.tests.gradle.configuration.cache", "true")
}
systemProperty("compose.tests.gradle.version", gradleVersion.version)
filter {
includeTestsMatching(gradleTestsPattern)
}

View File

@@ -248,6 +248,11 @@ private fun JvmApplicationContext.configureProguardTask(
val settings = buildType.proguard
mainClass.set(app.mainClass)
proguardVersion.set(settings.version)
proguardFiles.from(proguardVersion.map { proguardVersion ->
project.configurations.detachedConfiguration(
project.dependencies.create("com.guardsquare:proguard-gradle:${proguardVersion}")
)
})
configurationFiles.from(settings.configurationFiles)
// ProGuard uses -dontobfuscate option to turn off obfuscation, which is enabled by default
// We want to disable obfuscation by default, because often

View File

@@ -55,7 +55,8 @@ internal fun JvmApplicationContext.packageBuildVersionFor(
private fun JvmApplicationDistributions.packageBuildVersionFor(
targetFormat: TargetFormat
): String? {
check(targetFormat.targetOS == OS.MacOS)
if (targetFormat.targetOS != OS.MacOS) return null
val formatSpecificVersion: String? = when (targetFormat) {
TargetFormat.AppImage -> null
TargetFormat.Dmg -> macOS.dmgPackageBuildVersion

View File

@@ -5,11 +5,7 @@
package org.jetbrains.compose.desktop.application.tasks
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFile
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.file.*
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*
@@ -52,6 +48,9 @@ abstract class AbstractProguardTask : AbstractComposeDesktopTask() {
@get:Input
val proguardVersion: Property<String> = objects.notNullProperty()
@get:InputFiles
val proguardFiles: ConfigurableFileCollection = objects.fileCollection()
@get:Input
val javaHome: Property<String> = objects.notNullProperty(System.getProperty("java.home"))
@@ -74,9 +73,6 @@ abstract class AbstractProguardTask : AbstractComposeDesktopTask() {
@TaskAction
fun execute() {
val javaHome = File(javaHome.get())
val proguardFiles = project.configurations.detachedConfiguration(
project.dependencies.create("com.guardsquare:proguard-gradle:${proguardVersion.get()}")
).files
fileOperations.clearDirs(destinationDir, workingDir)
val destinationDir = destinationDir.ioFile.absoluteFile

View File

@@ -5,24 +5,87 @@
package org.jetbrains.compose.experimental.web.internal
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ResolvedDependency
import org.gradle.api.artifacts.UnresolvedDependency
import org.gradle.api.provider.Provider
import org.jetbrains.compose.ComposeBuildConfig
import org.jetbrains.compose.experimental.dsl.ExperimentalWebApplication
import org.jetbrains.compose.internal.utils.registerTask
import org.jetbrains.compose.experimental.web.tasks.ExperimentalUnpackSkikoWasmRuntimeTask
import org.jetbrains.compose.internal.utils.uppercaseFirstChar
import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget
internal fun KotlinJsIrTarget.configureExperimentalWebApplication(app: ExperimentalWebApplication) {
val mainCompilation = compilations.getByName("main")
val unpackedRuntimeDir = project.layout.buildDirectory.dir("compose/skiko-wasm/$targetName")
val taskName = "unpackSkikoWasmRuntime${targetName.capitalize()}"
val taskName = "unpackSkikoWasmRuntime${targetName.uppercaseFirstChar()}"
mainCompilation.defaultSourceSet.resources.srcDir(unpackedRuntimeDir)
val unpackRuntime = project.registerTask<ExperimentalUnpackSkikoWasmRuntimeTask>(taskName) {
runtimeClasspath = project.configurations.getByName(mainCompilation.runtimeDependencyConfigurationName)
outputDir.set(unpackedRuntimeDir)
val skikoJsWasmRuntimeDependency = skikoVersionProvider(project)
.map { skikoVersion ->
project.dependencies.create("org.jetbrains.skiko:skiko-js-wasm-runtime:$skikoVersion")
}
val skikoJsWasmRuntimeConfiguration = project.configurations.create("COMPOSE_SKIKO_JS_WASM_RUNTIME").defaultDependencies {
it.addLater(skikoJsWasmRuntimeDependency)
}
mainCompilation.compileKotlinTaskProvider.configure { compileTask ->
compileTask.dependsOn(unpackRuntime)
val unpackRuntime = project.registerTask<ExperimentalUnpackSkikoWasmRuntimeTask>(taskName) {
skikoRuntimeFiles = skikoJsWasmRuntimeConfiguration
outputDir.set(unpackedRuntimeDir)
}
project.tasks.named(mainCompilation.processResourcesTaskName).configure { processResourcesTask ->
processResourcesTask.dependsOn(unpackRuntime)
}
}
private const val SKIKO_GROUP = "org.jetbrains.skiko"
private fun skikoVersionProvider(project: Project): Provider<String> {
val composeVersion = ComposeBuildConfig.composeVersion
val configurationWithSkiko = project.configurations.detachedConfiguration(
project.dependencies.create("org.jetbrains.compose.ui:ui-graphics:$composeVersion")
)
return project.provider {
val skikoDependency = configurationWithSkiko.allDependenciesDescriptors.firstOrNull(::isSkikoDependency)
skikoDependency?.version
?: error("Cannot determine the version of Skiko for Compose '$composeVersion'")
}
}
private fun isSkikoDependency(dep: DependencyDescriptor): Boolean =
dep.group == SKIKO_GROUP && dep.version != null
private val Configuration.allDependenciesDescriptors: Sequence<DependencyDescriptor>
get() = with (resolvedConfiguration.lenientConfiguration) {
allModuleDependencies.asSequence().map { ResolvedDependencyDescriptor(it) } +
unresolvedModuleDependencies.asSequence().map { UnresolvedDependencyDescriptor(it) }
}
private abstract class DependencyDescriptor {
abstract val group: String?
abstract val name: String?
abstract val version: String?
}
private class ResolvedDependencyDescriptor(private val dependency: ResolvedDependency) : DependencyDescriptor() {
override val group: String?
get() = dependency.moduleGroup
override val name: String?
get() = dependency.moduleName
override val version: String?
get() = dependency.moduleVersion
}
private class UnresolvedDependencyDescriptor(private val dependency: UnresolvedDependency) : DependencyDescriptor() {
override val group: String?
get() = dependency.selector.group
override val name: String?
get() = dependency.selector.name
override val version: String?
get() = dependency.selector.version
}

View File

@@ -6,53 +6,45 @@
package org.jetbrains.compose.experimental.web.tasks
import org.gradle.api.DefaultTask
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.file.ArchiveOperations
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileCollection
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.jetbrains.compose.internal.utils.clearDirs
import org.jetbrains.compose.internal.utils.debug
import java.io.File
import javax.inject.Inject
abstract class ExperimentalUnpackSkikoWasmRuntimeTask : DefaultTask() {
@get:InputFiles
lateinit var runtimeClasspath: Configuration
lateinit var skikoRuntimeFiles: FileCollection
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@get:Inject
internal abstract val archiveOperations: ArchiveOperations
@get:Inject
internal abstract val fileOperations: FileSystemOperations
@TaskAction
fun run() {
fileOperations.clearDirs(outputDir)
val runtimeArtifacts = runtimeClasspath.resolvedConfiguration.resolvedArtifacts
for (artifact in runtimeArtifacts) {
logger.debug { "Checking artifact: id=${artifact.id}, file=${artifact.file}" }
val id = artifact.id.componentIdentifier
if (id is ModuleComponentIdentifier && id.group == "org.jetbrains.skiko") {
logger.debug { "Found skiko artifact: $artifact" }
unpackSkikoRuntime(id.version)
for (file in skikoRuntimeFiles.files) {
if (file.name.endsWith(".jar", ignoreCase = true)) {
unpackJar(file)
}
}
}
private fun unpackSkikoRuntime(skikoVersion: String) {
val skikoRuntimeConfig = project.configurations.detachedConfiguration(
project.dependencies.create("org.jetbrains.skiko:skiko-js-wasm-runtime:$skikoVersion")
)
for (file in skikoRuntimeConfig.resolve()) {
if (file.name.endsWith(".jar", ignoreCase = true)) {
project.copy { copySpec ->
copySpec.from(project.zipTree(file))
copySpec.into(outputDir)
}
}
private fun unpackJar(file: File) {
fileOperations.copy { copySpec ->
copySpec.from(archiveOperations.zipTree(file))
copySpec.into(outputDir)
}
}
}

View File

@@ -6,7 +6,6 @@
package org.jetbrains.compose.test.tests.integration
import org.gradle.internal.impldep.org.testng.Assert
import org.gradle.testkit.runner.TaskOutcome
import org.jetbrains.compose.internal.utils.MacUtils
import org.jetbrains.compose.internal.utils.OS
import org.jetbrains.compose.internal.utils.currentArch
@@ -17,13 +16,12 @@ import org.jetbrains.compose.test.utils.*
import java.io.File
import java.util.*
import java.util.jar.JarFile
import kotlin.collections.HashSet
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assumptions
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import java.util.jar.JarFile
class DesktopApplicationTest : GradlePluginTestBase() {
@Test
@@ -41,25 +39,25 @@ class DesktopApplicationTest : GradlePluginTestBase() {
}
""".trimIndent()
}
gradle("run").build().let { result ->
assertEquals(TaskOutcome.SUCCESS, result.task(":run")?.outcome)
gradle("run").checks {
check.taskSuccessful(":run")
}
gradle("runDistributable").build().let { result ->
assertEquals(TaskOutcome.SUCCESS, result.task(":createDistributable")!!.outcome)
assertEquals(TaskOutcome.SUCCESS, result.task(":runDistributable")?.outcome)
gradle("runDistributable").checks {
check.taskSuccessful(":createDistributable")
check.taskSuccessful(":runDistributable")
}
}
@Test
fun testRunMpp() = with(testProject(TestProjects.mpp)) {
val logLine = "Kotlin MPP app is running!"
gradle("run").build().checks { check ->
check.taskOutcome(":run", TaskOutcome.SUCCESS)
gradle("run").checks {
check.taskSuccessful(":run")
check.logContains(logLine)
}
gradle("runDistributable").build().checks { check ->
check.taskOutcome(":createDistributable", TaskOutcome.SUCCESS)
check.taskOutcome(":runDistributable", TaskOutcome.SUCCESS)
gradle("runDistributable").checks {
check.taskSuccessful(":createDistributable")
check.taskSuccessful(":runDistributable")
check.logContains(logLine)
}
}
@@ -102,7 +100,7 @@ class DesktopApplicationTest : GradlePluginTestBase() {
).checkCustomComposeCompiler()
private fun TestProject.checkCustomComposeCompiler() {
gradle(":runDistributable").build().checks {
gradle(":runDistributable").checks {
val actualMainImage = file("main-image.actual.png")
val expectedMainImage = file("main-image.expected.png")
assert(actualMainImage.readBytes().contentEquals(expectedMainImage.readBytes())) {
@@ -113,8 +111,8 @@ class DesktopApplicationTest : GradlePluginTestBase() {
@Test
fun kotlinDsl(): Unit = with(testProject(TestProjects.jvmKotlinDsl)) {
gradle(":packageDistributionForCurrentOS", "--dry-run").build()
gradle(":packageReleaseDistributionForCurrentOS", "--dry-run").build()
gradle(":packageDistributionForCurrentOS", "--dry-run")
gradle(":packageReleaseDistributionForCurrentOS", "--dry-run")
}
@Test
@@ -146,8 +144,8 @@ class DesktopApplicationTest : GradlePluginTestBase() {
}
checkImageBeforeBuild()
gradle(":runReleaseDistributable").build().checks { check ->
check.taskOutcome(":proguardReleaseJars", TaskOutcome.SUCCESS)
gradle(":runReleaseDistributable").checks {
check.taskSuccessful(":proguardReleaseJars")
checkImageAfterBuild()
assertEqualTextFiles(file("main-methods.actual.txt"), file("main-methods.expected.txt"))
}
@@ -155,8 +153,8 @@ class DesktopApplicationTest : GradlePluginTestBase() {
file("build.gradle").modify { "$it\n$enableObfuscation" }
actualMainImage.delete()
checkImageBeforeBuild()
gradle(":runReleaseDistributable").build().checks { check ->
check.taskOutcome(":proguardReleaseJars", TaskOutcome.SUCCESS)
gradle(":runReleaseDistributable").checks {
check.taskSuccessful(":proguardReleaseJars")
checkImageAfterBuild()
assertNotEqualTextFiles(file("main-methods.actual.txt"), file("main-methods.expected.txt"))
}
@@ -178,13 +176,13 @@ class DesktopApplicationTest : GradlePluginTestBase() {
}
val packagingTask = ":packageDistributionForCurrentOS"
gradle(packagingTask).build().checks { check ->
check.taskOutcome(packagingTask, TaskOutcome.SUCCESS)
gradle(packagingTask).checks {
check.taskSuccessful(packagingTask)
}
gradle("clean", packagingTask).build().checks { check ->
check.taskOutcome(":checkRuntime", TaskOutcome.FROM_CACHE)
check.taskOutcome(packagingTask, TaskOutcome.SUCCESS)
gradle("clean", packagingTask).checks {
check.taskFromCache(":checkRuntime")
check.taskSuccessful(packagingTask)
}
}
@@ -200,7 +198,7 @@ class DesktopApplicationTest : GradlePluginTestBase() {
private fun TestProject.testPackageJvmDistributions() {
val result = gradle(":packageDistributionForCurrentOS").build()
val result = gradle(":packageDistributionForCurrentOS")
val mainClass = file("build/classes").walk().single { it.isFile && it.name == "MainKt.class" }
val bytecodeVersion = readClassFileVersion(mainClass)
@@ -236,8 +234,10 @@ class DesktopApplicationTest : GradlePluginTestBase() {
} else {
Assert.assertEquals(packageFile.name, "TestPackage-1.0.0.$ext", "Unexpected package name")
}
assertEquals(TaskOutcome.SUCCESS, result.task(":package${ext.uppercaseFirstChar()}")?.outcome)
assertEquals(TaskOutcome.SUCCESS, result.task(":packageDistributionForCurrentOS")?.outcome)
result.checks {
check.taskSuccessful(":package${ext.uppercaseFirstChar()}")
check.taskSuccessful(":packageDistributionForCurrentOS")
}
}
@Test
@@ -278,8 +278,8 @@ class DesktopApplicationTest : GradlePluginTestBase() {
}
private fun TestProject.testPackageUberJarForCurrentOS() {
gradle(":packageUberJarForCurrentOS").build().let { result ->
assertEquals(TaskOutcome.SUCCESS, result.task(":packageUberJarForCurrentOS")?.outcome)
gradle(":packageUberJarForCurrentOS").checks {
check.taskSuccessful(":packageUberJarForCurrentOS")
val resultJarFile = file("build/compose/jars/TestPackage-${currentTarget.id}-1.0.0.jar")
resultJarFile.checkExists()
@@ -297,9 +297,9 @@ class DesktopApplicationTest : GradlePluginTestBase() {
@Test
fun testModuleClash() = with(testProject(TestProjects.moduleClashCli)) {
gradle(":app:runDistributable").build().checks { check ->
check.taskOutcome(":app:createDistributable", TaskOutcome.SUCCESS)
check.taskOutcome(":app:runDistributable", TaskOutcome.SUCCESS)
gradle(":app:runDistributable").checks {
check.taskSuccessful(":app:createDistributable")
check.taskSuccessful(":app:runDistributable")
check.logContains("Called lib1#util()")
check.logContains("Called lib2#util()")
}
@@ -307,8 +307,8 @@ class DesktopApplicationTest : GradlePluginTestBase() {
@Test
fun testJavaLogger() = with(testProject(TestProjects.javaLogger)) {
gradle(":runDistributable").build().checks { check ->
check.taskOutcome(":runDistributable", TaskOutcome.SUCCESS)
gradle(":runDistributable").checks {
check.taskSuccessful(":runDistributable")
check.logContains("Compose Gradle plugin test log warning!")
}
}
@@ -324,8 +324,8 @@ class DesktopApplicationTest : GradlePluginTestBase() {
Assumptions.assumeTrue(currentOS == OS.MacOS)
with(testProject(TestProjects.macOptions)) {
gradle(":runDistributable").build().checks { check ->
check.taskOutcome(":runDistributable", TaskOutcome.SUCCESS)
gradle(":runDistributable").checks {
check.taskSuccessful(":runDistributable")
check.logContains("Hello, from Mac OS!")
val appDir = testWorkDir.resolve("build/compose/binaries/main/app/TestPackage.app/Contents/")
val actualInfoPlist = appDir.resolve("Info.plist").checkExists()
@@ -373,8 +373,8 @@ class DesktopApplicationTest : GradlePluginTestBase() {
security("default-keychain", "-s", keychain)
security("unlock-keychain", "-p", password, keychain)
gradle(":createDistributable").build().checks { check ->
check.taskOutcome(":createDistributable", TaskOutcome.SUCCESS)
gradle(":createDistributable").checks {
check.taskSuccessful(":createDistributable")
val appDir = testWorkDir.resolve("build/compose/binaries/main/app/TestPackage.app/")
val result = runProcess(MacUtils.codesign, args = listOf("--verify", "--verbose", appDir.absolutePath))
val actualOutput = result.err.trim()
@@ -385,8 +385,8 @@ class DesktopApplicationTest : GradlePluginTestBase() {
Assert.assertEquals(expectedOutput, actualOutput)
}
gradle(":runDistributable").build().checks { check ->
check.taskOutcome(":runDistributable", TaskOutcome.SUCCESS)
gradle(":runDistributable").checks {
check.taskSuccessful(":runDistributable")
check.logContains("Signed app successfully started!")
}
}
@@ -397,8 +397,8 @@ class DesktopApplicationTest : GradlePluginTestBase() {
fun testOptionsWithSpaces() {
with(testProject(TestProjects.optionsWithSpaces)) {
fun testRunTask(runTask: String) {
gradle(runTask).build().checks { check ->
check.taskOutcome(runTask, TaskOutcome.SUCCESS)
gradle(runTask).checks {
check.taskSuccessful(runTask)
check.logContains("Running test options with spaces!")
check.logContains("Arg #1=Value 1!")
check.logContains("Arg #2=Value 2!")
@@ -409,8 +409,8 @@ class DesktopApplicationTest : GradlePluginTestBase() {
testRunTask(":runDistributable")
testRunTask(":run")
gradle(":packageDistributionForCurrentOS").build().checks { check ->
check.taskOutcome(":packageDistributionForCurrentOS", TaskOutcome.SUCCESS)
gradle(":packageDistributionForCurrentOS").checks {
check.taskSuccessful(":packageDistributionForCurrentOS")
}
}
}
@@ -419,8 +419,8 @@ class DesktopApplicationTest : GradlePluginTestBase() {
fun testDefaultArgs() {
with(testProject(TestProjects.defaultArgs)) {
fun testRunTask(runTask: String) {
gradle(runTask).build().checks { check ->
check.taskOutcome(runTask, TaskOutcome.SUCCESS)
gradle(runTask).checks {
check.taskSuccessful(runTask)
check.logContains("compose.application.configure.swing.globals=true")
}
}
@@ -428,8 +428,8 @@ class DesktopApplicationTest : GradlePluginTestBase() {
testRunTask(":runDistributable")
testRunTask(":run")
gradle(":packageDistributionForCurrentOS").build().checks { check ->
check.taskOutcome(":packageDistributionForCurrentOS", TaskOutcome.SUCCESS)
gradle(":packageDistributionForCurrentOS").checks {
check.taskSuccessful(":packageDistributionForCurrentOS")
}
}
}
@@ -438,8 +438,8 @@ class DesktopApplicationTest : GradlePluginTestBase() {
fun testDefaultArgsOverride() {
with(testProject(TestProjects.defaultArgsOverride)) {
fun testRunTask(runTask: String) {
gradle(runTask).build().checks { check ->
check.taskOutcome(runTask, TaskOutcome.SUCCESS)
gradle(runTask).checks {
check.taskSuccessful(runTask)
check.logContains("compose.application.configure.swing.globals=false")
}
}
@@ -447,8 +447,8 @@ class DesktopApplicationTest : GradlePluginTestBase() {
testRunTask(":runDistributable")
testRunTask(":run")
gradle(":packageDistributionForCurrentOS").build().checks { check ->
check.taskOutcome(":packageDistributionForCurrentOS", TaskOutcome.SUCCESS)
gradle(":packageDistributionForCurrentOS").checks {
check.taskSuccessful(":packageDistributionForCurrentOS")
}
}
}
@@ -456,8 +456,8 @@ class DesktopApplicationTest : GradlePluginTestBase() {
@Test
fun testSuggestModules() {
with(testProject(TestProjects.jvm)) {
gradle(":suggestRuntimeModules").build().checks { check ->
check.taskOutcome(":suggestRuntimeModules", TaskOutcome.SUCCESS)
gradle(":suggestRuntimeModules").checks {
check.taskSuccessful(":suggestRuntimeModules")
check.logContains("Suggested runtime modules to include:")
check.logContains("modules(\"java.instrument\", \"jdk.unsupported\")")
}
@@ -467,8 +467,8 @@ class DesktopApplicationTest : GradlePluginTestBase() {
@Test
fun testUnpackSkiko() {
with(testProject(TestProjects.unpackSkiko)) {
gradle(":runDistributable").build().checks { check ->
check.taskOutcome(":runDistributable", TaskOutcome.SUCCESS)
gradle(":runDistributable").checks {
check.taskSuccessful(":runDistributable")
val libraryPathPattern = "Read skiko library path: '(.*)'".toRegex()
val m = libraryPathPattern.find(check.log)
@@ -490,12 +490,12 @@ class DesktopApplicationTest : GradlePluginTestBase() {
@Test
fun resources() = with(testProject(TestProjects.resources)) {
gradle(":run").build().checks { check ->
check.taskOutcome(":run", TaskOutcome.SUCCESS)
gradle(":run").checks {
check.taskSuccessful(":run")
}
gradle(":runDistributable").build().checks { check ->
check.taskOutcome(":runDistributable", TaskOutcome.SUCCESS)
gradle(":runDistributable").checks {
check.taskSuccessful(":runDistributable")
}
}
}

View File

@@ -16,10 +16,17 @@ import java.net.SocketTimeoutException
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.thread
import org.gradle.testkit.runner.TaskOutcome
import org.junit.jupiter.api.Test
class GradlePluginTest : GradlePluginTestBase() {
@Test
fun skikoWasm() = with(testProject(TestProjects.skikoWasm)) {
gradle(":build").checks {
check.taskSuccessful(":unpackSkikoWasmRuntimeJs")
check.taskSuccessful(":compileKotlinJs")
}
}
@Test
fun jsMppIsNotBroken() =
with(
@@ -30,18 +37,13 @@ class GradlePluginTest : GradlePluginTestBase() {
)
)
) {
gradle(":compileKotlinJs").build().checks { check ->
check.taskOutcome(":compileKotlinJs", TaskOutcome.SUCCESS)
gradle(":compileKotlinJs").checks {
check.taskSuccessful(":compileKotlinJs")
}
}
@Test
fun configurePreviewWithoutConfigurationCache() = configurePreview(withConfigurationCache = false)
@Test
fun configurePreviewWithConfigurationCache() = configurePreview(withConfigurationCache = true)
private fun configurePreview(withConfigurationCache: Boolean) {
fun configurePreview() {
val isAlive = AtomicBoolean(true)
val receivedConfigCount = AtomicInteger(0)
val port = AtomicInteger(-1)
@@ -82,7 +84,7 @@ class GradlePluginTest : GradlePluginTestBase() {
}
try {
testConfigureDesktopPreviewImpl(port.get(), withConfigurationCache)
testConfigureDesktopPreviewImpl(port.get())
} finally {
isAlive.set(false)
connectionThread.interrupt()
@@ -96,35 +98,20 @@ class GradlePluginTest : GradlePluginTestBase() {
}
}
private fun testConfigureDesktopPreviewImpl(port: Int, withConfigurationCache: Boolean) {
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"
val configurationCacheArg = "--configuration-cache"
val jvmRunner = if (withConfigurationCache) {
gradle(jvmTask, portProperty, previewTargetProperty, configurationCacheArg)
} else {
gradle(jvmTask, portProperty, previewTargetProperty)
gradle(jvmTask, portProperty, previewTargetProperty).checks {
check.taskSuccessful(jvmTask)
}
jvmRunner
.build()
.checks { check ->
check.taskOutcome(jvmTask, TaskOutcome.SUCCESS)
}
val mppTask = ":mpp:configureDesktopPreviewDesktop"
val mppRunner = if (withConfigurationCache) {
gradle(mppTask, portProperty, previewTargetProperty, configurationCacheArg)
} else {
gradle(mppTask, portProperty, previewTargetProperty)
gradle(mppTask, portProperty, previewTargetProperty).checks {
check.taskSuccessful(mppTask)
}
mppRunner
.build()
.checks { check ->
check.taskOutcome(mppTask, TaskOutcome.SUCCESS)
}
}
}

View File

@@ -5,13 +5,10 @@
package org.jetbrains.compose.test.tests.integration
import org.gradle.testkit.runner.TaskOutcome
import org.gradle.testkit.runner.UnexpectedBuildFailure
import org.jetbrains.compose.test.utils.GradlePluginTestBase
import org.jetbrains.compose.test.utils.TestProjects
import org.jetbrains.compose.test.utils.checks
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
class KotlinCompatabilityTest : GradlePluginTestBase() {
@Test
@@ -33,8 +30,8 @@ class KotlinCompatabilityTest : GradlePluginTestBase() {
)
) {
val logLine = "Kotlin MPP app is running!"
gradle("run").build().checks { check ->
check.taskOutcome(":run", TaskOutcome.SUCCESS)
gradle("run").checks {
check.taskSuccessful(":run")
check.logContains(logLine)
}
}
@@ -45,8 +42,8 @@ class KotlinCompatabilityTest : GradlePluginTestBase() {
testEnvironment = defaultTestEnvironment.copy(kotlinVersion = kotlinVersion)
)
) {
gradle(":compileKotlinJs").build().checks { check ->
check.taskOutcome(":compileKotlinJs", TaskOutcome.SUCCESS)
gradle(":compileKotlinJs").checks {
check.taskSuccessful(":compileKotlinJs")
}
}
}

View File

@@ -5,7 +5,9 @@
package org.jetbrains.compose.test.utils
import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.GradleRunner
import org.gradle.util.GradleVersion
import org.jetbrains.compose.desktop.application.internal.ComposeProperties
import java.io.File
import java.util.Properties
@@ -64,18 +66,48 @@ class TestProject(
}
}
fun gradle(vararg args: String): GradleRunner =
GradleRunner.create().apply {
withGradleVersion(TestProperties.gradleVersionForTests)
withProjectDir(testEnvironment.workingDir)
withArguments(args.toList() + additionalArgs)
forwardOutput()
internal fun gradle(vararg args: String): BuildResult {
if (TestProperties.gradleConfigurationCache) {
if (GradleVersion.version(TestProperties.gradleVersionForTests) < GradleVersion.version("8.0-rc-1")) {
// Gradle 7.* does not use the configuration cache in the same build.
// In other words, if cache misses, Gradle performs configuration,
// but does not, use the serialized task graph.
// So in order to test the cache, we need to perform dry-run before the actual run.
// This should be fixed in https://github.com/gradle/gradle/issues/21985 (which is planned for 8.0 RC 1)
gradleRunner(args.withDryRun()).build()
}
}
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated("Do not commit!")
fun gradleDebug(vararg args: String): GradleRunner =
gradle(*args).withDebug(true)
return gradleRunner(args).build()
}
private fun Array<out String>.withDryRun(): Array<String> {
var sawDryRun = false
val dryRunArgs = ArrayList<String>(size)
for (arg in this) {
sawDryRun = sawDryRun || arg.trim() in listOf("-m", "--dry-run")
dryRunArgs.add(arg)
}
if (!sawDryRun) {
dryRunArgs.add("--dry-run")
}
return dryRunArgs.toTypedArray()
}
private fun gradleRunner(args: Array<out String>): GradleRunner {
val allArgs = args.toMutableList()
allArgs.addAll(additionalArgs)
if (TestProperties.gradleConfigurationCache) {
allArgs.add("--configuration-cache")
}
return GradleRunner.create().apply {
withGradleVersion(TestProperties.gradleVersionForTests)
withProjectDir(testEnvironment.workingDir)
withArguments(allArgs)
forwardOutput()
}
}
fun file(path: String): File =
testEnvironment.workingDir.resolve(path)

View File

@@ -22,5 +22,6 @@ object TestProjects {
const val unpackSkiko = "application/unpackSkiko"
const val resources = "application/resources"
const val jsMpp = "misc/jsMpp"
const val skikoWasm = "misc/skikoWasm"
const val jvmPreview = "misc/jvmPreview"
}

View File

@@ -23,6 +23,9 @@ object TestProperties {
val gradleVersionForTests: String?
get() = System.getProperty("compose.tests.gradle.version")
val gradleConfigurationCache: Boolean
get() = System.getProperty("compose.tests.gradle.configuration.cache") == "true"
val summaryFile: File?
get() = System.getProperty("compose.tests.summary.file")?.let { File(it) }

View File

@@ -18,10 +18,13 @@ internal fun <T> Collection<T>.checkContains(vararg elements: T) {
}
}
internal fun BuildResult.checks(fn: (BuildResultChecks) -> Unit) {
fn(BuildResultChecks(this))
internal fun BuildResult.checks(fn: ChecksWrapper.() -> Unit) {
fn(ChecksWrapper(BuildResultChecks(this)))
}
@JvmInline
internal value class ChecksWrapper(val check: BuildResultChecks)
internal class BuildResultChecks(private val result: BuildResult) {
val log: String
get() = result.output
@@ -32,7 +35,19 @@ internal class BuildResultChecks(private val result: BuildResult) {
}
}
fun taskOutcome(task: String, expectedOutcome: TaskOutcome) {
fun taskSuccessful(task: String) {
taskOutcome(task, TaskOutcome.SUCCESS)
}
fun taskFailed(task: String) {
taskOutcome(task, TaskOutcome.FAILED)
}
fun taskFromCache(task: String) {
taskOutcome(task, TaskOutcome.FROM_CACHE)
}
private fun taskOutcome(task: String, expectedOutcome: TaskOutcome) {
val actualOutcome = result.task(task)?.outcome
if (actualOutcome != expectedOutcome) {
throw AssertionError(

View File

@@ -0,0 +1,31 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.compose"
}
repositories {
jetbrainsCompose()
}
kotlin {
js(IR) {
browser()
binaries.executable()
}
sourceSets {
commonMain {
dependencies {
implementation(compose.ui)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.runtime)
}
}
}
}
compose.experimental {
web.application {}
}

View File

@@ -0,0 +1 @@
org.jetbrains.compose.experimental.jscanvas.enabled=true

View File

@@ -0,0 +1,12 @@
pluginManagement {
plugins {
id 'org.jetbrains.kotlin.multiplatform' version 'KOTLIN_VERSION_PLACEHOLDER'
id 'org.jetbrains.compose' version 'COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER'
}
repositories {
mavenLocal()
gradlePluginPortal()
maven { url "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" }
}
}
rootProject.name = "skikoWasm"

View File

@@ -0,0 +1,7 @@
import androidx.compose.material.Text
import androidx.compose.runtime.*
@Composable
fun HelloWorld() {
Text("Hello, World!")
}