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-404: added kaspresso runner, added kaspresso run listeners, allure results hack refactored #438

Merged

Conversation

eakurnikov
Copy link
Collaborator

@eakurnikov eakurnikov commented Nov 23, 2022

Added KaspressoRunner, it simply sets the SpyRunListener, which then calls the runner back on each junit lifecycle event. The runner being a KaspressoRunNotifier propagates each event to the list of KaspressoRunListeners which it stores.

This listeners interface is pretty much the same as junits RunListener but we have more control over it. Junit listeners are set on instrumentation runner creation and on each AndroidJunit4's run call which happens before each test and before the Kaspresso builder is called. This means that we can't add these runners dynamically in runtime, but we have to, as KaspressoRunner should live in the common module (not allure support) and should know nothing about allure support. That's why we need a list of KaspressoRunListeners that can be set in runtime.

We dynamically set AllureSupportRunner and AllureResultsHack in the Kaspresso.Builder. AllureSupportRunner is used to run the allure lifecycle without using allures runner.

AllureResultsHack is being notified by the HackyVideoRecordingTestInterceptor on each video is recorded and stores the binding between the test uuid and both stub and real video files. After all tests are finished it copies all the allure results to the SD card and replaces the stub video files attached to the report with the real videos. So this injection takes place only once when all tests are finished. It is ok to have a state like that in this case as all the tests are being run sequentially in the instrumentation thread, and after all of them are finished the thread is killed (if we're using the orchestration the number of the tests being run is just 1).

TODO: docs and tests

@@ -31,13 +36,6 @@ fun Kaspresso.Builder.Companion.withAllureSupport(
*/
fun Kaspresso.Builder.addAllureSupport(): Kaspresso.Builder = apply {
Copy link
Collaborator Author

@eakurnikov eakurnikov Nov 30, 2022

Choose a reason for hiding this comment

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

Reverted all the changes in it to have no breaking changes and added a withForcedAllureSupport builder

customize.invoke(this)
val instrumentalDependencyProvider =
instrumentalDependencyProviderFactory.getComponentProvider<Kaspresso>(instrumentation)
forceAllureSupportFileProviders(instrumentalDependencyProvider)
Copy link
Collaborator Author

@eakurnikov eakurnikov Nov 30, 2022

Choose a reason for hiding this comment

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

Forcing the allure support file providers entities here. We override the users entities here to avoid a complete mess with them. For example a user can set its own DirProvider which is used in other entities. If we simply wrap it with our AllureDirsProvider, it can lead to unexpected interaction between them, we don't know what behavior has the wrapped entity. So the solution here is simply force our file providers if the user wants to use our hack. The name withForcedAllureSupport is also descriptive

*/
fun provideStubVideoFile(actualVideoFile: File): File {
val resFileName: String = actualVideoFile.name
return resourcesDirsProvider.provide(resourcesRootDirsProvider.stubVideoDir)
Copy link
Collaborator Author

@eakurnikov eakurnikov Nov 30, 2022

Choose a reason for hiding this comment

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

Use dirs hierarchy here as stubs are being overridden if we run multiple tests one after another inside the single instrumentation.

override val stubVideoDir = File("stub_video")
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Just for consistency

import androidx.test.uiautomator.UiDevice
import java.io.File

class AllureResultInjector(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

A separate entity responsible to replace all stubs in the results dir with real videos

}

private fun File.replaceWith(actualVideo: File) {
uiDevice.executeShellCommand("cp ${actualVideo.absolutePath} $this")
Copy link
Collaborator Author

@eakurnikov eakurnikov Nov 30, 2022

Choose a reason for hiding this comment

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

Not removing the source video files (not processed by allure) along with other resources. Keeping the current behaviour.

import org.json.JSONObject
import java.io.File

class AllureResultJsonParser {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

A separate entity that knows how to parse allure results json

import org.junit.runner.Result
import java.io.File

class AllureResultsHack(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

All the trick is in one place. It lives as long as the instrumentation lives. Stores the stub-to-real videos bindings and injects them into allure results when all the tests are finished. Files are being moved and copied only once.

Being a KaspressoRunListener it is stored in our KaspressoRunner.

import org.junit.runner.Result
import org.junit.runner.notification.Failure

class AllureRunListener : KaspressoLateRunListener {
Copy link
Collaborator Author

@eakurnikov eakurnikov Nov 30, 2022

Choose a reason for hiding this comment

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

Runs the allure lifecycle. We can't use the allure runner no more and so need to manage it by ourselves.

class KaspressoRunner : AndroidJUnitRunner(), KaspressoRunNotifier by KaspressoRunNotifierImpl() {

override fun onCreate(arguments: Bundle) {
arguments.putArgs("listener", SpyRunListener::class.java.name)
Copy link
Collaborator Author

@eakurnikov eakurnikov Nov 30, 2022

Choose a reason for hiding this comment

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

Simply set the SpyRunListener to JUnit. This listener notifies us back on the test's lifecycle events, so we can notify our custom KaspressoRunListeners and propagate the junit lifecycle.

import org.junit.runner.Result
import org.junit.runner.notification.Failure

interface KaspressoRunListener {
Copy link
Collaborator Author

@eakurnikov eakurnikov Nov 30, 2022

Choose a reason for hiding this comment

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

Pretty much the same as junit's RunListener, but it can be set in runtime instead.


import org.junit.runner.Description

interface KaspressoLateRunListener : KaspressoRunListener {
Copy link
Collaborator Author

@eakurnikov eakurnikov Nov 30, 2022

Choose a reason for hiding this comment

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

KaspressoRunListeners are being set to runner when creating Kaspresso.Builder. At this moment testRunStarted and testStarted are already called, but the listener may need to handle these events little bit later. For example AllureRunListener shouldn't miss testStarted event. So KaspressoLateRunListener is a dedicated interface for them.


internal class KaspressoRunNotifierImpl : KaspressoRunNotifier {
private val cache = KaspressoLateRunListener.Cache()
override val listeners: MutableList<KaspressoRunListener> = arrayListOf()
Copy link
Collaborator Author

@eakurnikov eakurnikov Nov 30, 2022

Choose a reason for hiding this comment

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

Maybe it is better to have LinkedHashMap<KaspressoRunListener::class, KaspressoRunListener> here, will think about it tomorrow


override fun addListener(listener: KaspressoRunListener) {
if (listener is KaspressoLateRunListener) {
listener.lateInit(cache)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Initializing late run listeners here and passing the data they missed.

DumpLogcatTestInterceptor(logcatDumper),
ScreenshotTestInterceptor(screenshots),
DumpViewsTestInterceptor(viewHierarchyDumper),
HackyVideoRecordingTestInterceptor(videos, allureResourcesFilesProvider, provider.runNotifier.getUniqueListener())
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Taking the hack out of the listeners list. Another option is to have AllureResultsHack instance simply in static. It would be simpler and not so much different in behavior, as the instance is single in both ways and dies with the process.

}

private fun Bundle.putArgs(key: String, vararg values: CharSequence?) {
val valuesArg: String = listOfNotNull<CharSequence>(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The listeners class names are stored in a string, so we just add our listener to the end of it.

import org.junit.runner.notification.Failure
import java.lang.IllegalStateException

interface KaspressoRunNotifier : KaspressoRunListener {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It is notified about the junit lifecycle as a listener and propagates it to the list of other listeners.

interface KaspressoLateRunListener : KaspressoRunListener {

fun lateInit(cache: Cache) {
testRunStarted(cache.testRunStartedDescription)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Once this listener is created and added to the notifier, this method is called with all the cached data from missed events.

@eakurnikov eakurnikov changed the title ISSUE-404: add both instr runner and junit4 runner as a draft fix ISSUE-404: added kaspresso runner, added kaspresso run listeners, allure results hack refactored Nov 30, 2022
@Nikitae57 Nikitae57 merged commit ba58338 into issue-404/Support_storage_restrictions Nov 30, 2022
@Nikitae57 Nikitae57 deleted the issue-404/runner-fix branch November 30, 2022 10:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants