-
Notifications
You must be signed in to change notification settings - Fork 153
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
ISSUE-404: added kaspresso runner, added kaspresso run listeners, allure results hack refactored #438
Conversation
7a8c308
to
2a9121f
Compare
2a9121f
to
2164349
Compare
...com/kaspersky/components/alluresupport/interceptors/testrun/VideoRecordingTestInterceptor.kt
Outdated
Show resolved
Hide resolved
...ort/src/main/kotlin/com/kaspersky/components/alluresupport/report/AttachVideoToReportHack.kt
Outdated
Show resolved
Hide resolved
...mple/src/androidTest/kotlin/com/kaspersky/kaspresso/alluresupport/AllureSupportSanityTest.kt
Outdated
Show resolved
Hide resolved
…ure results hack refactored
4c7d090
to
27c0d15
Compare
@@ -31,13 +36,6 @@ fun Kaspresso.Builder.Companion.withAllureSupport( | |||
*/ | |||
fun Kaspresso.Builder.addAllureSupport(): Kaspresso.Builder = apply { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 KaspressoRunListener
s and propagate the junit lifecycle.
import org.junit.runner.Result | ||
import org.junit.runner.notification.Failure | ||
|
||
interface KaspressoRunListener { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
KaspressoRunListener
s 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() |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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>( |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
Added
KaspressoRunner
, it simply sets theSpyRunListener
, which then calls the runner back on each junit lifecycle event. The runner being aKaspressoRunNotifier
propagates each event to the list ofKaspressoRunListeners
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 eachAndroidJunit4
'srun
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, asKaspressoRunner
should live in the common module (not allure support) and should know nothing about allure support. That's why we need a list ofKaspressoRunListeners
that can be set in runtime.We dynamically set
AllureSupportRunner
andAllureResultsHack
in theKaspresso.Builder
.AllureSupportRunner
is used to run the allure lifecycle without using allures runner.AllureResultsHack
is being notified by theHackyVideoRecordingTestInterceptor
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