mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
543 lines
15 KiB
Python
543 lines
15 KiB
Python
# coding: utf-8
|
|
|
|
import time, copy
|
|
from tests.shared import assertException, getEmptyDeck
|
|
from anki.stdmodels import BasicModel
|
|
from anki.utils import stripHTML, intTime
|
|
from anki.hooks import addHook
|
|
|
|
def test_basics():
|
|
d = getEmptyDeck()
|
|
assert not d.sched.getCard()
|
|
|
|
def test_new():
|
|
d = getEmptyDeck()
|
|
assert d.sched.newCount == 0
|
|
# add a fact
|
|
f = d.newFact()
|
|
f['Front'] = u"one"; f['Back'] = u"two"
|
|
d.addFact(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 == 0
|
|
assert c.due >= t
|
|
# the default order should ensure siblings are not seen together, and
|
|
# should show all cards
|
|
m = d.currentModel()
|
|
m.templates[1]['actv'] = True
|
|
m.flush()
|
|
f = d.newFact()
|
|
f['Front'] = u"2"; f['Back'] = u"2"
|
|
d.addFact(f)
|
|
f = d.newFact()
|
|
f['Front'] = u"3"; f['Back'] = u"3"
|
|
d.addFact(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_learn():
|
|
d = getEmptyDeck()
|
|
# add a fact
|
|
f = d.newFact()
|
|
f['Front'] = u"one"; f['Back'] = u"two"
|
|
f = d.addFact(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
|
|
# it should have no cycles and a grade of 0
|
|
assert c.grade == c.cycles == 0
|
|
# fail it
|
|
d.sched.answerCard(c, 1)
|
|
# it should by due in 30 seconds
|
|
assert round(c.due - time.time()) == 30
|
|
# and have 1 cycle, but still a zero grade
|
|
assert c.grade == 0
|
|
assert c.cycles == 1
|
|
# pass it once
|
|
d.sched.answerCard(c, 2)
|
|
# it should by due in 3 minutes
|
|
assert round(c.due - time.time()) == 180
|
|
# and it should be grade 1 now
|
|
assert c.grade == 1
|
|
assert c.cycles == 2
|
|
# check log is accurate
|
|
log = d.db.first("select * from revlog order by time desc")
|
|
assert log[2] == 2
|
|
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()) == 600
|
|
# and it should be grade 1 now
|
|
assert c.grade == 2
|
|
assert c.cycles == 3
|
|
# the next pass should graduate the card
|
|
assert c.queue == 1
|
|
assert c.type == 0
|
|
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
|
|
# let's try early removal bonus
|
|
c.type = 0
|
|
c.queue = 1
|
|
c.cycles = 0
|
|
d.sched.answerCard(c, 3)
|
|
assert c.type == 2
|
|
assert c.ivl == 7
|
|
# 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 fact
|
|
f = d.newFact()
|
|
f['Front'] = u"one"; f['Back'] = u"two"
|
|
d.addFact(f)
|
|
# set the card up as a review card, due yesterday
|
|
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
|
|
##################################################
|
|
d.sched.answerCard(c, 1)
|
|
assert c.queue == 1
|
|
# it should be due tomorrow, with an interval of 1
|
|
assert c.due == d.sched.today + 1
|
|
assert c.ivl == 1
|
|
# factor should have been decremented
|
|
assert c.factor == 2300
|
|
# check counters
|
|
assert c.streak == 0
|
|
assert c.lapses == 2
|
|
assert c.reps == 4
|
|
# 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.streak == 3
|
|
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 = 15
|
|
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 "No cards are due" in d.sched.finishedMsg()
|
|
f = d.newFact()
|
|
f['Front'] = u"one"; f['Back'] = u"two"
|
|
d.addFact(f)
|
|
# have a new card
|
|
assert "1 new" in d.sched.finishedMsg()
|
|
# turn it into a review
|
|
c = f.cards()[0]
|
|
c.startTimer()
|
|
d.sched.answerCard(c, 3)
|
|
# nothing should be due tomorrow, as it's due in a week
|
|
assert "No cards are due" in d.sched.finishedMsg()
|
|
|
|
def test_nextIvl():
|
|
d = getEmptyDeck()
|
|
f = d.newFact()
|
|
f['Front'] = u"one"; f['Back'] = u"two"
|
|
d.addFact(f)
|
|
c = f.cards()[0]
|
|
# cards in learning
|
|
##################################################
|
|
ni = d.sched.nextIvl
|
|
assert ni(c, 1) == 30
|
|
assert ni(c, 2) == 180
|
|
# immediate removal is 7 days
|
|
assert ni(c, 3) == 7*86400
|
|
c.cycles = 1
|
|
c.grade = 1
|
|
assert ni(c, 1) == 30
|
|
assert ni(c, 2) == 600
|
|
# no first time bonus
|
|
assert ni(c, 3) == 4*86400
|
|
c.grade = 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) == 30
|
|
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 puts it at tomorrow
|
|
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.newFact()
|
|
f['Front'] = u"one"; f['Back'] = u"two"
|
|
d.addFact(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()
|
|
# burying
|
|
d.sched.buryFact(c.fid)
|
|
d.reset()
|
|
assert not d.sched.getCard()
|
|
d.sched.onClose()
|
|
d.reset()
|
|
assert d.sched.getCard()
|
|
# counts
|
|
assert d.sched.timeToday() == 0
|
|
assert d.sched.repsToday() == 0
|
|
c.timerStarted = time.time() - 10
|
|
d.sched.answerCard(c, 2)
|
|
assert d.sched.timeToday() > 0
|
|
assert d.sched.repsToday() == 1
|
|
|
|
def test_cram():
|
|
d = getEmptyDeck()
|
|
f = d.newFact()
|
|
f['Front'] = u"one"
|
|
d.addFact(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.cramGroups([1])
|
|
# 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()
|
|
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
|
|
# 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
|
|
# 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.cramGroups([1])
|
|
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)
|
|
print d.sched.nextIvl(c, 3)
|
|
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.cramGroups([1])
|
|
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
|
|
|
|
def test_cramLimits():
|
|
d = getEmptyDeck()
|
|
# create three cards, due tomorrow, the next, etc
|
|
for i in range(3):
|
|
f = d.newFact()
|
|
f['Front'] = str(i)
|
|
d.addFact(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.cramGroups([1])
|
|
assert d.sched.counts()[0] == 3
|
|
# if we start from the day after tomorrow, it should be 2
|
|
d.cramGroups([1], min=1)
|
|
assert d.sched.counts()[0] == 2
|
|
# or after 2 days
|
|
d.cramGroups([1], min=2)
|
|
assert d.sched.counts()[0] == 1
|
|
# we may get nothing
|
|
d.cramGroups([1], min=3)
|
|
assert d.sched.counts()[0] == 0
|
|
# tomorrow(0) + dayAfter(1) = 2
|
|
d.cramGroups([1], max=1)
|
|
assert d.sched.counts()[0] == 2
|
|
# if max is tomorrow, we get only one
|
|
d.cramGroups([1], max=0)
|
|
assert d.sched.counts()[0] == 1
|
|
# both should work
|
|
d.cramGroups([1], min=0, max=0)
|
|
assert d.sched.counts()[0] == 1
|
|
d.cramGroups([1], min=1, max=1)
|
|
assert d.sched.counts()[0] == 1
|
|
d.cramGroups([1], 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.currentModel()
|
|
m.templates[1]['actv'] = True
|
|
t = m.newTemplate()
|
|
t['name'] = "f2"
|
|
t['qfmt'] = "{{Front}}"
|
|
t['afmt'] = "{{Back}}"
|
|
m.addTemplate(t)
|
|
t = m.newTemplate()
|
|
t['name'] = "f3"
|
|
t['qfmt'] = "{{Front}}"
|
|
t['afmt'] = "{{Back}}"
|
|
m.addTemplate(t)
|
|
m.flush()
|
|
# create a new fact; it should have 4 cards
|
|
f = d.newFact()
|
|
f['Front'] = "1"; f['Back'] = "1"
|
|
d.addFact(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 == 7
|
|
# with the default settings, second card should be -1
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 3)
|
|
assert c.ivl == 6
|
|
# and third +1
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 3)
|
|
assert c.ivl == 8
|
|
# fourth exceeds default settings, so gets ideal again
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 3)
|
|
assert c.ivl == 7
|
|
# try again with another fact
|
|
f = d.newFact()
|
|
f['Front'] = "2"; f['Back'] = "2"
|
|
d.addFact(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 == 7
|
|
# and second too, because it's below the threshold
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 3)
|
|
assert c.ivl == 7
|
|
# 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.currentModel()
|
|
m.templates[1]['actv'] = True
|
|
t = m.newTemplate()
|
|
t['name'] = "f2"
|
|
t['qfmt'] = "{{Front}}"
|
|
t['afmt'] = "{{Back}}"
|
|
m.addTemplate(t)
|
|
m.flush()
|
|
# create a new fact; it should have 4 cards
|
|
f = d.newFact()
|
|
f['Front'] = "1"; f['Back'] = "1"
|
|
d.addFact(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():
|
|
d = getEmptyDeck()
|
|
# add a second group
|
|
assert d.groupId("new group") == 2
|
|
# for each card type
|
|
for type in range(3):
|
|
# and each of the groups
|
|
for gid in (1,2):
|
|
# create a new fact
|
|
f = d.newFact()
|
|
f['Front'] = u"one"
|
|
d.addFact(f)
|
|
c = f.cards()[0]
|
|
# set type/gid
|
|
c.type = type
|
|
c.queue = type
|
|
c.gid = gid
|
|
c.due = 0
|
|
c.flush()
|
|
d.reset()
|
|
# with the default settings, there's no count limit
|
|
assert d.sched.counts() == (2,2,2)
|
|
# check limit to one group
|
|
d.qconf['revGroups'] = [1]
|
|
d.qconf['newGroups'] = [1]
|
|
d.reset()
|
|
assert d.sched.counts() == (1,2,1)
|
|
# we can disable the groups without forgetting them
|
|
d.sched.useGroups = False
|
|
d.reset()
|
|
assert d.sched.counts() == (2,2,2)
|
|
# we don't need to build the queue to get the counts
|
|
assert d.sched.allCounts() == (2,2,2)
|
|
assert d.sched.selCounts() == (1,2,1)
|
|
assert d.sched.allCounts() == (2,2,2)
|