Skip to content

Commit

Permalink
Add support for configuration caching
Browse files Browse the repository at this point in the history
  • Loading branch information
geoff-powell committed Aug 24, 2024
1 parent 746afe5 commit b81c705
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 102 deletions.
13 changes: 8 additions & 5 deletions paparazzi-annotations/api/paparazzi-annotations.api
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,29 @@ 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 <init> (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V
public static final field $stable I
public fun <init> (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
public fun toString ()Ljava/lang/String;
}

public final class app/cash/paparazzi/annotations/PaparazziPreviewData$Error : app/cash/paparazzi/annotations/PaparazziPreviewData {
public static final field $stable I
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,72 +2,71 @@ 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 <T> Project.extensionByTypeOrNull(cls: Class<T>): 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" }
.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"

task.enabled = config.generatePreviewTestClass.get()
task.inputs.file(
"$projectDir${File.separator}$KSP_SOURCE_DIR${File.separator}${buildType}${File.separator}kotlin${File.separator}$namespaceDir${File.separator}$PREVIEW_DATA_FILE"
)
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)
}
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1410,7 +1410,7 @@ class PaparazziPluginTest {
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()

Expand Down Expand Up @@ -1463,6 +1463,50 @@ class PaparazziPluginTest {
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")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apply from: '../test.settings.gradle'

buildCache {
local {
directory = new File(rootDir, 'build-cache')
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<String> {
override val values: Sequence<String> = sequenceOf(
"Hello, Paparazzi One!",
"Hello, Paparazzi Two!",
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ fun HelloPaparazzi() {
@Preview
@Composable
fun HelloPaparazziParameterized(
@PreviewParameter(provider = PreviewData::class) text: String,
@PreviewParameter(provider = PreviewData::class) text: String
) {
Text(text)
}
Expand All @@ -45,6 +45,6 @@ fun HelloPaparazziPreviewConfig() {
object PreviewData : PreviewParameterProvider<String> {
override val values: Sequence<String> = sequenceOf(
"Hello, Paparazzi One!",
"Hello, Paparazzi Two!",
"Hello, Paparazzi Two!"
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,18 @@ internal object PaparazziPoet {
emptyList()
} else {
listOf(
buildAnnotationsFile("paparazziPreviews", functions, env)
buildAnnotationsFile("PaparazziPreviews", "paparazziPreviews", functions, env)
)
}

@Suppress("SameParameterValue")
private fun buildAnnotationsFile(
fileName: String,
propertyName: String,
functions: Sequence<KSFunctionDeclaration>,
env: EnvironmentOptions
) =
FileSpec.scriptBuilder(propertyName, env.namespace)
FileSpec.scriptBuilder(fileName, env.namespace)
.addCode(
buildCodeBlock {
addStatement("internal val %L = listOf<%L.PaparazziPreviewData>(", propertyName, PACKAGE_NAME)
Expand Down

0 comments on commit b81c705

Please sign in to comment.