-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #19 from allure-framework/feature/allure-android
Allure Android implementation
- Loading branch information
Showing
17 changed files
with
495 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1 @@ | ||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||
package="io.qameta.allure.android" /> | ||
<manifest package="io.qameta.allure.android" /> |
18 changes: 18 additions & 0 deletions
18
allure-kotlin-android/src/main/java/io/qameta/allure/android/AllureAndroidLifecycle.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package io.qameta.allure.android | ||
|
||
import android.os.Environment | ||
import io.qameta.allure.kotlin.AllureLifecycle | ||
import io.qameta.allure.kotlin.FileSystemResultsWriter | ||
import io.qameta.allure.kotlin.util.PropertiesUtils | ||
import java.io.File | ||
|
||
object AllureAndroidLifecycle : AllureLifecycle(writer = FileSystemResultsWriter(obtainResultsDirectory())) | ||
|
||
/** | ||
* Obtains results directory as a [File] reference. | ||
* It takes into consideration the results directory specified via **allure.results.directory** property. | ||
*/ | ||
private fun obtainResultsDirectory(): File { | ||
val defaultAllurePath = PropertiesUtils.resultsDirectoryPath | ||
return File(Environment.getExternalStorageDirectory(), defaultAllurePath) | ||
} |
42 changes: 42 additions & 0 deletions
42
allure-kotlin-android/src/main/java/io/qameta/allure/android/AllureScreenshot.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package io.qameta.allure.android | ||
|
||
import android.util.Log | ||
import androidx.annotation.IntRange | ||
import androidx.test.uiautomator.UiDevice | ||
import io.qameta.allure.android.internal.createTemporaryFile | ||
import io.qameta.allure.android.internal.uiDevice | ||
import io.qameta.allure.kotlin.Allure | ||
import java.io.InputStream | ||
|
||
private const val TAG = "AllureScreenshot" | ||
|
||
/** | ||
* Takes a screenshot of a device in a given [quality] & [scale], after that attaches it to current step or test run. | ||
* Quality must be in range [0..100] or an exception will be thrown. | ||
* | ||
* It uses [androidx.test.uiautomator.UiDevice] to take the screenshot. | ||
* | ||
* @return true if screen shot is created and attached successfully, false otherwise | ||
*/ | ||
@Suppress("unused") | ||
fun allureScreenshot( | ||
name: String = "screenshot", | ||
@IntRange(from = 0, to = 100) quality: Int = 90, | ||
scale: Float = 1.0f | ||
): Boolean { | ||
require(quality in (0..100)) { "quality must be 0..100" } | ||
val uiDevice = uiDevice ?: return false.also { | ||
Log.e(TAG, "UiAutomation is unavailable. Can't take the screenshot") | ||
} | ||
val inputStream = uiDevice.takeScreenshot(scale, quality) ?: return false.also { | ||
Log.e(TAG, "Failed to take the screenshot") | ||
} | ||
Allure.attachment(name = name, content = inputStream, type = "image/png", fileExtension = ".png") | ||
return true | ||
} | ||
|
||
private fun UiDevice.takeScreenshot(scale: Float, quality: Int): InputStream? { | ||
val tempFile = createTemporaryFile(prefix = "screenshot") | ||
if (!takeScreenshot(tempFile, scale, quality)) return null | ||
return tempFile.inputStream() | ||
} |
44 changes: 44 additions & 0 deletions
44
allure-kotlin-android/src/main/java/io/qameta/allure/android/internal/TestUtils.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package io.qameta.allure.android.internal | ||
|
||
import android.annotation.SuppressLint | ||
import android.os.Build | ||
import android.util.Log | ||
import androidx.test.platform.app.InstrumentationRegistry | ||
import androidx.test.runner.permission.PermissionRequester | ||
import androidx.test.uiautomator.UiDevice | ||
import java.io.File | ||
|
||
/** | ||
* Gives information about the environment in which the tests are running: | ||
* - @return true for Android device (real device or emulator) | ||
* - @return false for emulated environment of Robolectric | ||
* | ||
* This is the same logic as present in [androidx.test.ext.junit.runners.AndroidJUnit4] for resolving class runner. | ||
*/ | ||
@SuppressLint("DefaultLocale") | ||
internal fun isDeviceTest(): Boolean = | ||
System.getProperty("java.runtime.name")?.toLowerCase()?.contains("android") ?: false | ||
|
||
internal fun requestExternalStoragePermissions() { | ||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return | ||
|
||
with(PermissionRequester()) { | ||
addPermissions("android.permission.WRITE_EXTERNAL_STORAGE") | ||
addPermissions("android.permission.READ_EXTERNAL_STORAGE") | ||
requestPermissions() | ||
} | ||
} | ||
|
||
/** | ||
* Retrieves [UiDevice] if it's available, otherwise null is returned. | ||
* In Robolectric tests [UiDevice] is inaccessible and this property serves as a safe way of accessing it. | ||
*/ | ||
internal val uiDevice: UiDevice? | ||
get() = runCatching { UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) } | ||
.onFailure { Log.e("UiDevice", "UiDevice unavailable") } | ||
.getOrNull() | ||
|
||
internal fun createTemporaryFile(prefix: String = "temp", suffix: String? = null): File { | ||
val cacheDir = InstrumentationRegistry.getInstrumentation().targetContext.cacheDir | ||
return createTempFile(prefix, suffix, cacheDir) | ||
} |
95 changes: 95 additions & 0 deletions
95
allure-kotlin-android/src/main/java/io/qameta/allure/android/listeners/LogcatListener.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package io.qameta.allure.android.listeners | ||
|
||
import android.util.Log | ||
import io.qameta.allure.kotlin.listener.ContainerLifecycleListener | ||
import io.qameta.allure.kotlin.listener.StepLifecycleListener | ||
import io.qameta.allure.kotlin.listener.TestLifecycleListener | ||
import io.qameta.allure.kotlin.model.StepResult | ||
import io.qameta.allure.kotlin.model.TestResult | ||
import io.qameta.allure.kotlin.model.TestResultContainer | ||
|
||
class LogcatListener : | ||
ContainerLifecycleListener by LogcatContainerListener(), | ||
TestLifecycleListener by LogcatTestLifecycleListener(), | ||
StepLifecycleListener by LogcatStepLifecycleListener() | ||
|
||
class LogcatContainerListener : ContainerLifecycleListener { | ||
override fun beforeContainerStart(container: TestResultContainer) { | ||
container.log("START") | ||
} | ||
|
||
override fun beforeContainerUpdate(container: TestResultContainer) { | ||
container.log("UPDATE") | ||
} | ||
|
||
override fun beforeContainerStop(container: TestResultContainer) { | ||
container.log("STOP") | ||
} | ||
|
||
override fun beforeContainerWrite(container: TestResultContainer) { | ||
container.log("WRITE") | ||
} | ||
|
||
private fun TestResultContainer.log(action: String) { | ||
Log.d(TAG, "$action container: $uuid") | ||
} | ||
|
||
companion object { | ||
private const val TAG = "LogcatContainerListener" | ||
} | ||
} | ||
|
||
class LogcatTestLifecycleListener : TestLifecycleListener { | ||
|
||
override fun beforeTestSchedule(result: TestResult) { | ||
result.log("SCHEDULE") | ||
} | ||
|
||
override fun beforeTestStart(result: TestResult) { | ||
result.log("START") | ||
} | ||
|
||
override fun beforeTestUpdate(result: TestResult) { | ||
result.log("UPDATE") | ||
} | ||
|
||
override fun beforeTestStop(result: TestResult) { | ||
result.log("STOP") | ||
} | ||
|
||
override fun beforeTestWrite(result: TestResult) { | ||
result.log("WRITE") | ||
} | ||
|
||
private fun TestResult.log(action: String) { | ||
Log.d(TAG, "$action test: $uuid ($fullName)") | ||
} | ||
|
||
companion object { | ||
private const val TAG = "LogcatTestLifecycle" | ||
} | ||
} | ||
|
||
class LogcatStepLifecycleListener : StepLifecycleListener { | ||
|
||
override fun beforeStepStart(result: StepResult) { | ||
result.log("START") | ||
} | ||
|
||
override fun beforeStepUpdate(result: StepResult) { | ||
result.log("UPDATE") | ||
} | ||
|
||
override fun beforeStepStop(result: StepResult) { | ||
result.log("STOP") | ||
} | ||
|
||
private fun StepResult.log(action: String) { | ||
Log.d(TAG, "$action step: $name") | ||
} | ||
|
||
companion object { | ||
private const val TAG = "LogcatStepLifecycle" | ||
} | ||
} | ||
|
59 changes: 59 additions & 0 deletions
59
allure-kotlin-android/src/main/java/io/qameta/allure/android/rules/LogcatRule.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package io.qameta.allure.android.rules | ||
|
||
import android.os.Build | ||
import android.util.Log | ||
import androidx.test.uiautomator.UiDevice | ||
import io.qameta.allure.android.internal.uiDevice | ||
import io.qameta.allure.kotlin.Allure | ||
import org.junit.rules.* | ||
import org.junit.runner.* | ||
import org.junit.runners.model.* | ||
|
||
/** | ||
* Clears logcat before each test and dumps the logcat as an attachment after test failure. | ||
* | ||
* Available since API 21, no effect for other versions. | ||
*/ | ||
class LogcatRule(private val fileName: String = "logcat-dump") : TestRule { | ||
|
||
override fun apply(base: Statement?, description: Description?): Statement = object : Statement() { | ||
override fun evaluate() { | ||
try { | ||
clear() | ||
base?.evaluate() | ||
} catch (throwable: Throwable) { | ||
dump() | ||
throw throwable | ||
} | ||
} | ||
} | ||
|
||
private fun clear() { | ||
val uiDevice = uiDevice ?: return Unit.also { | ||
Log.e(TAG, "UiDevice is unavailable. Clearing logs failed.") | ||
} | ||
uiDevice.executeShellCommandSafely("logcat -c") | ||
} | ||
|
||
private fun dump() { | ||
val uiDevice = uiDevice ?: return Unit.also { | ||
Log.e(TAG, "UiDevice is unavailable. Dumping logs failed.") | ||
} | ||
val output = uiDevice.executeShellCommandSafely("logcat -d") ?: return | ||
Allure.attachment( | ||
name = fileName, | ||
content = output, | ||
type = "text/plain", | ||
fileExtension = ".txt" | ||
) | ||
} | ||
|
||
private fun UiDevice.executeShellCommandSafely(cmd: String): String? { | ||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return null | ||
return executeShellCommand(cmd) | ||
} | ||
|
||
companion object { | ||
private val TAG = LogcatRule::class.java.simpleName | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
allure-kotlin-android/src/main/java/io/qameta/allure/android/rules/ScreenshotRule.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package io.qameta.allure.android.rules | ||
|
||
import io.qameta.allure.android.allureScreenshot | ||
import org.junit.rules.* | ||
import org.junit.runner.Description | ||
import org.junit.runners.model.* | ||
import kotlin.Result | ||
|
||
/** | ||
* Makes screenshot of a device upon an end of a test case based on the specified [mode]. | ||
* It is then added as an attachment of a test case (with name [screenshotName]). | ||
* | ||
* By default, it will take a screenshot at the end of every test case (whether it failed or finished successfully). | ||
*/ | ||
class ScreenshotRule( | ||
private val mode: Mode = Mode.END, | ||
private val screenshotName: String = "end-screenshot" | ||
) : TestRule { | ||
|
||
override fun apply(base: Statement?, description: Description?): Statement = object : Statement() { | ||
override fun evaluate() { | ||
with(runCatching { base?.evaluate() }) { | ||
if (canTakeScreenshot(mode)) { | ||
allureScreenshot(screenshotName) | ||
} | ||
getOrThrow() | ||
} | ||
} | ||
} | ||
|
||
private fun Result<*>.canTakeScreenshot(mode: Mode): Boolean = | ||
when (mode) { | ||
Mode.END -> true | ||
Mode.SUCCESS -> isSuccess | ||
Mode.FAILURE -> isFailure | ||
} | ||
|
||
enum class Mode { | ||
END, | ||
SUCCESS, | ||
FAILURE | ||
} | ||
|
||
} | ||
|
53 changes: 53 additions & 0 deletions
53
allure-kotlin-android/src/main/java/io/qameta/allure/android/rules/WindowHierarchyRule.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package io.qameta.allure.android.rules | ||
|
||
import android.util.Log | ||
import androidx.test.uiautomator.UiDevice | ||
import io.qameta.allure.android.internal.uiDevice | ||
import io.qameta.allure.kotlin.Allure | ||
import org.junit.rules.* | ||
import org.junit.runner.* | ||
import org.junit.runners.model.* | ||
import java.io.ByteArrayInputStream | ||
import java.io.ByteArrayOutputStream | ||
import java.util.concurrent.TimeUnit | ||
|
||
/** | ||
* Dumps window hierarchy when the test fails and adds it as an attachment to allure results. | ||
*/ | ||
class WindowHierarchyRule(private val fileName: String = "window-hierarchy") : TestRule { | ||
|
||
override fun apply(base: Statement?, description: Description?): Statement = object : Statement() { | ||
override fun evaluate() { | ||
runCatching { base?.evaluate() } | ||
.onFailure { dumpWindowHierarchy() } | ||
.getOrThrow() | ||
} | ||
} | ||
|
||
private fun dumpWindowHierarchy() { | ||
val uiDevice = uiDevice ?: return Unit.also { | ||
Log.e(TAG, "UiAutomation is unavailable. Dumping window hierarchy failed.") | ||
} | ||
|
||
Allure.attachment( | ||
name = fileName, | ||
type = "text/xml", | ||
fileExtension = ".xml", | ||
content = uiDevice.dumpWindowHierarchy() | ||
) | ||
} | ||
|
||
private fun UiDevice.dumpWindowHierarchy(): ByteArrayInputStream = | ||
ByteArrayOutputStream() | ||
.apply { | ||
waitForIdle(TimeUnit.SECONDS.toMillis(5)) | ||
dumpWindowHierarchy(this) | ||
} | ||
.toByteArray() | ||
.inputStream() | ||
|
||
companion object { | ||
private val TAG = WindowHierarchyRule::class.java.simpleName | ||
} | ||
|
||
} |
Oops, something went wrong.