Loading .github/workflows/release.yml +1 −1 Original line number Diff line number Diff line Loading @@ -35,5 +35,5 @@ jobs: uses: softprops/action-gh-release@v0.1.14 with: prerelease: ${{ contains(github.ref_name, '-alpha') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-rc') }} files: app/build/outputs/apk/standard/release/*.apk files: app/build/outputs/apk/ose/release/*.apk fail_on_unmatched_files: true app/build.gradle +2 −2 Original line number Diff line number Diff line Loading @@ -59,7 +59,7 @@ android { flavorDimensions "distribution" productFlavors { standard { ose { versionNameSuffix "-ose" } } Loading Loading @@ -149,7 +149,7 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:${versions.okhttp}" implementation "com.squareup.okhttp3:okhttp-brotli:${versions.okhttp}" implementation "com.squareup.okhttp3:logging-interceptor:${versions.okhttp}" implementation 'commons-io:commons-io:2.11.0' implementation 'commons-io:commons-io:2.8.0' // don't update until API level 26 (Android 8) is the minimum API (DAVx5#130) //noinspection GradleDependency - dnsjava 3+ needs Java 8/Android 7 implementation 'dnsjava:dnsjava:2.1.9' //noinspection GradleDependency Loading app/src/androidTest/java/at/bitfire/davdroid/InitCalendarProviderRule.kt +58 −37 Original line number Diff line number Diff line Loading @@ -4,11 +4,15 @@ package at.bitfire.davdroid import android.Manifest import android.accounts.Account import android.content.ContentUris import android.content.ContentValues import android.os.Build import android.provider.CalendarContract import androidx.annotation.RequiresApi import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.LocalCalendar import at.bitfire.davdroid.resource.LocalEvent Loading @@ -16,29 +20,55 @@ import at.bitfire.ical4android.AndroidCalendar import at.bitfire.ical4android.Event import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.RRule import org.junit.rules.RuleChain import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement /** * JUnit ClassRule which initializes the AOSP CalendarProvider * Needed for some "flaky" tests which would otherwise only succeed on second run * JUnit ClassRule which initializes the AOSP CalendarProvider. * Needed for some "flaky" tests which would otherwise only succeed on second run. * * Currently tested on development machine (Ryzen) with Android 12 images (with/without Google Play). * Calendar provider behaves quite randomly, so it may or may not work. If you (the reader * if this comment) can find out on how to initialize the calendar provider so that the * tests are reliably run after `adb shell pm clear com.android.providers.calendar`, * please let us know! * * If you run tests manually, just make sure to ignore the first run after the calendar * provider has been accessed the first time. */ class InitCalendarProviderRule : TestRule { class InitCalendarProviderRule private constructor(): TestRule { companion object { private val account = Account("LocalCalendarTest", CalendarContract.ACCOUNT_TYPE_LOCAL) private val context = InstrumentationRegistry.getInstrumentation().targetContext private val provider = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!! private val uri = AndroidCalendar.create(account, provider, ContentValues()) private val calendar = AndroidCalendar.findByID(account, provider, LocalCalendar.Factory, ContentUris.parseId(uri)) fun getInstance() = RuleChain .outerRule(GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR)) .around(InitCalendarProviderRule()) } override fun apply(base: Statement, description: Description): Statement { Logger.log.info("Before test: ${description.displayName}") Logger.log.info("Initializing calendar provider before running ${description.displayName}") return InitCalendarProviderStatement(base) } Logger.log.info("Initializing CalendarProvider (InitCalendarProviderRule)") class InitCalendarProviderStatement(val base: Statement): Statement() { override fun evaluate() { if (Build.VERSION.SDK_INT < 31) Logger.log.warning("Calendar provider initialization may or may not work. See InitCalendarProviderRule") initCalendarProvider() base.evaluate() } private fun initCalendarProvider() { val account = Account("LocalCalendarTest", CalendarContract.ACCOUNT_TYPE_LOCAL) val context = InstrumentationRegistry.getInstrumentation().targetContext val provider = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!! val uri = AndroidCalendar.create(account, provider, ContentValues()) val calendar = AndroidCalendar.findByID(account, provider, LocalCalendar.Factory, ContentUris.parseId(uri)) try { // single event init val normalEvent = Event().apply { dtStart = DtStart("20220120T010203Z") Loading @@ -57,19 +87,10 @@ class InitCalendarProviderRule : TestRule { val localRecurringEvent = LocalEvent(calendar, recurringEvent, null, null, null, 0) localRecurringEvent.add() LocalEvent.numInstances(provider, account, localRecurringEvent.id!!) // Run test Logger.log.info("Evaluating test..") return try { object : Statement() { @Throws(Throwable::class) override fun evaluate() { base.evaluate() } } } finally { Logger.log.info("After test: $description") calendar.delete() } } } } No newline at end of file app/src/androidTest/java/at/bitfire/davdroid/resource/LocalCalendarTest.kt +2 −6 Original line number Diff line number Diff line Loading @@ -31,12 +31,8 @@ class LocalCalendarTest { companion object { @JvmField @ClassRule(order = 0) val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR)!! @JvmField @ClassRule(order = 1) val initCalendarProviderRule: TestRule = InitCalendarProviderRule() @ClassRule val initCalendarProviderRule: TestRule = InitCalendarProviderRule.getInstance() private lateinit var provider: ContentProviderClient Loading app/src/androidTest/java/at/bitfire/davdroid/resource/LocalEventTest.kt +95 −13 Original line number Diff line number Diff line Loading @@ -6,7 +6,9 @@ package at.bitfire.davdroid.resource import android.Manifest import android.accounts.Account import android.content.* import android.content.ContentProviderClient import android.content.ContentUris import android.content.ContentValues import android.os.Build import android.provider.CalendarContract import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL Loading @@ -25,17 +27,15 @@ import net.fortuna.ical4j.model.property.* import org.junit.* import org.junit.Assert.* import org.junit.rules.TestRule import java.util.* class LocalEventTest { companion object { @JvmField @ClassRule(order = 0) val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR)!! @JvmField @ClassRule(order = 1) val initCalendarProviderRule: TestRule = InitCalendarProviderRule() @ClassRule val initCalendarProviderRule: TestRule = InitCalendarProviderRule.getInstance() private val account = Account("LocalCalendarTest", ACCOUNT_TYPE_LOCAL) Loading Loading @@ -107,7 +107,7 @@ class LocalEventTest { } @Test // Flaky, Needs rec event init of CalendarProvider // flaky, needs InitCalendarProviderRule fun testNumDirectInstances_Recurring_LateEnd() { val event = Event().apply { dtStart = DtStart("20220120T010203Z") Loading @@ -124,7 +124,7 @@ class LocalEventTest { } @Test // Flaky, Needs rec event init of CalendarProvider // flaky, needs InitCalendarProviderRule fun testNumDirectInstances_Recurring_ManyInstances() { val event = Event().apply { dtStart = DtStart("20220120T010203Z") Loading @@ -141,7 +141,7 @@ class LocalEventTest { } @Test // Flaky, Needs single event init of CalendarProvider // flaky, needs InitCalendarProviderRule fun testNumDirectInstances_RecurringWithExdate() { val event = Event().apply { dtStart = DtStart(Date("20220120T010203Z")) Loading Loading @@ -180,7 +180,7 @@ class LocalEventTest { @Test // Flaky, Needs single or rec event init of CalendarProvider // flaky, needs InitCalendarProviderRule fun testNumInstances_SingleInstance() { val event = Event().apply { dtStart = DtStart("20220120T010203Z") Loading Loading @@ -219,7 +219,7 @@ class LocalEventTest { } @Test // Flaky, Needs rec event init of CalendarProvider // flaky, needs InitCalendarProviderRule fun testNumInstances_Recurring_LateEnd() { val event = Event().apply { dtStart = DtStart("20220120T010203Z") Loading @@ -236,7 +236,7 @@ class LocalEventTest { } @Test // Flaky, Needs rec event init of CalendarProvider // flaky, needs InitCalendarProviderRule fun testNumInstances_Recurring_ManyInstances() { val event = Event().apply { dtStart = DtStart("20220120T010203Z") Loading Loading @@ -306,6 +306,89 @@ class LocalEventTest { } @Test fun testPrepareForUpload_NoUid() { // create event val event = Event().apply { dtStart = DtStart("20220120T010203Z") summary = "Event without uid" } val localEvent = LocalEvent(calendar, event, null, null, null, 0) localEvent.add() // save it to calendar storage // prepare for upload - this should generate a new random uuid, returned as filename val fileNameWithSuffix = localEvent.prepareForUpload() val fileName = fileNameWithSuffix.removeSuffix(".ics") // throws an exception if fileName is not an UUID UUID.fromString(fileName) // UID in calendar storage should be the same as file name provider.query( ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account), arrayOf(Events.UID_2445), null, null, null )!!.use { cursor -> cursor.moveToFirst() assertEquals(fileName, cursor.getString(0)) } } @Test fun testPrepareForUpload_NormalUid() { // create event val event = Event().apply { dtStart = DtStart("20220120T010203Z") summary = "Event with normal uid" uid = "some-event@hostname.tld" // old UID format, UUID would be new format } val localEvent = LocalEvent(calendar, event, null, null, null, 0) localEvent.add() // save it to calendar storage // prepare for upload - this should use the UID for the file name val fileNameWithSuffix = localEvent.prepareForUpload() val fileName = fileNameWithSuffix.removeSuffix(".ics") assertEquals(event.uid, fileName) // UID in calendar storage should still be set, too provider.query( ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account), arrayOf(Events.UID_2445), null, null, null )!!.use { cursor -> cursor.moveToFirst() assertEquals(fileName, cursor.getString(0)) } } @Test fun testPrepareForUpload_UidHasDangerousChars() { // create event val event = Event().apply { dtStart = DtStart("20220120T010203Z") summary = "Event with funny uid" uid = "https://www.example.com/events/asdfewfe-cxyb-ewrws-sadfrwerxyvser-asdfxye-" } val localEvent = LocalEvent(calendar, event, null, null, null, 0) localEvent.add() // save it to calendar storage // prepare for upload - this should generate a new random uuid, returned as filename val fileNameWithSuffix = localEvent.prepareForUpload() val fileName = fileNameWithSuffix.removeSuffix(".ics") // throws an exception if fileName is not an UUID UUID.fromString(fileName) // UID in calendar storage shouldn't have been changed provider.query( ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account), arrayOf(Events.UID_2445), null, null, null )!!.use { cursor -> cursor.moveToFirst() assertEquals(event.uid, cursor.getString(0)) } } @Test fun testDeleteDirtyEventsWithoutInstances_NoInstances_Exdate() { // TODO Loading Loading @@ -360,7 +443,6 @@ class LocalEventTest { } @Test // Flaky, Needs single event init OR rec event init of CalendarProvider fun testDeleteDirtyEventsWithoutInstances_Recurring_Instances() { val event = Event().apply { dtStart = DtStart("20220120T010203Z") Loading Loading
.github/workflows/release.yml +1 −1 Original line number Diff line number Diff line Loading @@ -35,5 +35,5 @@ jobs: uses: softprops/action-gh-release@v0.1.14 with: prerelease: ${{ contains(github.ref_name, '-alpha') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-rc') }} files: app/build/outputs/apk/standard/release/*.apk files: app/build/outputs/apk/ose/release/*.apk fail_on_unmatched_files: true
app/build.gradle +2 −2 Original line number Diff line number Diff line Loading @@ -59,7 +59,7 @@ android { flavorDimensions "distribution" productFlavors { standard { ose { versionNameSuffix "-ose" } } Loading Loading @@ -149,7 +149,7 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:${versions.okhttp}" implementation "com.squareup.okhttp3:okhttp-brotli:${versions.okhttp}" implementation "com.squareup.okhttp3:logging-interceptor:${versions.okhttp}" implementation 'commons-io:commons-io:2.11.0' implementation 'commons-io:commons-io:2.8.0' // don't update until API level 26 (Android 8) is the minimum API (DAVx5#130) //noinspection GradleDependency - dnsjava 3+ needs Java 8/Android 7 implementation 'dnsjava:dnsjava:2.1.9' //noinspection GradleDependency Loading
app/src/androidTest/java/at/bitfire/davdroid/InitCalendarProviderRule.kt +58 −37 Original line number Diff line number Diff line Loading @@ -4,11 +4,15 @@ package at.bitfire.davdroid import android.Manifest import android.accounts.Account import android.content.ContentUris import android.content.ContentValues import android.os.Build import android.provider.CalendarContract import androidx.annotation.RequiresApi import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.LocalCalendar import at.bitfire.davdroid.resource.LocalEvent Loading @@ -16,29 +20,55 @@ import at.bitfire.ical4android.AndroidCalendar import at.bitfire.ical4android.Event import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.RRule import org.junit.rules.RuleChain import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement /** * JUnit ClassRule which initializes the AOSP CalendarProvider * Needed for some "flaky" tests which would otherwise only succeed on second run * JUnit ClassRule which initializes the AOSP CalendarProvider. * Needed for some "flaky" tests which would otherwise only succeed on second run. * * Currently tested on development machine (Ryzen) with Android 12 images (with/without Google Play). * Calendar provider behaves quite randomly, so it may or may not work. If you (the reader * if this comment) can find out on how to initialize the calendar provider so that the * tests are reliably run after `adb shell pm clear com.android.providers.calendar`, * please let us know! * * If you run tests manually, just make sure to ignore the first run after the calendar * provider has been accessed the first time. */ class InitCalendarProviderRule : TestRule { class InitCalendarProviderRule private constructor(): TestRule { companion object { private val account = Account("LocalCalendarTest", CalendarContract.ACCOUNT_TYPE_LOCAL) private val context = InstrumentationRegistry.getInstrumentation().targetContext private val provider = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!! private val uri = AndroidCalendar.create(account, provider, ContentValues()) private val calendar = AndroidCalendar.findByID(account, provider, LocalCalendar.Factory, ContentUris.parseId(uri)) fun getInstance() = RuleChain .outerRule(GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR)) .around(InitCalendarProviderRule()) } override fun apply(base: Statement, description: Description): Statement { Logger.log.info("Before test: ${description.displayName}") Logger.log.info("Initializing calendar provider before running ${description.displayName}") return InitCalendarProviderStatement(base) } Logger.log.info("Initializing CalendarProvider (InitCalendarProviderRule)") class InitCalendarProviderStatement(val base: Statement): Statement() { override fun evaluate() { if (Build.VERSION.SDK_INT < 31) Logger.log.warning("Calendar provider initialization may or may not work. See InitCalendarProviderRule") initCalendarProvider() base.evaluate() } private fun initCalendarProvider() { val account = Account("LocalCalendarTest", CalendarContract.ACCOUNT_TYPE_LOCAL) val context = InstrumentationRegistry.getInstrumentation().targetContext val provider = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!! val uri = AndroidCalendar.create(account, provider, ContentValues()) val calendar = AndroidCalendar.findByID(account, provider, LocalCalendar.Factory, ContentUris.parseId(uri)) try { // single event init val normalEvent = Event().apply { dtStart = DtStart("20220120T010203Z") Loading @@ -57,19 +87,10 @@ class InitCalendarProviderRule : TestRule { val localRecurringEvent = LocalEvent(calendar, recurringEvent, null, null, null, 0) localRecurringEvent.add() LocalEvent.numInstances(provider, account, localRecurringEvent.id!!) // Run test Logger.log.info("Evaluating test..") return try { object : Statement() { @Throws(Throwable::class) override fun evaluate() { base.evaluate() } } } finally { Logger.log.info("After test: $description") calendar.delete() } } } } No newline at end of file
app/src/androidTest/java/at/bitfire/davdroid/resource/LocalCalendarTest.kt +2 −6 Original line number Diff line number Diff line Loading @@ -31,12 +31,8 @@ class LocalCalendarTest { companion object { @JvmField @ClassRule(order = 0) val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR)!! @JvmField @ClassRule(order = 1) val initCalendarProviderRule: TestRule = InitCalendarProviderRule() @ClassRule val initCalendarProviderRule: TestRule = InitCalendarProviderRule.getInstance() private lateinit var provider: ContentProviderClient Loading
app/src/androidTest/java/at/bitfire/davdroid/resource/LocalEventTest.kt +95 −13 Original line number Diff line number Diff line Loading @@ -6,7 +6,9 @@ package at.bitfire.davdroid.resource import android.Manifest import android.accounts.Account import android.content.* import android.content.ContentProviderClient import android.content.ContentUris import android.content.ContentValues import android.os.Build import android.provider.CalendarContract import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL Loading @@ -25,17 +27,15 @@ import net.fortuna.ical4j.model.property.* import org.junit.* import org.junit.Assert.* import org.junit.rules.TestRule import java.util.* class LocalEventTest { companion object { @JvmField @ClassRule(order = 0) val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR)!! @JvmField @ClassRule(order = 1) val initCalendarProviderRule: TestRule = InitCalendarProviderRule() @ClassRule val initCalendarProviderRule: TestRule = InitCalendarProviderRule.getInstance() private val account = Account("LocalCalendarTest", ACCOUNT_TYPE_LOCAL) Loading Loading @@ -107,7 +107,7 @@ class LocalEventTest { } @Test // Flaky, Needs rec event init of CalendarProvider // flaky, needs InitCalendarProviderRule fun testNumDirectInstances_Recurring_LateEnd() { val event = Event().apply { dtStart = DtStart("20220120T010203Z") Loading @@ -124,7 +124,7 @@ class LocalEventTest { } @Test // Flaky, Needs rec event init of CalendarProvider // flaky, needs InitCalendarProviderRule fun testNumDirectInstances_Recurring_ManyInstances() { val event = Event().apply { dtStart = DtStart("20220120T010203Z") Loading @@ -141,7 +141,7 @@ class LocalEventTest { } @Test // Flaky, Needs single event init of CalendarProvider // flaky, needs InitCalendarProviderRule fun testNumDirectInstances_RecurringWithExdate() { val event = Event().apply { dtStart = DtStart(Date("20220120T010203Z")) Loading Loading @@ -180,7 +180,7 @@ class LocalEventTest { @Test // Flaky, Needs single or rec event init of CalendarProvider // flaky, needs InitCalendarProviderRule fun testNumInstances_SingleInstance() { val event = Event().apply { dtStart = DtStart("20220120T010203Z") Loading Loading @@ -219,7 +219,7 @@ class LocalEventTest { } @Test // Flaky, Needs rec event init of CalendarProvider // flaky, needs InitCalendarProviderRule fun testNumInstances_Recurring_LateEnd() { val event = Event().apply { dtStart = DtStart("20220120T010203Z") Loading @@ -236,7 +236,7 @@ class LocalEventTest { } @Test // Flaky, Needs rec event init of CalendarProvider // flaky, needs InitCalendarProviderRule fun testNumInstances_Recurring_ManyInstances() { val event = Event().apply { dtStart = DtStart("20220120T010203Z") Loading Loading @@ -306,6 +306,89 @@ class LocalEventTest { } @Test fun testPrepareForUpload_NoUid() { // create event val event = Event().apply { dtStart = DtStart("20220120T010203Z") summary = "Event without uid" } val localEvent = LocalEvent(calendar, event, null, null, null, 0) localEvent.add() // save it to calendar storage // prepare for upload - this should generate a new random uuid, returned as filename val fileNameWithSuffix = localEvent.prepareForUpload() val fileName = fileNameWithSuffix.removeSuffix(".ics") // throws an exception if fileName is not an UUID UUID.fromString(fileName) // UID in calendar storage should be the same as file name provider.query( ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account), arrayOf(Events.UID_2445), null, null, null )!!.use { cursor -> cursor.moveToFirst() assertEquals(fileName, cursor.getString(0)) } } @Test fun testPrepareForUpload_NormalUid() { // create event val event = Event().apply { dtStart = DtStart("20220120T010203Z") summary = "Event with normal uid" uid = "some-event@hostname.tld" // old UID format, UUID would be new format } val localEvent = LocalEvent(calendar, event, null, null, null, 0) localEvent.add() // save it to calendar storage // prepare for upload - this should use the UID for the file name val fileNameWithSuffix = localEvent.prepareForUpload() val fileName = fileNameWithSuffix.removeSuffix(".ics") assertEquals(event.uid, fileName) // UID in calendar storage should still be set, too provider.query( ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account), arrayOf(Events.UID_2445), null, null, null )!!.use { cursor -> cursor.moveToFirst() assertEquals(fileName, cursor.getString(0)) } } @Test fun testPrepareForUpload_UidHasDangerousChars() { // create event val event = Event().apply { dtStart = DtStart("20220120T010203Z") summary = "Event with funny uid" uid = "https://www.example.com/events/asdfewfe-cxyb-ewrws-sadfrwerxyvser-asdfxye-" } val localEvent = LocalEvent(calendar, event, null, null, null, 0) localEvent.add() // save it to calendar storage // prepare for upload - this should generate a new random uuid, returned as filename val fileNameWithSuffix = localEvent.prepareForUpload() val fileName = fileNameWithSuffix.removeSuffix(".ics") // throws an exception if fileName is not an UUID UUID.fromString(fileName) // UID in calendar storage shouldn't have been changed provider.query( ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account), arrayOf(Events.UID_2445), null, null, null )!!.use { cursor -> cursor.moveToFirst() assertEquals(event.uid, cursor.getString(0)) } } @Test fun testDeleteDirtyEventsWithoutInstances_NoInstances_Exdate() { // TODO Loading Loading @@ -360,7 +443,6 @@ class LocalEventTest { } @Test // Flaky, Needs single event init OR rec event init of CalendarProvider fun testDeleteDirtyEventsWithoutInstances_Recurring_Instances() { val event = Event().apply { dtStart = DtStart("20220120T010203Z") Loading