Skip to content

Commit

Permalink
ISSUE-404: Using documents to avoid storage restrictions; allure reco…
Browse files Browse the repository at this point in the history
…rds videos
  • Loading branch information
Nikitae57 authored and eakurnikov committed Dec 2, 2022
1 parent 18876c9 commit d27d220
Show file tree
Hide file tree
Showing 17 changed files with 477 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import com.kaspersky.components.alluresupport.interceptors.step.AllureMapperStep
import com.kaspersky.components.alluresupport.interceptors.step.ScreenshotStepInterceptor
import com.kaspersky.components.alluresupport.interceptors.testrun.DumpLogcatTestInterceptor
import com.kaspersky.components.alluresupport.interceptors.testrun.DumpViewsTestInterceptor
import com.kaspersky.components.alluresupport.interceptors.testrun.MoveReportsInterceptor
import com.kaspersky.components.alluresupport.interceptors.testrun.ScreenshotTestInterceptor
import com.kaspersky.components.alluresupport.interceptors.testrun.TestRunStateHolder
import com.kaspersky.components.alluresupport.interceptors.testrun.TestRunUuidInterceptor
import com.kaspersky.components.alluresupport.interceptors.testrun.VideoRecordingTestInterceptor
import com.kaspersky.kaspresso.files.dirs.AllureDirsProvider
import com.kaspersky.kaspresso.kaspresso.Kaspresso

