Skip to content

Commit

Permalink
Issue 554 automatic artifacts pulling (#558)
Browse files Browse the repository at this point in the history
* ISSUE-554: Pulling artifacts after tests

* ISSUE-554: Added sample

* ISSUE-554: Updated doc

* ISSUE-554: Lint

* ISSUE-554: PR comments

* ISSUE-554: Fix compose test
  • Loading branch information
Nikitae57 committed Aug 23, 2023
1 parent d95040b commit 4fc315c
Show file tree
Hide file tree
Showing 11 changed files with 205 additions and 5 deletions.
Expand Up @@ -71,7 +71,7 @@ fun Kaspresso.Builder.Companion.withForcedAllureSupport(
val instrumentalDependencyProvider = instrumentalDependencyProviderFactory.getComponentProvider<Kaspresso>(instrumentation)
forceAllureSupportFileProviders(instrumentalDependencyProvider)
addRunListenersIfNeeded(instrumentalDependencyProvider)
}.apply(::addAllureSupportInterceptors)
}.apply(::postInitAllure)

private fun Kaspresso.Builder.forceAllureSupportFileProviders(provider: InstrumentalDependencyProvider) {
resourcesDirNameProvider = DefaultResourcesDirNameProvider()
Expand Down Expand Up @@ -107,7 +107,7 @@ private fun Kaspresso.Builder.addRunListenersIfNeeded(provider: InstrumentalDepe
}
}

