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

Updates dropshots to always write reference image. #64

Merged
merged 29 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
142007b
Updates Dropshots to **always** save reference images.
rharter Oct 23, 2024
515848a
Updates api dump.
rharter Oct 23, 2024
9294a30
Updates sample app.
rharter Oct 23, 2024
58e8024
Restores functionality of the dropshots.record property.
rharter Oct 23, 2024
587cd80
Updates record task name.
rharter Oct 23, 2024
fdca880
Updates readme for record step.
rharter Oct 23, 2024
ac62496
Updates API for gradle plugin.
rharter Oct 23, 2024
4412a31
Updates test setup.
rharter Oct 24, 2024
cfd7370
Updates push task to complete if there are no connected devices.
rharter Oct 30, 2024
0133826
Fixes dropshots recording config in tests.
rharter Oct 31, 2024
0871f58
Updates build scripts to fix recording.
rharter Oct 31, 2024
a3cffc4
🤖 Updates screenshots
rharter Oct 31, 2024
3a55b21
removes unused build script.
rharter Oct 31, 2024
95e2e38
🤖 Updates screenshots
rharter Oct 31, 2024
51f1c08
Updates to build scripts.
rharter Oct 31, 2024
e966bf4
Pins emulator action commit.
rharter Oct 31, 2024
676f29f
Updates to emulator config script.
rharter Oct 31, 2024
65ad3bf
🤖 Updates screenshots
rharter Oct 31, 2024
5c1cf39
Moves emulator setup into gradle tasks so they always run, and updates
rharter Nov 1, 2024
96a5009
Adds config to set emulator to light mode.
rharter Nov 2, 2024
820865e
Updates theme for more consistent screenshots.
rharter Nov 4, 2024
b0b35b9
Introduces emulator cache to make config more explicit.
rharter Nov 4, 2024
48d73ee
Bumps cache key version.
rharter Nov 4, 2024
0d1a5a2
Adds missing api-level arg to verify screenshots task.
rharter Nov 4, 2024
a1f8406
adds some logging to the pull job
rharter Nov 4, 2024
16020d7
Moves activity creation to onCreate.
rharter Nov 4, 2024
cc7b1f9
Updates screenshot and lint warning.
rharter Nov 4, 2024
c0f9f98
Removes gesture nav and night mode adb commands.
rharter Nov 4, 2024
b9c1c15
🤖 Updates screenshots
rharter Nov 4, 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
7 changes: 0 additions & 7 deletions .github/scripts/emu_setup.sh

This file was deleted.

66 changes: 23 additions & 43 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,23 +71,6 @@ jobs:
pull-requests: write

steps:
- name: Delete unnecessary tools 🔧
run: |
echo Remote tool cache
sudo rm -rf "$AGENT_TOOLSDIRECTORY" || true

echo Remove dotnet runtime
sudo rm -rf /usr/share/dotnet || true

echo Remove haskell runtime
sudo rm -rf /opt/ghc || true
sudo rm -rf /usr/local/.ghcup || true

