From b28c5c7da4bc6e987c03b3e6d450b993c9b15c04 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Mon, 9 Jun 2025 11:58:28 +0200 Subject: [PATCH 1/7] Ensure tests are running with proper permissions and handle SecurityException --- .../DmfsStyleProvidersTaskTest.kt | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsStyleProvidersTaskTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsStyleProvidersTaskTest.kt index 3c626614..805d8349 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsStyleProvidersTaskTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsStyleProvidersTaskTest.kt @@ -9,11 +9,15 @@ package at.bitfire.ical4android import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import org.junit.After +import org.junit.Assert.assertNotNull import org.junit.Assume import org.junit.Before import org.junit.Rule +import org.junit.rules.TestRule +import org.junit.runner.Description import org.junit.runner.RunWith import org.junit.runners.Parameterized +import org.junit.runners.model.Statement import java.util.logging.Logger @RunWith(Parameterized::class) @@ -28,9 +32,22 @@ abstract class DmfsStyleProvidersTaskTest( fun taskProviders() = listOf(TaskProvider.ProviderName.OpenTasks,TaskProvider.ProviderName.TasksOrg) } - @JvmField - @Rule - val permissionRule = GrantPermissionRule.grant(*providerName.permissions) + @get:Rule + val permissionRule: TestRule = object : TestRule { + val rule = GrantPermissionRule.grant(*providerName.permissions) + + override fun apply(base: Statement, description: Description) = + object: Statement() { + override fun evaluate() { + val innerStatement = rule.apply(base, description) + try { + innerStatement.evaluate() + } catch (e: SecurityException) { + Assume.assumeNoException(e) + } + } + } + } var providerOrNull: TaskProvider? = null lateinit var provider: TaskProvider @@ -38,7 +55,7 @@ abstract class DmfsStyleProvidersTaskTest( @Before open fun prepare() { providerOrNull = TaskProvider.acquire(InstrumentationRegistry.getInstrumentation().context, providerName) - Assume.assumeNotNull(providerOrNull) // will halt here if providerOrNull is null + assertNotNull("$providerName is not installed", providerOrNull != null) provider = providerOrNull!! Logger.getLogger(javaClass.name).fine("Using task provider: $provider") From 57623dd4f566eb7463b0ec6b0f94a0a07f1e1b19 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Mon, 9 Jun 2025 12:02:02 +0200 Subject: [PATCH 2/7] Ensure tests run on push to main-ose and pull requests --- .github/workflows/test-dev.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml index e078a414..be05c89c 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/test-dev.yml @@ -1,5 +1,14 @@ name: Development tests -on: push +on: + push: + branches: + - 'main-ose' + pull_request: + +concurrency: + group: test-dev-${{ github.ref }} + cancel-in-progress: true + jobs: compile: name: Compile and cache @@ -59,9 +68,6 @@ jobs: sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: Check - run: ./gradlew --no-daemon check - - name: Cache AVD uses: actions/cache@v4 with: From 800d0b55d421f1e1002fbba0598ea261030f8cd4 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Mon, 9 Jun 2025 12:03:44 +0200 Subject: [PATCH 3/7] Compile/cache only on main branch --- .github/workflows/test-dev.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml index be05c89c..fcfdc81c 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/test-dev.yml @@ -12,6 +12,7 @@ concurrency: jobs: compile: name: Compile and cache + if: ${{ github.ref == 'refs/heads/main' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -31,6 +32,7 @@ jobs: test: needs: compile + if: ${{ !cancelled() }} # even if compile didn't run (because not on main branch) name: Unit tests runs-on: ubuntu-latest steps: @@ -49,6 +51,7 @@ jobs: test_on_emulator: needs: compile + if: ${{ !cancelled() }} # even if compile didn't run (because not on main branch) name: Instrumented tests runs-on: ubuntu-latest steps: From 3850977415f2e162219e36544984d2e806f2dabf Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Mon, 9 Jun 2025 12:16:24 +0200 Subject: [PATCH 4/7] Replace permission rule with custom rule to skip tests on missing permissions --- .../DmfsStyleProvidersTaskTest.kt | 22 ++---------- .../bitfire/ical4android/JtxCollectionTest.kt | 4 +-- .../bitfire/ical4android/JtxICalObjectTest.kt | 18 +++++++--- .../synctools/GrantPermissionOrSkipRule.kt | 36 +++++++++++++++++++ 4 files changed, 54 insertions(+), 26 deletions(-) create mode 100644 lib/src/androidTest/kotlin/at/bitfire/synctools/GrantPermissionOrSkipRule.kt diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsStyleProvidersTaskTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsStyleProvidersTaskTest.kt index 805d8349..e70a1777 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsStyleProvidersTaskTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsStyleProvidersTaskTest.kt @@ -7,17 +7,13 @@ package at.bitfire.ical4android import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.rule.GrantPermissionRule +import at.bitfire.synctools.GrantPermissionOrSkipRule import org.junit.After import org.junit.Assert.assertNotNull -import org.junit.Assume import org.junit.Before import org.junit.Rule -import org.junit.rules.TestRule -import org.junit.runner.Description import org.junit.runner.RunWith import org.junit.runners.Parameterized -import org.junit.runners.model.Statement import java.util.logging.Logger @RunWith(Parameterized::class) @@ -33,21 +29,7 @@ abstract class DmfsStyleProvidersTaskTest( } @get:Rule - val permissionRule: TestRule = object : TestRule { - val rule = GrantPermissionRule.grant(*providerName.permissions) - - override fun apply(base: Statement, description: Description) = - object: Statement() { - override fun evaluate() { - val innerStatement = rule.apply(base, description) - try { - innerStatement.evaluate() - } catch (e: SecurityException) { - Assume.assumeNoException(e) - } - } - } - } + val permissionRule = GrantPermissionOrSkipRule(providerName.permissions.toSet()) var providerOrNull: TaskProvider? = null lateinit var provider: TaskProvider diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt index 06512c9a..f114ca63 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt @@ -10,9 +10,9 @@ import android.accounts.Account import android.content.ContentProviderClient import android.content.ContentValues import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.rule.GrantPermissionRule import at.bitfire.ical4android.impl.TestJtxCollection import at.bitfire.ical4android.util.MiscUtils.closeCompat +import at.bitfire.synctools.GrantPermissionOrSkipRule import at.techbee.jtx.JtxContract import at.techbee.jtx.JtxContract.asSyncAdapter import junit.framework.TestCase.assertEquals @@ -37,7 +37,7 @@ class JtxCollectionTest { @JvmField @ClassRule - val permissionRule = GrantPermissionRule.grant(*TaskProvider.PERMISSIONS_JTX) + val permissionRule = GrantPermissionOrSkipRule(TaskProvider.PERMISSIONS_JTX.toSet()) @BeforeClass @JvmStatic diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt index 95dcd30f..d140feae 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt @@ -15,17 +15,27 @@ import android.database.DatabaseUtils import android.os.ParcelFileDescriptor import androidx.core.content.pm.PackageInfoCompat import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.rule.GrantPermissionRule import at.bitfire.ical4android.impl.TestJtxCollection import at.bitfire.ical4android.util.MiscUtils.closeCompat +import at.bitfire.synctools.GrantPermissionOrSkipRule import at.techbee.jtx.JtxContract import at.techbee.jtx.JtxContract.JtxICalObject import at.techbee.jtx.JtxContract.JtxICalObject.Component import at.techbee.jtx.JtxContract.asSyncAdapter -import junit.framework.TestCase.* +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import junit.framework.TestCase.assertTrue import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.Property -import org.junit.* +import org.junit.After +import org.junit.AfterClass +import org.junit.Assert +import org.junit.Assume +import org.junit.Before +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test import java.io.ByteArrayOutputStream import java.io.InputStreamReader @@ -40,7 +50,7 @@ class JtxICalObjectTest { @JvmField @ClassRule - val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(*TaskProvider.PERMISSIONS_JTX) + val permissionRule = GrantPermissionOrSkipRule(TaskProvider.PERMISSIONS_JTX.toSet()) @BeforeClass @JvmStatic diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/GrantPermissionOrSkipRule.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/GrantPermissionOrSkipRule.kt new file mode 100644 index 00000000..007ddd45 --- /dev/null +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/GrantPermissionOrSkipRule.kt @@ -0,0 +1,36 @@ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools + +import androidx.test.rule.GrantPermissionRule +import org.junit.Assume +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Requests the given permissions for testing. If the permissions are not available/granted, + * the tests are skipped. + * + * @param permissions requested permissions + */ +class GrantPermissionOrSkipRule(permissions: Set): TestRule { + + val grantRule: TestRule = GrantPermissionRule.grant(*permissions.toTypedArray()) + + override fun apply(base: Statement, description: Description) = + object: Statement() { + override fun evaluate() { + val innerStatement = grantRule.apply(base, description) + try { + innerStatement.evaluate() + } catch (e: SecurityException) { + Assume.assumeNoException(e) + } + } + } +} \ No newline at end of file From 71e043bc2bb22ab17f1bb5b257606f5169657ade Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Mon, 9 Jun 2025 12:18:49 +0200 Subject: [PATCH 5/7] Remove settings.gradle and rename test-dev.yml to tests.yml --- .github/workflows/{test-dev.yml => tests.yml} | 2 +- settings.gradle | 23 ------------------- settings.gradle.kts | 2 +- 3 files changed, 2 insertions(+), 25 deletions(-) rename .github/workflows/{test-dev.yml => tests.yml} (99%) delete mode 100644 settings.gradle diff --git a/.github/workflows/test-dev.yml b/.github/workflows/tests.yml similarity index 99% rename from .github/workflows/test-dev.yml rename to .github/workflows/tests.yml index fcfdc81c..15dc7adf 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: Development tests +name: Tests on: push: branches: diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index c692f1f0..00000000 --- a/settings.gradle +++ /dev/null @@ -1,23 +0,0 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ - -pluginManagement { - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() - } -} - -rootProject.name = "root" - -include ':lib' -project(':lib').name = 'ical4android' diff --git a/settings.gradle.kts b/settings.gradle.kts index cea42078..a0e20df4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,4 +16,4 @@ dependencyResolutionManagement { rootProject.name = "root" include(":lib") -project(":lib").name = "vcard4android" +project(":lib").name = "synctools" \ No newline at end of file From bc14ddcdc3d8a38cfd359dc2566c0e5087e4bd8c Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Mon, 9 Jun 2025 12:29:11 +0200 Subject: [PATCH 6/7] Refactor test setup and teardown for JtxICalObjectTest and JtxCollectionTest --- .../bitfire/ical4android/JtxCollectionTest.kt | 44 +++++++------------ .../bitfire/ical4android/JtxICalObjectTest.kt | 41 +++++------------ 2 files changed, 25 insertions(+), 60 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt index f114ca63..930ea5ff 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt @@ -20,41 +20,18 @@ import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertNull import junit.framework.TestCase.assertTrue import org.junit.After -import org.junit.AfterClass import org.junit.Assume -import org.junit.BeforeClass -import org.junit.ClassRule +import org.junit.Before +import org.junit.Rule import org.junit.Test class JtxCollectionTest { - companion object { + @get:Rule + val permissionRule = GrantPermissionOrSkipRule(TaskProvider.PERMISSIONS_JTX.toSet()) - val context = InstrumentationRegistry.getInstrumentation().targetContext - val contentResolver = context.contentResolver - - private lateinit var client: ContentProviderClient - - @JvmField - @ClassRule - val permissionRule = GrantPermissionOrSkipRule(TaskProvider.PERMISSIONS_JTX.toSet()) - - @BeforeClass - @JvmStatic - fun openProvider() { - val clientOrNull = contentResolver.acquireContentProviderClient(JtxContract.AUTHORITY) - Assume.assumeNotNull(clientOrNull) - - client = clientOrNull!! - } - - @AfterClass - @JvmStatic - fun closeProvider() { - client.closeCompat() - } - - } + val context = InstrumentationRegistry.getInstrumentation().targetContext + private lateinit var client: ContentProviderClient private val testAccount = Account("TEST", JtxContract.JtxCollection.TEST_ACCOUNT_TYPE) @@ -70,8 +47,17 @@ class JtxCollectionTest { put(JtxContract.JtxCollection.SYNC_VERSION, syncversion) } + @Before + fun setUp() { + val clientOrNull = context.contentResolver.acquireContentProviderClient(JtxContract.AUTHORITY) + Assume.assumeNotNull(clientOrNull) + client = clientOrNull!! + } + @After fun tearDown() { + client.closeCompat() + var collections = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null) collections.forEach { collection -> collection.delete() diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt index d140feae..09c2626e 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt @@ -8,9 +8,7 @@ package at.bitfire.ical4android import android.accounts.Account import android.content.ContentProviderClient -import android.content.ContentResolver import android.content.ContentValues -import android.content.Context import android.database.DatabaseUtils import android.os.ParcelFileDescriptor import androidx.core.content.pm.PackageInfoCompat @@ -29,45 +27,21 @@ import junit.framework.TestCase.assertTrue import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.Property import org.junit.After -import org.junit.AfterClass import org.junit.Assert import org.junit.Assume import org.junit.Before -import org.junit.BeforeClass -import org.junit.ClassRule +import org.junit.Rule import org.junit.Test import java.io.ByteArrayOutputStream import java.io.InputStreamReader class JtxICalObjectTest { - companion object { + @get:Rule + val permissionRule = GrantPermissionOrSkipRule(TaskProvider.PERMISSIONS_JTX.toSet()) - private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext - private val contentResolver: ContentResolver = context.contentResolver - - private lateinit var client: ContentProviderClient - - @JvmField - @ClassRule - val permissionRule = GrantPermissionOrSkipRule(TaskProvider.PERMISSIONS_JTX.toSet()) - - @BeforeClass - @JvmStatic - fun openProvider() { - val clientOrNull = contentResolver.acquireContentProviderClient(JtxContract.AUTHORITY) - Assume.assumeNotNull(clientOrNull) - - client = clientOrNull!! - } - - @AfterClass - @JvmStatic - fun closeProvider() { - client.closeCompat() - } - - } + val context = InstrumentationRegistry.getInstrumentation().targetContext + private lateinit var client: ContentProviderClient private val testAccount = Account("TEST", JtxContract.JtxCollection.TEST_ACCOUNT_TYPE) private var collection: JtxCollection? = null @@ -87,6 +61,10 @@ class JtxICalObjectTest { @Before fun setUp() { + val clientOrNull = context.contentResolver.acquireContentProviderClient(JtxContract.AUTHORITY) + Assume.assumeNotNull(clientOrNull) + client = clientOrNull!! + val collectionUri = JtxCollection.create(testAccount, client, cvCollection) assertNotNull(collectionUri) collection = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null)[0] @@ -138,6 +116,7 @@ class JtxICalObjectTest { @After fun tearDown() { + client.closeCompat() collection?.delete() val collections = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null) assertEquals(0, collections.size) From 7375df96b08303f8a174084391ea454ea24433a1 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Mon, 9 Jun 2025 12:33:43 +0200 Subject: [PATCH 7/7] Ensure tests are running in separate steps --- .github/workflows/tests.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 15dc7adf..551a8b14 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -25,7 +25,6 @@ jobs: - uses: gradle/actions/setup-gradle@v4 # creates build cache when on main branch with: cache-encryption-key: ${{ secrets.gradle_encryption_key }} - gradle-home-cache-cleanup: true # clean up unused files dependency-graph: generate-and-submit # submit Github Dependency Graph info - run: ./gradlew --build-cache --configuration-cache compileDebugSources @@ -46,8 +45,10 @@ jobs: cache-encryption-key: ${{ secrets.gradle_encryption_key }} cache-read-only: true - - name: Run lint and unit tests - run: ./gradlew --build-cache --configuration-cache lintDebug testDebugUnitTest + - name: Run lint + run: ./gradlew --build-cache --configuration-cache lintDebug + - name: Run unit tests + run: ./gradlew --build-cache --configuration-cache testDebugUnitTest test_on_emulator: needs: compile