Skip to content

Commit

Permalink
Merge pull request #262 from takahirom/takahirom/fix-roborazzi-output…
Browse files Browse the repository at this point in the history
…-cache/2024-02-17

Fix Roborazzi output cache
  • Loading branch information
takahirom authored Feb 18, 2024
2 parents 5d3c2e9 + 948928b commit d0bb4cb
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ class RoborazziGradleProject(val testProjectDir: TemporaryFolder) {
return runTask(task)
}

fun clean(): BuildResult {
val task = "clean"
return runTask(task)
}

fun compareWithSystemParameter(): BuildResult {
val task = "testDebugUnitTest"
return runTask(task, additionalParameters = arrayOf("-Proborazzi.test.compare=true"))
Expand Down Expand Up @@ -367,6 +372,28 @@ class MainActivity : ComponentActivity() {
)
}

fun resetScreen() {
val file =
testProjectDir.root.resolve("app/src/main/java/com/github/takahirom/integration_test_project/MainActivity.kt")
file.writeText(
"""package com.github.takahirom.integration_test_project
import android.os.Bundle
import androidx.activity.ComponentActivity
import android.widget.TextView
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(TextView(this).apply{
text = "Hello World!"
})
}
}
"""
)
}

fun addTest() {
val originalFileText =
testProjectDir.root.resolve("app/src/test/java/com/github/takahirom/integration_test_project/RoborazziTest.kt")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,30 @@ class RoborazziGradleProjectTest {
}
}

/**
* This test is for the issue
* https://github.com/takahirom/roborazzi/issues/261
*/
@Test
fun checkRevertCache() {
RoborazziGradleProject(testProjectDir).apply {
record()
changeScreen()
compare()
resetScreen()
clean()
record()
changeScreen()
compare()

checkResultsSummaryFileExists()
// checkRecordedFileExists("$screenshotAndName.testCapture.png")
checkRecordedFileExists("$screenshotAndName.testCapture_compare.png")
checkRecordedFileExists("$screenshotAndName.testCapture_actual.png")
checkResultCount(changed = 1)
}
}

