mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00

- drop lrnCount; rename lrnRepCount to lrnCount - on card fetch, decr count by card.left - drop cardCounts(), rename repCounts() to just counts() - fix lrn count bugs
902 lines
25 KiB
Python
902 lines
25 KiB
Python
# coding: utf-8
|
|
|
|
import time, copy
|
|
from tests.shared import assertException, getEmptyDeck
|
|
from anki.utils import stripHTML, intTime
|
|
from anki.hooks import addHook
|
|
from anki.consts import *
|
|
|
|
def test_basics():
|
|
d = getEmptyDeck()
|
|
d.reset()
|
|
assert not d.sched.getCard()
|
|
|
|
def test_new():
|
|
d = getEmptyDeck()
|
|
d.reset()
|
|
assert d.sched.newCount == 0
|
|
# add a note
|
|
f = d.newNote()
|
|
f['Front'] = u"one"; f['Back'] = u"two"
|
|
d.addNote(f)
|
|
d.reset()
|
|
assert d.sched.newCount == 1
|
|
# fetch it
|
|
c = d.sched.getCard()
|
|
assert c
|
|
assert c.queue == 0
|
|
assert c.type == 0
|
|
# if we answer it, it should become a learn card
|
|
t = intTime()
|
|
d.sched.answerCard(c, 1)
|
|
assert c.queue == 1
|
|
assert c.type == 1
|
|
assert c.due >= t
|
|
# the default order should ensure siblings are not seen together, and
|
|
# should show all cards
|
|
m = d.models.current(); mm = d.models
|
|
t = mm.newTemplate("Reverse")
|
|
t['qfmt'] = "{{Back}}"
|
|
t['afmt'] = "{{Front}}"
|
|
mm.addTemplate(m, t)
|
|
mm.save(m)
|
|
f = d.newNote()
|
|
f['Front'] = u"2"; f['Back'] = u"2"
|
|
d.addNote(f)
|
|
f = d.newNote()
|
|
f['Front'] = u"3"; f['Back'] = u"3"
|
|
d.addNote(f)
|
|
d.reset()
|
|
qs = ("2", "3", "2", "3")
|
|
for n in range(4):
|
|
c = d.sched.getCard()
|
|
assert(stripHTML(c.q()) == qs[n])
|
|
d.sched.answerCard(c, 2)
|
|
|
|
def test_newLimits():
|
|
d = getEmptyDeck()
|
|
# add some notes
|
|
g2 = d.decks.id("Default::foo")
|
|
for i in range(30):
|
|
f = d.newNote()
|
|
f['Front'] = str(i)
|
|
if i > 4:
|
|
f.did = g2
|
|
d.addNote(f)
|
|
# give the child deck a different configuration
|
|
c2 = d.decks.confId("new conf")
|
|
d.decks.setConf(d.decks.get(g2), c2)
|
|
d.reset()
|
|
# both confs have defaulted to a limit of 20
|
|
assert d.sched.newCount == 20
|
|
# first card we get comes from parent
|
|
c = d.sched.getCard()
|
|
assert c.did == 1
|
|
# limit the parent to 10 cards, meaning we get 10 in total
|
|
conf1 = d.decks.conf(1)
|
|
conf1['new']['perDay'] = 10
|
|
d.reset()
|
|
assert d.sched.newCount == 10
|
|
# if we limit child to 4, we should get 9
|
|
conf2 = d.decks.conf(g2)
|
|
conf2['new']['perDay'] = 4
|
|
d.reset()
|
|
assert d.sched.newCount == 9
|
|
|
|
def test_newOrder():
|
|
d = getEmptyDeck()
|
|
m = d.models.current()
|
|
for i in range(50):
|
|
t = d.models.newTemplate(m)
|
|
t['name'] = str(i)
|
|
t['qfmt'] = "{{Front}}"
|
|
t['afmt'] = "{{Back}}"
|
|
t['actv'] = i > 25
|
|
d.models.addTemplate(m, t)
|
|
d.models.save(m)
|
|
f = d.newNote()
|
|
f['Front'] = u'1'
|
|
f['Back'] = u'2'
|
|
# add first half
|
|
d.addNote(f)
|
|
# generate second half
|
|
d.db.execute("update cards set did = random()")
|
|
d.conf['newPerDay'] = 100
|
|
d.reset()
|
|
# cards should be sorted by id
|
|
assert d.sched._newQueue == list(reversed(sorted(d.sched._newQueue)))
|
|
|
|
def test_newBoxes():
|
|
d = getEmptyDeck()
|
|
f = d.newNote()
|
|
f['Front'] = u"one"
|
|
d.addNote(f)
|
|
d.reset()
|
|
c = d.sched.getCard()
|
|
d.sched._cardConf(c)['new']['delays'] = [1,2,3,4,5]
|
|
d.sched.answerCard(c, 2)
|
|
# should handle gracefully
|
|
d.sched._cardConf(c)['new']['delays'] = [1]
|
|
d.sched.answerCard(c, 2)
|
|
|
|
def test_learn():
|
|
d = getEmptyDeck()
|
|
# add a note
|
|
f = d.newNote()
|
|
f['Front'] = u"one"; f['Back'] = u"two"
|
|
f = d.addNote(f)
|
|
# set as a learn card and rebuild queues
|
|
d.db.execute("update cards set queue=0, type=0")
|
|
d.reset()
|
|
# sched.getCard should return it, since it's due in the past
|
|
c = d.sched.getCard()
|
|
assert c
|
|
d.sched._cardConf(c)['new']['delays'] = [0.5, 3, 10]
|
|
# fail it
|
|
d.sched.answerCard(c, 1)
|
|
# it should have three reps left to graduation
|
|
assert c.left == 3
|
|
# it should by due in 30 seconds
|
|
t = round(c.due - time.time())
|
|
assert t >= 25 and t <= 40
|
|
# pass it once
|
|
d.sched.answerCard(c, 2)
|
|
# it should by due in 3 minutes
|
|
assert round(c.due - time.time()) in (179, 180)
|
|
assert c.left == 2
|
|
# check log is accurate
|
|
log = d.db.first("select * from revlog order by id desc")
|
|
assert log[3] == 2
|
|
assert log[4] == -180
|
|
assert log[5] == -30
|
|
# pass again
|
|
d.sched.answerCard(c, 2)
|
|
# it should by due in 10 minutes
|
|
assert round(c.due - time.time()) in (599, 600)
|
|
assert c.left == 1
|
|
# the next pass should graduate the card
|
|
assert c.queue == 1
|
|
assert c.type == 1
|
|
d.sched.answerCard(c, 2)
|
|
assert c.queue == 2
|
|
assert c.type == 2
|
|
# should be due tomorrow, with an interval of 1
|
|
assert c.due == d.sched.today+1
|
|
assert c.ivl == 1
|
|
# or normal removal
|
|
c.type = 0
|
|
c.queue = 1
|
|
c.cycles = 1
|
|
d.sched.answerCard(c, 3)
|
|
assert c.type == 2
|
|
assert c.queue == 2
|
|
assert c.ivl == 4
|
|
# revlog should have been updated each time
|
|
d.db.scalar("select count() from revlog where type = 0") == 6
|
|
# now failed card handling
|
|
c.type = 2
|
|
c.queue = 1
|
|
c.edue = 123
|
|
d.sched.answerCard(c, 3)
|
|
assert c.due == 123
|
|
assert c.type == 2
|
|
assert c.queue == 2
|
|
# we should be able to remove manually, too
|
|
c.type = 2
|
|
c.queue = 1
|
|
c.edue = 321
|
|
c.flush()
|
|
d.sched.removeFailed()
|
|
c.load()
|
|
assert c.queue == 2
|
|
assert c.due == 321
|
|
|
|
def test_reviews():
|
|
d = getEmptyDeck()
|
|
# add a note
|
|
f = d.newNote()
|
|
f['Front'] = u"one"; f['Back'] = u"two"
|
|
d.addNote(f)
|
|
# set the card up as a review card, due 8 days ago
|
|
c = f.cards()[0]
|
|
c.type = 2
|
|
c.queue = 2
|
|
c.due = d.sched.today - 8
|
|
c.factor = 2500
|
|
c.reps = 3
|
|
c.streak = 2
|
|
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
|
|
d.reset()
|
|
d.sched._cardConf(c)['lapse']['delays'] = [2, 20]
|
|
d.sched.answerCard(c, 1)
|
|
assert c.queue == 1
|
|
# it should be due tomorrow, with an interval of 1
|
|
assert c.edue == d.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()) > 119
|
|
# factor should have been decremented
|
|
assert c.factor == 2300
|
|
# check counters
|
|
assert c.lapses == 2
|
|
assert c.reps == 4
|
|
# check ests.
|
|
ni = d.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()
|
|
d.sched.answerCard(c, 2)
|
|
assert c.queue == 2
|
|
# the new interval should be (100 + 8/4) * 1.2 = 122
|
|
assert c.ivl == 122
|
|
assert c.due == d.sched.today + 122
|
|
# 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()
|
|
d.sched.answerCard(c, 3)
|
|
# the new interval should be (100 + 8/2) * 2.5 = 260
|
|
assert c.ivl == 260
|
|
assert c.due == d.sched.today + 260
|
|
# factor should have been left alone
|
|
assert c.factor == 2500
|
|
# ease 4
|
|
##################################################
|
|
c = copy.copy(cardcopy)
|
|
c.flush()
|
|
d.sched.answerCard(c, 4)
|
|
# the new interval should be (100 + 8) * 2.5 * 1.3 = 351
|
|
assert c.ivl == 351
|
|
assert c.due == d.sched.today + 351
|
|
# factor should have been increased
|
|
assert c.factor == 2650
|
|
# leech handling
|
|
##################################################
|
|
c = copy.copy(cardcopy)
|
|
c.lapses = 7
|
|
c.flush()
|
|
# steup hook
|
|
hooked = []
|
|
def onLeech(card):
|
|
hooked.append(1)
|
|
addHook("leech", onLeech)
|
|
d.sched.answerCard(c, 1)
|
|
assert hooked
|
|
assert c.queue == -1
|
|
c.load()
|
|
assert c.queue == -1
|
|
|
|
def test_finished():
|
|
d = getEmptyDeck()
|
|
# nothing due
|
|
assert "Congratulations" in d.sched.finishedMsg()
|
|
assert "limit" not in d.sched.finishedMsg()
|
|
f = d.newNote()
|
|
f['Front'] = u"one"; f['Back'] = u"two"
|
|
d.addNote(f)
|
|
# have a new card
|
|
assert "new cards available" in d.sched.finishedMsg()
|
|
# turn it into a review
|
|
d.reset()
|
|
c = f.cards()[0]
|
|
c.startTimer()
|
|
d.sched.answerCard(c, 3)
|
|
# nothing should be due tomorrow, as it's due in a week
|
|
assert "Congratulations" in d.sched.finishedMsg()
|
|
assert "limit" not in d.sched.finishedMsg()
|
|
|
|
def test_nextIvl():
|
|
d = getEmptyDeck()
|
|
f = d.newNote()
|
|
f['Front'] = u"one"; f['Back'] = u"two"
|
|
d.addNote(f)
|
|
d.reset()
|
|
conf = d.decks.conf(1)
|
|
conf['new']['delays'] = [0.5, 3, 10]
|
|
conf['lapse']['delays'] = [1, 5, 9]
|
|
c = d.sched.getCard()
|
|
# new cards
|
|
##################################################
|
|
ni = d.sched.nextIvl
|
|
assert ni(c, 1) == 30
|
|
assert ni(c, 2) == 180
|
|
assert ni(c, 3) == 4*86400
|
|
d.sched.answerCard(c, 1)
|
|
# cards in learning
|
|
##################################################
|
|
assert ni(c, 1) == 30
|
|
assert ni(c, 2) == 180
|
|
assert ni(c, 3) == 4*86400
|
|
d.sched.answerCard(c, 2)
|
|
assert ni(c, 1) == 30
|
|
assert ni(c, 2) == 600
|
|
assert ni(c, 3) == 4*86400
|
|
d.sched.answerCard(c, 2)
|
|
# normal graduation is tomorrow
|
|
assert ni(c, 2) == 1*86400
|
|
assert ni(c, 3) == 4*86400
|
|
# lapsed cards
|
|
##################################################
|
|
c.type = 2
|
|
c.ivl = 100
|
|
c.factor = 2500
|
|
assert ni(c, 1) == 60
|
|
assert ni(c, 2) == 100*86400
|
|
assert ni(c, 3) == 100*86400
|
|
# review cards
|
|
##################################################
|
|
c.queue = 2
|
|
c.ivl = 100
|
|
c.factor = 2500
|
|
# failing it should put it at 60s
|
|
assert ni(c, 1) == 60
|
|
# or 1 day if relearn is false
|
|
d.sched._cardConf(c)['lapse']['relearn']=False
|
|
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 d.sched.nextIvlStr(c, 4) == "10.8 months"
|
|
|
|
def test_misc():
|
|
d = getEmptyDeck()
|
|
f = d.newNote()
|
|
f['Front'] = u"one"
|
|
d.addNote(f)
|
|
c = f.cards()[0]
|
|
# burying
|
|
d.sched.buryNote(c.nid)
|
|
d.reset()
|
|
assert not d.sched.getCard()
|
|
d.sched.onClose()
|
|
d.reset()
|
|
assert d.sched.getCard()
|
|
|
|
def test_suspend():
|
|
d = getEmptyDeck()
|
|
f = d.newNote()
|
|
f['Front'] = u"one"
|
|
d.addNote(f)
|
|
c = f.cards()[0]
|
|
# suspending
|
|
d.reset()
|
|
assert d.sched.getCard()
|
|
d.sched.suspendCards([c.id])
|
|
d.reset()
|
|
assert not d.sched.getCard()
|
|
# unsuspending
|
|
d.sched.unsuspendCards([c.id])
|
|
d.reset()
|
|
assert d.sched.getCard()
|
|
# should cope with rev cards being relearnt
|
|
c.due = 0; c.ivl = 100; c.type = 2; c.queue = 2; c.flush()
|
|
d.reset()
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 1)
|
|
assert c.due >= time.time()
|
|
assert c.queue == 1
|
|
assert c.type == 2
|
|
d.sched.suspendCards([c.id])
|
|
d.sched.unsuspendCards([c.id])
|
|
c.load()
|
|
assert c.queue == 2
|
|
assert c.type == 2
|
|
assert c.due == 1
|
|
|
|
def test_cram():
|
|
print "disabled for now"
|
|
return
|
|
d = getEmptyDeck()
|
|
f = d.newNote()
|
|
f['Front'] = u"one"
|
|
d.addNote(f)
|
|
c = f.cards()[0]
|
|
c.ivl = 100
|
|
c.type = c.queue = 2
|
|
# due in 25 days, so it's been waiting 75 days
|
|
c.due = d.sched.today + 25
|
|
c.mod = 1
|
|
c.startTimer()
|
|
c.flush()
|
|
cardcopy = copy.copy(c)
|
|
d.conf['decks'] = [1]
|
|
d.cramDecks()
|
|
# first, test with initial intervals preserved
|
|
conf = d.sched._lrnConf(c)
|
|
conf['reset'] = False
|
|
conf['resched'] = False
|
|
assert d.sched.counts() == (1, 0, 0)
|
|
c = d.sched.getCard()
|
|
d.sched._cardConf(c)['cram']['delays'] = [0.5, 3, 10]
|
|
assert d.sched.counts() == (0, 0, 0)
|
|
# check that estimates work
|
|
assert d.sched.nextIvl(c, 1) == 30
|
|
assert d.sched.nextIvl(c, 2) == 180
|
|
assert d.sched.nextIvl(c, 3) == 86400*100
|
|
# failing it should not reset ivl
|
|
assert c.ivl == 100
|
|
d.sched.answerCard(c, 1)
|
|
assert c.ivl == 100
|
|
# and should have incremented lrn count
|
|
assert d.sched.counts()[1] == 1
|
|
# reset ivl for exit test, and pass card
|
|
d.sched.answerCard(c, 2)
|
|
delta = c.due - time.time()
|
|
assert delta > 175 and delta <= 180
|
|
# another two answers should reschedule it
|
|
assert c.queue == 1
|
|
d.sched.answerCard(c, 2)
|
|
d.sched.answerCard(c, 2)
|
|
assert c.queue == -3
|
|
assert c.ivl == 100
|
|
assert c.due == d.sched.today + c.ivl
|
|
# and if the queue is reset, it shouldn't appear in the new queue again
|
|
d.reset()
|
|
assert d.sched.counts() == (0, 0, 0)
|
|
# now try again with ivl rescheduling
|
|
c = copy.copy(cardcopy)
|
|
c.flush()
|
|
d.cramDecks()
|
|
conf = d.sched._lrnConf(c)
|
|
conf['reset'] = False
|
|
conf['resched'] = True
|
|
# failures shouldn't matter
|
|
d.sched.answerCard(c, 1)
|
|
# graduating the card will keep the same interval, but shift the card
|
|
# forward the number of days it had been waiting (75)
|
|
assert d.sched.nextIvl(c, 3) == 75*86400
|
|
d.sched.answerCard(c, 3)
|
|
assert c.ivl == 100
|
|
assert c.due == d.sched.today + 75
|
|
# try with ivl reset
|
|
c = copy.copy(cardcopy)
|
|
c.flush()
|
|
d.cramDecks()
|
|
conf = d.sched._lrnConf(c)
|
|
conf['reset'] = True
|
|
conf['resched'] = True
|
|
d.sched.answerCard(c, 1)
|
|
assert d.sched.nextIvl(c, 3) == 1*86400
|
|
d.sched.answerCard(c, 3)
|
|
assert c.ivl == 1
|
|
assert c.due == d.sched.today + 1
|
|
# users should be able to cram entire deck too
|
|
d.conf['decks'] = []
|
|
d.cramDecks()
|
|
assert d.sched.counts()[0] > 0
|
|
|
|
def test_cramLimits():
|
|
d = getEmptyDeck()
|
|
# create three cards, due tomorrow, the next, etc
|
|
for i in range(3):
|
|
f = d.newNote()
|
|
f['Front'] = str(i)
|
|
d.addNote(f)
|
|
c = f.cards()[0]
|
|
c.type = c.queue = 2
|
|
c.due = d.sched.today + 1 + i
|
|
c.flush()
|
|
# the default cram should return all three
|
|
d.conf['decks'] = [1]
|
|
d.cramDecks()
|
|
assert d.sched.counts()[0] == 3
|
|
# if we start from the day after tomorrow, it should be 2
|
|
d.cramDecks(min=1)
|
|
assert d.sched.counts()[0] == 2
|
|
# or after 2 days
|
|
d.cramDecks(min=2)
|
|
assert d.sched.counts()[0] == 1
|
|
# we may get nothing
|
|
d.cramDecks(min=3)
|
|
assert d.sched.counts()[0] == 0
|
|
# tomorrow(0) + dayAfter(1) = 2
|
|
d.cramDecks(max=1)
|
|
assert d.sched.counts()[0] == 2
|
|
# if max is tomorrow, we get only one
|
|
d.cramDecks(max=0)
|
|
assert d.sched.counts()[0] == 1
|
|
# both should work
|
|
d.cramDecks(min=0, max=0)
|
|
assert d.sched.counts()[0] == 1
|
|
d.cramDecks(min=1, max=1)
|
|
assert d.sched.counts()[0] == 1
|
|
d.cramDecks(min=0, max=1)
|
|
assert d.sched.counts()[0] == 2
|
|
|
|
def test_adjIvl():
|
|
d = getEmptyDeck()
|
|
# add two more templates and set second active
|
|
m = d.models.current(); mm = d.models
|
|
t = mm.newTemplate("Reverse")
|
|
t['qfmt'] = "{{Back}}"
|
|
t['afmt'] = "{{Front}}"
|
|
mm.addTemplate(m, t)
|
|
mm.save(m)
|
|
t = d.models.newTemplate(m)
|
|
t['name'] = "f2"
|
|
t['qfmt'] = "{{Front}}"
|
|
t['afmt'] = "{{Back}}"
|
|
d.models.addTemplate(m, t)
|
|
t = d.models.newTemplate(m)
|
|
t['name'] = "f3"
|
|
t['qfmt'] = "{{Front}}"
|
|
t['afmt'] = "{{Back}}"
|
|
d.models.addTemplate(m, t)
|
|
d.models.save(m)
|
|
# create a new note; it should have 4 cards
|
|
f = d.newNote()
|
|
f['Front'] = "1"; f['Back'] = "1"
|
|
d.addNote(f)
|
|
assert d.cardCount() == 4
|
|
d.reset()
|
|
# immediately remove first; it should get ideal ivl
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 3)
|
|
assert c.ivl == 4
|
|
# with the default settings, second card should be -1
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 3)
|
|
assert c.ivl == 3
|
|
# and third +1
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 3)
|
|
assert c.ivl == 5
|
|
# fourth exceeds default settings, so gets ideal again
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 3)
|
|
assert c.ivl == 4
|
|
# try again with another note
|
|
f = d.newNote()
|
|
f['Front'] = "2"; f['Back'] = "2"
|
|
d.addNote(f)
|
|
d.reset()
|
|
# set a minSpacing of 0
|
|
conf = d.sched._cardConf(c)
|
|
conf['rev']['minSpace'] = 0
|
|
# first card gets ideal
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 3)
|
|
assert c.ivl == 4
|
|
# and second too, because it's below the threshold
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 3)
|
|
assert c.ivl == 4
|
|
# if we increase the ivl minSpace isn't needed
|
|
conf['new']['ints'][1] = 20
|
|
# ideal..
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 3)
|
|
assert c.ivl == 20
|
|
# adjusted
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 3)
|
|
assert c.ivl == 19
|
|
|
|
def test_ordcycle():
|
|
d = getEmptyDeck()
|
|
# add two more templates and set second active
|
|
m = d.models.current(); mm = d.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
|
|
f = d.newNote()
|
|
f['Front'] = "1"; f['Back'] = "1"
|
|
d.addNote(f)
|
|
assert d.cardCount() == 3
|
|
d.reset()
|
|
# ordinals should arrive in order
|
|
assert d.sched.getCard().ord == 0
|
|
assert d.sched.getCard().ord == 1
|
|
assert d.sched.getCard().ord == 2
|
|
|
|
def test_counts_idx():
|
|
d = getEmptyDeck()
|
|
f = d.newNote()
|
|
f['Front'] = u"one"; f['Back'] = u"two"
|
|
d.addNote(f)
|
|
d.reset()
|
|
assert d.sched.counts() == (1, 0, 0)
|
|
c = d.sched.getCard()
|
|
# counter's been decremented but idx indicates 1
|
|
assert d.sched.counts() == (0, 0, 0)
|
|
assert d.sched.countIdx(c) == 0
|
|
# answer to move to learn queue
|
|
d.sched.answerCard(c, 1)
|
|
assert d.sched.counts() == (0, 2, 0)
|
|
# fetching again will decrement the count
|
|
c = d.sched.getCard()
|
|
assert d.sched.counts() == (0, 0, 0)
|
|
assert d.sched.countIdx(c) == 1
|
|
# answering should add it back again
|
|
d.sched.answerCard(c, 1)
|
|
assert d.sched.counts() == (0, 2, 0)
|
|
|
|
def test_repCounts():
|
|
d = getEmptyDeck()
|
|
f = d.newNote()
|
|
f['Front'] = u"one"
|
|
d.addNote(f)
|
|
d.reset()
|
|
# lrnReps should be accurate on pass/fail
|
|
assert d.sched.counts() == (1, 0, 0)
|
|
d.sched.answerCard(d.sched.getCard(), 1)
|
|
assert d.sched.counts() == (0, 2, 0)
|
|
d.sched.answerCard(d.sched.getCard(), 1)
|
|
assert d.sched.counts() == (0, 2, 0)
|
|
d.sched.answerCard(d.sched.getCard(), 2)
|
|
assert d.sched.counts() == (0, 1, 0)
|
|
d.sched.answerCard(d.sched.getCard(), 1)
|
|
assert d.sched.counts() == (0, 2, 0)
|
|
d.sched.answerCard(d.sched.getCard(), 2)
|
|
assert d.sched.counts() == (0, 1, 0)
|
|
d.sched.answerCard(d.sched.getCard(), 2)
|
|
assert d.sched.counts() == (0, 0, 0)
|
|
f = d.newNote()
|
|
f['Front'] = u"two"
|
|
d.addNote(f)
|
|
d.reset()
|
|
# initial pass should be correct too
|
|
d.sched.answerCard(d.sched.getCard(), 2)
|
|
assert d.sched.counts() == (0, 1, 0)
|
|
d.sched.answerCard(d.sched.getCard(), 1)
|
|
assert d.sched.counts() == (0, 2, 0)
|
|
d.sched.answerCard(d.sched.getCard(), 3)
|
|
assert d.sched.counts() == (0, 0, 0)
|
|
# immediate graduate should work
|
|
f = d.newNote()
|
|
f['Front'] = u"three"
|
|
d.addNote(f)
|
|
d.reset()
|
|
d.sched.answerCard(d.sched.getCard(), 3)
|
|
assert d.sched.counts() == (0, 0, 0)
|
|
# and failing a review should too
|
|
f = d.newNote()
|
|
f['Front'] = u"three"
|
|
d.addNote(f)
|
|
c = f.cards()[0]
|
|
c.type = 2
|
|
c.queue = 2
|
|
c.due = d.sched.today
|
|
c.flush()
|
|
d.reset()
|
|
assert d.sched.counts() == (0, 0, 1)
|
|
d.sched.answerCard(d.sched.getCard(), 1)
|
|
assert d.sched.counts() == (0, 2, 0)
|
|
|
|
def test_timing():
|
|
d = getEmptyDeck()
|
|
# add a few review cards, due today
|
|
for i in range(5):
|
|
f = d.newNote()
|
|
f['Front'] = "num"+str(i)
|
|
d.addNote(f)
|
|
c = f.cards()[0]
|
|
c.type = 2
|
|
c.queue = 2
|
|
c.due = 0
|
|
c.flush()
|
|
# fail the first one
|
|
d.reset()
|
|
c = d.sched.getCard()
|
|
# set a a fail delay of 1 second so we don't have to wait
|
|
d.sched._cardConf(c)['lapse']['delays'][0] = 1/60.0
|
|
d.sched.answerCard(c, 1)
|
|
# the next card should be another review
|
|
c = d.sched.getCard()
|
|
assert c.queue == 2
|
|
# but if we wait for a second, the failed card should come back
|
|
time.sleep(1)
|
|
c = d.sched.getCard()
|
|
assert c.queue == 1
|
|
|
|
def test_collapse():
|
|
d = getEmptyDeck()
|
|
# add a note
|
|
f = d.newNote()
|
|
f['Front'] = u"one"
|
|
d.addNote(f)
|
|
d.reset()
|
|
# test collapsing
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 1)
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 3)
|
|
assert not d.sched.getCard()
|
|
|
|
def test_deckDue():
|
|
d = getEmptyDeck()
|
|
# add a note with default deck
|
|
f = d.newNote()
|
|
f['Front'] = u"one"
|
|
d.addNote(f)
|
|
# and one that's a child
|
|
f = d.newNote()
|
|
f['Front'] = u"two"
|
|
default1 = f.did = d.decks.id("Default::1")
|
|
d.addNote(f)
|
|
# make it a review card
|
|
c = f.cards()[0]
|
|
c.queue = 2
|
|
c.due = 0
|
|
c.flush()
|
|
# add one more with a new deck
|
|
f = d.newNote()
|
|
f['Front'] = u"two"
|
|
foobar = f.did = d.decks.id("foo::bar")
|
|
d.addNote(f)
|
|
# and one that's a sibling
|
|
f = d.newNote()
|
|
f['Front'] = u"three"
|
|
foobaz = f.did = d.decks.id("foo::baz")
|
|
d.addNote(f)
|
|
d.reset()
|
|
assert len(d.decks.decks) == 5
|
|
cnts = d.sched.deckDueList()
|
|
cnts.sort()
|
|
assert cnts[0] == ["Default", 1, 0, 1]
|
|
assert cnts[1] == ["Default::1", default1, 1, 0]
|
|
assert cnts[2] == ["foo", d.decks.id("foo"), 0, 0]
|
|
assert cnts[3] == ["foo::bar", foobar, 0, 1]
|
|
assert cnts[4] == ["foo::baz", foobaz, 0, 1]
|
|
tree = d.sched.deckDueTree()
|
|
assert tree[0][0] == "Default"
|
|
# sum of child and parent
|
|
assert tree[0][1] == 1
|
|
assert tree[0][2] == 1
|
|
assert tree[0][3] == 1
|
|
# child count is just review
|
|
assert tree[0][4][0][0] == "1"
|
|
assert tree[0][4][0][1] == default1
|
|
assert tree[0][4][0][2] == 1
|
|
assert tree[0][4][0][3] == 0
|
|
# code should not fail if a card has an invalid deck
|
|
c.did = 12345; c.flush()
|
|
d.sched.deckDueList()
|
|
d.sched.deckDueTree()
|
|
|
|
def test_deckTree():
|
|
d = getEmptyDeck()
|
|
d.decks.id("new::b::c")
|
|
d.decks.id("new2")
|
|
# new should not appear twice in tree
|
|
names = [x[0] for x in d.sched.deckDueTree()]
|
|
names.remove("new")
|
|
assert "new" not in names
|
|
|
|
def test_deckFlow():
|
|
d = getEmptyDeck()
|
|
# add a note with default deck
|
|
f = d.newNote()
|
|
f['Front'] = u"one"
|
|
d.addNote(f)
|
|
# and one that's a child
|
|
f = d.newNote()
|
|
f['Front'] = u"two"
|
|
default1 = f.did = d.decks.id("Default::2")
|
|
d.addNote(f)
|
|
# and another that's higher up
|
|
f = d.newNote()
|
|
f['Front'] = u"three"
|
|
default1 = f.did = d.decks.id("Default::1")
|
|
d.addNote(f)
|
|
# should get top level one first, then ::1, then ::2
|
|
d.reset()
|
|
assert d.sched.counts() == (3,0,0)
|
|
for i in "one", "three", "two":
|
|
c = d.sched.getCard()
|
|
assert c.note()['Front'] == i
|
|
d.sched.answerCard(c, 2)
|
|
|
|
def test_reorder():
|
|
d = getEmptyDeck()
|
|
# add a note with default deck
|
|
f = d.newNote()
|
|
f['Front'] = u"one"
|
|
d.addNote(f)
|
|
f2 = d.newNote()
|
|
f2['Front'] = u"two"
|
|
d.addNote(f2)
|
|
assert f2.cards()[0].due == 2
|
|
found=False
|
|
# 50/50 chance of being reordered
|
|
for i in range(20):
|
|
d.sched.randomizeCards()
|
|
if f.cards()[0].due != f.id:
|
|
found=True
|
|
break
|
|
assert found
|
|
d.sched.orderCards()
|
|
assert f.cards()[0].due == 1
|
|
# shifting
|
|
f3 = d.newNote()
|
|
f3['Front'] = u"three"
|
|
d.addNote(f3)
|
|
f4 = d.newNote()
|
|
f4['Front'] = u"four"
|
|
d.addNote(f4)
|
|
assert f.cards()[0].due == 1
|
|
assert f2.cards()[0].due == 2
|
|
assert f3.cards()[0].due == 3
|
|
assert f4.cards()[0].due == 4
|
|
d.sched.sortCards([
|
|
f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
|
|
assert f.cards()[0].due == 3
|
|
assert f2.cards()[0].due == 4
|
|
assert f3.cards()[0].due == 1
|
|
assert f4.cards()[0].due == 2
|
|
|
|
def test_forget():
|
|
d = getEmptyDeck()
|
|
f = d.newNote()
|
|
f['Front'] = u"one"
|
|
d.addNote(f)
|
|
c = f.cards()[0]
|
|
c.queue = 2; c.type = 2; c.ivl = 100; c.due = 0
|
|
c.flush()
|
|
d.reset()
|
|
assert d.sched.counts() == (0, 0, 1)
|
|
d.sched.forgetCards([c.id])
|
|
d.reset()
|
|
assert d.sched.counts() == (1, 0, 0)
|
|
|
|
def test_resched():
|
|
d = getEmptyDeck()
|
|
f = d.newNote()
|
|
f['Front'] = u"one"
|
|
d.addNote(f)
|
|
c = f.cards()[0]
|
|
d.sched.reschedCards([c.id], 0, 0)
|
|
c.load()
|
|
assert c.due == d.sched.today
|
|
assert c.ivl == 1
|
|
assert c.queue == c.type == 2
|
|
d.sched.reschedCards([c.id], 1, 1)
|
|
c.load()
|
|
assert c.due == d.sched.today+1
|
|
assert c.ivl == +1
|
|
|
|
def test_revlim():
|
|
d = getEmptyDeck()
|
|
for i in range(20):
|
|
f = d.newNote()
|
|
f['Front'] = str(i)
|
|
d.addNote(f)
|
|
d.db.execute("update cards set due = 0, queue = 2, type = 2")
|
|
d.reset()
|
|
assert d.sched.counts()[2] == 20
|
|
for i in range(5):
|
|
d.sched.answerCard(d.sched.getCard(), 3)
|
|
assert d.sched.counts()[2] == 15
|
|
t = d.decks.top()
|
|
t['revLim'] = 10
|
|
d.reset()
|
|
assert d.sched.counts()[2] == 5
|
|
|