Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POC] Switch to writing to additional test output and device provider #82

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
eba5eab
Adds config model.
rharter Nov 12, 2024
ff0f248
Updates Jvm target version config.
rharter Nov 12, 2024
ca56683
Adds support for writing a test run config file to the target device.
rharter Nov 14, 2024
ab8669a
Updates plugin to use input/output directories for connected checks.
rharter Nov 15, 2024
692a66a
Adds plugin tests.
rharter Nov 20, 2024
b3415f6
Updates dropshots runtime to use gradle plugin.
rharter Dec 5, 2024
b73a80d
Updates github action and documentation.
rharter Dec 5, 2024
3518906
Updates test collection scripts to handle empty find results.
rharter Dec 6, 2024
ff8826c
Removes erroneous project options service call used for experimentation.
rharter Dec 6, 2024
3f1d4f5
Updates changelog.
rharter Dec 6, 2024
3e5aeaa
Removes unused import
rharter Dec 6, 2024
8c7e17b
updates API dump.
rharter Dec 6, 2024
c129705
Adds alternative build directory.
rharter Dec 9, 2024
8996429
Updates test failure artifact contents
rharter Dec 10, 2024
0add684
Updates workflow to ensure new screenshot validation succeeds.
rharter Dec 11, 2024
9b9c6a8
Restores the emulator setup gradle tasks for runtime tests.
rharter Dec 11, 2024
653da2c
Adds some logging to test run config file to validate github actions …
rharter Dec 11, 2024
dbd3f92
🤖 Updates screenshots
rharter Dec 11, 2024
bd68f67
Moves test config writer log message to debug level.
rharter Dec 11, 2024
af9c9cf
Merge branch 'rharter/device-provider' of github.com:dropbox/dropshot…
rharter Dec 11, 2024
6362aac
Updates write config task to warn level.
rharter Dec 11, 2024
97a85b8
Adds directory checks and logging for update task.
rharter Dec 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ jobs:

- name: (Fail-only) Bundle test reports
if: failure()
run: find . -type d -name 'reports' | zip -@ -r unit-test-build-reports.zip
run: find . -type d -name 'reports' -exec zip -r unit-test-build-reports.zip {} +

- name: (Fail-only) Upload the build report
if: failure()
Expand Down Expand Up @@ -171,14 +171,19 @@ jobs:
run: |
echo "::error::Screenshot tests failed, please create a PR in your fork first." && exit 1