@Test
fun verify_addDetect() {
RoborazziGradleProject(testProjectDir).apply {
Expand Down Expand Up @@ -293,7 +317,6 @@ class RoborazziGradleProjectTest {
}
}


@Test
fun verifyAndRecord_nochange() {
RoborazziGradleProject(testProjectDir).apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,30 @@ import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.github.takahirom.roborazzi.CaptureResult
import com.github.takahirom.roborazzi.CaptureResults
import com.github.takahirom.roborazzi.RoborazziReportConst
import org.gradle.api.*
import org.gradle.api.Action
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.options.Option
import org.gradle.api.tasks.testing.Test
import org.gradle.build.event.BuildEventsListenerRegistry
import org.gradle.language.base.plugins.LifecycleBasePlugin.VERIFICATION_GROUP
import org.gradle.tooling.events.FinishEvent
import org.gradle.tooling.events.OperationCompletionListener
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
import java.util.*
import java.util.Locale
import javax.inject.Inject

private const val DEFAULT_OUTPUT_DIR = "outputs/roborazzi"
Expand All @@ -35,7 +45,9 @@ open class RoborazziExtension @Inject constructor(objects: ObjectFactory) {

@Suppress("unused")
// From Paparazzi: https://github.com/cashapp/paparazzi/blob/a76702744a7f380480f323ffda124e845f2733aa/paparazzi/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt
class RoborazziPlugin : Plugin<Project> {
abstract class RoborazziPlugin : Plugin<Project> {
@Inject abstract fun getEventsListenerRegistry(): BuildEventsListenerRegistry

override fun apply(project: Project) {
val extension = project.extensions.create("roborazzi", RoborazziExtension::class.java)

Expand Down Expand Up @@ -83,7 +95,12 @@ class RoborazziPlugin : Plugin<Project> {
return roborazziProperties["roborazzi.test.record"] == "true" || roborazziProperties["roborazzi.test.verify"] == "true" || roborazziProperties["roborazzi.test.compare"] == "true"
}

fun configureRoborazziTasks(variantSlug: String, testTaskName: String) {
fun configureRoborazziTasks(
variantSlug: String,
testTaskName: String,
testTaskSkipEventsServiceProvider: Provider<TestTaskSkipEventsServiceProvider>
) {
testTaskSkipEventsServiceProvider.get().addExpectingTestTaskName(testTaskName)
val testTaskOutputDirForEachVariant: DirectoryProperty = project.objects.directoryProperty()
val intermediateDirForEachVariant =
testTaskOutputDirForEachVariant.convention(
Expand Down Expand Up @@ -157,33 +174,37 @@ class RoborazziPlugin : Plugin<Project> {
val outputDirRelativePathFromProjectProvider = outputDir.map { project.relativePath(it) }
val resultDirFileTree =
project.fileTree(RoborazziReportConst.resultDirPath)
val resultsSummaryFile =
project.file(RoborazziReportConst.resultsSummaryFilePath)
val reportFile =
project.file(RoborazziReportConst.reportFilePath)
val resultDirFileProperty =
project.layout.projectDirectory.dir(
RoborazziReportConst.resultDirPath
)
val resultSummaryFileProperty =
project.layout.projectDirectory.file(RoborazziReportConst.resultsSummaryFilePath)
val reportFileProperty =
project.layout.projectDirectory.file(RoborazziReportConst.reportFilePath)

val finalizeTestRoborazziTask = project.tasks.register(
"finalizeTestRoborazzi$variantSlug",
object : Action<Task> {
/* name = */ "finalizeTestRoborazzi$variantSlug",
/* configurationAction = */ object : Action<Task> {
override fun execute(t: Task) {
t.onlyIf {
val doesRoborazziRun = doesRoborazziRunProvider.get()
t.infoln("Roborazzi: roborazziTestFinalizer.onlyIf doesRoborazziRun $doesRoborazziRun")
doesRoborazziRun
}
t.doLast {
t.infoln("Roborazzi: roborazziTestFinalizer.doLast")
// Copy all files from outputDir to intermediateDir
// so that we can use Gradle's output caching
t.infoln("Copy files from ${outputDir.get()} to ${intermediateDir.get()}")
// outputDir.get().asFileTree.forEach {
// println("Copy file ${it.absolutePath} to ${intermediateDir.get()}")
// }
outputDir.get().asFile.mkdirs()
outputDir.get().asFile.copyRecursively(
target = intermediateDir.get().asFile,
overwrite = true
)
val isTestSkipped =
testTaskSkipEventsServiceProvider.get().skipped
t.infoln("Roborazzi: roborazziTestFinalizer.doLast $isTestSkipped")
if (isTestSkipped) {
// If the test is skipped, we need to use cached files
t.infoln("Roborazzi: finalizeTestRoborazziTask isTestSkipped:$isTestSkipped Copy files from ${intermediateDir.get()} to ${outputDir.get()}")
intermediateDir.get().asFile.mkdirs()
intermediateDir.get().asFile.copyRecursively(
target = outputDir.get().asFile,
overwrite = true
)
}

val results: List<CaptureResult> = resultDirFileTree.mapNotNull {
if (it.name.endsWith(".json")) {
Expand All @@ -192,13 +213,15 @@ class RoborazziPlugin : Plugin<Project> {
null
}
}
t.infoln("Save result to ${resultsSummaryFile.absolutePath} with results:${results.size}")
val resultsSummaryFile = resultSummaryFileProperty.asFile

val roborazziResults = CaptureResults.from(results)
t.infoln("Roborazzi: Save result to ${resultsSummaryFile.absolutePath} with results:${results.size} summary:${roborazziResults.resultSummary}")

val jsonResult = roborazziResults.toJson()
resultsSummaryFile.parentFile.mkdirs()
resultsSummaryFile.writeText(jsonResult)
val reportFile = reportFileProperty.asFile
reportFile.parentFile.mkdirs()
reportFile.writeText(
RoborazziReportConst.reportHtml.replace(
Expand Down Expand Up @@ -229,7 +252,22 @@ class RoborazziPlugin : Plugin<Project> {
it
})
}
test.outputs.dir(intermediateDirForEachVariant)
test.outputs.dir(intermediateDirForEachVariant.map {
test.infoln("Roborazzi: Set output dir $it to test task")
it
})
test.outputs.dir(resultDirFileProperty.let {
test.infoln("Roborazzi: Set output dir $it to test task")
it
})
test.outputs.file(resultSummaryFileProperty.let {
test.infoln("Roborazzi: Set output file $it to test task")
it
})
test.outputs.file(reportFileProperty.let {
test.infoln("Roborazzi: Set output file $it to test task")
it
})

test.inputs.properties(
mapOf(
Expand Down Expand Up @@ -265,7 +303,8 @@ class RoborazziPlugin : Plugin<Project> {
!key.startsWith("roborazzi.test")
}
)
test.systemProperties["roborazzi.output.dir"] = outputDirRelativePathFromProjectProvider.get()
test.systemProperties["roborazzi.output.dir"] =
outputDirRelativePathFromProjectProvider.get()
test.systemProperties["roborazzi.test.record"] =
isRecordRun.get() || isVerifyAndRecordRun.get()
test.systemProperties["roborazzi.test.compare"] = isCompareRun.get()
Expand All @@ -275,6 +314,19 @@ class RoborazziPlugin : Plugin<Project> {
resultsDir.deleteRecursively()
resultsDir.mkdirs()
}
test.doLast {
// Copy all files from outputDir to intermediateDir
// so that we can use Gradle's output caching
it.infoln("Roborazzi: test.doLast Copy files from ${outputDir.get()} to ${intermediateDir.get()}")
// outputDir.get().asFileTree.forEach {
// println("Copy file ${finalizeTask.absolutePath} to ${intermediateDir.get()}")
// }
outputDir.get().asFile.mkdirs()
outputDir.get().asFile.copyRecursively(
target = intermediateDir.get().asFile,
overwrite = true
)
}
test.finalizedBy(finalizeTestRoborazziTask)
}

Expand All @@ -284,14 +336,26 @@ class RoborazziPlugin : Plugin<Project> {
verifyAndRecordTaskProvider.configure { it.dependsOn(testTaskProvider) }
}

val testTaskSkipEventsServiceProvider: Provider<TestTaskSkipEventsServiceProvider> =
project.gradle.sharedServices.registerIfAbsent(
"roborazziTestTaskEvents", TestTaskSkipEventsServiceProvider::class.java
) { spec ->
// do nothing
}
getEventsListenerRegistry().onTaskCompletion(testTaskSkipEventsServiceProvider)

fun AndroidComponentsExtension<*, *, *>.configureComponents() {
onVariants { variant ->
val unitTest = variant.unitTest ?: return@onVariants
val variantSlug = variant.name.capitalizeUS()
val testVariantSlug = unitTest.name.capitalizeUS()

// e.g. testDebugUnitTest -> recordRoborazziDebug
configureRoborazziTasks(variantSlug, "test$testVariantSlug")
configureRoborazziTasks(
variantSlug = variantSlug,
testTaskName = "test$testVariantSlug",
testTaskSkipEventsServiceProvider = testTaskSkipEventsServiceProvider
)
}
}

Expand All @@ -305,7 +369,11 @@ class RoborazziPlugin : Plugin<Project> {
}
project.pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
// e.g. test -> recordRoborazziJvm
configureRoborazziTasks("Jvm", "test")
configureRoborazziTasks(
variantSlug = "Jvm",
testTaskName = "test",
testTaskSkipEventsServiceProvider = testTaskSkipEventsServiceProvider
)
}
project.pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") {
val kotlinMppExtension = checkNotNull(
Expand All @@ -317,7 +385,11 @@ class RoborazziPlugin : Plugin<Project> {
if (target is KotlinJvmTarget) {
target.testRuns.all { testRun ->
// e.g. desktopTest -> recordRoborazziDesktop
configureRoborazziTasks(target.name.capitalizeUS(), testRun.executionTask.name)
configureRoborazziTasks(
variantSlug = target.name.capitalizeUS(),
testTaskName = testRun.executionTask.name,
testTaskSkipEventsServiceProvider = testTaskSkipEventsServiceProvider
)
}
}
}
Expand All @@ -340,6 +412,7 @@ class RoborazziPlugin : Plugin<Project> {
fun copy() {
val outputDirFile = outputDir.get().asFile
if (outputDirFile.exists() && outputDirFile.listFiles().isNotEmpty()) return
this.infoln("Roborazzi RestoreOutputDirRoborazziTask: Copy files from ${inputDir.get()} to ${outputDirFile}")
inputDir.get().asFile.copyRecursively(outputDirFile)
}
}
Expand All @@ -358,5 +431,37 @@ class RoborazziPlugin : Plugin<Project> {
}
}

/**
* We can't get whether the test is skipped or not from the test task itself
* because of the configuration cache
*/
abstract class TestTaskSkipEventsServiceProvider : BuildService<BuildServiceParameters.None?>,
OperationCompletionListener {
var skipped = false
private val expectingTestNames = mutableListOf<String>()
fun addExpectingTestTaskName(testName: String) {
expectingTestNames.add(testName)
}

override fun onFinish(finishEvent: FinishEvent) {
val displayName = finishEvent.displayName
// println(
// "Roborazzi: onFinish " +
// "expectingTestNames:$expectingTestNames" +
// "displayName:$displayName " +
// "finishEvent:$finishEvent " +
// "finishEvent.descriptor:${finishEvent.descriptor}" +
// "finishEvent.descriptor.name:${finishEvent.descriptor.name}"
// )
if (expectingTestNames.any {
displayName.contains(it, ignoreCase = true) &&
(displayName.contains("skipped", ignoreCase = true) ||
displayName.contains("FROM-CACHE", ignoreCase = true))
}) {
skipped = true
}
}
}

fun Task.infoln(format: String) =
logger.info(format)

0 comments on commit d0bb4cb

Please sign in to comment.