Skip to content

Commit

Permalink
Add V3 scheduler tests
Browse files Browse the repository at this point in the history
Like the desktop, this works by using a single set of tests that
alter some of the checks depending on the active scheduler version,
so that code duplication is avoided.
  • Loading branch information
dae committed Jul 9, 2022
1 parent 575fa6d commit d1e19af
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 21 deletions.
11 changes: 11 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/libanki/CollectionV16.kt
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,17 @@ class CollectionV16(
return status.undo ?: super.undoName(res)
}

/** Provided for legacy code/tests; new code should call undoNew() directly
* so that OpChanges can be observed.
*/
override fun undo(): Card? {
if (undoStatus().undo != null) {
undoNew()
return null
}
return super.undo()
}

/** True if the V3 scheduled is enabled when schedVer is 2. */
var v3Enabled: Boolean
get() = backend.getConfigBool(ConfigKey.Bool.SCHED_2021)
Expand Down
115 changes: 94 additions & 21 deletions AnkiDroid/src/test/java/com/ichi2/libanki/sched/SchedV2Test.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import com.ichi2.libanki.Consts.STARTING_FACTOR
import com.ichi2.libanki.Consts.SYNC_VER
import com.ichi2.libanki.backend.exception.BackendNotSupportedException
import com.ichi2.libanki.stats.Stats
import com.ichi2.libanki.utils.TimeManager
import com.ichi2.libanki.utils.TimeManager.time
import com.ichi2.testutils.AnkiAssert
import com.ichi2.testutils.libanki.CollectionAssert
Expand All @@ -48,6 +49,8 @@ import com.ichi2.utils.JSONObject
import com.ichi2.utils.KotlinCleanup
import net.ankiweb.rsdroid.BackendFactory.defaultLegacySchema
import net.ankiweb.rsdroid.RustCleanup
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert
import org.hamcrest.Matchers
import org.junit.Assert
Expand All @@ -58,17 +61,32 @@ import org.junit.runner.RunWith
import java.lang.Exception
import java.util.*
import kotlin.Throws
import kotlin.math.roundToLong
import kotlin.test.assertNotNull
import kotlin.test.assertNull

