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,11 +90,12 @@ 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()
@ -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
@ -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,14 +151,14 @@ 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
@ -157,8 +168,9 @@ def test_findCards():
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

@ -3,20 +3,26 @@
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,11 +187,12 @@ 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"))
@ -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

@ -7,14 +7,16 @@ 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
@ -109,6 +121,7 @@ def test_changes():
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"
@ -117,6 +130,6 @@ def test_illegal():
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,23 +112,25 @@ 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()
@ -134,36 +142,37 @@ def test_cloze_ordinals():
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

@ -8,32 +8,38 @@ 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,7 +136,7 @@ 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
@ -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,15 +225,16 @@ 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
@ -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
@ -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,15 +458,17 @@ 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
################################################## ##################################################
@ -484,7 +506,7 @@ 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
@ -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
@ -582,7 +611,7 @@ def test_cram():
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
@ -595,8 +624,7 @@ 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
@ -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,38 +1024,40 @@ 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
@ -1029,29 +1072,32 @@ def test_reorder():
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)
@ -1075,11 +1122,12 @@ def test_resched():
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

@ -12,31 +12,38 @@ from anki.hooks import addHook
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,7 +140,7 @@ 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
@ -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,15 +262,16 @@ 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
@ -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
@ -414,7 +435,7 @@ def test_review_limits():
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)
@ -428,9 +449,9 @@ def test_review_limits():
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
@ -438,10 +459,11 @@ def test_review_limits():
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,15 +542,17 @@ 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
################################################## ##################################################
@ -566,7 +593,7 @@ 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
@ -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
@ -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
@ -711,16 +747,17 @@ def test_filt_reviewing_early_normal():
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)
@ -753,21 +790,22 @@ def test_filt_keep_lrn_state():
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,38 +1047,40 @@ 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
@ -1044,29 +1095,32 @@ def test_reorder():
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)
@ -1090,11 +1145,12 @@ def test_resched():
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

@ -3,10 +3,11 @@
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

@ -4,13 +4,14 @@ 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"