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.
616 lines
22 KiB
616 lines
22 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.internal.impldep.org.testng.Assert |
|
import org.jetbrains.compose.internal.utils.MacUtils |
|
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.internal.utils.currentTarget |
|
import org.jetbrains.compose.internal.utils.uppercaseFirstChar |
|
import org.jetbrains.compose.test.utils.* |
|
|
|
import java.io.File |
|
import java.util.* |
|
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 |
|
fun smokeTestRunTask() = with(testProject(TestProjects.jvm)) { |
|
file("build.gradle").modify { |
|
it + """ |
|
afterEvaluate { |
|
tasks.getByName("run").doFirst { |
|
throw new StopExecutionException("Skip run task") |
|
} |
|
tasks.getByName("runRelease").doFirst { |
|
throw new StopExecutionException("Skip runRelease task") |
|
} |
|
|
|
tasks.getByName("runDistributable").doFirst { |
|
throw new StopExecutionException("Skip runDistributable task") |
|
} |
|
tasks.getByName("runReleaseDistributable").doFirst { |
|
throw new StopExecutionException("Skip runReleaseDistributable task") |
|
} |
|
} |
|
""".trimIndent() |
|
} |
|
gradle("run").checks { |
|
check.taskSuccessful(":run") |
|
} |
|
gradle("runRelease").checks { |
|
check.taskSuccessful(":runRelease") |
|
} |
|
gradle("runDistributable").checks { |
|
check.taskSuccessful(":createDistributable") |
|
check.taskSuccessful(":runDistributable") |
|
} |
|
gradle("runReleaseDistributable").checks { |
|
check.taskSuccessful(":createReleaseDistributable") |
|
check.taskSuccessful(":runReleaseDistributable") |
|
} |
|
} |
|
|
|
@Test |
|
fun testRunMpp() = with(testProject(TestProjects.mpp)) { |
|
val logLine = "Kotlin MPP app is running!" |
|
gradle("run").checks { |
|
check.taskSuccessful(":run") |
|
check.logContains(logLine) |
|
} |
|
gradle("runRelease").checks { |
|
check.taskSuccessful(":runRelease") |
|
check.logContains(logLine) |
|
} |
|
gradle("runDistributable").checks { |
|
check.taskSuccessful(":createDistributable") |
|
check.taskSuccessful(":runDistributable") |
|
check.logContains(logLine) |
|
} |
|
gradle("runReleaseDistributable").checks { |
|
check.taskSuccessful(":createReleaseDistributable") |
|
check.taskSuccessful(":runReleaseDistributable") |
|
check.logContains(logLine) |
|
} |
|
} |
|
|
|
/** |
|
* Test the version of Compose Compiler published by Google. |
|
* See https://developer.android.com/jetpack/androidx/releases/compose-kotlin |
|
*/ |
|
@Test |
|
fun testAndroidxCompiler() = testProject( |
|
TestProjects.customCompiler, defaultTestEnvironment.copy( |
|
kotlinVersion = "1.8.0", |
|
composeCompilerPlugin = "\"androidx.compose.compiler:compiler:1.4.0\"" |
|
) |
|
).checkCustomComposeCompiler() |
|
|
|
@Test |
|
fun testSettingLatestCompiler() = testProject( |
|
TestProjects.customCompiler, defaultTestEnvironment.copy( |
|
kotlinVersion = "1.8.20", |
|
composeCompilerPlugin = "dependencies.compiler.forKotlin(\"1.8.20\")", |
|
) |
|
).checkCustomComposeCompiler() |
|
|
|
@Test |
|
fun testSettingAutoCompiler() = testProject( |
|
TestProjects.customCompiler, defaultTestEnvironment.copy( |
|
kotlinVersion = "1.8.10", |
|
composeCompilerPlugin = "dependencies.compiler.auto", |
|
) |
|
).checkCustomComposeCompiler() |
|
|
|
@Test |
|
fun testKotlinCheckDisabled() = testProject( |
|
TestProjects.customCompilerArgs, defaultTestEnvironment.copy( |
|
kotlinVersion = "1.9.21", |
|
composeCompilerPlugin = "dependencies.compiler.forKotlin(\"1.9.20\")", |
|
composeCompilerArgs = "\"suppressKotlinVersionCompatibilityCheck=1.9.21\"" |
|
) |
|
).checkCustomComposeCompiler(checkKJS = true) |
|
|
|
private fun TestProject.checkCustomComposeCompiler(checkKJS: Boolean = false) { |
|
gradle(":runDistributable").checks { |
|
val actualMainImage = file("main-image.actual.png") |
|
val expectedMainImage = file("main-image.expected.png") |
|
assert(actualMainImage.readBytes().contentEquals(expectedMainImage.readBytes())) { |
|
"The actual image '$actualMainImage' does not match the expected image '$expectedMainImage'" |
|
} |
|
} |
|
if (checkKJS) { |
|
gradle(":jsBrowserProductionWebpack").checks { |
|
check.taskSuccessful(":jsBrowserProductionWebpack") |
|
} |
|
} |
|
} |
|
|
|
@Test |
|
fun kotlinDsl(): Unit = with(testProject(TestProjects.jvmKotlinDsl)) { |
|
gradle(":packageDistributionForCurrentOS", "--dry-run") |
|
gradle(":packageReleaseDistributionForCurrentOS", "--dry-run") |
|
} |
|
|
|
@Test |
|
fun proguard(): Unit = with( |
|
testProject( |
|
TestProjects.proguard, |
|
testEnvironment = defaultTestEnvironment.copy(composeVerbose = false)) |
|
) { |
|
val enableObfuscation = """ |
|
compose.desktop { |
|
application { |
|
buildTypes.release.proguard { |
|
obfuscate.set(true) |
|
} |
|
} |
|
} |
|
""".trimIndent() |
|
|
|
val actualMainImage = file("main-image.actual.png") |
|
val expectedMainImage = file("main-image.expected.png") |
|
|
|
fun checkImageBeforeBuild() { |
|
assertFalse(actualMainImage.exists(), "'$actualMainImage' exists") |
|
} |
|
fun checkImageAfterBuild() { |
|
assert(actualMainImage.readBytes().contentEquals(expectedMainImage.readBytes())) { |
|
"The actual image '$actualMainImage' does not match the expected image '$expectedMainImage'" |
|
} |
|
} |
|
|
|
checkImageBeforeBuild() |
|
gradle(":runReleaseDistributable").checks { |
|
check.taskSuccessful(":proguardReleaseJars") |
|
checkImageAfterBuild() |
|
assertEqualTextFiles(file("main-methods.actual.txt"), file("main-methods.expected.txt")) |
|
} |
|
|
|
file("build.gradle").modify { "$it\n$enableObfuscation" } |
|
actualMainImage.delete() |
|
checkImageBeforeBuild() |
|
gradle(":runReleaseDistributable").checks { |
|
check.taskSuccessful(":proguardReleaseJars") |
|
checkImageAfterBuild() |
|
assertNotEqualTextFiles(file("main-methods.actual.txt"), file("main-methods.expected.txt")) |
|
} |
|
} |
|
|
|
@Test |
|
fun joinOutputJarsJvm() = with(testProject(TestProjects.jvm)) { |
|
joinOutputJars() |
|
} |
|
|
|
@Test |
|
fun joinOutputJarsMpp() = with(testProject(TestProjects.mpp)) { |
|
joinOutputJars() |
|
} |
|
|
|
private fun TestProject.joinOutputJars() { |
|
enableJoinOutputJars() |
|
gradle(":createReleaseDistributable").checks { |
|
check.taskSuccessful(":createReleaseDistributable") |
|
|
|
val distributionPathPattern = "The distribution is written to (.*)".toRegex() |
|
val m = distributionPathPattern.find(check.log) |
|
val distributionDir = m?.groupValues?.get(1)?.let(::File) |
|
if (distributionDir == null || !distributionDir.exists()) { |
|
error("Invalid distribution path: $distributionDir") |
|
} |
|
val appDirSubPath = when (currentOS) { |
|
OS.Linux -> "TestPackage/lib/app" |
|
OS.Windows -> "TestPackage/app" |
|
OS.MacOS -> "TestPackage.app/Contents/app" |
|
} |
|
val appDir = distributionDir.resolve(appDirSubPath) |
|
val jarsCount = appDir.listFiles()?.count { it.name.endsWith(".jar", ignoreCase = true) } ?: 0 |
|
assert(jarsCount == 1) |
|
} |
|
} |
|
|
|
@Test |
|
fun gradleBuildCache() = with(testProject(TestProjects.jvm)) { |
|
modifyGradleProperties { |
|
setProperty("org.gradle.caching", "true") |
|
} |
|
modifyText("settings.gradle") { |
|
it + "\n" + """ |
|
buildCache { |
|
local { |
|
directory = new File(rootDir, 'build-cache') |
|
} |
|
} |
|
""".trimIndent() |
|
} |
|
|
|
val packagingTask = ":packageDistributionForCurrentOS" |
|
gradle(packagingTask).checks { |
|
check.taskSuccessful(packagingTask) |
|
} |
|
|
|
gradle("clean", packagingTask).checks { |
|
check.taskFromCache(":checkRuntime") |
|
check.taskSuccessful(packagingTask) |
|
} |
|
} |
|
|
|
@Test |
|
fun packageJvm() = with(testProject(TestProjects.jvm)) { |
|
testPackageJvmDistributions() |
|
} |
|
|
|
@Test |
|
fun packageMpp() = with(testProject(TestProjects.mpp)) { |
|
testPackageJvmDistributions() |
|
} |
|
|
|
|
|
private fun TestProject.testPackageJvmDistributions() { |
|
val result = gradle(":packageDistributionForCurrentOS") |
|
|
|
val mainClass = file("build/classes").walk().single { it.isFile && it.name == "MainKt.class" } |
|
val bytecodeVersion = readClassFileVersion(mainClass) |
|
assertEquals(JDK_11_BYTECODE_VERSION, bytecodeVersion, "$mainClass bytecode version") |
|
|
|
val ext = when (currentOS) { |
|
OS.Linux -> "deb" |
|
OS.Windows -> "msi" |
|
OS.MacOS -> "dmg" |
|
} |
|
val packageDir = file("build/compose/binaries/main/$ext") |
|
val packageDirFiles = packageDir.listFiles() ?: arrayOf() |
|
check(packageDirFiles.size == 1) { |
|
"Expected single package in $packageDir, got [${packageDirFiles.joinToString(", ") { it.name }}]" |
|
} |
|
val packageFile = packageDirFiles.single() |
|
|
|
if (currentOS == OS.Linux) { |
|
// The default naming scheme was changed in JDK 18 |
|
// https://bugs.openjdk.org/browse/JDK-8276084 |
|
// This test might be used with different JDKs, |
|
// so as a workaround we check that the |
|
// package name is either one of two expected values. |
|
// TODO: Check a corresponding value for each JDK |
|
val possibleNames = listOf( |
|
"test-package_1.0.0-1_amd64.$ext", |
|
"test-package_1.0.0_amd64.$ext", |
|
) |
|
check(possibleNames.any { packageFile.name.equals(it, ignoreCase = true) }) { |
|
"Unexpected package name '${packageFile.name}' in $packageDir\n" + |
|
"Possible names: ${possibleNames.joinToString(", ") { "'$it'" }}" |
|
} |
|
} else { |
|
Assert.assertEquals(packageFile.name, "TestPackage-1.0.0.$ext", "Unexpected package name") |
|
} |
|
result.checks { |
|
check.taskSuccessful(":package${ext.uppercaseFirstChar()}") |
|
check.taskSuccessful(":packageDistributionForCurrentOS") |
|
} |
|
} |
|
|
|
@Test |
|
fun testJdk19() = with(customJdkProject(19)) { |
|
testPackageJvmDistributions() |
|
} |
|
|
|
private fun customJdkProject(javaVersion: Int): TestProject = |
|
testProject(TestProjects.jvm).apply { |
|
appendText("build.gradle") { |
|
""" |
|
compose.desktop.application { |
|
javaHome = javaToolchains.launcherFor { |
|
languageVersion.set(JavaLanguageVersion.of($javaVersion)) |
|
}.get().metadata.installationPath.asFile.absolutePath |
|
} |
|
""".trimIndent() |
|
} |
|
} |
|
|
|
@Test |
|
fun packageUberJarForCurrentOSJvm() = with(testProject(TestProjects.jvm)) { |
|
testPackageUberJarForCurrentOS(false) |
|
} |
|
|
|
@Test |
|
fun packageUberJarForCurrentOSMpp() = with(testProject(TestProjects.mpp)) { |
|
testPackageUberJarForCurrentOS(false) |
|
} |
|
|
|
@Test |
|
fun packageReleaseUberJarForCurrentOSJvm() = with(testProject(TestProjects.jvm)) { |
|
testPackageUberJarForCurrentOS(true) |
|
} |
|
|
|
@Test |
|
fun packageReleaseUberJarForCurrentOSMpp() = with(testProject(TestProjects.mpp)) { |
|
testPackageUberJarForCurrentOS(true) |
|
} |
|
|
|
private fun TestProject.testPackageUberJarForCurrentOS(release: Boolean) { |
|
val task = when { |
|
release -> ":packageReleaseUberJarForCurrentOS" |
|
else -> ":packageUberJarForCurrentOS" |
|
} |
|
|
|
val jarFileName = when { |
|
release -> "build/compose/jars/TestPackage-${currentTarget.id}-1.0.0-release.jar" |
|
else -> "build/compose/jars/TestPackage-${currentTarget.id}-1.0.0.jar" |
|
} |
|
|
|
gradle(task).checks { |
|
check.taskSuccessful(task) |
|
|
|
val resultJarFile = file(jarFileName) |
|
resultJarFile.checkExists() |
|
|
|
JarFile(resultJarFile).use { jar -> |
|
val mainClass = jar.manifest.mainAttributes.getValue("Main-Class") |
|
assertEquals("MainKt", mainClass, "Unexpected main class") |
|
|
|
jar.entries().toList().mapTo(HashSet()) { it.name }.apply { |
|
checkContains("MainKt.class", "org/jetbrains/skiko/SkiaLayer.class") |
|
} |
|
} |
|
} |
|
} |
|
|
|
@Test |
|
fun testModuleClash() = with(testProject(TestProjects.moduleClashCli)) { |
|
gradle(":app:runDistributable").checks { |
|
check.taskSuccessful(":app:createDistributable") |
|
check.taskSuccessful(":app:runDistributable") |
|
check.logContains("Called lib1#util()") |
|
check.logContains("Called lib2#util()") |
|
} |
|
} |
|
|
|
@Test |
|
fun testJavaLogger() = with(testProject(TestProjects.javaLogger)) { |
|
gradle(":runDistributable").checks { |
|
check.taskSuccessful(":runDistributable") |
|
check.logContains("Compose Gradle plugin test log warning!") |
|
} |
|
} |
|
|
|
@Test |
|
fun testMacOptions() { |
|
fun String.normalized(): String = |
|
trim().replace( |
|
"Copyright (C) ${Calendar.getInstance().get(Calendar.YEAR)}", |
|
"Copyright (C) CURRENT_YEAR" |
|
) |
|
|
|
Assumptions.assumeTrue(currentOS == OS.MacOS) |
|
|
|
with(testProject(TestProjects.macOptions)) { |
|
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() |
|
val expectedInfoPlist = testWorkDir.resolve("Expected-Info.Plist") |
|
val actualInfoPlistNormalized = actualInfoPlist.readText().normalized() |
|
val expectedInfoPlistNormalized = expectedInfoPlist.readText().normalized() |
|
Assert.assertEquals(actualInfoPlistNormalized, expectedInfoPlistNormalized) |
|
} |
|
} |
|
} |
|
|
|
@Test |
|
fun testMacSignConfiguration() { |
|
Assumptions.assumeTrue(currentOS == OS.MacOS) |
|
|
|
with(testProject(TestProjects.macSign)) { |
|
gradle("--dry-run", ":createDistributable") |
|
} |
|
} |
|
|
|
@Test |
|
@Disabled |
|
// the test does not work on CI and locally unless test keychain is opened manually |
|
fun testMacSign() { |
|
Assumptions.assumeTrue(currentOS == OS.MacOS) |
|
|
|
fun security(vararg args: Any): ProcessRunResult { |
|
val args = args.map { |
|
if (it is File) it.absolutePath else it.toString() |
|
} |
|
return runProcess(MacUtils.security, args) |
|
} |
|
|
|
fun withNewDefaultKeychain(newKeychain: File, fn: () -> Unit) { |
|
val originalKeychain = |
|
security("default-keychain") |
|
.out |
|
.trim() |
|
.trim('"') |
|
|
|
try { |
|
security("default-keychain", "-s", newKeychain) |
|
fn() |
|
} finally { |
|
security("default-keychain", "-s", originalKeychain) |
|
} |
|
} |
|
|
|
with(testProject(TestProjects.macSign)) { |
|
val keychain = file("compose.test.keychain") |
|
val password = "compose.test" |
|
|
|
withNewDefaultKeychain(keychain) { |
|
security("default-keychain", "-s", keychain) |
|
security("unlock-keychain", "-p", password, keychain) |
|
|
|
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() |
|
val expectedOutput = """ |
|
|${appDir.absolutePath}: valid on disk |
|
|${appDir.absolutePath}: satisfies its Designated Requirement |
|
""".trimMargin().trim() |
|
Assert.assertEquals(expectedOutput, actualOutput) |
|
} |
|
|
|
gradle(":runDistributable").checks { |
|
check.taskSuccessful(":runDistributable") |
|
check.logContains("Signed app successfully started!") |
|
} |
|
} |
|
} |
|
} |
|
|
|
@Test |
|
fun testOptionsWithSpaces() { |
|
with(testProject(TestProjects.optionsWithSpaces)) { |
|
fun testRunTask(runTask: String) { |
|
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!") |
|
check.logContains("JVM system property arg=Value 3!") |
|
} |
|
} |
|
|
|
testRunTask(":runDistributable") |
|
testRunTask(":run") |
|
|
|
gradle(":packageDistributionForCurrentOS").checks { |
|
check.taskSuccessful(":packageDistributionForCurrentOS") |
|
} |
|
} |
|
} |
|
|
|
@Test |
|
fun testDefaultArgs() { |
|
with(testProject(TestProjects.defaultArgs)) { |
|
fun testRunTask(runTask: String) { |
|
gradle(runTask).checks { |
|
check.taskSuccessful(runTask) |
|
check.logContains("compose.application.configure.swing.globals=true") |
|
} |
|
} |
|
|
|
testRunTask(":runDistributable") |
|
testRunTask(":run") |
|
|
|
gradle(":packageDistributionForCurrentOS").checks { |
|
check.taskSuccessful(":packageDistributionForCurrentOS") |
|
} |
|
} |
|
} |
|
|
|
@Test |
|
fun testDefaultArgsOverride() { |
|
with(testProject(TestProjects.defaultArgsOverride)) { |
|
fun testRunTask(runTask: String) { |
|
gradle(runTask).checks { |
|
check.taskSuccessful(runTask) |
|
check.logContains("compose.application.configure.swing.globals=false") |
|
} |
|
} |
|
|
|
testRunTask(":runDistributable") |
|
testRunTask(":run") |
|
|
|
gradle(":packageDistributionForCurrentOS").checks { |
|
check.taskSuccessful(":packageDistributionForCurrentOS") |
|
} |
|
} |
|
} |
|
|
|
@Test |
|
fun testSuggestModules() { |
|
with(testProject(TestProjects.jvm)) { |
|
gradle(":suggestRuntimeModules").checks { |
|
check.taskSuccessful(":suggestRuntimeModules") |
|
check.logContains("Suggested runtime modules to include:") |
|
check.logContains("modules(\"java.instrument\", \"jdk.unsupported\")") |
|
} |
|
} |
|
} |
|
|
|
@Test |
|
fun testUnpackSkiko() = with(testProject(TestProjects.unpackSkiko)) { |
|
testUnpackSkiko(":runDistributable") |
|
} |
|
|
|
@Test |
|
fun testUnpackSkikoFromUberJar() = with(testProject(TestProjects.unpackSkiko)) { |
|
enableJoinOutputJars() |
|
testUnpackSkiko(":runReleaseDistributable") |
|
} |
|
|
|
private fun TestProject.testUnpackSkiko(runDistributableTask: String) { |
|
gradle(runDistributableTask).checks { |
|
check.taskSuccessful(runDistributableTask) |
|
|
|
val libraryPathPattern = "Read skiko library path: '(.*)'".toRegex() |
|
val m = libraryPathPattern.find(check.log) |
|
val skikoDir = m?.groupValues?.get(1)?.let(::File) |
|
if (skikoDir == null || !skikoDir.exists()) { |
|
error("Invalid skiko path: $skikoDir") |
|
} |
|
val filesToFind = when (currentOS) { |
|
OS.Linux -> listOf("libskiko-linux-${currentArch.id}.so") |
|
OS.Windows -> listOf("skiko-windows-${currentArch.id}.dll", "icudtl.dat") |
|
OS.MacOS -> listOf("libskiko-macos-${currentArch.id}.dylib") |
|
} |
|
for (fileName in filesToFind) { |
|
skikoDir.resolve(fileName).checkExists() |
|
} |
|
} |
|
} |
|
|
|
@Test |
|
fun resources() = with(testProject(TestProjects.resources)) { |
|
gradle(":run").checks { |
|
check.taskSuccessful(":run") |
|
} |
|
|
|
gradle(":runDistributable").checks { |
|
check.taskSuccessful(":runDistributable") |
|
} |
|
} |
|
|
|
@Test |
|
fun testWixUnzip() { |
|
Assumptions.assumeTrue(currentOS == OS.Windows) { "The test is only relevant for Windows" } |
|
|
|
with(testProject(TestProjects.jvm)) { |
|
gradle(":unzipWix").checks { |
|
check.taskSuccessful(":unzipWix") |
|
|
|
file("build/wix311").checkExists() |
|
file("build/wix311/light.exe").checkExists() |
|
file("wix311").checkNotExists() |
|
} |
|
} |
|
} |
|
|
|
private fun TestProject.enableJoinOutputJars() { |
|
val enableJoinOutputJars = """ |
|
compose.desktop { |
|
application { |
|
buildTypes.release.proguard { |
|
joinOutputJars.set(true) |
|
} |
|
} |
|
} |
|
""".trimIndent() |
|
file("build.gradle").modify { "$it\n$enableJoinOutputJars" } |
|
} |
|
}
|
|
|