Merge pull request #105 from fknives/issue#100-fix-screenshot-rule-api-21

Issue#100 Issue#104 Fix screenshot rule for API 21
This commit is contained in:
Gergely Hegedis 2022-07-14 10:59:47 +03:00 committed by GitHub
commit 54b3414771
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 200 additions and 31 deletions

View file

@ -131,3 +131,10 @@ jobs:
name: Emulator-Test-Results-${{ matrix.api-level }}
path: ./**/build/testscreenshots/*
retention-days: 1
- name: Upload Logcat Logs
uses: actions/upload-artifact@v2
if: always()
with:
name: Emulator-Logcat-Logs-${{ matrix.api-level }}
path: ./**/build/logcat.txt
retention-days: 1

54
.github/workflows/screenshot-tests.yml vendored Normal file
View file

@ -0,0 +1,54 @@
name: Verify Screenshots can be created and pulled
on:
workflow_dispatch:
env:
GITHUB_USERNAME: "fknives"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
run-screenshot-test-on-emulator:
runs-on: macos-latest
strategy:
matrix:
api-level: [ 21, 23, 24, 26, 28, 29, 30, 31 ]
fail-fast: false
steps:
- name: checkout
uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '11'
- name: Gradle cache
uses: gradle/gradle-build-action@v2
- name: AVD cache
uses: actions/cache@v3
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-${{ matrix.api-level }}
- name: create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
arch: 'x86_64'
api-level: ${{ matrix.api-level }}
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: false
script: echo "Generated AVD snapshot for caching."
- name: Run Android Tests
uses: reactivecircus/android-emulator-runner@v2
with:
arch: 'x86_64'
api-level: ${{ matrix.api-level }}
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: ./pullscreenshottest.sh

View file

@ -0,0 +1,38 @@
package org.fnives.test.showcase.rule
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule
import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule
import org.fnives.test.showcase.android.testutil.synchronization.MainDispatcherTestRule
import org.fnives.test.showcase.testutils.ReloadKoinModulesIfNecessaryTestRule
import org.fnives.test.showcase.ui.splash.SplashActivity
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.runner.RunWith
import org.koin.test.KoinTest
@Suppress("TestFunctionName")
@RunWith(AndroidJUnit4::class)
class ScreenshotTest : KoinTest {
private lateinit var activityScenario: ActivityScenario<SplashActivity>
private val mainDispatcherTestRule = MainDispatcherTestRule()
@Rule
@JvmField
val ruleOrder: RuleChain = RuleChain.outerRule(mainDispatcherTestRule)
.around(ReloadKoinModulesIfNecessaryTestRule())
.around(SafeCloseActivityRule { activityScenario })
.around(ScreenshotRule(prefix = "screenshot-rule", takeOnSuccess = true))
/** GIVEN loggedInState WHEN opened after some time THEN MainActivity is started */
@Test
fun screenshot() {
activityScenario = ActivityScenario.launch(SplashActivity::class.java)
activityScenario.moveToState(Lifecycle.State.RESUMED)
}
}

13
app/verifyfiles.sh Executable file
View file

@ -0,0 +1,13 @@
#!/usr/bin/env sh
# if given folder is empty throws error
if [ -d "$1" ]; then
if [ $(ls -A $1 | wc -l) ];
then
echo ""
else
exit 1
fi
else
exit 1
fi

View file

