mirror of https://github.com/tasks/tasks
Clean up recurrence jvm tests
parent
5e128fd6de
commit
c3080e6559
@ -1,241 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2012 Todoroo Inc
|
|
||||||
*
|
|
||||||
* See the file "LICENSE" for the full license governing this code.
|
|
||||||
*/
|
|
||||||
package com.todoroo.astrid.repeats
|
|
||||||
|
|
||||||
import com.todoroo.andlib.utility.DateUtilities
|
|
||||||
import com.todoroo.astrid.data.Task
|
|
||||||
import net.fortuna.ical4j.model.Recur
|
|
||||||
import net.fortuna.ical4j.model.Recur.Frequency
|
|
||||||
import net.fortuna.ical4j.model.WeekDay
|
|
||||||
import net.fortuna.ical4j.model.WeekDay.*
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import org.tasks.date.DateTimeUtils
|
|
||||||
import org.tasks.repeats.RecurrenceUtils.newRecur
|
|
||||||
import org.tasks.time.DateTime
|
|
||||||
import org.tasks.time.DateTimeUtils.printTimestamp
|
|
||||||
import java.text.ParseException
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
class AdvancedRepeatTest {
|
|
||||||
private var task: Task? = null
|
|
||||||
private var nextDueDate: Long = 0
|
|
||||||
private var recur: Recur? = null
|
|
||||||
|
|
||||||
// --- date with time tests
|
|
||||||
@Before
|
|
||||||
fun setUp() {
|
|
||||||
task = Task()
|
|
||||||
task!!.completionDate = DateUtilities.now()
|
|
||||||
recur = newRecur()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testDueDateSpecificTime() {
|
|
||||||
buildRecur(1, Frequency.DAILY)
|
|
||||||
|
|
||||||
// test specific day & time
|
|
||||||
val dayWithTime = Task.createDueDate(
|
|
||||||
Task.URGENCY_SPECIFIC_DAY_TIME, DateTime(2010, 8, 1, 10, 4, 0).millis)
|
|
||||||
task!!.dueDate = dayWithTime
|
|
||||||
val nextDayWithTime = dayWithTime + DateUtilities.ONE_DAY
|
|
||||||
nextDueDate = RepeatTaskHelper.computeNextDueDate(task!!, recur!!.toString(), false)
|
|
||||||
assertDateTimeEquals(nextDayWithTime, nextDueDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- due date tests
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testCompletionDateSpecificTime() {
|
|
||||||
buildRecur(1, Frequency.DAILY)
|
|
||||||
|
|
||||||
// test specific day & time
|
|
||||||
val dayWithTime = Task.createDueDate(
|
|
||||||
Task.URGENCY_SPECIFIC_DAY_TIME, DateTime(2010, 8, 1, 10, 4, 0).millis)
|
|
||||||
task!!.dueDate = dayWithTime
|
|
||||||
val todayWithTime = DateTimeUtils.newDateTime().withHourOfDay(10).withMinuteOfHour(4).withSecondOfMinute(1)
|
|
||||||
var nextDayWithTimeLong = todayWithTime.millis
|
|
||||||
nextDayWithTimeLong += DateUtilities.ONE_DAY
|
|
||||||
nextDayWithTimeLong = nextDayWithTimeLong / 1000L * 1000
|
|
||||||
nextDueDate = RepeatTaskHelper.computeNextDueDate(task!!, recur!!.toString(), true)
|
|
||||||
assertDateTimeEquals(nextDayWithTimeLong, nextDueDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** test multiple days per week - DUE DATE */
|
|
||||||
@Test
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun testDueDateInPastSingleWeekMultiDay() {
|
|
||||||
buildRecur(1, Frequency.WEEKLY, MO, WE, FR)
|
|
||||||
setTaskDueDate(THIS, Calendar.SUNDAY)
|
|
||||||
computeNextDueDate(false)
|
|
||||||
assertDueDate(nextDueDate, THIS, Calendar.MONDAY)
|
|
||||||
setTaskDueDate(THIS, Calendar.MONDAY)
|
|
||||||
computeNextDueDate(false)
|
|
||||||
assertDueDate(nextDueDate, THIS, Calendar.WEDNESDAY)
|
|
||||||
setTaskDueDate(THIS, Calendar.FRIDAY)
|
|
||||||
computeNextDueDate(false)
|
|
||||||
assertDueDate(nextDueDate, THIS, Calendar.MONDAY)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** test single day repeats - DUE DATE */
|
|
||||||
@Test
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun testDueDateSingleDay() {
|
|
||||||
buildRecur(1, Frequency.WEEKLY, MO)
|
|
||||||
setTaskDueDate(PREV_PREV, Calendar.MONDAY)
|
|
||||||
computeNextDueDate(false)
|
|
||||||
assertDueDate(nextDueDate, NEXT, Calendar.MONDAY)
|
|
||||||
setTaskDueDate(PREV_PREV, Calendar.FRIDAY)
|
|
||||||
computeNextDueDate(false)
|
|
||||||
assertDueDate(nextDueDate, THIS, Calendar.MONDAY)
|
|
||||||
setTaskDueDate(PREV, Calendar.MONDAY)
|
|
||||||
computeNextDueDate(false)
|
|
||||||
assertDueDate(nextDueDate, NEXT, Calendar.MONDAY)
|
|
||||||
setTaskDueDate(PREV, Calendar.FRIDAY)
|
|
||||||
computeNextDueDate(false)
|
|
||||||
assertDueDate(nextDueDate, THIS, Calendar.MONDAY)
|
|
||||||
setTaskDueDate(THIS, Calendar.SUNDAY)
|
|
||||||
computeNextDueDate(false)
|
|
||||||
assertDueDate(nextDueDate, THIS, Calendar.MONDAY)
|
|
||||||
setTaskDueDate(THIS, Calendar.MONDAY)
|
|
||||||
computeNextDueDate(false)
|
|
||||||
assertDueDate(nextDueDate, NEXT, Calendar.MONDAY)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** test multiple days per week - DUE DATE */
|
|
||||||
@Test
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun testDueDateSingleWeekMultiDay() {
|
|
||||||
buildRecur(1, Frequency.WEEKLY, MO, WE, FR)
|
|
||||||
setTaskDueDate(THIS, Calendar.SUNDAY)
|
|
||||||
computeNextDueDate(false)
|
|
||||||
assertDueDate(nextDueDate, THIS, Calendar.MONDAY)
|
|
||||||
setTaskDueDate(THIS, Calendar.MONDAY)
|
|
||||||
computeNextDueDate(false)
|
|
||||||
assertDueDate(nextDueDate, THIS, Calendar.WEDNESDAY)
|
|
||||||
setTaskDueDate(THIS, Calendar.FRIDAY)
|
|
||||||
computeNextDueDate(false)
|
|
||||||
assertDueDate(nextDueDate, THIS, Calendar.MONDAY)
|
|
||||||
}
|
|
||||||
// --- completion tests
|
|
||||||
/** test multiple days per week, multiple intervals - DUE DATE */
|
|
||||||
@Test
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun testDueDateMultiWeekMultiDay() {
|
|
||||||
buildRecur(2, Frequency.WEEKLY, MO, WE, FR)
|
|
||||||
setTaskDueDate(THIS, Calendar.SUNDAY)
|
|
||||||
computeNextDueDate(false)
|
|
||||||
assertDueDate(nextDueDate, NEXT, Calendar.MONDAY)
|
|
||||||
setTaskDueDate(THIS, Calendar.MONDAY)
|
|
||||||
computeNextDueDate(false)
|
|
||||||
assertDueDate(nextDueDate, THIS, Calendar.WEDNESDAY)
|
|
||||||
setTaskDueDate(THIS, Calendar.FRIDAY)
|
|
||||||
computeNextDueDate(false)
|
|
||||||
assertDueDate(nextDueDate, NEXT, Calendar.MONDAY)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** test multiple days per week - COMPLETE DATE */
|
|
||||||
@Test
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun testCompleteDateSingleWeek() {
|
|
||||||
for (wday in weekdays) {
|
|
||||||
buildRecur(1, Frequency.WEEKLY, wday)
|
|
||||||
computeNextDueDate(true)
|
|
||||||
val expected = getDate(DateUtilities.now() + DateUtilities.ONE_DAY, THIS, getCalendarDay(wday))
|
|
||||||
nextDueDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, nextDueDate)
|
|
||||||
assertEquals(expected, nextDueDate)
|
|
||||||
}
|
|
||||||
for (wday1 in weekdays) {
|
|
||||||
for (wday2 in weekdays) {
|
|
||||||
if (wday1 == wday2) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
buildRecur(1, Frequency.WEEKLY, wday1, wday2)
|
|
||||||
val nextOne = getDate(DateUtilities.now() + DateUtilities.ONE_DAY, THIS, getCalendarDay(wday1))
|
|
||||||
val nextTwo = getDate(DateUtilities.now() + DateUtilities.ONE_DAY, THIS, getCalendarDay(wday2))
|
|
||||||
computeNextDueDate(true)
|
|
||||||
nextDueDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, nextDueDate)
|
|
||||||
assertEquals(min(nextOne, nextTwo), nextDueDate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// --- helpers
|
|
||||||
/** test multiple days per week, multiple intervals - COMPLETE DATE */
|
|
||||||
@Test
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun testCompleteDateMultiWeek() {
|
|
||||||
for (wday in weekdays) {
|
|
||||||
buildRecur(2, Frequency.WEEKLY, wday)
|
|
||||||
computeNextDueDate(true)
|
|
||||||
val expected = getDate(DateUtilities.now() + DateUtilities.ONE_DAY, NEXT, getCalendarDay(wday))
|
|
||||||
nextDueDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, nextDueDate)
|
|
||||||
assertEquals(expected, nextDueDate)
|
|
||||||
}
|
|
||||||
for (wday1 in weekdays) {
|
|
||||||
for (wday2 in weekdays) {
|
|
||||||
if (wday1 == wday2) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
buildRecur(2, Frequency.WEEKLY, wday1, wday2)
|
|
||||||
val nextOne = getDate(DateUtilities.now() + DateUtilities.ONE_DAY, NEXT, getCalendarDay(wday1))
|
|
||||||
val nextTwo = getDate(DateUtilities.now() + DateUtilities.ONE_DAY, NEXT, getCalendarDay(wday2))
|
|
||||||
computeNextDueDate(true)
|
|
||||||
nextDueDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, nextDueDate)
|
|
||||||
assertEquals(min(nextOne, nextTwo), nextDueDate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
private fun computeNextDueDate(fromComplete: Boolean) {
|
|
||||||
nextDueDate = RepeatTaskHelper.computeNextDueDate(task!!, recur!!.toString(), fromComplete)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildRecur(interval: Int, freq: Frequency, vararg weekdays: WeekDay) {
|
|
||||||
recur!!.interval = interval
|
|
||||||
recur!!.setFrequency(freq.name)
|
|
||||||
recur!!.dayList.clear()
|
|
||||||
recur!!.dayList.addAll(weekdays)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun assertDueDate(actual: Long, expectedWhich: Int, expectedDayOfWeek: Int) {
|
|
||||||
val expected = getDate(task!!.dueDate, expectedWhich, expectedDayOfWeek)
|
|
||||||
assertEquals(expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setTaskDueDate(which: Int, day: Int) {
|
|
||||||
val time = getDate(DateUtilities.now(), which, day)
|
|
||||||
task!!.dueDate = time
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDate(start: Long, which: Int, dayOfWeek: Int): Long {
|
|
||||||
val c = Calendar.getInstance()
|
|
||||||
c.timeInMillis = start
|
|
||||||
val direction = if (which > 0) 1 else -1
|
|
||||||
while (c[Calendar.DAY_OF_WEEK] != dayOfWeek) {
|
|
||||||
c.add(Calendar.DAY_OF_MONTH, direction)
|
|
||||||
}
|
|
||||||
c.add(Calendar.DAY_OF_MONTH, (abs(which) - 1) * direction * 7)
|
|
||||||
return Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, c.timeInMillis)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val PREV_PREV = -2
|
|
||||||
private const val PREV = -1
|
|
||||||
private const val THIS = 1
|
|
||||||
private const val NEXT = 2
|
|
||||||
|
|
||||||
private val weekdays = listOf(SU, MO, TU, WE, TH, FR, SA)
|
|
||||||
|
|
||||||
fun assertDateTimeEquals(date: Long, other: Long) {
|
|
||||||
assertEquals("Expected: ${printTimestamp(date)}, Actual: ${printTimestamp(other)}", date, other)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,268 +0,0 @@
|
|||||||
package com.todoroo.astrid.repeats
|
|
||||||
|
|
||||||
import com.natpryce.makeiteasy.MakeItEasy.with
|
|
||||||
import com.todoroo.astrid.data.Task
|
|
||||||
import net.fortuna.ical4j.model.Recur.Frequency
|
|
||||||
import net.fortuna.ical4j.model.WeekDay
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Test
|
|
||||||
import org.tasks.makers.TaskMaker.AFTER_COMPLETE
|
|
||||||
import org.tasks.makers.TaskMaker.COMPLETION_TIME
|
|
||||||
import org.tasks.makers.TaskMaker.DUE_TIME
|
|
||||||
import org.tasks.makers.TaskMaker.RECUR
|
|
||||||
import org.tasks.makers.TaskMaker.newTask
|
|
||||||
import org.tasks.repeats.RecurrenceUtils.newRecur
|
|
||||||
import org.tasks.time.DateTime
|
|
||||||
import java.text.ParseException
|
|
||||||
|
|
||||||
class NewRepeatTests {
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatMinutelyFromDueDate() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 26, 12, 30)
|
|
||||||
val task = newFromDue(Frequency.MINUTELY, 1, dueDateTime)
|
|
||||||
assertEquals(newDayTime(2016, 8, 26, 12, 31), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatHourlyFromDueDate() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 26, 12, 30)
|
|
||||||
val task = newFromDue(Frequency.HOURLY, 1, dueDateTime)
|
|
||||||
assertEquals(newDayTime(2016, 8, 26, 13, 30), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatDailyFromDueDate() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 26, 12, 30)
|
|
||||||
val task = newFromDue(Frequency.DAILY, 1, dueDateTime)
|
|
||||||
assertEquals(newDayTime(2016, 8, 27, 12, 30), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatWeeklyFromDueDate() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 28, 1, 34)
|
|
||||||
val task = newFromDue(Frequency.WEEKLY, 1, dueDateTime)
|
|
||||||
assertEquals(newDayTime(2016, 9, 4, 1, 34), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatMonthlyFromDueDate() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 28, 1, 44)
|
|
||||||
val task = newFromDue(Frequency.MONTHLY, 1, dueDateTime)
|
|
||||||
assertEquals(newDayTime(2016, 9, 28, 1, 44), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatYearlyFromDueDate() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 28, 1, 44)
|
|
||||||
val task = newFromDue(Frequency.YEARLY, 1, dueDateTime)
|
|
||||||
assertEquals(newDayTime(2017, 8, 28, 1, 44), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Tests for repeating from completionDate */
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatMinutelyFromCompleteDateCompleteBefore() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 30, 0, 25)
|
|
||||||
val completionDateTime = newDayTime(2016, 8, 29, 0, 14)
|
|
||||||
val task = newFromCompleted(Frequency.MINUTELY, 1, dueDateTime, completionDateTime)
|
|
||||||
assertEquals(newDayTime(2016, 8, 29, 0, 15), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatMinutelyFromCompleteDateCompleteAfter() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 28, 0, 4)
|
|
||||||
val completionDateTime = newDayTime(2016, 8, 29, 0, 14)
|
|
||||||
val task = newFromCompleted(Frequency.MINUTELY, 1, dueDateTime, completionDateTime)
|
|
||||||
assertEquals(newDayTime(2016, 8, 29, 0, 15), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatHourlyFromCompleteDateCompleteBefore() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 30, 0, 25)
|
|
||||||
val completionDateTime = newDayTime(2016, 8, 29, 0, 14)
|
|
||||||
val task = newFromCompleted(Frequency.HOURLY, 1, dueDateTime, completionDateTime)
|
|
||||||
assertEquals(newDayTime(2016, 8, 29, 1, 14), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatHourlyFromCompleteDateCompleteAfter() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 28, 0, 4)
|
|
||||||
val completionDateTime = newDayTime(2016, 8, 29, 0, 14)
|
|
||||||
val task = newFromCompleted(Frequency.HOURLY, 1, dueDateTime, completionDateTime)
|
|
||||||
assertEquals(newDayTime(2016, 8, 29, 1, 14), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatDailyFromCompleteDateCompleteBefore() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 30, 0, 25)
|
|
||||||
val completionDateTime = newDayTime(2016, 8, 29, 0, 14)
|
|
||||||
val task = newFromCompleted(Frequency.DAILY, 1, dueDateTime, completionDateTime)
|
|
||||||
assertEquals(newDayTime(2016, 8, 30, 0, 25), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatDailyFromCompleteDateCompleteAfter() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 28, 0, 4)
|
|
||||||
val completionDateTime = newDayTime(2016, 8, 29, 0, 14)
|
|
||||||
val task = newFromCompleted(Frequency.DAILY, 1, dueDateTime, completionDateTime)
|
|
||||||
assertEquals(newDayTime(2016, 8, 30, 0, 4), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatWeeklyFromCompleteDateCompleteBefore() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 30, 0, 25)
|
|
||||||
val completionDateTime = newDayTime(2016, 8, 29, 0, 14)
|
|
||||||
val task = newFromCompleted(Frequency.WEEKLY, 1, dueDateTime, completionDateTime)
|
|
||||||
assertEquals(newDayTime(2016, 9, 5, 0, 25), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatWeeklyFromCompleteDateCompleteAfter() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 28, 0, 4)
|
|
||||||
val completionDateTime = newDayTime(2016, 8, 29, 0, 14)
|
|
||||||
val task = newFromCompleted(Frequency.WEEKLY, 1, dueDateTime, completionDateTime)
|
|
||||||
assertEquals(newDayTime(2016, 9, 5, 0, 4), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatMonthlyFromCompleteDateCompleteBefore() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 30, 0, 25)
|
|
||||||
val completionDateTime = newDayTime(2016, 8, 29, 0, 14)
|
|
||||||
val task = newFromCompleted(Frequency.MONTHLY, 1, dueDateTime, completionDateTime)
|
|
||||||
assertEquals(newDayTime(2016, 9, 29, 0, 25), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatMonthlyFromCompleteDateCompleteAfter() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 28, 0, 4)
|
|
||||||
val completionDateTime = newDayTime(2016, 8, 29, 0, 14)
|
|
||||||
val task = newFromCompleted(Frequency.MONTHLY, 1, dueDateTime, completionDateTime)
|
|
||||||
assertEquals(newDayTime(2016, 9, 29, 0, 4), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatYearlyFromCompleteDateCompleteBefore() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 30, 0, 25)
|
|
||||||
val completionDateTime = newDayTime(2016, 8, 29, 0, 14)
|
|
||||||
val task = newFromCompleted(Frequency.YEARLY, 1, dueDateTime, completionDateTime)
|
|
||||||
assertEquals(newDayTime(2017, 8, 29, 0, 25), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatYearlyFromCompleteDateCompleteAfter() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 28, 0, 4)
|
|
||||||
val completionDateTime = newDayTime(2016, 8, 29, 0, 14)
|
|
||||||
val task = newFromCompleted(Frequency.YEARLY, 1, dueDateTime, completionDateTime)
|
|
||||||
assertEquals(newDayTime(2017, 8, 29, 0, 4), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testAdvancedRepeatWeeklyFromDueDate() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 29, 0, 25)
|
|
||||||
val task = newWeeklyFromDue(
|
|
||||||
1, dueDateTime, WeekDay(WeekDay.MO, 0), WeekDay(WeekDay.WE, 0))
|
|
||||||
assertEquals(newDayTime(2016, 8, 31, 0, 25), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testAdvancedRepeatWeeklyFromCompleteDateCompleteBefore() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 29, 0, 25)
|
|
||||||
val completionDateTime = newDayTime(2016, 8, 28, 1, 9)
|
|
||||||
val task = newWeeklyFromCompleted(
|
|
||||||
1,
|
|
||||||
dueDateTime,
|
|
||||||
completionDateTime,
|
|
||||||
WeekDay(WeekDay.MO, 0),
|
|
||||||
WeekDay(WeekDay.WE, 0))
|
|
||||||
assertEquals(newDayTime(2016, 8, 29, 0, 25), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testAdvancedRepeatWeeklyFromCompleteDateCompleteAfter() {
|
|
||||||
val dueDateTime = newDayTime(2016, 8, 29, 0, 25)
|
|
||||||
val completionDateTime = newDayTime(2016, 9, 1, 1, 9)
|
|
||||||
val task = newWeeklyFromCompleted(
|
|
||||||
1,
|
|
||||||
dueDateTime,
|
|
||||||
completionDateTime,
|
|
||||||
WeekDay(WeekDay.MO, 0),
|
|
||||||
WeekDay(WeekDay.WE, 0))
|
|
||||||
assertEquals(newDayTime(2016, 9, 5, 0, 25), calculateNextDueDate(task))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun newDayTime(year: Int, month: Int, day: Int, hour: Int, minute: Int): DateTime {
|
|
||||||
return DateTime(
|
|
||||||
Task.createDueDate(
|
|
||||||
Task.URGENCY_SPECIFIC_DAY_TIME,
|
|
||||||
DateTime(year, month, day, hour, minute).millis))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
private fun calculateNextDueDate(task: Task): DateTime {
|
|
||||||
return DateTime(
|
|
||||||
RepeatTaskHelper.computeNextDueDate(task, task.recurrence!!, task.repeatAfterCompletion()))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun newFromDue(frequency: Frequency, interval: Int, dueDateTime: DateTime): Task {
|
|
||||||
return newTask(
|
|
||||||
with(RECUR, getRecurrenceRule(frequency, interval)),
|
|
||||||
with(AFTER_COMPLETE, false),
|
|
||||||
with(DUE_TIME, dueDateTime))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun newWeeklyFromDue(interval: Int, dueDateTime: DateTime, vararg weekdays: WeekDay): Task {
|
|
||||||
return newTask(
|
|
||||||
with(RECUR, getRecurrenceRule(Frequency.WEEKLY, interval, *weekdays)),
|
|
||||||
with(AFTER_COMPLETE, false),
|
|
||||||
with(DUE_TIME, dueDateTime))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun newFromCompleted(
|
|
||||||
frequency: Frequency, interval: Int, dueDateTime: DateTime, completionDate: DateTime): Task {
|
|
||||||
return newTask(
|
|
||||||
with(RECUR, getRecurrenceRule(frequency, interval)),
|
|
||||||
with(AFTER_COMPLETE, true),
|
|
||||||
with(DUE_TIME, dueDateTime),
|
|
||||||
with(COMPLETION_TIME, completionDate))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun newWeeklyFromCompleted(
|
|
||||||
interval: Int, dueDateTime: DateTime, completionDate: DateTime, vararg weekdays: WeekDay): Task {
|
|
||||||
return newTask(
|
|
||||||
with(RECUR, getRecurrenceRule(Frequency.WEEKLY, interval, *weekdays)),
|
|
||||||
with(AFTER_COMPLETE, true),
|
|
||||||
with(DUE_TIME, dueDateTime),
|
|
||||||
with(COMPLETION_TIME, completionDate))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getRecurrenceRule(
|
|
||||||
frequency: Frequency, interval: Int, vararg weekdays: WeekDay): String {
|
|
||||||
val rrule = newRecur()
|
|
||||||
rrule.setFrequency(frequency.name)
|
|
||||||
rrule.interval = interval
|
|
||||||
if (weekdays.isNotEmpty()) {
|
|
||||||
rrule.dayList.addAll(weekdays)
|
|
||||||
}
|
|
||||||
return rrule.toString()
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,46 @@
|
|||||||
|
package com.todoroo.astrid.repeats
|
||||||
|
|
||||||
|
import com.natpryce.makeiteasy.MakeItEasy.with
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.tasks.makers.TaskMaker.COMPLETION_TIME
|
||||||
|
import org.tasks.time.DateTime
|
||||||
|
|
||||||
|
class RepeatDailyTests : RepeatTests() {
|
||||||
|
@Test
|
||||||
|
fun testRepeatDailyFromDueDate() {
|
||||||
|
val task = newFromDue("FREQ=DAILY;INTERVAL=3", newDayTime(2016, 8, 26, 12, 30))
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 8, 29, 12, 30), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRepeatDailyFromCompleteDateCompleteBefore() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=DAILY;INTERVAL=1",
|
||||||
|
newDayTime(2016, 8, 30, 0, 25),
|
||||||
|
with(COMPLETION_TIME, DateTime(2016, 8, 29, 0, 14, 13, 451)),
|
||||||
|
afterComplete = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 8, 30, 0, 25), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRepeatDailyFromCompleteDateCompleteAfter() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=DAILY;INTERVAL=1",
|
||||||
|
newDayTime(2016, 8, 28, 0, 4),
|
||||||
|
with(COMPLETION_TIME, DateTime(2016, 8, 29, 0, 14, 13, 451)),
|
||||||
|
afterComplete = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 8, 30, 0, 4), next)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package com.todoroo.astrid.repeats
|
||||||
|
|
||||||
|
import com.natpryce.makeiteasy.MakeItEasy.with
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.tasks.Freeze.Companion.freezeAt
|
||||||
|
import org.tasks.makers.TaskMaker.COMPLETION_TIME
|
||||||
|
import org.tasks.time.DateTime
|
||||||
|
|
||||||
|
class RepeatHourlyTests : RepeatTests() {
|
||||||
|
@Test
|
||||||
|
fun testRepeatHourlyFromDueDate() {
|
||||||
|
val task = newFromDue("FREQ=HOURLY;INTERVAL=6", newDayTime(2016, 8, 26, 12, 30))
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 8, 26, 18, 30), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRepeatHourlyFromCompleteDateCompleteBefore() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=HOURLY;INTERVAL=1",
|
||||||
|
newDayTime(2016, 8, 30, 0, 25),
|
||||||
|
with(COMPLETION_TIME, DateTime(2016, 8, 29, 0, 14, 13, 451)),
|
||||||
|
afterComplete = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 8, 29, 1, 14), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRepeatHourlyFromCompleteDateCompleteAfter() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=HOURLY;INTERVAL=1",
|
||||||
|
newDayTime(2016, 8, 28, 0, 4),
|
||||||
|
with(COMPLETION_TIME, DateTime(2016, 8, 29, 0, 14, 13, 451)),
|
||||||
|
afterComplete = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = freezeAt(newDayTime(2016, 8, 29, 0, 14)) {
|
||||||
|
calculateNextDueDate(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 8, 29, 1, 14), next)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package com.todoroo.astrid.repeats
|
||||||
|
|
||||||
|
import com.natpryce.makeiteasy.MakeItEasy.with
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.tasks.makers.TaskMaker.COMPLETION_TIME
|
||||||
|
import org.tasks.time.DateTime
|
||||||
|
|
||||||
|
class RepeatMinutelyTests : RepeatTests() {
|
||||||
|
@Test
|
||||||
|
fun testRepeatMinutelyFromDueDate() {
|
||||||
|
val task = newFromDue("FREQ=MINUTELY;INTERVAL=30", newDayTime(2016, 8, 26, 12, 30))
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 8, 26, 13, 0), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRepeatMinutelyFromCompleteDateCompleteBefore() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=MINUTELY;INTERVAL=1",
|
||||||
|
newDayTime(2016, 8, 30, 0, 25),
|
||||||
|
with(COMPLETION_TIME, DateTime(2016, 8, 29, 0, 14, 13, 451)),
|
||||||
|
afterComplete = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 8, 29, 0, 15), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRepeatMinutelyFromCompleteDateCompleteAfter() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=MINUTELY;INTERVAL=1",
|
||||||
|
newDayTime(2016, 8, 28, 0, 4),
|
||||||
|
with(COMPLETION_TIME, DateTime(2016, 8, 29, 0, 14, 13, 451)),
|
||||||
|
afterComplete = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 8, 29, 0, 15), next)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
package com.todoroo.astrid.repeats
|
||||||
|
|
||||||
|
import com.natpryce.makeiteasy.MakeItEasy.with
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.tasks.makers.TaskMaker.COMPLETION_TIME
|
||||||
|
import org.tasks.time.DateTime
|
||||||
|
|
||||||
|
class RepeatMonthlyTests : RepeatTests() {
|
||||||
|
@Test
|
||||||
|
fun testRepeatMonthlyFromDueDate() {
|
||||||
|
val task = newFromDue("FREQ=MONTHLY;INTERVAL=3", newDayTime(2016, 8, 28, 1, 44))
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 11, 28, 1, 44), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRepeatMonthlyFromCompleteDateCompleteBefore() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=MONTHLY;INTERVAL=1",
|
||||||
|
newDayTime(2016, 8, 30, 0, 25),
|
||||||
|
with(COMPLETION_TIME, DateTime(2016, 8, 29, 0, 14, 13, 451)),
|
||||||
|
afterComplete = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 9, 29, 0, 25), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRepeatMonthlyFromCompleteDateCompleteAfter() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=MONTHLY;INTERVAL=1",
|
||||||
|
newDayTime(2016, 8, 28, 0, 4),
|
||||||
|
with(COMPLETION_TIME, DateTime(2016, 8, 29, 0, 14, 13, 451)),
|
||||||
|
afterComplete = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 9, 29, 0, 4), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun repeatAtEndOfJanuary() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=MONTHLY;INTERVAL=1",
|
||||||
|
newDayTime(2017, 1, 31, 13, 30)
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2017, 2, 28, 13, 30), next)
|
||||||
|
}
|
||||||
|
}
|
@ -1,211 +0,0 @@
|
|||||||
package com.todoroo.astrid.repeats
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import com.natpryce.makeiteasy.MakeItEasy.with
|
|
||||||
import com.todoroo.astrid.alarms.AlarmService
|
|
||||||
import com.todoroo.astrid.dao.TaskDao
|
|
||||||
import com.todoroo.astrid.data.Task
|
|
||||||
import com.todoroo.astrid.gcal.GCalHelper
|
|
||||||
import com.todoroo.astrid.service.TaskCompleter
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runBlockingTest
|
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import org.mockito.InOrder
|
|
||||||
import org.mockito.Mockito
|
|
||||||
import org.tasks.Freeze.Companion.freezeClock
|
|
||||||
import org.tasks.LocalBroadcastManager
|
|
||||||
import org.tasks.makers.TaskMaker.AFTER_COMPLETE
|
|
||||||
import org.tasks.makers.TaskMaker.COMPLETION_TIME
|
|
||||||
import org.tasks.makers.TaskMaker.DUE_TIME
|
|
||||||
import org.tasks.makers.TaskMaker.ID
|
|
||||||
import org.tasks.makers.TaskMaker.RECUR
|
|
||||||
import org.tasks.makers.TaskMaker.newTask
|
|
||||||
import org.tasks.repeats.RecurrenceUtils.newRecur
|
|
||||||
import org.tasks.time.DateTime
|
|
||||||
import java.text.ParseException
|
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
class RepeatTaskHelperTest {
|
|
||||||
private lateinit var taskDao: TaskDao
|
|
||||||
private lateinit var localBroadcastManager: LocalBroadcastManager
|
|
||||||
private lateinit var alarmService: AlarmService
|
|
||||||
private lateinit var gCalHelper: GCalHelper
|
|
||||||
private lateinit var helper: RepeatTaskHelper
|
|
||||||
private lateinit var mocks: InOrder
|
|
||||||
private lateinit var taskCompleter: TaskCompleter
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setUp() {
|
|
||||||
taskDao = Mockito.mock(TaskDao::class.java)
|
|
||||||
alarmService = Mockito.mock(AlarmService::class.java)
|
|
||||||
gCalHelper = Mockito.mock(GCalHelper::class.java)
|
|
||||||
localBroadcastManager = Mockito.mock(LocalBroadcastManager::class.java)
|
|
||||||
taskCompleter = Mockito.mock(TaskCompleter::class.java)
|
|
||||||
mocks = Mockito.inOrder(alarmService, gCalHelper, localBroadcastManager)
|
|
||||||
helper = RepeatTaskHelper(gCalHelper, alarmService, taskDao, localBroadcastManager, taskCompleter)
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun after() {
|
|
||||||
Mockito.verifyNoMoreInteractions(localBroadcastManager, gCalHelper, alarmService)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun noRepeat() = runBlockingTest {
|
|
||||||
helper.handleRepeat(newTask(with(DUE_TIME, DateTime(2017, 10, 4, 13, 30))))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testMinutelyRepeat() {
|
|
||||||
val task = newTask(
|
|
||||||
with(ID, 1L),
|
|
||||||
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
|
|
||||||
with(RECUR, "RRULE:FREQ=MINUTELY;INTERVAL=30"))
|
|
||||||
repeatAndVerify(
|
|
||||||
task, DateTime(2017, 10, 4, 13, 30, 1), DateTime(2017, 10, 4, 14, 0, 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testMinutelyRepeatAfterCompletion() {
|
|
||||||
val task = newTask(
|
|
||||||
with(ID, 1L),
|
|
||||||
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
|
|
||||||
with(COMPLETION_TIME, DateTime(2017, 10, 4, 13, 17, 45, 340)),
|
|
||||||
with(RECUR, "RRULE:FREQ=MINUTELY;INTERVAL=30"),
|
|
||||||
with(AFTER_COMPLETE, true))
|
|
||||||
repeatAndVerify(
|
|
||||||
task, DateTime(2017, 10, 4, 13, 30, 1), DateTime(2017, 10, 4, 13, 47, 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testMinutelyDecrementCount() {
|
|
||||||
val task = newTask(
|
|
||||||
with(ID, 1L),
|
|
||||||
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
|
|
||||||
with(RECUR, "RRULE:FREQ=MINUTELY;COUNT=2;INTERVAL=30"))
|
|
||||||
repeatAndVerify(
|
|
||||||
task, DateTime(2017, 10, 4, 13, 30, 1), DateTime(2017, 10, 4, 14, 0, 1))
|
|
||||||
assertEquals(1, newRecur(task.recurrence!!).count)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testMinutelyLastOccurrence() = runBlockingTest {
|
|
||||||
val task = newTask(
|
|
||||||
with(ID, 1L),
|
|
||||||
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
|
|
||||||
with(RECUR, "RRULE:FREQ=MINUTELY;COUNT=1;INTERVAL=30"))
|
|
||||||
helper.handleRepeat(task)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testHourlyRepeat() {
|
|
||||||
val task = newTask(
|
|
||||||
with(ID, 1L),
|
|
||||||
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
|
|
||||||
with(RECUR, "RRULE:FREQ=HOURLY;INTERVAL=6"))
|
|
||||||
repeatAndVerify(
|
|
||||||
task, DateTime(2017, 10, 4, 13, 30, 1), DateTime(2017, 10, 4, 19, 30, 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testHourlyRepeatAfterCompletion() {
|
|
||||||
val task = newTask(
|
|
||||||
with(ID, 1L),
|
|
||||||
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
|
|
||||||
with(COMPLETION_TIME, DateTime(2017, 10, 4, 13, 17, 45, 340)),
|
|
||||||
with(RECUR, "RRULE:FREQ=HOURLY;INTERVAL=6"),
|
|
||||||
with(AFTER_COMPLETE, true))
|
|
||||||
repeatAndVerify(
|
|
||||||
task, DateTime(2017, 10, 4, 13, 30, 1), DateTime(2017, 10, 4, 19, 17, 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testDailyRepeat() {
|
|
||||||
val task = newTask(
|
|
||||||
with(ID, 1L),
|
|
||||||
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
|
|
||||||
with(RECUR, "RRULE:FREQ=DAILY;INTERVAL=6"))
|
|
||||||
repeatAndVerify(
|
|
||||||
task, DateTime(2017, 10, 4, 13, 30, 1), DateTime(2017, 10, 10, 13, 30, 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testRepeatWeeklyNoDays() {
|
|
||||||
val task = newTask(
|
|
||||||
with(ID, 1L),
|
|
||||||
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
|
|
||||||
with(RECUR, "RRULE:FREQ=WEEKLY;INTERVAL=2"))
|
|
||||||
repeatAndVerify(
|
|
||||||
task, DateTime(2017, 10, 4, 13, 30, 1), DateTime(2017, 10, 18, 13, 30, 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testYearly() {
|
|
||||||
val task = newTask(
|
|
||||||
with(ID, 1L),
|
|
||||||
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
|
|
||||||
with(RECUR, "RRULE:FREQ=YEARLY;INTERVAL=3"))
|
|
||||||
repeatAndVerify(
|
|
||||||
task, DateTime(2017, 10, 4, 13, 30, 1), DateTime(2020, 10, 4, 13, 30, 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testMonthlyRepeat() {
|
|
||||||
val task = newTask(
|
|
||||||
with(ID, 1L),
|
|
||||||
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
|
|
||||||
with(RECUR, "RRULE:FREQ=MONTHLY;INTERVAL=3"))
|
|
||||||
repeatAndVerify(
|
|
||||||
task, DateTime(2017, 10, 4, 13, 30, 1), DateTime(2018, 1, 4, 13, 30, 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(ParseException::class)
|
|
||||||
fun testMonthlyRepeatAtEndOfMonth() {
|
|
||||||
val task = newTask(
|
|
||||||
with(ID, 1L),
|
|
||||||
with(DUE_TIME, DateTime(2017, 1, 31, 13, 30)),
|
|
||||||
with(RECUR, "RRULE:FREQ=MONTHLY;INTERVAL=1"))
|
|
||||||
repeatAndVerify(
|
|
||||||
task, DateTime(2017, 1, 31, 13, 30, 1), DateTime(2017, 2, 28, 13, 30, 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testAlarmShiftWithNoDueDate() {
|
|
||||||
val task = newTask(
|
|
||||||
with(ID, 1L),
|
|
||||||
with(RECUR, "RRULE:FREQ=DAILY")
|
|
||||||
)
|
|
||||||
freezeClock {
|
|
||||||
repeatAndVerify(
|
|
||||||
task,
|
|
||||||
Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, DateTime().millis),
|
|
||||||
Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, DateTime().plusDays(1).millis)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun repeatAndVerify(task: Task, oldDueDate: DateTime, newDueDate: DateTime) =
|
|
||||||
repeatAndVerify(task, oldDueDate.millis, newDueDate.millis)
|
|
||||||
|
|
||||||
private fun repeatAndVerify(task: Task, oldDueDate: Long, newDueDate: Long) = runBlockingTest {
|
|
||||||
helper.handleRepeat(task)
|
|
||||||
mocks.verify(gCalHelper).rescheduleRepeatingTask(task)
|
|
||||||
mocks.verify(alarmService).rescheduleAlarms(1, oldDueDate, newDueDate)
|
|
||||||
mocks.verify(localBroadcastManager).broadcastRepeat(1, oldDueDate, newDueDate)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,80 @@
|
|||||||
|
package com.todoroo.astrid.repeats
|
||||||
|
|
||||||
|
import com.natpryce.makeiteasy.MakeItEasy.with
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
import org.tasks.Freeze.Companion.freezeAt
|
||||||
|
import org.tasks.makers.TaskMaker.COMPLETION_TIME
|
||||||
|
import org.tasks.repeats.RecurrenceUtils.newRecur
|
||||||
|
import org.tasks.time.DateTime
|
||||||
|
|
||||||
|
class RepeatTaskHelperTests : RepeatTests() {
|
||||||
|
@Test
|
||||||
|
fun shouldDecrementCount() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"RRULE:FREQ=MINUTELY;COUNT=2;INTERVAL=30",
|
||||||
|
newDayTime(2017, 10, 4, 13, 30)
|
||||||
|
)
|
||||||
|
|
||||||
|
calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(1, newRecur(task.recurrence!!).count)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldUncompleteTask() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=DAILY;INTERVAL=1",
|
||||||
|
newDayTime(2017, 10, 4, 13, 30),
|
||||||
|
with(COMPLETION_TIME, DateTime())
|
||||||
|
)
|
||||||
|
|
||||||
|
calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertFalse(task.isCompleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dontAdjustOnLastInstance() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=MINUTELY;COUNT=1;INTERVAL=30",
|
||||||
|
newDayTime(2017, 10, 4, 13, 30),
|
||||||
|
with(COMPLETION_TIME, DateTime())
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(1, newRecur(task.recurrence!!).count)
|
||||||
|
assertEquals(newDayTime(2017, 10, 4, 13, 30), next)
|
||||||
|
assertTrue(task.isCompleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun useCompletionWhenNoDue() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=DAILY;INTERVAL=1",
|
||||||
|
DateTime(0)
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = freezeAt(DateTime(2021, 2, 1, 16, 54, 32, 451)) {
|
||||||
|
calculateNextDueDate(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(newDay(2021, 2, 2), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun useNowWhenNoCompletion() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=DAILY;INTERVAL=1",
|
||||||
|
newDay(2021, 3, 15),
|
||||||
|
afterComplete = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = freezeAt(newDayTime(2021, 3, 29, 14, 32)) {
|
||||||
|
calculateNextDueDate(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2021, 3, 30, 12, 0), next)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
package com.todoroo.astrid.repeats
|
||||||
|
|
||||||
|
import com.natpryce.makeiteasy.MakeItEasy
|
||||||
|
import com.natpryce.makeiteasy.PropertyValue
|
||||||
|
import com.todoroo.astrid.alarms.AlarmService
|
||||||
|
import com.todoroo.astrid.dao.TaskDao
|
||||||
|
import com.todoroo.astrid.data.Task
|
||||||
|
import com.todoroo.astrid.gcal.GCalHelper
|
||||||
|
import com.todoroo.astrid.service.TaskCompleter
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.tasks.LocalBroadcastManager
|
||||||
|
import org.tasks.makers.TaskMaker
|
||||||
|
import org.tasks.time.DateTime
|
||||||
|
|
||||||
|
abstract class RepeatTests {
|
||||||
|
private val helper = RepeatTaskHelper(
|
||||||
|
Mockito.mock(GCalHelper::class.java),
|
||||||
|
Mockito.mock(AlarmService::class.java),
|
||||||
|
Mockito.mock(TaskDao::class.java),
|
||||||
|
Mockito.mock(LocalBroadcastManager::class.java),
|
||||||
|
Mockito.mock(TaskCompleter::class.java)
|
||||||
|
)
|
||||||
|
|
||||||
|
protected fun newDay(year: Int, month: Int, day: Int) =
|
||||||
|
DateTime(
|
||||||
|
Task.createDueDate(
|
||||||
|
Task.URGENCY_SPECIFIC_DAY,
|
||||||
|
DateTime(year, month, day).millis
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
protected fun newDayTime(year: Int, month: Int, day: Int, hour: Int, minute: Int) =
|
||||||
|
DateTime(
|
||||||
|
Task.createDueDate(
|
||||||
|
Task.URGENCY_SPECIFIC_DAY_TIME,
|
||||||
|
DateTime(year, month, day, hour, minute).millis
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
protected fun calculateNextDueDate(task: Task): DateTime = runBlocking {
|
||||||
|
helper.handleRepeat(task)
|
||||||
|
DateTime(task.dueDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun newFromDue(
|
||||||
|
recur: String,
|
||||||
|
due: DateTime,
|
||||||
|
vararg properties: PropertyValue<in Task?, *>,
|
||||||
|
afterComplete: Boolean = false
|
||||||
|
) = TaskMaker.newTask(
|
||||||
|
MakeItEasy.with(TaskMaker.RECUR, recur),
|
||||||
|
MakeItEasy.with(TaskMaker.AFTER_COMPLETE, afterComplete),
|
||||||
|
MakeItEasy.with(TaskMaker.DUE_TIME, due),
|
||||||
|
*properties
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,190 @@
|
|||||||
|
package com.todoroo.astrid.repeats
|
||||||
|
|
||||||
|
import com.natpryce.makeiteasy.MakeItEasy.with
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.tasks.makers.TaskMaker.COMPLETION_TIME
|
||||||
|
|
||||||
|
class RepeatWeeklyTests : RepeatTests() {
|
||||||
|
@Test
|
||||||
|
fun testRepeatWeeklyFromDueDate() {
|
||||||
|
val task = newFromDue("FREQ=WEEKLY;INTERVAL=1", newDayTime(2016, 8, 28, 1, 34))
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 9, 4, 1, 34), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRepeatBiWeekly() {
|
||||||
|
val task = newFromDue("FREQ=WEEKLY;INTERVAL=2", newDayTime(2016, 8, 28, 1, 34))
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 9, 11, 1, 34), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRepeatWeeklyFromCompleteDateCompleteBefore() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=WEEKLY;INTERVAL=1",
|
||||||
|
newDayTime(2016, 8, 30, 0, 25),
|
||||||
|
with(COMPLETION_TIME, newDayTime(2016, 8, 29, 0, 14)),
|
||||||
|
afterComplete = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 9, 5, 0, 25), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRepeatWeeklyFromCompleteDateCompleteAfter() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=WEEKLY;INTERVAL=1",
|
||||||
|
newDayTime(2016, 8, 28, 0, 4),
|
||||||
|
with(COMPLETION_TIME, newDayTime(2016, 8, 29, 0, 14)),
|
||||||
|
afterComplete = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 9, 5, 0, 4), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testWeeklyBySingleDayBefore() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=WEEKLY;INTERVAL=1;BYDAY=MO",
|
||||||
|
newDayTime(2016, 8, 28, 0, 25) // Sunday
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 8, 29, 0, 25), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testWeeklyBySingleDayOf() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=WEEKLY;INTERVAL=1;BYDAY=MO",
|
||||||
|
newDayTime(2016, 8, 29, 0, 25) // Monday
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 9, 5, 0, 25), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testWeeklyBySingleDayAfter() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=WEEKLY;INTERVAL=1;BYDAY=MO",
|
||||||
|
newDayTime(2016, 8, 30, 0, 25) // Sunday
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 9, 5, 0, 25), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testByDayBeforeFirstDate() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,WE",
|
||||||
|
newDayTime(2016, 8, 28, 0, 25) // Sunday
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 8, 29, 0, 25), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAdvancedRepeatWeeklyOnFirstDate() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,WE",
|
||||||
|
newDayTime(2016, 8, 29, 0, 25) // Monday
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 8, 31, 0, 25), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAdvancedRepeatWeeklyOnLastDate() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,WE",
|
||||||
|
newDayTime(2016, 8, 31, 0, 25) // Wednesday
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 9, 5, 0, 25), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAdvancedRepeatWeeklyFromCompleteDateCompleteBefore() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,WE",
|
||||||
|
newDayTime(2016, 8, 29, 0, 25),
|
||||||
|
with(COMPLETION_TIME, newDayTime(2016, 8, 28, 1, 9)),
|
||||||
|
afterComplete = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 8, 29, 0, 25), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAdvancedRepeatWeeklyFromCompleteDateCompleteAfter() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,WE",
|
||||||
|
newDayTime(2016, 8, 29, 0, 25),
|
||||||
|
with(COMPLETION_TIME, newDayTime(2016, 9, 1, 1, 9)),
|
||||||
|
afterComplete = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 9, 5, 0, 25), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun biweeklyOnBeforeFirstDay() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE",
|
||||||
|
newDayTime(2016, 8, 28, 0, 25), // Sunday
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 9, 5, 0, 25), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun biweeklyOnFirstDay() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE",
|
||||||
|
newDayTime(2016, 8, 29, 0, 25), // Monday
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 8, 31, 0, 25), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun biweeklyOnLastDay() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE",
|
||||||
|
newDayTime(2016, 8, 31, 0, 25), // Wednesday
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2016, 9, 12, 0, 25), next)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package com.todoroo.astrid.repeats
|
||||||
|
|
||||||
|
import com.natpryce.makeiteasy.MakeItEasy.with
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.tasks.makers.TaskMaker.COMPLETION_TIME
|
||||||
|
|
||||||
|
class RepeatYearlyTests : RepeatTests() {
|
||||||
|
@Test
|
||||||
|
fun testRepeatYearlyFromDueDate() {
|
||||||
|
val task = newFromDue("FREQ=YEARLY;INTERVAL=2", newDayTime(2016, 8, 28, 1, 44))
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2018, 8, 28, 1, 44), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRepeatYearlyFromCompleteDateCompleteBefore() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=YEARLY;INTERVAL=1",
|
||||||
|
newDayTime(2016, 8, 30, 0, 25),
|
||||||
|
with(COMPLETION_TIME, newDayTime(2016, 8, 29, 0, 14)),
|
||||||
|
afterComplete = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2017, 8, 29, 0, 25), next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRepeatYearlyFromCompleteDateCompleteAfter() {
|
||||||
|
val task = newFromDue(
|
||||||
|
"FREQ=YEARLY;INTERVAL=1",
|
||||||
|
newDayTime(2016, 8, 28, 0, 4),
|
||||||
|
with(COMPLETION_TIME, newDayTime(2016, 8, 29, 0, 14)),
|
||||||
|
afterComplete = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val next = calculateNextDueDate(task)
|
||||||
|
|
||||||
|
assertEquals(newDayTime(2017, 8, 29, 0, 4), next)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue