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
RUNARGS :=
.SUFFIXES:
BLACKARGS := -t py36 anki aqt
BLACKARGS := -t py36 anki aqt tests
RUSTARGS := --release --strip
$(shell mkdir -p .build)

View file

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

View file

@ -9,65 +9,48 @@ from aqt.addons import AddonManager
def test_readMinimalManifest():
assertReadManifest(
'{"package": "yes", "name": "no"}',
{"package": "yes", "name": "no"}
'{"package": "yes", "name": "no"}', {"package": "yes", "name": "no"}
)
def test_readExtraKeys():
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"]},
)
def test_invalidManifest():
assertReadManifest(
'{"one": 1}',
{}
)
assertReadManifest('{"one": 1}', {})
def test_mustHaveName():
assertReadManifest(
'{"package": "something"}',
{}
)
assertReadManifest('{"package": "something"}', {})
def test_mustHavePackage():
assertReadManifest(
'{"name": "something"}',
{}
)
assertReadManifest('{"name": "something"}', {})
def test_invalidJson():
assertReadManifest(
'this is not a JSON dictionary',
{}
)
assertReadManifest("this is not a JSON dictionary", {})
def test_missingManifest():
assertReadManifest(
'{"package": "what", "name": "ever"}',
{},
nameInZip="not-manifest.bin"
'{"package": "what", "name": "ever"}', {}, nameInZip="not-manifest.bin"
)
def test_ignoreExtraKeys():
assertReadManifest(
'{"package": "a", "name": "b", "game": "c"}',
{"package": "a", "name": "b"}
'{"package": "a", "name": "b", "game": "c"}', {"package": "a", "name": "b"}
)
def test_conflictsMustBeStrings():
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
def test_previewCards():
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = '1'
f['Back'] = '2'
f["Front"] = "1"
f["Back"] = "2"
# non-empty and active
cards = deck.previewCards(f, 0)
assert len(cards) == 1
@ -22,11 +23,12 @@ def test_previewCards():
# make sure we haven't accidentally added cards to the db
assert deck.cardCount() == 1
def test_delete():
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = '1'
f['Back'] = '2'
f["Front"] = "1"
f["Back"] = "2"
deck.addNote(f)
cid = f.cards()[0].id
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 graves") == 2
def test_misc():
d = getEmptyCol()
f = d.newNote()
f['Front'] = '1'
f['Back'] = '2'
f["Front"] = "1"
f["Back"] = "2"
d.addNote(f)
c = f.cards()[0]
id = d.models.current()['id']
assert c.template()['ord'] == 0
id = d.models.current()["id"]
assert c.template()["ord"] == 0
def test_genrem():
d = getEmptyCol()
f = d.newNote()
f['Front'] = '1'
f['Back'] = ''
f["Front"] = "1"
f["Back"] = ""
d.addNote(f)
assert len(f.cards()) == 1
m = d.models.current()
mm = d.models
# adding a new template should automatically create cards
t = mm.newTemplate("rev")
t['qfmt'] = '{{Front}}'
t['afmt'] = ""
t["qfmt"] = "{{Front}}"
t["afmt"] = ""
mm.addTemplate(m, t)
mm.save(m, templates=True)
assert len(f.cards()) == 2
# if the template is changed to remove cards, they'll be removed
t['qfmt'] = "{{Back}}"
t["qfmt"] = "{{Back}}"
mm.save(m, templates=True)
d.remCards(d.emptyCids())
assert len(f.cards()) == 1
# if we add to the note, a card should be automatically generated
f.load()
f['Back'] = "1"
f["Back"] = "1"
f.flush()
assert len(f.cards()) == 2
def test_gendeck():
d = getEmptyCol()
cloze = d.models.byName("Cloze")
d.models.setCurrent(cloze)
f = d.newNote()
f['Text'] = '{{c1::one}}'
f["Text"] = "{{c1::one}}"
d.addNote(f)
assert d.cardCount() == 1
assert f.cards()[0].did == 1
# set the model to a new default deck
newId = d.decks.id("new")
cloze['did'] = newId
cloze["did"] = newId
d.models.save(cloze, updateReqs=False)
# a newly generated card should share the first card's deck
f['Text'] += '{{c2::two}}'
f["Text"] += "{{c2::two}}"
f.flush()
assert f.cards()[1].did == 1
# and same with multiple cards
f['Text'] += '{{c3::three}}'
f["Text"] += "{{c3::three}}"
f.flush()
assert f.cards()[2].did == 1
# 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.did = newId
c.flush()
f['Text'] += '{{c4::four}}'
f["Text"] += "{{c4::four}}"
f.flush()
assert f.cards()[3].did == newId

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@ from nose2.tools.such import helper
from anki.find import Finder
from tests.shared import getEmptyCol
def test_parse():
f = Finder(None)
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 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("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("deck:'two words'") == ["deck:two words"]
def test_findCards():
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = 'dog'
f['Back'] = 'cat'
f["Front"] = "dog"
f["Back"] = "cat"
f.tags.append("monkey animal_1 * %")
f1id = f.id
deck.addNote(f)
firstCardId = f.cards()[0].id
f = deck.newNote()
f['Front'] = 'goats are fun'
f['Back'] = 'sheep'
f["Front"] = "goats are fun"
f["Back"] = "sheep"
f.tags.append("sheep goat horse animal11")
deck.addNote(f)
f2id = f.id
f = deck.newNote()
f['Front'] = 'cat'
f['Back'] = 'sheep'
f["Front"] = "cat"
f["Back"] = "sheep"
deck.addNote(f)
catCard = f.cards()[0]
m = deck.models.current(); mm = deck.models
m = deck.models.current()
mm = deck.models
t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}"
t['afmt'] = "{{Front}}"
t["qfmt"] = "{{Back}}"
t["afmt"] = "{{Front}}"
mm.addTemplate(m, t)
mm.save(m)
f = deck.newNote()
f['Front'] = 'test'
f['Back'] = 'foo bar'
f["Front"] = "test"
f["Back"] = "foo bar"
deck.addNote(f)
latestCardIds = [c.id for c in f.cards()]
# tag searches
@ -66,9 +78,7 @@ def test_findCards():
assert len(deck.findCards("tag:sheep -tag:monkey")) == 1
assert len(deck.findCards("-tag:sheep")) == 4
deck.tags.bulkAdd(deck.db.list("select id from notes"), "foo bar")
assert (len(deck.findCards("tag:foo")) ==
len(deck.findCards("tag:bar")) ==
5)
assert len(deck.findCards("tag:foo")) == len(deck.findCards("tag:bar")) == 5
deck.tags.bulkRem(deck.db.list("select id from notes"), "foo")
assert len(deck.findCards("tag:foo")) == 0
assert len(deck.findCards("tag:bar")) == 5
@ -86,7 +96,8 @@ def test_findCards():
c.flush()
assert deck.findCards("is:review") == [c.id]
assert deck.findCards("is:due") == []
c.due = 0; c.queue = 2
c.due = 0
c.queue = 2
c.flush()
assert deck.findCards("is:due") == [c.id]
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:*")) == 5
# ordering
deck.conf['sortType'] = "noteCrt"
deck.conf["sortType"] = "noteCrt"
assert deck.findCards("front:*", 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)[-1] in latestCardIds
deck.conf['sortType'] = "cardMod"
deck.conf["sortType"] = "cardMod"
assert deck.findCards("", order=True)[-1] in latestCardIds
assert deck.findCards("", order=True)[0] == firstCardId
deck.conf['sortBackwards'] = True
deck.conf["sortBackwards"] = True
assert deck.findCards("", order=True)[0] in latestCardIds
# model
assert len(deck.findCards("note:basic")) == 5
@ -140,14 +151,14 @@ def test_findCards():
deck.findCards("deck:*cefault")
# full search
f = deck.newNote()
f['Front'] = 'hello<b>world</b>'
f['Back'] = 'abc'
f["Front"] = "hello<b>world</b>"
f["Back"] = "abc"
deck.addNote(f)
# as it's the sort field, it matches
assert len(deck.findCards("helloworld")) == 2
# assert len(deck.findCards("helloworld", full=True)) == 2
# 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()
assert len(deck.findCards("helloworld")) == 0
# assert len(deck.findCards("helloworld", full=True)) == 2
@ -157,8 +168,9 @@ def test_findCards():
len(deck.findCards("is:invalid"))
# should be able to limit to parent deck, no children
id = deck.db.scalar("select id from cards limit 1")
deck.db.execute("update cards set did = ? where id = ?",
deck.decks.id("Default::Child"), id)
deck.db.execute(
"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::child")) == 1
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")
deck.db.execute(
"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
@ -205,8 +219,8 @@ def test_findCards():
# empty field
assert len(deck.findCards("front:")) == 0
f = deck.newNote()
f['Front'] = ''
f['Back'] = 'abc2'
f["Front"] = ""
f["Back"] = "abc2"
assert deck.addNote(f) == 1
assert len(deck.findCards("front:")) == 1
# OR searches and nesting
@ -220,8 +234,7 @@ def test_findCards():
assert len(deck.findCards("(()")) == 0
# added
assert len(deck.findCards("added:0")) == 0
deck.db.execute("update cards set id = id - 86400*1000 where id = ?",
id)
deck.db.execute("update cards set id = id - 86400*1000 where id = ?", id)
assert len(deck.findCards("added:1")) == deck.cardCount() - 1
assert len(deck.findCards("added:2")) == deck.cardCount()
# flag
@ -230,50 +243,58 @@ def test_findCards():
with helper.assertRaises(Exception):
deck.findCards("flag:12")
def test_findReplace():
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = 'foo'
f['Back'] = 'bar'
f["Front"] = "foo"
f["Back"] = "bar"
deck.addNote(f)
f2 = deck.newNote()
f2['Front'] = 'baz'
f2['Back'] = 'foo'
f2["Front"] = "baz"
f2["Back"] = "foo"
deck.addNote(f2)
nids = [f.id, f2.id]
# should do nothing
assert deck.findReplace(nids, "abc", "123") == 0
# global replace
assert deck.findReplace(nids, "foo", "qux") == 2
f.load(); assert f['Front'] == "qux"
f2.load(); assert f2['Back'] == "qux"
f.load()
assert f["Front"] == "qux"
f2.load()
assert f2["Back"] == "qux"
# single field replace
assert deck.findReplace(nids, "qux", "foo", field="Front") == 1
f.load(); assert f['Front'] == "foo"
f2.load(); assert f2['Back'] == "qux"
f.load()
assert f["Front"] == "foo"
f2.load()
assert f2["Back"] == "qux"
# regex replace
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
f.load(); assert f['Back'] == "reg"
f.load()
assert f["Back"] == "reg"
def test_findDupes():
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = 'foo'
f['Back'] = 'bar'
f["Front"] = "foo"
f["Back"] = "bar"
deck.addNote(f)
f2 = deck.newNote()
f2['Front'] = 'baz'
f2['Back'] = 'bar'
f2["Front"] = "baz"
f2["Back"] = "bar"
deck.addNote(f2)
f3 = deck.newNote()
f3['Front'] = 'quux'
f3['Back'] = 'bar'
f3["Front"] = "quux"
f3["Back"] = "bar"
deck.addNote(f3)
f4 = deck.newNote()
f4['Front'] = 'quuux'
f4['Back'] = 'nope'
f4["Front"] = "quuux"
f4["Back"] = "nope"
deck.addNote(f4)
r = deck.findDupes("Back")
assert r[0][0] == "bar"

View file

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

View file

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

View file

@ -7,14 +7,16 @@ import shutil
from tests.shared import getEmptyCol
from anki.utils import stripHTML
def test_latex():
d = getEmptyCol()
# change latex cmd to simulate broken build
import anki.latex
anki.latex.pngCommands[0][0] = "nolatex"
# add a note with latex
f = d.newNote()
f['Front'] = "[latex]hello[/latex]"
f["Front"] = "[latex]hello[/latex]"
d.addNote(f)
# but since latex couldn't run, there's nothing there
assert len(os.listdir(d.media.dir())) == 0
@ -34,13 +36,13 @@ def test_latex():
assert ".png" in f.cards()[0].q()
# adding new notes should cause generation on question display
f = d.newNote()
f['Front'] = "[latex]world[/latex]"
f["Front"] = "[latex]world[/latex]"
d.addNote(f)
f.cards()[0].q()
assert len(os.listdir(d.media.dir())) == 2
# another note with the same media should reuse
f = d.newNote()
f['Front'] = " [latex]world[/latex]"
f["Front"] = " [latex]world[/latex]"
d.addNote(f)
assert len(os.listdir(d.media.dir())) == 2
oldcard = f.cards()[0]
@ -49,7 +51,7 @@ def test_latex():
# missing media will show the latex
anki.latex.build = False
f = d.newNote()
f['Front'] = "[latex]foo[/latex]"
f["Front"] = "[latex]foo[/latex]"
d.addNote(f)
assert len(os.listdir(d.media.dir())) == 2
assert stripHTML(f.cards()[0].q()) == "[latex]foo[/latex]"
@ -86,10 +88,11 @@ def test_latex():
(result, msg) = _test_includes_bad_command("\\emph")
assert not result, msg
def _test_includes_bad_command(bad):
d = getEmptyCol()
f = d.newNote()
f['Front'] = '[latex]%s[/latex]' % bad
f["Front"] = "[latex]%s[/latex]" % bad
d.addNote(f)
q = f.cards()[0].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")
assert d.media.addFile(path) == "foo (1).jpg"
def test_strings():
d = getEmptyCol()
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' style='test'>ao") == ["foo.jpg"]
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, "<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\"><img class=yo src=fo>ao") == [
"foo.jpg", "fo"]
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') == [
"foo.jpg",
"fo",
]
assert mf(mid, "aou[sound:foo.mp3]aou") == ["foo.mp3"]
sp = d.media.strip
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="foo bar.jpg">') == '<img src="foo%20bar.jpg">'
def test_deckIntegration():
d = getEmptyCol()
# create a media dir
@ -56,11 +62,13 @@ def test_deckIntegration():
d.media.addFile(file)
# add a note which references it
f = d.newNote()
f['Front'] = "one"; f['Back'] = "<img src='fake.png'>"
f["Front"] = "one"
f["Back"] = "<img src='fake.png'>"
d.addNote(f)
# and one which references a non-existent file
f = d.newNote()
f['Front'] = "one"; f['Back'] = "<img src='fake2.png'>"
f["Front"] = "one"
f["Back"] = "<img src='fake2.png'>"
d.addNote(f)
# and add another file which isn't used
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[1] == ["foo.jpg"]
def test_changes():
d = getEmptyCol()
def added():
return d.media.db.execute("select fname from media where csum is not null")
def removed():
return d.media.db.execute("select fname from media where csum is null")
assert not list(added())
assert not list(removed())
# add a file
@ -109,6 +121,7 @@ def test_changes():
assert len(list(added())) == 1
assert len(list(removed())) == 1
def test_illegal():
d = getEmptyCol()
aString = "a:b|cd\\e/f\0g*h"
@ -117,6 +130,6 @@ def test_illegal():
for c in aString:
bad = d.media.hasIllegal("somestring" + c + "morestring")
if bad:
assert(c not in good)
assert c not in good
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
import anki.template
def test_modelDelete():
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = '1'
f['Back'] = '2'
f["Front"] = "1"
f["Back"] = "2"
deck.addNote(f)
assert deck.cardCount() == 1
deck.models.rem(deck.models.current())
assert deck.cardCount() == 0
def test_modelCopy():
deck = getEmptyCol()
m = deck.models.current()
m2 = deck.models.copy(m)
assert m2['name'] == "Basic copy"
assert m2['id'] != m['id']
assert len(m2['flds']) == 2
assert len(m['flds']) == 2
assert len(m2['flds']) == len(m['flds'])
assert len(m['tmpls']) == 1
assert len(m2['tmpls']) == 1
assert m2["name"] == "Basic copy"
assert m2["id"] != m["id"]
assert len(m2["flds"]) == 2
assert len(m["flds"]) == 2
assert len(m2["flds"]) == len(m["flds"])
assert len(m["tmpls"]) == 1
assert len(m2["tmpls"]) == 1
assert deck.models.scmhash(m) == deck.models.scmhash(m2)
def test_fields():
d = getEmptyCol()
f = d.newNote()
f['Front'] = '1'
f['Back'] = '2'
f["Front"] = "1"
f["Back"] = "2"
d.addNote(f)
m = d.models.current()
# make sure renaming a field updates the templates
d.models.renameField(m, m['flds'][0], "NewFront")
assert "{{NewFront}}" in m['tmpls'][0]['qfmt']
d.models.renameField(m, m["flds"][0], "NewFront")
assert "{{NewFront}}" in m["tmpls"][0]["qfmt"]
h = d.models.scmhash(m)
# add a field
f = d.models.newField("foo")
@ -47,44 +50,46 @@ def test_fields():
assert d.models.scmhash(m) != h
# rename it
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
d.models.remField(m, m['flds'][1])
d.models.remField(m, m["flds"][1])
assert d.getNote(d.models.nids(m)[0]).fields == ["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"]
# 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", ""]
# add another and put in middle
f = d.models.newField("baz")
d.models.addField(m, f)
f = d.getNote(d.models.nids(m)[0])
f['baz'] = "2"
f["baz"] = "2"
f.flush()
assert d.getNote(d.models.nids(m)[0]).fields == ["1", "", "2"]
# 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", ""]
# 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"]
# 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"]
def test_templates():
d = getEmptyCol()
m = d.models.current(); mm = d.models
m = d.models.current()
mm = d.models
t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}"
t['afmt'] = "{{Front}}"
t["qfmt"] = "{{Back}}"
t["afmt"] = "{{Front}}"
mm.addTemplate(m, t)
mm.save(m)
f = d.newNote()
f['Front'] = '1'
f['Back'] = '2'
f["Front"] = "1"
f["Back"] = "2"
d.addNote(f)
assert d.cardCount() == 2
(c, c2) = f.cards()
@ -93,11 +98,12 @@ def test_templates():
assert c2.ord == 1
# switch templates
d.models.moveTemplate(m, c.template(), 1)
c.load(); c2.load()
c.load()
c2.load()
assert c.ord == 1
assert c2.ord == 0
# 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
# and should have updated the other cards' ordinals
c = f.cards()[0]
@ -106,23 +112,25 @@ def test_templates():
# it shouldn't be possible to orphan notes by removing templates
t = mm.newTemplate("template name")
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():
d = getEmptyCol()
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
t = mm.newTemplate("ChainedCloze")
t['qfmt'] = "{{text:cloze:Text}}"
t['afmt'] = "{{text:cloze:Text}}"
t["qfmt"] = "{{text:cloze:Text}}"
t["afmt"] = "{{text:cloze:Text}}"
mm.addTemplate(m, t)
mm.save(m)
d.models.remTemplate(m, m['tmpls'][0])
d.models.remTemplate(m, m["tmpls"][0])
f = d.newNote()
f['Text'] = '{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}'
f["Text"] = "{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}"
d.addNote(f)
assert d.cardCount() == 2
(c, c2) = f.cards()
@ -134,36 +142,37 @@ def test_cloze_ordinals():
def test_text():
d = getEmptyCol()
m = d.models.current()
m['tmpls'][0]['qfmt'] = "{{text:Front}}"
m["tmpls"][0]["qfmt"] = "{{text:Front}}"
d.models.save(m)
f = d.newNote()
f['Front'] = 'hello<b>world'
f["Front"] = "hello<b>world"
d.addNote(f)
assert "helloworld" in f.cards()[0].q()
def test_cloze():
d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze"))
f = d.newNote()
assert f.model()['name'] == "Cloze"
assert f.model()["name"] == "Cloze"
# a cloze model with no clozes is not empty
f['Text'] = 'nothing'
f["Text"] = "nothing"
assert d.addNote(f)
# try with one cloze
f = d.newNote()
f['Text'] = "hello {{c1::world}}"
f["Text"] = "hello {{c1::world}}"
assert d.addNote(f) == 1
assert "hello <span class=cloze>[...]</span>" in f.cards()[0].q()
assert "hello <span class=cloze>world</span>" in f.cards()[0].a()
# and with a comment
f = d.newNote()
f['Text'] = "hello {{c1::world::typical}}"
f["Text"] = "hello {{c1::world::typical}}"
assert d.addNote(f) == 1
assert "<span class=cloze>[typical]</span>" in f.cards()[0].q()
assert "<span class=cloze>world</span>" in f.cards()[0].a()
# and with 2 clozes
f = d.newNote()
f['Text'] = "hello {{c1::world}} {{c2::bar}}"
f["Text"] = "hello {{c1::world}} {{c2::bar}}"
assert d.addNote(f) == 2
(c1, c2) = f.cards()
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
# list
f = d.newNote()
f['Text'] = "a {{c1::b}} {{c1::c}}"
f["Text"] = "a {{c1::b}} {{c1::c}}"
assert d.addNote(f) == 1
assert "<span class=cloze>b</span> <span class=cloze>c</span>" in (
f.cards()[0].a())
assert "<span class=cloze>b</span> <span class=cloze>c</span>" in (f.cards()[0].a())
# if we add another cloze, a card should be generated
cnt = d.cardCount()
f['Text'] = "{{c2::hello}} {{c1::foo}}"
f["Text"] = "{{c2::hello}} {{c1::foo}}"
f.flush()
assert d.cardCount() == cnt + 1
# 0 or negative indices are not supported
f['Text'] += "{{c0::zero}} {{c-1:foo}}"
f["Text"] += "{{c0::zero}} {{c-1:foo}}"
f.flush()
assert len(f.cards()) == 2
def test_cloze_mathjax():
d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze"))
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 len(f.cards()) == 5
assert "class=cloze" in f.cards()[0].q()
@ -201,56 +212,70 @@ def test_cloze_mathjax():
assert "class=cloze" in f.cards()[4].q()
f = d.newNote()
f['Text'] = r'\(a\) {{c1::b}} \[ {{c1::c}} \]'
f["Text"] = r"\(a\) {{c1::b}} \[ {{c1::c}} \]"
assert d.addNote(f)
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():
d = getEmptyCol()
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
t = mm.newTemplate("ChainedCloze")
t['qfmt'] = "{{cloze:text:Text}}"
t['afmt'] = "{{cloze:text:Text}}"
t["qfmt"] = "{{cloze:text:Text}}"
t["afmt"] = "{{cloze:text:Text}}"
mm.addTemplate(m, t)
mm.save(m)
d.models.remTemplate(m, m['tmpls'][0])
d.models.remTemplate(m, m["tmpls"][0])
f = d.newNote()
q1 = '<span style=\"color:red\">phrase</span>'
a1 = '<b>sentence</b>'
q2 = '<span style=\"color:red\">en chaine</span>'
a2 = '<i>chained</i>'
f['Text'] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (q1,a1,q2,a2)
q1 = '<span style="color:red">phrase</span>'
a1 = "<b>sentence</b>"
q2 = '<span style="color:red">en chaine</span>'
a2 = "<i>chained</i>"
f["Text"] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (
q1,
a1,
q2,
a2,
)
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 "This <span class=cloze>phrase</span> demonstrates <span class=cloze>en chaine</span> clozes." in f.cards()[0].a()
assert (
"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():
deck = getEmptyCol()
basic = deck.models.byName("Basic")
cloze = deck.models.byName("Cloze")
# 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['qfmt'] = "{{Back}}"
t['afmt'] = "{{Front}}"
t["qfmt"] = "{{Back}}"
t["afmt"] = "{{Front}}"
mm.addTemplate(m, t)
mm.save(m)
f = deck.newNote()
f['Front'] = 'f'
f['Back'] = 'b123'
f["Front"] = "f"
f["Back"] = "b123"
deck.addNote(f)
# switch fields
map = {0: 1, 1: 0}
deck.models.change(basic, [f.id], basic, map, None)
f.load()
assert f['Front'] == 'b123'
assert f['Back'] == 'f'
assert f["Front"] == "b123"
assert f["Back"] == "f"
# switch cards
c0 = f.cards()[0]
c1 = f.cards()[1]
@ -259,7 +284,9 @@ def test_modelChange():
assert c0.ord == 0
assert c1.ord == 1
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 "b123" in c1.q()
assert c0.ord == 1
@ -283,30 +310,31 @@ def test_modelChange():
# but we have two cards, as a new one was generated
assert len(f.cards()) == 2
# an unmapped field becomes blank
assert f['Front'] == 'b123'
assert f['Back'] == 'f'
assert f["Front"] == "b123"
assert f["Back"] == "f"
deck.models.change(basic, [f.id], basic, map, None)
f.load()
assert f['Front'] == ''
assert f['Back'] == 'f'
assert f["Front"] == ""
assert f["Back"] == "f"
# another note to try model conversion
f = deck.newNote()
f['Front'] = 'f2'
f['Back'] = 'b2'
f["Front"] = "f2"
f["Back"] = "b2"
deck.addNote(f)
assert deck.models.useCount(basic) == 2
assert deck.models.useCount(cloze) == 0
map = {0: 0, 1: 1}
deck.models.change(basic, [f.id], cloze, map, map)
f.load()
assert f['Text'] == "f2"
assert f["Text"] == "f2"
assert len(f.cards()) == 2
# 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
deck.models.change(cloze, [f.id], basic, map, map)
assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 1
def test_templates():
d = dict(Foo="x", Bar="y")
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("{{#Baz}}{{#Foo}}{{Foo}}{{/Foo}}{{/Baz}}", d) == ""
def test_availOrds():
d = getEmptyCol()
m = d.models.current(); mm = d.models
t = m['tmpls'][0]
m = d.models.current()
mm = d.models
t = m["tmpls"][0]
f = d.newNote()
f['Front'] = "1"
f["Front"] = "1"
# simple templates
assert mm.availOrds(m, joinFields(f.fields)) == [0]
t['qfmt'] = "{{Back}}"
t["qfmt"] = "{{Back}}"
mm.save(m, templates=True)
assert not mm.availOrds(m, joinFields(f.fields))
# AND
t['qfmt'] = "{{#Front}}{{#Back}}{{Front}}{{/Back}}{{/Front}}"
t["qfmt"] = "{{#Front}}{{#Back}}{{Front}}{{/Back}}{{/Front}}"
mm.save(m, templates=True)
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)
assert not mm.availOrds(m, joinFields(f.fields))
# OR
t['qfmt'] = "{{Front}}\n{{Back}}"
t["qfmt"] = "{{Front}}\n{{Back}}"
mm.save(m, templates=True)
assert mm.availOrds(m, joinFields(f.fields)) == [0]
t['Front'] = ""
t['Back'] = "1"
t["Front"] = ""
t["Back"] = "1"
assert mm.availOrds(m, joinFields(f.fields)) == [0]
def test_req():
def reqSize(model):
if model['type'] == MODEL_CLOZE:
if model["type"] == MODEL_CLOZE:
return
assert (len(model['tmpls']) == len(model['req']))
assert len(model["tmpls"]) == len(model["req"])
d = getEmptyCol()
mm = d.models
basic = mm.byName("Basic")
assert 'req' in basic
assert "req" in basic
reqSize(basic)
r = basic['req'][0]
r = basic["req"][0]
assert r[0] == 0
assert r[1] in ("any", "all")
assert r[2] == [0]
opt = mm.byName("Basic (optional reversed card)")
reqSize(opt)
r = opt['req'][0]
r = opt["req"][0]
assert r[1] in ("any", "all")
assert r[2] == [0]
assert opt['req'][1] == [1, 'all', [1, 2]]
assert opt["req"][1] == [1, "all", [1, 2]]
# testing any
opt['tmpls'][1]['qfmt'] = "{{Back}}{{Add Reverse}}"
opt["tmpls"][1]["qfmt"] = "{{Back}}{{Add Reverse}}"
mm.save(opt, templates=True)
assert opt['req'][1] == [1, 'any', [1, 2]]
assert opt["req"][1] == [1, "any", [1, 2]]
# 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)
assert opt['req'][1] == [1, 'none', []]
assert opt["req"][1] == [1, "none", []]
opt = mm.byName("Basic (type in the answer)")
reqSize(opt)
r = opt['req'][0]
r = opt["req"][0]
assert r[1] in ("any", "all")
assert r[2] == [0]
# def test_updatereqs_performance():
# import time
# d = getEmptyCol()

