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

[Android] Writing test into sdcard using TestStorage api #69

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,18 @@ $ adb pull /data/local/tmp/allure-results
```
Finally, you can generate the report via Allure CLI (see the [Allure Documentation][allure-cli]) or generate report with [allure-gradle][allure-gradle-plugin] plugin.

##### **Orchestrator TestStorage**
When tests clears app data between each tests then saving test results in app storage will not work because old test results will be cleared when app data is cleared.
To save test results directly on sdcard new TestStorage from androidx.test.services can be used.

Enabling test storage for automation tests:
- add `allure.results.useTestStorage=true` to `allure.properties` in androidTest resources
- add `androidTestUtil("androidx.test:orchestrator:VERSION}` to your app dependencies (if you do not have it already)

After that allure will use TestStorage to save test results. Test results will be saved by default into `/sdcard/googletest/test_outputfiles/allure-results`
To get those files from device you can use e.g `adb exec-out sh -c 'cd /sdcard/googletest/test_outputfiles && tar cf - allure-results' | tar xvf - -C /output/dir`

*NOTE: allure-results folder name can be changed using `allure.results.directory` property.*

##### Features

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package io.qameta.allure.android
import androidx.test.platform.app.InstrumentationRegistry
import io.qameta.allure.kotlin.AllureLifecycle
import io.qameta.allure.kotlin.FileSystemResultsWriter
import io.qameta.allure.kotlin.AllureResultsWriter
import io.qameta.allure.kotlin.util.PropertiesUtils
import java.io.File

object AllureAndroidLifecycle : AllureLifecycle(writer = FileSystemResultsWriter(::obtainResultsDirectory))
open class AllureAndroidLifecycle(writer: AllureResultsWriter = FileSystemResultsWriter(::obtainResultsDirectory)) :
AllureLifecycle(writer = writer)

/**
* Obtains results directory as a [File] reference.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.qameta.allure.android.listeners

import android.os.Build
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.runner.*
import org.junit.runner.notification.*

class ExternalStoragePermissionsListener : RunListener() {

override fun testRunStarted(description: Description?) {
InstrumentationRegistry.getInstrumentation().uiAutomation.apply {
val testServicesPackage = "androidx.test.services"
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
executeShellCommand("appops set $testServicesPackage MANAGE_EXTERNAL_STORAGE allow")
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> {
executeShellCommand("pm grant $testServicesPackage android.permission.READ_EXTERNAL_STORAGE")
executeShellCommand("pm grant $testServicesPackage android.permission.WRITE_EXTERNAL_STORAGE")
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.runner.AndroidJUnitRunner
import io.qameta.allure.android.AllureAndroidLifecycle
import io.qameta.allure.android.internal.isDeviceTest
import io.qameta.allure.android.listeners.ExternalStoragePermissionsListener
import io.qameta.allure.android.writer.TestStorageResultsWriter
import io.qameta.allure.kotlin.Allure
import io.qameta.allure.kotlin.junit4.AllureJunit4
import io.qameta.allure.kotlin.util.PropertiesUtils
import org.junit.runner.*
import org.junit.runner.manipulation.*
import org.junit.runner.notification.*

/**
* Wrapper over [AndroidJUnit4] that attaches the [AllureJunit4] listener
*/
class AllureAndroidJUnit4(clazz: Class<*>) : Runner(), Filterable, Sortable {
open class AllureAndroidJUnit4(clazz: Class<*>) : Runner(), Filterable, Sortable {

private val delegate = AndroidJUnit4(clazz)

Expand Down Expand Up @@ -43,12 +46,16 @@ class AllureAndroidJUnit4(clazz: Class<*>) : Runner(), Filterable, Sortable {
* if so it means that in one way or another the listener has already been attached.
*/
private fun createDeviceListener(): RunListener? {
if (Allure.lifecycle == AllureAndroidLifecycle) return null
if (Allure.lifecycle is AllureAndroidLifecycle) return null

Allure.lifecycle = AllureAndroidLifecycle
return AllureJunit4(AllureAndroidLifecycle)
val androidLifecycle = createAllureAndroidLifecycle()
Allure.lifecycle = androidLifecycle
return AllureJunit4(androidLifecycle)
}

protected open fun createAllureAndroidLifecycle() : AllureAndroidLifecycle =
createDefaultAllureAndroidLifecycle()

/**
* Creates listener for tests running in an emulated Robolectric environment.
*
Expand All @@ -70,15 +77,18 @@ class AllureAndroidJUnit4(clazz: Class<*>) : Runner(), Filterable, Sortable {
open class AllureAndroidJUnitRunner : AndroidJUnitRunner() {

override fun onCreate(arguments: Bundle) {
Allure.lifecycle = AllureAndroidLifecycle
Allure.lifecycle = createAllureAndroidLifecycle()
val listenerArg = listOfNotNull(
arguments.getCharSequence("listener"),
AllureJunit4::class.java.name
AllureJunit4::class.java.name,
ExternalStoragePermissionsListener::class.java.name.takeIf { useTestStorage }
).joinToString(separator = ",")
arguments.putCharSequence("listener", listenerArg)
super.onCreate(arguments)
}

protected open fun createAllureAndroidLifecycle() : AllureAndroidLifecycle =
createDefaultAllureAndroidLifecycle()
}

/**
Expand All @@ -92,3 +102,16 @@ open class MultiDexAllureAndroidJUnitRunner : AllureAndroidJUnitRunner() {
}
}

private fun createDefaultAllureAndroidLifecycle() : AllureAndroidLifecycle {
if (useTestStorage) {
return AllureAndroidLifecycle(TestStorageResultsWriter())
}

return AllureAndroidLifecycle()
}

private val useTestStorage: Boolean
get() = PropertiesUtils.loadAllureProperties()
.getProperty("allure.results.useTestStorage", "false")
.toBoolean()

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.qameta.allure.android.writer

import androidx.test.services.storage.TestStorage
import io.qameta.allure.kotlin.AllureResultsWriter
import io.qameta.allure.kotlin.OutputStreamResultsWriter
import io.qameta.allure.kotlin.model.TestResult
import io.qameta.allure.kotlin.model.TestResultContainer
import io.qameta.allure.kotlin.util.PropertiesUtils
import java.io.InputStream

class TestStorageResultsWriter : AllureResultsWriter {
private val defaultAllurePath by lazy { PropertiesUtils.resultsDirectoryPath }
private val testStorage by lazy { TestStorage() }

private val outputStreamResultsWriter = OutputStreamResultsWriter { name ->
testStorage.openOutputFile("$defaultAllurePath/$name")
}

override fun write(testResult: TestResult) {
outputStreamResultsWriter.write(testResult)
}

override fun write(testResultContainer: TestResultContainer) {
outputStreamResultsWriter.write(testResultContainer)
}

override fun write(source: String, attachment: InputStream) {
outputStreamResultsWriter.write(source, attachment)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.qameta.allure.kotlin

import io.qameta.allure.kotlin.model.TestResult
import io.qameta.allure.kotlin.model.TestResultContainer
import kotlinx.serialization.json.Json
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.util.UUID

class OutputStreamResultsWriter(private val streamProvider: (name: String) -> OutputStream) : AllureResultsWriter {

private val mapper: Json = Json {
prettyPrint = true
useArrayPolymorphism = true
}

override fun write(testResult: TestResult) {
val testResultName = generateTestResultName(testResult.uuid)
try {
val json = mapper.encodeToString(TestResult.serializer(), testResult)
streamProvider(testResultName).use {
it.write(json.toByteArray())
}
} catch (e: IOException) {
throw AllureResultsWriteException("Could not write Allure test result", e)
}
}

override fun write(testResultContainer: TestResultContainer) {
val testResultContainerName = generateTestResultContainerName(testResultContainer.uuid)
try {
val json = mapper.encodeToString(TestResultContainer.serializer(), testResultContainer)
streamProvider(testResultContainerName).use {
it.write(json.toByteArray())
}
} catch (e: IOException) {
throw AllureResultsWriteException("Could not write Allure test result container", e)
}
}

override fun write(source: String, attachment: InputStream) {
try {
attachment.use { input ->
streamProvider(source).use { output ->
input.copyTo(output)
}
}
} catch (e: IOException) {
throw AllureResultsWriteException("Could not write Allure attachment", e)
}
}

private fun generateTestResultContainerName(uuid: String? = UUID.randomUUID().toString()): String =
uuid + AllureConstants.TEST_RESULT_CONTAINER_FILE_SUFFIX

companion object {
@JvmStatic
@JvmOverloads
fun generateTestResultName(uuid: String = UUID.randomUUID().toString()): String {
return uuid + AllureConstants.TEST_RESULT_FILE_SUFFIX
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.qameta.allure.kotlin

import io.github.benas.randombeans.api.EnhancedRandom
import io.qameta.allure.kotlin.FileSystemResultsWriter.Companion.generateTestResultName
import io.qameta.allure.kotlin.model.*
import io.qameta.allure.kotlin.model.Attachment
import io.qameta.allure.kotlin.model.Link
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.*

class OutputStreamResultsWriterTest {

@Test
fun shouldWriteTestResult() {
var name = ""
val os = ByteArrayOutputStream()
val writer = OutputStreamResultsWriter {
name = it
os
}
val uuid = UUID.randomUUID().toString()
val testResult = generateTestResult(uuid)
writer.write(testResult)

val expectedName = generateTestResultName(uuid)
Assertions.assertThat(name)
.isEqualTo(expectedName)
Assertions.assertThat(os.size())
.isGreaterThan(0)
}

private fun generateTestResult(uuid: String = EnhancedRandom.random(String::class.java)): TestResult = TestResult(
uuid = uuid,
historyId = uuid,
testCaseId = uuid,
rerunOf = uuid,
fullName = uuid,
labels = EnhancedRandom.randomListOf(10, Label::class.java),
links = EnhancedRandom.randomListOf(10, Link::class.java)
).apply {
name = uuid
start = EnhancedRandom.random(Long::class.java)
stop = EnhancedRandom.random(Long::class.java)
stage = EnhancedRandom.random(Stage::class.java)
description = uuid
descriptionHtml = uuid
status = EnhancedRandom.random(Status::class.java)
statusDetails = EnhancedRandom.random(StatusDetails::class.java)
steps.addAll(EnhancedRandom.randomListOf(10, StepResult::class.java))
attachments.addAll(EnhancedRandom.randomListOf(10, Attachment::class.java))
parameters.addAll(EnhancedRandom.randomListOf(10, Parameter::class.java))
}
}
3 changes: 2 additions & 1 deletion buildSrc/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ object Versions {
const val multiDex = "2.0.1"

object Test {
const val runner = "1.3.0"
const val orchestrator = "1.4.1"
const val runner = "1.4.0"
const val junit = "1.1.2"
const val espresso = "3.3.0"
const val robolectric = "4.3.1"
Expand Down
1 change: 1 addition & 0 deletions samples/junit4-android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ dependencies {
}

testImplementation("org.robolectric:robolectric:${Versions.Android.Test.robolectric}")
androidTestUtil("androidx.test:orchestrator:${Versions.Android.Test.orchestrator}")
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
allure.results.useTestStorage=true
allure.results.directory=allure-results
allure.link.issue.pattern=https://jira.domain-name.com/browse/{}