@ -23,7 +23,6 @@ afterEvaluate {
from components.release
groupId "$testUtilGroupId"
println("$testUtilArtifactId")
version "$testUtilVersion"
artifactId "$testUtilArtifactId"
artifact sourcesJar

View file

@ -20,7 +20,7 @@ def findAdbFromLocal = {
def sdkDir = properties.getProperty('sdk.dir')
return "$sdkDir/platform-tools/adb"
} else {
System.err.println("WARNING: SDK dir not found by local properties, returning static: $System.env.HOME/Library/Android/sdk/platform-tools/adb")
println("WARNING: SDK dir not found by local properties, returning static: $System.env.HOME/Library/Android/sdk/platform-tools/adb")
return "$System.env.HOME/Library/Android/sdk/platform-tools/adb"
}
}
@ -29,47 +29,64 @@ def packageName = propertyOrNull("screenshotsPackageName") ?: "$android.defaultC
def screenshotDirectory = propertyOrNull("screenshotsDirectory") ?: "test-screenshots"
def savePath = propertyOrNull("screenshotsSavePath") ?: "build/testscreenshots/"
def adb = propertyOrNull("adbPath") ?: findAdbFromLocal()
def internalFullPath = "/sdcard/Android/data/$packageName/files/Pictures/$screenshotDirectory/"
def deprecatedFullPath = "/sdcard/Pictures/$packageName/$screenshotDirectory/"
def contextInternalFullPath = "/data/data/$packageName/files/$screenshotDirectory/"
def contextExternalFullPath = "/sdcard/Android/data/$packageName/files/Pictures/$screenshotDirectory/"
def environmentExternalFullPath = "/sdcard/Pictures/$packageName/$screenshotDirectory/"
task pullScreenshotsInternal(type: Exec) {
task pullScreenshotsContextInternal(type: Exec) {
group = 'Test-Screenshots'
description = 'Pull screenshots From internal Storage'
description = 'Pull screenshots From context.external Storage'
ignoreExitValue(true)
commandLine "$adb", 'pull', "$internalFullPath", "$savePath/"
commandLine "$adb", 'pull', "$contextInternalFullPath", "$savePath/"
}
task pullScreenshotsDeprecated(type: Exec) {
task pullScreenshotsContextExternal(type: Exec) {
group = 'Test-Screenshots'
description = 'Pull screenshots From deprecated External Storage'
description = 'Pull screenshots From context.external Storage'
ignoreExitValue(true)
commandLine "$adb", 'pull', "$deprecatedFullPath", "$savePath/"
commandLine "$adb", 'pull', "$contextExternalFullPath", "$savePath/"
}
task pullScreenshots(dependsOn: [pullScreenshotsInternal, pullScreenshotsDeprecated]) {
task pullScreenshotsEnvironmentExternal(type: Exec) {
group = 'Test-Screenshots'
description = 'Pull screenshots From environment.external Storage'
ignoreExitValue(true)
commandLine "$adb", 'pull', "$environmentExternalFullPath", "$savePath/"
}
task pullScreenshots(dependsOn: [pullScreenshotsContextInternal, pullScreenshotsContextExternal, pullScreenshotsEnvironmentExternal]) {
group = 'Test-Screenshots'
description = 'Pull screenshots From Device'
}
task removeScreenshotsFromDeviceInternal(type: Exec) {
task removeScreenshotsFromDeviceContextInternal(type: Exec) {
group = 'Test-Screenshots'
description = 'Remove screenshots From internal Storage'
description = 'Remove screenshots From context.internal Storage'
ignoreExitValue(true)
commandLine "$adb", 'shell', 'rm', '-r', "$internalFullPath"
commandLine "$adb", 'shell', 'rm', '-r', "$contextInternalFullPath"
}
task removeScreenshotsFromDeviceDeprecated(type: Exec) {
task removeScreenshotsFromDeviceContextExternal(type: Exec) {
group = 'Test-Screenshots'
description = 'Remove screenshots From deprecated External Storage'
description = 'Remove screenshots From context.external Storage'
ignoreExitValue(true)
commandLine "$adb", 'shell', 'rm', '-r', "$deprecatedFullPath"
commandLine "$adb", 'shell', 'rm', '-r', "$contextExternalFullPath"
}
task removeScreenshotsFromDevice(dependsOn: [removeScreenshotsFromDeviceInternal, removeScreenshotsFromDeviceDeprecated]) {
task removeScreenshotsFromDeviceEnvironmentExternal(type: Exec) {
group = 'Test-Screenshots'
description = 'Remove screenshots From environment.external Storage'
ignoreExitValue(true)
commandLine "$adb", 'shell', 'rm', '-r', "$environmentExternalFullPath"
}
task removeScreenshotsFromDevice(dependsOn: [removeScreenshotsFromDeviceContextInternal, removeScreenshotsFromDeviceContextExternal, removeScreenshotsFromDeviceEnvironmentExternal]) {
group = 'Test-Screenshots'
description = 'Remove screenshots From Device'
}
@ -81,8 +98,22 @@ task removeLocalScreenshots(type: Delete) {
delete files("$savePath")
}
task saveLogcatLogs(type: Exec) {
group = 'Test-Screenshots'
description = 'Show Logcat'
doFirst {
standardOutput = new FileOutputStream("${buildDir}/logcat.txt")
}
commandLine "$adb", 'logcat', '-d'
}
task hasScreenshots(type: Exec) {
commandLine "sh", "./verifyfiles.sh", "$savePath"
}
afterEvaluate {
connectedDebugAndroidTest.finalizedBy pullScreenshots
pullScreenshots.finalizedBy removeScreenshotsFromDevice
connectedDebugAndroidTest.finalizedBy saveLogcatLogs
saveLogcatLogs.finalizedBy pullScreenshots
clean.dependsOn(removeLocalScreenshots)
}

3
pullscreenshottest.sh Executable file
View file

@ -0,0 +1,3 @@
./gradlew clean
./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=org.fnives.test.showcase.rule.ScreenshotTest
./gradlew app:hasScreenshots

View file

@ -1,6 +1,7 @@
package org.fnives.test.showcase.android.testutil.screenshot
import android.graphics.Bitmap
import android.util.Log
import androidx.test.runner.screenshot.ScreenCapture
import androidx.test.runner.screenshot.ScreenCaptureProcessor
import androidx.test.runner.screenshot.Screenshot
@ -38,28 +39,30 @@ class ScreenshotRule(
}
}
fun takeScreenshot(prefix: String = this.prefix, baseName: String) {
fun takeScreenshot(prefix: String = this.prefix, baseName: String, capture: ScreenCapture = Screenshot.capture()) {
val fileName = if (timestampSuffix) {
"$prefix-$baseName-${System.currentTimeMillis()}"
} else {
"$prefix-$baseName"
}
takeScreenshot(filename = fileName)
takeScreenshot(filename = fileName, capture = capture)
}
@Suppress("PrintStackTrace")
private fun takeScreenshot(filename: String) {
val capture: ScreenCapture = Screenshot.capture()
private fun takeScreenshot(filename: String, capture: ScreenCapture) {
capture.name = filename
capture.format = Bitmap.CompressFormat.JPEG
try {
capture.process(setOf(processor))
Log.d(TAG, "Saved image: $filename")
} catch (e: IOException) {
Log.d(TAG, "Couldn't save image: $e")
e.printStackTrace()
}
}
companion object {
const val TAG = "Screenshot Rule"
val Description.testScreenshotName get() = "${testClass.simpleName}-$methodName"
val Description.beforeTestScreenshotName get() = "$testScreenshotName-BEFORE"
val Description.successTestScreenshotName get() = "$testScreenshotName-SUCCESS"

View file

@ -2,18 +2,39 @@ package org.fnives.test.showcase.android.testutil.screenshot
import android.os.Build
import android.os.Environment
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.screenshot.ScreenCaptureProcessor
import androidx.test.runner.screenshot.basicScreenCaptureProcessor
import java.io.File
fun basicScreenCaptureProcessor(subDir: String = "test-screenshots") =
basicScreenCaptureProcessor(File(getTestPicturesDir(), subDir))
fun basicScreenCaptureProcessor(subDir: String = "test-screenshots"): ScreenCaptureProcessor {
val directory = File(getTestPicturesDir(), subDir)
Log.d(ScreenshotRule.TAG, "directory to save screenshots = ${directory.absolutePath}")
return basicScreenCaptureProcessor(File(getTestPicturesDir(), subDir))
}
/**
* BasicScreenCaptureProcessor seems to work differently on API versions,
* based on where we have access to save and pull the images from.
*
* see example issue: https://github.com/android/android-test/issues/818
*/
@Suppress("DEPRECATION")
fun getTestPicturesDir() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
InstrumentationRegistry.getInstrumentation().targetContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
} else {
fun getTestPicturesDir(): File? =
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
Log.d(ScreenshotRule.TAG, "context.internal folder")
InstrumentationRegistry.getInstrumentation().targetContext.filesDir
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val packageName = InstrumentationRegistry.getInstrumentation().targetContext.packageName
File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), packageName)
val environmentFolder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
val externalFolder = File(environmentFolder, packageName)
Log.d(ScreenshotRule.TAG, "environment.external folder")
externalFolder
} else {
Log.d(ScreenshotRule.TAG, "context.external folder")
InstrumentationRegistry.getInstrumentation().targetContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
}