format tests

This commit is contained in:
Damien Elmes 2019-12-25 14:18:34 +10:00
parent 9791bcb36b
commit e5c4618a9a
19 changed files with 801 additions and 585 deletions

View file

@ -7,7 +7,7 @@ MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules MAKEFLAGS += --no-builtin-rules
RUNARGS := RUNARGS :=
.SUFFIXES: .SUFFIXES:
BLACKARGS := -t py36 anki aqt BLACKARGS := -t py36 anki aqt tests
RUSTARGS := --release --strip RUSTARGS := --release --strip
$(shell mkdir -p .build) $(shell mkdir -p .build)

View file

@ -1,6 +1,7 @@
import tempfile, os, shutil import tempfile, os, shutil
from anki import Collection as aopen from anki import Collection as aopen
def assertException(exception, func): def assertException(exception, func):
found = False found = False
try: try:
@ -25,6 +26,7 @@ def getEmptyCol():
col = aopen(nam) col = aopen(nam)
return col return col
getEmptyCol.master = "" getEmptyCol.master = ""
# Fallback for when the DB needs options passed in. # Fallback for when the DB needs options passed in.
@ -34,10 +36,12 @@ def getEmptyDeckWith(**kwargs):
os.unlink(nam) os.unlink(nam)
return aopen(nam, **kwargs) return aopen(nam, **kwargs)
def getUpgradeDeckPath(name="anki12.anki"): def getUpgradeDeckPath(name="anki12.anki"):
src = os.path.join(testDir, "support", name) src = os.path.join(testDir, "support", name)
(fd, dst) = tempfile.mkstemp(suffix=".anki2") (fd, dst) = tempfile.mkstemp(suffix=".anki2")
shutil.copy(src, dst) shutil.copy(src, dst)
return dst return dst
testDir = os.path.dirname(__file__) testDir = os.path.dirname(__file__)

View file

@ -9,65 +9,48 @@ from aqt.addons import AddonManager
def test_readMinimalManifest(): def test_readMinimalManifest():
assertReadManifest( assertReadManifest(
'{"package": "yes", "name": "no"}', '{"package": "yes", "name": "no"}', {"package": "yes", "name": "no"}
{"package": "yes", "name": "no"}
) )
def test_readExtraKeys(): def test_readExtraKeys():
assertReadManifest( assertReadManifest(
'{"package": "a", "name": "b", "mod": 3, "conflicts": ["d", "e"]}', '{"package": "a", "name": "b", "mod": 3, "conflicts": ["d", "e"]}',
{"package": "a", "name": "b", "mod": 3, "conflicts": ["d", "e"]} {"package": "a", "name": "b", "mod": 3, "conflicts": ["d", "e"]},
) )
def test_invalidManifest(): def test_invalidManifest():
assertReadManifest( assertReadManifest('{"one": 1}', {})
'{"one": 1}',
{}
)
def test_mustHaveName(): def test_mustHaveName():
assertReadManifest( assertReadManifest('{"package": "something"}', {})
'{"package": "something"}',
{}
)
def test_mustHavePackage(): def test_mustHavePackage():
assertReadManifest( assertReadManifest('{"name": "something"}', {})
'{"name": "something"}',
{}
)
def test_invalidJson(): def test_invalidJson():
assertReadManifest( assertReadManifest("this is not a JSON dictionary", {})
'this is not a JSON dictionary',
{}
)
def test_missingManifest(): def test_missingManifest():
assertReadManifest( assertReadManifest(
'{"package": "what", "name": "ever"}', '{"package": "what", "name": "ever"}', {}, nameInZip="not-manifest.bin"
{},
nameInZip="not-manifest.bin"
) )
def test_ignoreExtraKeys(): def test_ignoreExtraKeys():
assertReadManifest( assertReadManifest(
'{"package": "a", "name": "b", "game": "c"}', '{"package": "a", "name": "b", "game": "c"}', {"package": "a", "name": "b"}
{"package": "a", "name": "b"}
) )
def test_conflictsMustBeStrings(): def test_conflictsMustBeStrings():
assertReadManifest( assertReadManifest(
'{"package": "a", "name": "b", "conflicts": ["c", 4, {"d": "e"}]}', '{"package": "a", "name": "b", "conflicts": ["c", 4, {"d": "e"}]}', {}
{}
) )

View file

@ -2,11 +2,12 @@
from tests.shared import getEmptyCol from tests.shared import getEmptyCol
def test_previewCards(): def test_previewCards():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = '1' f["Front"] = "1"
f['Back'] = '2' f["Back"] = "2"
# non-empty and active # non-empty and active
cards = deck.previewCards(f, 0) cards = deck.previewCards(f, 0)
assert len(cards) == 1 assert len(cards) == 1
@ -22,11 +23,12 @@ def test_previewCards():
# make sure we haven't accidentally added cards to the db # make sure we haven't accidentally added cards to the db
assert deck.cardCount() == 1 assert deck.cardCount() == 1
def test_delete(): def test_delete():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = '1' f["Front"] = "1"
f['Back'] = '2' f["Back"] = "2"
deck.addNote(f) deck.addNote(f)
cid = f.cards()[0].id cid = f.cards()[0].id
deck.reset() deck.reset()
@ -38,62 +40,65 @@ def test_delete():
assert deck.db.scalar("select count() from cards") == 0 assert deck.db.scalar("select count() from cards") == 0
assert deck.db.scalar("select count() from graves") == 2 assert deck.db.scalar("select count() from graves") == 2
def test_misc(): def test_misc():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = '1' f["Front"] = "1"
f['Back'] = '2' f["Back"] = "2"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
id = d.models.current()['id'] id = d.models.current()["id"]
assert c.template()['ord'] == 0 assert c.template()["ord"] == 0
def test_genrem(): def test_genrem():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = '1' f["Front"] = "1"
f['Back'] = '' f["Back"] = ""
d.addNote(f) d.addNote(f)
assert len(f.cards()) == 1 assert len(f.cards()) == 1
m = d.models.current() m = d.models.current()
mm = d.models mm = d.models
# adding a new template should automatically create cards # adding a new template should automatically create cards
t = mm.newTemplate("rev") t = mm.newTemplate("rev")
t['qfmt'] = '{{Front}}' t["qfmt"] = "{{Front}}"
t['afmt'] = "" t["afmt"] = ""
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m, templates=True) mm.save(m, templates=True)
assert len(f.cards()) == 2 assert len(f.cards()) == 2
# if the template is changed to remove cards, they'll be removed # if the template is changed to remove cards, they'll be removed
t['qfmt'] = "{{Back}}" t["qfmt"] = "{{Back}}"
mm.save(m, templates=True) mm.save(m, templates=True)
d.remCards(d.emptyCids()) d.remCards(d.emptyCids())
assert len(f.cards()) == 1 assert len(f.cards()) == 1
# if we add to the note, a card should be automatically generated # if we add to the note, a card should be automatically generated
f.load() f.load()
f['Back'] = "1" f["Back"] = "1"
f.flush() f.flush()
assert len(f.cards()) == 2 assert len(f.cards()) == 2
def test_gendeck(): def test_gendeck():
d = getEmptyCol() d = getEmptyCol()
cloze = d.models.byName("Cloze") cloze = d.models.byName("Cloze")
d.models.setCurrent(cloze) d.models.setCurrent(cloze)
f = d.newNote() f = d.newNote()
f['Text'] = '{{c1::one}}' f["Text"] = "{{c1::one}}"
d.addNote(f) d.addNote(f)
assert d.cardCount() == 1 assert d.cardCount() == 1
assert f.cards()[0].did == 1 assert f.cards()[0].did == 1
# set the model to a new default deck # set the model to a new default deck
newId = d.decks.id("new") newId = d.decks.id("new")
cloze['did'] = newId cloze["did"] = newId
d.models.save(cloze, updateReqs=False) d.models.save(cloze, updateReqs=False)
# a newly generated card should share the first card's deck # a newly generated card should share the first card's deck
f['Text'] += '{{c2::two}}' f["Text"] += "{{c2::two}}"
f.flush() f.flush()
assert f.cards()[1].did == 1 assert f.cards()[1].did == 1
# and same with multiple cards # and same with multiple cards
f['Text'] += '{{c3::three}}' f["Text"] += "{{c3::three}}"
f.flush() f.flush()
assert f.cards()[2].did == 1 assert f.cards()[2].did == 1
# if one of the cards is in a different deck, it should revert to the # if one of the cards is in a different deck, it should revert to the
@ -101,9 +106,6 @@ def test_gendeck():
c = f.cards()[1] c = f.cards()[1]
c.did = newId c.did = newId
c.flush() c.flush()
f['Text'] += '{{c4::four}}' f["Text"] += "{{c4::four}}"
f.flush() f.flush()
assert f.cards()[3].did == newId assert f.cards()[3].did == newId

View file

@ -8,6 +8,7 @@ from anki.stdmodels import addBasicModel, models
from anki import Collection as aopen from anki import Collection as aopen
def test_create_open(): def test_create_open():
(fd, path) = tempfile.mkstemp(suffix=".anki2", prefix="test_attachNew") (fd, path) = tempfile.mkstemp(suffix=".anki2", prefix="test_attachNew")
try: try:
@ -32,27 +33,28 @@ def test_create_open():
dir = "c:\root.anki2" dir = "c:\root.anki2"
else: else:
dir = "/attachroot.anki2" dir = "/attachroot.anki2"
assertException(Exception, assertException(Exception, lambda: aopen(dir))
lambda: aopen(dir))
# reuse tmp file from before, test non-writeable file # reuse tmp file from before, test non-writeable file
os.chmod(newPath, 0) os.chmod(newPath, 0)
assertException(Exception, assertException(Exception, lambda: aopen(newPath))
lambda: aopen(newPath))
os.chmod(newPath, 0o666) os.chmod(newPath, 0o666)
os.unlink(newPath) os.unlink(newPath)
def test_noteAddDelete(): def test_noteAddDelete():
deck = getEmptyCol() deck = getEmptyCol()
# add a note # add a note
f = deck.newNote() f = deck.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
n = deck.addNote(f) n = deck.addNote(f)
assert n == 1 assert n == 1
# test multiple cards - add another template # test multiple cards - add another template
m = deck.models.current(); mm = deck.models m = deck.models.current()
mm = deck.models
t = mm.newTemplate("Reverse") t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}" t["qfmt"] = "{{Back}}"
t['afmt'] = "{{Front}}" t["afmt"] = "{{Front}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
# the default save doesn't generate cards # the default save doesn't generate cards
@ -63,7 +65,8 @@ def test_noteAddDelete():
assert deck.cardCount() == 2 assert deck.cardCount() == 2
# creating new notes should use both cards # creating new notes should use both cards
f = deck.newNote() f = deck.newNote()
f['Front'] = "three"; f['Back'] = "four" f["Front"] = "three"
f["Back"] = "four"
n = deck.addNote(f) n = deck.addNote(f)
assert n == 2 assert n == 2
assert deck.cardCount() == 4 assert deck.cardCount() == 4
@ -74,36 +77,39 @@ def test_noteAddDelete():
assert not f.dupeOrEmpty() assert not f.dupeOrEmpty()
# now let's make a duplicate # now let's make a duplicate
f2 = deck.newNote() f2 = deck.newNote()
f2['Front'] = "one"; f2['Back'] = "" f2["Front"] = "one"
f2["Back"] = ""
assert f2.dupeOrEmpty() assert f2.dupeOrEmpty()
# empty first field should not be permitted either # empty first field should not be permitted either
f2['Front'] = " " f2["Front"] = " "
assert f2.dupeOrEmpty() assert f2.dupeOrEmpty()
def test_fieldChecksum(): def test_fieldChecksum():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = "new"; f['Back'] = "new2" f["Front"] = "new"
f["Back"] = "new2"
deck.addNote(f) deck.addNote(f)
assert deck.db.scalar( assert deck.db.scalar("select csum from notes") == int("c2a6b03f", 16)
"select csum from notes") == int("c2a6b03f", 16)
# changing the val should change the checksum # changing the val should change the checksum
f['Front'] = "newx" f["Front"] = "newx"
f.flush() f.flush()
assert deck.db.scalar( assert deck.db.scalar("select csum from notes") == int("302811ae", 16)
"select csum from notes") == int("302811ae", 16)
def test_addDelTags(): def test_addDelTags():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = "1" f["Front"] = "1"
deck.addNote(f) deck.addNote(f)
f2 = deck.newNote() f2 = deck.newNote()
f2['Front'] = "2" f2["Front"] = "2"
deck.addNote(f2) deck.addNote(f2)
# adding for a given id # adding for a given id
deck.tags.bulkAdd([f.id], "foo") deck.tags.bulkAdd([f.id], "foo")
f.load(); f2.load() f.load()
f2.load()
assert "foo" in f.tags assert "foo" in f.tags
assert "foo" not in f2.tags assert "foo" not in f2.tags
# should be canonified # should be canonified
@ -112,6 +118,7 @@ def test_addDelTags():
assert f.tags[0] == "aaa" assert f.tags[0] == "aaa"
assert len(f.tags) == 2 assert len(f.tags) == 2
def test_timestamps(): def test_timestamps():
deck = getEmptyCol() deck = getEmptyCol()
assert len(deck.models.models) == len(models) assert len(deck.models.models) == len(models)
@ -119,23 +126,24 @@ def test_timestamps():
addBasicModel(deck) addBasicModel(deck)
assert len(deck.models.models) == 100 + len(models) assert len(deck.models.models) == 100 + len(models)
def test_furigana(): def test_furigana():
deck = getEmptyCol() deck = getEmptyCol()
mm = deck.models mm = deck.models
m = mm.current() m = mm.current()
# filter should work # filter should work
m['tmpls'][0]['qfmt'] = '{{kana:Front}}' m["tmpls"][0]["qfmt"] = "{{kana:Front}}"
mm.save(m) mm.save(m)
n = deck.newNote() n = deck.newNote()
n['Front'] = 'foo[abc]' n["Front"] = "foo[abc]"
deck.addNote(n) deck.addNote(n)
c = n.cards()[0] c = n.cards()[0]
assert c.q().endswith("abc") assert c.q().endswith("abc")
# and should avoid sound # and should avoid sound
n['Front'] = 'foo[sound:abc.mp3]' n["Front"] = "foo[sound:abc.mp3]"
n.flush() n.flush()
assert "sound:" in c.q(reload=True) assert "sound:" in c.q(reload=True)
# it shouldn't throw an error while people are editing # it shouldn't throw an error while people are editing
m['tmpls'][0]['qfmt'] = '{{kana:}}' m["tmpls"][0]["qfmt"] = "{{kana:}}"
mm.save(m) mm.save(m)
c.q(reload=True) c.q(reload=True)

View file

@ -3,6 +3,7 @@
from anki.errors import DeckRenameError from anki.errors import DeckRenameError
from tests.shared import assertException, getEmptyCol from tests.shared import assertException, getEmptyCol
def test_basic(): def test_basic():
deck = getEmptyCol() deck = getEmptyCol()
# we start with a standard deck # we start with a standard deck
@ -34,21 +35,22 @@ def test_basic():
# parents with a different case should be handled correctly # parents with a different case should be handled correctly
deck.decks.id("ONE") deck.decks.id("ONE")
m = deck.models.current() m = deck.models.current()
m['did'] = deck.decks.id("one::two") m["did"] = deck.decks.id("one::two")
deck.models.save(m, updateReqs=False) deck.models.save(m, updateReqs=False)
n = deck.newNote() n = deck.newNote()
n['Front'] = "abc" n["Front"] = "abc"
deck.addNote(n) deck.addNote(n)
# this will error if child and parent case don't match # this will error if child and parent case don't match
deck.sched.deckDueList() deck.sched.deckDueList()
def test_remove(): def test_remove():
deck = getEmptyCol() deck = getEmptyCol()
# create a new deck, and add a note/card to it # create a new deck, and add a note/card to it
g1 = deck.decks.id("g1") g1 = deck.decks.id("g1")
f = deck.newNote() f = deck.newNote()
f['Front'] = "1" f["Front"] = "1"
f.model()['did'] = g1 f.model()["did"] = g1
deck.addNote(f) deck.addNote(f)
c = f.cards()[0] c = f.cards()[0]
assert c.did == g1 assert c.did == g1
@ -62,12 +64,14 @@ def test_remove():
assert deck.decks.name(c.did) == "[no deck]" assert deck.decks.name(c.did) == "[no deck]"
# let's create another deck and explicitly set the card to it # let's create another deck and explicitly set the card to it
g2 = deck.decks.id("g2") g2 = deck.decks.id("g2")
c.did = g2; c.flush() c.did = g2
c.flush()
# this time we'll delete the card/note too # this time we'll delete the card/note too
deck.decks.rem(g2, cardsToo=True) deck.decks.rem(g2, cardsToo=True)
assert deck.cardCount() == 0 assert deck.cardCount() == 0
assert deck.noteCount() == 0 assert deck.noteCount() == 0
def test_rename(): def test_rename():
d = getEmptyCol() d = getEmptyCol()
id = d.decks.id("hello::world") id = d.decks.id("hello::world")
@ -80,8 +84,7 @@ def test_rename():
# create another deck # create another deck
id = d.decks.id("tmp") id = d.decks.id("tmp")
# we can't rename it if it conflicts # we can't rename it if it conflicts
assertException( assertException(Exception, lambda: d.decks.rename(d.decks.get(id), "foo"))
Exception, lambda: d.decks.rename(d.decks.get(id), "foo"))
# when renaming, the children should be renamed too # when renaming, the children should be renamed too
d.decks.id("one::two::three") d.decks.id("one::two::three")
id = d.decks.id("one") id = d.decks.id("one")
@ -102,62 +105,66 @@ def test_rename():
assertException(DeckRenameError, lambda: d.decks.rename(child, "PARENT::child")) assertException(DeckRenameError, lambda: d.decks.rename(child, "PARENT::child"))
def test_renameForDragAndDrop(): def test_renameForDragAndDrop():
d = getEmptyCol() d = getEmptyCol()
def deckNames(): def deckNames():
return [ name for name in sorted(d.decks.allNames()) if name != 'Default' ] return [name for name in sorted(d.decks.allNames()) if name != "Default"]
languages_did = d.decks.id('Languages') languages_did = d.decks.id("Languages")
chinese_did = d.decks.id('Chinese') chinese_did = d.decks.id("Chinese")
hsk_did = d.decks.id('Chinese::HSK') hsk_did = d.decks.id("Chinese::HSK")
# Renaming also renames children # Renaming also renames children
d.decks.renameForDragAndDrop(chinese_did, languages_did) d.decks.renameForDragAndDrop(chinese_did, languages_did)
assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
# Dragging a deck onto itself is a no-op # Dragging a deck onto itself is a no-op
d.decks.renameForDragAndDrop(languages_did, languages_did) d.decks.renameForDragAndDrop(languages_did, languages_did)
assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
# Dragging a deck onto its parent is a no-op # Dragging a deck onto its parent is a no-op
d.decks.renameForDragAndDrop(hsk_did, chinese_did) d.decks.renameForDragAndDrop(hsk_did, chinese_did)
assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
# Dragging a deck onto a descendant is a no-op # Dragging a deck onto a descendant is a no-op
d.decks.renameForDragAndDrop(languages_did, hsk_did) d.decks.renameForDragAndDrop(languages_did, hsk_did)
assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
# Can drag a grandchild onto its grandparent. It becomes a child # Can drag a grandchild onto its grandparent. It becomes a child
d.decks.renameForDragAndDrop(hsk_did, languages_did) d.decks.renameForDragAndDrop(hsk_did, languages_did)
assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::HSK' ] assert deckNames() == ["Languages", "Languages::Chinese", "Languages::HSK"]
# Can drag a deck onto its sibling # Can drag a deck onto its sibling
d.decks.renameForDragAndDrop(hsk_did, chinese_did) d.decks.renameForDragAndDrop(hsk_did, chinese_did)
assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
# Can drag a deck back to the top level # Can drag a deck back to the top level
d.decks.renameForDragAndDrop(chinese_did, None) d.decks.renameForDragAndDrop(chinese_did, None)
assert deckNames() == [ 'Chinese', 'Chinese::HSK', 'Languages' ] assert deckNames() == ["Chinese", "Chinese::HSK", "Languages"]
# Dragging a top level deck to the top level is a no-op # Dragging a top level deck to the top level is a no-op
d.decks.renameForDragAndDrop(chinese_did, None) d.decks.renameForDragAndDrop(chinese_did, None)
assert deckNames() == [ 'Chinese', 'Chinese::HSK', 'Languages' ] assert deckNames() == ["Chinese", "Chinese::HSK", "Languages"]
# can't drack a deck where sibling have same name # can't drack a deck where sibling have same name
new_hsk_did = d.decks.id("HSK") new_hsk_did = d.decks.id("HSK")
assertException(DeckRenameError, lambda: d.decks.renameForDragAndDrop(new_hsk_did, chinese_did)) assertException(
DeckRenameError, lambda: d.decks.renameForDragAndDrop(new_hsk_did, chinese_did)
)
d.decks.rem(new_hsk_did) d.decks.rem(new_hsk_did)
# can't drack a deck where sibling have same name different case # can't drack a deck where sibling have same name different case
new_hsk_did = d.decks.id("hsk") new_hsk_did = d.decks.id("hsk")
assertException(DeckRenameError, lambda: d.decks.renameForDragAndDrop(new_hsk_did, chinese_did)) assertException(
DeckRenameError, lambda: d.decks.renameForDragAndDrop(new_hsk_did, chinese_did)
)
d.decks.rem(new_hsk_did) d.decks.rem(new_hsk_did)
# '' is a convenient alias for the top level DID # '' is a convenient alias for the top level DID
d.decks.renameForDragAndDrop(hsk_did, '') d.decks.renameForDragAndDrop(hsk_did, "")
assert deckNames() == [ 'Chinese', 'HSK', 'Languages' ] assert deckNames() == ["Chinese", "HSK", "Languages"]
def test_check(): def test_check():
d = getEmptyCol() d = getEmptyCol()