echo Remove swap storage
sudo swapoff -a || true
sudo rm -f /mnt/swapfile || true
free -h

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
Expand Down Expand Up @@ -117,35 +100,32 @@ jobs:
path: |
~/.android/avd/*
~/.android/adb*
key: ${{ runner.os }}-avd-x86_64-pixel_5-31
key: ${{ runner.os }}-avd1-x86_64-pixel_5-31

- name: Create AVD snapshot for caching
- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # v2.33.0
with:
api-level: 31
arch: x86_64
profile: pixel_5
force-avd-creation: false
disable-animations: false
ram-size: 4096M
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
script: ./.github/scripts/emu_setup.sh && echo "Generated AVD snapshot for caching."
disable-animations: false
script: echo "Generated AVD snapshot for caching."

- name: Run instrumentation tests
id: screenshotsverify
continue-on-error: true
uses: reactivecircus/android-emulator-runner@v2
uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # v2.33.0
with:
api-level: 31
arch: x86_64
profile: pixel_5
disable-animations: true
force-avd-creation: false
ram-size: 4096M
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
# Workaround for https://github.com/ReactiveCircus/android-emulator-runner/issues/319
script: ./.github/scripts/emu_setup.sh && ./gradlew connectedCheck --stacktrace
disable-animations: true
script: ./gradlew connectedCheck --stacktrace

- name: Prevent pushing new screenshots if this is a fork
id: checkfork_screenshots
Expand All @@ -154,28 +134,28 @@ jobs:
run: |
echo "::error::Screenshot tests failed, please create a PR in your fork first." && exit 1

- name: Record new screenshots
id: screenshotsrecord
- name: Pull screenshots
id: screenshotspull
continue-on-error: true
if: steps.screenshotsverify.outcome == 'failure' && github.event_name == 'pull_request'
uses: reactivecircus/android-emulator-runner@v2
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/

# 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.
- name: Validate updated screenshots
uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # v2.33.0
if: steps.screenshotsverify.outcome == 'failure' && steps.screenshotspull.outcome == 'success'
with:
api-level: 31
arch: x86_64
profile: pixel_5
disable-animations: true
force-avd-creation: false
ram-size: 4096M
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
# Workaround for https://github.com/ReactiveCircus/android-emulator-runner/issues/319
script: ./.github/scripts/emu_setup.sh && ./gradlew connectedCheck -Pdropshots.record --stacktrace

- name: Pull screenshots
id: screenshotspull
continue-on-error: true
if: steps.screenshotsrecord.outcome == 'success' && github.event_name == 'pull_request'
run: |
rm dropshots/src/androidTest/assets/*.png || true
cp dropshots/build/reports/androidTests/dropshots/*.png dropshots/src/androidTest/assets/
disable-animations: true
script: ./gradlew connectedCheck --stacktrace

- name: Push new screenshots if available
uses: stefanzweifel/git-auto-commit-action@4b8a201e31cadd9829df349894b28c54e6c19fe6
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,15 @@ have to create them...

### Updating reference images

Updating reference screenshots is as simple as running the tests with a `dropshots.record` property
added to Gradle. This makes it easy to update screenshots in a single step, without requiring you to
Updating reference screenshots is as simple as running the `record[variant]Screenshots` 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.

> **Important**: Ensure that you record screenshots on an emulator that's been configured in the
> same way as the emulators on which you'll validate the screenshots.

```shell
./gradlew :path:to:module:connectedAndroidTest -Pdropshots.record
./gradlew :path:to:module:recordDebugScreenshots
```

After running this command, you'll see that all reference screenshots for the module will have been
Expand Down
10 changes: 10 additions & 0 deletions dropshots-gradle-plugin/api/dropshots-gradle-plugin.api
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ public abstract class com/dropbox/dropshots/PullScreenshotsTask : org/gradle/api
public final fun pullScreenshots ()V
}

public abstract class com/dropbox/dropshots/PushFileTask : 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 final class com/dropbox/dropshots/VersionKt {
public static final field VERSION Ljava/lang/String;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.TestedExtension
import com.android.build.gradle.api.ApkVariant
import com.android.build.gradle.internal.tasks.AndroidTestTask
import com.android.build.gradle.internal.tasks.factory.dependsOn
import java.util.Locale
import org.gradle.api.Plugin
import org.gradle.api.Project
Expand All @@ -27,12 +28,6 @@ public class DropshotsPlugin : Plugin<Project> {
}

private fun Project.configureDropshots(extension: TestedExtension) {
val isRecordingScreenshots = hasProperty(recordScreenshotsArg)

extension.buildTypes.getByName("debug") {
it.resValue("bool", "is_recording_screenshots", isRecordingScreenshots.toString())
}

project.afterEvaluate {
it.dependencies.add(
"androidTestImplementation",
Expand All @@ -54,6 +49,7 @@ public class DropshotsPlugin : Plugin<Project> {
val adbExecutablePath = provider { extension.adbExecutable.path }
extension.testVariants.all { variant ->
val testTaskProvider = variant.connectedInstrumentTestProvider
val variantSlug = variant.name.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }

val screenshotDir = provider {
val appId = if (variant.testedVariant is ApkVariant) {
Expand All @@ -66,49 +62,61 @@ public class DropshotsPlugin : Plugin<Project> {
}

val clearScreenshotsTask = tasks.register(
"clear${variant.name.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }}Screenshots",
"clear${variantSlug}Screenshots",
ClearScreenshotsTask::class.java,
) {
it.adbExecutable.set(adbExecutablePath)
it.screenshotDir.set(screenshotDir)
}

val pullScreenshotsTask = tasks.register(
"pull${variant.name.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }}Screenshots",
"pull${variantSlug}Screenshots",
PullScreenshotsTask::class.java,
) {
it.onlyIf { !isRecordingScreenshots }
it.adbExecutable.set(adbExecutablePath)
it.screenshotDir.set(screenshotDir)
it.screenshotDir.set(screenshotDir.map { base -> "$base/diff" })
it.outputDirectory.set(testTaskProvider.flatMap { (it as AndroidTestTask).resultsDir })
it.finalizedBy(clearScreenshotsTask)
}

val updateScreenshotsTask = tasks.register(
"update${variant.name.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }}Screenshots",
val recordScreenshotsTask = tasks.register(
"record${variantSlug}Screenshots",
PullScreenshotsTask::class.java,
) {
it.description = "Updates the local reference screenshots"

it.onlyIf { isRecordingScreenshots }
it.adbExecutable.set(adbExecutablePath)
it.screenshotDir.set(screenshotDir)
it.screenshotDir.set(screenshotDir.map { base -> "$base/reference" })
it.outputDirectory.set(referenceScreenshotDirectory)
it.dependsOn(testTaskProvider)
it.finalizedBy(clearScreenshotsTask)
}

testTaskProvider.configure {
it.finalizedBy(pullScreenshotsTask, updateScreenshotsTask)
val isRecordingScreenshots = project.objects.property(Boolean::class.java)
if (hasProperty(recordScreenshotsArg)) {
project.logger.warn("The 'dropshots.record' property has been deprecated and will " +
"be removed in a future version.")
isRecordingScreenshots.set(true)
}
}
}
project.gradle.taskGraph.whenReady { graph ->
isRecordingScreenshots.set(recordScreenshotsTask.map { graph.hasTask(it) })
}

val writeMarkerFileTask = tasks.register(
"push${variantSlug}ScreenshotMarkerFile",
PushFileTask::class.java,
) {
it.onlyIf { isRecordingScreenshots.get() }
it.adbExecutable.set(adbExecutablePath)
it.fileContents.set("\n")
it.remotePath.set(screenshotDir.map { dir -> "$dir/.isRecordingScreenshots" })
it.finalizedBy(clearScreenshotsTask)
}
testTaskProvider.dependsOn(writeMarkerFileTask)

private fun Project.getAndroidExtension(): TestedExtension {
return when {
plugins.hasPlugin("com.android.application") -> extensions.findByType(AppExtension::class.java)!!
plugins.hasPlugin("com.android.library") -> extensions.findByType(LibraryExtension::class.java)!!
else -> throw IllegalArgumentException("Dropshots can only be applied to an Android project.")
testTaskProvider.configure {
it.finalizedBy(pullScreenshotsTask)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.dropbox.dropshots

import java.io.ByteArrayOutputStream
import java.nio.charset.StandardCharsets
import java.util.regex.Pattern
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.internal.file.temp.TemporaryFileProvider
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations

public abstract class PushFileTask : DefaultTask() {

@get:Input
public abstract val adbExecutable: Property<String>

/**
* The file to push to the emulator.
*/
@get:Input
public abstract val fileContents: Property<String>

/**
* The path to the file or directory on the emulator.
*/
@get:Input
public abstract val remotePath: Property<String>

@get:Inject
protected abstract val execOperations: ExecOperations

@get:Inject
protected abstract val tempFileProvider: TemporaryFileProvider

init {
description = "Push files to an emulator or device using adb."
outputs.upToDateWhen { false }
}

@TaskAction
public fun push() {
val tempFile = tempFileProvider.createTemporaryFile("adb-file", "", "dropshots")
tempFile.writeText(fileContents.get())

val adb = adbExecutable.get()
val remote = remotePath.get()
val output = ByteArrayOutputStream()
val checkResult = execOperations.exec {
it.executable = adb
it.args = listOf("devices")
it.isIgnoreExitValue = true
it.standardOutput = output
}

val whitespacePattern = Pattern.compile("\\s")
val hasConnectedDevice = output.toString(StandardCharsets.UTF_8).lines()
.any { it.trim().split(whitespacePattern).last() == "device" }

if (checkResult.exitValue == 0 && hasConnectedDevice) {
execOperations.exec {
it.executable = adb
it.args = listOf("push", tempFile.absolutePath, remote)
}
}
}
}
Loading