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

Issue 554 automatic artifacts pulling #558

Merged
merged 6 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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")
}
}
Original file line number Diff line number Diff line change
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()

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.kaspersky.kaspresso.params

data class ArtifactsPullParams(
/**
* Whether Kaspresso should pull the artifacts after a test run. Needs an ADB server to work
*/
val enabled: Boolean = false,

/**
* 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()
)
2 changes: 1 addition & 1 deletion samples/kaspresso-sample/build.gradle.kts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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

class ArtifactsPullingTest : TestCase(kaspressoBuilder = Kaspresso.Builder.simple {
artifactsPullParams = ArtifactsPullParams(enabled = true, artifactsRegex = Regex("screenshots"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in samples maybe it is better to use all params, to show full scenario usage.

}) {

@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()
)
}
}
}
Loading