Skip to content

Commit 5fc6688

Browse files
authored
Extract SEQUENCE updating to a dedicated class (#134)
* Add SequenceUpdater class and tests - Introduce `SequenceUpdater` class to handle SEQUENCE updates for events and exceptions - Add methods `processDeletedExceptions` and `processDirtyExceptions` in `SequenceUpdater` - Update `AndroidCalendar` with `updateEventRow` method for updating specific event rows - Modify `AssertHelpers` to improve error messages in assertions * Move sequence updater logic to SequenceUpdater class - Remove increaseSequence function from AndroidEventProcessor - Add increaseSequence function to SequenceUpdater - Update tests accordingly * Move some methods and classes * Make sequence updater public, improve various KDoc * Minor fixes
1 parent 5f9802c commit 5fc6688

File tree

10 files changed

+594
-182
lines changed

10 files changed

+594
-182
lines changed

lib/src/androidTest/kotlin/at/bitfire/synctools/storage/calendar/AndroidCalendarTest.kt

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class AndroidCalendarTest {
4444
lateinit var calendar: AndroidCalendar
4545

4646
@Before
47-
fun prepare() {
47+
fun setUp() {
4848
val context = InstrumentationRegistry.getInstrumentation().targetContext
4949
client = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!
5050

@@ -321,6 +321,22 @@ class AndroidCalendarTest {
321321
assertEquals("New Title", calendar.getEvent(id)!!.entityValues.getAsString(Events.TITLE))
322322
}
323323

324+
@Test
325+
fun testUpdateEventRowBatch() {
326+
val id = calendar.addEvent(Entity(contentValuesOf(
327+
Events.CALENDAR_ID to calendar.id,
328+
Events.DTSTART to now,
329+
Events.DTEND to now + 3600000,
330+
Events.TITLE to "Some Event 1"
331+
)))
332+
333+
val batch = CalendarBatchOperation(calendar.client)
334+
calendar.updateEventRow(id, contentValuesOf(Events.TITLE to "New Title"), batch)
335+
batch.commit()
336+
337+
assertEquals("New Title", calendar.getEvent(id)!!.entityValues.getAsString(Events.TITLE))
338+
}
339+
324340
@Test
325341
fun testUpdateEvent_NoRebuild() {
326342
val entity = Entity(contentValuesOf(
@@ -559,4 +575,94 @@ class AndroidCalendarTest {
559575
assertEquals(5 + /* one extra outside the recurrence */ 1, calendar.numInstances(id))
560576
}
561577

578+
@Test
579+
fun testDeleteDirtyEventsWithoutInstances_NoInstances() {
580+
// create recurring event with only deleted/cancelled instances
581+
val now = System.currentTimeMillis()
582+
val recurringCalendar = AndroidRecurringCalendar(calendar)
583+
val id = recurringCalendar.addEventAndExceptions(EventAndExceptions(
584+
main = Entity(contentValuesOf(
585+
Events._SYNC_ID to "event-without-instances",
586+
Events.CALENDAR_ID to calendar.id,
587+
Events.ALL_DAY to 0,
588+
Events.DTSTART to now,
589+
Events.DURATION to "PT1H",
590+
Events.RRULE to "FREQ=DAILY;COUNT=3",
591+
Events.DIRTY to 1
592+
)),
593+
exceptions = listOf(
594+
Entity(contentValuesOf( // first instance: cancelled
595+
Events.CALENDAR_ID to calendar.id,
596+
Events.ORIGINAL_INSTANCE_TIME to now,
597+
Events.ORIGINAL_ALL_DAY to 0,
598+
Events.DTSTART to now,
599+
Events.DTEND to now + 3600000,
600+
Events.STATUS to Events.STATUS_CANCELED
601+
)),
602+
Entity(contentValuesOf( // second instance: cancelled
603+
Events.CALENDAR_ID to calendar.id,
604+
Events.ORIGINAL_INSTANCE_TIME to now + 86400000,
605+
Events.ORIGINAL_ALL_DAY to 0,
606+
Events.DTSTART to now + 86400000,
607+
Events.DTEND to now + 86400000 + 3600000,
608+
Events.STATUS to Events.STATUS_CANCELED
609+
)),
610+
Entity(contentValuesOf( // third and last instance: cancelled
611+
Events.CALENDAR_ID to calendar.id,
612+
Events.ORIGINAL_INSTANCE_TIME to now + 2*86400000,
613+
Events.ORIGINAL_ALL_DAY to 0,
614+
Events.DTSTART to now + 2*86400000,
615+
Events.DTEND to now + 2*86400000 + 3600000,
616+
Events.STATUS to Events.STATUS_CANCELED
617+
))
618+
)
619+
))
620+
assertEquals(0, calendar.numInstances(id))
621+
622+
// this method should mark the event as deleted
623+
calendar.deleteDirtyEventsWithoutInstances()
624+
625+
// verify that event is now marked as deleted
626+
val result = calendar.getEventRow(id)!!
627+
assertEquals(1, result.getAsInteger(Events.DELETED))
628+
}
629+
630+
@Test
631+
fun testDeleteDirtyEventsWithoutInstances_OneInstanceRemaining() {
632+
// create recurring event with only deleted/cancelled instances
633+
val syncId = "event-with-instances"
634+
val recurringCalendar = AndroidRecurringCalendar(calendar)
635+
val id = recurringCalendar.addEventAndExceptions(EventAndExceptions(
636+
main = Entity(contentValuesOf(
637+
Events.CALENDAR_ID to calendar.id,
638+
Events._SYNC_ID to syncId,
639+
Events.DTSTART to 1642640523000,
640+
Events.DURATION to "PT1H",
641+
Events.TITLE to "Event with 2 instances, one of them cancelled",
642+
Events.RRULE to "FREQ=DAILY;COUNT=2",
643+
Events.DIRTY to 1
644+
)),
645+
exceptions = listOf(
646+
Entity(contentValuesOf( // first instance: cancelled
647+
Events.CALENDAR_ID to calendar.id,
648+
Events.ORIGINAL_SYNC_ID to syncId,
649+
Events.ORIGINAL_INSTANCE_TIME to 1642640523000,
650+
Events.DTSTART to 1642640523000 + 86400000,
651+
Events.DTEND to 1642640523000 + 86400000 + 3600000,
652+
Events.STATUS to Events.STATUS_CANCELED
653+
))
654+
// however second instance is NOT cancelled
655+
)
656+
))
657+
assertEquals(1, calendar.numInstances(id))
658+
659+
// this method should mark the event as deleted
660+
calendar.deleteDirtyEventsWithoutInstances()
661+
662+
// verify that event is still marked as dirty, but not as deleted
663+
val result = calendar.getEventRow(id)!!
664+
assertEquals(1, result.getAsInteger(Events.DIRTY))
665+
assertEquals(0, result.getAsInteger(Events.DELETED))
666+
}
667+
562668
}

lib/src/androidTest/kotlin/at/bitfire/synctools/storage/calendar/AndroidRecurringCalendarTest.kt

Lines changed: 119 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@
66

77
package at.bitfire.synctools.storage.calendar
88

9-
import android.Manifest
109
import android.accounts.Account
1110
import android.content.ContentProviderClient
11+
import android.content.ContentValues
1212
import android.content.Entity
1313
import android.provider.CalendarContract
1414
import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL
1515
import android.provider.CalendarContract.Events
1616
import androidx.core.content.contentValuesOf
1717
import androidx.test.platform.app.InstrumentationRegistry
18-
import androidx.test.rule.GrantPermissionRule
1918
import at.bitfire.ical4android.impl.TestCalendar
2019
import at.bitfire.ical4android.util.MiscUtils.closeCompat
20+
import at.bitfire.synctools.test.InitCalendarProviderRule
2121
import at.bitfire.synctools.test.assertContentValuesEqual
2222
import at.bitfire.synctools.test.assertEventAndExceptionsEqual
2323
import at.bitfire.synctools.test.withId
@@ -37,15 +37,14 @@ import org.junit.Test
3737
class AndroidRecurringCalendarTest {
3838

3939
@get:Rule
40-
val mockkRule = MockKRule(this)
40+
val initCalendarProviderRule = InitCalendarProviderRule.initialize()
4141

4242
@get:Rule
43-
val permissonRule = GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR)
43+
val mockkRule = MockKRule(this)
4444

4545
private val testAccount = Account(javaClass.name, ACCOUNT_TYPE_LOCAL)
4646

4747
lateinit var client: ContentProviderClient
48-
lateinit var provider: AndroidCalendarProvider
4948
lateinit var calendar: AndroidCalendar
5049

5150
lateinit var recurringCalendar: AndroidRecurringCalendar
@@ -54,7 +53,7 @@ class AndroidRecurringCalendarTest {
5453
fun setUp() {
5554
val context = InstrumentationRegistry.getInstrumentation().targetContext
5655
client = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!
57-
provider = AndroidCalendarProvider(testAccount, client)
56+
5857
calendar = TestCalendar.findOrCreate(testAccount, client)
5958
recurringCalendar = spyk(AndroidRecurringCalendar(calendar))
6059
}
@@ -64,7 +63,9 @@ class AndroidRecurringCalendarTest {
6463
calendar.delete()
6564
client.closeCompat()
6665
}
67-
66+
67+
68+
// test CRUD
6869

6970
@Test
7071
fun testAddEventAndExceptions() {
@@ -253,6 +254,8 @@ class AndroidRecurringCalendarTest {
253254
assertNull(deletedEvent)
254255
}
255256

257+
258+
// test validation / clean-up logic
256259

257260
@Test
258261
fun testCleanUp_Recurring_Exceptions_NoSyncId() {
@@ -338,4 +341,113 @@ class AndroidRecurringCalendarTest {
338341
)
339342
}
340343

344+
345+
// test helpers for dirty/deleted events and exceptions
346+
347+
@Test
348+
fun testProcessDeletedExceptions() {
349+
val now = System.currentTimeMillis()
350+
val mainValues = contentValuesOf(
351+
Events._SYNC_ID to "testProcessDeletedExceptions",
352+
Events.CALENDAR_ID to calendar.id,
353+
Events.DTSTART to now,
354+
Events.DURATION to "PT1H",
355+
Events.RRULE to "FREQ=DAILY;COUNT=5",
356+
Events.DIRTY to 0,
357+
Events.DELETED to 0,
358+
EventsContract.COLUMN_SEQUENCE to 15
359+
)
360+
val exNotDeleted = Entity(
361+
contentValuesOf(
362+
Events.CALENDAR_ID to calendar.id,
363+
Events.ORIGINAL_INSTANCE_TIME to now,
364+
Events.ORIGINAL_ALL_DAY to 0,
365+
Events.DTSTART to now,
366+
Events.TITLE to "not marked as deleted",
367+
Events.DIRTY to 0,
368+
Events.DELETED to 0
369+
)
370+
)
371+
val mainId = recurringCalendar.addEventAndExceptions(
372+
EventAndExceptions(
373+
main = Entity(mainValues),
374+
exceptions = listOf(
375+
exNotDeleted,
376+
Entity(
377+
contentValuesOf(
378+
Events.CALENDAR_ID to calendar.id,
379+
Events.ORIGINAL_INSTANCE_TIME to now,
380+
Events.ORIGINAL_ALL_DAY to 0,
381+
Events.DTSTART to now,
382+
Events.DIRTY to 1,
383+
Events.DELETED to 1,
384+
Events.TITLE to "marked as deleted"
385+
)
386+
)
387+
)
388+
)
389+
)
390+
391+
// should update main event and purge the deleted exception
392+
recurringCalendar.processDeletedExceptions()
393+
394+
val result = recurringCalendar.getById(mainId)!!
395+
assertEventAndExceptionsEqual(
396+
EventAndExceptions(
397+
main = Entity(ContentValues(mainValues).apply {
398+
put(Events.DIRTY, 1)
399+
put(EventsContract.COLUMN_SEQUENCE, 16)
400+
}),
401+
exceptions = listOf(exNotDeleted)
402+
), result, onlyFieldsInExpected = true
403+
)
404+
}
405+
406+
@Test
407+
fun testProcessDirtyExceptions() {
408+
val now = System.currentTimeMillis()
409+
val mainValues = contentValuesOf(
410+
Events._SYNC_ID to "testProcessDirtyExceptions",
411+
Events.CALENDAR_ID to calendar.id,
412+
Events.DTSTART to now,
413+
Events.DURATION to "PT1H",
414+
Events.RRULE to "FREQ=DAILY;COUNT=5",
415+
Events.DIRTY to 0,
416+
Events.DELETED to 0,
417+
EventsContract.COLUMN_SEQUENCE to 15
418+
)
419+
val exDirtyValues = contentValuesOf(
420+
Events.CALENDAR_ID to calendar.id,
421+
Events.ORIGINAL_INSTANCE_TIME to now,
422+
Events.ORIGINAL_ALL_DAY to 0,
423+
Events.DTSTART to now,
424+
Events.DIRTY to 1,
425+
Events.DELETED to 0,
426+
Events.TITLE to "marked as dirty",
427+
EventsContract.COLUMN_SEQUENCE to null
428+
)
429+
val mainId = recurringCalendar.addEventAndExceptions(
430+
EventAndExceptions(
431+
main = Entity(mainValues),
432+
exceptions = listOf(Entity(exDirtyValues))
433+
)
434+
)
435+
436+
// should mark main event as dirty and increase exception SEQUENCE
437+
recurringCalendar.processDirtyExceptions()
438+
439+
val result = recurringCalendar.getById(mainId)!!
440+
assertEventAndExceptionsEqual(
441+
EventAndExceptions(
442+
main = Entity(ContentValues(mainValues).apply {
443+
put(Events.DIRTY, 1)
444+
}),
445+
exceptions = listOf(Entity(ContentValues(exDirtyValues).apply {
446+
put(Events.DIRTY, 0)
447+
put(EventsContract.COLUMN_SEQUENCE, 1)
448+
}))
449+
), result, onlyFieldsInExpected = true
450+
)
451+
}
452+
341453
}

0 commit comments

Comments
 (0)