/**
Expand All @@ -26,6 +30,10 @@ fun Kaspresso.Builder.Companion.withAllureSupport(
*/
fun Kaspresso.Builder.addAllureSupport(): Kaspresso.Builder = apply {
if (isAndroidRuntime) {
val device = instrumentalDependencyProviderFactory.getComponentProvider<Kaspresso>(instrumentation).uiDevice
val allureDirsProvider = AllureDirsProvider(instrumentation, resourcesRootDirsProvider, device)
val stateHolder = TestRunStateHolder()

stepWatcherInterceptors.addAll(
listOf(
ScreenshotStepInterceptor(screenshots),
Expand All @@ -34,10 +42,12 @@ fun Kaspresso.Builder.addAllureSupport(): Kaspresso.Builder = apply {
)
testRunWatcherInterceptors.addAll(
listOf(
TestRunUuidInterceptor(stateHolder),
DumpLogcatTestInterceptor(logcatDumper),
ScreenshotTestInterceptor(screenshots),
VideoRecordingTestInterceptor(videos),
DumpViewsTestInterceptor(viewHierarchyDumper)
DumpViewsTestInterceptor(viewHierarchyDumper),
VideoRecordingTestInterceptor(videos, allureDirsProvider, stateHolder),
MoveReportsInterceptor(instrumentation, dirsProvider, resourcesDirsProvider, resourcesRootDirsProvider, stateHolder, device)
)
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.kaspersky.components.alluresupport.interceptors.testrun

import android.app.Instrumentation
import androidx.test.uiautomator.UiDevice
import com.google.common.io.CharStreams
import com.kaspersky.kaspresso.files.dirs.DirsProvider
import com.kaspersky.kaspresso.files.resources.ResourcesDirsProvider
import com.kaspersky.kaspresso.files.resources.ResourcesRootDirsProvider
import com.kaspersky.kaspresso.interceptors.watcher.testcase.TestRunWatcherInterceptor
import com.kaspersky.kaspresso.testcases.models.info.TestInfo
import org.json.JSONObject
import java.io.File

private const val ATTACHMENTS_JSON_FIELD = "attachments"
private const val NAME_JSON_FIELD = "name"
private const val SOURCE_JSON_FIELD = "source"
private const val MP4_EXTENSION = "mp4"

/**
* Current allure version stores reports to /data/data/your.package.name/files/allure-results.
* This interceptor moves them to default artifacts folder after test and replaces mock videos with real ones
*/
class MoveReportsInterceptor(
private val instrumentation: Instrumentation,
private val dirsProvider: DirsProvider,
private val resourcesDirsProvider: ResourcesDirsProvider,
private val rootDirsProvider: ResourcesRootDirsProvider,
private val stateHolder: TestRunStateHolder,
private val device: UiDevice
) : TestRunWatcherInterceptor {
override fun onTestFinished(testInfo: TestInfo, success: Boolean) {
val allureTargetDir = moveAllureReportToSdCard()
removeStubs(allureTargetDir)
moveAttachedVideosToProperDirectories(allureTargetDir)
cleanUp()
}

/**
* Deletes allure report under /data/data/your.package.name/files and empty directories after moving real videos from them
*/
private fun cleanUp() {
getOriginalAllureDir().deleteRecursively()
val videosDir = dirsProvider.provideNew(rootDirsProvider.videoRootDir)
device.executeShellCommand("rm -rf ${videosDir.absolutePath}")

dirsProvider.provideNew(rootDirsProvider.screenshotsRootDir).deleteRecursively()
dirsProvider.provideNew(rootDirsProvider.logcatRootDir).deleteRecursively()
dirsProvider.provideNew(rootDirsProvider.viewHierarchy).deleteRecursively()
}

/**
* @return allure results dir under /data/data/your.package.name/files
*/
private fun getOriginalAllureDir(): File = instrumentation.targetContext.filesDir.resolve("allure-results")

/**
* Moves allure report from /data/data/your.package.name/files/allure-report to external storage e.g.
* /sdcard/Documents/.../allure-results
* @return allure target directory on sdcard
*/
private fun moveAllureReportToSdCard(): File {
val allureResultsFile = getOriginalAllureDir()
val allureTargetDir = dirsProvider.provideNew(rootDirsProvider.allureRootDir)
if (!allureResultsFile.exists()) {
throw IllegalArgumentException("Unable to move allure results from $allureResultsFile. File not found")
}
allureResultsFile.copyRecursively(allureTargetDir)

return allureTargetDir
}

/**
* During allure test stubs are attached to report because real videos can't be recorded directly to /data/data.
* After kaspresso moves report to sdcard it has to replace stubs with real videos. Videos are moved using adb shell
* 👀 https://issuetracker.google.com/issues/258277873
* @param allureTargetDir allure directory under /sdcard
*/
private fun removeStubs(allureTargetDir: File) {
allureTargetDir.listFiles()?.forEach {
if (it.extension.contains(MP4_EXTENSION, ignoreCase = true)) {
it.delete()
}
}
}

/**
* Move (and rename) real videos from their original directories to report dir
* @param allureTargetDir allure directory under /sdcard
*/
private fun moveAttachedVideosToProperDirectories(allureTargetDir: File) {
stateHolder.attachedVideos.forEach {
saveAttachedVideo(it, allureTargetDir)
}
}

/**
* Used for screen recording workaround:
* screen cast can't be recorded to /data/data/.../allure-results because of permissions issues (👀 https://issuetracker.google.com/issues/258277873).
* We have to save video on /sdcard and in the same time attach a stub to allure report. Before moving report to /sdcard
* we need to parse report and save stub file name to rename and move an actual one to allure report dir after moving report to /sdcard. Then we
* remove stub videos in /sdcard and /data/data and move real video file to allure report directory to replace stubs
* @param attachedVideo video attached to report
* @param allureTargetDir allure directory under /sdcard
*/
private fun saveAttachedVideo(attachedVideo: AttachedVideo, allureTargetDir: File) {
val allureReportFile = allureTargetDir.resolve("${stateHolder.lastTestCaseUuid ?: ""}-result.json")
if (!allureReportFile.exists()) {
throw IllegalStateException("Can't attach video to report because the latter not found. Tried path ${allureReportFile.absolutePath}")
}

allureReportFile.inputStream().use {
val json = JSONObject(CharStreams.toString(it.reader()))
val attachments = json.getJSONArray(ATTACHMENTS_JSON_FIELD)
for (i in 0 until attachments.length()) {
val attachment = attachments.getJSONObject(i)
val attachmentName = attachment.getString(NAME_JSON_FIELD)
if (attachmentName.equals(attachedVideo.attachedStubFile.name, ignoreCase = true)) {
val source = attachment.getString(SOURCE_JSON_FIELD) // Attachment real filename
device.executeShellCommand("mv ${attachedVideo.actualFile.absolutePath} ${allureTargetDir.resolve(source)}")
attachedVideo.attachedStubFile.delete()
break
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.kaspersky.components.alluresupport.interceptors.testrun

import java.io.File

/**
* Used to store shared state used by multiple interceptors
*/
class TestRunStateHolder {
private val _attachedVideos = mutableListOf<AttachedVideo>()
val attachedVideos: List<AttachedVideo>
get() = _attachedVideos.toList()

var lastTestCaseUuid: String? = null

fun rememberAttachedVideo(stubFile: File, actualFile: File) {
val attachedVideo = AttachedVideo(attachedStubFile = stubFile, actualFile = actualFile)
_attachedVideos.add(attachedVideo)
}
}

/**
* This model represents allure video attachment. It's used for screen recording workaround
*/
data class AttachedVideo(
/**
* Stub while which has been used to attach a video to report. Should be replaced with a real video file after moving
* report to /sdcard
*/
val attachedStubFile: File,
/**
* Actual screen record file which has been saved into /sdcard. Should be used to replace a stub in allure report
*/
val actualFile: File
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.kaspersky.components.alluresupport.interceptors.testrun

import com.kaspersky.kaspresso.interceptors.watcher.testcase.TestRunWatcherInterceptor
import com.kaspersky.kaspresso.testcases.models.info.TestInfo
import io.qameta.allure.kotlin.Allure

class TestRunUuidInterceptor(
private val stateHolder: TestRunStateHolder
) : TestRunWatcherInterceptor {
override fun onTestStarted(testInfo: TestInfo) {
stateHolder.lastTestCaseUuid = Allure.lifecycle.getCurrentTestCase()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,39 @@ package com.kaspersky.components.alluresupport.interceptors.testrun

import com.kaspersky.components.alluresupport.files.attachVideoToAllureReport
import com.kaspersky.kaspresso.device.video.Videos
import com.kaspersky.kaspresso.files.dirs.AllureDirsProvider
import com.kaspersky.kaspresso.interceptors.watcher.testcase.TestRunWatcherInterceptor
import com.kaspersky.kaspresso.testcases.models.info.TestInfo
import io.qameta.allure.kotlin.Allure
import io.qameta.allure.kotlin.model.Status

/**
* Due to screen recorder bug we have to perform a workaround which requires VideoRecordingTestInterceptor
* and MoveReportsInterceptor to be the last two interceptors in allure reports. So if you use VideoRecordingTestInterceptor
* be sure to use MoveReportsInterceptor too. Otherwise your report will be under /data/data/your.package.name/files/allure-results
* and videos under /sdcard
*/
class VideoRecordingTestInterceptor(
private val videos: Videos
private val videos: Videos,
private val allureDirsProvider: AllureDirsProvider,
private val stateHolder: TestRunStateHolder
) : TestRunWatcherInterceptor {

override fun onTestStarted(testInfo: TestInfo) {
videos.record("Video_${testInfo.testName}")
}

override fun onTestFinished(testInfo: TestInfo, success: Boolean) {
videos.saveAndApply { attachVideoToAllureReport() }
videos.saveAndApply {
val stubFile = allureDirsProvider.provideReportVideoAttachmentStub(this)
stubFile.attachVideoToAllureReport()
val uuid = Allure.lifecycle.getCurrentTestCase() ?: ""
Allure.lifecycle.updateTestCase {
it.status = if (success) Status.PASSED else Status.FAILED
}
Allure.lifecycle.stopTestCase(uuid)
Allure.lifecycle.writeTestCase(uuid)
stateHolder.rememberAttachedVideo(stubFile = stubFile, actualFile = this)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.kaspersky.kaspresso.files.dirs

import android.annotation.SuppressLint
import android.app.Instrumentation
import android.content.Context
import android.os.Build
import android.os.Environment
import androidx.test.uiautomator.UiDevice
import com.kaspersky.kaspresso.files.resources.ResourcesRootDirsProvider
import com.kaspersky.kaspresso.internal.extensions.other.createDirIfNeeded
import com.kaspersky.kaspresso.internal.extensions.other.createFileIfNeeded
import java.io.File

class AllureDirsProvider(
private val instrumentation: Instrumentation,
private val resourcesRootDirsProvider: ResourcesRootDirsProvider,
device: UiDevice
) : DefaultDirsProvider(instrumentation, device) {

@Suppress("DEPRECATION")
@SuppressLint("WorldReadableFiles", "ObsoleteSdkInt")
override fun provideNew(dest: File): File {
if (isVideoDir(dest)) { // screen recorder can't record to /data/data
return super.provideNew(dest)
}

val dir: File = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> instrumentation.targetContext.applicationContext.filesDir.resolve(dest)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> Environment.getExternalStorageDirectory().resolve(dest)
else -> instrumentation.targetContext.applicationContext.getDir(dest.canonicalPath, Context.MODE_WORLD_READABLE)
}

return dir.createDirIfNeeded()
}

/**
* Used for allure report video attachment workaround. Creates stub video file in package private directory so allure could attach it to report
* @param actualVideoFile vide file saved by screen recorder
* @return stub video file under /data/data
*/
fun provideReportVideoAttachmentStub(actualVideoFile: File): File {
val defaultVideoDir = super.provideNew(resourcesRootDirsProvider.videoRootDir).absolutePath
val targetVideoDir = instrumentation.targetContext.applicationContext.filesDir.resolve(resourcesRootDirsProvider.videoRootDir).absolutePath
val targetFilePath = actualVideoFile.absolutePath.replace(defaultVideoDir, targetVideoDir)

return File(targetFilePath).parentFile!!
.createDirIfNeeded()
.resolve(actualVideoFile.name)
.createFileIfNeeded()
}

private fun isVideoDir(dest: File): Boolean {
return dest == resourcesRootDirsProvider.videoRootDir
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,54 @@ import android.app.Instrumentation
import android.content.Context
import android.os.Build
import android.os.Environment
import androidx.test.uiautomator.UiDevice
import com.kaspersky.kaspresso.internal.extensions.other.createDirIfNeeded
import java.io.File

class DefaultDirsProvider(
private val instrumentation: Instrumentation
open class DefaultDirsProvider(
private val instrumentation: Instrumentation,
private val device: UiDevice
) : DirsProvider {
private val clearedDirs = HashSet<File>()

@Suppress("DEPRECATION")
@SuppressLint("WorldReadableFiles", "ObsoleteSdkInt")
override fun provideNew(dest: File): File {
val dir: File = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> instrumentation.targetContext.applicationContext.filesDir.resolve(dest)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> Environment.getExternalStorageDirectory().resolve(dest)
else -> instrumentation.targetContext.applicationContext.getDir(dest.canonicalPath, Context.MODE_WORLD_READABLE)
val dir = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).resolve(dest)
} else {
instrumentation.targetContext.applicationContext.getDir(dest.canonicalPath, Context.MODE_WORLD_READABLE)
}

return dir.createDirIfNeeded()
}

override fun provideCleared(dest: File): File {
if (!clearedDirs.contains(dest)) {
clearDir(path = dest, inclusive = false)
clearDir(dest, inclusive = false)
clearedDirs.add(dest)
}
return dest.createDirIfNeeded()
}
@Suppress("SameParameterValue")
private fun clearDir(dest: File, inclusive: Boolean) {
clearDirManually(dest, inclusive)
if (dest.exists() && dest.list()?.isNotEmpty() == true) {
clearDirThroughShell(dest, inclusive)
}
}

private fun clearDirThroughShell(dest: File, inclusive: Boolean) {
if (inclusive) {
device.executeShellCommand("rm -r ${dest.absolutePath}")
} else {
device.executeShellCommand("find ${dest.absolutePath} -type f -delete")
}
}

private fun clearDir(path: File, inclusive: Boolean) {
private fun clearDirManually(path: File, inclusive: Boolean) {
if (path.isDirectory) {
path.listFiles()?.forEach { clearDir(path = it, inclusive = true) }
path.listFiles()?.forEach { clearDirManually(path = it, inclusive = true) }
}
if (inclusive) {
path.delete()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ interface ResourcesRootDirsProvider {
val screenshotsRootDir: File
val videoRootDir: File
val viewHierarchy: File
val allureRootDir: File
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class DefaultResourceFilesProvider(
FileExtension.MP4.toString()
)
val resourceDir = resourcesDirsProvider.provide(resourcesRootDirsProvider.videoRootDir, subDir)
.createDirIfNeeded()
return resourceDir.createDirIfNeeded()
.resolve(resFileName)
.createFileIfNeeded()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ class DefaultResourcesRootDirsProvider : ResourcesRootDirsProvider {
override val screenshotsRootDir = File("screenshots")
override val videoRootDir = File("video")
override val viewHierarchy = File("view_hierarchy")
override val allureRootDir = File("allure-results")
}
Loading

0 comments on commit d27d220

Please sign in to comment.