private fun addAllureSupportInterceptors(builder: Kaspresso.Builder): Unit = with(builder) {
private fun postInitAllure(builder: Kaspresso.Builder): Unit = with(builder) {
if (!isAndroidRuntime) {
return@with
}
Expand Down
19 changes: 19 additions & 0 deletions docs/Wiki/Kaspresso_Robolectric.en.md
Expand Up @@ -135,3 +135,22 @@ As of Robolectric 4.8.1, there are some limitations to sharedTest: those tests r

1. Robolectric-Espresso supports Idling resources, but [doesn't support posting delayed messages to the Looper](https://github.com/robolectric/robolectric/issues/4807#issuecomment-1075863097)
2. Robolectric-Espresso will not support [tests that start new activities](https://github.com/robolectric/robolectric/issues/5104) (i.e. activity jumping)

#### Pulling the artifacts from the device to the host

Depending on your test configuration, useful artifacts may remain on the device after test finish: screenshots, reports, videos, etc.
In order to pull them off the device special scripts are programmed, which are executed after the completion of the test run on CI. With Kaspresso,
you can simplify this process. To do this, you need to configure the `artifactsPullParams` variable in the Kaspresso Builder. Example:

```kotlin
class SomeTest : TestCase(
kaspressoBuilder = Kaspresso.Builder.simple {
artifactsPullParams = ArtifactsPullParams(enabled = true, destinationPath = "artifacts/", artifactsRegex = Regex("(screenshots)|(videos)"))
}
) {
...
}
```

For this mechanism to work, you need to start the ADB server before running the test. After the test is completed, the artifacts will be located by the path specified in the `destinationPath`
argument relative to the working directory from which the ADB server was launched.
19 changes: 19 additions & 0 deletions docs/Wiki/Kaspresso_configuration.ru.md
Expand Up @@ -315,3 +315,22 @@ class EnricherBaseTestCase : BaseTestCase<TestCaseDsl, TestCaseData>(
```

После того, как это будет сделано, описанные вами действия будут выполняться до или после блока ```run``` основной секции.

#### Стягивание артефактов с устройства на хост

В зависимости от вашей конфигурации тестов, после выполнения последних на устройстве могут оставаться полезные артефакты: скриншоты, отчеты, видео и т.д.
Для того, чтобы стянуть их с устройства, как правило, программируют специальные скрипты, которые выполняют после завершения тестового прогона на CI. С Kaspresso
вы можете упростить этот процесс. Для этого в Kaspresso Builder'e необходимо сконфигурировать переменную `artifactsPullParams`. Пример:

```kotlin
class SomeTest : TestCase(
kaspressoBuilder = Kaspresso.Builder.simple {
artifactsPullParams = ArtifactsPullParams(enabled = true, destinationPath = "artifacts/", artifactsRegex = Regex("(screenshots)|(videos)"))
}
) {
...
}
```

Для работы этого механизма перед выполнением теста необходимо запустить ADB server. После завершения теста артефакты будут лежать по указанному в аргументе `destinationPath` пути относительно
рабочей директории, из которой был запущен ADB server.
Expand Up @@ -45,6 +45,7 @@ class FilesImpl(
* @param serverPath a path to copy. (If empty - pulls in adbServer directory (folder with file "adbserver-desktop.jar"))
*/
override fun pull(devicePath: String, serverPath: String) {
adbServer.performCmd("mkdir -p $serverPath")
adbServer.performAdb("pull $devicePath $serverPath")
logger.i("Pull file from $devicePath to $serverPath")
}
Expand Down
Expand Up @@ -2,8 +2,10 @@ package com.kaspersky.kaspresso.device.video.recorder

import android.app.Instrumentation
import android.content.Context
import android.hardware.display.DisplayManager
import android.media.MediaCodecList
import android.os.Build
import android.view.Display
import android.view.WindowManager
import androidx.annotation.RequiresApi
import androidx.test.uiautomator.UiDevice
Expand Down Expand Up @@ -49,7 +51,9 @@ class VideoRecordingThread(
val codecWidth = videoCapabilities.supportedHeights.upper

val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
instrumentation.targetContext.display!!
instrumentation.targetContext
.getSystemService(DisplayManager::class.java)
.getDisplay(Display.DEFAULT_DISPLAY)
} else {
(instrumentation.targetContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager?)?.defaultDisplay!!
}
Expand Down
@@ -0,0 +1,48 @@
package com.kaspersky.kaspresso.internal.runlisteners.artifactspull

import androidx.test.platform.app.InstrumentationRegistry
import com.kaspersky.kaspresso.device.files.Files
import com.kaspersky.kaspresso.files.dirs.DefaultDirsProvider
import com.kaspersky.kaspresso.files.dirs.DirsProvider
import com.kaspersky.kaspresso.instrumental.InstrumentalDependencyProviderFactory
import com.kaspersky.kaspresso.kaspresso.Kaspresso
import com.kaspersky.kaspresso.logger.Logger
import com.kaspersky.kaspresso.params.ArtifactsPullParams
import com.kaspersky.kaspresso.runner.listener.KaspressoRunListener
import org.junit.runner.Result
import java.io.File

class ArtifactsPullRunListener(
private val params: ArtifactsPullParams,
private val dirsProvider: DirsProvider = DefaultDirsProvider(InstrumentalDependencyProviderFactory().getComponentProvider<Kaspresso>(InstrumentationRegistry.getInstrumentation())),
private val files: Files,
private val logger: Logger
) : KaspressoRunListener {
override fun testRunFinished(result: Result) {
if (!params.enabled) return

val rootDir = dirsProvider.provideNew(File(""))
val filesInRootDir = rootDir.listFiles()
if (filesInRootDir.isNullOrEmpty()) {
logger.d("After test artifacts pulling abort: found no files to move")
return
}

logger.d("After test artifacts pulling started. Root dir=${rootDir.absolutePath}; artifacts regex=${params.artifactsRegex}; destination path=${params.destinationPath}")
filesInRootDir.forEach { file ->
try {
if (file.name.matches(params.artifactsRegex)) {
val fullFilePath = File(rootDir, file.name)
files.pull(
devicePath = fullFilePath.absolutePath,
serverPath = params.destinationPath
)
}
} catch (ex: Throwable) {
logger.e("Failed to move file $file due to exception")
logger.e(ex.stackTraceToString())
}
}
logger.d("After test artifacts pulling finished")
}
}
Expand Up @@ -110,8 +110,10 @@ import com.kaspersky.kaspresso.interceptors.watcher.view.impl.logging.LoggingAto
import com.kaspersky.kaspresso.interceptors.watcher.view.impl.logging.LoggingViewActionWatcherInterceptor
import com.kaspersky.kaspresso.interceptors.watcher.view.impl.logging.LoggingViewAssertionWatcherInterceptor
import com.kaspersky.kaspresso.interceptors.watcher.view.impl.logging.LoggingWebAssertionWatcherInterceptor
import com.kaspersky.kaspresso.internal.runlisteners.artifactspull.ArtifactsPullRunListener
import com.kaspersky.kaspresso.logger.UiTestLogger
import com.kaspersky.kaspresso.logger.UiTestLoggerImpl
import com.kaspersky.kaspresso.params.ArtifactsPullParams
import com.kaspersky.kaspresso.params.AutoScrollParams
import com.kaspersky.kaspresso.params.ClickParams
import com.kaspersky.kaspresso.params.ContinuouslyParams
Expand All @@ -122,6 +124,7 @@ import com.kaspersky.kaspresso.params.ScreenshotParams
import com.kaspersky.kaspresso.params.StepParams
import com.kaspersky.kaspresso.params.SystemDialogsSafetyParams
import com.kaspersky.kaspresso.params.VideoParams
import com.kaspersky.kaspresso.runner.listener.addUniqueListener
import com.kaspersky.kaspresso.testcases.core.testcontext.BaseTestContext
import io.github.kakaocup.kakao.Kakao

Expand Down Expand Up @@ -481,6 +484,12 @@ data class Kaspresso(
* If it was not specified, the default implementation is used.
*/
lateinit var clickParams: ClickParams

/**
* Holds the [ArtifactsPullParams].
* If it was not specified, the default implementation is used.
*/
lateinit var artifactsPullParams: ArtifactsPullParams
/**
* Holds an implementation of [DirsProvider] interface. If it was not specified, the default implementation is used.
*/
Expand Down Expand Up @@ -744,6 +753,7 @@ data class Kaspresso(
if (!::videoParams.isInitialized) videoParams = VideoParams()
if (!::elementLoaderParams.isInitialized) elementLoaderParams = ElementLoaderParams()
if (!::clickParams.isInitialized) clickParams = ClickParams.default()
if (!::artifactsPullParams.isInitialized) artifactsPullParams = ArtifactsPullParams(enabled = false)

if (!::screenshots.isInitialized) {
screenshots = ScreenshotsImpl(
Expand Down Expand Up @@ -910,6 +920,12 @@ data class Kaspresso(
TestRunLoggerWatcherInterceptor(libLogger),
defaultsTestRunWatcherInterceptor
)

if (artifactsPullParams.enabled) {
instrumentalDependencyProviderFactory.getComponentProvider<Kaspresso>(instrumentation).runNotifier.addUniqueListener {
ArtifactsPullRunListener(params = artifactsPullParams, files = files, logger = libLogger)
}
}
}

/**
Expand Down
@@ -0,0 +1,18 @@
package com.kaspersky.kaspresso.params

data class ArtifactsPullParams(
/**
* Relative path. Absolute one depends on the working directory from which ADB server was started
*/
val destinationPath: String = ".",

/**
* Artifacts would be pulled if it's name fits regex
*/
val artifactsRegex: Regex = "(screenshots)|(video)|(logcat)|(view_hierarchy)".toRegex(),

/**
* Whether Kaspresso should pull the artifacts after a test run. Needs an ADB server to work
*/
val enabled: Boolean = true
)
2 changes: 1 addition & 1 deletion samples/kaspresso-sample/build.gradle.kts
Expand Up @@ -5,7 +5,7 @@ plugins {
android {
defaultConfig {
applicationId = "com.kaspersky.kaspressample"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunner = "com.kaspersky.kaspresso.runner.KaspressoRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true"
}

Expand Down
@@ -0,0 +1,75 @@
package com.kaspersky.kaspressample.artifacts_pulling

import android.Manifest
import androidx.test.ext.junit.rules.activityScenarioRule
import androidx.test.rule.GrantPermissionRule
import com.kaspersky.kaspressample.MainActivity
import com.kaspersky.kaspressample.R
import com.kaspersky.kaspressample.screen.MainScreen
import com.kaspersky.kaspressample.screen.SimpleScreen
import com.kaspersky.kaspressample.simple_tests.CheckEditScenario
import com.kaspersky.kaspresso.kaspresso.Kaspresso
import com.kaspersky.kaspresso.params.ArtifactsPullParams
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import org.junit.Rule
import org.junit.Test

/**
* After test completes screenshots directory would be pulled into provided destination path ob the host machine
*/
class ArtifactsPullingTest : TestCase(kaspressoBuilder = Kaspresso.Builder.simple {
artifactsPullParams = ArtifactsPullParams(artifactsRegex = Regex("screenshots"), destinationPath = "../../output/artifacts")
}) {

@get:Rule
val runtimePermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)

@get:Rule
val activityRule = activityScenarioRule<MainActivity>()

@Test
fun test() = run {
step("Open Simple Screen") {
testLogger.i("I am testLogger")
device.screenshots.take("Additional_screenshot")
MainScreen {
simpleButton {
isVisible()
click()
}
}
}

step("Click button_1 and check button_2") {
SimpleScreen {
button1 {
click()
}
button2 {
isVisible()
}
}
}

step("Click button_2 and check edit") {
SimpleScreen {
button2 {
click()
}
edit {
flakySafely(timeoutMs = 7000) { isVisible() }
hasText(R.string.simple_fragment_text_edittext)
}
}
}

step("Check all possibilities of edit") {
scenario(
CheckEditScenario()
)
}
}
}
Expand Up @@ -43,7 +43,7 @@ class ComplexComposeTest : TestCase() {
}

step("Handle potential unexpected behavior") {
compose {
compose(timeoutMs = 60_000L) {
// the first potential branch when ComplexComposeScreen.stage1Button is visible
or(ComplexComposeScreen.stage1Button) {
isVisible()
Expand Down

0 comments on commit 4fc315c

Please sign in to comment.