anki/pylib/tests/test_schedv1.py
Damien Elmes 704b5e581a Rework reschedule tool
The old rescheduling dialog's two options have been split into two
separate menu items, "Forget", and "Set Due Date"

For cards that are not review cards, "Set Due Date" behaves like the
old reschedule option, changing the cards into a review card, and
and setting both the interval and due date to the provided number of
days.

When "Set Due Date" is applied to a review card, it no longer resets
the card's interval. Instead, it looks at how much the provided number
of days will change the original interval, and adjusts the interval by
that amount, so that cards that are answered earlier receive a smaller
next interval, and cards that are answered after a longer delay receive
a bonus.

For example, imagine a card was answered on day 5, and given an interval
of 10 days, so it has a due date of day 15.

- if on day 10 the due date is changed to day 12 (today+2), the card
is being scheduled 3 days earlier than it was supposed to be, so the
interval will be adjusted to 7 days.
- and if on day 10 the due date is changed to day 20, the interval will
be changed from 10 days to 15 days.

There is no separate option to reset the interval of a review card, but
it can be accomplished by forgetting the card(s), and then setting the
desired due date.

Other notes:

- Added the action to the review screen as well.
- Set the shortcut to Ctrl+Shift+D, and changed the existing Delete
Tags shortcut to Ctrl+Alt+Shift+A.
2021-02-07 21:57:51 +10:00

1126 lines
32 KiB
Python