View file

@ -13,20 +13,26 @@ deck = None
ds = None ds = None
testDir = os.path.dirname(__file__) testDir = os.path.dirname(__file__)
def setup1(): def setup1():
global deck global deck
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = "foo"; f['Back'] = "bar<br>"; f.tags = ["tag", "tag2"] f["Front"] = "foo"
f["Back"] = "bar<br>"
f.tags = ["tag", "tag2"]
deck.addNote(f) deck.addNote(f)
# with a different deck # with a different deck
f = deck.newNote() f = deck.newNote()
f['Front'] = "baz"; f['Back'] = "qux" f["Front"] = "baz"
f.model()['did'] = deck.decks.id("new deck") f["Back"] = "qux"
f.model()["did"] = deck.decks.id("new deck")
deck.addNote(f) deck.addNote(f)
########################################################################## ##########################################################################
@with_setup(setup1) @with_setup(setup1)
def test_export_anki(): def test_export_anki():
# create a new deck with its own conf to test conf copying # create a new deck with its own conf to test conf copying
@ -34,7 +40,7 @@ def test_export_anki():
dobj = deck.decks.get(did) dobj = deck.decks.get(did)
confId = deck.decks.confId("newconf") confId = deck.decks.confId("newconf")
conf = deck.decks.getConf(confId) conf = deck.decks.getConf(confId)
conf['new']['perDay'] = 5 conf["new"]["perDay"] = 5
deck.decks.save(conf) deck.decks.save(conf)
deck.decks.setConf(dobj, confId) deck.decks.setConf(dobj, confId)
# export # export
@ -46,7 +52,7 @@ def test_export_anki():
e.exportInto(newname) e.exportInto(newname)
# exporting should not have changed conf for original deck # exporting should not have changed conf for original deck
conf = deck.decks.confForDid(did) conf = deck.decks.confForDid(did)
assert conf['id'] != 1 assert conf["id"] != 1
# connect to new deck # connect to new deck
d2 = aopen(newname) d2 = aopen(newname)
assert d2.cardCount() == 2 assert d2.cardCount() == 2
@ -54,10 +60,10 @@ def test_export_anki():
did = d2.decks.id("test", create=False) did = d2.decks.id("test", create=False)
assert did assert did
conf2 = d2.decks.confForDid(did) conf2 = d2.decks.confForDid(did)
assert conf2['new']['perDay'] == 20 assert conf2["new"]["perDay"] == 20
dobj = d2.decks.get(did) dobj = d2.decks.get(did)
# conf should be 1 # conf should be 1
assert dobj['conf'] == 1 assert dobj["conf"] == 1
# try again, limited to a deck # try again, limited to a deck
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2") fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
newname = str(newname) newname = str(newname)
@ -68,13 +74,14 @@ def test_export_anki():
d2 = aopen(newname) d2 = aopen(newname)
assert d2.cardCount() == 1 assert d2.cardCount() == 1
@with_setup(setup1) @with_setup(setup1)
def test_export_ankipkg(): def test_export_ankipkg():
# add a test file to the media folder # add a test file to the media folder
with open(os.path.join(deck.media.dir(), "今日.mp3"), "w") as f: with open(os.path.join(deck.media.dir(), "今日.mp3"), "w") as f:
f.write("test") f.write("test")
n = deck.newNote() n = deck.newNote()
n['Front'] = '[sound:今日.mp3]' n["Front"] = "[sound:今日.mp3]"
deck.addNote(n) deck.addNote(n)
e = AnkiPackageExporter(deck) e = AnkiPackageExporter(deck)
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".apkg") fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".apkg")
@ -83,13 +90,14 @@ def test_export_ankipkg():
os.unlink(newname) os.unlink(newname)
e.exportInto(newname) e.exportInto(newname)
@with_setup(setup1) @with_setup(setup1)
def test_export_anki_due(): def test_export_anki_due():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = "foo" f["Front"] = "foo"
deck.addNote(f) deck.addNote(f)
deck.crt -= 86400*10 deck.crt -= 86400 * 10
deck.sched.reset() deck.sched.reset()
c = deck.sched.getCard() c = deck.sched.getCard()
deck.sched.answerCard(c, 3) deck.sched.answerCard(c, 3)
@ -115,6 +123,7 @@ def test_export_anki_due():
deck2.sched.reset() deck2.sched.reset()
assert c.due - deck2.sched.today == 1 assert c.due - deck2.sched.today == 1
# @with_setup(setup1) # @with_setup(setup1)
# def test_export_textcard(): # def test_export_textcard():
# e = TextCardExporter(deck) # e = TextCardExporter(deck)
@ -124,6 +133,7 @@ def test_export_anki_due():
# e.includeTags = True # e.includeTags = True
# e.exportInto(f) # e.exportInto(f)
@with_setup(setup1) @with_setup(setup1)
def test_export_textnote(): def test_export_textnote():
e = TextNoteExporter(deck) e = TextNoteExporter(deck)
@ -138,5 +148,6 @@ def test_export_textnote():
e.exportInto(f) e.exportInto(f)
assert open(f).readline() == "foo\tbar\n" assert open(f).readline() == "foo\tbar\n"
def test_exporters(): def test_exporters():
assert "*.apkg" in str(exporters()) assert "*.apkg" in str(exporters())

View file

@ -4,6 +4,7 @@ from nose2.tools.such import helper
from anki.find import Finder from anki.find import Finder
from tests.shared import getEmptyCol from tests.shared import getEmptyCol
def test_parse(): def test_parse():
f = Finder(None) f = Finder(None)
assert f._tokenize("hello world") == ["hello", "world"] assert f._tokenize("hello world") == ["hello", "world"]
@ -12,43 +13,54 @@ def test_parse():
assert f._tokenize("one --two") == ["one", "-", "two"] assert f._tokenize("one --two") == ["one", "-", "two"]
assert f._tokenize("one - two") == ["one", "-", "two"] assert f._tokenize("one - two") == ["one", "-", "two"]
assert f._tokenize("one or -two") == ["one", "or", "-", "two"] assert f._tokenize("one or -two") == ["one", "or", "-", "two"]
assert f._tokenize("'hello \"world\"'") == ["hello \"world\""] assert f._tokenize("'hello \"world\"'") == ['hello "world"']
assert f._tokenize('"hello world"') == ["hello world"] assert f._tokenize('"hello world"') == ["hello world"]
assert f._tokenize("one (two or ( three or four))") == [ assert f._tokenize("one (two or ( three or four))") == [
"one", "(", "two", "or", "(", "three", "or", "four", "one",
")", ")"] "(",
"two",
"or",
"(",
"three",
"or",
"four",
")",
")",
]
assert f._tokenize("embedded'string") == ["embedded'string"] assert f._tokenize("embedded'string") == ["embedded'string"]
assert f._tokenize("deck:'two words'") == ["deck:two words"] assert f._tokenize("deck:'two words'") == ["deck:two words"]
def test_findCards(): def test_findCards():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = 'dog' f["Front"] = "dog"
f['Back'] = 'cat' f["Back"] = "cat"
f.tags.append("monkey animal_1 * %") f.tags.append("monkey animal_1 * %")
f1id = f.id f1id = f.id
deck.addNote(f) deck.addNote(f)
firstCardId = f.cards()[0].id firstCardId = f.cards()[0].id
f = deck.newNote() f = deck.newNote()
f['Front'] = 'goats are fun' f["Front"] = "goats are fun"
f['Back'] = 'sheep' f["Back"] = "sheep"
f.tags.append("sheep goat horse animal11") f.tags.append("sheep goat horse animal11")
deck.addNote(f) deck.addNote(f)
f2id = f.id f2id = f.id
f = deck.newNote() f = deck.newNote()
f['Front'] = 'cat' f["Front"] = "cat"
f['Back'] = 'sheep' f["Back"] = "sheep"
deck.addNote(f) deck.addNote(f)
catCard = f.cards()[0] catCard = f.cards()[0]
m = deck.models.current(); mm = deck.models m = deck.models.current()
mm = deck.models
t = mm.newTemplate("Reverse") t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}" t["qfmt"] = "{{Back}}"
t['afmt'] = "{{Front}}" t["afmt"] = "{{Front}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
f = deck.newNote() f = deck.newNote()
f['Front'] = 'test' f["Front"] = "test"
f['Back'] = 'foo bar' f["Back"] = "foo bar"
deck.addNote(f) deck.addNote(f)
latestCardIds = [c.id for c in f.cards()] latestCardIds = [c.id for c in f.cards()]
# tag searches # tag searches
@ -66,9 +78,7 @@ def test_findCards():
assert len(deck.findCards("tag:sheep -tag:monkey")) == 1 assert len(deck.findCards("tag:sheep -tag:monkey")) == 1
assert len(deck.findCards("-tag:sheep")) == 4 assert len(deck.findCards("-tag:sheep")) == 4
deck.tags.bulkAdd(deck.db.list("select id from notes"), "foo bar") deck.tags.bulkAdd(deck.db.list("select id from notes"), "foo bar")
assert (len(deck.findCards("tag:foo")) == assert len(deck.findCards("tag:foo")) == len(deck.findCards("tag:bar")) == 5
len(deck.findCards("tag:bar")) ==
5)
deck.tags.bulkRem(deck.db.list("select id from notes"), "foo") deck.tags.bulkRem(deck.db.list("select id from notes"), "foo")
assert len(deck.findCards("tag:foo")) == 0 assert len(deck.findCards("tag:foo")) == 0
assert len(deck.findCards("tag:bar")) == 5 assert len(deck.findCards("tag:bar")) == 5
@ -86,7 +96,8 @@ def test_findCards():
c.flush() c.flush()
assert deck.findCards("is:review") == [c.id] assert deck.findCards("is:review") == [c.id]
assert deck.findCards("is:due") == [] assert deck.findCards("is:due") == []
c.due = 0; c.queue = 2 c.due = 0
c.queue = 2
c.flush() c.flush()
assert deck.findCards("is:due") == [c.id] assert deck.findCards("is:due") == [c.id]
assert len(deck.findCards("-is:due")) == 4 assert len(deck.findCards("-is:due")) == 4
@ -97,7 +108,7 @@ def test_findCards():
assert deck.findCards("is:suspended") == [c.id] assert deck.findCards("is:suspended") == [c.id]
# nids # nids
assert deck.findCards("nid:54321") == [] assert deck.findCards("nid:54321") == []
assert len(deck.findCards("nid:%d"%f.id)) == 2 assert len(deck.findCards("nid:%d" % f.id)) == 2
assert len(deck.findCards("nid:%d,%d" % (f1id, f2id))) == 2 assert len(deck.findCards("nid:%d,%d" % (f1id, f2id))) == 2
# templates # templates
with helper.assertRaises(Exception): with helper.assertRaises(Exception):
@ -115,16 +126,16 @@ def test_findCards():
assert len(deck.findCards("front:do")) == 0 assert len(deck.findCards("front:do")) == 0
assert len(deck.findCards("front:*")) == 5 assert len(deck.findCards("front:*")) == 5
# ordering # ordering
deck.conf['sortType'] = "noteCrt" deck.conf["sortType"] = "noteCrt"
assert deck.findCards("front:*", order=True)[-1] in latestCardIds assert deck.findCards("front:*", order=True)[-1] in latestCardIds
assert deck.findCards("", order=True)[-1] in latestCardIds assert deck.findCards("", order=True)[-1] in latestCardIds
deck.conf['sortType'] = "noteFld" deck.conf["sortType"] = "noteFld"
assert deck.findCards("", order=True)[0] == catCard.id assert deck.findCards("", order=True)[0] == catCard.id
assert deck.findCards("", order=True)[-1] in latestCardIds assert deck.findCards("", order=True)[-1] in latestCardIds
deck.conf['sortType'] = "cardMod" deck.conf["sortType"] = "cardMod"
assert deck.findCards("", order=True)[-1] in latestCardIds assert deck.findCards("", order=True)[-1] in latestCardIds
assert deck.findCards("", order=True)[0] == firstCardId assert deck.findCards("", order=True)[0] == firstCardId
deck.conf['sortBackwards'] = True deck.conf["sortBackwards"] = True
assert deck.findCards("", order=True)[0] in latestCardIds assert deck.findCards("", order=True)[0] in latestCardIds
# model # model
assert len(deck.findCards("note:basic")) == 5 assert len(deck.findCards("note:basic")) == 5
@ -140,25 +151,26 @@ def test_findCards():
deck.findCards("deck:*cefault") deck.findCards("deck:*cefault")
# full search # full search
f = deck.newNote() f = deck.newNote()
f['Front'] = 'hello<b>world</b>' f["Front"] = "hello<b>world</b>"
f['Back'] = 'abc' f["Back"] = "abc"
deck.addNote(f) deck.addNote(f)
# as it's the sort field, it matches # as it's the sort field, it matches
assert len(deck.findCards("helloworld")) == 2 assert len(deck.findCards("helloworld")) == 2
#assert len(deck.findCards("helloworld", full=True)) == 2 # assert len(deck.findCards("helloworld", full=True)) == 2
# if we put it on the back, it won't # if we put it on the back, it won't
(f['Front'], f['Back']) = (f['Back'], f['Front']) (f["Front"], f["Back"]) = (f["Back"], f["Front"])
f.flush() f.flush()
assert len(deck.findCards("helloworld")) == 0 assert len(deck.findCards("helloworld")) == 0
#assert len(deck.findCards("helloworld", full=True)) == 2 # assert len(deck.findCards("helloworld", full=True)) == 2
#assert len(deck.findCards("back:helloworld", full=True)) == 2 # assert len(deck.findCards("back:helloworld", full=True)) == 2
# searching for an invalid special tag should not error # searching for an invalid special tag should not error
with helper.assertRaises(Exception): with helper.assertRaises(Exception):
len(deck.findCards("is:invalid")) len(deck.findCards("is:invalid"))
# should be able to limit to parent deck, no children # should be able to limit to parent deck, no children
id = deck.db.scalar("select id from cards limit 1") id = deck.db.scalar("select id from cards limit 1")
deck.db.execute("update cards set did = ? where id = ?", deck.db.execute(
deck.decks.id("Default::Child"), id) "update cards set did = ? where id = ?", deck.decks.id("Default::Child"), id
)
assert len(deck.findCards("deck:default")) == 7 assert len(deck.findCards("deck:default")) == 7
assert len(deck.findCards("deck:default::child")) == 1 assert len(deck.findCards("deck:default::child")) == 1
assert len(deck.findCards("deck:default -deck:default::*")) == 6 assert len(deck.findCards("deck:default -deck:default::*")) == 6
@ -166,7 +178,9 @@ def test_findCards():
id = deck.db.scalar("select id from cards limit 1") id = deck.db.scalar("select id from cards limit 1")
deck.db.execute( deck.db.execute(
"update cards set queue=2, ivl=10, reps=20, due=30, factor=2200 " "update cards set queue=2, ivl=10, reps=20, due=30, factor=2200 "
"where id = ?", id) "where id = ?",
id,
)
assert len(deck.findCards("prop:ivl>5")) == 1 assert len(deck.findCards("prop:ivl>5")) == 1
assert len(deck.findCards("prop:ivl<5")) > 1 assert len(deck.findCards("prop:ivl<5")) > 1
assert len(deck.findCards("prop:ivl>=5")) == 1 assert len(deck.findCards("prop:ivl>=5")) == 1
@ -205,8 +219,8 @@ def test_findCards():
# empty field # empty field
assert len(deck.findCards("front:")) == 0 assert len(deck.findCards("front:")) == 0
f = deck.newNote() f = deck.newNote()
f['Front'] = '' f["Front"] = ""
f['Back'] = 'abc2' f["Back"] = "abc2"
assert deck.addNote(f) == 1 assert deck.addNote(f) == 1
assert len(deck.findCards("front:")) == 1 assert len(deck.findCards("front:")) == 1
# OR searches and nesting # OR searches and nesting
@ -220,8 +234,7 @@ def test_findCards():
assert len(deck.findCards("(()")) == 0 assert len(deck.findCards("(()")) == 0
# added # added
assert len(deck.findCards("added:0")) == 0 assert len(deck.findCards("added:0")) == 0
deck.db.execute("update cards set id = id - 86400*1000 where id = ?", deck.db.execute("update cards set id = id - 86400*1000 where id = ?", id)
id)
assert len(deck.findCards("added:1")) == deck.cardCount() - 1 assert len(deck.findCards("added:1")) == deck.cardCount() - 1
assert len(deck.findCards("added:2")) == deck.cardCount() assert len(deck.findCards("added:2")) == deck.cardCount()
# flag # flag
@ -230,50 +243,58 @@ def test_findCards():
with helper.assertRaises(Exception): with helper.assertRaises(Exception):
deck.findCards("flag:12") deck.findCards("flag:12")
def test_findReplace(): def test_findReplace():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = 'foo' f["Front"] = "foo"
f['Back'] = 'bar' f["Back"] = "bar"
deck.addNote(f) deck.addNote(f)
f2 = deck.newNote() f2 = deck.newNote()
f2['Front'] = 'baz' f2["Front"] = "baz"
f2['Back'] = 'foo' f2["Back"] = "foo"
deck.addNote(f2) deck.addNote(f2)
nids = [f.id, f2.id] nids = [f.id, f2.id]
# should do nothing # should do nothing
assert deck.findReplace(nids, "abc", "123") == 0 assert deck.findReplace(nids, "abc", "123") == 0
# global replace # global replace
assert deck.findReplace(nids, "foo", "qux") == 2 assert deck.findReplace(nids, "foo", "qux") == 2
f.load(); assert f['Front'] == "qux" f.load()
f2.load(); assert f2['Back'] == "qux" assert f["Front"] == "qux"
f2.load()
assert f2["Back"] == "qux"
# single field replace # single field replace
assert deck.findReplace(nids, "qux", "foo", field="Front") == 1 assert deck.findReplace(nids, "qux", "foo", field="Front") == 1
f.load(); assert f['Front'] == "foo" f.load()
f2.load(); assert f2['Back'] == "qux" assert f["Front"] == "foo"
f2.load()
assert f2["Back"] == "qux"
# regex replace # regex replace
assert deck.findReplace(nids, "B.r", "reg") == 0 assert deck.findReplace(nids, "B.r", "reg") == 0
f.load(); assert f['Back'] != "reg" f.load()
assert f["Back"] != "reg"
assert deck.findReplace(nids, "B.r", "reg", regex=True) == 1 assert deck.findReplace(nids, "B.r", "reg", regex=True) == 1
f.load(); assert f['Back'] == "reg" f.load()
assert f["Back"] == "reg"
def test_findDupes(): def test_findDupes():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = 'foo' f["Front"] = "foo"
f['Back'] = 'bar' f["Back"] = "bar"
deck.addNote(f) deck.addNote(f)
f2 = deck.newNote() f2 = deck.newNote()
f2['Front'] = 'baz' f2["Front"] = "baz"
f2['Back'] = 'bar' f2["Back"] = "bar"
deck.addNote(f2) deck.addNote(f2)
f3 = deck.newNote() f3 = deck.newNote()
f3['Front'] = 'quux' f3["Front"] = "quux"
f3['Back'] = 'bar' f3["Back"] = "bar"
deck.addNote(f3) deck.addNote(f3)
f4 = deck.newNote() f4 = deck.newNote()
f4['Front'] = 'quuux' f4["Front"] = "quuux"
f4['Back'] = 'nope' f4["Back"] = "nope"
deck.addNote(f4) deck.addNote(f4)
r = deck.findDupes("Back") r = deck.findDupes("Back")
assert r[0][0] == "bar" assert r[0][0] == "bar"

View file

@ -1,9 +1,11 @@
from tests.shared import assertException, getEmptyCol from tests.shared import assertException, getEmptyCol
def test_flags(): def test_flags():
col = getEmptyCol() col = getEmptyCol()
n = col.newNote() n = col.newNote()
n['Front'] = "one"; n['Back'] = "two" n["Front"] = "one"
n["Back"] = "two"
cnt = col.addNote(n) cnt = col.addNote(n)
c = n.cards()[0] c = n.cards()[0]
# make sure higher bits are preserved # make sure higher bits are preserved

View file

@ -1,22 +1,28 @@
# coding: utf-8 # coding: utf-8
import os import os
from tests.shared import getUpgradeDeckPath, getEmptyCol from tests.shared import getUpgradeDeckPath, getEmptyCol
from anki.utils import ids2str from anki.utils import ids2str
from anki.importing import Anki2Importer, TextImporter, \ from anki.importing import (
SupermemoXmlImporter, MnemosyneImporter, AnkiPackageImporter Anki2Importer,
TextImporter,
SupermemoXmlImporter,
MnemosyneImporter,
AnkiPackageImporter,
)
testDir = os.path.dirname(__file__) testDir = os.path.dirname(__file__)
srcNotes=None srcNotes = None
srcCards=None srcCards = None
def test_anki2_mediadupes(): def test_anki2_mediadupes():
tmp = getEmptyCol() tmp = getEmptyCol()
# add a note that references a sound # add a note that references a sound
n = tmp.newNote() n = tmp.newNote()
n['Front'] = "[sound:foo.mp3]" n["Front"] = "[sound:foo.mp3]"
mid = n.model()['id'] mid = n.model()["id"]
tmp.addNote(n) tmp.addNote(n)
# add that sound to media folder # add that sound to media folder
with open(os.path.join(tmp.media.dir(), "foo.mp3"), "w") as f: with open(os.path.join(tmp.media.dir(), "foo.mp3"), "w") as f:
@ -41,8 +47,7 @@ def test_anki2_mediadupes():
f.write("bar") f.write("bar")
imp = Anki2Importer(empty, tmp.path) imp = Anki2Importer(empty, tmp.path)
imp.run() imp.run()
assert sorted(os.listdir(empty.media.dir())) == [ assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid]
"foo.mp3", "foo_%s.mp3" % mid]
n = empty.getNote(empty.db.scalar("select id from notes")) n = empty.getNote(empty.db.scalar("select id from notes"))
assert "_" in n.fields[0] assert "_" in n.fields[0]
# if the localized media file already exists, we rewrite the note and # if the localized media file already exists, we rewrite the note and
@ -52,25 +57,24 @@ def test_anki2_mediadupes():
f.write("bar") f.write("bar")
imp = Anki2Importer(empty, tmp.path) imp = Anki2Importer(empty, tmp.path)
imp.run() imp.run()
assert sorted(os.listdir(empty.media.dir())) == [ assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid]
"foo.mp3", "foo_%s.mp3" % mid] assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid]
assert sorted(os.listdir(empty.media.dir())) == [
"foo.mp3", "foo_%s.mp3" % mid]
n = empty.getNote(empty.db.scalar("select id from notes")) n = empty.getNote(empty.db.scalar("select id from notes"))
assert "_" in n.fields[0] assert "_" in n.fields[0]
def test_apkg(): def test_apkg():
tmp = getEmptyCol() tmp = getEmptyCol()
apkg = str(os.path.join(testDir, "support/media.apkg")) apkg = str(os.path.join(testDir, "support/media.apkg"))
imp = AnkiPackageImporter(tmp, apkg) imp = AnkiPackageImporter(tmp, apkg)
assert os.listdir(tmp.media.dir()) == [] assert os.listdir(tmp.media.dir()) == []
imp.run() imp.run()
assert os.listdir(tmp.media.dir()) == ['foo.wav'] assert os.listdir(tmp.media.dir()) == ["foo.wav"]
# importing again should be idempotent in terms of media # importing again should be idempotent in terms of media
tmp.remCards(tmp.db.list("select id from cards")) tmp.remCards(tmp.db.list("select id from cards"))
imp = AnkiPackageImporter(tmp, apkg) imp = AnkiPackageImporter(tmp, apkg)
imp.run() imp.run()
assert os.listdir(tmp.media.dir()) == ['foo.wav'] assert os.listdir(tmp.media.dir()) == ["foo.wav"]
# but if the local file has different data, it will rename # but if the local file has different data, it will rename
tmp.remCards(tmp.db.list("select id from cards")) tmp.remCards(tmp.db.list("select id from cards"))
with open(os.path.join(tmp.media.dir(), "foo.wav"), "w") as f: with open(os.path.join(tmp.media.dir(), "foo.wav"), "w") as f:
@ -79,6 +83,7 @@ def test_apkg():
imp.run() imp.run()
assert len(os.listdir(tmp.media.dir())) == 2 assert len(os.listdir(tmp.media.dir())) == 2
def test_anki2_diffmodel_templates(): def test_anki2_diffmodel_templates():
# different from the above as this one tests only the template text being # different from the above as this one tests only the template text being
# changed, not the number of cards/fields # changed, not the number of cards/fields
@ -94,11 +99,12 @@ def test_anki2_diffmodel_templates():
imp.dupeOnSchemaChange = True imp.dupeOnSchemaChange = True
imp.run() imp.run()
# collection should contain the note we imported # collection should contain the note we imported
assert(dst.noteCount() == 1) assert dst.noteCount() == 1
# the front template should contain the text added in the 2nd package # the front template should contain the text added in the 2nd package
tcid = dst.findCards("")[0] # only 1 note in collection tcid = dst.findCards("")[0] # only 1 note in collection
tnote = dst.getCard(tcid).note() tnote = dst.getCard(tcid).note()
assert("Changed Front Template" in dst.findTemplates(tnote)[0]['qfmt']) assert "Changed Front Template" in dst.findTemplates(tnote)[0]["qfmt"]
def test_anki2_updates(): def test_anki2_updates():
# create a new empty deck # create a new empty deck
@ -127,6 +133,7 @@ def test_anki2_updates():
assert dst.noteCount() == 1 assert dst.noteCount() == 1
assert dst.db.scalar("select flds from notes").startswith("goodbye") assert dst.db.scalar("select flds from notes").startswith("goodbye")
def test_csv(): def test_csv():
deck = getEmptyCol() deck = getEmptyCol()
file = str(os.path.join(testDir, "support/text-2fields.txt")) file = str(os.path.join(testDir, "support/text-2fields.txt"))
@ -147,7 +154,7 @@ def test_csv():
n.flush() n.flush()
i.run() i.run()
n.load() n.load()
assert n.tags == ['test'] assert n.tags == ["test"]
# if add-only mode, count will be 0 # if add-only mode, count will be 0
i.importMode = 1 i.importMode = 1
i.run() i.run()
@ -161,6 +168,7 @@ def test_csv():
assert deck.cardCount() == 11 assert deck.cardCount() == 11
deck.close() deck.close()
def test_csv2(): def test_csv2():
deck = getEmptyCol() deck = getEmptyCol()
mm = deck.models mm = deck.models
@ -169,9 +177,9 @@ def test_csv2():
mm.addField(m, f) mm.addField(m, f)
mm.save(m) mm.save(m)
n = deck.newNote() n = deck.newNote()
n['Front'] = "1" n["Front"] = "1"
n['Back'] = "2" n["Back"] = "2"
n['Three'] = "3" n["Three"] = "3"
deck.addNote(n) deck.addNote(n)
# an update with unmapped fields should not clobber those fields # an update with unmapped fields should not clobber those fields
file = str(os.path.join(testDir, "support/text-update.txt")) file = str(os.path.join(testDir, "support/text-update.txt"))
@ -179,16 +187,17 @@ def test_csv2():
i.initMapping() i.initMapping()
i.run() i.run()
n.load() n.load()
assert n['Front'] == "1" assert n["Front"] == "1"
assert n['Back'] == "x" assert n["Back"] == "x"
assert n['Three'] == "3" assert n["Three"] == "3"
deck.close() deck.close()
def test_supermemo_xml_01_unicode(): def test_supermemo_xml_01_unicode():
deck = getEmptyCol() deck = getEmptyCol()
file = str(os.path.join(testDir, "support/supermemo1.xml")) file = str(os.path.join(testDir, "support/supermemo1.xml"))
i = SupermemoXmlImporter(deck, file) i = SupermemoXmlImporter(deck, file)
#i.META.logToStdOutput = True # i.META.logToStdOutput = True
i.run() i.run()
assert i.total == 1 assert i.total == 1
cid = deck.db.scalar("select id from cards") cid = deck.db.scalar("select id from cards")
@ -198,6 +207,7 @@ def test_supermemo_xml_01_unicode():
assert c.reps == 7 assert c.reps == 7
deck.close() deck.close()
def test_mnemo(): def test_mnemo():
deck = getEmptyCol() deck = getEmptyCol()
file = str(os.path.join(testDir, "support/mnemo.db")) file = str(os.path.join(testDir, "support/mnemo.db"))

View file

@ -4,17 +4,19 @@ import os
import shutil import shutil
from tests.shared import getEmptyCol from tests.shared import getEmptyCol
from anki.utils import stripHTML from anki.utils import stripHTML
def test_latex(): def test_latex():
d = getEmptyCol() d = getEmptyCol()
# change latex cmd to simulate broken build # change latex cmd to simulate broken build
import anki.latex import anki.latex
anki.latex.pngCommands[0][0] = "nolatex" anki.latex.pngCommands[0][0] = "nolatex"
# add a note with latex # add a note with latex
f = d.newNote() f = d.newNote()
f['Front'] = "[latex]hello[/latex]" f["Front"] = "[latex]hello[/latex]"
d.addNote(f) d.addNote(f)
# but since latex couldn't run, there's nothing there # but since latex couldn't run, there's nothing there
assert len(os.listdir(d.media.dir())) == 0 assert len(os.listdir(d.media.dir())) == 0
@ -34,13 +36,13 @@ def test_latex():
assert ".png" in f.cards()[0].q() assert ".png" in f.cards()[0].q()
# adding new notes should cause generation on question display # adding new notes should cause generation on question display
f = d.newNote() f = d.newNote()
f['Front'] = "[latex]world[/latex]" f["Front"] = "[latex]world[/latex]"
d.addNote(f) d.addNote(f)
f.cards()[0].q() f.cards()[0].q()
assert len(os.listdir(d.media.dir())) == 2 assert len(os.listdir(d.media.dir())) == 2
# another note with the same media should reuse # another note with the same media should reuse
f = d.newNote() f = d.newNote()
f['Front'] = " [latex]world[/latex]" f["Front"] = " [latex]world[/latex]"
d.addNote(f) d.addNote(f)
assert len(os.listdir(d.media.dir())) == 2 assert len(os.listdir(d.media.dir())) == 2
oldcard = f.cards()[0] oldcard = f.cards()[0]
@ -49,7 +51,7 @@ def test_latex():
# missing media will show the latex # missing media will show the latex
anki.latex.build = False anki.latex.build = False
f = d.newNote() f = d.newNote()
f['Front'] = "[latex]foo[/latex]" f["Front"] = "[latex]foo[/latex]"
d.addNote(f) d.addNote(f)
assert len(os.listdir(d.media.dir())) == 2 assert len(os.listdir(d.media.dir())) == 2
assert stripHTML(f.cards()[0].q()) == "[latex]foo[/latex]" assert stripHTML(f.cards()[0].q()) == "[latex]foo[/latex]"
@ -86,10 +88,11 @@ def test_latex():
(result, msg) = _test_includes_bad_command("\\emph") (result, msg) = _test_includes_bad_command("\\emph")
assert not result, msg assert not result, msg
def _test_includes_bad_command(bad): def _test_includes_bad_command(bad):
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = '[latex]%s[/latex]' % bad f["Front"] = "[latex]%s[/latex]" % bad
d.addNote(f) d.addNote(f)
q = f.cards()[0].q() q = f.cards()[0].q()
return ("'%s' is not allowed on cards" % bad in q, "Card content: %s" % q) return ("'%s' is not allowed on cards" % bad in q, "Card content: %s" % q)

View file

@ -23,6 +23,7 @@ def test_add():
f.write("world") f.write("world")
assert d.media.addFile(path) == "foo (1).jpg" assert d.media.addFile(path) == "foo (1).jpg"
def test_strings(): def test_strings():
d = getEmptyCol() d = getEmptyCol()
mf = d.media.filesInStr mf = d.media.filesInStr
@ -31,12 +32,16 @@ def test_strings():
assert mf(mid, "aoeu<img src='foo.jpg'>ao") == ["foo.jpg"] assert mf(mid, "aoeu<img src='foo.jpg'>ao") == ["foo.jpg"]
assert mf(mid, "aoeu<img src='foo.jpg' style='test'>ao") == ["foo.jpg"] assert mf(mid, "aoeu<img src='foo.jpg' style='test'>ao") == ["foo.jpg"]
assert mf(mid, "aoeu<img src='foo.jpg'><img src=\"bar.jpg\">ao") == [ assert mf(mid, "aoeu<img src='foo.jpg'><img src=\"bar.jpg\">ao") == [
"foo.jpg", "bar.jpg"] "foo.jpg",
"bar.jpg",
]
assert mf(mid, "aoeu<img src=foo.jpg style=bar>ao") == ["foo.jpg"] assert mf(mid, "aoeu<img src=foo.jpg style=bar>ao") == ["foo.jpg"]
assert mf(mid, "<img src=one><img src=two>") == ["one", "two"] assert mf(mid, "<img src=one><img src=two>") == ["one", "two"]
assert mf(mid, "aoeu<img src=\"foo.jpg\">ao") == ["foo.jpg"] assert mf(mid, 'aoeu<img src="foo.jpg">ao') == ["foo.jpg"]
assert mf(mid, "aoeu<img src=\"foo.jpg\"><img class=yo src=fo>ao") == [ assert mf(mid, 'aoeu<img src="foo.jpg"><img class=yo src=fo>ao') == [
"foo.jpg", "fo"] "foo.jpg",
"fo",
]
assert mf(mid, "aou[sound:foo.mp3]aou") == ["foo.mp3"] assert mf(mid, "aou[sound:foo.mp3]aou") == ["foo.mp3"]
sp = d.media.strip sp = d.media.strip
assert sp("aoeu") == "aoeu" assert sp("aoeu") == "aoeu"
@ -47,6 +52,7 @@ def test_strings():
assert es("<img src='http://foo.com'>") == "<img src='http://foo.com'>" assert es("<img src='http://foo.com'>") == "<img src='http://foo.com'>"
assert es('<img src="foo bar.jpg">') == '<img src="foo%20bar.jpg">' assert es('<img src="foo bar.jpg">') == '<img src="foo%20bar.jpg">'
def test_deckIntegration(): def test_deckIntegration():
d = getEmptyCol() d = getEmptyCol()
# create a media dir # create a media dir
@ -56,11 +62,13 @@ def test_deckIntegration():
d.media.addFile(file) d.media.addFile(file)
# add a note which references it # add a note which references it
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "<img src='fake.png'>" f["Front"] = "one"
f["Back"] = "<img src='fake.png'>"
d.addNote(f) d.addNote(f)
# and one which references a non-existent file # and one which references a non-existent file
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "<img src='fake2.png'>" f["Front"] = "one"
f["Back"] = "<img src='fake2.png'>"
d.addNote(f) d.addNote(f)
# and add another file which isn't used # and add another file which isn't used
with open(os.path.join(d.media.dir(), "foo.jpg"), "w") as f: with open(os.path.join(d.media.dir(), "foo.jpg"), "w") as f:
@ -70,12 +78,16 @@ def test_deckIntegration():
assert ret[0] == ["fake2.png"] assert ret[0] == ["fake2.png"]
assert ret[1] == ["foo.jpg"] assert ret[1] == ["foo.jpg"]
def test_changes(): def test_changes():
d = getEmptyCol() d = getEmptyCol()
def added(): def added():
return d.media.db.execute("select fname from media where csum is not null") return d.media.db.execute("select fname from media where csum is not null")
def removed(): def removed():
return d.media.db.execute("select fname from media where csum is null") return d.media.db.execute("select fname from media where csum is null")
assert not list(added()) assert not list(added())
assert not list(removed()) assert not list(removed())
# add a file # add a file
@ -97,26 +109,27 @@ def test_changes():
assert not list(removed()) assert not list(removed())
# but if we add another file, it will # but if we add another file, it will
time.sleep(1) time.sleep(1)
with open(path+"2", "w") as f: with open(path + "2", "w") as f:
f.write("yo") f.write("yo")
d.media.findChanges() d.media.findChanges()
assert len(list(added())) == 2 assert len(list(added())) == 2
assert not list(removed()) assert not list(removed())
# deletions should get noticed too # deletions should get noticed too
time.sleep(1) time.sleep(1)
os.unlink(path+"2") os.unlink(path + "2")
d.media.findChanges() d.media.findChanges()
assert len(list(added())) == 1 assert len(list(added())) == 1
assert len(list(removed())) == 1 assert len(list(removed())) == 1
def test_illegal(): def test_illegal():
d = getEmptyCol() d = getEmptyCol()
aString = "a:b|cd\\e/f\0g*h" aString = "a:b|cd\\e/f\0g*h"
good = "abcdefgh" good = "abcdefgh"
assert d.media.stripIllegal(aString) == good assert d.media.stripIllegal(aString) == good
for c in aString: for c in aString:
bad = d.media.hasIllegal("somestring"+c+"morestring") bad = d.media.hasIllegal("somestring" + c + "morestring")
if bad: if bad:
assert(c not in good) assert c not in good
else: else:
assert(c in good) assert c in good

View file

