Skip to content

Commit a0eb575

Browse files
authored
Handle SecurityException when tasks apps are not available (#8)
* Ensure tests are running with proper permissions and handle SecurityException * Ensure tests run on push to main-ose and pull requests * Compile/cache only on main branch * Replace permission rule with custom rule to skip tests on missing permissions * Remove settings.gradle and rename test-dev.yml to tests.yml * Refactor test setup and teardown for JtxICalObjectTest and JtxCollectionTest * Ensure tests are running in separate steps
1 parent d884531 commit a0eb575

File tree

7 files changed

+96
-99
lines changed

7 files changed

+96
-99
lines changed
Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
1-
name: Development tests
2-
on: push
1+
name: Tests
2+
on:
3+
push:
4+
branches:
5+
- 'main-ose'
6+
pull_request:
7+
8+
concurrency:
9+
group: test-dev-${{ github.ref }}
10+
cancel-in-progress: true
11+
312
jobs:
413
compile:
514
name: Compile and cache
15+
if: ${{ github.ref == 'refs/heads/main' }}
616
runs-on: ubuntu-latest
717
steps:
818
- uses: actions/checkout@v4
@@ -15,13 +25,13 @@ jobs:
1525
- uses: gradle/actions/setup-gradle@v4 # creates build cache when on main branch
1626
with:
1727
cache-encryption-key: ${{ secrets.gradle_encryption_key }}
18-
gradle-home-cache-cleanup: true # clean up unused files
1928
dependency-graph: generate-and-submit # submit Github Dependency Graph info
2029

2130
- run: ./gradlew --build-cache --configuration-cache compileDebugSources
2231

2332
test:
2433
needs: compile
34+
if: ${{ !cancelled() }} # even if compile didn't run (because not on main branch)
2535
name: Unit tests
2636
runs-on: ubuntu-latest
2737
steps:
@@ -35,11 +45,14 @@ jobs:
3545
cache-encryption-key: ${{ secrets.gradle_encryption_key }}
3646
cache-read-only: true
3747

38-
- name: Run lint and unit tests
39-
run: ./gradlew --build-cache --configuration-cache lintDebug testDebugUnitTest
48+
- name: Run lint
49+
run: ./gradlew --build-cache --configuration-cache lintDebug
50+
- name: Run unit tests
51+
run: ./gradlew --build-cache --configuration-cache testDebugUnitTest
4052

4153
test_on_emulator:
4254
needs: compile
55+
if: ${{ !cancelled() }} # even if compile didn't run (because not on main branch)
4356
name: Instrumented tests
4457
runs-on: ubuntu-latest
4558
steps:
@@ -59,9 +72,6 @@ jobs:
5972
sudo udevadm control --reload-rules
6073
sudo udevadm trigger --name-match=kvm
6174
62-
- name: Check
63-
run: ./gradlew --no-daemon check
64-
6575
- name: Cache AVD
6676
uses: actions/cache@v4
6777
with:

lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsStyleProvidersTaskTest.kt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
package at.bitfire.ical4android
88

99
import androidx.test.platform.app.InstrumentationRegistry
10-
import androidx.test.rule.GrantPermissionRule
10+
import at.bitfire.synctools.GrantPermissionOrSkipRule
1111
import org.junit.After
12-
import org.junit.Assume
12+
import org.junit.Assert.assertNotNull
1313
import org.junit.Before
1414
import org.junit.Rule
1515
import org.junit.runner.RunWith
@@ -28,17 +28,16 @@ abstract class DmfsStyleProvidersTaskTest(
2828
fun taskProviders() = listOf(TaskProvider.ProviderName.OpenTasks,TaskProvider.ProviderName.TasksOrg)
2929
}
3030

31-
@JvmField
32-
@Rule
33-
val permissionRule = GrantPermissionRule.grant(*providerName.permissions)
31+
@get:Rule
32+
val permissionRule = GrantPermissionOrSkipRule(providerName.permissions.toSet())
3433

3534
var providerOrNull: TaskProvider? = null
3635
lateinit var provider: TaskProvider
3736

3837
@Before
3938
open fun prepare() {
4039
providerOrNull = TaskProvider.acquire(InstrumentationRegistry.getInstrumentation().context, providerName)
41-
Assume.assumeNotNull(providerOrNull) // will halt here if providerOrNull is null
40+
assertNotNull("$providerName is not installed", providerOrNull != null)
4241

4342
provider = providerOrNull!!
4443
Logger.getLogger(javaClass.name).fine("Using task provider: $provider")

lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,51 +10,28 @@ import android.accounts.Account
1010
import android.content.ContentProviderClient
1111
import android.content.ContentValues
1212
import androidx.test.platform.app.InstrumentationRegistry
13-
import androidx.test.rule.GrantPermissionRule
1413
import at.bitfire.ical4android.impl.TestJtxCollection
1514
import at.bitfire.ical4android.util.MiscUtils.closeCompat
15+
import at.bitfire.synctools.GrantPermissionOrSkipRule
1616
import at.techbee.jtx.JtxContract
1717
import at.techbee.jtx.JtxContract.asSyncAdapter
1818
import junit.framework.TestCase.assertEquals
1919
import junit.framework.TestCase.assertNotNull
2020
import junit.framework.TestCase.assertNull
2121
import junit.framework.TestCase.assertTrue
2222
import org.junit.After
23-
import org.junit.AfterClass
2423
import org.junit.Assume
25-
import org.junit.BeforeClass
26-
import org.junit.ClassRule
24+
import org.junit.Before
25+
import org.junit.Rule
2726
import org.junit.Test
2827

2928
class JtxCollectionTest {
3029

31-
companion object {
30+
@get:Rule
31+
val permissionRule = GrantPermissionOrSkipRule(TaskProvider.PERMISSIONS_JTX.toSet())
3232

33-
val context = InstrumentationRegistry.getInstrumentation().targetContext
34-
val contentResolver = context.contentResolver
35-
36-
private lateinit var client: ContentProviderClient
37-
38-
@JvmField
39-
@ClassRule
40-
val permissionRule = GrantPermissionRule.grant(*TaskProvider.PERMISSIONS_JTX)
41-
42-
@BeforeClass
43-
@JvmStatic
44-
fun openProvider() {
45-
val clientOrNull = contentResolver.acquireContentProviderClient(JtxContract.AUTHORITY)
46-
Assume.assumeNotNull(clientOrNull)
47-
48-
client = clientOrNull!!
49-
}
50-
51-
@AfterClass
52-
@JvmStatic
53-
fun closeProvider() {
54-
client.closeCompat()
55-
}
56-
57-
}
33+
val context = InstrumentationRegistry.getInstrumentation().targetContext
34+
private lateinit var client: ContentProviderClient
5835

5936
private val testAccount = Account("TEST", JtxContract.JtxCollection.TEST_ACCOUNT_TYPE)
6037

@@ -70,8 +47,17 @@ class JtxCollectionTest {
7047
put(JtxContract.JtxCollection.SYNC_VERSION, syncversion)
7148
}
7249

50+
@Before
51+
fun setUp() {
52+
val clientOrNull = context.contentResolver.acquireContentProviderClient(JtxContract.AUTHORITY)
53+
Assume.assumeNotNull(clientOrNull)
54+
client = clientOrNull!!
55+
}
56+
7357
@After
7458
fun tearDown() {
59+
client.closeCompat()
60+
7561
var collections = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null)
7662
collections.forEach { collection ->
7763
collection.delete()

lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,56 +8,40 @@ package at.bitfire.ical4android
88

99
import android.accounts.Account
1010
import android.content.ContentProviderClient
11-
import android.content.ContentResolver
1211
import android.content.ContentValues
13-
import android.content.Context
1412
import android.database.DatabaseUtils
1513
import android.os.ParcelFileDescriptor
1614
import androidx.core.content.pm.PackageInfoCompat
1715
import androidx.test.platform.app.InstrumentationRegistry
18-
import androidx.test.rule.GrantPermissionRule
1916
import at.bitfire.ical4android.impl.TestJtxCollection
2017
import at.bitfire.ical4android.util.MiscUtils.closeCompat
18+
import at.bitfire.synctools.GrantPermissionOrSkipRule
2119
import at.techbee.jtx.JtxContract
2220
import at.techbee.jtx.JtxContract.JtxICalObject
2321
import at.techbee.jtx.JtxContract.JtxICalObject.Component
2422
import at.techbee.jtx.JtxContract.asSyncAdapter
25-
import junit.framework.TestCase.*
23+
import junit.framework.TestCase.assertEquals
24+
import junit.framework.TestCase.assertNotNull
25+
import junit.framework.TestCase.assertNull
26+
import junit.framework.TestCase.assertTrue
2627
import net.fortuna.ical4j.model.Calendar
2728
import net.fortuna.ical4j.model.Property
28-
import org.junit.*
29+
import org.junit.After
30+
import org.junit.Assert
31+
import org.junit.Assume
32+
import org.junit.Before
33+
import org.junit.Rule
34+
import org.junit.Test
2935
import java.io.ByteArrayOutputStream
3036
import java.io.InputStreamReader
3137

3238
class JtxICalObjectTest {
3339

34-
companion object {
40+
@get:Rule
41+
val permissionRule = GrantPermissionOrSkipRule(TaskProvider.PERMISSIONS_JTX.toSet())
3542

36-
private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
37-
private val contentResolver: ContentResolver = context.contentResolver
38-
39-
private lateinit var client: ContentProviderClient
40-
41-
@JvmField
42-
@ClassRule
43-
val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(*TaskProvider.PERMISSIONS_JTX)
44-
45-
@BeforeClass
46-
@JvmStatic
47-
fun openProvider() {
48-
val clientOrNull = contentResolver.acquireContentProviderClient(JtxContract.AUTHORITY)
49-
Assume.assumeNotNull(clientOrNull)
50-
51-
client = clientOrNull!!
52-
}
53-
54-
@AfterClass
55-
@JvmStatic
56-
fun closeProvider() {
57-
client.closeCompat()
58-
}
59-
60-
}
43+
val context = InstrumentationRegistry.getInstrumentation().targetContext
44+
private lateinit var client: ContentProviderClient
6145

6246
private val testAccount = Account("TEST", JtxContract.JtxCollection.TEST_ACCOUNT_TYPE)
6347
private var collection: JtxCollection<at.bitfire.ical4android.JtxICalObject>? = null
@@ -77,6 +61,10 @@ class JtxICalObjectTest {
7761

7862
@Before
7963
fun setUp() {
64+
val clientOrNull = context.contentResolver.acquireContentProviderClient(JtxContract.AUTHORITY)
65+
Assume.assumeNotNull(clientOrNull)
66+
client = clientOrNull!!
67+
8068
val collectionUri = JtxCollection.create(testAccount, client, cvCollection)
8169
assertNotNull(collectionUri)
8270
collection = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null)[0]
@@ -128,6 +116,7 @@ class JtxICalObjectTest {
128116

129117
@After
130118
fun tearDown() {
119+
client.closeCompat()
131120
collection?.delete()
132121
val collections = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null)
133122
assertEquals(0, collections.size)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* This file is part of ical4android which is released under GPLv3.
3+
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
4+
* SPDX-License-Identifier: GPL-3.0-or-later
5+
*/
6+
7+
package at.bitfire.synctools
8+
9+
import androidx.test.rule.GrantPermissionRule
10+
import org.junit.Assume
11+
import org.junit.rules.TestRule
12+
import org.junit.runner.Description
13+
import org.junit.runners.model.Statement
14+
15+
/**
16+
* Requests the given permissions for testing. If the permissions are not available/granted,
17+
* the tests are skipped.
18+
*
19+
* @param permissions requested permissions
20+
*/
21+
class GrantPermissionOrSkipRule(permissions: Set<String>): TestRule {
22+
23+
val grantRule: TestRule = GrantPermissionRule.grant(*permissions.toTypedArray())
24+
25+
override fun apply(base: Statement, description: Description) =
26+
object: Statement() {
27+
override fun evaluate() {
28+
val innerStatement = grantRule.apply(base, description)
29+
try {
30+
innerStatement.evaluate()
31+
} catch (e: SecurityException) {
32+
Assume.assumeNoException(e)
33+
}
34+
}
35+
}
36+
}

settings.gradle

Lines changed: 0 additions & 23 deletions
This file was deleted.

settings.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ dependencyResolutionManagement {
1616

1717
rootProject.name = "root"
1818
include(":lib")
19-
project(":lib").name = "vcard4android"
19+
project(":lib").name = "synctools"

0 commit comments

Comments
 (0)