From 37924dd47a113c01a88fd575ccf917cc66bf54a1 Mon Sep 17 00:00:00 2001 From: "v.scharf" Date: Tue, 20 Oct 2026 12:22:11 +0200 Subject: [PATCH 1/2] Retry failed tests --- .woodpecker/integration-test.yml | 13 ++++++--- .../java/eu/opencloud/android/LoginTest.kt | 6 ++++ .../opencloud/android/RemoveAccountsRule.kt | 16 +++++++++++ .../java/eu/opencloud/android/RetryRule.kt | 28 +++++++++++++++++++ 4 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 opencloudApp/src/integrationTest/java/eu/opencloud/android/RemoveAccountsRule.kt create mode 100644 opencloudApp/src/integrationTest/java/eu/opencloud/android/RetryRule.kt diff --git a/.woodpecker/integration-test.yml b/.woodpecker/integration-test.yml index 0e3f4cdd5b..e970dc83ef 100644 --- a/.woodpecker/integration-test.yml +++ b/.woodpecker/integration-test.yml @@ -19,7 +19,8 @@ steps: - curl -fL -o adbserver-desktop.jar https://raw.githubusercontent.com/KasperskyLab/Kaspresso/v.1.6.0/artifacts/adbserver-desktop.jar - java -jar adbserver-desktop.jar & - sleep 3 - - ./gradlew :opencloudApp:connectedOriginalDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.annotation=eu.opencloud.android.IntegrationTest --console=plain + - adb logcat -c + - trap 'adb logcat -d -s RetryRule:W | tee retries.log' EXIT; ./gradlew :opencloudApp:connectedOriginalDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.annotation=eu.opencloud.android.IntegrationTest -Pandroid.testInstrumentationRunnerArguments.retries=1 --console=plain - name: instrumented data tests image: *android_image @@ -36,8 +37,9 @@ steps: commands: - | python3 -c " - import xml.etree.ElementTree as ET, glob - passed = skipped = failed = 0 + import xml.etree.ElementTree as ET, glob, os + retries = open('retries.log').read() if os.path.exists('retries.log') else '' + passed = skipped = failed = flaky = 0 for f in glob.glob('**/TEST-*.xml', recursive=True): root = ET.parse(f).getroot() suites = [root] if root.tag == 'testsuite' else root.findall('testsuite') @@ -48,11 +50,14 @@ steps: print(f'⏭ {name}'); skipped += 1 elif tc.find('failure') is not None: print(f'❌ {name}'); failed += 1 + elif f\"{tc.get('name')}({suite.get('name')})\" in retries: + print(f'🔁 {name} (passed on retry)'); flaky += 1 else: print(f'✅ {name}'); passed += 1 print() print(f'✅ passed: {passed}') print(f'❌ failed: {failed}') + print(f'🔁 flaky: {flaky}') print(f'⏭ skipped: {skipped}') - print(f' total: {passed+failed+skipped}') + print(f' total: {passed+failed+flaky+skipped}') " \ No newline at end of file diff --git a/opencloudApp/src/integrationTest/java/eu/opencloud/android/LoginTest.kt b/opencloudApp/src/integrationTest/java/eu/opencloud/android/LoginTest.kt index 9abe60fdb7..5b38bc932b 100644 --- a/opencloudApp/src/integrationTest/java/eu/opencloud/android/LoginTest.kt +++ b/opencloudApp/src/integrationTest/java/eu/opencloud/android/LoginTest.kt @@ -22,6 +22,12 @@ class LoginTest : TestCase( ) } ) { + @get:Rule(order = Int.MIN_VALUE) + val retryRule = RetryRule() + + @get:Rule(order = Int.MIN_VALUE + 1) + val removeAccountsRule = RemoveAccountsRule() + @get:Rule val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( android.Manifest.permission.POST_NOTIFICATIONS diff --git a/opencloudApp/src/integrationTest/java/eu/opencloud/android/RemoveAccountsRule.kt b/opencloudApp/src/integrationTest/java/eu/opencloud/android/RemoveAccountsRule.kt new file mode 100644 index 0000000000..8708afb1fa --- /dev/null +++ b/opencloudApp/src/integrationTest/java/eu/opencloud/android/RemoveAccountsRule.kt @@ -0,0 +1,16 @@ +package eu.opencloud.android + +import android.accounts.AccountManager +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.rules.ExternalResource + +/** Removes all app accounts so every attempt starts from the start screen. */ +class RemoveAccountsRule : ExternalResource() { + override fun before() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val accountManager = AccountManager.get(context) + accountManager.getAccountsByType(context.getString(R.string.account_type)).forEach { + accountManager.removeAccountExplicitly(it) + } + } +} diff --git a/opencloudApp/src/integrationTest/java/eu/opencloud/android/RetryRule.kt b/opencloudApp/src/integrationTest/java/eu/opencloud/android/RetryRule.kt new file mode 100644 index 0000000000..b6cf039bff --- /dev/null +++ b/opencloudApp/src/integrationTest/java/eu/opencloud/android/RetryRule.kt @@ -0,0 +1,28 @@ +package eu.opencloud.android + +import android.util.Log +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Re-runs a failed test. Retries come from the "retries" instrumentation argument (default 0). + * Must be the outermost rule. + */ +class RetryRule( + private val retries: Int = InstrumentationRegistry.getArguments().getString("retries")?.toIntOrNull() ?: 0 +) : TestRule { + override fun apply(base: Statement, description: Description) = object : Statement() { + override fun evaluate() { + repeat(retries) { + try { + return base.evaluate() + } catch (t: Throwable) { + Log.w("RetryRule", "${description.displayName} failed, retrying", t) + } + } + base.evaluate() + } + } +} From dd540795e37941e8f151be27dfd07918b3233379 Mon Sep 17 00:00:00 2001 From: "v.scharf" Date: Tue, 20 Oct 2026 12:22:12 +0200 Subject: [PATCH 2/2] Hide Kotlin warnings in CI --- build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.gradle b/build.gradle index 49193d69f5..3b39f20583 100644 --- a/build.gradle +++ b/build.gradle @@ -53,6 +53,11 @@ subprojects { } } + // Hide Kotlin compiler warnings in CI logs + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + if (System.getenv("CI")) kotlinOptions.suppressWarnings = true + } + plugins.withId("com.android.application") { android { buildTypes {