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 ef0424b0f328..5557d1afabcc 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,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) @@ -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( @@ -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( @@ -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))) @@ -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", @@ -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(), @@ -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 } @@ -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() @@ -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))) @@ -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()) @@ -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) @@ -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) @@ -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)) @@ -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")) @@ -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) @@ -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 @@ -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' @@ -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 @@ -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) @@ -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) @@ -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)) @@ -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) @@ -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) @@ -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") @@ -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() { 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"