tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 6b9e8789bd3316d7714246af0f9403c9dd7767cc
parent 2a5d6919051e6046cc2278b8b4072a1cf6debbbd
Author: Delia Pop <dpop@mozilla.com>
Date:   Wed, 15 Oct 2025 12:31:23 +0000

Bug 1990878 - Simplify RetryTestRule.apply (ComplexMethod) r=aaronmt

Refactoring of the RetryTestRule.apply method, as Arron suggested.
10 UI tests successfully passed 25x on Firebase.
Try links: [[ https://treeherder.mozilla.org/jobs?repo=try&revision=991fa576f854575fa6679ec68ed8649a07ebae0b | 1 ]] , [[ https://treeherder.mozilla.org/jobs?repo=try&revision=06e94c05d13153bcf2385b8bc649298b8c2c626b | 2 ]]

Try with test scenario: [[ https://treeherder.mozilla.org/jobs?repo=try&landoCommitID=158232 | here ]] The test could not be picked up on Try.

Differential Revision: https://phabricator.services.mozilla.com/D266995

Diffstat:
Mmobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/RetryTestRule.kt | 124++++++++++++++++++++++++++++++++++++++-----------------------------------------
Amobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/RetryRuleRetryableExceptionsTest.kt | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 162 insertions(+), 64 deletions(-)

diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/RetryTestRule.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/RetryTestRule.kt @@ -14,7 +14,6 @@ import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement import org.mozilla.fenix.ext.components -import org.mozilla.fenix.helpers.Constants.TAG import org.mozilla.fenix.helpers.IdlingResourceHelper.unregisterAllIdlingResources import org.mozilla.fenix.helpers.TestHelper.appContext import org.mozilla.fenix.helpers.TestHelper.exitMenu @@ -26,75 +25,27 @@ import org.mozilla.fenix.helpers.TestHelper.exitMenu * */ class RetryTestRule(private val retryCount: Int = 5) : TestRule { - @Suppress("TooGenericExceptionCaught") + override fun apply(base: Statement, description: Description): Statement { return statement { - for (i in 1..retryCount) { + repeat(retryCount) { attempt -> try { - Log.i(TAG, "RetryTestRule: Started try #$i.") + Log.i(TAG, "RetryTestRule: Started try #${attempt + 1}.") base.evaluate() - break + return@statement // success, exit early } catch (t: NoLeakAssertionFailedError) { Log.i(TAG, "RetryTestRule: NoLeakAssertionFailedError caught, not retrying") - unregisterAllIdlingResources() - appContext.components.useCases.tabsUseCases.removeAllTabs() - exitMenu() + cleanup(true) throw t - } catch (t: AssertionError) { - Log.i(TAG, "RetryTestRule: AssertionError caught, retrying the UI test") - unregisterAllIdlingResources() - appContext.components.useCases.tabsUseCases.removeAllTabs() - exitMenu() - if (i == retryCount) { - Log.i(TAG, "RetryTestRule: Max numbers of retries reached.") - throw t - } - } catch (t: AssertionFailedError) { - Log.i(TAG, "RetryTestRule: AssertionFailedError caught, retrying the UI test") - unregisterAllIdlingResources() - exitMenu() - if (i == retryCount) { - Log.i(TAG, "RetryTestRule: Max numbers of retries reached.") - throw t - } - } catch (t: UiObjectNotFoundException) { - Log.i(TAG, "RetryTestRule: UiObjectNotFoundException caught, retrying the UI test") - unregisterAllIdlingResources() - exitMenu() - if (i == retryCount) { - Log.i(TAG, "RetryTestRule: Max numbers of retries reached.") - throw t - } - } catch (t: NoMatchingViewException) { - Log.i(TAG, "RetryTestRule: NoMatchingViewException caught, retrying the UI test") - unregisterAllIdlingResources() - exitMenu() - if (i == retryCount) { - Log.i(TAG, "RetryTestRule: Max numbers of retries reached.") - throw t - } - } catch (t: IdlingResourceTimeoutException) { - Log.i(TAG, "RetryTestRule: IdlingResourceTimeoutException caught, retrying the UI test") - unregisterAllIdlingResources() - exitMenu() - if (i == retryCount) { - Log.i(TAG, "RetryTestRule: Max numbers of retries reached.") - throw t - } - } catch (t: RuntimeException) { - Log.i(TAG, "RetryTestRule: RuntimeException caught, retrying the UI test") - unregisterAllIdlingResources() - exitMenu() - if (i == retryCount) { - Log.i(TAG, "RetryTestRule: Max numbers of retries reached.") - throw t - } - } catch (t: NullPointerException) { - Log.i(TAG, "RetryTestRule: NullPointerException caught, retrying the UI test") - unregisterAllIdlingResources() - exitMenu() - if (i == retryCount) { - Log.i(TAG, "RetryTestRule: Max numbers of retries reached.") + } catch (t: Throwable) { + if (t.isRetryable()) { + Log.i(TAG, "RetryTestRule: ${t::class.simpleName} caught, retrying the UI test") + cleanup() + if (attempt == retryCount - 1) { + Log.i(TAG, "RetryTestRule: Max number of retries reached.") + throw t + } + } else { throw t } } @@ -102,9 +53,54 @@ class RetryTestRule(private val retryCount: Int = 5) : TestRule { } } + private fun cleanup(removeTabs: Boolean = false) { + unregisterAllIdlingResources() + if (removeTabs) { + appContext.components.useCases.tabsUseCases.removeAllTabs() + } + exitMenu() + } + + private fun Throwable.isRetryable(): Boolean = when (this) { + is AssertionError, + is AssertionFailedError, + is UiObjectNotFoundException, + is NoMatchingViewException, + is IdlingResourceTimeoutException, + is RuntimeException, + is NullPointerException, + -> true + else -> false + } + + companion object { + private const val TAG = "RetryTestRule" + } +} + private inline fun statement(crossinline eval: () -> Unit): Statement { return object : Statement() { override fun evaluate() = eval() } } -} + + /** + * Represents a test case that supplies a Throwable to be thrown during a test. + * + * @property name A human-readable name for the test case. + * Used for display in test runner output and logs. + * @property supplier A lambda that returns a new instance of the Throwable to throw. + * It's evaluated during the test execution. + */ + data class ThrowableCase( + // The display name used in parameterized test output (e.g., "NullPointerException") + val name: String, + // Function that supplies the Throwable to throw when invoked + val supplier: () -> Throwable, + ) { + /** + * Overrides the default toString() so that the test runner displays the 'name' + * instead of a default data class string or lambda object ID. + */ + override fun toString(): String = name + } diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/RetryRuleRetryableExceptionsTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/RetryRuleRetryableExceptionsTest.kt @@ -0,0 +1,102 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.ui + +import android.util.Log +import androidx.test.espresso.IdlingResourceTimeoutException +import androidx.test.espresso.NoMatchingViewException +import androidx.test.filters.LargeTest +import androidx.test.uiautomator.UiObjectNotFoundException +import junit.framework.AssertionFailedError +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.mozilla.fenix.helpers.RetryTestRule +import org.mozilla.fenix.helpers.TestSetup +import org.mozilla.fenix.helpers.ThrowableCase +import java.util.concurrent.atomic.AtomicInteger + +@RunWith(Parameterized::class) +@LargeTest +class RetryRuleRetryableExceptionsTest( + private val case: ThrowableCase, +) : TestSetup() { + + private val attempts = AtomicInteger(0) + + @get:Rule + val retryRule = RetryTestRule(retryCount = 2) + +companion object { + @JvmStatic + @Parameterized.Parameters(name = "{index}: {0}") + fun data(): Collection<Array<Any>> = listOf( + arrayOf( + ThrowableCase( + "AssertionError", + ) { + AssertionError("Retryable AssertionError") + }, + ), + arrayOf( + ThrowableCase( + "AssertionFailedError", + ) { + AssertionFailedError("Retryable AssertionFailedError") + }, + ), + arrayOf( + ThrowableCase( + "UiObjectNotFoundException", + ) { + UiObjectNotFoundException("Retryable UiObjectNotFoundException") + }, + ), + arrayOf( + ThrowableCase( + "NoMatchingViewException", + ) { + NoMatchingViewException.Builder() + .withCause(Throwable("Retryable NoMatchingViewException")) + .build() + }, + ), + arrayOf( + ThrowableCase( + "IdlingResourceTimeoutException", + ) { + IdlingResourceTimeoutException( + listOf("Retryable IdlingResourceTimeoutException"), + ) + }, + ), + arrayOf( + ThrowableCase( + "RuntimeException", + ) { + RuntimeException("Retryable RuntimeException") + }, + ), + arrayOf( + ThrowableCase( + "NullPointerException", + ) { + NullPointerException("Retryable NullPointerException") + }, + ), + ) +} + + @Test + fun testRetryableExceptionsAreRetried() { + Log.i("RetryTest", "Running test with ${case.name}") + if (attempts.incrementAndGet() < 2) { + throw case.supplier.invoke() + } + assertTrue("Test retried and passed on attempt=${attempts.get()}", true) + } +}