From 382ad5b78f2ccbde41e4f774db4a5f3ef3c6b5da Mon Sep 17 00:00:00 2001 From: Alexey Tsvetkov <654232+AlexeyTsvetkov@users.noreply.github.com> Date: Wed, 30 Nov 2022 14:37:05 +0100 Subject: [PATCH] Test Gradle plugin on relevant PRs (#2509) * Update Gradle used in tooling subprojects * Update Kotlin in Compose Gradle plugin * Decrease verbosity of Gradle plugin tests * Disable mac sign test * Add workflow to test Gradle plugin * Fix custom jdk tests on Linux * Make Compose Gradle plugin build compatible with Configuration cache * Print tests summary * Remove unused code * Refactor tests configuration * Turn off parallel execution * Try adding windows runner * Turn off fail fast * Fix Windows test issues #2368 * Adjust default proguard rules The following rule is needed to fix tests on Windows: ``` -dontwarn org.graalvm.compiler.core.aarch64.AArch64NodeMatchRules_MatchStatementSet* ``` Other rules are just to make builds less noisy. Kotlin's `*.internal` packages often contain bytecode, which triggers ProGuard's notes. However, these notes are not actionable for most users, so we can ignore notes by default. #2393 --- .github/workflows/gradle-plugin.yml | 35 +++++ gradle-plugins/build.gradle.kts | 12 +- .../src/main/kotlin/CheckJarPackagesTask.kt | 65 ++++++++ .../src/main/kotlin/SerializeClasspathTask.kt | 32 ++++ .../buildSrc/src/main/kotlin/gradleUtils.kt | 38 ++++- gradle-plugins/compose/build.gradle.kts | 87 ++--------- .../ComposeCompilerKotlinSupportPlugin.kt | 1 + .../application/tasks/AbstractJPackageTask.kt | 33 ++-- .../default-compose-desktop-rules.pro | 17 +- .../integration/DesktopApplicationTest.kt | 37 +++-- .../compose/test/utils/ComposeTestSummary.kt | 146 ++++++++++++++++++ .../compose/test/utils/TestProject.kt | 3 +- .../compose/test/utils/TestProperties.kt | 6 + ...it.platform.launcher.TestExecutionListener | 1 + .../androidx-compiler/build.gradle | 9 +- .../application/proguard/build.gradle | 9 +- .../application/proguard/rules.pro | 3 +- gradle-plugins/gradle.properties | 2 + .../gradle/wrapper/gradle-wrapper.properties | 2 +- gradle-plugins/preview-rpc/build.gradle.kts | 28 ++-- .../ui/tooling/preview/rpc/utils/utils.kt | 6 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- 23 files changed, 448 insertions(+), 128 deletions(-) create mode 100644 .github/workflows/gradle-plugin.yml create mode 100644 gradle-plugins/buildSrc/src/main/kotlin/CheckJarPackagesTask.kt create mode 100644 gradle-plugins/buildSrc/src/main/kotlin/SerializeClasspathTask.kt create mode 100644 gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/ComposeTestSummary.kt create mode 100644 gradle-plugins/compose/src/test/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener diff --git a/.github/workflows/gradle-plugin.yml b/.github/workflows/gradle-plugin.yml new file mode 100644 index 0000000000..daceebae01 --- /dev/null +++ b/.github/workflows/gradle-plugin.yml @@ -0,0 +1,35 @@ +name: Test Gradle plugin +on: + pull_request: + paths: + - 'gradle-plugins/**' + - '.github/workflows/gradle-plugin.yml' +jobs: + test-gradle-plugin: + strategy: + fail-fast: false + matrix: + os: [ubuntu-20.04, macos-12, windows-2022] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'corretto' + java-version: '16' + - name: Test Gradle plugin + shell: bash + run: | + cd gradle-plugins + ./gradlew assemble + ./gradlew :compose:check --continue + - name: Print summary + shell: bash + if: always() + run: | + cd gradle-plugins/compose/build/test-summary + for SUMMARY_FILE in `find . -name "*.md"`; do + FILE_NAME=`basename $SUMMARY_FILE` + echo "## $FILE_NAME" >> $GITHUB_STEP_SUMMARY + cat $SUMMARY_FILE >> $GITHUB_STEP_SUMMARY + done \ No newline at end of file diff --git a/gradle-plugins/build.gradle.kts b/gradle-plugins/build.gradle.kts index f05a4470ec..67e1db1fa5 100644 --- a/gradle-plugins/build.gradle.kts +++ b/gradle-plugins/build.gradle.kts @@ -1,7 +1,8 @@ import com.gradle.publish.PluginBundleExtension +import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile plugins { - val kotlinVersion = "1.5.30" + val kotlinVersion = "1.7.20" kotlin("jvm") version kotlinVersion apply false kotlin("plugin.serialization") version kotlinVersion apply false id("com.gradle.plugin-publish") version "0.17.0" apply false @@ -26,6 +27,15 @@ subprojects { } } + plugins.withId("org.jetbrains.kotlin.jvm") { + tasks.withType(KotlinJvmCompile::class).configureEach { + // must be set to a language version of the kotlin compiler & runtime, + // which is bundled to the oldest supported Gradle + kotlinOptions.languageVersion = "1.5" + kotlinOptions.apiVersion = "1.5" + } + } + plugins.withId("maven-publish") { configureIfExists { repositories { diff --git a/gradle-plugins/buildSrc/src/main/kotlin/CheckJarPackagesTask.kt b/gradle-plugins/buildSrc/src/main/kotlin/CheckJarPackagesTask.kt new file mode 100644 index 0000000000..d21125f2c7 --- /dev/null +++ b/gradle-plugins/buildSrc/src/main/kotlin/CheckJarPackagesTask.kt @@ -0,0 +1,65 @@ +/* + * 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. + */ + +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFile +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.TaskAction +import java.util.* +import java.util.zip.ZipFile +import javax.inject.Inject + +/** + * Checks that every class in a [jarFile] matches one of [allowedPackagePrefixes] + */ +abstract class CheckJarPackagesTask @Inject constructor( + objects: ObjectFactory +) : DefaultTask() { + @get:InputFile + val jarFile: Property = objects.fileProperty() + + @get:Input + val allowedPackagePrefixes: SetProperty = objects.setProperty(String::class.java) + + @TaskAction + fun run() { + ZipFile(jarFile.get().asFile).use { zip -> + checkJarContainsExpectedPackages(zip) + } + } + + private fun checkJarContainsExpectedPackages(jar: ZipFile) { + val unexpectedClasses = arrayListOf() + val allowedPrefixes = allowedPackagePrefixes.get().map { it.replace(".", "/") } + + for (entry in jar.entries()) { + if (entry.isDirectory || !entry.name.endsWith(".class")) continue + + if (allowedPrefixes.none { prefix -> entry.name.startsWith(prefix) }) { + unexpectedClasses.add(entry.name) + } + } + + if (unexpectedClasses.any()) { + error(buildString { + appendLine("All classes in ${jar.name} must match allowed prefixes:") + allowedPrefixes.forEach { + appendLine(" * $it") + } + appendLine("Non-valid classes:") + val unexpectedGroups = unexpectedClasses + .groupByTo(TreeMap()) { it.substringBeforeLast("/") } + for ((_, classes) in unexpectedGroups) { + appendLine(" * ${classes.first()}") + } + }) + } + } +} + diff --git a/gradle-plugins/buildSrc/src/main/kotlin/SerializeClasspathTask.kt b/gradle-plugins/buildSrc/src/main/kotlin/SerializeClasspathTask.kt new file mode 100644 index 0000000000..83aa492059 --- /dev/null +++ b/gradle-plugins/buildSrc/src/main/kotlin/SerializeClasspathTask.kt @@ -0,0 +1,32 @@ +/* + * 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. + */ + +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import java.io.File +import javax.inject.Inject + +abstract class SerializeClasspathTask @Inject constructor( + objects: ObjectFactory +) : DefaultTask() { + @get:InputFiles + val classpathFileCollection: ConfigurableFileCollection = objects.fileCollection() + + @get:OutputFile + val outputFile: RegularFileProperty = objects.fileProperty() + + @TaskAction + fun run() { + val classpath = classpathFileCollection.files.joinToString(File.pathSeparator) { it.absolutePath } + val outputFile = outputFile.get().asFile + outputFile.parentFile.mkdirs() + outputFile.writeText(classpath) + } +} \ No newline at end of file diff --git a/gradle-plugins/buildSrc/src/main/kotlin/gradleUtils.kt b/gradle-plugins/buildSrc/src/main/kotlin/gradleUtils.kt index eca0997c01..afe7cf1c34 100644 --- a/gradle-plugins/buildSrc/src/main/kotlin/gradleUtils.kt +++ b/gradle-plugins/buildSrc/src/main/kotlin/gradleUtils.kt @@ -5,10 +5,17 @@ import org.gradle.api.JavaVersion import org.gradle.api.Project +import org.gradle.api.Task import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.api.file.RegularFile import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskContainer +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.bundling.Jar import org.gradle.api.tasks.testing.Test import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.withType import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform import java.io.File @@ -35,13 +42,14 @@ fun Test.configureJavaForComposeTest() { } } -fun Project.configureJUnit() { +fun Project.configureAllTests(fn: Test.() -> Unit = {}) { fun DependencyHandler.testImplementation(notation: Any) = add(JavaPlugin.TEST_IMPLEMENTATION_CONFIGURATION_NAME, notation) dependencies { testImplementation(platform("org.junit:junit-bom:5.7.0")) testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.junit.platform:junit-platform-launcher") } tasks.withType().configureEach { @@ -49,5 +57,31 @@ fun Project.configureJUnit() { testLogging { events("passed", "skipped", "failed") } + fn() } -} \ No newline at end of file +} + +fun Test.systemProperties(map: Map) { + for ((k, v) in map) { + systemProperty(k, v) + } +} + +fun TaskProvider<*>.dependsOn(vararg dependencies: Any) { + configure { + dependsOn(dependencies) + } +} + +inline fun TaskContainer.registerVerificationTask( + name: String, + crossinline fn: T.() -> Unit +): TaskProvider = + register(name, T::class) { + fn() + }.apply { + named("check").dependsOn(this) + } + +val Provider.archiveFile: Provider + get() = flatMap { it.archiveFile } \ No newline at end of file diff --git a/gradle-plugins/compose/build.gradle.kts b/gradle-plugins/compose/build.gradle.kts index 0bc5c7f91a..60237a7cad 100644 --- a/gradle-plugins/compose/build.gradle.kts +++ b/gradle-plugins/compose/build.gradle.kts @@ -1,6 +1,4 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar -import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.getCurrentOperatingSystem -import java.util.zip.ZipFile plugins { kotlin("jvm") @@ -61,8 +59,6 @@ dependencies { compileOnly(kotlin("native-utils")) testImplementation(gradleTestKit()) - testImplementation(platform("org.junit:junit-bom:5.7.0")) - testImplementation("org.junit.jupiter:junit-jupiter") testImplementation(kotlin("gradle-plugin-api")) // include relocated download task to avoid potential runtime conflicts @@ -89,62 +85,17 @@ val jar = tasks.named("jar") { this.duplicatesStrategy = DuplicatesStrategy.INCLUDE } -// __SUPPORTED_GRADLE_VERSIONS__ -//testGradleVersion("6.7.1") // min supported by kotlin 1.7.0 gradle plugin https://kotlinlang.org/docs/gradle.html -// despite that, some tests didn't pass -testGradleVersion("7.1.1") -testGradleVersion("7.3.3") - -val javaHomeForTests: String? = when { - // __COMPOSE_NATIVE_DISTRIBUTIONS_MIN_JAVA_VERSION__ - JavaVersion.current() >= JavaVersion.VERSION_15 -> System.getProperty("java.home") - else -> System.getenv("JDK_15") - ?: System.getenv("JDK_FOR_GRADLE_TESTS") -} -val isWindows = getCurrentOperatingSystem().isWindows +val supportedGradleVersions = project.property("compose.tests.gradle.versions") + .toString().split(",") + .map { it.trim() } val gradleTestsPattern = "org.jetbrains.compose.test.tests.integration.*" // check we don't accidentally including unexpected classes (e.g. from embedded dependencies) -val checkJar by tasks.registering { +tasks.registerVerificationTask("checkJar") { dependsOn(jar) - - doLast { - val file = jar.get().archiveFile.get().asFile - ZipFile(file).use { zip -> - checkJarContainsExpectedPackages(zip) - } - } -} - -// we want to avoid accidentally including unexpected jars/packages, e.g kotlin-stdlib etc -fun checkJarContainsExpectedPackages(jar: ZipFile) { - val expectedPackages = arrayOf( - "org/jetbrains/compose", - "kotlinx/serialization" - ) - val unexpectedClasses = arrayListOf() - - for (entry in jar.entries()) { - if (entry.isDirectory || !entry.name.endsWith(".class")) continue - - if (expectedPackages.none { prefix -> entry.name.startsWith(prefix) }) { - unexpectedClasses.add(entry.name) - } - } - - if (unexpectedClasses.any()) { - error(buildString { - appendLine("Some classes from ${jar.name} are not from 'org.jetbrains.compose' package:") - unexpectedClasses.forEach { - appendLine(" * $it") - } - }) - } -} - -tasks.check { - dependsOn(checkJar) + jarFile.set(jar.archiveFile) + allowedPackagePrefixes.addAll("org.jetbrains.compose", "kotlinx.serialization") } tasks.test { @@ -154,34 +105,24 @@ tasks.test { excludeTestsMatching(gradleTestsPattern) } } -fun testGradleVersion(gradleVersion: String) { - val taskProvider = tasks.register("testGradle-$gradleVersion", Test::class) { - tasks.test.get().let { defaultTest -> - classpath = defaultTest.classpath - } + +for (gradleVersion in supportedGradleVersions) { + tasks.registerVerificationTask("testGradle-$gradleVersion") { + classpath = tasks.test.get().classpath systemProperty("compose.tests.gradle.version", gradleVersion) filter { includeTestsMatching(gradleTestsPattern) } } - tasks.named("check") { - dependsOn(taskProvider) - } } -configureJUnit() - -tasks.withType().configureEach { +configureAllTests { configureJavaForComposeTest() - dependsOn(":publishToMavenLocal") - systemProperty("compose.tests.compose.gradle.plugin.version", BuildProperties.deployVersion(project)) - for ((k, v) in project.properties) { - if (k.startsWith("compose.")) { - systemProperty(k, v.toString()) - } - } + val summaryDir = project.buildDir.resolve("test-summary") + systemProperty("compose.tests.summary.file", summaryDir.resolve("$name.md").absolutePath) + systemProperties(project.properties.filter { it.key.startsWith("compose.") }) } task("printAllAndroidxReplacements") { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeCompilerKotlinSupportPlugin.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeCompilerKotlinSupportPlugin.kt index d2855675c3..fe9e9d55d2 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeCompilerKotlinSupportPlugin.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeCompilerKotlinSupportPlugin.kt @@ -44,6 +44,7 @@ class ComposeCompilerKotlinSupportPlugin : KotlinCompilerPluginSupportPlugin { KotlinPlatformType.js -> isApplicableJsTarget(kotlinCompilation.target) KotlinPlatformType.androidJvm -> true KotlinPlatformType.native -> true + KotlinPlatformType.wasm -> false } private fun isApplicableJsTarget(kotlinTarget: KotlinTarget): Boolean { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt index e0a4b796a8..c8ad3331ea 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt @@ -316,10 +316,8 @@ abstract class AbstractJPackageTask @Inject constructor( cliArg("--main-jar", mappedJar) cliArg("--main-class", launcherMainClass) - when (currentOS) { - OS.Windows -> { - cliArg("--win-console", winConsole) - } + if (currentOS == OS.Windows) { + cliArg("--win-console", winConsole) } cliArg("--icon", iconFile) launcherArgs.orNull?.forEach { @@ -369,6 +367,7 @@ abstract class AbstractJPackageTask @Inject constructor( cliArg("--win-menu-group", winMenuGroup) cliArg("--win-upgrade-uuid", winUpgradeUuid) } + OS.MacOS -> {} } } @@ -383,20 +382,18 @@ abstract class AbstractJPackageTask @Inject constructor( cliArg("--app-version", packageVersion) cliArg("--vendor", packageVendor) - when (currentOS) { - OS.MacOS -> { - cliArg("--mac-package-name", macPackageName) - cliArg("--mac-package-identifier", nonValidatedMacBundleID) - cliArg("--mac-app-store", macAppStore) - cliArg("--mac-app-category", macAppCategory) - cliArg("--mac-entitlements", macEntitlementsFile) - - macSigner?.let { signer -> - cliArg("--mac-sign", true) - cliArg("--mac-signing-key-user-name", signer.settings.identity) - cliArg("--mac-signing-keychain", signer.settings.keychain) - cliArg("--mac-package-signing-prefix", signer.settings.prefix) - } + if (currentOS == OS.MacOS) { + cliArg("--mac-package-name", macPackageName) + cliArg("--mac-package-identifier", nonValidatedMacBundleID) + cliArg("--mac-app-store", macAppStore) + cliArg("--mac-app-category", macAppCategory) + cliArg("--mac-entitlements", macEntitlementsFile) + + macSigner?.let { signer -> + cliArg("--mac-sign", true) + cliArg("--mac-signing-key-user-name", signer.settings.identity) + cliArg("--mac-signing-keychain", signer.settings.keychain) + cliArg("--mac-package-signing-prefix", signer.settings.prefix) } } } diff --git a/gradle-plugins/compose/src/main/resources/default-compose-desktop-rules.pro b/gradle-plugins/compose/src/main/resources/default-compose-desktop-rules.pro index 2ef55d7a5e..89455d78d3 100644 --- a/gradle-plugins/compose/src/main/resources/default-compose-desktop-rules.pro +++ b/gradle-plugins/compose/src/main/resources/default-compose-desktop-rules.pro @@ -30,4 +30,19 @@ -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement # https://github.com/Kotlin/kotlinx.coroutines/issues/2046 --dontwarn android.annotation.SuppressLint \ No newline at end of file +-dontwarn android.annotation.SuppressLint + +# https://github.com/JetBrains/compose-jb/issues/2393 +-dontnote kotlin.coroutines.jvm.internal.** +-dontnote kotlin.internal.** +-dontnote kotlin.jvm.internal.** +-dontnote kotlin.reflect.** +-dontnote kotlinx.coroutines.debug.internal.** +-dontnote kotlinx.coroutines.internal.** +-keep class kotlin.coroutines.Continuation +-keep class kotlinx.coroutines.CancellableContinuation +-keep class kotlinx.coroutines.channels.Channel +-keep class kotlinx.coroutines.CoroutineDispatcher +-keep class kotlinx.coroutines.CoroutineScope +# this is a weird one, but breaks build on some combinations of OS and JDK (reproduced on Windows 10 + Corretto 16) +-dontwarn org.graalvm.compiler.core.aarch64.AArch64NodeMatchRules_MatchStatementSet* diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt index 9c1d45e5c6..93cd24085b 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt @@ -18,6 +18,7 @@ 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() { @@ -77,7 +78,11 @@ class DesktopApplicationTest : GradlePluginTestBase() { } @Test - fun proguard(): Unit = with(testProject(TestProjects.proguard)) { + fun proguard(): Unit = with( + testProject( + TestProjects.proguard, + testEnvironment = defaultTestEnvironment.copy(composeVerbose = false)) + ) { val enableObfuscation = """ compose.desktop { application { @@ -117,11 +122,6 @@ class DesktopApplicationTest : GradlePluginTestBase() { } } - @Test - fun packageJvm() = with(testProject(TestProjects.jvm)) { - testPackageJvmDistributions() - } - @Test fun gradleBuildCache() = with(testProject(TestProjects.jvm)) { modifyGradleProperties { @@ -148,6 +148,11 @@ class DesktopApplicationTest : GradlePluginTestBase() { } } + @Test + fun packageJvm() = with(testProject(TestProjects.jvm)) { + testPackageJvmDistributions() + } + @Test fun packageMpp() = with(testProject(TestProjects.mpp)) { testPackageJvmDistributions() @@ -174,11 +179,19 @@ class DesktopApplicationTest : GradlePluginTestBase() { val packageFile = packageDirFiles.single() if (currentOS == OS.Linux) { - val isTestPackage = packageFile.name.contains("test-package", ignoreCase = true) || - packageFile.name.contains("testpackage", ignoreCase = true) - val isDeb = packageFile.name.endsWith(".$ext") - check(isTestPackage && isDeb) { - "Expected contain testpackage*.deb or test-package*.deb package in $packageDir, got '${packageFile.name}'" + // 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") @@ -285,6 +298,8 @@ class DesktopApplicationTest : GradlePluginTestBase() { } @Test + @Disabled + // the test does not work on CI and locally unless test keychain is opened manually fun testMacSign() { Assumptions.assumeTrue(currentOS == OS.MacOS) diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/ComposeTestSummary.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/ComposeTestSummary.kt new file mode 100644 index 0000000000..4839927daa --- /dev/null +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/ComposeTestSummary.kt @@ -0,0 +1,146 @@ +/* + * 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.utils + +import org.junit.platform.engine.TestExecutionResult +import org.junit.platform.engine.support.descriptor.MethodSource +import org.junit.platform.launcher.TestExecutionListener +import org.junit.platform.launcher.TestIdentifier +import org.junit.platform.launcher.TestPlan +import java.io.File +import java.io.Writer + +class ComposeTestSummary : TestExecutionListener { + private val summaryFile = TestProperties.summaryFile + private val isEnabled = summaryFile != null + private val startNanoTime = hashMapOf() + private val results = arrayListOf() + + override fun executionStarted(testIdentifier: TestIdentifier) { + if (isEnabled && testIdentifier.isTest) { + startNanoTime[testIdentifier] = System.nanoTime() + } + } + + override fun executionSkipped(testIdentifier: TestIdentifier, reason: String?) { + if (isEnabled && testIdentifier.isTest) { + addTestResult(testIdentifier, TestResult.Status.Skipped, durationMs = null) + } + } + + override fun executionFinished(testIdentifier: TestIdentifier, testExecutionResult: TestExecutionResult) { + if (isEnabled && testIdentifier.isTest) { + val durationMs = (System.nanoTime() - startNanoTime[testIdentifier]!!) / 1_000_000 + val status = when (testExecutionResult.status!!) { + TestExecutionResult.Status.SUCCESSFUL -> TestResult.Status.Successful + TestExecutionResult.Status.ABORTED -> TestResult.Status.Aborted + TestExecutionResult.Status.FAILED -> + TestResult.Status.Failed( + testExecutionResult.throwable.orElse(null) + ) + } + addTestResult(testIdentifier, status, durationMs = durationMs) + } + } + + override fun testPlanExecutionFinished(testPlan: TestPlan) { + if (isEnabled) { + MarkdownSummary.write(results, summaryFile!!) + } + } + + private fun addTestResult( + identifier: TestIdentifier, + status: TestResult.Status, + durationMs: Long? + ) { + val result = TestResult( + testCase = identifier.displayName, + testClass = (identifier.source.get() as? MethodSource)?.className ?: "", + status = status, + durationMs = durationMs + ) + results.add(result) + } +} + +internal data class TestResult( + val testCase: String, + val testClass: String, + val durationMs: Long?, + val status: Status +) { + sealed class Status { + object Successful : Status() + object Aborted : Status() + object Skipped : Status() + class Failed(val exception: Throwable?) : Status() + } + + val displayName: String + get() = "${testClass.substringAfterLast(".")}.$testCase" +} + +internal object MarkdownSummary { + fun write(testResults: List, file: File) { + file.parentFile.mkdirs() + file.bufferedWriter().use { writer -> + writer.writeSummary(testResults) + } + } + + private fun Writer.writeSummary(testResults: List) { + writeLn() + writeLn("|Status|Test case|Duration|") + writeLn("|---|---|---:|") + + for (result in testResults) { + val status = when (result.status) { + is TestResult.Status.Successful -> ":white_check_mark:" + is TestResult.Status.Aborted -> ":fast_forward:" + is TestResult.Status.Failed -> ":x:" + is TestResult.Status.Skipped -> ":fast_forward:" + } + writeLn("|$status|${result.displayName}|${result.durationMs ?: 0} ms|") + } + + val failedTests = testResults.filter { it.status is TestResult.Status.Failed } + if (failedTests.isEmpty()) return + + writeLn("#### ${failedTests.size} failed tests") + for (failedTest in failedTests) { + withDetails(failedTest.displayName) { + withHtmlTag("samp") { + val exception = (failedTest.status as TestResult.Status.Failed).exception + val stacktrace = exception?.stackTraceToString() ?: "" + write(stacktrace.replace("\n", "
")) + } + } + writeLn() + } + } + + private inline fun Writer.withDetails(summary: String, details: Writer.() -> Unit) { + withHtmlTag("details") { + withHtmlTag("summary") { + write(summary) + } + + details() + } + } + + private inline fun Writer.withHtmlTag(tag: String, fn: Writer.() -> Unit) { + writeLn("<$tag>") + fn() + writeLn("") + } + + private fun Writer.writeLn(str: String = "") { + write(str) + write("\n") + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProject.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProject.kt index 47cecdd1ce..a73c4f4a30 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProject.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProject.kt @@ -15,6 +15,7 @@ data class TestEnvironment( val kotlinVersion: String = TestKotlinVersions.Default, val composeGradlePluginVersion: String = TestProperties.composeGradlePluginVersion, val composeCompilerArtifact: String? = null, + val composeVerbose: Boolean = true ) { private val placeholders = linkedMapOf( "COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER" to composeGradlePluginVersion, @@ -41,7 +42,7 @@ class TestProject( private val additionalArgs = listOf( "--stacktrace", "--init-script", testProjectsRootDir.resolve("init.gradle").absolutePath, - "-P${ComposeProperties.VERBOSE}=true" + "-P${ComposeProperties.VERBOSE}=${testEnvironment.composeVerbose}" ) init { diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProperties.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProperties.kt index fe857842f8..ef4bd4423d 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProperties.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProperties.kt @@ -5,6 +5,8 @@ package org.jetbrains.compose.test.utils +import java.io.File + object TestProperties { val composeCompilerVersion: String get() = notNullSystemProperty("compose.tests.compiler.version") @@ -27,6 +29,10 @@ object TestProperties { val gradleVersionForTests: String? get() = System.getProperty("compose.tests.gradle.version") + val summaryFile: File? + get() = System.getProperty("compose.tests.summary.file")?.let { File(it) } + + private fun notNullSystemProperty(property: String): String = System.getProperty(property) ?: error("The '$property' system property is not set") } diff --git a/gradle-plugins/compose/src/test/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener b/gradle-plugins/compose/src/test/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener new file mode 100644 index 0000000000..bee2b4f9cb --- /dev/null +++ b/gradle-plugins/compose/src/test/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener @@ -0,0 +1 @@ +org.jetbrains.compose.test.utils.ComposeTestSummary \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/androidx-compiler/build.gradle b/gradle-plugins/compose/src/test/test-projects/application/androidx-compiler/build.gradle index 1bb4fcdef8..3b66bdad8b 100644 --- a/gradle-plugins/compose/src/test/test-projects/application/androidx-compiler/build.gradle +++ b/gradle-plugins/compose/src/test/test-projects/application/androidx-compiler/build.gradle @@ -1,3 +1,4 @@ +import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { @@ -21,11 +22,15 @@ compose { desktop { application { mainClass = "Main" - args(project.projectDir.absolutePath) nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) } - args(project.projectDir.absolutePath) + + def projectPath = project.projectDir.absolutePath + if (DefaultNativePlatform.currentOperatingSystem.isWindows()) { + projectPath = projectPath.replace("\\", "\\\\") + } + args(projectPath) } } } diff --git a/gradle-plugins/compose/src/test/test-projects/application/proguard/build.gradle b/gradle-plugins/compose/src/test/test-projects/application/proguard/build.gradle index dfa5d41cc5..9a16e0469e 100644 --- a/gradle-plugins/compose/src/test/test-projects/application/proguard/build.gradle +++ b/gradle-plugins/compose/src/test/test-projects/application/proguard/build.gradle @@ -1,3 +1,4 @@ +import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { @@ -17,11 +18,15 @@ dependencies { compose.desktop { application { mainClass = "Main" - args(project.projectDir.absolutePath) nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) } - args(project.projectDir.absolutePath) + + def projectPath = project.projectDir.absolutePath + if (DefaultNativePlatform.currentOperatingSystem.isWindows()) { + projectPath = projectPath.replace("\\", "\\\\") + } + args(projectPath) buildTypes.release.proguard { configurationFiles.from("rules.pro") diff --git a/gradle-plugins/compose/src/test/test-projects/application/proguard/rules.pro b/gradle-plugins/compose/src/test/test-projects/application/proguard/rules.pro index 4e0f5c6865..1abc1a6fb8 100644 --- a/gradle-plugins/compose/src/test/test-projects/application/proguard/rules.pro +++ b/gradle-plugins/compose/src/test/test-projects/application/proguard/rules.pro @@ -1,3 +1,4 @@ -keep public class Main { public void keptByKeepRule(...); -} \ No newline at end of file +} +-dontnote \ No newline at end of file diff --git a/gradle-plugins/gradle.properties b/gradle-plugins/gradle.properties index 53f2223ab6..9679715ba7 100644 --- a/gradle-plugins/gradle.properties +++ b/gradle-plugins/gradle.properties @@ -14,6 +14,8 @@ compose.tests.js.compiler.compatible.kotlin.version=1.7.20 # https://developer.android.com/jetpack/androidx/releases/compose-kotlin compose.tests.androidx.compiler.version=1.1.1 compose.tests.androidx.compiler.compatible.kotlin.version=1.6.10 +# __SUPPORTED_GRADLE_VERSIONS__ +compose.tests.gradle.versions=7.0.2, 7.6 # A version of Gradle plugin, that will be published, # unless overridden by COMPOSE_GRADLE_PLUGIN_VERSION env var. diff --git a/gradle-plugins/gradle/wrapper/gradle-wrapper.properties b/gradle-plugins/gradle/wrapper/gradle-wrapper.properties index ae04661ee7..070cb702f0 100644 --- a/gradle-plugins/gradle/wrapper/gradle-wrapper.properties +++ b/gradle-plugins/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradle-plugins/preview-rpc/build.gradle.kts b/gradle-plugins/preview-rpc/build.gradle.kts index b4945c5ab4..bea6f46c06 100644 --- a/gradle-plugins/preview-rpc/build.gradle.kts +++ b/gradle-plugins/preview-rpc/build.gradle.kts @@ -13,20 +13,24 @@ dependencies { implementation(kotlin("stdlib")) } -configureJUnit() +configureAllTests() + +val serializeClasspath by tasks.registering(SerializeClasspathTask::class) { + val runtimeClasspath = configurations.runtimeClasspath + val jar = tasks.jar + dependsOn(runtimeClasspath, jar) + + classpathFileCollection.from(jar.flatMap { it.archiveFile }) + classpathFileCollection.from(runtimeClasspath) + outputFile.set(project.layout.buildDirectory.file("rpc.classpath.txt")) +} tasks.test.configure { configureJavaForComposeTest() - val runtimeClasspath = configurations.runtimeClasspath - dependsOn(runtimeClasspath) - val jar = tasks.jar - dependsOn(jar) - doFirst { - val rpcClasspath = LinkedHashSet() - rpcClasspath.add(jar.get().archiveFile.get().asFile) - rpcClasspath.addAll(runtimeClasspath.get().files) - val classpathString = rpcClasspath.joinToString(File.pathSeparator) { it.absolutePath } - systemProperty("org.jetbrains.compose.test.rpc.classpath", classpathString) - } + dependsOn(serializeClasspath) + systemProperty( + "org.jetbrains.compose.tests.rpc.classpath.file", + serializeClasspath.get().outputFile.get().asFile.absolutePath + ) } \ No newline at end of file diff --git a/gradle-plugins/preview-rpc/src/test/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/utils/utils.kt b/gradle-plugins/preview-rpc/src/test/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/utils/utils.kt index b99eded9d6..8c390bbf7a 100644 --- a/gradle-plugins/preview-rpc/src/test/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/utils/utils.kt +++ b/gradle-plugins/preview-rpc/src/test/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/utils/utils.kt @@ -5,6 +5,8 @@ package org.jetbrains.compose.desktop.ui.tooling.preview.rpc.utils +import java.io.File + internal fun systemProperty(name: String): String = System.getProperty(name) ?: error("System property is not found: '$name'") @@ -12,7 +14,9 @@ internal val isWindows = systemProperty("os.name").startsWith("windows", ignoreCase = true) internal val previewTestClaspath: String - get() = systemProperty("org.jetbrains.compose.test.rpc.classpath") + get() = systemProperty("org.jetbrains.compose.tests.rpc.classpath.file").let { + File(it).readText() + } internal val Int.secondsAsMillis: Int get() = this * 1000 diff --git a/idea-plugin/gradle/wrapper/gradle-wrapper.properties b/idea-plugin/gradle/wrapper/gradle-wrapper.properties index 2e6e5897b5..070cb702f0 100644 --- a/idea-plugin/gradle/wrapper/gradle-wrapper.properties +++ b/idea-plugin/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/tooling/gradle/wrapper/gradle-wrapper.properties b/tooling/gradle/wrapper/gradle-wrapper.properties index 2e6e5897b5..070cb702f0 100644 --- a/tooling/gradle/wrapper/gradle-wrapper.properties +++ b/tooling/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists