Skip to content

Commit

Permalink
Merge pull request #1 from takahirom/takahirom/add-gradle-plugin-and-…
Browse files Browse the repository at this point in the history
…verify-screenshot/2023-03-20

Add gradle plugin and verify screenshot
  • Loading branch information
takahirom authored Mar 20, 2023
2 parents 3878592 + a2b0053 commit 7a4736c
Show file tree
Hide file tree
Showing 14 changed files with 290 additions and 34 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ jobs:
java-version: 19

- name: test
run: ./gradlew app:testDebugUnitTest --stacktrace
run: |
./gradlew roborazzi-gradle-plugin:build --stacktrace
./gradlew app:testDebugUnitTest --stacktrace
- uses: actions/upload-artifact@v3
with:
name: screenshots
Expand Down
1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
buildscript {
dependencies {
classpath files('../roborazzi-gradle-plugin/build/libs/roborazzi-gradle-plugin.jar')
}
}
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
if (file('../roborazzi-gradle-plugin/build/libs/roborazzi-gradle-plugin.jar').exists()) {
// for sync
apply plugin: 'io.github.takahirom.roborazzi.gradle'
}

android {
namespace 'com.github.takahirom.roborazzi.sample'
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<string name="app_name">roborazzi</string>
<string name="action_settings">Settings</string>
<!-- Strings used for fragments for navigation -->
<string name="first_fragment_label">First Fragment</string>
<string name="first_fragment_label">Not First Fragment</string>
<string name="second_fragment_label">Second Fragment</string>
<string name="next">Next</string>
<string name="previous">Previous</string>
Expand Down
1 change: 1 addition & 0 deletions roborazzi-gradle-plugin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build/
20 changes: 20 additions & 0 deletions roborazzi-gradle-plugin/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
apply plugin: 'org.jetbrains.kotlin.jvm'
apply plugin: 'java-gradle-plugin'

gradlePlugin {
plugins {
roborazzi {
id = 'io.github.takahirom.roborazzi.gradle'
implementationClass = 'io.github.takahirom.roborazzi.RoborazziPlugin'
}
}
}

dependencies {
compileOnly gradleApi()
compileOnly "com.android.tools.build:gradle:7.3.1"
}

sourceSets {
main.java.srcDir 'src/generated/kotlin'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package io.github.takahirom.roborazzi

import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
import java.util.Locale
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.tasks.options.Option
import org.gradle.api.tasks.testing.Test
import org.gradle.language.base.plugins.LifecycleBasePlugin.VERIFICATION_GROUP

@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> {
override fun apply(project: Project) {
val verifyVariants = project.tasks.register("verifyRoborazzi")
val recordVariants = project.tasks.register("recordRoborazzi")

val hasLibraryPlugin = project.pluginManager.hasPlugin("com.android.library")
val variants = if (hasLibraryPlugin) {
project.extensions.getByType(LibraryExtension::class.java)
.libraryVariants
} else {
project.extensions.getByType(BaseAppModuleExtension::class.java)
.applicationVariants
}
variants.all { variant ->
val variantSlug = variant.name.capitalize(Locale.US)

// val reportOutputDir = project.layout.buildDirectory.dir("reports/roborazzi")
// val snapshotOutputDir = project.layout.projectDirectory.dir("src/test/snapshots")

val testVariantSlug = variant.unitTestVariant.name.capitalize(Locale.US)

val recordTaskProvider = project.tasks.register("recordRoborazzi$variantSlug", RoborazziTask::class.java) {
it.group = VERIFICATION_GROUP
}
recordVariants.configure { it.dependsOn(recordTaskProvider) }
val verifyTaskProvider = project.tasks.register("verifyRoborazzi$variantSlug", RoborazziTask::class.java) {
it.group = VERIFICATION_GROUP
}
verifyVariants.configure { it.dependsOn(verifyTaskProvider) }

val isRecordRun = project.objects.property(Boolean::class.java)
val isVerifyRun = project.objects.property(Boolean::class.java)

project.gradle.taskGraph.whenReady { graph ->
isRecordRun.set(recordTaskProvider.map { graph.hasTask(it) })
isVerifyRun.set(verifyTaskProvider.map { graph.hasTask(it) })
}

val testTaskProvider = project.tasks.named("test$testVariantSlug", Test::class.java) { test ->
test.systemProperties["roborazzi.build.dir"] =
project.layout.buildDirectory.get().toString()

// test.outputs.dir(reportOutputDir)
// test.outputs.dir(snapshotOutputDir)

val roborazziProperties = project.properties.filterKeys { it.startsWith("io.github.takahirom.roborazzi") }

@Suppress("ObjectLiteralToLambda")
// why not a lambda? See: https://docs.gradle.org/7.2/userguide/validation_problems.html#implementation_unknown
test.doFirst(object : Action<Task> {
override fun execute(t: Task) {
test.systemProperties["roborazzi.test.record"] = isRecordRun.get()
test.systemProperties["roborazzi.test.verify"] = isVerifyRun.get()
test.systemProperties.putAll(roborazziProperties)
}
})
}

recordTaskProvider.configure { it.dependsOn(testTaskProvider) }
verifyTaskProvider.configure { it.dependsOn(testTaskProvider) }

// testTaskProvider.configure { test ->
// @Suppress("ObjectLiteralToLambda")
// // why not a lambda? See: https://docs.gradle.org/7.2/userguide/validation_problems.html#implementation_unknown
// test.doLast(object : Action<Task> {
// override fun execute(t: Task) {
// val uri = reportOutputDir.get().asFile.toPath().resolve("index.html").toUri()
// test.logger.log(LIFECYCLE, "See the Roborazzi report at: $uri")
// }
// })
// }
}
}

open class RoborazziTask : DefaultTask() {
@Option(option = "tests", description = "Sets test class or method name to be included, '*' is supported.")
open fun setTestNameIncludePatterns(testNamePattern: List<String>): RoborazziTask {
project.tasks.withType(Test::class.java).configureEach {
it.setTestNameIncludePatterns(testNamePattern)
}
return this
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class RoborazziRule private constructor(
}

data class Options(
val captureType: CaptureType = CaptureType.Gif,
val captureType: CaptureType = CaptureType.LastImage,
/**
* capture only when the test fail
*/
Expand Down Expand Up @@ -88,6 +88,15 @@ class RoborazziRule private constructor(
throw e
}
}
if (!roborazziEnabled()) {
evaluate()
return
}
if(!roborazziRecordingEnabled() && options.captureType == CaptureType.Gif) {
// currently, gif compare is not supported
evaluate()
return
}
if (description.annotations.filterIsInstance<Ignore>().isNotEmpty()) return evaluate()
val folder = File(options.outputDirectoryPath)
if (!folder.exists()) {
Expand Down
1 change: 1 addition & 0 deletions roborazzi-painter/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ plugins {

dependencies {
compileOnly "org.robolectric:android-all:Q-robolectric-5415296"
api "com.dropbox.differ:differ:0.0.1-alpha1"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.github.takahirom.roborazzi

import com.dropbox.differ.Color
import com.dropbox.differ.Image
import java.awt.image.BufferedImage

internal class DifferBufferedImage(private val bufferedImage: BufferedImage) : Image {
override val height: Int
get() = bufferedImage.height
override val width: Int
get() = bufferedImage.width

override fun getPixel(x: Int, y: Int): Color {
return Color(bufferedImage.getRGB(x, y))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package com.github.takahirom.roborazzi
import android.graphics.Bitmap
import android.graphics.Paint
import android.graphics.Rect
import com.dropbox.differ.ImageComparator
import com.dropbox.differ.SimpleImageComparator
import java.awt.*
import java.awt.font.FontRenderContext
import java.awt.font.TextLayout
Expand All @@ -13,13 +15,13 @@ import java.io.File
import javax.imageio.ImageIO


class RoboCanvas(width: Int, height: Int) {
class RoboCanvas(width: Int, height: Int, filled: Boolean = false) {
private val bufferedImage = BufferedImage(width, height, BufferedImage.TYPE_USHORT_565_RGB)
val width: Int get() = bufferedImage.width
val height: Int get() = bufferedImage.height
val croppedWidth: Int get() = croppedImage.width
val croppedHeight: Int get() = croppedImage.height
private var rightBottomPoint = 0 to 0
private var rightBottomPoint = if (filled) width to height else 0 to 0
private fun updateRightBottom(x: Int, y: Int) {
rightBottomPoint = maxOf(x, rightBottomPoint.first) to maxOf(y, rightBottomPoint.second)
}
Expand All @@ -44,6 +46,19 @@ class RoboCanvas(width: Int, height: Int) {
consumeEmptyPoints(r)
}

internal fun drawImage(image: BufferedImage) {
bufferedImage.graphics { graphics2D ->
graphics2D.drawImage(
image,
0,
0,
null
)
}
updateRightBottom(image.width, image.height)
// consumeEmptyPoints(r)
}

fun drawRectOutline(r: Rect, paint: Paint) {
bufferedImage.graphics { graphics2D ->
graphics2D.color = Color(paint.getColor(), true)
Expand Down Expand Up @@ -181,6 +196,12 @@ class RoboCanvas(width: Int, height: Int) {
)
}

fun differ(other: RoboCanvas): ImageComparator.ComparisonResult {
val otherImage = other.bufferedImage
val simpleImageComparator = SimpleImageComparator(maxDistance = 0.007F)
return simpleImageComparator.compare(DifferBufferedImage(bufferedImage), DifferBufferedImage(otherImage))
}

private fun drawPendingDraw() {
// val start = System.currentTimeMillis()
baseDrawList.forEach { it() }
Expand All @@ -202,10 +223,55 @@ class RoboCanvas(width: Int, height: Int) {
}

companion object {
fun load(file: File): RoboCanvas {
val loadedImage: BufferedImage = ImageIO.read(file)
val roboCanvas = RoboCanvas(loadedImage.width, loadedImage.height)
roboCanvas.drawImage(loadedImage)
return roboCanvas
}

const val TRANSPARENT_NONE = 0xFF shl 56
const val TRANSPARENT_BIT = 0xEE shl 56
const val TRANSPARENT_MEDIUM = 0x88 shl 56
const val TRANSPARENT_STRONG = 0x66 shl 56

fun generateCompareCanvas(goldenCanvas: RoboCanvas, newCanvas: RoboCanvas): RoboCanvas {
newCanvas.drawPendingDraw()
val image1 = goldenCanvas.bufferedImage
val image2 = newCanvas.bufferedImage
val diff = generateDiffImage(image1, image2)
val width = image1.width + diff.width + image2.width
val height = image1.height.coerceAtLeast(diff.height).coerceAtLeast(image2.height)

val combined = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)

val g = combined.createGraphics()
g.drawImage(image1, 0, 0, null)
g.drawImage(diff, image1.width, 0, null)
g.drawImage(image2, image1.width + diff.width, 0, null)
g.dispose()
return RoboCanvas(width, height, true).apply {
drawImage(combined)
}
}

private fun generateDiffImage(originalImage: BufferedImage, comparedImage: BufferedImage): BufferedImage {
val width = minOf(originalImage.width, comparedImage.width)
val height = minOf(originalImage.height, comparedImage.height)
val diffImage = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
for (x in 0 until width) {
for (y in 0 until height) {
val rgbOrig = originalImage.getRGB(x, y)
val rgbComp = comparedImage.getRGB(x, y)
if (rgbOrig != rgbComp) {
diffImage.setRGB(x, y, -0x10000)
} else {
0x0
}
}
}
return diffImage
}
}
}

Expand Down

This file was deleted.

Loading

0 comments on commit 7a4736c

Please sign in to comment.