@RunWith(AndroidJUnit4::class)
class SchedV2Test : RobolectricTest() {
open class SchedV2Test : RobolectricTest() {
open val v3 = false

fun ifV3(block: () -> Unit) {
if (v3) {
block()
}
}

fun ifV2(block: () -> Unit) {
if (!v3) {
block()
}
}

/**
* Reported by /u/CarelessSecretary9 on reddit:
*/
@Test
fun filteredDeckSchedulingOptionsRegressionTest() {
val col = col
val col = colV2
col.crt = 1587852900L
// 30 minutes learn ahead. required as we have 20m delay
col.set_config("collapseTime", 1800)
Expand Down Expand Up @@ -185,7 +203,7 @@ class SchedV2Test : RobolectricTest() {
@Test
@Throws(ConfirmModSchemaException::class)
fun emptyFilteredDeckSuspendHandling() {
col.changeSchedulerVer(2)
val col = colV2
val cardId = addNoteUsingBasicModel("Hello", "World").firstCard().id
val filteredDid = FilteredDeckUtil.createFilteredDeck(col, "Filtered", "(is:new or is:due)")
MatcherAssert.assertThat(
Expand Down Expand Up @@ -218,7 +236,7 @@ class SchedV2Test : RobolectricTest() {
@Test
@Throws(ConfirmModSchemaException::class)
fun rebuildFilteredDeckSuspendHandling() {
col.changeSchedulerVer(2)
val col = colV2
val cardId = addNoteUsingBasicModel("Hello", "World").firstCard().id
val filteredDid = FilteredDeckUtil.createFilteredDeck(col, "Filtered", "(is:new or is:due)")
MatcherAssert.assertThat(
Expand Down Expand Up @@ -251,8 +269,8 @@ class SchedV2Test : RobolectricTest() {
@Test
@Throws(ConfirmModSchemaException::class)
fun handlesSmallSteps() {
val col = colV2
// a delay of 0 crashed the app (step of 0.01).
col.changeSchedulerVer(2)
addNoteUsingBasicModel("Hello", "World")
col.decks.allConf()[0].getJSONObject("new")
.put("delays", JSONArray(Arrays.asList(0.01, 10)))
Expand All @@ -264,6 +282,7 @@ class SchedV2Test : RobolectricTest() {
@Test
@Throws(BackendNotSupportedException::class)
fun newTimezoneHandling() {
val col = colV2
// #5805
MatcherAssert.assertThat(
"Sync ver should be updated if we have a valid Rust collection",
Expand All @@ -275,7 +294,7 @@ class SchedV2Test : RobolectricTest() {
col.has_config("localOffset"),
Matchers.`is`(true)
)
val sched = col.sched as SchedV2
val sched = col.sched
MatcherAssert.assertThat(
"new timezone should be enabled by default",
sched._new_timezone_enabled(),
Expand Down Expand Up @@ -304,6 +323,10 @@ class SchedV2Test : RobolectricTest() {
get() {
val col = col
col.changeSchedulerVer(2)
ifV3 {
assumeThat(defaultLegacySchema, `is`(false))
col.newBackend.v3Enabled = true
}
return col
}

Expand Down Expand Up @@ -423,6 +446,7 @@ class SchedV2Test : RobolectricTest() {
@Test
@Throws(Exception::class)
fun test_learnV2() {
TimeManager.reset()
val col = colV2
// add a note
val note = col.newNote()
Expand All @@ -442,7 +466,7 @@ class SchedV2Test : RobolectricTest() {
col.sched.answerCard(c, BUTTON_ONE)
// it should have three reps left to graduation
Assert.assertEquals(3, (c.left % 1000).toLong())
Assert.assertEquals(3, (c.left / 1000).toLong())
ifV2 { Assert.assertEquals(3, (c.left / 1000).toLong()) }
// it should be due in 30 seconds
val t = Math.round((c.due - time.intTime()).toFloat()).toLong()
MatcherAssert.assertThat(t, Matchers.`is`(Matchers.greaterThanOrEqualTo(25L)))
Expand All @@ -457,7 +481,7 @@ class SchedV2Test : RobolectricTest() {
Matchers.`is`(Matchers.lessThanOrEqualTo((180 * 1.25).toLong()))
)
Assert.assertEquals(2, (c.left % 1000).toLong())
Assert.assertEquals(2, (c.left / 1000).toLong())
ifV2 { Assert.assertEquals(2, (c.left / 1000).toLong()) }
// check log is accurate
val log = col.db.database.query("select * from revlog order by id desc")
Assert.assertTrue(log.moveToFirst())
Expand All @@ -474,7 +498,7 @@ class SchedV2Test : RobolectricTest() {
Matchers.`is`(Matchers.lessThanOrEqualTo((600 * 1.25).toLong()))
)
Assert.assertEquals(1, (c.left % 1000).toLong())
Assert.assertEquals(1, (c.left / 1000).toLong())
ifV2 { Assert.assertEquals(1, (c.left / 1000).toLong()) }
// the next pass should graduate the card
Assert.assertEquals(QUEUE_TYPE_LRN, c.queue)
Assert.assertEquals(CARD_TYPE_LRN, c.type)
Expand All @@ -487,6 +511,7 @@ class SchedV2Test : RobolectricTest() {
// or normal removal
c.type = CARD_TYPE_NEW
c.queue = QUEUE_TYPE_LRN
c.flush()
col.sched.answerCard(c, BUTTON_FOUR)
Assert.assertEquals(CARD_TYPE_REV, c.type)
Assert.assertEquals(QUEUE_TYPE_REV, c.queue)
Expand Down Expand Up @@ -599,7 +624,7 @@ class SchedV2Test : RobolectricTest() {
col.sched.answerCard(c, BUTTON_THREE)
// two reps to graduate, 1 more today
Assert.assertEquals(3, (c.left % 1000).toLong())
Assert.assertEquals(1, (c.left / 1000).toLong())
ifV2 { Assert.assertEquals(1, (c.left / 1000).toLong()) }
Assert.assertEquals(Counts(0, 1, 0), col.sched.counts())
c = card!!
Assert.assertEquals(Stats.SECONDS_PER_DAY, col.sched.nextIvl(c, BUTTON_THREE))
Expand Down Expand Up @@ -732,6 +757,7 @@ class SchedV2Test : RobolectricTest() {
@RustCleanup("the legacySchema special case can be removed")
@Throws(Exception::class)
fun test_review_limits() {
TimeManager.reset()
val col = colV2
val parent = col.decks.get(addDeck("parent"))
val child = col.decks.get(addDeck("parent::child"))
Expand Down Expand Up @@ -948,7 +974,8 @@ class SchedV2Test : RobolectricTest() {
Assert.assertEquals(4 * Stats.SECONDS_PER_DAY, col.sched.nextIvl(c, BUTTON_FOUR))
col.sched.answerCard(c, BUTTON_THREE)
Assert.assertEquals(30, col.sched.nextIvl(c, BUTTON_ONE))
Assert.assertEquals(((180 + 600) / 2).toLong(), col.sched.nextIvl(c, BUTTON_TWO))
ifV2 { Assert.assertEquals(((180 + 600) / 2).toLong(), col.sched.nextIvl(c, BUTTON_TWO)) }
ifV3 { Assert.assertEquals(180, col.sched.nextIvl(c, BUTTON_TWO)) }
Assert.assertEquals(600, col.sched.nextIvl(c, BUTTON_THREE))
Assert.assertEquals(4 * Stats.SECONDS_PER_DAY, col.sched.nextIvl(c, BUTTON_FOUR))
col.sched.answerCard(c, BUTTON_THREE)
Expand All @@ -957,17 +984,20 @@ class SchedV2Test : RobolectricTest() {
Assert.assertEquals(4 * Stats.SECONDS_PER_DAY, col.sched.nextIvl(c, BUTTON_FOUR))
// lapsed cards
// //////////////////////////////////////////////////////////////////////////////////////////////////
c.type = CARD_TYPE_REV
c.type = CARD_TYPE_RELEARNING
c.ivl = 100
c.factor = STARTING_FACTOR
c.flush()
Assert.assertEquals(60, col.sched.nextIvl(c, BUTTON_ONE))
Assert.assertEquals(100 * Stats.SECONDS_PER_DAY, col.sched.nextIvl(c, BUTTON_THREE))
Assert.assertEquals(101 * Stats.SECONDS_PER_DAY, col.sched.nextIvl(c, BUTTON_FOUR))
// review cards
// //////////////////////////////////////////////////////////////////////////////////////////////////
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.ivl = 100
c.factor = STARTING_FACTOR
c.flush()
// failing it should put it at 60s
Assert.assertEquals(60, col.sched.nextIvl(c, BUTTON_ONE))
// or 1 day if relearn is false
Expand Down Expand Up @@ -1119,13 +1149,18 @@ class SchedV2Test : RobolectricTest() {
Math.round(75 * 1.2) * Stats.SECONDS_PER_DAY,
col.sched.nextIvl(c, BUTTON_TWO)
)
val toLong = if (v3) {
fun (v: Double) = v.roundToLong() * Stats.SECONDS_PER_DAY
} else {
fun (v: Double) = v.toLong() * Stats.SECONDS_PER_DAY
}
MatcherAssert.assertThat(
col.sched.nextIvl(c, BUTTON_THREE),
Matchers.`is`((75 * 2.5).toLong() * Stats.SECONDS_PER_DAY)
equalTo(toLong(75 * 2.5))
)
MatcherAssert.assertThat(
col.sched.nextIvl(c, BUTTON_FOUR),
Matchers.`is`((75 * 2.5 * 1.15).toLong() * Stats.SECONDS_PER_DAY)
equalTo(toLong(75 * 2.5 * 1.15))
)

// answer 'good'
Expand All @@ -1150,7 +1185,7 @@ class SchedV2Test : RobolectricTest() {
c = card!!
Assert.assertEquals(60 * Stats.SECONDS_PER_DAY, col.sched.nextIvl(c, BUTTON_TWO))
Assert.assertEquals(100 * Stats.SECONDS_PER_DAY, col.sched.nextIvl(c, BUTTON_THREE))
Assert.assertEquals(114 * Stats.SECONDS_PER_DAY, col.sched.nextIvl(c, BUTTON_FOUR))
Assert.assertEquals(toLong(114.5), col.sched.nextIvl(c, BUTTON_FOUR))
}

@Test
Expand All @@ -1169,7 +1204,7 @@ class SchedV2Test : RobolectricTest() {
col.sched.answerCard(c, BUTTON_ONE)
Assert.assertEquals(CARD_TYPE_LRN, c.queue)
Assert.assertEquals(QUEUE_TYPE_LRN, c.type)
Assert.assertEquals(3003, c.left)
Assert.assertEquals(3, c.left % 1000)
col.sched.answerCard(c, BUTTON_THREE)
Assert.assertEquals(CARD_TYPE_LRN, c.queue)
Assert.assertEquals(QUEUE_TYPE_LRN, c.type)
Expand All @@ -1183,7 +1218,7 @@ class SchedV2Test : RobolectricTest() {
c.load()
Assert.assertEquals(CARD_TYPE_LRN, c.queue)
Assert.assertEquals(QUEUE_TYPE_LRN, c.type)
Assert.assertEquals(2002, c.left)
Assert.assertEquals(2, c.left % 1000)

// should be able to advance learning steps
col.sched.answerCard(c, BUTTON_THREE)
Expand All @@ -1198,7 +1233,7 @@ class SchedV2Test : RobolectricTest() {
c.load()
Assert.assertEquals(CARD_TYPE_LRN, c.queue)
Assert.assertEquals(QUEUE_TYPE_LRN, c.type)
Assert.assertEquals(1001, c.left)
Assert.assertEquals(1, c.left % 1000)
MatcherAssert.assertThat(
c.due - time.intTime(),
Matchers.`is`(Matchers.greaterThan(60 * 60L))
Expand Down Expand Up @@ -1227,9 +1262,15 @@ class SchedV2Test : RobolectricTest() {
col.reset()
// grab the first card
c = card!!
Assert.assertEquals(2, col.sched.answerButtons(c).toLong())
Assert.assertEquals(600, col.sched.nextIvl(c, BUTTON_ONE))
Assert.assertEquals(0, col.sched.nextIvl(c, BUTTON_TWO))
ifV2 {
Assert.assertEquals(2, col.sched.answerButtons(c).toLong())
Assert.assertEquals(0, col.sched.nextIvl(c, BUTTON_TWO))
}
ifV3 {
Assert.assertEquals(4, col.sched.answerButtons(c).toLong())
Assert.assertEquals(900, col.sched.nextIvl(c, BUTTON_TWO))
}
// failing it will push its due time back
val due = c.due
col.sched.answerCard(c, BUTTON_ONE)
Expand All @@ -1240,7 +1281,7 @@ class SchedV2Test : RobolectricTest() {
Assert.assertNotEquals(c2.id, c.id)

// passing it will remove it
col.sched.answerCard(c2, BUTTON_TWO)
col.sched.answerCard(c2, if (v3) { BUTTON_FOUR } else { BUTTON_TWO })
Assert.assertEquals(QUEUE_TYPE_NEW, c2.queue)
Assert.assertEquals(0, c2.reps)
Assert.assertEquals(CARD_TYPE_NEW, c2.type)
Expand Down Expand Up @@ -1309,6 +1350,9 @@ class SchedV2Test : RobolectricTest() {
@Test
@Throws(Exception::class)
fun test_counts_idxV2() {
if (v3) {
return
}
val col = colV2
val note = col.newNote()
note.setItem("Front", "one")
Expand All @@ -1332,6 +1376,35 @@ class SchedV2Test : RobolectricTest() {
Assert.assertEquals(Counts(0, 1, 0), col.sched.counts())
}

@Test
@Throws(Exception::class)
fun test_counts_idxV3() {
if (!v3) {
return
}
val col = colV2
val note = col.newNote()
note.setItem("Front", "one")
note.setItem("Back", "two")
col.addNote(note)
val note2 = col.newNote()
note2.setItem("Front", "one")
note2.setItem("Back", "two")
col.addNote(note2)
Assert.assertEquals(Counts(2, 0, 0), col.sched.counts())
var c = card
// getCard does not decrement counts
Assert.assertEquals(Counts(2, 0, 0), col.sched.counts())
Assert.assertEquals(Counts.Queue.NEW, col.sched.countIdx(c!!))
// answer to move to learn queue
col.sched.answerCard(c, BUTTON_ONE)
Assert.assertEquals(Counts(1, 1, 0), col.sched.counts())
// fetching next will not decrement the count
c = card
Assert.assertEquals(Counts(1, 1, 0), col.sched.counts())
Assert.assertEquals(Counts.Queue.NEW, col.sched.countIdx(c!!))
}

@Test
@Throws(Exception::class)
fun test_repCountsV2() {
Expand Down
22 changes: 22 additions & 0 deletions AnkiDroid/src/test/java/com/ichi2/libanki/sched/SchedV3Test.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/***************************************************************************************
* Copyright (c) 2022 Ankitects Pty Ltd <https://apps.ankiweb.net> *
* *
* This program is free software; you can redistribute it and/or modify it under *
* the terms of the GNU General Public License as published by the Free Software *
* Foundation; either version 3 of the License, or (at your option) any later *
* version. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT ANY *
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
* PARTICULAR PURPOSE. See the GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License along with *
* this program. If not, see <http://www.gnu.org/licenses/>. *
****************************************************************************************/

package com.ichi2.libanki.sched

/** Runs the tests in SchedV2Test, with v3 enabled */
class SchedV3Test : SchedV2Test() {
override val v3 = true
}
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ subprojects {
includeAndroidResources = true
}
project.android.testOptions.unitTests.all {
// tell backend to avoid rollover time, and disable interval fuzzing
environment "ANKI_TEST_MODE", "1"

useJUnitPlatform()
testLogging {
events "failed", "skipped"
Expand Down

0 comments on commit d1e19af

Please sign in to comment.