diff options
| author | Albin <albin@mullvad.net> | 2022-12-28 14:51:06 +0100 |
|---|---|---|
| committer | Albin <albin@mullvad.net> | 2023-01-10 15:32:38 +0100 |
| commit | 332ebf63b4dd6abd54e57043e287865cf81fe713 (patch) | |
| tree | 4612eeff18d56b552f75944ea8f45743f498a2a2 /android | |
| parent | 42610bc223085e23181af2e679fce538e4b4b5c8 (diff) | |
| download | mullvadvpn-332ebf63b4dd6abd54e57043e287865cf81fe713.tar.xz mullvadvpn-332ebf63b4dd6abd54e57043e287865cf81fe713.zip | |
Improve test failure screenshot support
This commit improves the test failure auto screenshot on
newer devices. It also removes the auto-download of
screenshots via gradle as it's rarely used.
Diffstat (limited to 'android')
5 files changed, 120 insertions, 53 deletions
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 2a866ff601..e1a8f8be40 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,9 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> + + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + <application android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher" diff --git a/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/rule/CaptureScreenshotOnFailedTestRule.kt b/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/rule/CaptureScreenshotOnFailedTestRule.kt index 73d515c501..f5bbe5fa6b 100644 --- a/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/rule/CaptureScreenshotOnFailedTestRule.kt +++ b/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/rule/CaptureScreenshotOnFailedTestRule.kt @@ -1,30 +1,114 @@ package net.mullvad.mullvadvpn.test.common.rule +import android.content.ContentResolver +import android.content.ContentValues +import android.graphics.Bitmap +import android.os.Build +import android.os.Environment +import android.os.Environment.DIRECTORY_PICTURES +import android.provider.MediaStore import android.util.Log -import androidx.test.runner.screenshot.BasicScreenCaptureProcessor -import androidx.test.runner.screenshot.ScreenCaptureProcessor -import androidx.test.runner.screenshot.Screenshot -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter +import androidx.annotation.RequiresApi +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.nio.file.Paths +import java.time.OffsetDateTime +import java.time.temporal.ChronoUnit import org.junit.rules.TestWatcher import org.junit.runner.Description -class CaptureScreenshotOnFailedTestRule(private val logTag: String) : TestWatcher() { - override fun failed(e: Throwable?, description: Description?) { - Log.d(logTag, "Capturing screenshot of failed test: " + description?.methodName) - val timestamp = DateTimeFormatter.ISO_DATE_TIME.format(LocalDateTime.now()).replace(":", "") - val screenshotName = "$timestamp-${description?.methodName}" - captureScreenshot(screenshotName) +class CaptureScreenshotOnFailedTestRule(private val testTag: String) : TestWatcher() { + + override fun failed(e: Throwable?, description: Description) { + Log.d(testTag, "Capturing screenshot of failed test: " + description.methodName) + val timestamp = OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS) + val screenshotName = "$timestamp-${description.methodName}.jpeg" + captureScreenshot(testTag, screenshotName) + } + + private fun captureScreenshot(baseDir: String, filename: String) { + val contentResolver = getInstrumentation().targetContext.applicationContext.contentResolver + val contentValues = createBaseScreenshotContentValues() + + getInstrumentation().uiAutomation.takeScreenshot().apply { + if (Build.VERSION.SDK_INT >= 29) { + writeToMediaStore( + contentValues = contentValues, + contentResolver = contentResolver, + baseDir = baseDir, + filename = filename + ) + } else { + writeToExternalStorage( + contentValues = contentValues, + contentResolver = contentResolver, + baseDir = baseDir, + filename = filename + ) + } + } + } + + @RequiresApi(29) + private fun Bitmap.writeToMediaStore( + contentValues: ContentValues, + contentResolver: ContentResolver, + baseDir: String, + filename: String + ) { + contentValues.apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, filename) + put( + MediaStore.Images.Media.RELATIVE_PATH, + "$DIRECTORY_PICTURES/$baseDir" + ) + } + + val uri = + contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + + if (uri != null) { + contentResolver.openOutputStream(uri).use { + try { + this.compress(Bitmap.CompressFormat.JPEG, 50, it) + } catch (e: IOException) { + Log.e(testTag, "Unable to store screenshot: ${e.message}") + } + } + contentResolver.update(uri, contentValues, null, null) + } else { + Log.e(testTag, "Unable to store screenshot") + } } - private fun captureScreenshot(screenShotName: String) { - try { - val screenCapture = Screenshot.capture().apply { name = screenShotName } - val processorSet: MutableSet<ScreenCaptureProcessor> = HashSet() - processorSet.add(BasicScreenCaptureProcessor()) - screenCapture.process(processorSet) - } catch (ex: Exception) { - Log.d(logTag, "Error capturing screenshot: " + ex.message) + private fun Bitmap.writeToExternalStorage( + contentValues: ContentValues, + contentResolver: ContentResolver, + baseDir: String, + filename: String + ) { + val screenshotBaseDirectory = Paths.get( + Environment.getExternalStoragePublicDirectory(DIRECTORY_PICTURES).path, + baseDir, + ).toFile().apply { + if (exists().not()) { + mkdirs() + } } + FileOutputStream(File(screenshotBaseDirectory, filename)).use { outputStream -> + try { + this.compress(Bitmap.CompressFormat.JPEG, 50, outputStream) + } catch (e: IOException) { + Log.e(testTag, "Unable to store screenshot: ${e.message}") + } + } + contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + } + + private fun createBaseScreenshotContentValues() = ContentValues().apply { + put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") + put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) } } diff --git a/android/test/e2e/build.gradle.kts b/android/test/e2e/build.gradle.kts index fdd9f55593..a7950ef753 100644 --- a/android/test/e2e/build.gradle.kts +++ b/android/test/e2e/build.gradle.kts @@ -43,6 +43,7 @@ android { testInstrumentationRunnerArguments += mutableMapOf<String, String>().apply { put("clearPackageData", "true") + put("useTestStorageService", "true") addOptionalPropertyAsArgument("valid_test_account_token") addOptionalPropertyAsArgument("invalid_test_account_token") } @@ -62,39 +63,6 @@ android { } } -val localScreenshotPath = "$buildDir/reports/androidTests/connected/screenshots" -val deviceScreenshotPath = "/sdcard/Pictures/Screenshots" - -tasks.register("createDeviceScreenshotDir", Exec::class) { - executable = android.adbExecutable.toString() - args = listOf("shell", "mkdir", "-p", deviceScreenshotPath) -} - -tasks.register("createLocalScreenshotDir", Exec::class) { - executable = "mkdir" - args = listOf("-p", localScreenshotPath) -} - -tasks.register("clearDeviceScreenshots", Exec::class) { - executable = android.adbExecutable.toString() - args = listOf("shell", "rm", "-r", deviceScreenshotPath) -} - -tasks.register("fetchScreenshots", Exec::class) { - executable = android.adbExecutable.toString() - args = listOf("pull", "$deviceScreenshotPath/.", localScreenshotPath) - - dependsOn(tasks.getByName("createLocalScreenshotDir")) - finalizedBy(tasks.getByName("clearDeviceScreenshots")) -} - -tasks.whenTaskAdded { - if (name == "connectedDebugAndroidTest") { - dependsOn(tasks.getByName("createDeviceScreenshotDir")) - finalizedBy(tasks.getByName("fetchScreenshots")) - } -} - configure<org.owasp.dependencycheck.gradle.extension.DependencyCheckExtension> { // Skip the lintClassPath configuration, which relies on many dependencies that has been flagged // to have CVEs, as it's related to the lint tooling rather than the project's compilation class diff --git a/android/test/e2e/src/main/AndroidManifest.xml b/android/test/e2e/src/main/AndroidManifest.xml index 931f79d291..9ba07f4905 100644 --- a/android/test/e2e/src/main/AndroidManifest.xml +++ b/android/test/e2e/src/main/AndroidManifest.xml @@ -1,5 +1,8 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + <uses-permission android:name="android.permission.INTERNET" /> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/EndToEndTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/EndToEndTest.kt index 35ba3fbe46..b65c43e23c 100644 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/EndToEndTest.kt +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/EndToEndTest.kt @@ -1,7 +1,9 @@ package net.mullvad.mullvadvpn.test.e2e +import android.Manifest import android.content.Context import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule import androidx.test.runner.AndroidJUnit4 import androidx.test.uiautomator.UiDevice import net.mullvad.mullvadvpn.test.common.interactor.AppInteractor @@ -22,10 +24,16 @@ abstract class EndToEndTest { @JvmField val rule = CaptureScreenshotOnFailedTestRule(LOG_TAG) + @Rule + @JvmField + val permissionRule: GrantPermissionRule = GrantPermissionRule.grant( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + ) + lateinit var device: UiDevice lateinit var targetContext: Context lateinit var app: AppInteractor - lateinit var web: WebViewInteractor lateinit var validTestAccountToken: String lateinit var invalidTestAccountToken: String |