@ -6,39 +6,42 @@ from anki.consts import MODEL_CLOZE
from anki.utils import stripHTML, joinFields, isWin from anki.utils import stripHTML, joinFields, isWin
import anki.template import anki.template
def test_modelDelete(): def test_modelDelete():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = '1' f["Front"] = "1"
f['Back'] = '2' f["Back"] = "2"
deck.addNote(f) deck.addNote(f)
assert deck.cardCount() == 1 assert deck.cardCount() == 1
deck.models.rem(deck.models.current()) deck.models.rem(deck.models.current())
assert deck.cardCount() == 0 assert deck.cardCount() == 0
def test_modelCopy(): def test_modelCopy():
deck = getEmptyCol() deck = getEmptyCol()
m = deck.models.current() m = deck.models.current()
m2 = deck.models.copy(m) m2 = deck.models.copy(m)
assert m2['name'] == "Basic copy" assert m2["name"] == "Basic copy"
assert m2['id'] != m['id'] assert m2["id"] != m["id"]
assert len(m2['flds']) == 2 assert len(m2["flds"]) == 2
assert len(m['flds']) == 2 assert len(m["flds"]) == 2
assert len(m2['flds']) == len(m['flds']) assert len(m2["flds"]) == len(m["flds"])
assert len(m['tmpls']) == 1 assert len(m["tmpls"]) == 1
assert len(m2['tmpls']) == 1 assert len(m2["tmpls"]) == 1
assert deck.models.scmhash(m) == deck.models.scmhash(m2) assert deck.models.scmhash(m) == deck.models.scmhash(m2)
def test_fields(): def test_fields():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = '1' f["Front"] = "1"
f['Back'] = '2' f["Back"] = "2"
d.addNote(f) d.addNote(f)
m = d.models.current() m = d.models.current()
# make sure renaming a field updates the templates # make sure renaming a field updates the templates
d.models.renameField(m, m['flds'][0], "NewFront") d.models.renameField(m, m["flds"][0], "NewFront")
assert "{{NewFront}}" in m['tmpls'][0]['qfmt'] assert "{{NewFront}}" in m["tmpls"][0]["qfmt"]
h = d.models.scmhash(m) h = d.models.scmhash(m)
# add a field # add a field
f = d.models.newField("foo") f = d.models.newField("foo")
@ -47,44 +50,46 @@ def test_fields():
assert d.models.scmhash(m) != h assert d.models.scmhash(m) != h
# rename it # rename it
d.models.renameField(m, f, "bar") d.models.renameField(m, f, "bar")
assert d.getNote(d.models.nids(m)[0])['bar'] == '' assert d.getNote(d.models.nids(m)[0])["bar"] == ""
# delete back # delete back
d.models.remField(m, m['flds'][1]) d.models.remField(m, m["flds"][1])
assert d.getNote(d.models.nids(m)[0]).fields == ["1", ""] assert d.getNote(d.models.nids(m)[0]).fields == ["1", ""]
# move 0 -> 1 # move 0 -> 1
d.models.moveField(m, m['flds'][0], 1) d.models.moveField(m, m["flds"][0], 1)
assert d.getNote(d.models.nids(m)[0]).fields == ["", "1"] assert d.getNote(d.models.nids(m)[0]).fields == ["", "1"]
# move 1 -> 0 # move 1 -> 0
d.models.moveField(m, m['flds'][1], 0) d.models.moveField(m, m["flds"][1], 0)
assert d.getNote(d.models.nids(m)[0]).fields == ["1", ""] assert d.getNote(d.models.nids(m)[0]).fields == ["1", ""]
# add another and put in middle # add another and put in middle
f = d.models.newField("baz") f = d.models.newField("baz")
d.models.addField(m, f) d.models.addField(m, f)
f = d.getNote(d.models.nids(m)[0]) f = d.getNote(d.models.nids(m)[0])
f['baz'] = "2" f["baz"] = "2"
f.flush() f.flush()
assert d.getNote(d.models.nids(m)[0]).fields == ["1", "", "2"] assert d.getNote(d.models.nids(m)[0]).fields == ["1", "", "2"]
# move 2 -> 1 # move 2 -> 1
d.models.moveField(m, m['flds'][2], 1) d.models.moveField(m, m["flds"][2], 1)
assert d.getNote(d.models.nids(m)[0]).fields == ["1", "2", ""] assert d.getNote(d.models.nids(m)[0]).fields == ["1", "2", ""]
# move 0 -> 2 # move 0 -> 2
d.models.moveField(m, m['flds'][0], 2) d.models.moveField(m, m["flds"][0], 2)
assert d.getNote(d.models.nids(m)[0]).fields == ["2", "", "1"] assert d.getNote(d.models.nids(m)[0]).fields == ["2", "", "1"]
# move 0 -> 1 # move 0 -> 1
d.models.moveField(m, m['flds'][0], 1) d.models.moveField(m, m["flds"][0], 1)
assert d.getNote(d.models.nids(m)[0]).fields == ["", "2", "1"] assert d.getNote(d.models.nids(m)[0]).fields == ["", "2", "1"]
def test_templates(): def test_templates():
d = getEmptyCol() d = getEmptyCol()
m = d.models.current(); mm = d.models m = d.models.current()
mm = d.models
t = mm.newTemplate("Reverse") t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}" t["qfmt"] = "{{Back}}"
t['afmt'] = "{{Front}}" t["afmt"] = "{{Front}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
f = d.newNote() f = d.newNote()
f['Front'] = '1' f["Front"] = "1"
f['Back'] = '2' f["Back"] = "2"
d.addNote(f) d.addNote(f)
assert d.cardCount() == 2 assert d.cardCount() == 2
(c, c2) = f.cards() (c, c2) = f.cards()
@ -93,11 +98,12 @@ def test_templates():
assert c2.ord == 1 assert c2.ord == 1
# switch templates # switch templates
d.models.moveTemplate(m, c.template(), 1) d.models.moveTemplate(m, c.template(), 1)
c.load(); c2.load() c.load()
c2.load()
assert c.ord == 1 assert c.ord == 1
assert c2.ord == 0 assert c2.ord == 0
# removing a template should delete its cards # removing a template should delete its cards
assert d.models.remTemplate(m, m['tmpls'][0]) assert d.models.remTemplate(m, m["tmpls"][0])
assert d.cardCount() == 1 assert d.cardCount() == 1
# and should have updated the other cards' ordinals # and should have updated the other cards' ordinals
c = f.cards()[0] c = f.cards()[0]
@ -106,64 +112,67 @@ def test_templates():
# it shouldn't be possible to orphan notes by removing templates # it shouldn't be possible to orphan notes by removing templates
t = mm.newTemplate("template name") t = mm.newTemplate("template name")
mm.addTemplate(m, t) mm.addTemplate(m, t)
assert not d.models.remTemplate(m, m['tmpls'][0]) assert not d.models.remTemplate(m, m["tmpls"][0])
def test_cloze_ordinals(): def test_cloze_ordinals():
d = getEmptyCol() d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze")) d.models.setCurrent(d.models.byName("Cloze"))
m = d.models.current(); mm = d.models m = d.models.current()
mm = d.models
#We replace the default Cloze template
# We replace the default Cloze template
t = mm.newTemplate("ChainedCloze") t = mm.newTemplate("ChainedCloze")
t['qfmt'] = "{{text:cloze:Text}}" t["qfmt"] = "{{text:cloze:Text}}"
t['afmt'] = "{{text:cloze:Text}}" t["afmt"] = "{{text:cloze:Text}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
d.models.remTemplate(m, m['tmpls'][0]) d.models.remTemplate(m, m["tmpls"][0])
f = d.newNote() f = d.newNote()
f['Text'] = '{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}' f["Text"] = "{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}"
d.addNote(f) d.addNote(f)
assert d.cardCount() == 2 assert d.cardCount() == 2
(c, c2) = f.cards() (c, c2) = f.cards()
# first card should have first ord # first card should have first ord
assert c.ord == 0 assert c.ord == 0
assert c2.ord == 1 assert c2.ord == 1
def test_text(): def test_text():
d = getEmptyCol() d = getEmptyCol()
m = d.models.current() m = d.models.current()
m['tmpls'][0]['qfmt'] = "{{text:Front}}" m["tmpls"][0]["qfmt"] = "{{text:Front}}"
d.models.save(m) d.models.save(m)
f = d.newNote() f = d.newNote()
f['Front'] = 'hello<b>world' f["Front"] = "hello<b>world"
d.addNote(f) d.addNote(f)
assert "helloworld" in f.cards()[0].q() assert "helloworld" in f.cards()[0].q()
def test_cloze(): def test_cloze():
d = getEmptyCol() d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze")) d.models.setCurrent(d.models.byName("Cloze"))
f = d.newNote() f = d.newNote()
assert f.model()['name'] == "Cloze" assert f.model()["name"] == "Cloze"
# a cloze model with no clozes is not empty # a cloze model with no clozes is not empty
f['Text'] = 'nothing' f["Text"] = "nothing"
assert d.addNote(f) assert d.addNote(f)
# try with one cloze # try with one cloze
f = d.newNote() f = d.newNote()
f['Text'] = "hello {{c1::world}}" f["Text"] = "hello {{c1::world}}"
assert d.addNote(f) == 1 assert d.addNote(f) == 1
assert "hello <span class=cloze>[...]</span>" in f.cards()[0].q() assert "hello <span class=cloze>[...]</span>" in f.cards()[0].q()
assert "hello <span class=cloze>world</span>" in f.cards()[0].a() assert "hello <span class=cloze>world</span>" in f.cards()[0].a()
# and with a comment # and with a comment
f = d.newNote() f = d.newNote()
f['Text'] = "hello {{c1::world::typical}}" f["Text"] = "hello {{c1::world::typical}}"
assert d.addNote(f) == 1 assert d.addNote(f) == 1
assert "<span class=cloze>[typical]</span>" in f.cards()[0].q() assert "<span class=cloze>[typical]</span>" in f.cards()[0].q()
assert "<span class=cloze>world</span>" in f.cards()[0].a() assert "<span class=cloze>world</span>" in f.cards()[0].a()
# and with 2 clozes # and with 2 clozes
f = d.newNote() f = d.newNote()
f['Text'] = "hello {{c1::world}} {{c2::bar}}" f["Text"] = "hello {{c1::world}} {{c2::bar}}"
assert d.addNote(f) == 2 assert d.addNote(f) == 2
(c1, c2) = f.cards() (c1, c2) = f.cards()
assert "<span class=cloze>[...]</span> bar" in c1.q() assert "<span class=cloze>[...]</span> bar" in c1.q()
@ -173,25 +182,27 @@ def test_cloze():
# if there are multiple answers for a single cloze, they are given in a # if there are multiple answers for a single cloze, they are given in a
# list # list
f = d.newNote() f = d.newNote()
f['Text'] = "a {{c1::b}} {{c1::c}}" f["Text"] = "a {{c1::b}} {{c1::c}}"
assert d.addNote(f) == 1 assert d.addNote(f) == 1
assert "<span class=cloze>b</span> <span class=cloze>c</span>" in ( assert "<span class=cloze>b</span> <span class=cloze>c</span>" in (f.cards()[0].a())
f.cards()[0].a())
# if we add another cloze, a card should be generated # if we add another cloze, a card should be generated
cnt = d.cardCount() cnt = d.cardCount()
f['Text'] = "{{c2::hello}} {{c1::foo}}" f["Text"] = "{{c2::hello}} {{c1::foo}}"
f.flush() f.flush()
assert d.cardCount() == cnt + 1 assert d.cardCount() == cnt + 1
# 0 or negative indices are not supported # 0 or negative indices are not supported
f['Text'] += "{{c0::zero}} {{c-1:foo}}" f["Text"] += "{{c0::zero}} {{c-1:foo}}"
f.flush() f.flush()
assert len(f.cards()) == 2 assert len(f.cards()) == 2
def test_cloze_mathjax(): def test_cloze_mathjax():
d = getEmptyCol() d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze")) d.models.setCurrent(d.models.byName("Cloze"))
f = d.newNote() f = d.newNote()
f['Text'] = r'{{c1::ok}} \(2^2\) {{c2::not ok}} \(2^{{c3::2}}\) \(x^3\) {{c4::blah}} {{c5::text with \(x^2\) jax}}' f[
"Text"
] = r"{{c1::ok}} \(2^2\) {{c2::not ok}} \(2^{{c3::2}}\) \(x^3\) {{c4::blah}} {{c5::text with \(x^2\) jax}}"
assert d.addNote(f) assert d.addNote(f)
assert len(f.cards()) == 5 assert len(f.cards()) == 5
assert "class=cloze" in f.cards()[0].q() assert "class=cloze" in f.cards()[0].q()
@ -201,56 +212,70 @@ def test_cloze_mathjax():
assert "class=cloze" in f.cards()[4].q() assert "class=cloze" in f.cards()[4].q()
f = d.newNote() f = d.newNote()
f['Text'] = r'\(a\) {{c1::b}} \[ {{c1::c}} \]' f["Text"] = r"\(a\) {{c1::b}} \[ {{c1::c}} \]"
assert d.addNote(f) assert d.addNote(f)
assert len(f.cards()) == 1 assert len(f.cards()) == 1
assert f.cards()[0].q().endswith('\(a\) <span class=cloze>[...]</span> \[ [...] \]') assert f.cards()[0].q().endswith("\(a\) <span class=cloze>[...]</span> \[ [...] \]")
def test_chained_mods(): def test_chained_mods():
d = getEmptyCol() d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze")) d.models.setCurrent(d.models.byName("Cloze"))
m = d.models.current(); mm = d.models m = d.models.current()
mm = d.models
#We replace the default Cloze template
# We replace the default Cloze template
t = mm.newTemplate("ChainedCloze") t = mm.newTemplate("ChainedCloze")
t['qfmt'] = "{{cloze:text:Text}}" t["qfmt"] = "{{cloze:text:Text}}"
t['afmt'] = "{{cloze:text:Text}}" t["afmt"] = "{{cloze:text:Text}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
d.models.remTemplate(m, m['tmpls'][0]) d.models.remTemplate(m, m["tmpls"][0])
f = d.newNote() f = d.newNote()
q1 = '<span style=\"color:red\">phrase</span>' q1 = '<span style="color:red">phrase</span>'
a1 = '<b>sentence</b>' a1 = "<b>sentence</b>"
q2 = '<span style=\"color:red\">en chaine</span>' q2 = '<span style="color:red">en chaine</span>'
a2 = '<i>chained</i>' a2 = "<i>chained</i>"
f['Text'] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (q1,a1,q2,a2) f["Text"] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (
q1,
a1,
q2,
a2,
)
assert d.addNote(f) == 1 assert d.addNote(f) == 1
assert "This <span class=cloze>[sentence]</span> demonstrates <span class=cloze>[chained]</span> clozes." in f.cards()[0].q() assert (
assert "This <span class=cloze>phrase</span> demonstrates <span class=cloze>en chaine</span> clozes." in f.cards()[0].a() "This <span class=cloze>[sentence]</span> demonstrates <span class=cloze>[chained]</span> clozes."
in f.cards()[0].q()
)
assert (
"This <span class=cloze>phrase</span> demonstrates <span class=cloze>en chaine</span> clozes."
in f.cards()[0].a()
)
def test_modelChange(): def test_modelChange():
deck = getEmptyCol() deck = getEmptyCol()
basic = deck.models.byName("Basic") basic = deck.models.byName("Basic")
cloze = deck.models.byName("Cloze") cloze = deck.models.byName("Cloze")
# enable second template and add a note # enable second template and add a note
m = deck.models.current(); mm = deck.models m = deck.models.current()
mm = deck.models
t = mm.newTemplate("Reverse") t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}" t["qfmt"] = "{{Back}}"
t['afmt'] = "{{Front}}" t["afmt"] = "{{Front}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
f = deck.newNote() f = deck.newNote()
f['Front'] = 'f' f["Front"] = "f"
f['Back'] = 'b123' f["Back"] = "b123"
deck.addNote(f) deck.addNote(f)
# switch fields # switch fields
map = {0: 1, 1: 0} map = {0: 1, 1: 0}
deck.models.change(basic, [f.id], basic, map, None) deck.models.change(basic, [f.id], basic, map, None)
f.load() f.load()
assert f['Front'] == 'b123' assert f["Front"] == "b123"
assert f['Back'] == 'f' assert f["Back"] == "f"
# switch cards # switch cards
c0 = f.cards()[0] c0 = f.cards()[0]
c1 = f.cards()[1] c1 = f.cards()[1]
@ -259,7 +284,9 @@ def test_modelChange():
assert c0.ord == 0 assert c0.ord == 0
assert c1.ord == 1 assert c1.ord == 1
deck.models.change(basic, [f.id], basic, None, map) deck.models.change(basic, [f.id], basic, None, map)
f.load(); c0.load(); c1.load() f.load()
c0.load()
c1.load()
assert "f" in c0.q() assert "f" in c0.q()
assert "b123" in c1.q() assert "b123" in c1.q()
assert c0.ord == 1 assert c0.ord == 1
@ -283,30 +310,31 @@ def test_modelChange():
# but we have two cards, as a new one was generated # but we have two cards, as a new one was generated
assert len(f.cards()) == 2 assert len(f.cards()) == 2
# an unmapped field becomes blank # an unmapped field becomes blank
assert f['Front'] == 'b123' assert f["Front"] == "b123"
assert f['Back'] == 'f' assert f["Back"] == "f"
deck.models.change(basic, [f.id], basic, map, None) deck.models.change(basic, [f.id], basic, map, None)
f.load() f.load()
assert f['Front'] == '' assert f["Front"] == ""
assert f['Back'] == 'f' assert f["Back"] == "f"
# another note to try model conversion # another note to try model conversion
f = deck.newNote() f = deck.newNote()
f['Front'] = 'f2' f["Front"] = "f2"
f['Back'] = 'b2' f["Back"] = "b2"
deck.addNote(f) deck.addNote(f)
assert deck.models.useCount(basic) == 2 assert deck.models.useCount(basic) == 2
assert deck.models.useCount(cloze) == 0 assert deck.models.useCount(cloze) == 0
map = {0: 0, 1: 1} map = {0: 0, 1: 1}
deck.models.change(basic, [f.id], cloze, map, map) deck.models.change(basic, [f.id], cloze, map, map)
f.load() f.load()
assert f['Text'] == "f2" assert f["Text"] == "f2"
assert len(f.cards()) == 2 assert len(f.cards()) == 2
# back the other way, with deletion of second ord # back the other way, with deletion of second ord
deck.models.remTemplate(basic, basic['tmpls'][1]) deck.models.remTemplate(basic, basic["tmpls"][1])
assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 2 assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 2
deck.models.change(cloze, [f.id], basic, map, map) deck.models.change(cloze, [f.id], basic, map, map)
assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 1 assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 1
def test_templates(): def test_templates():
d = dict(Foo="x", Bar="y") d = dict(Foo="x", Bar="y")
assert anki.template.render("{{Foo}}", d) == "x" assert anki.template.render("{{Foo}}", d) == "x"
@ -315,68 +343,72 @@ def test_templates():
assert anki.template.render("{{#Bar}}{{#Foo}}{{Foo}}{{/Foo}}{{/Bar}}", d) == "x" assert anki.template.render("{{#Bar}}{{#Foo}}{{Foo}}{{/Foo}}{{/Bar}}", d) == "x"
assert anki.template.render("{{#Baz}}{{#Foo}}{{Foo}}{{/Foo}}{{/Baz}}", d) == "" assert anki.template.render("{{#Baz}}{{#Foo}}{{Foo}}{{/Foo}}{{/Baz}}", d) == ""
def test_availOrds(): def test_availOrds():
d = getEmptyCol() d = getEmptyCol()
m = d.models.current(); mm = d.models m = d.models.current()
t = m['tmpls'][0] mm = d.models
t = m["tmpls"][0]
f = d.newNote() f = d.newNote()
f['Front'] = "1" f["Front"] = "1"
# simple templates # simple templates
assert mm.availOrds(m, joinFields(f.fields)) == [0] assert mm.availOrds(m, joinFields(f.fields)) == [0]
t['qfmt'] = "{{Back}}" t["qfmt"] = "{{Back}}"
mm.save(m, templates=True) mm.save(m, templates=True)
assert not mm.availOrds(m, joinFields(f.fields)) assert not mm.availOrds(m, joinFields(f.fields))
# AND # AND
t['qfmt'] = "{{#Front}}{{#Back}}{{Front}}{{/Back}}{{/Front}}" t["qfmt"] = "{{#Front}}{{#Back}}{{Front}}{{/Back}}{{/Front}}"
mm.save(m, templates=True) mm.save(m, templates=True)
assert not mm.availOrds(m, joinFields(f.fields)) assert not mm.availOrds(m, joinFields(f.fields))
t['qfmt'] = "{{#Front}}\n{{#Back}}\n{{Front}}\n{{/Back}}\n{{/Front}}" t["qfmt"] = "{{#Front}}\n{{#Back}}\n{{Front}}\n{{/Back}}\n{{/Front}}"
mm.save(m, templates=True) mm.save(m, templates=True)
assert not mm.availOrds(m, joinFields(f.fields)) assert not mm.availOrds(m, joinFields(f.fields))
# OR # OR
t['qfmt'] = "{{Front}}\n{{Back}}" t["qfmt"] = "{{Front}}\n{{Back}}"
mm.save(m, templates=True) mm.save(m, templates=True)
assert mm.availOrds(m, joinFields(f.fields)) == [0] assert mm.availOrds(m, joinFields(f.fields)) == [0]
t['Front'] = "" t["Front"] = ""
t['Back'] = "1" t["Back"] = "1"
assert mm.availOrds(m, joinFields(f.fields)) == [0] assert mm.availOrds(m, joinFields(f.fields)) == [0]
def test_req(): def test_req():
def reqSize(model): def reqSize(model):
if model['type'] == MODEL_CLOZE: if model["type"] == MODEL_CLOZE:
return return
assert (len(model['tmpls']) == len(model['req'])) assert len(model["tmpls"]) == len(model["req"])
d = getEmptyCol() d = getEmptyCol()
mm = d.models mm = d.models
basic = mm.byName("Basic") basic = mm.byName("Basic")
assert 'req' in basic assert "req" in basic
reqSize(basic) reqSize(basic)
r = basic['req'][0] r = basic["req"][0]
assert r[0] == 0 assert r[0] == 0
assert r[1] in ("any", "all") assert r[1] in ("any", "all")
assert r[2] == [0] assert r[2] == [0]
opt = mm.byName("Basic (optional reversed card)") opt = mm.byName("Basic (optional reversed card)")
reqSize(opt) reqSize(opt)
r = opt['req'][0] r = opt["req"][0]
assert r[1] in ("any", "all") assert r[1] in ("any", "all")
assert r[2] == [0] assert r[2] == [0]
assert opt['req'][1] == [1, 'all', [1, 2]] assert opt["req"][1] == [1, "all", [1, 2]]
#testing any # testing any
opt['tmpls'][1]['qfmt'] = "{{Back}}{{Add Reverse}}" opt["tmpls"][1]["qfmt"] = "{{Back}}{{Add Reverse}}"
mm.save(opt, templates=True) mm.save(opt, templates=True)
assert opt['req'][1] == [1, 'any', [1, 2]] assert opt["req"][1] == [1, "any", [1, 2]]
#testing None # testing None
opt['tmpls'][1]['qfmt'] = "{{^Add Reverse}}{{Back}}{{/Add Reverse}}" opt["tmpls"][1]["qfmt"] = "{{^Add Reverse}}{{Back}}{{/Add Reverse}}"
mm.save(opt, templates=True) mm.save(opt, templates=True)
assert opt['req'][1] == [1, 'none', []] assert opt["req"][1] == [1, "none", []]
opt = mm.byName("Basic (type in the answer)") opt = mm.byName("Basic (type in the answer)")
reqSize(opt) reqSize(opt)
r = opt['req'][0] r = opt["req"][0]
assert r[1] in ("any", "all") assert r[1] in ("any", "all")
assert r[2] == [0] assert r[2] == [0]
# def test_updatereqs_performance(): # def test_updatereqs_performance():
# import time # import time
# d = getEmptyCol() # d = getEmptyCol()