- name: Pull screenshots
- name: Record screenshots
id: screenshotspull
continue-on-error: true
continue-on-error: false
uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # v2.33.0
if: steps.screenshotsverify.outcome == 'failure' && github.event_name == 'pull_request'
run: |
echo "Pulling $(ls -1q dropshots/build/reports/androidTests/dropshots/reference/*.png | wc -l) files..."
ls -1q dropshots/build/reports/androidTests/dropshots/reference/*.png
cp dropshots/build/reports/androidTests/dropshots/reference/*.png dropshots/src/androidTest/assets/
with:
api-level: 31
arch: x86_64
profile: pixel_5
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: ./gradlew updateDropshotsScreenshots --stacktrace

# Since commits from actions don't trigger new actions, we validate the new screenshots here
# before we commit them to ensure there isn't flakiness in the tests.
Expand All @@ -204,7 +209,7 @@ jobs:

- name: Bundle test reports
if: always()
run: find . -type d '(' -name 'reports' -o -name 'androidTest-results' ')' | zip -@ -r instrumentation-test-build-reports.zip
run: find . -type d '(' -name 'reports' -o -name 'androidTest-results' -o -name 'connected_android_test_additional_output' ')' -exec zip -r instrumentation-test-build-reports.zip {} +

- name: Upload the build report
if: always()
Expand Down
13 changes: 9 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
[Unreleased]: https://github.com/dropbox/dropshots/compare/0.4.2...HEAD

New:
- Nothing yet!
- Adds support for `additional_test_output` directory.

Changed:
- Updates runtime to always record reference **and** diff images.
- Updates Gradle plugin to deprecate the `dropshots.record` property.

Fixed:
Expand All @@ -15,12 +16,16 @@ Fixed:
### Updated Gradle tasks

This version of Dropshots updates the Gradle plugin to deprecate the use of the
`dropshots.record` property in favor of a new `recordDebugAndroidTestScreenshots` task
`dropshots.record` property in favor of a new `updateDropshotsScreenshots` task
to update the local reference images.

By adding support for the `additional_test_output` directory, the Dropshots runtime
now records images in the `build/outputs/connected_android_test_additional_output`
directory.

With this change the behavior of `Dropshots` has also changed, such that it will **always**
record reference images on the test device so that the local copies can be updated without
the need to recompile the app.
record reference images so that the local copies can be updated without the need to
recompile the app.

## [0.4.2] = 2024-05-21
[0.4.2]: https://github.com/dropbox/dropshots/releases/tags/0.4.2
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,16 +123,16 @@ class MyTest {
With this test in place, any time the `connectedAndroidTest` task is run the screenshot of the
Activity or View will be validated against the reference images stored in the repository. If any
screenshots fail to match the reference images (within configurable thresholds), then an image will
be written to the test report folder that shows the reference image, the actual image, and the diff
of the two. By default, the test report folder is
`${project.buildDir}/outputs/androidTest-results/connected`.
be written to the additional test output folder that shows the reference image, the actual image,
and the diff of the two. By default, the test report folder is
`${project.buildDir}/outputs/connected_android_test_additional_output/debugAndroidTest/$device/connected`.

The first time you create a screenshot test, however, there won't be any reference images, so you'll
have to create them...

### Updating reference images

Updating reference screenshots is as simple as running the `record[variant]Screenshots` Gradle task.
Updating reference screenshots is as simple as running the `updateDropshotsScreenshots` Gradle task.
This makes it easy to update screenshots in a single step, without requiring you to
interact with the emulator or use esoteric `adb` commands.

Expand Down
26 changes: 21 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import com.vanniktech.maven.publish.MavenPublishBaseExtension
import com.vanniktech.maven.publish.SonatypeHost
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlinx.serialization) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.ktlint)
alias(libs.plugins.mavenPublish)
Expand All @@ -14,11 +18,6 @@ allprojects {
group = project.property("GROUP") as String
version = project.property("VERSION_NAME") as String

repositories {
google()
mavenCentral()
}

plugins.withId("com.vanniktech.maven.publish.base") {
configure<MavenPublishBaseExtension> {
publishToMavenCentral(SonatypeHost.S01)
Expand All @@ -36,6 +35,23 @@ allprojects {
}
}
}

plugins.withType<KotlinBasePlugin>().configureEach {
tasks.withType<KotlinCompile>().configureEach {
compilerOptions {
apiVersion.set(KotlinVersion.KOTLIN_1_9)
languageVersion.set(KotlinVersion.KOTLIN_1_9)
}
}
}

plugins.withType(JavaBasePlugin::class.java).configureEach {
extensions.configure(JavaPluginExtension::class.java) {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
}
}

tasks.register("printVersionName") {
Expand Down
58 changes: 42 additions & 16 deletions dropshots-gradle-plugin/api/dropshots-gradle-plugin.api
Original file line number Diff line number Diff line change
@@ -1,37 +1,63 @@
public abstract class com/dropbox/dropshots/ClearScreenshotsTask : org/gradle/api/DefaultTask {
public abstract class com/dropbox/dropshots/DropshotsExtension {
public fun <init> (Lorg/gradle/api/model/ObjectFactory;)V
public final fun getApplyDependency ()Lorg/gradle/api/provider/Property;
}

public final class com/dropbox/dropshots/DropshotsPlugin : org/gradle/api/Plugin {
public fun <init> ()V
public synthetic fun apply (Ljava/lang/Object;)V
public fun apply (Lorg/gradle/api/Project;)V
}

public final class com/dropbox/dropshots/VersionKt {
public static final field VERSION Ljava/lang/String;
}

public abstract class com/dropbox/dropshots/device/DeviceProviderFactory {
public fun <init> ()V
public final fun getDeviceProvider (Lorg/gradle/api/provider/Provider;Lcom/android/utils/ILogger;Ljava/lang/String;)Lcom/android/builder/testing/api/DeviceProvider;
public static synthetic fun getDeviceProvider$default (Lcom/dropbox/dropshots/device/DeviceProviderFactory;Lorg/gradle/api/provider/Provider;Lcom/android/utils/ILogger;Ljava/lang/String;ILjava/lang/Object;)Lcom/android/builder/testing/api/DeviceProvider;
}

public abstract class com/dropbox/dropshots/tasks/ClearScreenshotsTask : org/gradle/api/DefaultTask {
public fun <init> ()V
public final fun clearScreenshots ()V
public abstract fun getAdbExecutable ()Lorg/gradle/api/provider/Property;
protected abstract fun getExecOperations ()Lorg/gradle/process/ExecOperations;
public abstract fun getScreenshotDir ()Lorg/gradle/api/provider/Property;
}

public final class com/dropbox/dropshots/DropshotsPlugin : org/gradle/api/Plugin {
public abstract class com/dropbox/dropshots/tasks/GenerateReferenceScreenshotsTask : org/gradle/api/DefaultTask {
public fun <init> ()V
public synthetic fun apply (Ljava/lang/Object;)V
public fun apply (Lorg/gradle/api/Project;)V
public abstract fun getOutputDir ()Lorg/gradle/api/file/DirectoryProperty;
public abstract fun getReferenceImageDir ()Lorg/gradle/api/file/DirectoryProperty;
public final fun performAction ()V
}

public abstract class com/dropbox/dropshots/PullScreenshotsTask : org/gradle/api/DefaultTask {
public abstract class com/dropbox/dropshots/tasks/PullScreenshotsTask : org/gradle/api/DefaultTask {
public fun <init> ()V
public abstract fun getAdbExecutable ()Lorg/gradle/api/provider/Property;
protected abstract fun getExecOperations ()Lorg/gradle/process/ExecOperations;
public abstract fun getOutputDirectory ()Lorg/gradle/api/file/DirectoryProperty;
public abstract fun getScreenshotDir ()Lorg/gradle/api/provider/Property;
public abstract fun getRemoteDir ()Lorg/gradle/api/provider/Property;
public final fun pullScreenshots ()V
}

public abstract class com/dropbox/dropshots/PushFileTask : org/gradle/api/DefaultTask {
public abstract class com/dropbox/dropshots/tasks/UpdateScreenshotsTask : org/gradle/api/DefaultTask {
public fun <init> ()V
public abstract fun getAdbExecutable ()Lorg/gradle/api/provider/Property;
protected abstract fun getExecOperations ()Lorg/gradle/process/ExecOperations;
public abstract fun getFileContents ()Lorg/gradle/api/provider/Property;
public abstract fun getRemotePath ()Lorg/gradle/api/provider/Property;
protected abstract fun getTempFileProvider ()Lorg/gradle/api/internal/file/temp/TemporaryFileProvider;
public final fun push ()V
public abstract fun getDeviceProviderName ()Lorg/gradle/api/provider/Property;
public abstract fun getOutputBasePath ()Lorg/gradle/api/provider/Property;
public abstract fun getOutputDir ()Lorg/gradle/api/file/DirectoryProperty;
public abstract fun getReferenceImageDir ()Lorg/gradle/api/file/DirectoryProperty;
public final fun performAction ()V
}

public final class com/dropbox/dropshots/VersionKt {
public static final field VERSION Ljava/lang/String;
public abstract class com/dropbox/dropshots/tasks/WriteConfigFileTask : org/gradle/api/DefaultTask {
public fun <init> ()V
public abstract fun getAdbExecutable ()Lorg/gradle/api/provider/Property;
public abstract fun getDeviceProviderFactory ()Lcom/dropbox/dropshots/device/DeviceProviderFactory;
public abstract fun getDeviceProviderName ()Lorg/gradle/api/provider/Property;
public abstract fun getRecordingScreenshots ()Lorg/gradle/api/provider/Property;
public abstract fun getRemoteDir ()Lorg/gradle/api/provider/Property;
public final fun performAction ()V
}

78 changes: 22 additions & 56 deletions dropshots-gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,46 +1,32 @@
import com.android.build.gradle.tasks.SourceJarTask
import com.vanniktech.maven.publish.GradlePlugin
import com.vanniktech.maven.publish.JavadocJar.Dokka
import org.gradle.jvm.tasks.Jar
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import com.vanniktech.maven.publish.MavenPublishBaseExtension

plugins {
`java-gradle-plugin`
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.dokka)
alias(libs.plugins.mavenPublish)
alias(libs.plugins.binaryCompatibilityValidator)
}

buildscript {
repositories {
mavenCentral()
gradlePluginPortal()
}
}

repositories {
mavenCentral()
gradlePluginPortal()
}

sourceSets {
main.configure {
java.srcDir("src/generated/kotlin")
// This module is included in two projects:
// - In the root project where it's released as one of our artifacts
// - In build-logic project where we can use it for the runtime and samples.
//
// We only want to publish when it's being built in the root project.
if (rootProject.name == "dropshots-root") {
apply(plugin = libs.plugins.mavenPublish.get().pluginId)
extensions.configure<MavenPublishBaseExtension> {
configure(GradlePlugin(Dokka("dokkaJavadoc")))
}
}

mavenPublishing {
configure(GradlePlugin(Dokka("dokkaJavadoc")))
} else {
// Move the build directory when included in build-support so as to not poison the real build.
// If we don't the configuration cache is broken and all tasks are considered not up-to-date.
layout.buildDirectory = File(rootDir, "build/dropshots-gradle-plugin")
}

val generateVersionTask = tasks.register("generateVersion") {
inputs.property("version", project.property("VERSION_NAME") as String)
outputs.dir(project.layout.projectDirectory.dir("src/generated/kotlin"))

outputs.dir(project.layout.buildDirectory.dir("generated/version/kotlin"))
doLast {
val output = File(outputs.files.first(), "com/dropbox/dropshots/Version.kt")
output.parentFile.mkdirs()
Expand All @@ -52,28 +38,8 @@ val generateVersionTask = tasks.register("generateVersion") {
}
}

tasks.withType<Jar>().configureEach {
dependsOn(generateVersionTask)
}

tasks.named("dokkaJavadoc").configure {
dependsOn(generateVersionTask)
}

tasks.named("compileKotlin").configure {
dependsOn(generateVersionTask)
}

tasks.withType<KotlinCompile>().configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
apiVersion.set(KotlinVersion.KOTLIN_1_8)
languageVersion.set(KotlinVersion.KOTLIN_1_8)
}
}

tasks.withType<JavaCompile>().configureEach {
options.release.set(11)
sourceSets.main {
java.srcDir(generateVersionTask)
}

kotlin {
Expand All @@ -94,8 +60,12 @@ val releaseMode = hasProperty("dropshots.releaseMode")
dependencies {
compileOnly(gradleApi())
implementation(platform(libs.kotlin.bom))
// Don't impose our version of KGP on consumers
implementation(libs.android.builder.test)
implementation(libs.android.common)
implementation(libs.android.ddmlib)
implementation(projects.model)

// Don't impose our version of KGP on consumers
if (releaseMode) {
compileOnly(libs.android)
compileOnly(libs.kotlin.plugin)
Expand All @@ -115,7 +85,3 @@ tasks.register("printVersionName") {
println(project.property("VERSION_NAME"))
}
}

tasks.withType<Test>().configureEach {
dependsOn(":dropshots:publishMavenPublicationToProjectLocalMavenRepository")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.dropbox.dropshots

import javax.inject.Inject
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property

public abstract class DropshotsExtension @Inject constructor(objects: ObjectFactory) {
/**
* Whether the Dropshots plugin should automatically apply the
* Dropshots runtime dependency.
*
* You can use this to disable automatic addition of the runtime dependency
* if you have your own fork, but in most cases this should use the default
* value of `true`.
*/
public val applyDependency: Property<Boolean> = objects.property(Boolean::class.java)
.convention(true)
}
Loading
Loading