diff --git a/README.md b/README.md index 1e79a0c..db57190 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/allure-kotlin-android/src/main/kotlin/io/qameta/allure/android/AllureAndroidLifecycle.kt b/allure-kotlin-android/src/main/kotlin/io/qameta/allure/android/AllureAndroidLifecycle.kt index 540152d..470e5cb 100644 --- a/allure-kotlin-android/src/main/kotlin/io/qameta/allure/android/AllureAndroidLifecycle.kt +++ b/allure-kotlin-android/src/main/kotlin/io/qameta/allure/android/AllureAndroidLifecycle.kt @@ -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. diff --git a/allure-kotlin-android/src/main/kotlin/io/qameta/allure/android/listeners/ExternalStoragePermissionsListener.kt b/allure-kotlin-android/src/main/kotlin/io/qameta/allure/android/listeners/ExternalStoragePermissionsListener.kt new file mode 100644 index 0000000..3deedd9 --- /dev/null +++ b/allure-kotlin-android/src/main/kotlin/io/qameta/allure/android/listeners/ExternalStoragePermissionsListener.kt @@ -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") + } + } + } + } +} \ No newline at end of file diff --git a/allure-kotlin-android/src/main/kotlin/io/qameta/allure/android/runners/AllureAndroidJUnitRunners.kt b/allure-kotlin-android/src/main/kotlin/io/qameta/allure/android/runners/AllureAndroidJUnitRunners.kt index 48c495d..894e6fa 100644 --- a/allure-kotlin-android/src/main/kotlin/io/qameta/allure/android/runners/AllureAndroidJUnitRunners.kt +++ b/allure-kotlin-android/src/main/kotlin/io/qameta/allure/android/runners/AllureAndroidJUnitRunners.kt @@ -6,8 +6,11 @@ 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.* @@ -15,7 +18,7 @@ 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) @@ -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. * @@ -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() } /** @@ -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() + diff --git a/allure-kotlin-android/src/main/kotlin/io/qameta/allure/android/writer/TestStorageResultsWriter.kt b/allure-kotlin-android/src/main/kotlin/io/qameta/allure/android/writer/TestStorageResultsWriter.kt new file mode 100644 index 0000000..e7504ad --- /dev/null +++ b/allure-kotlin-android/src/main/kotlin/io/qameta/allure/android/writer/TestStorageResultsWriter.kt @@ -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) + } +} \ No newline at end of file diff --git a/allure-kotlin-commons/src/main/kotlin/io/qameta/allure/kotlin/OutputStreamResultsWriter.kt b/allure-kotlin-commons/src/main/kotlin/io/qameta/allure/kotlin/OutputStreamResultsWriter.kt new file mode 100644 index 0000000..09dde4d --- /dev/null +++ b/allure-kotlin-commons/src/main/kotlin/io/qameta/allure/kotlin/OutputStreamResultsWriter.kt @@ -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 + } + + } +} \ No newline at end of file diff --git a/allure-kotlin-commons/src/test/kotlin/io/qameta/allure/kotlin/OutputStreamResultsWriterTest.kt b/allure-kotlin-commons/src/test/kotlin/io/qameta/allure/kotlin/OutputStreamResultsWriterTest.kt new file mode 100644 index 0000000..a92c673 --- /dev/null +++ b/allure-kotlin-commons/src/test/kotlin/io/qameta/allure/kotlin/OutputStreamResultsWriterTest.kt @@ -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)) + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 323ad28..be97425 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -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" diff --git a/samples/junit4-android/build.gradle.kts b/samples/junit4-android/build.gradle.kts index 9d6d93e..0a5511c 100644 --- a/samples/junit4-android/build.gradle.kts +++ b/samples/junit4-android/build.gradle.kts @@ -52,4 +52,5 @@ dependencies { } testImplementation("org.robolectric:robolectric:${Versions.Android.Test.robolectric}") + androidTestUtil("androidx.test:orchestrator:${Versions.Android.Test.orchestrator}") } \ No newline at end of file diff --git a/samples/junit4-android/src/androidTest/resources/allure.properties b/samples/junit4-android/src/androidTest/resources/allure.properties index c858edb..02bcddd 100644 --- a/samples/junit4-android/src/androidTest/resources/allure.properties +++ b/samples/junit4-android/src/androidTest/resources/allure.properties @@ -1,2 +1,3 @@ +allure.results.useTestStorage=true allure.results.directory=allure-results allure.link.issue.pattern=https://jira.domain-name.com/browse/{}