View file

@ -5,35 +5,41 @@ import copy
from anki.consts import STARTING_FACTOR from anki.consts import STARTING_FACTOR
from tests.shared import getEmptyCol as getEmptyColOrig from tests.shared import getEmptyCol as getEmptyColOrig
from anki.utils import intTime from anki.utils import intTime
from anki.hooks import addHook from anki.hooks import addHook
def getEmptyCol(): def getEmptyCol():
col = getEmptyColOrig() col = getEmptyColOrig()
col.changeSchedulerVer(1) col.changeSchedulerVer(1)
return col return col
def test_clock(): def test_clock():
d = getEmptyCol() d = getEmptyCol()
if (d.sched.dayCutoff - intTime()) < 10*60: if (d.sched.dayCutoff - intTime()) < 10 * 60:
raise Exception("Unit tests will fail around the day rollover.") raise Exception("Unit tests will fail around the day rollover.")
def checkRevIvl(d, c, targetIvl): def checkRevIvl(d, c, targetIvl):
min, max = d.sched._fuzzIvlRange(targetIvl) min, max = d.sched._fuzzIvlRange(targetIvl)
return min <= c.ivl <= max return min <= c.ivl <= max
def test_basics(): def test_basics():
d = getEmptyCol() d = getEmptyCol()
d.reset() d.reset()
assert not d.sched.getCard() assert not d.sched.getCard()
def test_new(): def test_new():
d = getEmptyCol() d = getEmptyCol()
d.reset() d.reset()
assert d.sched.newCount == 0 assert d.sched.newCount == 0
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert d.sched.newCount == 1 assert d.sched.newCount == 1
@ -71,15 +77,16 @@ def test_new():
# assert qs[n] in c.q() # assert qs[n] in c.q()
# d.sched.answerCard(c, 2) # d.sched.answerCard(c, 2)
def test_newLimits(): def test_newLimits():
d = getEmptyCol() d = getEmptyCol()
# add some notes # add some notes
g2 = d.decks.id("Default::foo") g2 = d.decks.id("Default::foo")
for i in range(30): for i in range(30):
f = d.newNote() f = d.newNote()
f['Front'] = str(i) f["Front"] = str(i)
if i > 4: if i > 4:
f.model()['did'] = g2 f.model()["did"] = g2
d.addNote(f) d.addNote(f)
# give the child deck a different configuration # give the child deck a different configuration
c2 = d.decks.confId("new conf") c2 = d.decks.confId("new conf")
@ -92,33 +99,36 @@ def test_newLimits():
assert c.did == 1 assert c.did == 1
# limit the parent to 10 cards, meaning we get 10 in total # limit the parent to 10 cards, meaning we get 10 in total
conf1 = d.decks.confForDid(1) conf1 = d.decks.confForDid(1)
conf1['new']['perDay'] = 10 conf1["new"]["perDay"] = 10
d.reset() d.reset()
assert d.sched.newCount == 10 assert d.sched.newCount == 10
# if we limit child to 4, we should get 9 # if we limit child to 4, we should get 9
conf2 = d.decks.confForDid(g2) conf2 = d.decks.confForDid(g2)
conf2['new']['perDay'] = 4 conf2["new"]["perDay"] = 4
d.reset() d.reset()
assert d.sched.newCount == 9 assert d.sched.newCount == 9
def test_newBoxes(): def test_newBoxes():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
c = d.sched.getCard() c = d.sched.getCard()
d.sched._cardConf(c)['new']['delays'] = [1,2,3,4,5] d.sched._cardConf(c)["new"]["delays"] = [1, 2, 3, 4, 5]
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
# should handle gracefully # should handle gracefully
d.sched._cardConf(c)['new']['delays'] = [1] d.sched._cardConf(c)["new"]["delays"] = [1]
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
def test_learn(): def test_learn():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
f = d.addNote(f) f = d.addNote(f)
# set as a learn card and rebuild queues # set as a learn card and rebuild queues
d.db.execute("update cards set queue=0, type=0") d.db.execute("update cards set queue=0, type=0")
@ -126,12 +136,12 @@ def test_learn():
# sched.getCard should return it, since it's due in the past # sched.getCard should return it, since it's due in the past
c = d.sched.getCard() c = d.sched.getCard()
assert c assert c
d.sched._cardConf(c)['new']['delays'] = [0.5, 3, 10] d.sched._cardConf(c)["new"]["delays"] = [0.5, 3, 10]
# fail it # fail it
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
# it should have three reps left to graduation # it should have three reps left to graduation
assert c.left%1000 == 3 assert c.left % 1000 == 3
assert c.left//1000 == 3 assert c.left // 1000 == 3
# it should by due in 30 seconds # it should by due in 30 seconds
t = round(c.due - time.time()) t = round(c.due - time.time())
assert t >= 25 and t <= 40 assert t >= 25 and t <= 40
@ -139,8 +149,8 @@ def test_learn():
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
# it should by due in 3 minutes # it should by due in 3 minutes
assert round(c.due - time.time()) in (179, 180) assert round(c.due - time.time()) in (179, 180)
assert c.left%1000 == 2 assert c.left % 1000 == 2
assert c.left//1000 == 2 assert c.left // 1000 == 2
# check log is accurate # check log is accurate
log = d.db.first("select * from revlog order by id desc") log = d.db.first("select * from revlog order by id desc")
assert log[3] == 2 assert log[3] == 2
@ -150,8 +160,8 @@ def test_learn():
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
# it should by due in 10 minutes # it should by due in 10 minutes
assert round(c.due - time.time()) in (599, 600) assert round(c.due - time.time()) in (599, 600)
assert c.left%1000 == 1 assert c.left % 1000 == 1
assert c.left//1000 == 1 assert c.left // 1000 == 1
# the next pass should graduate the card # the next pass should graduate the card
assert c.queue == 1 assert c.queue == 1
assert c.type == 1 assert c.type == 1
@ -159,7 +169,7 @@ def test_learn():
assert c.queue == 2 assert c.queue == 2
assert c.type == 2 assert c.type == 2
# should be due tomorrow, with an interval of 1 # should be due tomorrow, with an interval of 1
assert c.due == d.sched.today+1 assert c.due == d.sched.today + 1
assert c.ivl == 1 assert c.ivl == 1
# or normal removal # or normal removal
c.type = 0 c.type = 0
@ -188,14 +198,15 @@ def test_learn():
assert c.queue == 2 assert c.queue == 2
assert c.due == 321 assert c.due == 321
def test_learn_collapsed(): def test_learn_collapsed():
d = getEmptyCol() d = getEmptyCol()
# add 2 notes # add 2 notes
f = d.newNote() f = d.newNote()
f['Front'] = "1" f["Front"] = "1"
f = d.addNote(f) f = d.addNote(f)
f = d.newNote() f = d.newNote()
f['Front'] = "2" f["Front"] = "2"
f = d.addNote(f) f = d.addNote(f)
# set as a learn card and rebuild queues # set as a learn card and rebuild queues
d.db.execute("update cards set queue=0, type=0") d.db.execute("update cards set queue=0, type=0")
@ -214,27 +225,28 @@ def test_learn_collapsed():
c = d.sched.getCard() c = d.sched.getCard()
assert not c.q().endswith("2") assert not c.q().endswith("2")
def test_learn_day(): def test_learn_day():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
f = d.addNote(f) f = d.addNote(f)
d.sched.reset() d.sched.reset()
c = d.sched.getCard() c = d.sched.getCard()
d.sched._cardConf(c)['new']['delays'] = [1, 10, 1440, 2880] d.sched._cardConf(c)["new"]["delays"] = [1, 10, 1440, 2880]
# pass it # pass it
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
# two reps to graduate, 1 more today # two reps to graduate, 1 more today
assert c.left%1000 == 3 assert c.left % 1000 == 3
assert c.left//1000 == 1 assert c.left // 1000 == 1
assert d.sched.counts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
c = d.sched.getCard() c = d.sched.getCard()
ni = d.sched.nextIvl ni = d.sched.nextIvl
assert ni(c, 2) == 86400 assert ni(c, 2) == 86400
# answering it will place it in queue 3 # answering it will place it in queue 3
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
assert c.due == d.sched.today+1 assert c.due == d.sched.today + 1
assert c.queue == 3 assert c.queue == 3
assert not d.sched.getCard() assert not d.sched.getCard()
# for testing, move it back a day # for testing, move it back a day
@ -244,7 +256,7 @@ def test_learn_day():
assert d.sched.counts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
c = d.sched.getCard() c = d.sched.getCard()
# nextIvl should work # nextIvl should work
assert ni(c, 2) == 86400*2 assert ni(c, 2) == 86400 * 2
# if we fail it, it should be back in the correct queue # if we fail it, it should be back in the correct queue
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.queue == 1 assert c.queue == 1
@ -266,17 +278,19 @@ def test_learn_day():
c.flush() c.flush()
d.reset() d.reset()
assert d.sched.counts() == (0, 0, 1) assert d.sched.counts() == (0, 0, 1)
d.sched._cardConf(c)['lapse']['delays'] = [1440] d.sched._cardConf(c)["lapse"]["delays"] = [1440]
c = d.sched.getCard() c = d.sched.getCard()
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.queue == 3 assert c.queue == 3
assert d.sched.counts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
def test_reviews(): def test_reviews():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
# set the card up as a review card, due 8 days ago # set the card up as a review card, due 8 days ago
c = f.cards()[0] c = f.cards()[0]
@ -295,7 +309,7 @@ def test_reviews():
################################################## ##################################################
# different delay to new # different delay to new
d.reset() d.reset()
d.sched._cardConf(c)['lapse']['delays'] = [2, 20] d.sched._cardConf(c)["lapse"]["delays"] = [2, 20]
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.queue == 1 assert c.queue == 1
# it should be due tomorrow, with an interval of 1 # it should be due tomorrow, with an interval of 1
@ -313,7 +327,7 @@ def test_reviews():
# check ests. # check ests.
ni = d.sched.nextIvl ni = d.sched.nextIvl
assert ni(c, 1) == 120 assert ni(c, 1) == 120
assert ni(c, 2) == 20*60 assert ni(c, 2) == 20 * 60
# try again with an ease of 2 instead # try again with an ease of 2 instead
################################################## ##################################################
c = copy.copy(cardcopy) c = copy.copy(cardcopy)
@ -355,8 +369,10 @@ def test_reviews():
c.flush() c.flush()
# steup hook # steup hook
hooked = [] hooked = []
def onLeech(card): def onLeech(card):
hooked.append(1) hooked.append(1)
addHook("leech", onLeech) addHook("leech", onLeech)
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert hooked assert hooked
@ -364,10 +380,11 @@ def test_reviews():
c.load() c.load()
assert c.queue == -1 assert c.queue == -1
def test_button_spacing(): def test_button_spacing():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# 1 day ivl review card due now # 1 day ivl review card due now
c = f.cards()[0] c = f.cards()[0]
@ -384,13 +401,14 @@ def test_button_spacing():
assert ni(c, 3) == "3 days" assert ni(c, 3) == "3 days"
assert ni(c, 4) == "4 days" assert ni(c, 4) == "4 days"
def test_overdue_lapse(): def test_overdue_lapse():
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2 # disabled in commit 3069729776990980f34c25be66410e947e9d51a2
return return
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# simulate a review that was lapsed and is now due for its normal review # simulate a review that was lapsed and is now due for its normal review
c = f.cards()[0] c = f.cards()[0]
@ -419,13 +437,15 @@ def test_overdue_lapse():
d.sched.reset() d.sched.reset()
assert d.sched.counts() == (0, 0, 1) assert d.sched.counts() == (0, 0, 1)
def test_finished(): def test_finished():
d = getEmptyCol() d = getEmptyCol()
# nothing due # nothing due
assert "Congratulations" in d.sched.finishedMsg() assert "Congratulations" in d.sched.finishedMsg()
assert "limit" not in d.sched.finishedMsg() assert "limit" not in d.sched.finishedMsg()
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
# have a new card # have a new card
assert "new cards available" in d.sched.finishedMsg() assert "new cards available" in d.sched.finishedMsg()
@ -438,44 +458,46 @@ def test_finished():
assert "Congratulations" in d.sched.finishedMsg() assert "Congratulations" in d.sched.finishedMsg()
assert "limit" not in d.sched.finishedMsg() assert "limit" not in d.sched.finishedMsg()
def test_nextIvl(): def test_nextIvl():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
conf = d.decks.confForDid(1) conf = d.decks.confForDid(1)
conf['new']['delays'] = [0.5, 3, 10] conf["new"]["delays"] = [0.5, 3, 10]
conf['lapse']['delays'] = [1, 5, 9] conf["lapse"]["delays"] = [1, 5, 9]
c = d.sched.getCard() c = d.sched.getCard()
# new cards # new cards
################################################## ##################################################
ni = d.sched.nextIvl ni = d.sched.nextIvl
assert ni(c, 1) == 30 assert ni(c, 1) == 30
assert ni(c, 2) == 180 assert ni(c, 2) == 180
assert ni(c, 3) == 4*86400 assert ni(c, 3) == 4 * 86400
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
# cards in learning # cards in learning
################################################## ##################################################
assert ni(c, 1) == 30 assert ni(c, 1) == 30
assert ni(c, 2) == 180 assert ni(c, 2) == 180
assert ni(c, 3) == 4*86400 assert ni(c, 3) == 4 * 86400
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
assert ni(c, 1) == 30 assert ni(c, 1) == 30
assert ni(c, 2) == 600 assert ni(c, 2) == 600
assert ni(c, 3) == 4*86400 assert ni(c, 3) == 4 * 86400
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
# normal graduation is tomorrow # normal graduation is tomorrow
assert ni(c, 2) == 1*86400 assert ni(c, 2) == 1 * 86400
assert ni(c, 3) == 4*86400 assert ni(c, 3) == 4 * 86400
# lapsed cards # lapsed cards
################################################## ##################################################
c.type = 2 c.type = 2
c.ivl = 100 c.ivl = 100
c.factor = STARTING_FACTOR c.factor = STARTING_FACTOR
assert ni(c, 1) == 60 assert ni(c, 1) == 60
assert ni(c, 2) == 100*86400 assert ni(c, 2) == 100 * 86400
assert ni(c, 3) == 100*86400 assert ni(c, 3) == 100 * 86400
# review cards # review cards
################################################## ##################################################
c.queue = 2 c.queue = 2
@ -484,8 +506,8 @@ def test_nextIvl():
# failing it should put it at 60s # failing it should put it at 60s
assert ni(c, 1) == 60 assert ni(c, 1) == 60
# or 1 day if relearn is false # or 1 day if relearn is false
d.sched._cardConf(c)['lapse']['delays']=[] d.sched._cardConf(c)["lapse"]["delays"] = []
assert ni(c, 1) == 1*86400 assert ni(c, 1) == 1 * 86400
# (* 100 1.2 86400)10368000.0 # (* 100 1.2 86400)10368000.0
assert ni(c, 2) == 10368000 assert ni(c, 2) == 10368000
# (* 100 2.5 86400)21600000.0 # (* 100 2.5 86400)21600000.0
@ -494,10 +516,11 @@ def test_nextIvl():
assert ni(c, 4) == 28080000 assert ni(c, 4) == 28080000
assert d.sched.nextIvlStr(c, 4) == "10.8 months" assert d.sched.nextIvlStr(c, 4) == "10.8 months"
def test_misc(): def test_misc():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
# burying # burying
@ -508,10 +531,11 @@ def test_misc():
d.reset() d.reset()
assert d.sched.getCard() assert d.sched.getCard()
def test_suspend(): def test_suspend():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
# suspending # suspending
@ -525,7 +549,11 @@ def test_suspend():
d.reset() d.reset()
assert d.sched.getCard() assert d.sched.getCard()
# should cope with rev cards being relearnt # should cope with rev cards being relearnt
c.due = 0; c.ivl = 100; c.type = 2; c.queue = 2; c.flush() c.due = 0
c.ivl = 100
c.type = 2
c.queue = 2
c.flush()
d.reset() d.reset()
c = d.sched.getCard() c = d.sched.getCard()
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
@ -551,10 +579,11 @@ def test_suspend():
assert c.due == 1 assert c.due == 1
assert c.did == 1 assert c.did == 1
def test_cram(): def test_cram():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.ivl = 100 c.ivl = 100
@ -566,7 +595,7 @@ def test_cram():
c.startTimer() c.startTimer()
c.flush() c.flush()
d.reset() d.reset()
assert d.sched.counts() == (0,0,0) assert d.sched.counts() == (0, 0, 0)
cardcopy = copy.copy(c) cardcopy = copy.copy(c)
# create a dynamic deck and refresh it # create a dynamic deck and refresh it
did = d.decks.newDyn("Cram") did = d.decks.newDyn("Cram")
@ -575,18 +604,18 @@ def test_cram():
# should appear as new in the deck list # should appear as new in the deck list
assert sorted(d.sched.deckDueList())[0][4] == 1 assert sorted(d.sched.deckDueList())[0][4] == 1
# and should appear in the counts # and should appear in the counts
assert d.sched.counts() == (1,0,0) assert d.sched.counts() == (1, 0, 0)
# grab it and check estimates # grab it and check estimates
c = d.sched.getCard() c = d.sched.getCard()
assert d.sched.answerButtons(c) == 2 assert d.sched.answerButtons(c) == 2
assert d.sched.nextIvl(c, 1) == 600 assert d.sched.nextIvl(c, 1) == 600
assert d.sched.nextIvl(c, 2) == 138*60*60*24 assert d.sched.nextIvl(c, 2) == 138 * 60 * 60 * 24
cram = d.decks.get(did) cram = d.decks.get(did)
cram['delays'] = [1, 10] cram["delays"] = [1, 10]
assert d.sched.answerButtons(c) == 3 assert d.sched.answerButtons(c) == 3
assert d.sched.nextIvl(c, 1) == 60 assert d.sched.nextIvl(c, 1) == 60
assert d.sched.nextIvl(c, 2) == 600 assert d.sched.nextIvl(c, 2) == 600
assert d.sched.nextIvl(c, 3) == 138*60*60*24 assert d.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
# elapsed time was 75 days # elapsed time was 75 days
# factor = 2.5+1.2/2 = 1.85 # factor = 2.5+1.2/2 = 1.85
@ -595,12 +624,11 @@ def test_cram():
assert c.odue == 138 assert c.odue == 138
assert c.queue == 1 assert c.queue == 1
# should be logged as a cram rep # should be logged as a cram rep
assert d.db.scalar( assert d.db.scalar("select type from revlog order by id desc limit 1") == 3
"select type from revlog order by id desc limit 1") == 3
# check ivls again # check ivls again
assert d.sched.nextIvl(c, 1) == 60 assert d.sched.nextIvl(c, 1) == 60
assert d.sched.nextIvl(c, 2) == 138*60*60*24 assert d.sched.nextIvl(c, 2) == 138 * 60 * 60 * 24
assert d.sched.nextIvl(c, 3) == 138*60*60*24 assert d.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24
# when it graduates, due is updated # when it graduates, due is updated
c = d.sched.getCard() c = d.sched.getCard()
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
@ -616,7 +644,7 @@ def test_cram():
# check ivls again - passing should be idempotent # check ivls again - passing should be idempotent
assert d.sched.nextIvl(c, 1) == 60 assert d.sched.nextIvl(c, 1) == 60
assert d.sched.nextIvl(c, 2) == 600 assert d.sched.nextIvl(c, 2) == 600
assert d.sched.nextIvl(c, 3) == 138*60*60*24 assert d.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
assert c.ivl == 138 assert c.ivl == 138
assert c.odue == 138 assert c.odue == 138
@ -630,20 +658,20 @@ def test_cram():
assert len(d.sched.deckDueList()) == 1 assert len(d.sched.deckDueList()) == 1
c.load() c.load()
assert c.ivl == 1 assert c.ivl == 1
assert c.due == d.sched.today+1 assert c.due == d.sched.today + 1
# make it due # make it due
d.reset() d.reset()
assert d.sched.counts() == (0,0,0) assert d.sched.counts() == (0, 0, 0)
c.due = -5 c.due = -5
c.ivl = 100 c.ivl = 100
c.flush() c.flush()
d.reset() d.reset()
assert d.sched.counts() == (0,0,1) assert d.sched.counts() == (0, 0, 1)
# cram again # cram again
did = d.decks.newDyn("Cram") did = d.decks.newDyn("Cram")
d.sched.rebuildDyn(did) d.sched.rebuildDyn(did)
d.reset() d.reset()
assert d.sched.counts() == (0,0,1) assert d.sched.counts() == (0, 0, 1)
c.load() c.load()
assert d.sched.answerButtons(c) == 4 assert d.sched.answerButtons(c) == 4
# add a sibling so we can test minSpace, etc # add a sibling so we can test minSpace, etc
@ -661,10 +689,11 @@ def test_cram():
# it should have been moved back to the original deck # it should have been moved back to the original deck
assert c.did == 1 assert c.did == 1
def test_cram_rem(): def test_cram_rem():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
oldDue = f.cards()[0].due oldDue = f.cards()[0].due
did = d.decks.newDyn("Cram") did = d.decks.newDyn("Cram")
@ -681,16 +710,17 @@ def test_cram_rem():
assert c.type == c.queue == 0 assert c.type == c.queue == 0
assert c.due == oldDue assert c.due == oldDue
def test_cram_resched(): def test_cram_resched():
# add card # add card
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# cram deck # cram deck
did = d.decks.newDyn("Cram") did = d.decks.newDyn("Cram")
cram = d.decks.get(did) cram = d.decks.get(did)
cram['resched'] = False cram["resched"] = False
d.sched.rebuildDyn(did) d.sched.rebuildDyn(did)
d.reset() d.reset()
# graduate should return it to new # graduate should return it to new
@ -786,22 +816,25 @@ def test_cram_resched():
# d.sched.answerCard(c, 2) # d.sched.answerCard(c, 2)
# print c.__dict__ # print c.__dict__
def test_ordcycle(): def test_ordcycle():
d = getEmptyCol() d = getEmptyCol()
# add two more templates and set second active # add two more templates and set second active
m = d.models.current(); mm = d.models m = d.models.current()
mm = d.models
t = mm.newTemplate("Reverse") t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}" t["qfmt"] = "{{Back}}"
t['afmt'] = "{{Front}}" t["afmt"] = "{{Front}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
t = mm.newTemplate("f2") t = mm.newTemplate("f2")
t['qfmt'] = "{{Front}}" t["qfmt"] = "{{Front}}"
t['afmt'] = "{{Back}}" t["afmt"] = "{{Back}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
# create a new note; it should have 3 cards # create a new note; it should have 3 cards
f = d.newNote() f = d.newNote()
f['Front'] = "1"; f['Back'] = "1" f["Front"] = "1"
f["Back"] = "1"
d.addNote(f) d.addNote(f)
assert d.cardCount() == 3 assert d.cardCount() == 3
d.reset() d.reset()
@ -810,10 +843,12 @@ def test_ordcycle():
assert d.sched.getCard().ord == 1 assert d.sched.getCard().ord == 1
assert d.sched.getCard().ord == 2 assert d.sched.getCard().ord == 2
def test_counts_idx(): def test_counts_idx():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert d.sched.counts() == (1, 0, 0) assert d.sched.counts() == (1, 0, 0)
@ -832,10 +867,11 @@ def test_counts_idx():
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert d.sched.counts() == (0, 2, 0) assert d.sched.counts() == (0, 2, 0)
def test_repCounts(): def test_repCounts():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
# lrnReps should be accurate on pass/fail # lrnReps should be accurate on pass/fail
@ -853,7 +889,7 @@ def test_repCounts():
d.sched.answerCard(d.sched.getCard(), 2) d.sched.answerCard(d.sched.getCard(), 2)
assert d.sched.counts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
# initial pass should be correct too # initial pass should be correct too
@ -865,14 +901,14 @@ def test_repCounts():
assert d.sched.counts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
# immediate graduate should work # immediate graduate should work
f = d.newNote() f = d.newNote()
f['Front'] = "three" f["Front"] = "three"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
d.sched.answerCard(d.sched.getCard(), 3) d.sched.answerCard(d.sched.getCard(), 3)
assert d.sched.counts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
# and failing a review should too # and failing a review should too
f = d.newNote() f = d.newNote()
f['Front'] = "three" f["Front"] = "three"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.type = 2 c.type = 2
@ -884,12 +920,13 @@ def test_repCounts():
d.sched.answerCard(d.sched.getCard(), 1) d.sched.answerCard(d.sched.getCard(), 1)
assert d.sched.counts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
def test_timing(): def test_timing():
d = getEmptyCol() d = getEmptyCol()
# add a few review cards, due today # add a few review cards, due today
for i in range(5): for i in range(5):
f = d.newNote() f = d.newNote()
f['Front'] = "num"+str(i) f["Front"] = "num" + str(i)
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.type = 2 c.type = 2
@ -900,7 +937,7 @@ def test_timing():
d.reset() d.reset()
c = d.sched.getCard() c = d.sched.getCard()
# set a a fail delay of 1 second so we don't have to wait # 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._cardConf(c)["lapse"]["delays"][0] = 1 / 60.0
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
# the next card should be another review # the next card should be another review
c = d.sched.getCard() c = d.sched.getCard()
@ -910,11 +947,12 @@ def test_timing():
c = d.sched.getCard() c = d.sched.getCard()
assert c.queue == 1 assert c.queue == 1
def test_collapse(): def test_collapse():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
# test collapsing # test collapsing
@ -924,16 +962,17 @@ def test_collapse():
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
assert not d.sched.getCard() assert not d.sched.getCard()
def test_deckDue(): def test_deckDue():
d = getEmptyCol() d = getEmptyCol()
# add a note with default deck # add a note with default deck
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# and one that's a child # and one that's a child
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
default1 = f.model()['did'] = d.decks.id("Default::1") default1 = f.model()["did"] = d.decks.id("Default::1")
d.addNote(f) d.addNote(f)
# make it a review card # make it a review card
c = f.cards()[0] c = f.cards()[0]
@ -942,13 +981,13 @@ def test_deckDue():
c.flush() c.flush()
# add one more with a new deck # add one more with a new deck
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
foobar = f.model()['did'] = d.decks.id("foo::bar") foobar = f.model()["did"] = d.decks.id("foo::bar")
d.addNote(f) d.addNote(f)
# and one that's a sibling # and one that's a sibling
f = d.newNote() f = d.newNote()
f['Front'] = "three" f["Front"] = "three"
foobaz = f.model()['did'] = d.decks.id("foo::baz") foobaz = f.model()["did"] = d.decks.id("foo::baz")
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert len(d.decks.decks) == 5 assert len(d.decks.decks) == 5
@ -970,10 +1009,12 @@ def test_deckDue():
assert tree[0][5][0][2] == 1 assert tree[0][5][0][2] == 1
assert tree[0][5][0][4] == 0 assert tree[0][5][0][4] == 0
# code should not fail if a card has an invalid deck # code should not fail if a card has an invalid deck
c.did = 12345; c.flush() c.did = 12345
c.flush()
d.sched.deckDueList() d.sched.deckDueList()
d.sched.deckDueTree() d.sched.deckDueTree()
def test_deckTree(): def test_deckTree():
d = getEmptyCol() d = getEmptyCol()
d.decks.id("new::b::c") d.decks.id("new::b::c")
@ -983,75 +1024,80 @@ def test_deckTree():
names.remove("new") names.remove("new")
assert "new" not in names assert "new" not in names
def test_deckFlow(): def test_deckFlow():
d = getEmptyCol() d = getEmptyCol()
# add a note with default deck # add a note with default deck
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# and one that's a child # and one that's a child
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
default1 = f.model()['did'] = d.decks.id("Default::2") default1 = f.model()["did"] = d.decks.id("Default::2")
d.addNote(f) d.addNote(f)
# and another that's higher up # and another that's higher up
f = d.newNote() f = d.newNote()
f['Front'] = "three" f["Front"] = "three"
default1 = f.model()['did'] = d.decks.id("Default::1") default1 = f.model()["did"] = d.decks.id("Default::1")
d.addNote(f) d.addNote(f)
# should get top level one first, then ::1, then ::2 # should get top level one first, then ::1, then ::2
d.reset() d.reset()
assert d.sched.counts() == (3,0,0) assert d.sched.counts() == (3, 0, 0)
for i in "one", "three", "two": for i in "one", "three", "two":
c = d.sched.getCard() c = d.sched.getCard()
assert c.note()['Front'] == i assert c.note()["Front"] == i
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
def test_reorder(): def test_reorder():
d = getEmptyCol() d = getEmptyCol()
# add a note with default deck # add a note with default deck
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
f2 = d.newNote() f2 = d.newNote()
f2['Front'] = "two" f2["Front"] = "two"
d.addNote(f2) d.addNote(f2)
assert f2.cards()[0].due == 2 assert f2.cards()[0].due == 2
found=False found = False
# 50/50 chance of being reordered # 50/50 chance of being reordered
for i in range(20): for i in range(20):
d.sched.randomizeCards(1) d.sched.randomizeCards(1)
if f.cards()[0].due != f.id: if f.cards()[0].due != f.id:
found=True found = True
break break
assert found assert found
d.sched.orderCards(1) d.sched.orderCards(1)
assert f.cards()[0].due == 1 assert f.cards()[0].due == 1
# shifting # shifting
f3 = d.newNote() f3 = d.newNote()
f3['Front'] = "three" f3["Front"] = "three"
d.addNote(f3) d.addNote(f3)
f4 = d.newNote() f4 = d.newNote()
f4['Front'] = "four" f4["Front"] = "four"
d.addNote(f4) d.addNote(f4)
assert f.cards()[0].due == 1 assert f.cards()[0].due == 1
assert f2.cards()[0].due == 2 assert f2.cards()[0].due == 2
assert f3.cards()[0].due == 3 assert f3.cards()[0].due == 3
assert f4.cards()[0].due == 4 assert f4.cards()[0].due == 4
d.sched.sortCards([ d.sched.sortCards([f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
assert f.cards()[0].due == 3 assert f.cards()[0].due == 3
assert f2.cards()[0].due == 4 assert f2.cards()[0].due == 4
assert f3.cards()[0].due == 1 assert f3.cards()[0].due == 1
assert f4.cards()[0].due == 2 assert f4.cards()[0].due == 2
def test_forget(): def test_forget():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.queue = 2; c.type = 2; c.ivl = 100; c.due = 0 c.queue = 2
c.type = 2
c.ivl = 100
c.due = 0
c.flush() c.flush()
d.reset() d.reset()
assert d.sched.counts() == (0, 0, 1) assert d.sched.counts() == (0, 0, 1)
@ -1059,10 +1105,11 @@ def test_forget():
d.reset() d.reset()
assert d.sched.counts() == (1, 0, 0) assert d.sched.counts() == (1, 0, 0)
def test_resched(): def test_resched():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
d.sched.reschedCards([c.id], 0, 0) d.sched.reschedCards([c.id], 0, 0)
@ -1072,14 +1119,15 @@ def test_resched():
assert c.queue == c.type == 2 assert c.queue == c.type == 2
d.sched.reschedCards([c.id], 1, 1) d.sched.reschedCards([c.id], 1, 1)
c.load() c.load()
assert c.due == d.sched.today+1 assert c.due == d.sched.today + 1
assert c.ivl == +1 assert c.ivl == +1
def test_norelearn(): def test_norelearn():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.type = 2 c.type = 2
@ -1093,13 +1141,15 @@ def test_norelearn():
c.flush() c.flush()
d.reset() d.reset()
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
d.sched._cardConf(c)['lapse']['delays'] = [] d.sched._cardConf(c)["lapse"]["delays"] = []
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
def test_failmult(): def test_failmult():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.type = 2 c.type = 2
@ -1111,7 +1161,7 @@ def test_failmult():
c.lapses = 1 c.lapses = 1
c.startTimer() c.startTimer()
c.flush() c.flush()
d.sched._cardConf(c)['lapse']['mult'] = 0.5 d.sched._cardConf(c)["lapse"]["mult"] = 0.5
c = d.sched.getCard() c = d.sched.getCard()
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.ivl == 50 assert c.ivl == 50

View file

@ -5,38 +5,45 @@ import copy
from anki.consts import STARTING_FACTOR from anki.consts import STARTING_FACTOR
from tests.shared import getEmptyCol from tests.shared import getEmptyCol
from anki.utils import intTime from anki.utils import intTime
from anki.hooks import addHook from anki.hooks import addHook
# Between 2-4AM, shift the time back so test assumptions hold. # Between 2-4AM, shift the time back so test assumptions hold.
lt = time.localtime() lt = time.localtime()
if lt.tm_hour > 2 and lt.tm_hour < 4: if lt.tm_hour > 2 and lt.tm_hour < 4:
orig_time = time.time orig_time = time.time
def adjusted_time(): def adjusted_time():
return orig_time() - 60*60*2 return orig_time() - 60 * 60 * 2
time.time = adjusted_time time.time = adjusted_time
def test_clock(): def test_clock():
d = getEmptyCol() d = getEmptyCol()
if (d.sched.dayCutoff - intTime()) < 10*60: if (d.sched.dayCutoff - intTime()) < 10 * 60:
raise Exception("Unit tests will fail around the day rollover.") raise Exception("Unit tests will fail around the day rollover.")
def checkRevIvl(d, c, targetIvl): def checkRevIvl(d, c, targetIvl):
min, max = d.sched._fuzzIvlRange(targetIvl) min, max = d.sched._fuzzIvlRange(targetIvl)
return min <= c.ivl <= max return min <= c.ivl <= max
def test_basics(): def test_basics():
d = getEmptyCol() d = getEmptyCol()
d.reset() d.reset()
assert not d.sched.getCard() assert not d.sched.getCard()
def test_new(): def test_new():
d = getEmptyCol() d = getEmptyCol()
d.reset() d.reset()
assert d.sched.newCount == 0 assert d.sched.newCount == 0
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert d.sched.newCount == 1 assert d.sched.newCount == 1
@ -74,15 +81,16 @@ def test_new():
# assert qs[n] in c.q() # assert qs[n] in c.q()
# d.sched.answerCard(c, 2) # d.sched.answerCard(c, 2)
def test_newLimits(): def test_newLimits():
d = getEmptyCol() d = getEmptyCol()
# add some notes # add some notes
g2 = d.decks.id("Default::foo") g2 = d.decks.id("Default::foo")
for i in range(30): for i in range(30):
f = d.newNote() f = d.newNote()
f['Front'] = str(i) f["Front"] = str(i)
if i > 4: if i > 4:
f.model()['did'] = g2 f.model()["did"] = g2
d.addNote(f) d.addNote(f)
# give the child deck a different configuration # give the child deck a different configuration
c2 = d.decks.confId("new conf") c2 = d.decks.confId("new conf")
@ -95,33 +103,36 @@ def test_newLimits():
assert c.did == 1 assert c.did == 1
# limit the parent to 10 cards, meaning we get 10 in total # limit the parent to 10 cards, meaning we get 10 in total
conf1 = d.decks.confForDid(1) conf1 = d.decks.confForDid(1)
conf1['new']['perDay'] = 10 conf1["new"]["perDay"] = 10
d.reset() d.reset()
assert d.sched.newCount == 10 assert d.sched.newCount == 10
# if we limit child to 4, we should get 9 # if we limit child to 4, we should get 9
conf2 = d.decks.confForDid(g2) conf2 = d.decks.confForDid(g2)
conf2['new']['perDay'] = 4 conf2["new"]["perDay"] = 4
d.reset() d.reset()
assert d.sched.newCount == 9 assert d.sched.newCount == 9
def test_newBoxes(): def test_newBoxes():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
c = d.sched.getCard() c = d.sched.getCard()
d.sched._cardConf(c)['new']['delays'] = [1,2,3,4,5] d.sched._cardConf(c)["new"]["delays"] = [1, 2, 3, 4, 5]
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
# should handle gracefully # should handle gracefully
d.sched._cardConf(c)['new']['delays'] = [1] d.sched._cardConf(c)["new"]["delays"] = [1]
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
def test_learn(): def test_learn():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
f = d.addNote(f) f = d.addNote(f)
# set as a learn card and rebuild queues # set as a learn card and rebuild queues
d.db.execute("update cards set queue=0, type=0") d.db.execute("update cards set queue=0, type=0")
@ -129,12 +140,12 @@ def test_learn():
# sched.getCard should return it, since it's due in the past # sched.getCard should return it, since it's due in the past
c = d.sched.getCard() c = d.sched.getCard()
assert c assert c
d.sched._cardConf(c)['new']['delays'] = [0.5, 3, 10] d.sched._cardConf(c)["new"]["delays"] = [0.5, 3, 10]
# fail it # fail it
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
# it should have three reps left to graduation # it should have three reps left to graduation
assert c.left%1000 == 3 assert c.left % 1000 == 3
assert c.left//1000 == 3 assert c.left // 1000 == 3
# it should by due in 30 seconds # it should by due in 30 seconds
t = round(c.due - time.time()) t = round(c.due - time.time())
assert t >= 25 and t <= 40 assert t >= 25 and t <= 40
@ -142,9 +153,9 @@ def test_learn():
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
# it should by due in 3 minutes # it should by due in 3 minutes
dueIn = c.due - time.time() dueIn = c.due - time.time()
assert 179 <= dueIn <= 180*1.25 assert 179 <= dueIn <= 180 * 1.25
assert c.left%1000 == 2 assert c.left % 1000 == 2
assert c.left//1000 == 2 assert c.left // 1000 == 2
# check log is accurate # check log is accurate
log = d.db.first("select * from revlog order by id desc") log = d.db.first("select * from revlog order by id desc")
assert log[3] == 3 assert log[3] == 3
@ -154,9 +165,9 @@ def test_learn():
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
# it should by due in 10 minutes # it should by due in 10 minutes
dueIn = c.due - time.time() dueIn = c.due - time.time()
assert 599 <= dueIn <= 600*1.25 assert 599 <= dueIn <= 600 * 1.25
assert c.left%1000 == 1 assert c.left % 1000 == 1
assert c.left//1000 == 1 assert c.left // 1000 == 1
# the next pass should graduate the card # the next pass should graduate the card
assert c.queue == 1 assert c.queue == 1
assert c.type == 1 assert c.type == 1
@ -164,7 +175,7 @@ def test_learn():
assert c.queue == 2 assert c.queue == 2
assert c.type == 2 assert c.type == 2
# should be due tomorrow, with an interval of 1 # should be due tomorrow, with an interval of 1
assert c.due == d.sched.today+1 assert c.due == d.sched.today + 1
assert c.ivl == 1 assert c.ivl == 1
# or normal removal # or normal removal
c.type = 0 c.type = 0
@ -176,10 +187,11 @@ def test_learn():
# revlog should have been updated each time # revlog should have been updated each time
assert d.db.scalar("select count() from revlog where type = 0") == 5 assert d.db.scalar("select count() from revlog where type = 0") == 5
def test_relearn(): def test_relearn():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.ivl = 100 c.ivl = 100
@ -201,10 +213,11 @@ def test_relearn():
assert c.ivl == 2 assert c.ivl == 2
assert c.due == d.sched.today + c.ivl assert c.due == d.sched.today + c.ivl
def test_relearn_no_steps(): def test_relearn_no_steps():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.ivl = 100 c.ivl = 100
@ -213,7 +226,7 @@ def test_relearn_no_steps():
c.flush() c.flush()
conf = d.decks.confForDid(1) conf = d.decks.confForDid(1)
conf['lapse']['delays'] = [] conf["lapse"]["delays"] = []
d.decks.save(conf) d.decks.save(conf)
# fail the card # fail the card
@ -222,14 +235,15 @@ def test_relearn_no_steps():
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.type == c.queue == 2 assert c.type == c.queue == 2
def test_learn_collapsed(): def test_learn_collapsed():
d = getEmptyCol() d = getEmptyCol()
# add 2 notes # add 2 notes
f = d.newNote() f = d.newNote()
f['Front'] = "1" f["Front"] = "1"
f = d.addNote(f) f = d.addNote(f)
f = d.newNote() f = d.newNote()
f['Front'] = "2" f["Front"] = "2"
f = d.addNote(f) f = d.addNote(f)
# set as a learn card and rebuild queues # set as a learn card and rebuild queues
d.db.execute("update cards set queue=0, type=0") d.db.execute("update cards set queue=0, type=0")
@ -248,27 +262,28 @@ def test_learn_collapsed():
c = d.sched.getCard() c = d.sched.getCard()
assert not c.q().endswith("2") assert not c.q().endswith("2")
def test_learn_day(): def test_learn_day():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
f = d.addNote(f) f = d.addNote(f)
d.sched.reset() d.sched.reset()
c = d.sched.getCard() c = d.sched.getCard()
d.sched._cardConf(c)['new']['delays'] = [1, 10, 1440, 2880] d.sched._cardConf(c)["new"]["delays"] = [1, 10, 1440, 2880]
# pass it # pass it
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
# two reps to graduate, 1 more today # two reps to graduate, 1 more today
assert c.left%1000 == 3 assert c.left % 1000 == 3
assert c.left//1000 == 1 assert c.left // 1000 == 1
assert d.sched.counts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
c = d.sched.getCard() c = d.sched.getCard()
ni = d.sched.nextIvl ni = d.sched.nextIvl
assert ni(c, 3) == 86400 assert ni(c, 3) == 86400
# answering it will place it in queue 3 # answering it will place it in queue 3
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
assert c.due == d.sched.today+1 assert c.due == d.sched.today + 1
assert c.queue == 3 assert c.queue == 3
assert not d.sched.getCard() assert not d.sched.getCard()
# for testing, move it back a day # for testing, move it back a day
@ -278,7 +293,7 @@ def test_learn_day():
assert d.sched.counts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
c = d.sched.getCard() c = d.sched.getCard()
# nextIvl should work # nextIvl should work
assert ni(c, 3) == 86400*2 assert ni(c, 3) == 86400 * 2
# if we fail it, it should be back in the correct queue # if we fail it, it should be back in the correct queue
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.queue == 1 assert c.queue == 1
@ -300,17 +315,19 @@ def test_learn_day():
c.flush() c.flush()
d.reset() d.reset()
assert d.sched.counts() == (0, 0, 1) assert d.sched.counts() == (0, 0, 1)
d.sched._cardConf(c)['lapse']['delays'] = [1440] d.sched._cardConf(c)["lapse"]["delays"] = [1440]
c = d.sched.getCard() c = d.sched.getCard()
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.queue == 3 assert c.queue == 3
assert d.sched.counts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
def test_reviews(): def test_reviews():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
# set the card up as a review card, due 8 days ago # set the card up as a review card, due 8 days ago
c = f.cards()[0] c = f.cards()[0]
@ -367,8 +384,10 @@ def test_reviews():
c.flush() c.flush()
# steup hook # steup hook
hooked = [] hooked = []
def onLeech(card): def onLeech(card):
hooked.append(1) hooked.append(1)
addHook("leech", onLeech) addHook("leech", onLeech)
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert hooked assert hooked
@ -376,6 +395,7 @@ def test_reviews():
c.load() c.load()
assert c.queue == -1 assert c.queue == -1
def test_review_limits(): def test_review_limits():
d = getEmptyCol() d = getEmptyCol()
@ -385,21 +405,22 @@ def test_review_limits():
pconf = d.decks.getConf(d.decks.confId("parentConf")) pconf = d.decks.getConf(d.decks.confId("parentConf"))
cconf = d.decks.getConf(d.decks.confId("childConf")) cconf = d.decks.getConf(d.decks.confId("childConf"))
pconf['rev']['perDay'] = 5 pconf["rev"]["perDay"] = 5
d.decks.updateConf(pconf) d.decks.updateConf(pconf)
d.decks.setConf(parent, pconf['id']) d.decks.setConf(parent, pconf["id"])
cconf['rev']['perDay'] = 10 cconf["rev"]["perDay"] = 10
d.decks.updateConf(cconf) d.decks.updateConf(cconf)
d.decks.setConf(child, cconf['id']) d.decks.setConf(child, cconf["id"])
m = d.models.current() m = d.models.current()
m['did'] = child['id'] m["did"] = child["id"]
d.models.save(m, updateReqs=False) d.models.save(m, updateReqs=False)
# add some cards # add some cards
for i in range(20): for i in range(20):
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
# make them reviews # make them reviews
@ -410,11 +431,11 @@ def test_review_limits():
tree = d.sched.deckDueTree() tree = d.sched.deckDueTree()
# (('Default', 1, 0, 0, 0, ()), ('parent', 1514457677462, 5, 0, 0, (('child', 1514457677463, 5, 0, 0, ()),))) # (('Default', 1, 0, 0, 0, ()), ('parent', 1514457677462, 5, 0, 0, (('child', 1514457677463, 5, 0, 0, ()),)))
assert tree[1][2] == 5 # parent assert tree[1][2] == 5 # parent
assert tree[1][5][0][2] == 5 # child assert tree[1][5][0][2] == 5 # child
# .counts() should match # .counts() should match
d.decks.select(child['id']) d.decks.select(child["id"])
d.sched.reset() d.sched.reset()
assert d.sched.counts() == (0, 0, 5) assert d.sched.counts() == (0, 0, 5)
@ -424,24 +445,25 @@ def test_review_limits():
assert d.sched.counts() == (0, 0, 4) assert d.sched.counts() == (0, 0, 4)
tree = d.sched.deckDueTree() tree = d.sched.deckDueTree()
assert tree[1][2] == 4 # parent assert tree[1][2] == 4 # parent
assert tree[1][5][0][2] == 4 # child assert tree[1][5][0][2] == 4 # child
# switch limits # switch limits
d.decks.setConf(parent, cconf['id']) d.decks.setConf(parent, cconf["id"])
d.decks.setConf(child, pconf['id']) d.decks.setConf(child, pconf["id"])
d.decks.select(parent['id']) d.decks.select(parent["id"])
d.sched.reset() d.sched.reset()
# child limits do not affect the parent # child limits do not affect the parent
tree = d.sched.deckDueTree() tree = d.sched.deckDueTree()
assert tree[1][2] == 9 # parent assert tree[1][2] == 9 # parent
assert tree[1][5][0][2] == 4 # child assert tree[1][5][0][2] == 4 # child
def test_button_spacing(): def test_button_spacing():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# 1 day ivl review card due now # 1 day ivl review card due now
c = f.cards()[0] c = f.cards()[0]
@ -460,16 +482,17 @@ def test_button_spacing():
# if hard factor is <= 1, then hard may not increase # if hard factor is <= 1, then hard may not increase
conf = d.decks.confForDid(1) conf = d.decks.confForDid(1)
conf['rev']['hardFactor'] = 1 conf["rev"]["hardFactor"] = 1
assert ni(c, 2) == "1 day" assert ni(c, 2) == "1 day"
def test_overdue_lapse(): def test_overdue_lapse():
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2 # disabled in commit 3069729776990980f34c25be66410e947e9d51a2
return return
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# simulate a review that was lapsed and is now due for its normal review # simulate a review that was lapsed and is now due for its normal review
c = f.cards()[0] c = f.cards()[0]
@ -498,13 +521,15 @@ def test_overdue_lapse():
d.sched.reset() d.sched.reset()
assert d.sched.counts() == (0, 0, 1) assert d.sched.counts() == (0, 0, 1)
def test_finished(): def test_finished():
d = getEmptyCol() d = getEmptyCol()
# nothing due # nothing due
assert "Congratulations" in d.sched.finishedMsg() assert "Congratulations" in d.sched.finishedMsg()
assert "limit" not in d.sched.finishedMsg() assert "limit" not in d.sched.finishedMsg()
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
# have a new card # have a new card
assert "new cards available" in d.sched.finishedMsg() assert "new cards available" in d.sched.finishedMsg()
@ -517,47 +542,49 @@ def test_finished():
assert "Congratulations" in d.sched.finishedMsg() assert "Congratulations" in d.sched.finishedMsg()
assert "limit" not in d.sched.finishedMsg() assert "limit" not in d.sched.finishedMsg()
def test_nextIvl(): def test_nextIvl():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
conf = d.decks.confForDid(1) conf = d.decks.confForDid(1)
conf['new']['delays'] = [0.5, 3, 10] conf["new"]["delays"] = [0.5, 3, 10]
conf['lapse']['delays'] = [1, 5, 9] conf["lapse"]["delays"] = [1, 5, 9]
c = d.sched.getCard() c = d.sched.getCard()
# new cards # new cards
################################################## ##################################################
ni = d.sched.nextIvl ni = d.sched.nextIvl
assert ni(c, 1) == 30 assert ni(c, 1) == 30
assert ni(c, 2) == (30+180)//2 assert ni(c, 2) == (30 + 180) // 2
assert ni(c, 3) == 180 assert ni(c, 3) == 180
assert ni(c, 4) == 4*86400 assert ni(c, 4) == 4 * 86400
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
# cards in learning # cards in learning
################################################## ##################################################
assert ni(c, 1) == 30 assert ni(c, 1) == 30
assert ni(c, 2) == (30+180)//2 assert ni(c, 2) == (30 + 180) // 2
assert ni(c, 3) == 180 assert ni(c, 3) == 180
assert ni(c, 4) == 4*86400 assert ni(c, 4) == 4 * 86400
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
assert ni(c, 1) == 30 assert ni(c, 1) == 30
assert ni(c, 2) == (180+600)//2 assert ni(c, 2) == (180 + 600) // 2
assert ni(c, 3) == 600 assert ni(c, 3) == 600
assert ni(c, 4) == 4*86400 assert ni(c, 4) == 4 * 86400
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
# normal graduation is tomorrow # normal graduation is tomorrow
assert ni(c, 3) == 1*86400 assert ni(c, 3) == 1 * 86400
assert ni(c, 4) == 4*86400 assert ni(c, 4) == 4 * 86400
# lapsed cards # lapsed cards
################################################## ##################################################
c.type = 2 c.type = 2
c.ivl = 100 c.ivl = 100
c.factor = STARTING_FACTOR c.factor = STARTING_FACTOR
assert ni(c, 1) == 60 assert ni(c, 1) == 60
assert ni(c, 3) == 100*86400 assert ni(c, 3) == 100 * 86400
assert ni(c, 4) == 101*86400 assert ni(c, 4) == 101 * 86400
# review cards # review cards
################################################## ##################################################
c.queue = 2 c.queue = 2
@ -566,8 +593,8 @@ def test_nextIvl():
# failing it should put it at 60s # failing it should put it at 60s
assert ni(c, 1) == 60 assert ni(c, 1) == 60
# or 1 day if relearn is false # or 1 day if relearn is false
d.sched._cardConf(c)['lapse']['delays']=[] d.sched._cardConf(c)["lapse"]["delays"] = []
assert ni(c, 1) == 1*86400 assert ni(c, 1) == 1 * 86400
# (* 100 1.2 86400)10368000.0 # (* 100 1.2 86400)10368000.0
assert ni(c, 2) == 10368000 assert ni(c, 2) == 10368000
# (* 100 2.5 86400)21600000.0 # (* 100 2.5 86400)21600000.0
@ -576,14 +603,15 @@ def test_nextIvl():
assert ni(c, 4) == 28080000 assert ni(c, 4) == 28080000
assert d.sched.nextIvlStr(c, 4) == "10.8 months" assert d.sched.nextIvlStr(c, 4) == "10.8 months"
def test_bury(): def test_bury():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
d.addNote(f) d.addNote(f)
c2 = f.cards()[0] c2 = f.cards()[0]
# burying # burying
@ -598,11 +626,14 @@ def test_bury():
assert not d.sched.getCard() assert not d.sched.getCard()
d.sched.unburyCardsForDeck(type="manual") d.sched.unburyCardsForDeck(type="manual")
c.load(); assert c.queue == 0 c.load()
c2.load(); assert c2.queue == -2 assert c.queue == 0
c2.load()
assert c2.queue == -2
d.sched.unburyCardsForDeck(type="siblings") d.sched.unburyCardsForDeck(type="siblings")
c2.load(); assert c2.queue == 0 c2.load()
assert c2.queue == 0
d.sched.buryCards([c.id, c2.id]) d.sched.buryCards([c.id, c2.id])
d.sched.unburyCardsForDeck(type="all") d.sched.unburyCardsForDeck(type="all")
@ -611,10 +642,11 @@ def test_bury():
assert d.sched.counts() == (2, 0, 0) assert d.sched.counts() == (2, 0, 0)
def test_suspend(): def test_suspend():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
# suspending # suspending
@ -628,7 +660,11 @@ def test_suspend():
d.reset() d.reset()
assert d.sched.getCard() assert d.sched.getCard()
# should cope with rev cards being relearnt # should cope with rev cards being relearnt
c.due = 0; c.ivl = 100; c.type = 2; c.queue = 2; c.flush() c.due = 0
c.ivl = 100
c.type = 2
c.queue = 2
c.flush()
d.reset() d.reset()
c = d.sched.getCard() c = d.sched.getCard()
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
@ -656,10 +692,11 @@ def test_suspend():
assert c.did != 1 assert c.did != 1
assert c.odue == 1 assert c.odue == 1
def test_filt_reviewing_early_normal(): def test_filt_reviewing_early_normal():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.ivl = 100 c.ivl = 100
@ -671,7 +708,7 @@ def test_filt_reviewing_early_normal():
c.startTimer() c.startTimer()
c.flush() c.flush()
d.reset() d.reset()
assert d.sched.counts() == (0,0,0) assert d.sched.counts() == (0, 0, 0)
# create a dynamic deck and refresh it # create a dynamic deck and refresh it
did = d.decks.newDyn("Cram") did = d.decks.newDyn("Cram")
d.sched.rebuildDyn(did) d.sched.rebuildDyn(did)
@ -679,14 +716,14 @@ def test_filt_reviewing_early_normal():
# should appear as normal in the deck list # should appear as normal in the deck list
assert sorted(d.sched.deckDueList())[0][2] == 1 assert sorted(d.sched.deckDueList())[0][2] == 1
# and should appear in the counts # and should appear in the counts
assert d.sched.counts() == (0,0,1) assert d.sched.counts() == (0, 0, 1)
# grab it and check estimates # grab it and check estimates
c = d.sched.getCard() c = d.sched.getCard()
assert d.sched.answerButtons(c) == 4 assert d.sched.answerButtons(c) == 4
assert d.sched.nextIvl(c, 1) == 600 assert d.sched.nextIvl(c, 1) == 600
assert d.sched.nextIvl(c, 2) == int(75*1.2)*86400 assert d.sched.nextIvl(c, 2) == int(75 * 1.2) * 86400
assert d.sched.nextIvl(c, 3) == int(75*2.5)*86400 assert d.sched.nextIvl(c, 3) == int(75 * 2.5) * 86400
assert d.sched.nextIvl(c, 4) == int(75*2.5*1.15)*86400 assert d.sched.nextIvl(c, 4) == int(75 * 2.5 * 1.15) * 86400
# answer 'good' # answer 'good'
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
@ -696,8 +733,7 @@ def test_filt_reviewing_early_normal():
# should not be in learning # should not be in learning
assert c.queue == 2 assert c.queue == 2
# should be logged as a cram rep # should be logged as a cram rep
assert d.db.scalar( assert d.db.scalar("select type from revlog order by id desc limit 1") == 3
"select type from revlog order by id desc limit 1") == 3
# due in 75 days, so it's been waiting 25 days # due in 75 days, so it's been waiting 25 days
c.ivl = 100 c.ivl = 100
@ -707,20 +743,21 @@ def test_filt_reviewing_early_normal():
d.reset() d.reset()
c = d.sched.getCard() c = d.sched.getCard()
assert d.sched.nextIvl(c, 2) == 60*86400 assert d.sched.nextIvl(c, 2) == 60 * 86400
assert d.sched.nextIvl(c, 3) == 100*86400 assert d.sched.nextIvl(c, 3) == 100 * 86400
assert d.sched.nextIvl(c, 4) == 114*86400 assert d.sched.nextIvl(c, 4) == 114 * 86400
def test_filt_keep_lrn_state(): def test_filt_keep_lrn_state():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# fail the card outside filtered deck # fail the card outside filtered deck
c = d.sched.getCard() c = d.sched.getCard()
d.sched._cardConf(c)['new']['delays'] = [1, 10, 61] d.sched._cardConf(c)["new"]["delays"] = [1, 10, 61]
d.decks.save() d.decks.save()
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
@ -744,30 +781,31 @@ def test_filt_keep_lrn_state():
# should be able to advance learning steps # should be able to advance learning steps
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
# should be due at least an hour in the future # should be due at least an hour in the future
assert c.due - intTime() > 60*60 assert c.due - intTime() > 60 * 60
# emptying the deck preserves learning state # emptying the deck preserves learning state
d.sched.emptyDyn(did) d.sched.emptyDyn(did)
c.load() c.load()
assert c.type == c.queue == 1 assert c.type == c.queue == 1
assert c.left == 1001 assert c.left == 1001
assert c.due - intTime() > 60*60 assert c.due - intTime() > 60 * 60
def test_preview(): def test_preview():
# add cards # add cards
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
orig = copy.copy(c) orig = copy.copy(c)
f2 = d.newNote() f2 = d.newNote()
f2['Front'] = "two" f2["Front"] = "two"
d.addNote(f2) d.addNote(f2)
# cram deck # cram deck
did = d.decks.newDyn("Cram") did = d.decks.newDyn("Cram")
cram = d.decks.get(did) cram = d.decks.get(did)
cram['resched'] = False cram["resched"] = False
d.sched.rebuildDyn(did) d.sched.rebuildDyn(did)
d.reset() d.reset()
# grab the first card # grab the first card
@ -801,22 +839,25 @@ def test_preview():
assert c.reps == 0 assert c.reps == 0
assert c.type == 0 assert c.type == 0
def test_ordcycle(): def test_ordcycle():
d = getEmptyCol() d = getEmptyCol()
# add two more templates and set second active # add two more templates and set second active
m = d.models.current(); mm = d.models m = d.models.current()
mm = d.models
t = mm.newTemplate("Reverse") t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}" t["qfmt"] = "{{Back}}"
t['afmt'] = "{{Front}}" t["afmt"] = "{{Front}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
t = mm.newTemplate("f2") t = mm.newTemplate("f2")
t['qfmt'] = "{{Front}}" t["qfmt"] = "{{Front}}"
t['afmt'] = "{{Back}}" t["afmt"] = "{{Back}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
# create a new note; it should have 3 cards # create a new note; it should have 3 cards
f = d.newNote() f = d.newNote()
f['Front'] = "1"; f['Back'] = "1" f["Front"] = "1"
f["Back"] = "1"
d.addNote(f) d.addNote(f)
assert d.cardCount() == 3 assert d.cardCount() == 3
d.reset() d.reset()
@ -825,10 +866,12 @@ def test_ordcycle():
assert d.sched.getCard().ord == 1 assert d.sched.getCard().ord == 1
assert d.sched.getCard().ord == 2 assert d.sched.getCard().ord == 2
def test_counts_idx(): def test_counts_idx():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert d.sched.counts() == (1, 0, 0) assert d.sched.counts() == (1, 0, 0)
@ -847,10 +890,11 @@ def test_counts_idx():
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert d.sched.counts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
def test_repCounts(): def test_repCounts():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
# lrnReps should be accurate on pass/fail # lrnReps should be accurate on pass/fail
@ -868,7 +912,7 @@ def test_repCounts():
d.sched.answerCard(d.sched.getCard(), 3) d.sched.answerCard(d.sched.getCard(), 3)
assert d.sched.counts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
# initial pass should be correct too # initial pass should be correct too
@ -880,14 +924,14 @@ def test_repCounts():
assert d.sched.counts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
# immediate graduate should work # immediate graduate should work
f = d.newNote() f = d.newNote()
f['Front'] = "three" f["Front"] = "three"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
d.sched.answerCard(d.sched.getCard(), 4) d.sched.answerCard(d.sched.getCard(), 4)
assert d.sched.counts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
# and failing a review should too # and failing a review should too
f = d.newNote() f = d.newNote()
f['Front'] = "three" f["Front"] = "three"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.type = 2 c.type = 2
@ -899,12 +943,13 @@ def test_repCounts():
d.sched.answerCard(d.sched.getCard(), 1) d.sched.answerCard(d.sched.getCard(), 1)
assert d.sched.counts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
def test_timing(): def test_timing():
d = getEmptyCol() d = getEmptyCol()
# add a few review cards, due today # add a few review cards, due today
for i in range(5): for i in range(5):
f = d.newNote() f = d.newNote()
f['Front'] = "num"+str(i) f["Front"] = "num" + str(i)
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.type = 2 c.type = 2
@ -925,11 +970,12 @@ def test_timing():
c = d.sched.getCard() c = d.sched.getCard()
assert c.queue == 1 assert c.queue == 1
def test_collapse(): def test_collapse():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
# test collapsing # test collapsing
@ -939,16 +985,17 @@ def test_collapse():
d.sched.answerCard(c, 4) d.sched.answerCard(c, 4)
assert not d.sched.getCard() assert not d.sched.getCard()
def test_deckDue(): def test_deckDue():
d = getEmptyCol() d = getEmptyCol()
# add a note with default deck # add a note with default deck
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# and one that's a child # and one that's a child
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
default1 = f.model()['did'] = d.decks.id("Default::1") default1 = f.model()["did"] = d.decks.id("Default::1")
d.addNote(f) d.addNote(f)
# make it a review card # make it a review card
c = f.cards()[0] c = f.cards()[0]
@ -957,13 +1004,13 @@ def test_deckDue():
c.flush() c.flush()
# add one more with a new deck # add one more with a new deck
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
foobar = f.model()['did'] = d.decks.id("foo::bar") foobar = f.model()["did"] = d.decks.id("foo::bar")
d.addNote(f) d.addNote(f)
# and one that's a sibling # and one that's a sibling
f = d.newNote() f = d.newNote()
f['Front'] = "three" f["Front"] = "three"
foobaz = f.model()['did'] = d.decks.id("foo::baz") foobaz = f.model()["did"] = d.decks.id("foo::baz")
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert len(d.decks.decks) == 5 assert len(d.decks.decks) == 5
@ -985,10 +1032,12 @@ def test_deckDue():
assert tree[0][5][0][2] == 1 assert tree[0][5][0][2] == 1
assert tree[0][5][0][4] == 0 assert tree[0][5][0][4] == 0
# code should not fail if a card has an invalid deck # code should not fail if a card has an invalid deck
c.did = 12345; c.flush() c.did = 12345
c.flush()
d.sched.deckDueList() d.sched.deckDueList()
d.sched.deckDueTree() d.sched.deckDueTree()
def test_deckTree(): def test_deckTree():
d = getEmptyCol() d = getEmptyCol()
d.decks.id("new::b::c") d.decks.id("new::b::c")
@ -998,75 +1047,80 @@ def test_deckTree():
names.remove("new") names.remove("new")
assert "new" not in names assert "new" not in names
def test_deckFlow(): def test_deckFlow():
d = getEmptyCol() d = getEmptyCol()
# add a note with default deck # add a note with default deck
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# and one that's a child # and one that's a child
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
default1 = f.model()['did'] = d.decks.id("Default::2") default1 = f.model()["did"] = d.decks.id("Default::2")
d.addNote(f) d.addNote(f)
# and another that's higher up # and another that's higher up
f = d.newNote() f = d.newNote()
f['Front'] = "three" f["Front"] = "three"
default1 = f.model()['did'] = d.decks.id("Default::1") default1 = f.model()["did"] = d.decks.id("Default::1")
d.addNote(f) d.addNote(f)
# should get top level one first, then ::1, then ::2 # should get top level one first, then ::1, then ::2
d.reset() d.reset()
assert d.sched.counts() == (3,0,0) assert d.sched.counts() == (3, 0, 0)
for i in "one", "three", "two": for i in "one", "three", "two":
c = d.sched.getCard() c = d.sched.getCard()
assert c.note()['Front'] == i assert c.note()["Front"] == i
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
def test_reorder(): def test_reorder():
d = getEmptyCol() d = getEmptyCol()
# add a note with default deck # add a note with default deck
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
f2 = d.newNote() f2 = d.newNote()
f2['Front'] = "two" f2["Front"] = "two"
d.addNote(f2) d.addNote(f2)
assert f2.cards()[0].due == 2 assert f2.cards()[0].due == 2
found=False found = False
# 50/50 chance of being reordered # 50/50 chance of being reordered
for i in range(20): for i in range(20):
d.sched.randomizeCards(1) d.sched.randomizeCards(1)
if f.cards()[0].due != f.id: if f.cards()[0].due != f.id:
found=True found = True
break break
assert found assert found
d.sched.orderCards(1) d.sched.orderCards(1)
assert f.cards()[0].due == 1 assert f.cards()[0].due == 1
# shifting # shifting
f3 = d.newNote() f3 = d.newNote()
f3['Front'] = "three" f3["Front"] = "three"
d.addNote(f3) d.addNote(f3)
f4 = d.newNote() f4 = d.newNote()
f4['Front'] = "four" f4["Front"] = "four"
d.addNote(f4) d.addNote(f4)
assert f.cards()[0].due == 1 assert f.cards()[0].due == 1
assert f2.cards()[0].due == 2 assert f2.cards()[0].due == 2
assert f3.cards()[0].due == 3 assert f3.cards()[0].due == 3
assert f4.cards()[0].due == 4 assert f4.cards()[0].due == 4
d.sched.sortCards([ d.sched.sortCards([f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
assert f.cards()[0].due == 3 assert f.cards()[0].due == 3
assert f2.cards()[0].due == 4 assert f2.cards()[0].due == 4
assert f3.cards()[0].due == 1 assert f3.cards()[0].due == 1
assert f4.cards()[0].due == 2 assert f4.cards()[0].due == 2
def test_forget(): def test_forget():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.queue = 2; c.type = 2; c.ivl = 100; c.due = 0 c.queue = 2
c.type = 2
c.ivl = 100
c.due = 0
c.flush() c.flush()
d.reset() d.reset()
assert d.sched.counts() == (0, 0, 1) assert d.sched.counts() == (0, 0, 1)
@ -1074,10 +1128,11 @@ def test_forget():
d.reset() d.reset()
assert d.sched.counts() == (1, 0, 0) assert d.sched.counts() == (1, 0, 0)
def test_resched(): def test_resched():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
d.sched.reschedCards([c.id], 0, 0) d.sched.reschedCards([c.id], 0, 0)
@ -1087,14 +1142,15 @@ def test_resched():
assert c.queue == c.type == 2 assert c.queue == c.type == 2
d.sched.reschedCards([c.id], 1, 1) d.sched.reschedCards([c.id], 1, 1)
c.load() c.load()
assert c.due == d.sched.today+1 assert c.due == d.sched.today + 1
assert c.ivl == +1 assert c.ivl == +1
def test_norelearn(): def test_norelearn():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.type = 2 c.type = 2
@ -1108,13 +1164,15 @@ def test_norelearn():
c.flush() c.flush()
d.reset() d.reset()
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
d.sched._cardConf(c)['lapse']['delays'] = [] d.sched._cardConf(c)["lapse"]["delays"] = []
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
def test_failmult(): def test_failmult():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.type = 2 c.type = 2
@ -1126,19 +1184,20 @@ def test_failmult():
c.lapses = 1 c.lapses = 1
c.startTimer() c.startTimer()
c.flush() c.flush()
d.sched._cardConf(c)['lapse']['mult'] = 0.5 d.sched._cardConf(c)["lapse"]["mult"] = 0.5
c = d.sched.getCard() c = d.sched.getCard()
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.ivl == 50 assert c.ivl == 50
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.ivl == 25 assert c.ivl == 25
def test_moveVersions(): def test_moveVersions():
col = getEmptyCol() col = getEmptyCol()
col.changeSchedulerVer(1) col.changeSchedulerVer(1)
n = col.newNote() n = col.newNote()
n['Front'] = "one" n["Front"] = "one"
col.addNote(n) col.addNote(n)
# make it a learning card # make it a learning card
@ -1176,8 +1235,10 @@ def test_moveVersions():
col.changeSchedulerVer(2) col.changeSchedulerVer(2)
# card with 100 day interval, answering again # card with 100 day interval, answering again
col.sched.reschedCards([c.id], 100, 100) col.sched.reschedCards([c.id], 100, 100)
c.load(); c.due = 0; c.flush() c.load()
col.sched._cardConf(c)['lapse']['mult'] = 0.5 c.due = 0
c.flush()
col.sched._cardConf(c)["lapse"]["mult"] = 0.5
col.sched.reset() col.sched.reset()
c = col.sched.getCard() c = col.sched.getCard()
col.sched.answerCard(c, 1) col.sched.answerCard(c, 1)
@ -1186,6 +1247,7 @@ def test_moveVersions():
c.load() c.load()
assert c.due == 50 assert c.due == 50
# cards with a due date earlier than the collection should retain # cards with a due date earlier than the collection should retain
# their due date when removed # their due date when removed
def test_negativeDueFilter(): def test_negativeDueFilter():
@ -1193,7 +1255,8 @@ def test_negativeDueFilter():
# card due prior to collection date # card due prior to collection date
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.due = -5 c.due = -5
@ -1209,4 +1272,3 @@ def test_negativeDueFilter():
c.load() c.load()
assert c.due == -5 assert c.due == -5

View file

@ -1,12 +1,13 @@
# coding: utf-8 # coding: utf-8
import os import os
from tests.shared import getEmptyCol from tests.shared import getEmptyCol
def test_stats(): def test_stats():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "foo" f["Front"] = "foo"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
# card stats # card stats
@ -17,12 +18,15 @@ def test_stats():
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
assert d.cardStats(c) assert d.cardStats(c)
def test_graphs_empty(): def test_graphs_empty():
d = getEmptyCol() d = getEmptyCol()
assert d.stats().report() assert d.stats().report()
def test_graphs(): def test_graphs():
from anki import Collection as aopen from anki import Collection as aopen
d = aopen(os.path.expanduser("~/test.anki2")) d = aopen(os.path.expanduser("~/test.anki2"))
g = d.stats() g = d.stats()
rep = g.report() rep = g.report()

View file

@ -2,15 +2,18 @@ from anki.template import Template
def test_remove_formatting_from_mathjax(): def test_remove_formatting_from_mathjax():
t = Template('') t = Template("")
assert t._removeFormattingFromMathjax(r'\(2^{{c3::2}}\)', 3) == r'\(2^{{C3::2}}\)' assert t._removeFormattingFromMathjax(r"\(2^{{c3::2}}\)", 3) == r"\(2^{{C3::2}}\)"
txt = (r'{{c1::ok}} \(2^2\) {{c2::not ok}} \(2^{{c3::2}}\) \(x^3\) ' txt = (
r'{{c4::blah}} {{c5::text with \(x^2\) jax}}') r"{{c1::ok}} \(2^2\) {{c2::not ok}} \(2^{{c3::2}}\) \(x^3\) "
r"{{c4::blah}} {{c5::text with \(x^2\) jax}}"
)
# Cloze 2 is not in MathJax, so it should not get protected against # Cloze 2 is not in MathJax, so it should not get protected against
# formatting. # formatting.
assert t._removeFormattingFromMathjax(txt, 2) == txt assert t._removeFormattingFromMathjax(txt, 2) == txt
txt = r'\(a\) {{c1::b}} \[ {{c1::c}} \]' txt = r"\(a\) {{c1::b}} \[ {{c1::c}} \]"
assert t._removeFormattingFromMathjax(txt, 1) == ( assert t._removeFormattingFromMathjax(txt, 1) == (
r'\(a\) {{c1::b}} \[ {{C1::c}} \]') r"\(a\) {{c1::b}} \[ {{C1::c}} \]"
)

View file

@ -1,16 +1,17 @@
# coding: utf-8 # coding: utf-8
import time import time
from tests.shared import getEmptyCol from tests.shared import getEmptyCol
from anki.consts import * from anki.consts import *
def test_op(): def test_op():
d = getEmptyCol() d = getEmptyCol()
# should have no undo by default # should have no undo by default
assert not d.undoName() assert not d.undoName()
# let's adjust a study option # let's adjust a study option
d.save("studyopts") d.save("studyopts")
d.conf['abc'] = 5 d.conf["abc"] = 5
# it should be listed as undoable # it should be listed as undoable
assert d.undoName() == "studyopts" assert d.undoName() == "studyopts"
# with about 5 minutes until it's clobbered # with about 5 minutes until it's clobbered
@ -18,7 +19,7 @@ def test_op():
# undoing should restore the old value # undoing should restore the old value
d.undo() d.undo()
assert not d.undoName() assert not d.undoName()
assert 'abc' not in d.conf assert "abc" not in d.conf
# an (auto)save will clear the undo # an (auto)save will clear the undo
d.save("foo") d.save("foo")
assert d.undoName() == "foo" assert d.undoName() == "foo"
@ -27,7 +28,7 @@ def test_op():
# and a review will, too # and a review will, too
d.save("add") d.save("add")
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert d.undoName() == "add" assert d.undoName() == "add"
@ -35,11 +36,12 @@ def test_op():
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
assert d.undoName() == "Review" assert d.undoName() == "Review"
def test_review(): def test_review():
d = getEmptyCol() d = getEmptyCol()
d.conf['counts'] = COUNT_REMAINING d.conf["counts"] = COUNT_REMAINING
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert not d.undoName() assert not d.undoName()
@ -62,7 +64,7 @@ def test_review():
assert not d.undoName() assert not d.undoName()
# we should be able to undo multiple answers too # we should be able to undo multiple answers too
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert d.sched.counts() == (2, 0, 0) assert d.sched.counts() == (2, 0, 0)
@ -85,5 +87,3 @@ def test_review():
assert d.undoName() == "foo" assert d.undoName() == "foo"
d.undo() d.undo()
assert not d.undoName() assert not d.undoName()

View file

@ -2,6 +2,7 @@
from anki.utils import fmtTimeSpan from anki.utils import fmtTimeSpan
def test_fmtTimeSpan(): def test_fmtTimeSpan():
assert fmtTimeSpan(5) == "5 seconds" assert fmtTimeSpan(5) == "5 seconds"
assert fmtTimeSpan(5, inTime=True) == "in 5 seconds" assert fmtTimeSpan(5, inTime=True) == "in 5 seconds"