From c76980f3916ea156dccc1affa7d9b01cff7db6ac Mon Sep 17 00:00:00 2001 From: Geoff Powell Date: Fri, 23 Aug 2024 14:15:05 -0400 Subject: [PATCH] Add support for configuration caching --- .../api/paparazzi-annotations.api | 13 ++- .../cash/paparazzi/gradle/PaparazziPlugin.kt | 12 +- .../paparazzi/gradle/utils/PreviewUtils.kt | 107 +++++++++--------- .../paparazzi/gradle/PaparazziPluginTest.kt | 55 +++++++-- ...review[HelloPaparazzi_HelloPaparazzi].png} | Bin .../settings.gradle | 7 ++ .../paparazzi/plugin/test/HelloPaparazzi.kt | 37 ------ .../paparazzi/plugin/test/HelloPaparazzi.kt | 4 +- 8 files changed, 126 insertions(+), 109 deletions(-) rename paparazzi-gradle-plugin/src/test/projects/preview-annotation-empty-test-suite/src/test/{resources/hello_paparazzi.png => snapshots/images/app.cash.paparazzi.plugin.test_PreviewTests_preview[HelloPaparazzi_HelloPaparazzi].png} (100%) create mode 100644 paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample-configuration-cache/settings.gradle diff --git a/paparazzi-annotations/api/paparazzi-annotations.api b/paparazzi-annotations/api/paparazzi-annotations.api index 913e18bbf8..c4016dc313 100644 --- a/paparazzi-annotations/api/paparazzi-annotations.api +++ b/paparazzi-annotations/api/paparazzi-annotations.api @@ -5,19 +5,21 @@ public abstract interface class app/cash/paparazzi/annotations/PaparazziPreviewD } public final class app/cash/paparazzi/annotations/PaparazziPreviewData$Default : app/cash/paparazzi/annotations/PaparazziPreviewData { - public fun (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V + public static final field $stable I + public fun (Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lkotlin/jvm/functions/Function0; - public final fun copy (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Default; - public static synthetic fun copy$default (Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Default;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Default; + public final fun component2 ()Lkotlin/jvm/functions/Function2; + public final fun copy (Ljava/lang/String;Lkotlin/jvm/functions/Function2;)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Default; + public static synthetic fun copy$default (Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Default;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Default; public fun equals (Ljava/lang/Object;)Z - public final fun getComposable ()Lkotlin/jvm/functions/Function0; + public final fun getComposable ()Lkotlin/jvm/functions/Function2; public final fun getSnapshotName ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } public final class app/cash/paparazzi/annotations/PaparazziPreviewData$Empty : app/cash/paparazzi/annotations/PaparazziPreviewData { + public static final field $stable I public static final field INSTANCE Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Empty; public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I @@ -25,6 +27,7 @@ public final class app/cash/paparazzi/annotations/PaparazziPreviewData$Empty : a } public final class app/cash/paparazzi/annotations/PaparazziPreviewData$Error : app/cash/paparazzi/annotations/PaparazziPreviewData { + public static final field $stable I public fun (Ljava/lang/String;Ljava/lang/String;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; diff --git a/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt b/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt index 14f82a2304..284e5a8bc0 100644 --- a/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt +++ b/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt @@ -87,7 +87,7 @@ public class PaparazziPlugin @Inject constructor( else -> error("${androidComponents.javaClass.name} from $plugin is not supported in Paparazzi") } setupPaparazzi(project, androidComponents) - setupPreviewProcessor(project) + setupPreviewProcessor(project, androidComponents) } } } @@ -245,19 +245,19 @@ public class PaparazziPlugin @Inject constructor( private fun setupPreviewProcessor( project: Project, + extension: AndroidComponentsExtension<*, *, *> ) { project.pluginManager.apply(KspGradleSubplugin::class.java) project.addAnnotationsDependency() project.addProcessorDependency() + project.registerGeneratePreviewTask(config, extension) project.afterEvaluate { // pass the namespace to the processor val kspExtension = project.extensions.getByType(KspExtension::class.java) val android = project.extensions.getByType(BaseExtension::class.java) kspExtension.arg(KSP_ARG_NAMESPACE, android.packageName()) - - project.registerGeneratePreviewTask(config) } } @@ -340,7 +340,11 @@ public class PaparazziPlugin @Inject constructor( } else { dependencies.create("app.cash.paparazzi:paparazzi-preview-processor:$VERSION") } - configurations.getByName("ksp").dependencies.add(dependency) + if (project.plugins.hasPlugin("org.jetbrains.kotlin.multiplatform")) { + configurations.getByName("kspCommonMainMetadata").dependencies.add(dependency) + } else { + configurations.getByName("ksp").dependencies.add(dependency) + } } private fun Project.isInternal(): Boolean = diff --git a/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/utils/PreviewUtils.kt b/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/utils/PreviewUtils.kt index b556b2f0cb..ffa2e3521a 100644 --- a/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/utils/PreviewUtils.kt +++ b/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/utils/PreviewUtils.kt @@ -2,72 +2,75 @@ package app.cash.paparazzi.gradle.utils import app.cash.paparazzi.gradle.PaparazziExtension import com.android.build.api.variant.AndroidComponentsExtension -import com.android.build.gradle.LibraryExtension -import com.android.build.gradle.TestedExtension +import com.android.build.api.variant.HasUnitTest import org.gradle.api.Project +import org.gradle.language.base.plugins.LifecycleBasePlugin.VERIFICATION_GROUP import java.io.File import java.util.Locale private const val TEST_SOURCE_DIR = "build/generated/source/paparazzi" private const val KSP_SOURCE_DIR = "build/generated/ksp" -private const val PREVIEW_DATA_FILE = "paparazziPreviews.kt" +private const val PREVIEW_DATA_FILE = "PaparazziPreviews.kt" private const val PREVIEW_TEST_FILE = "PreviewTests.kt" -private val Project.libraryExtension: LibraryExtension? - get() = extensionByTypeOrNull(LibraryExtension::class.java) - -private fun Project.extensionByTypeOrNull(cls: Class): T? = - try { - extensions.getByType(cls) - } catch (e: Exception) { - null - } - internal fun Project.registerGeneratePreviewTask( - config: PaparazziExtension + config: PaparazziExtension, + extension: AndroidComponentsExtension<*, *, *> ) { - libraryExtension?.let { library -> - library.libraryVariants.all { variant -> - val namespace = library.namespace - val namespaceDir = namespace?.replace(".", "/") + extension.onVariants { variant -> + val testVariant = (variant as? HasUnitTest)?.unitTest ?: return@onVariants + val testVariantSlug = testVariant.name.capitalize() - val typeName = variant.buildType.name - val typeNameCap = typeName.capitalize() + val buildType = testVariant.buildType + val buildTypeCap = testVariant.buildType?.capitalize() - val testSourceDir = "$projectDir/$TEST_SOURCE_DIR/${typeName}UnitTest" - val previewTestDir = "$testSourceDir/$namespaceDir" + val taskName = "paparazziGeneratePreview${testVariantSlug}Kotlin" + val taskProvider = tasks.register(taskName) { task -> + task.group = VERIFICATION_GROUP + task.description = "Generates the preview test class to the test source set for $testVariantSlug" - library.sourceSets.getByName("test$typeNameCap").java { - srcDir(testSourceDir) - } - println("typeName: ${variant.buildType.name} ${library.namespace} $namespaceDir $typeNameCap") - if (config.generatePreviewTestClass.get()) { - val taskName = "paparazziGeneratePreview${typeNameCap}UnitTestKotlin" - tasks.register(taskName) { task -> - task.description = "Generates the preview test class to the test source set for $typeName" - - task.dependsOn("ksp${typeNameCap}Kotlin") - task.inputs.file( - "$projectDir/$KSP_SOURCE_DIR/$typeName/kotlin/$namespaceDir/$PREVIEW_DATA_FILE" + task.dependsOn("ksp${buildTypeCap}Kotlin") + } + + val testSourceDir = "$projectDir${File.separator}$TEST_SOURCE_DIR${File.separator}${buildType}UnitTest" + testVariant.sources.java?.addStaticSourceDirectory(testSourceDir) + + // test compilation depends on the task + project.tasks.named { + it == "compile${testVariantSlug}Kotlin" || + it == "generate${testVariantSlug}LintModel" || + it == "lintAnalyze$testVariantSlug" + }.configureEach { it.dependsOn(taskProvider) } + // run task before processing symbols + project.tasks.named { it == "ksp${testVariantSlug}Kotlin" } + .configureEach { it.mustRunAfter(taskProvider) } + + gradle.taskGraph.whenReady { + taskProvider.configure { task -> + // Test variant appends .test to the namespace + val namespace = testVariant.namespace.get().replace(".test$".toRegex(), "") + val namespaceDir = namespace.replace(".", File.separator) + val previewTestDir = "$testSourceDir${File.separator}$namespaceDir" + + // Optional input if KSP doesn't output preview annotation file + task.inputs + .file("$projectDir${File.separator}$KSP_SOURCE_DIR${File.separator}${buildType}${File.separator}kotlin${File.separator}$namespaceDir${File.separator}$PREVIEW_DATA_FILE") + .optional() + .skipWhenEmpty() + task.enabled = config.generatePreviewTestClass.get() + + task.outputs.dir(previewTestDir) + task.outputs.file("$previewTestDir${File.separator}$PREVIEW_TEST_FILE") + task.outputs.cacheIf { true } + + task.doLast { + File(previewTestDir).mkdirs() + File(previewTestDir, PREVIEW_TEST_FILE).writeText( + buildString { + appendLine("package $namespace") + append(PREVIEW_TEST_SOURCE) + } ) - task.outputs.dir(previewTestDir) - task.outputs.file("$previewTestDir/$PREVIEW_TEST_FILE") - task.outputs.cacheIf { true } - - // test compilation depends on the task - tasks.findByName("compile${typeNameCap}UnitTestKotlin")?.dependsOn(taskName) - // run task before processing symbols - tasks.findByName("ksp${typeNameCap}UnitTestKotlin")?.mustRunAfter(taskName) - - task.doLast { - File(previewTestDir).mkdirs() - File(previewTestDir, PREVIEW_TEST_FILE).writeText( - buildString { - appendLine("package $namespace") - append(PREVIEW_TEST_SOURCE) - } - ) - } } } } diff --git a/paparazzi-gradle-plugin/src/test/java/app/cash/paparazzi/gradle/PaparazziPluginTest.kt b/paparazzi-gradle-plugin/src/test/java/app/cash/paparazzi/gradle/PaparazziPluginTest.kt index 3d690ff440..9dc1e87e70 100644 --- a/paparazzi-gradle-plugin/src/test/java/app/cash/paparazzi/gradle/PaparazziPluginTest.kt +++ b/paparazzi-gradle-plugin/src/test/java/app/cash/paparazzi/gradle/PaparazziPluginTest.kt @@ -1398,19 +1398,12 @@ class PaparazziPluginTest { .withArguments("verifyPaparazziDebug", "--stacktrace") .runFixture(fixtureRoot) { build() } - val snapshotsDir = File(fixtureRoot, "build/reports/paparazzi/debug/images") - val snapshots = snapshotsDir.listFilesSorted() - assertThat(snapshots!!).hasSize(1) - - val goldenImage = File(fixtureRoot, "src/test/resources/hello_paparazzi.png") - assertThat(snapshots[0]).isSimilarTo(goldenImage).withDefaultThreshold() - assertThat(result.task(":paparazziGeneratePreviewDebugUnitTestKotlin")).isNotNull() val generatedPreviewsDir = File(fixtureRoot, "build/generated/ksp/debug/kotlin/app/cash/paparazzi/plugin/test/") assertThat( generatedPreviewsDir.listFiles()?.any { - it.name == "paparazziPreviews.kt" + it.name == "PaparazziPreviews.kt" } ).isTrue() @@ -1457,12 +1450,56 @@ class PaparazziPluginTest { .withArguments("verifyPaparazziDebug", "--stacktrace") .runFixture(fixtureRoot) { build() } - assertThat(result.task(":paparazziGeneratePreviewDebugUnitTestKotlin")).isNull() + assertThat(result.task(":paparazziGeneratePreviewDebugUnitTestKotlin")?.outcome).isEqualTo(TaskOutcome.SKIPPED) val generatedPreviewTestDir = File(fixtureRoot, "build/generated/source/paparazzi/debugUnitTest/app/cash/paparazzi/plugin/test/") assertThat(generatedPreviewTestDir.exists()).isFalse() } + @Test + fun previewAnnotationSample() { + val fixtureRoot = File("src/test/projects/preview-annotation-sample") + + val result = gradleRunner + .forwardOutput() + .withArguments("verifyPaparazziDebug", "--stacktrace") + .runFixture(fixtureRoot) { buildAndFail() } // Currently fails because of preview parameter usage + + assertThat(result.task(":paparazziGeneratePreviewDebugUnitTestKotlin")).isNotNull() + + val generatedPreviewTestDir = File(fixtureRoot, "build/generated/source/paparazzi/debugUnitTest/app/cash/paparazzi/plugin/test/") + assertThat(generatedPreviewTestDir.exists()).isTrue() + } + + @Test + fun previewAnnotationSampleConfigCache() { + val fixtureRoot = File("src/test/projects/preview-annotation-sample-configuration-cache") + fixtureRoot.resolve("build").apply { + deleteRecursively() + registerForDeletionOnExit() + } + fixtureRoot.resolve("build-cache").registerForDeletionOnExit() + + val result = gradleRunner + .forwardOutput() + .withArguments( + "testDebugUnitTest", "--stacktrace", "--build-cache", "--configuration-cache" + ) + .runFixture(fixtureRoot) { build() } + assertThat(result.task(":paparazziGeneratePreviewDebugUnitTestKotlin")?.outcome).isEqualTo(SUCCESS) + assertThat(result.task(":testDebugUnitTest")?.outcome).isEqualTo(SUCCESS) + + fixtureRoot.resolve("build").deleteRecursively() + + val result2 = gradleRunner + .forwardOutput() + .withArguments("testDebugUnitTest", "--stacktrace", "--build-cache", "--configuration-cache") + .runFixture(fixtureRoot) { build() } + + assertThat(result2.task(":paparazziGeneratePreviewDebugUnitTestKotlin")?.outcome).isEqualTo(FROM_CACHE) + assertThat(result2.task(":testDebugUnitTest")?.outcome).isEqualTo(SUCCESS) + } + @Test fun disabledUnitTestVariant() { val fixtureRoot = File("src/test/projects/disabled-unit-test-variant") diff --git a/paparazzi-gradle-plugin/src/test/projects/preview-annotation-empty-test-suite/src/test/resources/hello_paparazzi.png b/paparazzi-gradle-plugin/src/test/projects/preview-annotation-empty-test-suite/src/test/snapshots/images/app.cash.paparazzi.plugin.test_PreviewTests_preview[HelloPaparazzi_HelloPaparazzi].png similarity index 100% rename from paparazzi-gradle-plugin/src/test/projects/preview-annotation-empty-test-suite/src/test/resources/hello_paparazzi.png rename to paparazzi-gradle-plugin/src/test/projects/preview-annotation-empty-test-suite/src/test/snapshots/images/app.cash.paparazzi.plugin.test_PreviewTests_preview[HelloPaparazzi_HelloPaparazzi].png diff --git a/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample-configuration-cache/settings.gradle b/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample-configuration-cache/settings.gradle new file mode 100644 index 0000000000..79b3ab9f07 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample-configuration-cache/settings.gradle @@ -0,0 +1,7 @@ +apply from: '../test.settings.gradle' + +buildCache { + local { + directory = new File(rootDir, 'build-cache') + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample-configuration-cache/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt b/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample-configuration-cache/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt index 8db06bffdc..1c3909eccc 100644 --- a/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample-configuration-cache/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt +++ b/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample-configuration-cache/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt @@ -1,12 +1,8 @@ package app.cash.paparazzi.plugin.test -import android.content.res.Configuration import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.tooling.preview.Wallpapers import app.cash.paparazzi.annotations.Paparazzi @Paparazzi @@ -15,36 +11,3 @@ import app.cash.paparazzi.annotations.Paparazzi fun HelloPaparazzi() { Text("Hello, Paparazzi!") } - -@Paparazzi -@Preview -@Composable -fun HelloPaparazziParameterized( - @PreviewParameter(provider = PreviewData::class) text: String, -) { - Text(text) -} - -@Paparazzi -@Preview( - name = "PreviewConfig", - group = "Previews", - device = "id:Nexus 6", - apiLevel = 33, - showSystemUi = true, - uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL, - wallpaper = Wallpapers.RED_DOMINATED_EXAMPLE, - showBackground = true, - fontScale = 1.5f -) -@Composable -fun HelloPaparazziPreviewConfig() { - Text("Hello Paparazzi Preview Config!") -} - -object PreviewData : PreviewParameterProvider { - override val values: Sequence = sequenceOf( - "Hello, Paparazzi One!", - "Hello, Paparazzi Two!", - ) -} diff --git a/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt b/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt index 8db06bffdc..ae54ea5c90 100644 --- a/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt +++ b/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt @@ -20,7 +20,7 @@ fun HelloPaparazzi() { @Preview @Composable fun HelloPaparazziParameterized( - @PreviewParameter(provider = PreviewData::class) text: String, + @PreviewParameter(provider = PreviewData::class) text: String ) { Text(text) } @@ -45,6 +45,6 @@ fun HelloPaparazziPreviewConfig() { object PreviewData : PreviewParameterProvider { override val values: Sequence = sequenceOf( "Hello, Paparazzi One!", - "Hello, Paparazzi Two!", + "Hello, Paparazzi Two!" ) }