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.
461 lines
18 KiB
461 lines
18 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.gradle.testkit.runner.TaskOutcome |
|
import org.jetbrains.compose.desktop.application.internal.* |
|
import org.jetbrains.compose.internal.uppercaseFirstChar |
|
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 |
|
|
|
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("runDistributable").doFirst { |
|
throw new StopExecutionException("Skip runDistributable task") |
|
} |
|
} |
|
""".trimIndent() |
|
} |
|
gradle("run").build().let { result -> |
|
assertEquals(TaskOutcome.SUCCESS, result.task(":run")?.outcome) |
|
} |
|
gradle("runDistributable").build().let { result -> |
|
assertEquals(TaskOutcome.SUCCESS, result.task(":createDistributable")!!.outcome) |
|
assertEquals(TaskOutcome.SUCCESS, result.task(":runDistributable")?.outcome) |
|
} |
|
} |
|
|
|
@Test |
|
fun testRunMpp() = with(testProject(TestProjects.mpp)) { |
|
val logLine = "Kotlin MPP app is running!" |
|
gradle("run").build().checks { check -> |
|
check.taskOutcome(":run", TaskOutcome.SUCCESS) |
|
check.logContains(logLine) |
|
} |
|
gradle("runDistributable").build().checks { check -> |
|
check.taskOutcome(":createDistributable", TaskOutcome.SUCCESS) |
|
check.taskOutcome(":runDistributable", TaskOutcome.SUCCESS) |
|
check.logContains(logLine) |
|
} |
|
} |
|
|
|
@Test |
|
fun testAndroidxCompiler() = with(testProject(TestProjects.androidxCompiler, defaultAndroidxCompilerEnvironment)) { |
|
gradle(":runDistributable").build().checks { check -> |
|
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'" |
|
} |
|
} |
|
} |
|
|
|
@Test |
|
fun kotlinDsl(): Unit = with(testProject(TestProjects.jvmKotlinDsl)) { |
|
gradle(":packageDistributionForCurrentOS", "--dry-run").build() |
|
gradle(":packageReleaseDistributionForCurrentOS", "--dry-run").build() |
|
} |
|
|
|
@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").build().checks { check -> |
|
check.taskOutcome(":proguardReleaseJars", TaskOutcome.SUCCESS) |
|
checkImageAfterBuild() |
|
assertEqualTextFiles(file("main-methods.actual.txt"), file("main-methods.expected.txt")) |
|
} |
|
|
|
file("build.gradle").modify { "$it\n$enableObfuscation" } |
|
actualMainImage.delete() |
|
checkImageBeforeBuild() |
|
gradle(":runReleaseDistributable").build().checks { check -> |
|
check.taskOutcome(":proguardReleaseJars", TaskOutcome.SUCCESS) |
|
checkImageAfterBuild() |
|
assertNotEqualTextFiles(file("main-methods.actual.txt"), file("main-methods.expected.txt")) |
|
} |
|
} |
|
|
|
@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).build().checks { check -> |
|
check.taskOutcome(packagingTask, TaskOutcome.SUCCESS) |
|
} |
|
|
|
gradle("clean", packagingTask).build().checks { check -> |
|
check.taskOutcome(":checkRuntime", TaskOutcome.FROM_CACHE) |
|
check.taskOutcome(packagingTask, TaskOutcome.SUCCESS) |
|
} |
|
} |
|
|
|
@Test |
|
fun packageJvm() = with(testProject(TestProjects.jvm)) { |
|
testPackageJvmDistributions() |
|
} |
|
|
|
@Test |
|
fun packageMpp() = with(testProject(TestProjects.mpp)) { |
|
testPackageJvmDistributions() |
|
} |
|
|
|
|
|
private fun TestProject.testPackageJvmDistributions() { |
|
val result = gradle(":packageDistributionForCurrentOS").build() |
|
|
|
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") |
|
} |
|
assertEquals(TaskOutcome.SUCCESS, result.task(":package${ext.uppercaseFirstChar()}")?.outcome) |
|
assertEquals(TaskOutcome.SUCCESS, result.task(":packageDistributionForCurrentOS")?.outcome) |
|
} |
|
|
|
@Test |
|
fun testJdk15() = with(customJdkProject(15)) { |
|
testPackageJvmDistributions() |
|
} |
|
@Test |
|
fun testJdk18() = with(customJdkProject(18)) { |
|
testPackageJvmDistributions() |
|
} |
|
|
|
@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() |
|
} |
|
|
|
@Test |
|
fun packageUberJarForCurrentOSMpp() = with(testProject(TestProjects.mpp)) { |
|
testPackageUberJarForCurrentOS() |
|
} |
|
|
|
private fun TestProject.testPackageUberJarForCurrentOS() { |
|
gradle(":packageUberJarForCurrentOS").build().let { result -> |
|
assertEquals(TaskOutcome.SUCCESS, result.task(":packageUberJarForCurrentOS")?.outcome) |
|
|
|
val resultJarFile = file("build/compose/jars/TestPackage-${currentTarget.id}-1.0.0.jar") |
|
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").build().checks { check -> |
|
check.taskOutcome(":app:createDistributable", TaskOutcome.SUCCESS) |
|
check.taskOutcome(":app:runDistributable", TaskOutcome.SUCCESS) |
|
check.logContains("Called lib1#util()") |
|
check.logContains("Called lib2#util()") |
|
} |
|
} |
|
|
|
@Test |
|
fun testJavaLogger() = with(testProject(TestProjects.javaLogger)) { |
|
gradle(":runDistributable").build().checks { check -> |
|
check.taskOutcome(":runDistributable", TaskOutcome.SUCCESS) |
|
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").build().checks { check -> |
|
check.taskOutcome(":runDistributable", TaskOutcome.SUCCESS) |
|
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 |
|
@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").build().checks { check -> |
|
check.taskOutcome(":createDistributable", TaskOutcome.SUCCESS) |
|
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").build().checks { check -> |
|
check.taskOutcome(":runDistributable", TaskOutcome.SUCCESS) |
|
check.logContains("Signed app successfully started!") |
|
} |
|
} |
|
} |
|
} |
|
|
|
@Test |
|
fun testOptionsWithSpaces() { |
|
with(testProject(TestProjects.optionsWithSpaces)) { |
|
fun testRunTask(runTask: String) { |
|
gradle(runTask).build().checks { check -> |
|
check.taskOutcome(runTask, TaskOutcome.SUCCESS) |
|
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").build().checks { check -> |
|
check.taskOutcome(":packageDistributionForCurrentOS", TaskOutcome.SUCCESS) |
|
} |
|
} |
|
} |
|
|
|
@Test |
|
fun testDefaultArgs() { |
|
with(testProject(TestProjects.defaultArgs)) { |
|
fun testRunTask(runTask: String) { |
|
gradle(runTask).build().checks { check -> |
|
check.taskOutcome(runTask, TaskOutcome.SUCCESS) |
|
check.logContains("compose.application.configure.swing.globals=true") |
|
} |
|
} |
|
|
|
testRunTask(":runDistributable") |
|
testRunTask(":run") |
|
|
|
gradle(":packageDistributionForCurrentOS").build().checks { check -> |
|
check.taskOutcome(":packageDistributionForCurrentOS", TaskOutcome.SUCCESS) |
|
} |
|
} |
|
} |
|
|
|
@Test |
|
fun testDefaultArgsOverride() { |
|
with(testProject(TestProjects.defaultArgsOverride)) { |
|
fun testRunTask(runTask: String) { |
|
gradle(runTask).build().checks { check -> |
|
check.taskOutcome(runTask, TaskOutcome.SUCCESS) |
|
check.logContains("compose.application.configure.swing.globals=false") |
|
} |
|
} |
|
|
|
testRunTask(":runDistributable") |
|
testRunTask(":run") |
|
|
|
gradle(":packageDistributionForCurrentOS").build().checks { check -> |
|
check.taskOutcome(":packageDistributionForCurrentOS", TaskOutcome.SUCCESS) |
|
} |
|
} |
|
} |
|
|
|
@Test |
|
fun testSuggestModules() { |
|
with(testProject(TestProjects.jvm)) { |
|
gradle(":suggestRuntimeModules").build().checks { check -> |
|
check.taskOutcome(":suggestRuntimeModules", TaskOutcome.SUCCESS) |
|
check.logContains("Suggested runtime modules to include:") |
|
check.logContains("modules(\"java.instrument\", \"jdk.unsupported\")") |
|
} |
|
} |
|
} |
|
|
|
@Test |
|
fun testUnpackSkiko() { |
|
with(testProject(TestProjects.unpackSkiko)) { |
|
gradle(":runDistributable").build().checks { check -> |
|
check.taskOutcome(":runDistributable", TaskOutcome.SUCCESS) |
|
|
|
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").build().checks { check -> |
|
check.taskOutcome(":run", TaskOutcome.SUCCESS) |
|
} |
|
|
|
gradle(":runDistributable").build().checks { check -> |
|
check.taskOutcome(":runDistributable", TaskOutcome.SUCCESS) |
|
} |
|
} |
|
}
|
|
|