From 4075b5bc273be57d8a95fef6def40305f3c6f7ea Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 9 Jul 2022 15:39:26 +1000 Subject: [PATCH] Add V3 scheduler tests 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. --- .../java/com/ichi2/libanki/CollectionV16.kt | 11 ++ .../com/ichi2/libanki/sched/SchedV2Test.kt | 115 ++++++++++++++---- .../com/ichi2/libanki/sched/SchedV3Test.kt | 22 ++++ build.gradle | 3 + 4 files changed, 130 insertions(+), 21 deletions(-) create mode 100644 AnkiDroid/src/test/java/com/ichi2/libanki/sched/SchedV3Test.kt diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/CollectionV16.kt b/AnkiDroid/src/main/java/com/ichi2/libanki/CollectionV16.kt index 5bb3d6986848..c398684554eb 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/CollectionV16.kt +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/CollectionV16.kt @@ -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) diff --git a/AnkiDroid/src/test/java/com/ichi2/libanki/sched/SchedV2Test.kt b/AnkiDroid/src/test/java/com/ichi2/libanki/sched/SchedV2Test.kt index a254c66c1164..7ea95d80e7e9 100644 --- a/AnkiDroid/src/test/java/com/ichi2/libanki/sched/SchedV2Test.kt +++ b/AnkiDroid/src/test/java/com/ichi2/libanki/sched/SchedV2Test.kt @@ -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 @@ -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 @@ -58,19 +61,34 @@ 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) // please wait for #11808 to be merged before starting cleanup @KotlinCleanup("fix ide lints and improve kotlin code where possible (eg `is`)") -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) @@ -187,7 +205,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( @@ -220,7 +238,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( @@ -253,8 +271,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))) @@ -266,6 +284,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", @@ -277,7 +296,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(), @@ -306,6 +325,10 @@ class SchedV2Test : RobolectricTest() { get() { val col = col col.changeSchedulerVer(2) + ifV3 { + assumeThat(defaultLegacySchema, `is`(false)) + col.newBackend.v3Enabled = true + } return col } @@ -425,6 +448,7 @@ class SchedV2Test : RobolectricTest() { @Test @Throws(Exception::class) fun test_learnV2() { + TimeManager.reset() val col = colV2 // add a note val note = col.newNote() @@ -444,7 +468,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))) @@ -459,7 +483,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()) @@ -476,7 +500,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) @@ -489,6 +513,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) @@ -601,7 +626,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)) @@ -734,6 +759,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")) @@ -950,7 +976,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) @@ -959,17 +986,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 @@ -1121,13 +1151,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' @@ -1152,7 +1187,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 @@ -1171,7 +1206,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) @@ -1185,7 +1220,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) @@ -1200,7 +1235,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)) @@ -1229,9 +1264,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) @@ -1242,7 +1283,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) @@ -1311,6 +1352,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") @@ -1334,6 +1378,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() { diff --git a/AnkiDroid/src/test/java/com/ichi2/libanki/sched/SchedV3Test.kt b/AnkiDroid/src/test/java/com/ichi2/libanki/sched/SchedV3Test.kt new file mode 100644 index 000000000000..5c037a48ff06 --- /dev/null +++ b/AnkiDroid/src/test/java/com/ichi2/libanki/sched/SchedV3Test.kt @@ -0,0 +1,22 @@ +/*************************************************************************************** + * Copyright (c) 2022 Ankitects Pty Ltd * + * * + * 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 . * + ****************************************************************************************/ + +package com.ichi2.libanki.sched + +/** Runs the tests in SchedV2Test, with v3 enabled */ +class SchedV3Test : SchedV2Test() { + override val v3 = true +} diff --git a/build.gradle b/build.gradle index a790a471b507..60d7d0d9cb2f 100644 --- a/build.gradle +++ b/build.gradle @@ -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"