View file

@ -8,32 +8,38 @@ from tests.shared import getEmptyCol as getEmptyColOrig
from anki.utils import intTime
from anki.hooks import addHook
def getEmptyCol():
col = getEmptyColOrig()
col.changeSchedulerVer(1)
return col
def test_clock():
d = getEmptyCol()
if (d.sched.dayCutoff - intTime()) < 10 * 60:
raise Exception("Unit tests will fail around the day rollover.")
def checkRevIvl(d, c, targetIvl):
min, max = d.sched._fuzzIvlRange(targetIvl)
return min <= c.ivl <= max
def test_basics():
d = getEmptyCol()
d.reset()
assert not d.sched.getCard()
def test_new():
d = getEmptyCol()
d.reset()
assert d.sched.newCount == 0
# add a note
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
d.reset()
assert d.sched.newCount == 1
@ -71,15 +77,16 @@ def test_new():
# assert qs[n] in c.q()
# d.sched.answerCard(c, 2)
def test_newLimits():
d = getEmptyCol()
# add some notes
g2 = d.decks.id("Default::foo")
for i in range(30):
f = d.newNote()
f['Front'] = str(i)
f["Front"] = str(i)
if i > 4:
f.model()['did'] = g2
f.model()["did"] = g2
d.addNote(f)
# give the child deck a different configuration
c2 = d.decks.confId("new conf")
@ -92,33 +99,36 @@ def test_newLimits():
assert c.did == 1
# limit the parent to 10 cards, meaning we get 10 in total
conf1 = d.decks.confForDid(1)
conf1['new']['perDay'] = 10
conf1["new"]["perDay"] = 10
d.reset()
assert d.sched.newCount == 10
# if we limit child to 4, we should get 9
conf2 = d.decks.confForDid(g2)
conf2['new']['perDay'] = 4
conf2["new"]["perDay"] = 4
d.reset()
assert d.sched.newCount == 9
def test_newBoxes():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
d.reset()
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)
# should handle gracefully
d.sched._cardConf(c)['new']['delays'] = [1]
d.sched._cardConf(c)["new"]["delays"] = [1]
d.sched.answerCard(c, 2)
def test_learn():
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
f = d.addNote(f)
# set as a learn card and rebuild queues
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
c = d.sched.getCard()
assert c
d.sched._cardConf(c)['new']['delays'] = [0.5, 3, 10]
d.sched._cardConf(c)["new"]["delays"] = [0.5, 3, 10]
# fail it
d.sched.answerCard(c, 1)
# it should have three reps left to graduation
@ -188,14 +198,15 @@ def test_learn():
assert c.queue == 2
assert c.due == 321
def test_learn_collapsed():
d = getEmptyCol()
# add 2 notes
f = d.newNote()
f['Front'] = "1"
f["Front"] = "1"
f = d.addNote(f)
f = d.newNote()
f['Front'] = "2"
f["Front"] = "2"
f = d.addNote(f)
# set as a learn card and rebuild queues
d.db.execute("update cards set queue=0, type=0")
@ -214,15 +225,16 @@ def test_learn_collapsed():
c = d.sched.getCard()
assert not c.q().endswith("2")
def test_learn_day():
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
f = d.addNote(f)
d.sched.reset()
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
d.sched.answerCard(c, 2)
# two reps to graduate, 1 more today
@ -266,17 +278,19 @@ def test_learn_day():
c.flush()
d.reset()
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()
d.sched.answerCard(c, 1)
assert c.queue == 3
assert d.sched.counts() == (0, 0, 0)
def test_reviews():
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
# set the card up as a review card, due 8 days ago
c = f.cards()[0]
@ -295,7 +309,7 @@ def test_reviews():
##################################################
# different delay to new
d.reset()
d.sched._cardConf(c)['lapse']['delays'] = [2, 20]
d.sched._cardConf(c)["lapse"]["delays"] = [2, 20]
d.sched.answerCard(c, 1)
assert c.queue == 1
# it should be due tomorrow, with an interval of 1
@ -355,8 +369,10 @@ def test_reviews():
c.flush()
# steup hook
hooked = []
def onLeech(card):
hooked.append(1)
addHook("leech", onLeech)
d.sched.answerCard(c, 1)
assert hooked
@ -364,10 +380,11 @@ def test_reviews():
c.load()
assert c.queue == -1
def test_button_spacing():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
# 1 day ivl review card due now
c = f.cards()[0]
@ -384,13 +401,14 @@ def test_button_spacing():
assert ni(c, 3) == "3 days"
assert ni(c, 4) == "4 days"
def test_overdue_lapse():
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2
return
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
# simulate a review that was lapsed and is now due for its normal review
c = f.cards()[0]
@ -419,13 +437,15 @@ def test_overdue_lapse():
d.sched.reset()
assert d.sched.counts() == (0, 0, 1)
def test_finished():
d = getEmptyCol()
# nothing due
assert "Congratulations" in d.sched.finishedMsg()
assert "limit" not in d.sched.finishedMsg()
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
# have a new card
assert "new cards available" in d.sched.finishedMsg()
@ -438,15 +458,17 @@ def test_finished():
assert "Congratulations" in d.sched.finishedMsg()
assert "limit" not in d.sched.finishedMsg()
def test_nextIvl():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
d.reset()
conf = d.decks.confForDid(1)
conf['new']['delays'] = [0.5, 3, 10]
conf['lapse']['delays'] = [1, 5, 9]
conf["new"]["delays"] = [0.5, 3, 10]
conf["lapse"]["delays"] = [1, 5, 9]
c = d.sched.getCard()
# new cards
##################################################
@ -484,7 +506,7 @@ def test_nextIvl():
# failing it should put it at 60s
assert ni(c, 1) == 60
# 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
# (* 100 1.2 86400)10368000.0
assert ni(c, 2) == 10368000
@ -494,10 +516,11 @@ def test_nextIvl():
assert ni(c, 4) == 28080000
assert d.sched.nextIvlStr(c, 4) == "10.8 months"
def test_misc():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
# burying
@ -508,10 +531,11 @@ def test_misc():
d.reset()
assert d.sched.getCard()
def test_suspend():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
# suspending
@ -525,7 +549,11 @@ def test_suspend():
d.reset()
assert d.sched.getCard()
# should cope with rev cards being relearnt
c.due = 0; c.ivl = 100; c.type = 2; c.queue = 2; c.flush()
c.due = 0
c.ivl = 100
c.type = 2
c.queue = 2
c.flush()
d.reset()
c = d.sched.getCard()
d.sched.answerCard(c, 1)
@ -551,10 +579,11 @@ def test_suspend():
assert c.due == 1
assert c.did == 1
def test_cram():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
c.ivl = 100
@ -582,7 +611,7 @@ def test_cram():
assert d.sched.nextIvl(c, 1) == 600
assert d.sched.nextIvl(c, 2) == 138 * 60 * 60 * 24
cram = d.decks.get(did)
cram['delays'] = [1, 10]
cram["delays"] = [1, 10]
assert d.sched.answerButtons(c) == 3
assert d.sched.nextIvl(c, 1) == 60
assert d.sched.nextIvl(c, 2) == 600
@ -595,8 +624,7 @@ def test_cram():
assert c.odue == 138
assert c.queue == 1
# should be logged as a cram rep
assert d.db.scalar(
"select type from revlog order by id desc limit 1") == 3
assert d.db.scalar("select type from revlog order by id desc limit 1") == 3
# check ivls again
assert d.sched.nextIvl(c, 1) == 60
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
assert c.did == 1
def test_cram_rem():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
oldDue = f.cards()[0].due
did = d.decks.newDyn("Cram")
@ -681,16 +710,17 @@ def test_cram_rem():
assert c.type == c.queue == 0
assert c.due == oldDue
def test_cram_resched():
# add card
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
# cram deck
did = d.decks.newDyn("Cram")
cram = d.decks.get(did)
cram['resched'] = False
cram["resched"] = False
d.sched.rebuildDyn(did)
d.reset()
# graduate should return it to new
@ -786,22 +816,25 @@ def test_cram_resched():
# d.sched.answerCard(c, 2)
# print c.__dict__
def test_ordcycle():
d = getEmptyCol()
# 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['qfmt'] = "{{Back}}"
t['afmt'] = "{{Front}}"
t["qfmt"] = "{{Back}}"
t["afmt"] = "{{Front}}"
mm.addTemplate(m, t)
t = mm.newTemplate("f2")
t['qfmt'] = "{{Front}}"
t['afmt'] = "{{Back}}"
t["qfmt"] = "{{Front}}"
t["afmt"] = "{{Back}}"
mm.addTemplate(m, t)
mm.save(m)
# create a new note; it should have 3 cards
f = d.newNote()
f['Front'] = "1"; f['Back'] = "1"
f["Front"] = "1"
f["Back"] = "1"
d.addNote(f)
assert d.cardCount() == 3
d.reset()
@ -810,10 +843,12 @@ def test_ordcycle():
assert d.sched.getCard().ord == 1
assert d.sched.getCard().ord == 2
def test_counts_idx():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
d.reset()
assert d.sched.counts() == (1, 0, 0)
@ -832,10 +867,11 @@ def test_counts_idx():
d.sched.answerCard(c, 1)
assert d.sched.counts() == (0, 2, 0)
def test_repCounts():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
d.reset()
# lrnReps should be accurate on pass/fail
@ -853,7 +889,7 @@ def test_repCounts():
d.sched.answerCard(d.sched.getCard(), 2)
assert d.sched.counts() == (0, 0, 0)
f = d.newNote()
f['Front'] = "two"
f["Front"] = "two"
d.addNote(f)
d.reset()
# initial pass should be correct too
@ -865,14 +901,14 @@ def test_repCounts():
assert d.sched.counts() == (0, 0, 0)
# immediate graduate should work
f = d.newNote()
f['Front'] = "three"
f["Front"] = "three"
d.addNote(f)
d.reset()
d.sched.answerCard(d.sched.getCard(), 3)
assert d.sched.counts() == (0, 0, 0)
# and failing a review should too
f = d.newNote()
f['Front'] = "three"
f["Front"] = "three"
d.addNote(f)
c = f.cards()[0]
c.type = 2
@ -884,12 +920,13 @@ def test_repCounts():
d.sched.answerCard(d.sched.getCard(), 1)
assert d.sched.counts() == (0, 1, 0)
def test_timing():
d = getEmptyCol()
# add a few review cards, due today
for i in range(5):
f = d.newNote()
f['Front'] = "num"+str(i)
f["Front"] = "num" + str(i)
d.addNote(f)
c = f.cards()[0]
c.type = 2
@ -900,7 +937,7 @@ def test_timing():
d.reset()
c = d.sched.getCard()
# set a a fail delay of 1 second so we don't have to wait
d.sched._cardConf(c)['lapse']['delays'][0] = 1/60.0
d.sched._cardConf(c)["lapse"]["delays"][0] = 1 / 60.0
d.sched.answerCard(c, 1)
# the next card should be another review
c = d.sched.getCard()
@ -910,11 +947,12 @@ def test_timing():
c = d.sched.getCard()
assert c.queue == 1
def test_collapse():
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
d.reset()
# test collapsing
@ -924,16 +962,17 @@ def test_collapse():
d.sched.answerCard(c, 3)
assert not d.sched.getCard()
def test_deckDue():
d = getEmptyCol()
# add a note with default deck
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
# and one that's a child
f = d.newNote()
f['Front'] = "two"
default1 = f.model()['did'] = d.decks.id("Default::1")
f["Front"] = "two"
default1 = f.model()["did"] = d.decks.id("Default::1")
d.addNote(f)
# make it a review card
c = f.cards()[0]
@ -942,13 +981,13 @@ def test_deckDue():
c.flush()
# add one more with a new deck
f = d.newNote()
f['Front'] = "two"
foobar = f.model()['did'] = d.decks.id("foo::bar")
f["Front"] = "two"
foobar = f.model()["did"] = d.decks.id("foo::bar")
d.addNote(f)
# and one that's a sibling
f = d.newNote()
f['Front'] = "three"
foobaz = f.model()['did'] = d.decks.id("foo::baz")
f["Front"] = "three"
foobaz = f.model()["did"] = d.decks.id("foo::baz")
d.addNote(f)
d.reset()
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][4] == 0
# 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.deckDueTree()
def test_deckTree():
d = getEmptyCol()
d.decks.id("new::b::c")
@ -983,38 +1024,40 @@ def test_deckTree():
names.remove("new")
assert "new" not in names
def test_deckFlow():
d = getEmptyCol()
# add a note with default deck
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
# and one that's a child
f = d.newNote()
f['Front'] = "two"
default1 = f.model()['did'] = d.decks.id("Default::2")
f["Front"] = "two"
default1 = f.model()["did"] = d.decks.id("Default::2")
d.addNote(f)
# and another that's higher up
f = d.newNote()
f['Front'] = "three"
default1 = f.model()['did'] = d.decks.id("Default::1")
f["Front"] = "three"
default1 = f.model()["did"] = d.decks.id("Default::1")
d.addNote(f)
# should get top level one first, then ::1, then ::2
d.reset()
assert d.sched.counts() == (3, 0, 0)
for i in "one", "three", "two":
c = d.sched.getCard()
assert c.note()['Front'] == i
assert c.note()["Front"] == i
d.sched.answerCard(c, 2)
def test_reorder():
d = getEmptyCol()
# add a note with default deck
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
f2 = d.newNote()
f2['Front'] = "two"
f2["Front"] = "two"
d.addNote(f2)
assert f2.cards()[0].due == 2
found = False
@ -1029,29 +1072,32 @@ def test_reorder():
assert f.cards()[0].due == 1
# shifting
f3 = d.newNote()
f3['Front'] = "three"
f3["Front"] = "three"
d.addNote(f3)
f4 = d.newNote()
f4['Front'] = "four"
f4["Front"] = "four"
d.addNote(f4)
assert f.cards()[0].due == 1
assert f2.cards()[0].due == 2
assert f3.cards()[0].due == 3
assert f4.cards()[0].due == 4
d.sched.sortCards([
f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
d.sched.sortCards([f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
assert f.cards()[0].due == 3
assert f2.cards()[0].due == 4
assert f3.cards()[0].due == 1
assert f4.cards()[0].due == 2
def test_forget():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
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()
d.reset()
assert d.sched.counts() == (0, 0, 1)
@ -1059,10 +1105,11 @@ def test_forget():
d.reset()
assert d.sched.counts() == (1, 0, 0)
def test_resched():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
d.sched.reschedCards([c.id], 0, 0)
@ -1075,11 +1122,12 @@ def test_resched():
assert c.due == d.sched.today + 1
assert c.ivl == +1
def test_norelearn():
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
c.type = 2
@ -1093,13 +1141,15 @@ def test_norelearn():
c.flush()
d.reset()
d.sched.answerCard(c, 1)
d.sched._cardConf(c)['lapse']['delays'] = []
d.sched._cardConf(c)["lapse"]["delays"] = []
d.sched.answerCard(c, 1)
def test_failmult():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
c = f.cards()[0]
c.type = 2
@ -1111,7 +1161,7 @@ def test_failmult():
c.lapses = 1
c.startTimer()
c.flush()
d.sched._cardConf(c)['lapse']['mult'] = 0.5
d.sched._cardConf(c)["lapse"]["mult"] = 0.5
c = d.sched.getCard()
d.sched.answerCard(c, 1)
assert c.ivl == 50

View file

@ -12,31 +12,38 @@ from anki.hooks import addHook
lt = time.localtime()
if lt.tm_hour > 2 and lt.tm_hour < 4:
orig_time = time.time
def adjusted_time():
return orig_time() - 60 * 60 * 2
time.time = adjusted_time
def test_clock():
d = getEmptyCol()
if (d.sched.dayCutoff - intTime()) < 10 * 60:
raise Exception("Unit tests will fail around the day rollover.")
def checkRevIvl(d, c, targetIvl):
min, max = d.sched._fuzzIvlRange(targetIvl)
return min <= c.ivl <= max
def test_basics():
d = getEmptyCol()
d.reset()
assert not d.sched.getCard()
def test_new():
d = getEmptyCol()
d.reset()
assert d.sched.newCount == 0
# add a note
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
d.reset()
assert d.sched.newCount == 1
@ -74,15 +81,16 @@ def test_new():
# assert qs[n] in c.q()
# d.sched.answerCard(c, 2)
def test_newLimits():
d = getEmptyCol()
# add some notes
g2 = d.decks.id("Default::foo")
for i in range(30):
f = d.newNote()
f['Front'] = str(i)
f["Front"] = str(i)
if i > 4:
f.model()['did'] = g2
f.model()["did"] = g2
d.addNote(f)
# give the child deck a different configuration
c2 = d.decks.confId("new conf")
@ -95,33 +103,36 @@ def test_newLimits():
assert c.did == 1
# limit the parent to 10 cards, meaning we get 10 in total
conf1 = d.decks.confForDid(1)
conf1['new']['perDay'] = 10
conf1["new"]["perDay"] = 10
d.reset()
assert d.sched.newCount == 10
# if we limit child to 4, we should get 9
conf2 = d.decks.confForDid(g2)
conf2['new']['perDay'] = 4
conf2["new"]["perDay"] = 4
d.reset()
assert d.sched.newCount == 9
def test_newBoxes():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
d.reset()
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)
# should handle gracefully
d.sched._cardConf(c)['new']['delays'] = [1]
d.sched._cardConf(c)["new"]["delays"] = [1]
d.sched.answerCard(c, 2)
def test_learn():
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
f = d.addNote(f)
# set as a learn card and rebuild queues
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
c = d.sched.getCard()
assert c
d.sched._cardConf(c)['new']['delays'] = [0.5, 3, 10]
d.sched._cardConf(c)["new"]["delays"] = [0.5, 3, 10]
# fail it
d.sched.answerCard(c, 1)
# it should have three reps left to graduation
@ -176,10 +187,11 @@ def test_learn():
# revlog should have been updated each time
assert d.db.scalar("select count() from revlog where type = 0") == 5
def test_relearn():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
c.ivl = 100
@ -201,10 +213,11 @@ def test_relearn():
assert c.ivl == 2
assert c.due == d.sched.today + c.ivl
def test_relearn_no_steps():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
c.ivl = 100
@ -213,7 +226,7 @@ def test_relearn_no_steps():
c.flush()
conf = d.decks.confForDid(1)
conf['lapse']['delays'] = []
conf["lapse"]["delays"] = []
d.decks.save(conf)
# fail the card
@ -222,14 +235,15 @@ def test_relearn_no_steps():
d.sched.answerCard(c, 1)
assert c.type == c.queue == 2
def test_learn_collapsed():
d = getEmptyCol()
# add 2 notes
f = d.newNote()
f['Front'] = "1"
f["Front"] = "1"
f = d.addNote(f)
f = d.newNote()
f['Front'] = "2"
f["Front"] = "2"
f = d.addNote(f)
# set as a learn card and rebuild queues
d.db.execute("update cards set queue=0, type=0")
@ -248,15 +262,16 @@ def test_learn_collapsed():
c = d.sched.getCard()
assert not c.q().endswith("2")
def test_learn_day():
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
f = d.addNote(f)
d.sched.reset()
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
d.sched.answerCard(c, 3)
# two reps to graduate, 1 more today
@ -300,17 +315,19 @@ def test_learn_day():
c.flush()
d.reset()
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()
d.sched.answerCard(c, 1)
assert c.queue == 3
assert d.sched.counts() == (0, 0, 0)
def test_reviews():
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
# set the card up as a review card, due 8 days ago
c = f.cards()[0]
@ -367,8 +384,10 @@ def test_reviews():
c.flush()
# steup hook
hooked = []
def onLeech(card):
hooked.append(1)
addHook("leech", onLeech)
d.sched.answerCard(c, 1)
assert hooked
@ -376,6 +395,7 @@ def test_reviews():
c.load()
assert c.queue == -1
def test_review_limits():
d = getEmptyCol()
@ -385,21 +405,22 @@ def test_review_limits():
pconf = d.decks.getConf(d.decks.confId("parentConf"))
cconf = d.decks.getConf(d.decks.confId("childConf"))
pconf['rev']['perDay'] = 5
pconf["rev"]["perDay"] = 5
d.decks.updateConf(pconf)
d.decks.setConf(parent, pconf['id'])
cconf['rev']['perDay'] = 10
d.decks.setConf(parent, pconf["id"])
cconf["rev"]["perDay"] = 10
d.decks.updateConf(cconf)
d.decks.setConf(child, cconf['id'])
d.decks.setConf(child, cconf["id"])
m = d.models.current()
m['did'] = child['id']
m["did"] = child["id"]
d.models.save(m, updateReqs=False)
# add some cards
for i in range(20):
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
# make them reviews
@ -414,7 +435,7 @@ def test_review_limits():
assert tree[1][5][0][2] == 5 # child
# .counts() should match
d.decks.select(child['id'])
d.decks.select(child["id"])
d.sched.reset()
assert d.sched.counts() == (0, 0, 5)
@ -428,9 +449,9 @@ def test_review_limits():
assert tree[1][5][0][2] == 4 # child
# switch limits
d.decks.setConf(parent, cconf['id'])
d.decks.setConf(child, pconf['id'])
d.decks.select(parent['id'])
d.decks.setConf(parent, cconf["id"])
d.decks.setConf(child, pconf["id"])
d.decks.select(parent["id"])
d.sched.reset()
# child limits do not affect the parent
@ -438,10 +459,11 @@ def test_review_limits():
assert tree[1][2] == 9 # parent
assert tree[1][5][0][2] == 4 # child
def test_button_spacing():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
# 1 day ivl review card due now
c = f.cards()[0]
@ -460,16 +482,17 @@ def test_button_spacing():
# if hard factor is <= 1, then hard may not increase
conf = d.decks.confForDid(1)
conf['rev']['hardFactor'] = 1
conf["rev"]["hardFactor"] = 1
assert ni(c, 2) == "1 day"
def test_overdue_lapse():
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2
return
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
# simulate a review that was lapsed and is now due for its normal review
c = f.cards()[0]
@ -498,13 +521,15 @@ def test_overdue_lapse():
d.sched.reset()
assert d.sched.counts() == (0, 0, 1)
def test_finished():
d = getEmptyCol()
# nothing due
assert "Congratulations" in d.sched.finishedMsg()
assert "limit" not in d.sched.finishedMsg()
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
# have a new card
assert "new cards available" in d.sched.finishedMsg()
@ -517,15 +542,17 @@ def test_finished():
assert "Congratulations" in d.sched.finishedMsg()
assert "limit" not in d.sched.finishedMsg()
def test_nextIvl():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
d.reset()
conf = d.decks.confForDid(1)
conf['new']['delays'] = [0.5, 3, 10]
conf['lapse']['delays'] = [1, 5, 9]
conf["new"]["delays"] = [0.5, 3, 10]
conf["lapse"]["delays"] = [1, 5, 9]
c = d.sched.getCard()
# new cards
##################################################
@ -566,7 +593,7 @@ def test_nextIvl():
# failing it should put it at 60s
assert ni(c, 1) == 60
# 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
# (* 100 1.2 86400)10368000.0
assert ni(c, 2) == 10368000
@ -576,14 +603,15 @@ def test_nextIvl():
assert ni(c, 4) == 28080000
assert d.sched.nextIvlStr(c, 4) == "10.8 months"
def test_bury():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
f = d.newNote()
f['Front'] = "two"
f["Front"] = "two"
d.addNote(f)
c2 = f.cards()[0]
# burying
@ -598,11 +626,14 @@ def test_bury():
assert not d.sched.getCard()
d.sched.unburyCardsForDeck(type="manual")
c.load(); assert c.queue == 0
c2.load(); assert c2.queue == -2
c.load()
assert c.queue == 0
c2.load()
assert c2.queue == -2
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.unburyCardsForDeck(type="all")
@ -611,10 +642,11 @@ def test_bury():
assert d.sched.counts() == (2, 0, 0)
def test_suspend():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
# suspending
@ -628,7 +660,11 @@ def test_suspend():
d.reset()
assert d.sched.getCard()
# should cope with rev cards being relearnt
c.due = 0; c.ivl = 100; c.type = 2; c.queue = 2; c.flush()
c.due = 0
c.ivl = 100
c.type = 2
c.queue = 2
c.flush()
d.reset()
c = d.sched.getCard()
d.sched.answerCard(c, 1)
@ -656,10 +692,11 @@ def test_suspend():
assert c.did != 1
assert c.odue == 1
def test_filt_reviewing_early_normal():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
c.ivl = 100
@ -696,8 +733,7 @@ def test_filt_reviewing_early_normal():
# should not be in learning
assert c.queue == 2
# should be logged as a cram rep
assert d.db.scalar(
"select type from revlog order by id desc limit 1") == 3
assert d.db.scalar("select type from revlog order by id desc limit 1") == 3
# due in 75 days, so it's been waiting 25 days
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, 4) == 114 * 86400
def test_filt_keep_lrn_state():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
# fail the card outside filtered deck
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.sched.answerCard(c, 1)
@ -753,21 +790,22 @@ def test_filt_keep_lrn_state():
assert c.left == 1001
assert c.due - intTime() > 60 * 60
def test_preview():
# add cards
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
orig = copy.copy(c)
f2 = d.newNote()
f2['Front'] = "two"
f2["Front"] = "two"
d.addNote(f2)
# cram deck
did = d.decks.newDyn("Cram")
cram = d.decks.get(did)
cram['resched'] = False
cram["resched"] = False
d.sched.rebuildDyn(did)
d.reset()
# grab the first card
@ -801,22 +839,25 @@ def test_preview():
assert c.reps == 0
assert c.type == 0
def test_ordcycle():
d = getEmptyCol()
# 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['qfmt'] = "{{Back}}"
t['afmt'] = "{{Front}}"
t["qfmt"] = "{{Back}}"
t["afmt"] = "{{Front}}"
mm.addTemplate(m, t)
t = mm.newTemplate("f2")
t['qfmt'] = "{{Front}}"
t['afmt'] = "{{Back}}"
t["qfmt"] = "{{Front}}"
t["afmt"] = "{{Back}}"
mm.addTemplate(m, t)
mm.save(m)
# create a new note; it should have 3 cards
f = d.newNote()
f['Front'] = "1"; f['Back'] = "1"
f["Front"] = "1"
f["Back"] = "1"
d.addNote(f)
assert d.cardCount() == 3
d.reset()
@ -825,10 +866,12 @@ def test_ordcycle():
assert d.sched.getCard().ord == 1
assert d.sched.getCard().ord == 2
def test_counts_idx():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
d.reset()
assert d.sched.counts() == (1, 0, 0)
@ -847,10 +890,11 @@ def test_counts_idx():
d.sched.answerCard(c, 1)
assert d.sched.counts() == (0, 1, 0)
def test_repCounts():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
d.reset()
# lrnReps should be accurate on pass/fail
@ -868,7 +912,7 @@ def test_repCounts():
d.sched.answerCard(d.sched.getCard(), 3)
assert d.sched.counts() == (0, 0, 0)
f = d.newNote()
f['Front'] = "two"
f["Front"] = "two"
d.addNote(f)
d.reset()
# initial pass should be correct too
@ -880,14 +924,14 @@ def test_repCounts():
assert d.sched.counts() == (0, 0, 0)
# immediate graduate should work
f = d.newNote()
f['Front'] = "three"
f["Front"] = "three"
d.addNote(f)
d.reset()
d.sched.answerCard(d.sched.getCard(), 4)
assert d.sched.counts() == (0, 0, 0)
# and failing a review should too
f = d.newNote()
f['Front'] = "three"
f["Front"] = "three"
d.addNote(f)
c = f.cards()[0]
c.type = 2
@ -899,12 +943,13 @@ def test_repCounts():
d.sched.answerCard(d.sched.getCard(), 1)
assert d.sched.counts() == (0, 1, 0)
def test_timing():
d = getEmptyCol()
# add a few review cards, due today
for i in range(5):
f = d.newNote()
f['Front'] = "num"+str(i)
f["Front"] = "num" + str(i)
d.addNote(f)
c = f.cards()[0]
c.type = 2
@ -925,11 +970,12 @@ def test_timing():
c = d.sched.getCard()
assert c.queue == 1
def test_collapse():
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
d.reset()
# test collapsing
@ -939,16 +985,17 @@ def test_collapse():
d.sched.answerCard(c, 4)
assert not d.sched.getCard()
def test_deckDue():
d = getEmptyCol()
# add a note with default deck
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
# and one that's a child
f = d.newNote()
f['Front'] = "two"
default1 = f.model()['did'] = d.decks.id("Default::1")
f["Front"] = "two"
default1 = f.model()["did"] = d.decks.id("Default::1")
d.addNote(f)
# make it a review card
c = f.cards()[0]
@ -957,13 +1004,13 @@ def test_deckDue():
c.flush()
# add one more with a new deck
f = d.newNote()
f['Front'] = "two"
foobar = f.model()['did'] = d.decks.id("foo::bar")
f["Front"] = "two"
foobar = f.model()["did"] = d.decks.id("foo::bar")
d.addNote(f)
# and one that's a sibling
f = d.newNote()
f['Front'] = "three"
foobaz = f.model()['did'] = d.decks.id("foo::baz")
f["Front"] = "three"
foobaz = f.model()["did"] = d.decks.id("foo::baz")
d.addNote(f)
d.reset()
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][4] == 0
# 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.deckDueTree()
def test_deckTree():
d = getEmptyCol()
d.decks.id("new::b::c")
@ -998,38 +1047,40 @@ def test_deckTree():
names.remove("new")
assert "new" not in names
def test_deckFlow():
d = getEmptyCol()
# add a note with default deck
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
# and one that's a child
f = d.newNote()
f['Front'] = "two"
default1 = f.model()['did'] = d.decks.id("Default::2")
f["Front"] = "two"
default1 = f.model()["did"] = d.decks.id("Default::2")
d.addNote(f)
# and another that's higher up
f = d.newNote()
f['Front'] = "three"
default1 = f.model()['did'] = d.decks.id("Default::1")
f["Front"] = "three"
default1 = f.model()["did"] = d.decks.id("Default::1")
d.addNote(f)
# should get top level one first, then ::1, then ::2
d.reset()
assert d.sched.counts() == (3, 0, 0)
for i in "one", "three", "two":
c = d.sched.getCard()
assert c.note()['Front'] == i
assert c.note()["Front"] == i
d.sched.answerCard(c, 3)
def test_reorder():
d = getEmptyCol()
# add a note with default deck
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
f2 = d.newNote()
f2['Front'] = "two"
f2["Front"] = "two"
d.addNote(f2)
assert f2.cards()[0].due == 2
found = False
@ -1044,29 +1095,32 @@ def test_reorder():
assert f.cards()[0].due == 1
# shifting
f3 = d.newNote()
f3['Front'] = "three"
f3["Front"] = "three"
d.addNote(f3)
f4 = d.newNote()
f4['Front'] = "four"
f4["Front"] = "four"
d.addNote(f4)
assert f.cards()[0].due == 1
assert f2.cards()[0].due == 2
assert f3.cards()[0].due == 3
assert f4.cards()[0].due == 4
d.sched.sortCards([
f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
d.sched.sortCards([f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
assert f.cards()[0].due == 3
assert f2.cards()[0].due == 4
assert f3.cards()[0].due == 1
assert f4.cards()[0].due == 2
def test_forget():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
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()
d.reset()
assert d.sched.counts() == (0, 0, 1)
@ -1074,10 +1128,11 @@ def test_forget():
d.reset()
assert d.sched.counts() == (1, 0, 0)
def test_resched():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
d.sched.reschedCards([c.id], 0, 0)
@ -1090,11 +1145,12 @@ def test_resched():
assert c.due == d.sched.today + 1
assert c.ivl == +1
def test_norelearn():
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
c.type = 2
@ -1108,13 +1164,15 @@ def test_norelearn():
c.flush()
d.reset()
d.sched.answerCard(c, 1)
d.sched._cardConf(c)['lapse']['delays'] = []
d.sched._cardConf(c)["lapse"]["delays"] = []
d.sched.answerCard(c, 1)
def test_failmult():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
c = f.cards()[0]
c.type = 2
@ -1126,19 +1184,20 @@ def test_failmult():
c.lapses = 1
c.startTimer()
c.flush()
d.sched._cardConf(c)['lapse']['mult'] = 0.5
d.sched._cardConf(c)["lapse"]["mult"] = 0.5
c = d.sched.getCard()
d.sched.answerCard(c, 1)
assert c.ivl == 50
d.sched.answerCard(c, 1)
assert c.ivl == 25
def test_moveVersions():
col = getEmptyCol()
col.changeSchedulerVer(1)
n = col.newNote()
n['Front'] = "one"
n["Front"] = "one"
col.addNote(n)
# make it a learning card
@ -1176,8 +1235,10 @@ def test_moveVersions():
col.changeSchedulerVer(2)
# card with 100 day interval, answering again
col.sched.reschedCards([c.id], 100, 100)
c.load(); c.due = 0; c.flush()
col.sched._cardConf(c)['lapse']['mult'] = 0.5
c.load()
c.due = 0
c.flush()
col.sched._cardConf(c)["lapse"]["mult"] = 0.5
col.sched.reset()
c = col.sched.getCard()
col.sched.answerCard(c, 1)
@ -1186,6 +1247,7 @@ def test_moveVersions():
c.load()
assert c.due == 50
# cards with a due date earlier than the collection should retain
# their due date when removed
def test_negativeDueFilter():
@ -1193,7 +1255,8 @@ def test_negativeDueFilter():
# card due prior to collection date
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
c = f.cards()[0]
c.due = -5
@ -1209,4 +1272,3 @@ def test_negativeDueFilter():
c.load()
assert c.due == -5

View file

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

View file

@ -2,15 +2,18 @@ from anki.template import Template
def test_remove_formatting_from_mathjax():
t = Template('')
assert t._removeFormattingFromMathjax(r'\(2^{{c3::2}}\)', 3) == r'\(2^{{C3::2}}\)'
t = Template("")
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\) '
r'{{c4::blah}} {{c5::text with \(x^2\) jax}}')
txt = (
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
# formatting.
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) == (
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 anki.consts import *
def test_op():
d = getEmptyCol()
# should have no undo by default
assert not d.undoName()
# let's adjust a study option
d.save("studyopts")
d.conf['abc'] = 5
d.conf["abc"] = 5
# it should be listed as undoable
assert d.undoName() == "studyopts"
# with about 5 minutes until it's clobbered
@ -18,7 +19,7 @@ def test_op():
# undoing should restore the old value
d.undo()
assert not d.undoName()
assert 'abc' not in d.conf
assert "abc" not in d.conf
# an (auto)save will clear the undo
d.save("foo")
assert d.undoName() == "foo"
@ -27,7 +28,7 @@ def test_op():
# and a review will, too
d.save("add")
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
d.reset()
assert d.undoName() == "add"
@ -35,11 +36,12 @@ def test_op():
d.sched.answerCard(c, 2)
assert d.undoName() == "Review"
def test_review():
d = getEmptyCol()
d.conf['counts'] = COUNT_REMAINING
d.conf["counts"] = COUNT_REMAINING
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
d.reset()
assert not d.undoName()
@ -62,7 +64,7 @@ def test_review():
assert not d.undoName()
# we should be able to undo multiple answers too
f = d.newNote()
f['Front'] = "two"
f["Front"] = "two"
d.addNote(f)
d.reset()
assert d.sched.counts() == (2, 0, 0)
@ -85,5 +87,3 @@ def test_review():
assert d.undoName() == "foo"
d.undo()
assert not d.undoName()

View file

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