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

[WIP] Describing assertion failures #19

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
@@ -1,13 +1,15 @@
package androidx.test.espresso.assertion

import android.view.View
import androidx.test.espresso.ViewAssertion
import org.hamcrest.Matcher
import org.hamcrest.StringDescription

/**
* @return a [String] description of [ViewAssertion].
*/
internal fun ViewAssertion?.describe(): String {
if (this == null) return "null"
this ?: return "null"
Copy link
Contributor

Choose a reason for hiding this comment

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

я не знаю code style, но контрол флоу читается оч плохо когда ретурны не в начале строки, так что я скорее за прошлую конструкцию, еще и с фигурными скобками


val builder = StringBuilder("Check ")

Expand All @@ -21,4 +23,9 @@ internal fun ViewAssertion?.describe(): String {
}

return builder.toString()
}

internal fun ViewAssertion.getViewMatcher(): Matcher<in View>? {
if (this is ViewAssertions.MatchesViewAssertion) return viewMatcher
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package androidx.test.espresso.assertion

import androidx.test.espresso.ViewAssertion
import androidx.test.espresso.matcher.ViewMatchersProvider

abstract class ViewAssertionsAndMatcherProvider : ViewMatchersProvider() {

protected val MATCHES_VIEW_ASSERTION: Class<out ViewAssertion> =
ViewAssertions.MatchesViewAssertion::class.java

// Here will be more view assertions
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package androidx.test.espresso.matcher

import android.view.View
import org.hamcrest.Matcher

abstract class ViewMatchersProvider {

protected val WITH_TEXT_MATCHER: Class<out Matcher<in View>> =
ViewMatchers.WithTextMatcher::class.java

protected val WITH_EFFECTIVE_VISIBILITY: Class<out Matcher<in View>> =
ViewMatchers.WithEffectiveVisibilityMatcher::class.java

// Here will be more view matchers
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ class ComposeProviderImpl(

val (flakySafetyProvider, failureLoggingProvider) = getProviders()

failureLoggingProvider.withLoggingOnFailureIfNotNull {
failureLoggingProvider.withLoggingOnFailureIfNotNull(null) {
flakySafetyProvider.flakySafelyIfNotNull {
invokeComposed(actions, kaspresso.libLogger)
invokeComposed(actions, kaspresso.libLogger, elements)
}
}

Expand All @@ -63,7 +63,7 @@ class ComposeProviderImpl(

val (flakySafetyProvider, failureLoggingProvider) = getProviders()

failureLoggingProvider.withLoggingOnFailureIfNotNull {
failureLoggingProvider.withLoggingOnFailureIfNotNull(view.interaction) {
flakySafetyProvider.flakySafelyIfNotNull {
invokeComposed(actions, kaspresso.libLogger)
}
Expand All @@ -72,9 +72,9 @@ class ComposeProviderImpl(
setInterception()
}

private fun getProviders(): Pair<FlakySafetyProvider?, FailureLoggingProvider?> {
private fun getProviders(): Pair<FlakySafetyProvider?, FailureLoggingProvider<ViewInteraction>?> {
var flakySafetyProvider: FlakySafetyProvider? = null
var failureLoggingProvider: FailureLoggingProvider? = null
var failureLoggingProvider: FailureLoggingProvider<ViewInteraction>? = null

kaspresso.viewBehaviorInterceptors.forEach { viewBehaviorInterceptor: ViewBehaviorInterceptor ->
when (viewBehaviorInterceptor) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.kaspersky.kaspresso.compose

import com.agoda.kakao.intercept.Interceptable
import com.kaspersky.kaspresso.internal.exceptions.ThrowableWithInteraction
import com.kaspersky.kaspresso.logger.UiTestLogger
import io.reactivex.exceptions.ExtCompositeException

Expand All @@ -9,17 +11,21 @@ import io.reactivex.exceptions.ExtCompositeException
* @param actions the list of actions to invoke.
* @param logger the logger
*/
internal fun invokeComposed(actions: List<() -> Unit>, logger: UiTestLogger) {
internal fun invokeComposed(
actions: List<() -> Unit>,
logger: UiTestLogger,
elements: List<Interceptable<*, *, *>>? = null
) {
logger.i("Composed action started.")
val cachedErrors: MutableList<Throwable> = mutableListOf()

actions.forEach { action: () -> Unit ->
actions.forEachIndexed { i: Int, action: () -> Unit ->
try {
action.invoke()
logger.i("Composed action succeeded.")
return
} catch (error: Throwable) {
cachedErrors += error
cachedErrors += elements?.let { ThrowableWithInteraction(error, it[i].view.interaction as Any) } ?: error
logger.i("One part of composed action failed.")
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.kaspersky.kaspresso.compose

import androidx.test.espresso.web.sugar.Web
import com.agoda.kakao.web.WebElementBuilder
import com.kaspersky.kaspresso.compose.pack.ActionsOnWebElementsPack
import com.kaspersky.kaspresso.compose.pack.ActionsPack
Expand Down Expand Up @@ -34,7 +35,7 @@ class WebComposeProviderImpl(

val (flakySafetyProvider, failureLoggingProvider) = getProviders()

failureLoggingProvider.withLoggingOnFailureIfNotNull {
failureLoggingProvider.withLoggingOnFailureIfNotNull(null) {
flakySafetyProvider.flakySafelyIfNotNull {
invokeComposed(actions, kaspresso.libLogger)
}
Expand All @@ -59,7 +60,7 @@ class WebComposeProviderImpl(

val (flakySafetyProvider, failureLoggingProvider) = getProviders()

failureLoggingProvider.withLoggingOnFailureIfNotNull {
failureLoggingProvider.withLoggingOnFailureIfNotNull(web.interaction) {
flakySafetyProvider.flakySafelyIfNotNull {
invokeComposed(actions, kaspresso.libLogger)
}
Expand All @@ -68,9 +69,9 @@ class WebComposeProviderImpl(
webElementBuilder.setInterception()
}

private fun getProviders(): Pair<FlakySafetyProvider?, FailureLoggingProvider?> {
private fun getProviders(): Pair<FlakySafetyProvider?, FailureLoggingProvider<Web.WebInteraction<*>>?> {
var flakySafetyProvider: FlakySafetyProvider? = null
var failureLoggingProvider: FailureLoggingProvider? = null
var failureLoggingProvider: FailureLoggingProvider<Web.WebInteraction<*>>? = null

kaspresso.webBehaviorInterceptors.forEach { webBehaviorInterceptor: WebBehaviorInterceptor ->
when (webBehaviorInterceptor) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import java.lang.IllegalArgumentException
* The builder class for parameters of [com.kaspersky.kaspresso.compose.ComposeProvider.compose] method.
*/
class ActionsOnElementsPack {

private val elements: MutableList<Interceptable<ViewInteraction, ViewAssertion, ViewAction>> = mutableListOf()
private val actions: MutableList<() -> Unit> = mutableListOf()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.kaspersky.kaspresso.failure

import android.view.View
import androidx.test.espresso.ViewAssertion
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.assertion.ViewAssertionsAndMatcherProvider
import com.kaspersky.kaspresso.obtain_value.actions.ObtainTextViewAction
import com.kaspersky.kaspresso.obtain_value.atom.ObtainValueAtom
import com.kaspersky.kaspresso.obtain_value.actions.ObtainValueViewAction
import com.kaspersky.kaspresso.obtain_value.actions.ObtainVisibilityViewAction
import org.hamcrest.Matcher

data class FailureLoggingParams internal constructor(
var viewAssertionDescribers: MutableMap<Class<out ViewAssertion>, ViewInteraction.() -> String>,
var viewMatcherDescribers: MutableMap<Class<out Matcher<in View>>, () -> ObtainValueViewAction>,
var webMatcherDescribers: MutableMap<Class<out Matcher<*>>, () -> ObtainValueAtom<*>>
) : ViewAssertionsAndMatcherProvider() {

internal constructor() : this(
viewAssertionDescribers = hashMapOf(),
viewMatcherDescribers = hashMapOf(),
webMatcherDescribers = hashMapOf()
) {
viewMatcherDescribers = hashMapOf(
WITH_TEXT_MATCHER to { ObtainTextViewAction() },
WITH_EFFECTIVE_VISIBILITY to { ObtainVisibilityViewAction() }
// here will be more view matchers
// users can add their own
)

// here will be webMatcherDescribers default initialization

// here will be viewAssertionDescribers default initialization
// viewAssertionDescribers = hashMapOf(
// MATCHES_VIEW_ASSERTION to { ObtainTextViewAction().apply { perform(this) }.value }
// )
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import org.hamcrest.Matcher
/**
* An interface to provide the logging failures functionality.
*/
interface FailureLoggingProvider {
interface FailureLoggingProvider<Interaction> {

/**
* Invokes the given [action] and logs if it fails.
Expand All @@ -15,14 +15,14 @@ interface FailureLoggingProvider {
*
* @return the result of the [action] invocation.
*/
fun <T> withLoggingOnFailure(action: () -> T): T
fun <T> withLoggingOnFailure(interaction: Interaction?, action: () -> T): T

/**
* Logs the [error]'s stacktrace.
*
* @param error the error to get stacktrace from.
*/
fun logStackTrace(error: Throwable)
fun logStackTrace(interaction: Interaction?, error: Throwable)

/**
* Logs the [error] description got by [viewMatcher] and throws the [error].
Expand All @@ -40,5 +40,7 @@ interface FailureLoggingProvider {
*
* @return the result of the [action] invocation.
*/
internal fun <T> FailureLoggingProvider?.withLoggingOnFailureIfNotNull(action: () -> T): T =
if (this != null) withLoggingOnFailure(action) else action.invoke()
internal fun <Interaction, T> FailureLoggingProvider<Interaction>?.withLoggingOnFailureIfNotNull(
interaction: Interaction?,
action: () -> T
): T = if (this != null) withLoggingOnFailure(interaction, action) else action.invoke()
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ package com.kaspersky.kaspresso.failure

import android.view.View
import androidx.test.espresso.PerformException
import com.kaspersky.kaspresso.internal.exceptions.ThrowableWithInteraction
import com.kaspersky.kaspresso.internal.extensions.espressoext.describe
import com.kaspersky.kaspresso.internal.extensions.other.getException
import com.kaspersky.kaspresso.internal.extensions.other.getStackTraceAsString
import com.kaspersky.kaspresso.logger.UiTestLogger
import com.kaspersky.kaspresso.failure.describe.FailedAssertionDescriber
import io.reactivex.exceptions.ExtCompositeException
import junit.framework.AssertionFailedError
import org.hamcrest.Matcher

/**
* The implementation of the [FailureLoggingProvider] interface.
*/
class FailureLoggingProviderImpl(
private val logger: UiTestLogger
) : FailureLoggingProvider {
class FailureLoggingProviderImpl<Interaction>(
private val logger: UiTestLogger,
private val failedAssertionDescriber: FailedAssertionDescriber<Interaction>? = null
) : FailureLoggingProvider<Interaction> {

/**
* Invokes the given [action] and logs if it fails.
Expand All @@ -26,12 +30,23 @@ class FailureLoggingProviderImpl(
* @throws Throwable if the [action] invocation failed.
*/
@Throws(Throwable::class)
override fun <T> withLoggingOnFailure(action: () -> T): T {
override fun <T> withLoggingOnFailure(interaction: Interaction?, action: () -> T): T {
return try {
action.invoke()
} catch (error: Throwable) {
logStackTrace(error)
throw error
logStackTrace(interaction, error)

throw when (error) {
is ThrowableWithInteraction -> {
error.throwable
}
is ExtCompositeException -> {
error.exceptions.map { if (it is ThrowableWithInteraction) it.throwable else it }.getException()!!
}
else -> {
error
}
}
}
}

Expand All @@ -40,13 +55,43 @@ class FailureLoggingProviderImpl(
*
* @param error the error to get stacktrace from.
*/
override fun logStackTrace(error: Throwable) {
logger.e(error.getStackTraceAsString())

if (error is ExtCompositeException) {
error.exceptions.forEachIndexed { i: Int, e: Throwable ->
logger.e("Composed exception ${i + 1} :")
logger.e(e.getStackTraceAsString())
override fun logStackTrace(interaction: Interaction?, error: Throwable) {
matzuk marked this conversation as resolved.
Show resolved Hide resolved
when (error) {
is ExtCompositeException -> {
logger.e(error.getStackTraceAsString())
error.exceptions.forEachIndexed { i: Int, e: Throwable ->
logger.e("Composed exception ${i + 1} :")
if (e is ThrowableWithInteraction) {
if (e.throwable is AssertionError) {
failedAssertionDescriber?.getDescription(e.interaction as Interaction)
?.let {
logger.e(
e.throwable.getStackTraceAsString()
.replace(e.throwable.toString(), "${e.throwable.javaClass.name}: $it")
)
}
?: logger.e(e.throwable.getStackTraceAsString())
} else {
logger.e(e.throwable.getStackTraceAsString())
}
} else {
logger.e(e.getStackTraceAsString())
}
}
}
else -> {
if (interaction != null && error is AssertionError) {
failedAssertionDescriber?.getDescription(interaction)
?.let {
logger.e(
error.getStackTraceAsString()
.replace(error.toString(), "${error.javaClass.name}: $it")
)
}
?: logger.e(error.getStackTraceAsString())
} else {
logger.e(error.getStackTraceAsString())
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.kaspersky.kaspresso.failure

import android.view.View
import androidx.test.espresso.FailureHandler
import androidx.test.espresso.ViewInteraction
import com.kaspersky.kaspresso.logger.UiTestLogger
import org.hamcrest.Matcher

Expand All @@ -11,7 +12,7 @@ import org.hamcrest.Matcher
class LoggingFailureHandler(
logger: UiTestLogger
) : FailureHandler,
FailureLoggingProvider by FailureLoggingProviderImpl(logger) {
FailureLoggingProvider<ViewInteraction> by FailureLoggingProviderImpl(logger) {

/**
* Calls [logDescriptionAndThrow] on each failure.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.kaspersky.kaspresso.failure.describe

import android.view.View
import androidx.test.espresso.ViewAssertion
import org.hamcrest.Matcher

data class AssertionCache(
internal var cachedViewAssertion: Class<out ViewAssertion>? = null,
internal var cachedViewMatcher: Matcher<in View>? = null,
internal var cachedWebMatcher: Matcher<*>? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.kaspersky.kaspresso.failure.describe

interface FailedAssertionDescriber<Interaction> {
fun getDescription(interaction: Interaction): String
}
Loading