# coding: utf-8
import copy
import time
from anki.consts import *
from anki.lang import without_unicode_isolation
from anki.utils import intTime
from tests.shared import getEmptyCol as getEmptyColOrig
def getEmptyCol():
col = getEmptyColOrig()
col.changeSchedulerVer(1)
return col
def test_clock():
col = getEmptyCol()
if (col.sched.dayCutoff - intTime()) < 10 * 60:
raise Exception("Unit tests will fail around the day rollover.")
def checkRevIvl(col, c, targetIvl):
min, max = col.sched._fuzzIvlRange(targetIvl)
return min <= c.ivl <= max
def test_basics():
col = getEmptyCol()
col.reset()
assert not col.sched.getCard()
def test_new():
col = getEmptyCol()
col.reset()
assert col.sched.newCount == 0
# add a note
note = col.newNote()
note["Front"] = "one"
note["Back"] = "two"
col.addNote(note)
col.reset()
assert col.sched.newCount == 1
# fetch it
c = col.sched.getCard()
assert c
assert c.queue == QUEUE_TYPE_NEW
assert c.type == CARD_TYPE_NEW
# if we answer it, it should become a learn card
t = intTime()
col.sched.answerCard(c, 1)
assert c.queue == QUEUE_TYPE_LRN
assert c.type == CARD_TYPE_LRN
assert c.due >= t
# disabled for now, as the learn fudging makes this randomly fail
# # the default order should ensure siblings are not seen together, and
# # should show all cards
# m = col.models.current(); mm = col.models
# t = mm.newTemplate("Reverse")
# t['qfmt'] = "{{Back}}"
# t['afmt'] = "{{Front}}"
# mm.addTemplate(m, t)
# mm.save(m)
# note = col.newNote()
# note['Front'] = u"2"; note['Back'] = u"2"
# col.addNote(note)
# note = col.newNote()
# note['Front'] = u"3"; note['Back'] = u"3"
# col.addNote(note)
# col.reset()
# qs = ("2", "3", "2", "3")
# for n in range(4):
# c = col.sched.getCard()
# assert qs[n] in c.q()
# col.sched.answerCard(c, 2)
def test_newLimits():
col = getEmptyCol()
# add some notes
deck2 = col.decks.id("Default::foo")
for i in range(30):
note = col.newNote()
note["Front"] = str(i)
if i > 4:
note.model()["did"] = deck2
col.addNote(note)
# give the child deck a different configuration
c2 = col.decks.add_config_returning_id("new conf")
col.decks.setConf(col.decks.get(deck2), c2)
col.reset()
# both confs have defaulted to a limit of 20
assert col.sched.newCount == 20
# first card we get comes from parent
c = col.sched.getCard()
assert c.did == 1
# limit the parent to 10 cards, meaning we get 10 in total
conf1 = col.decks.confForDid(1)
conf1["new"]["perDay"] = 10
col.decks.save(conf1)
col.reset()
assert col.sched.newCount == 10
# if we limit child to 4, we should get 9
conf2 = col.decks.confForDid(deck2)
conf2["new"]["perDay"] = 4
col.decks.save(conf2)
col.reset()
assert col.sched.newCount == 9
def test_newBoxes():
col = getEmptyCol()
note = col.newNote()
note["Front"] = "one"
col.addNote(note)
col.reset()
c = col.sched.getCard()
conf = col.sched._cardConf(c)
conf["new"]["delays"] = [1, 2, 3, 4, 5]
col.decks.save(conf)
col.sched.answerCard(c, 2)
# should handle gracefully
conf["new"]["delays"] = [1]
col.decks.save(conf)
col.sched.answerCard(c, 2)
def test_learn():
col = getEmptyCol()
# add a note
note = col.newNote()
note["Front"] = "one"
note["Back"] = "two"
col.addNote(note)
# set as a new card and rebuild queues
col.db.execute(f"update cards set queue={QUEUE_TYPE_NEW}, type={CARD_TYPE_NEW}")
col.reset()
# sched.getCard should return it, since it's due in the past
c = col.sched.getCard()
assert c
conf = col.sched._cardConf(c)
conf["new"]["delays"] = [0.5, 3, 10]
col.decks.save(conf)
# fail it
col.sched.answerCard(c, 1)
# it should have three reps left to graduation
assert c.left % 1000 == 3
assert c.left // 1000 == 3
# it should be due in 30 seconds
t = round(c.due - time.time())
assert t >= 25 and t <= 40
# pass it once
col.sched.answerCard(c, 2)
# it should be due in 3 minutes
assert round(c.due - time.time()) in (179, 180)
assert c.left % 1000 == 2
assert c.left // 1000 == 2
# check log is accurate
log = col.db.first("select * from revlog order by id desc")
assert log[3] == 2
assert log[4] == -180
assert log[5] == -30
# pass again
col.sched.answerCard(c, 2)
# it should be due in 10 minutes
assert round(c.due - time.time()) in (599, 600)
assert c.left % 1000 == 1
assert c.left // 1000 == 1
# the next pass should graduate the card
assert c.queue == QUEUE_TYPE_LRN
assert c.type == CARD_TYPE_LRN
col.sched.answerCard(c, 2)
assert c.queue == QUEUE_TYPE_REV
assert c.type == CARD_TYPE_REV
# should be due tomorrow, with an interval of 1
assert c.due == col.sched.today + 1
assert c.ivl == 1
# or normal removal
c.type = CARD_TYPE_NEW
c.queue = QUEUE_TYPE_LRN
col.sched.answerCard(c, 3)
assert c.type == CARD_TYPE_REV
assert c.queue == QUEUE_TYPE_REV
assert checkRevIvl(col, c, 4)
# revlog should have been updated each time
assert col.db.scalar("select count() from revlog where type = 0") == 5
# now failed card handling
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_LRN
c.odue = 123
col.sched.answerCard(c, 3)
assert c.due == 123
assert c.type == CARD_TYPE_REV
assert c.queue == QUEUE_TYPE_REV
# we should be able to remove manually, too
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_LRN
c.odue = 321
c.flush()
col.sched.removeLrn()
c.load()
assert c.queue == QUEUE_TYPE_REV
assert c.due == 321
def test_learn_collapsed():
col = getEmptyCol()
# add 2 notes
note = col.newNote()
note["Front"] = "1"
col.addNote(note)
note = col.newNote()
note["Front"] = "2"
col.addNote(note)
# set as a new card and rebuild queues
col.db.execute(f"update cards set queue={QUEUE_TYPE_NEW}, type={CARD_TYPE_NEW}")
col.reset()
# should get '1' first
c = col.sched.getCard()
assert c.q().endswith("1")
# pass it so it's due in 10 minutes
col.sched.answerCard(c, 2)
# get the other card
c = col.sched.getCard()
assert c.q().endswith("2")
# fail it so it's due in 1 minute
col.sched.answerCard(c, 1)
# we shouldn't get the same card again
c = col.sched.getCard()
assert not c.q().endswith("2")
def test_learn_day():
col = getEmptyCol()
# add a note
note = col.newNote()
note["Front"] = "one"
col.addNote(note)
col.sched.reset()
c = col.sched.getCard()
conf = col.sched._cardConf(c)
conf["new"]["delays"] = [1, 10, 1440, 2880]
col.decks.save(conf)
# pass it
col.sched.answerCard(c, 2)
# two reps to graduate, 1 more today
assert c.left % 1000 == 3
assert c.left // 1000 == 1
assert col.sched.counts() == (0, 1, 0)
c = col.sched.getCard()
ni = col.sched.nextIvl
assert ni(c, 2) == 86400
# answering it will place it in queue 3
col.sched.answerCard(c, 2)
assert c.due == col.sched.today + 1
assert c.queue == CARD_TYPE_RELEARNING
assert not col.sched.getCard()
# for testing, move it back a day
c.due -= 1
c.flush()
col.reset()
assert col.sched.counts() == (0, 1, 0)
c = col.sched.getCard()
# nextIvl should work
assert ni(c, 2) == 86400 * 2
# if we fail it, it should be back in the correct queue
col.sched.answerCard(c, 1)
assert c.queue == QUEUE_TYPE_LRN
col.undo()
col.reset()
c = col.sched.getCard()
col.sched.answerCard(c, 2)
# simulate the passing of another two days
c.due -= 2
c.flush()
col.reset()
# the last pass should graduate it into a review card
assert ni(c, 2) == 86400
col.sched.answerCard(c, 2)
assert c.queue == CARD_TYPE_REV and c.type == QUEUE_TYPE_REV
# if the lapse step is tomorrow, failing it should handle the counts
# correctly
c.due = 0
c.flush()
col.reset()
assert col.sched.counts() == (0, 0, 1)
conf = col.sched._cardConf(c)
conf["lapse"]["delays"] = [1440]
col.decks.save(conf)
c = col.sched.getCard()
col.sched.answerCard(c, 1)
assert c.queue == CARD_TYPE_RELEARNING
assert col.sched.counts() == (0, 0, 0)
def test_reviews():
col = getEmptyCol()
# add a note
note = col.newNote()
note["Front"] = "one"
note["Back"] = "two"
col.addNote(note)
# set the card up as a review card, due 8 days ago
c = note.cards()[0]
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.due = col.sched.today - 8
c.factor = STARTING_FACTOR
c.reps = 3
c.lapses = 1
c.ivl = 100
c.startTimer()
c.flush()
# save it for later use as well
cardcopy = copy.copy(c)
# failing it should put it in the learn queue with the default options
##################################################
# different delay to new
col.reset()
conf = col.sched._cardConf(c)
conf["lapse"]["delays"] = [2, 20]
col.decks.save(conf)
col.sched.answerCard(c, 1)
assert c.queue == QUEUE_TYPE_LRN
# it should be due tomorrow, with an interval of 1
assert c.odue == col.sched.today + 1
assert c.ivl == 1
# but because it's in the learn queue, its current due time should be in
# the future
assert c.due >= time.time()
assert (c.due - time.time()) > 118
# factor should have been decremented
assert c.factor == 2300
# check counters
assert c.lapses == 2
assert c.reps == 4
# check ests.
ni = col.sched.nextIvl
assert ni(c, 1) == 120
assert ni(c, 2) == 20 * 60
# try again with an ease of 2 instead
##################################################
c = copy.copy(cardcopy)
c.flush()
col.sched.answerCard(c, 2)
assert c.queue == QUEUE_TYPE_REV
# the new interval should be (100 + 8/4) * 1.2 = 122
assert checkRevIvl(col, c, 122)
assert c.due == col.sched.today + c.ivl
# factor should have been decremented
assert c.factor == 2350
# check counters
assert c.lapses == 1
assert c.reps == 4
# ease 3
##################################################
c = copy.copy(cardcopy)
c.flush()
col.sched.answerCard(c, 3)
# the new interval should be (100 + 8/2) * 2.5 = 260
assert checkRevIvl(col, c, 260)
assert c.due == col.sched.today + c.ivl
# factor should have been left alone
assert c.factor == STARTING_FACTOR
# ease 4
##################################################
c = copy.copy(cardcopy)
c.flush()
col.sched.answerCard(c, 4)
# the new interval should be (100 + 8) * 2.5 * 1.3 = 351
assert checkRevIvl(col, c, 351)
assert c.due == col.sched.today + c.ivl
# factor should have been increased
assert c.factor == 2650
def test_button_spacing():
col = getEmptyCol()
note = col.newNote()
note["Front"] = "one"
col.addNote(note)
# 1 day ivl review card due now
c = note.cards()[0]
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.due = col.sched.today
c.reps = 1
c.ivl = 1
c.startTimer()
c.flush()
col.reset()
ni = col.sched.nextIvlStr
wo = without_unicode_isolation
assert wo(ni(c, 2)) == "2d"
assert wo(ni(c, 3)) == "3d"
assert wo(ni(c, 4)) == "4d"
def test_overdue_lapse():
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2
return
col = getEmptyCol() # pylint: disable=unreachable
# add a note
note = col.newNote()
note["Front"] = "one"
col.addNote(note)
# simulate a review that was lapsed and is now due for its normal review
c = note.cards()[0]
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_LRN
c.due = -1
c.odue = -1
c.factor = STARTING_FACTOR
c.left = 2002
c.ivl = 0
c.flush()
# checkpoint
col.save()
col.sched.reset()
assert col.sched.counts() == (0, 2, 0)
c = col.sched.getCard()
col.sched.answerCard(c, 3)
# it should be due tomorrow
assert c.due == col.sched.today + 1
# revert to before
col.rollback()
# with the default settings, the overdue card should be removed from the
# learning queue
col.sched.reset()
assert col.sched.counts() == (0, 0, 1)
def test_nextIvl():
col = getEmptyCol()
note = col.newNote()
note["Front"] = "one"
note["Back"] = "two"
col.addNote(note)
col.reset()
conf = col.decks.confForDid(1)
conf["new"]["delays"] = [0.5, 3, 10]
conf["lapse"]["delays"] = [1, 5, 9]
col.decks.save(conf)
c = col.sched.getCard()
# new cards
##################################################
ni = col.sched.nextIvl
assert ni(c, 1) == 30
assert ni(c, 2) == 180
assert ni(c, 3) == 4 * 86400
col.sched.answerCard(c, 1)
# cards in learning
##################################################
assert ni(c, 1) == 30
assert ni(c, 2) == 180
assert ni(c, 3) == 4 * 86400
col.sched.answerCard(c, 2)
assert ni(c, 1) == 30
assert ni(c, 2) == 600
assert ni(c, 3) == 4 * 86400
col.sched.answerCard(c, 2)
# normal graduation is tomorrow
assert ni(c, 2) == 1 * 86400
assert ni(c, 3) == 4 * 86400
# lapsed cards
##################################################
c.type = CARD_TYPE_REV
c.ivl = 100
c.factor = STARTING_FACTOR
assert ni(c, 1) == 60
assert ni(c, 2) == 100 * 86400
assert ni(c, 3) == 100 * 86400
# review cards
##################################################
c.queue = QUEUE_TYPE_REV
c.ivl = 100
c.factor = STARTING_FACTOR
# failing it should put it at 60s
assert ni(c, 1) == 60
# or 1 day if relearn is false
conf["lapse"]["delays"] = []
col.decks.save(conf)
assert ni(c, 1) == 1 * 86400
# (* 100 1.2 86400)10368000.0
assert ni(c, 2) == 10368000
# (* 100 2.5 86400)21600000.0
assert ni(c, 3) == 21600000
# (* 100 2.5 1.3 86400)28080000.0
assert ni(c, 4) == 28080000
assert without_unicode_isolation(col.sched.nextIvlStr(c, 4)) == "10.8mo"
def test_misc():
col = getEmptyCol()
note = col.newNote()
note["Front"] = "one"
col.addNote(note)
c = note.cards()[0]
# burying
col.sched.bury_note(note)
col.reset()
assert not col.sched.getCard()
col.sched.unbury_cards_in_current_deck()
col.reset()
assert col.sched.getCard()
def test_suspend():
col = getEmptyCol()
note = col.newNote()
note["Front"] = "one"
col.addNote(note)
c = note.cards()[0]
# suspending
col.reset()
assert col.sched.getCard()
col.sched.suspend_cards([c.id])
col.reset()
assert not col.sched.getCard()
# unsuspending
col.sched.unsuspend_cards([c.id])
col.reset()
assert col.sched.getCard()
# should cope with rev cards being relearnt
c.due = 0
c.ivl = 100
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.flush()
col.reset()
c = col.sched.getCard()
col.sched.answerCard(c, 1)
assert c.due >= time.time()
assert c.queue == QUEUE_TYPE_LRN
assert c.type == CARD_TYPE_REV
col.sched.suspend_cards([c.id])
col.sched.unsuspend_cards([c.id])
c.load()
assert c.queue == QUEUE_TYPE_REV
assert c.type == CARD_TYPE_REV
assert c.due == 1
# should cope with cards in cram decks
c.due = 1
c.flush()
did = col.decks.new_filtered("tmp")
col.sched.rebuild_filtered_deck(did)
c.load()
assert c.due != 1
assert c.did != 1
col.sched.suspend_cards([c.id])
c.load()
assert c.due == 1
assert c.did == 1
def test_cram():
col = getEmptyCol()
note = col.newNote()
note["Front"] = "one"
col.addNote(note)
c = note.cards()[0]
c.ivl = 100
c.queue = CARD_TYPE_REV
c.type = QUEUE_TYPE_REV
# due in 25 days, so it's been waiting 75 days
c.due = col.sched.today + 25
c.mod = 1
c.factor = STARTING_FACTOR
c.startTimer()
c.flush()
col.reset()
assert col.sched.counts() == (0, 0, 0)
cardcopy = copy.copy(c)
# create a dynamic deck and refresh it
did = col.decks.new_filtered("Cram")
col.sched.rebuild_filtered_deck(did)
col.reset()
# should appear as new in the deck list
assert sorted(col.sched.deck_due_tree().children)[0].new_count == 1
# and should appear in the counts
assert col.sched.counts() == (1, 0, 0)
# grab it and check estimates
c = col.sched.getCard()
assert col.sched.answerButtons(c) == 2
assert col.sched.nextIvl(c, 1) == 600
assert col.sched.nextIvl(c, 2) == 138 * 60 * 60 * 24
cram = col.decks.get(did)
cram["delays"] = [1, 10]
col.decks.save(cram)
assert col.sched.answerButtons(c) == 3
assert col.sched.nextIvl(c, 1) == 60
assert col.sched.nextIvl(c, 2) == 600
assert col.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24
col.sched.answerCard(c, 2)
# elapsed time was 75 days
# factor = 2.5+1.2/2 = 1.85
# int(75*1.85) = 138
assert c.ivl == 138
assert c.odue == 138
assert c.queue == QUEUE_TYPE_LRN
# should be logged as a cram rep
assert col.db.scalar("select type from revlog order by id desc limit 1") == 3
# check ivls again
assert col.sched.nextIvl(c, 1) == 60
assert col.sched.nextIvl(c, 2) == 138 * 60 * 60 * 24
assert col.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24
# when it graduates, due is updated
c = col.sched.getCard()
col.sched.answerCard(c, 2)
assert c.ivl == 138
assert c.due == 138
assert c.queue == QUEUE_TYPE_REV
# and it will have moved back to the previous deck
assert c.did == 1
# cram the deck again
col.sched.rebuild_filtered_deck(did)
col.reset()
c = col.sched.getCard()
# check ivls again - passing should be idempotent
assert col.sched.nextIvl(c, 1) == 60
assert col.sched.nextIvl(c, 2) == 600
assert col.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24
col.sched.answerCard(c, 2)
assert c.ivl == 138
assert c.odue == 138
# fail
col.sched.answerCard(c, 1)
assert col.sched.nextIvl(c, 1) == 60
assert col.sched.nextIvl(c, 2) == 600
assert col.sched.nextIvl(c, 3) == 86400
# delete the deck, returning the card mid-study
col.decks.rem(col.decks.selected())
assert len(col.sched.deck_due_tree().children) == 1
c.load()
assert c.ivl == 1
assert c.due == col.sched.today + 1
# make it due
col.reset()
assert col.sched.counts() == (0, 0, 0)
c.due = -5
c.ivl = 100
c.flush()
col.reset()
assert col.sched.counts() == (0, 0, 1)
# cram again
did = col.decks.new_filtered("Cram")
col.sched.rebuild_filtered_deck(did)
col.reset()
assert col.sched.counts() == (0, 0, 1)
c.load()
assert col.sched.answerButtons(c) == 4
# add a sibling so we can test minSpace, etc
c.col = None
c2 = copy.deepcopy(c)
c2.col = c.col = col
c2.id = 0
c2.ord = 1
c2.due = 325
c2.flush()
# should be able to answer it
c = col.sched.getCard()
col.sched.answerCard(c, 4)
# it should have been moved back to the original deck
assert c.did == 1
def test_cram_rem():
col = getEmptyCol()
note = col.newNote()
note["Front"] = "one"
col.addNote(note)
oldDue = note.cards()[0].due
did = col.decks.new_filtered("Cram")
col.sched.rebuild_filtered_deck(did)
col.reset()
c = col.sched.getCard()
col.sched.answerCard(c, 2)
# answering the card will put it in the learning queue
assert c.type == CARD_TYPE_LRN and c.queue == QUEUE_TYPE_LRN
assert c.due != oldDue
# if we terminate cramming prematurely it should be set back to new
col.sched.empty_filtered_deck(did)
c.load()
assert c.type == CARD_TYPE_NEW and c.queue == QUEUE_TYPE_NEW
assert c.due == oldDue
def test_cram_resched():
# add card
col = getEmptyCol()
note = col.newNote()
note["Front"] = "one"
col.addNote(note)
# cram deck
did = col.decks.new_filtered("Cram")
cram = col.decks.get(did)
cram["resched"] = False
col.decks.save(cram)
col.sched.rebuild_filtered_deck(did)
col.reset()
# graduate should return it to new
c = col.sched.getCard()
ni = col.sched.nextIvl
assert ni(c, 1) == 60
assert ni(c, 2) == 600
assert ni(c, 3) == 0
assert col.sched.nextIvlStr(c, 3) == "(end)"
col.sched.answerCard(c, 3)
assert c.type == CARD_TYPE_NEW and c.queue == QUEUE_TYPE_NEW
# undue reviews should also be unaffected
c.ivl = 100
c.queue = CARD_TYPE_REV
c.type = QUEUE_TYPE_REV
c.due = col.sched.today + 25
c.factor = STARTING_FACTOR
c.flush()
cardcopy = copy.copy(c)
col.sched.rebuild_filtered_deck(did)
col.reset()
c = col.sched.getCard()
assert ni(c, 1) == 600
assert ni(c, 2) == 0
assert ni(c, 3) == 0
col.sched.answerCard(c, 2)
assert c.ivl == 100
assert c.due == col.sched.today + 25
# check failure too
c = cardcopy
c.flush()
col.sched.rebuild_filtered_deck(did)
col.reset()
c = col.sched.getCard()
col.sched.answerCard(c, 1)
col.sched.empty_filtered_deck(did)
c.load()
assert c.ivl == 100
assert c.due == col.sched.today + 25
# fail+grad early
c = cardcopy
c.flush()
col.sched.rebuild_filtered_deck(did)
col.reset()
c = col.sched.getCard()
col.sched.answerCard(c, 1)
col.sched.answerCard(c, 3)
col.sched.empty_filtered_deck(did)
c.load()
assert c.ivl == 100
assert c.due == col.sched.today + 25
# due cards - pass
c = cardcopy
c.due = -25
c.flush()
col.sched.rebuild_filtered_deck(did)
col.reset()
c = col.sched.getCard()
col.sched.answerCard(c, 3)
col.sched.empty_filtered_deck(did)
c.load()
assert c.ivl == 100
assert c.due == -25
# fail
c = cardcopy
c.due = -25
c.flush()
col.sched.rebuild_filtered_deck(did)
col.reset()
c = col.sched.getCard()
col.sched.answerCard(c, 1)
col.sched.empty_filtered_deck(did)
c.load()
assert c.ivl == 100
assert c.due == -25
# fail with normal grad
c = cardcopy
c.due = -25
c.flush()
col.sched.rebuild_filtered_deck(did)
col.reset()
c = col.sched.getCard()
col.sched.answerCard(c, 1)
col.sched.answerCard(c, 3)
c.load()
assert c.ivl == 100
assert c.due == -25
# lapsed card pulled into cram
# col.sched._cardConf(c)['lapse']['mult']=0.5
# col.sched.answerCard(c, 1)
# col.sched.rebuild_filtered_deck(did)
# col.reset()
# c = col.sched.getCard()
# col.sched.answerCard(c, 2)
# print c.__dict__
def test_ordcycle():
col = getEmptyCol()
# add two more templates and set second active
m = col.models.current()
mm = col.models
t = mm.newTemplate("Reverse")
t["qfmt"] = "{{Back}}"
t["afmt"] = "{{Front}}"
mm.addTemplate(m, t)
t = mm.newTemplate("f2")
t["qfmt"] = "{{Front}}"
t["afmt"] = "{{Back}}"
mm.addTemplate(m, t)
mm.save(m)
# create a new note; it should have 3 cards
note = col.newNote()
note["Front"] = "1"
note["Back"] = "1"
col.addNote(note)
assert col.cardCount() == 3
col.reset()
# ordinals should arrive in order
assert col.sched.getCard().ord == 0
assert col.sched.getCard().ord == 1
assert col.sched.getCard().ord == 2
def test_counts_idx():
col = getEmptyCol()
note = col.newNote()
note["Front"] = "one"
note["Back"] = "two"
col.addNote(note)
col.reset()
assert col.sched.counts() == (1, 0, 0)
c = col.sched.getCard()
# counter's been decremented but idx indicates 1
assert col.sched.counts() == (0, 0, 0)
assert col.sched.countIdx(c) == 0
# answer to move to learn queue
col.sched.answerCard(c, 1)
assert col.sched.counts() == (0, 2, 0)
# fetching again will decrement the count
c = col.sched.getCard()
assert col.sched.counts() == (0, 0, 0)
assert col.sched.countIdx(c) == 1
# answering should add it back again
col.sched.answerCard(c, 1)
assert col.sched.counts() == (0, 2, 0)
def test_repCounts():
col = getEmptyCol()
note = col.newNote()
note["Front"] = "one"
col.addNote(note)
col.reset()
# lrnReps should be accurate on pass/fail
assert col.sched.counts() == (1, 0, 0)
col.sched.answerCard(col.sched.getCard(), 1)
assert col.sched.counts() == (0, 2, 0)
col.sched.answerCard(col.sched.getCard(), 1)
assert col.sched.counts() == (0, 2, 0)
col.sched.answerCard(col.sched.getCard(), 2)
assert col.sched.counts() == (0, 1, 0)
col.sched.answerCard(col.sched.getCard(), 1)
assert col.sched.counts() == (0, 2, 0)
col.sched.answerCard(col.sched.getCard(), 2)
assert col.sched.counts() == (0, 1, 0)
col.sched.answerCard(col.sched.getCard(), 2)
assert col.sched.counts() == (0, 0, 0)
note = col.newNote()
note["Front"] = "two"
col.addNote(note)
col.reset()
# initial pass should be correct too
col.sched.answerCard(col.sched.getCard(), 2)
assert col.sched.counts() == (0, 1, 0)
col.sched.answerCard(col.sched.getCard(), 1)
assert col.sched.counts() == (0, 2, 0)
col.sched.answerCard(col.sched.getCard(), 3)
assert col.sched.counts() == (0, 0, 0)
# immediate graduate should work
note = col.newNote()
note["Front"] = "three"
col.addNote(note)
col.reset()
col.sched.answerCard(col.sched.getCard(), 3)
assert col.sched.counts() == (0, 0, 0)
# and failing a review should too
note = col.newNote()
note["Front"] = "three"
col.addNote(note)
c = note.cards()[0]
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.due = col.sched.today
c.flush()
col.reset()
assert col.sched.counts() == (0, 0, 1)
col.sched.answerCard(col.sched.getCard(), 1)
assert col.sched.counts() == (0, 1, 0)
def test_timing():
col = getEmptyCol()
# add a few review cards, due today
for i in range(5):
note = col.newNote()
note["Front"] = "num" + str(i)
col.addNote(note)
c = note.cards()[0]
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.due = 0
c.flush()
# fail the first one
col.reset()
c = col.sched.getCard()
# set a a fail delay of 4 seconds
conf = col.sched._cardConf(c)
conf["lapse"]["delays"][0] = 1 / 15.0
col.decks.save(conf)
col.sched.answerCard(c, 1)
# the next card should be another review
c = col.sched.getCard()
assert c.queue == QUEUE_TYPE_REV
# but if we wait for a few seconds, the failed card should come back
orig_time = time.time
def adjusted_time():
return orig_time() + 5
time.time = adjusted_time
c = col.sched.getCard()
assert c.queue == QUEUE_TYPE_LRN
time.time = orig_time
def test_collapse():
col = getEmptyCol()
# add a note
note = col.newNote()
note["Front"] = "one"
col.addNote(note)
col.reset()
# test collapsing
c = col.sched.getCard()
col.sched.answerCard(c, 1)
c = col.sched.getCard()
col.sched.answerCard(c, 3)
assert not col.sched.getCard()
def test_deckDue():
col = getEmptyCol()
# add a note with default deck
note = col.newNote()
note["Front"] = "one"
col.addNote(note)
# and one that's a child
note = col.newNote()
note["Front"] = "two"
default1 = note.model()["did"] = col.decks.id("Default::1")
col.addNote(note)
# make it a review card
c = note.cards()[0]
c.queue = QUEUE_TYPE_REV
c.due = 0
c.flush()
# add one more with a new deck
note = col.newNote()
note["Front"] = "two"
note.model()["did"] = col.decks.id("foo::bar")
col.addNote(note)
# and one that's a sibling
note = col.newNote()
note["Front"] = "three"
note.model()["did"] = col.decks.id("foo::baz")
col.addNote(note)
col.reset()
assert len(col.decks.all_names_and_ids()) == 5
tree = col.sched.deck_due_tree().children
assert tree[0].name == "Default"
# sum of child and parent
assert tree[0].deck_id == 1
assert tree[0].review_count == 1
assert tree[0].new_count == 1
# child count is just review
child = tree[0].children[0]
assert child.name == "1"
assert child.deck_id == default1
assert child.review_count == 1
assert child.new_count == 0
# code should not fail if a card has an invalid deck
c.did = 12345
c.flush()
col.sched.deck_due_tree()
def test_deckFlow():
col = getEmptyCol()
# add a note with default deck
note = col.newNote()
note["Front"] = "one"
col.addNote(note)
# and one that's a child
note = col.newNote()
note["Front"] = "two"
note.model()["did"] = col.decks.id("Default::2")
col.addNote(note)
# and another that's higher up
note = col.newNote()
note["Front"] = "three"
default1 = note.model()["did"] = col.decks.id("Default::1")
col.addNote(note)
# should get top level one first, then ::1, then ::2
col.reset()
assert col.sched.counts() == (3, 0, 0)
for i in "one", "three", "two":
c = col.sched.getCard()
assert c.note()["Front"] == i
col.sched.answerCard(c, 2)
def test_reorder():
col = getEmptyCol()
# add a note with default deck
note = col.newNote()
note["Front"] = "one"
col.addNote(note)
note2 = col.newNote()
note2["Front"] = "two"
col.addNote(note2)
assert note2.cards()[0].due == 2
found = False
# 50/50 chance of being reordered
for i in range(20):
col.sched.randomizeCards(1)
if note.cards()[0].due != note.id:
found = True
break
assert found
col.sched.orderCards(1)
assert note.cards()[0].due == 1
# shifting
note3 = col.newNote()
note3["Front"] = "three"
col.addNote(note3)
note4 = col.newNote()
note4["Front"] = "four"
col.addNote(note4)
assert note.cards()[0].due == 1
assert note2.cards()[0].due == 2
assert note3.cards()[0].due == 3
assert note4.cards()[0].due == 4
col.sched.sortCards([note3.cards()[0].id, note4.cards()[0].id], start=1, shift=True)
assert note.cards()[0].due == 3
assert note2.cards()[0].due == 4
assert note3.cards()[0].due == 1
assert note4.cards()[0].due == 2
def test_forget():
col = getEmptyCol()
note = col.newNote()
note["Front"] = "one"
col.addNote(note)
c = note.cards()[0]
c.queue = QUEUE_TYPE_REV
c.type = CARD_TYPE_REV
c.ivl = 100
c.due = 0
c.flush()
col.reset()
assert col.sched.counts() == (0, 0, 1)
col.sched.forgetCards([c.id])
col.reset()
assert col.sched.counts() == (1, 0, 0)
def test_norelearn():
col = getEmptyCol()
# add a note
note = col.newNote()
note["Front"] = "one"
col.addNote(note)
c = note.cards()[0]
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.due = 0
c.factor = STARTING_FACTOR
c.reps = 3
c.lapses = 1
c.ivl = 100
c.startTimer()
c.flush()
col.reset()
col.sched.answerCard(c, 1)
col.sched._cardConf(c)["lapse"]["delays"] = []
col.sched.answerCard(c, 1)
def test_failmult():
col = getEmptyCol()
note = col.newNote()
note["Front"] = "one"
note["Back"] = "two"
col.addNote(note)
c = note.cards()[0]
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.ivl = 100
c.due = col.sched.today - c.ivl
c.factor = STARTING_FACTOR
c.reps = 3
c.lapses = 1
c.startTimer()
c.flush()
conf = col.sched._cardConf(c)
conf["lapse"]["mult"] = 0.5
col.decks.save(conf)
c = col.sched.getCard()
col.sched.answerCard(c, 1)
assert c.ivl == 50
col.sched.answerCard(c, 1)
assert c.ivl == 25