diff --git a/Makefile b/Makefile index 9cae8398a..791b9df50 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/tests/shared.py b/tests/shared.py index 70acfe4c0..1ddf13601 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -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__) diff --git a/tests/test_addons.py b/tests/test_addons.py index 954ea8594..69bc54245 100644 --- a/tests/test_addons.py +++ b/tests/test_addons.py @@ -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"}]}', {} ) diff --git a/tests/test_cards.py b/tests/test_cards.py index b0732b80b..6e519c84f 100644 --- a/tests/test_cards.py +++ b/tests/test_cards.py @@ -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 - - - diff --git a/tests/test_collection.py b/tests/test_collection.py index 37304cd47..1868cf8b9 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -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) diff --git a/tests/test_decks.py b/tests/test_decks.py index 86aa8f50c..b53510d27 100644 --- a/tests/test_decks.py +++ b/tests/test_decks.py @@ -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() diff --git a/tests/test_exporting.py b/tests/test_exporting.py index f1a14d800..f02754c3e 100644 --- a/tests/test_exporting.py +++ b/tests/test_exporting.py @@ -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
"; f.tags = ["tag", "tag2"] + f["Front"] = "foo" + f["Back"] = "bar
" + 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,13 +90,14 @@ 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.crt -= 86400 * 10 deck.sched.reset() c = deck.sched.getCard() deck.sched.answerCard(c, 3) @@ -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()) diff --git a/tests/test_find.py b/tests/test_find.py index cc7e91ede..f1a79edee 100644 --- a/tests/test_find.py +++ b/tests/test_find.py @@ -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 @@ -97,7 +108,7 @@ def test_findCards(): assert deck.findCards("is:suspended") == [c.id] # nids assert deck.findCards("nid:54321") == [] - assert len(deck.findCards("nid:%d"%f.id)) == 2 + assert len(deck.findCards("nid:%d" % f.id)) == 2 assert len(deck.findCards("nid:%d,%d" % (f1id, f2id))) == 2 # templates with helper.assertRaises(Exception): @@ -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,25 +151,26 @@ def test_findCards(): deck.findCards("deck:*cefault") # full search f = deck.newNote() - f['Front'] = 'helloworld' - f['Back'] = 'abc' + f["Front"] = "helloworld" + 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 + # 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 - #assert len(deck.findCards("back:helloworld", full=True)) == 2 + # assert len(deck.findCards("helloworld", full=True)) == 2 + # assert len(deck.findCards("back:helloworld", full=True)) == 2 # searching for an invalid special tag should not error with helper.assertRaises(Exception): 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" diff --git a/tests/test_flags.py b/tests/test_flags.py index 11796b692..1fb988fc4 100644 --- a/tests/test_flags.py +++ b/tests/test_flags.py @@ -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 diff --git a/tests/test_importing.py b/tests/test_importing.py index 67218d994..fb51a9383 100644 --- a/tests/test_importing.py +++ b/tests/test_importing.py @@ -1,22 +1,28 @@ # coding: utf-8 -import os -from tests.shared import getUpgradeDeckPath, getEmptyCol +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 +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 + 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,16 +187,17 @@ 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")) i = SupermemoXmlImporter(deck, file) - #i.META.logToStdOutput = True + # i.META.logToStdOutput = True i.run() assert i.total == 1 cid = deck.db.scalar("select id from cards") @@ -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")) diff --git a/tests/test_latex.py b/tests/test_latex.py index 8b5937a20..5bd6c9dae 100644 --- a/tests/test_latex.py +++ b/tests/test_latex.py @@ -4,17 +4,19 @@ import os import shutil -from tests.shared import getEmptyCol +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) diff --git a/tests/test_media.py b/tests/test_media.py index e8198a543..6f23cd392 100644 --- a/tests/test_media.py +++ b/tests/test_media.py @@ -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, "aoeuao") == ["foo.jpg"] assert mf(mid, "aoeuao") == ["foo.jpg"] assert mf(mid, "aoeuao") == [ - "foo.jpg", "bar.jpg"] + "foo.jpg", + "bar.jpg", + ] assert mf(mid, "aoeuao") == ["foo.jpg"] assert mf(mid, "") == ["one", "two"] - assert mf(mid, "aoeuao") == ["foo.jpg"] - assert mf(mid, "aoeuao") == [ - "foo.jpg", "fo"] + assert mf(mid, 'aoeuao') == ["foo.jpg"] + assert mf(mid, 'aoeuao') == [ + "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("") == "" assert es('') == '' + 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'] = "" + f["Front"] = "one" + f["Back"] = "" d.addNote(f) # and one which references a non-existent file f = d.newNote() - f['Front'] = "one"; f['Back'] = "" + f["Front"] = "one" + f["Back"] = "" 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 @@ -97,26 +109,27 @@ def test_changes(): assert not list(removed()) # but if we add another file, it will time.sleep(1) - with open(path+"2", "w") as f: + with open(path + "2", "w") as f: f.write("yo") d.media.findChanges() assert len(list(added())) == 2 assert not list(removed()) # deletions should get noticed too time.sleep(1) - os.unlink(path+"2") + os.unlink(path + "2") d.media.findChanges() assert len(list(added())) == 1 assert len(list(removed())) == 1 + def test_illegal(): d = getEmptyCol() aString = "a:b|cd\\e/f\0g*h" good = "abcdefgh" assert d.media.stripIllegal(aString) == good for c in aString: - bad = d.media.hasIllegal("somestring"+c+"morestring") + 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 diff --git a/tests/test_models.py b/tests/test_models.py index 823be124e..7deb26b44 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -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,64 +112,67 @@ 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 - - #We replace the default Cloze template + 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() # first card should have first ord assert c.ord == 0 assert c2.ord == 1 - + 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'] = 'helloworld' + f["Front"] = "helloworld" 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 [...]" in f.cards()[0].q() assert "hello world" 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 "[typical]" in f.cards()[0].q() assert "world" 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 "[...] 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 "b c" in ( - f.cards()[0].a()) + assert "b c" 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\) [...] \[ [...] \]') + assert f.cards()[0].q().endswith("\(a\) [...] \[ [...] \]") def test_chained_mods(): d = getEmptyCol() d.models.setCurrent(d.models.byName("Cloze")) - m = d.models.current(); mm = d.models - - #We replace the default Cloze template + 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 = 'phrase' - a1 = 'sentence' - q2 = 'en chaine' - a2 = 'chained' - f['Text'] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (q1,a1,q2,a2) + q1 = 'phrase' + a1 = "sentence" + q2 = 'en chaine' + a2 = "chained" + f["Text"] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % ( + q1, + a1, + q2, + a2, + ) assert d.addNote(f) == 1 - assert "This [sentence] demonstrates [chained] clozes." in f.cards()[0].q() - assert "This phrase demonstrates en chaine clozes." in f.cards()[0].a() + assert ( + "This [sentence] demonstrates [chained] clozes." + in f.cards()[0].q() + ) + assert ( + "This phrase demonstrates en chaine 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]] - #testing any - opt['tmpls'][1]['qfmt'] = "{{Back}}{{Add Reverse}}" + assert opt["req"][1] == [1, "all", [1, 2]] + # testing any + opt["tmpls"][1]["qfmt"] = "{{Back}}{{Add Reverse}}" mm.save(opt, templates=True) - assert opt['req'][1] == [1, 'any', [1, 2]] - #testing None - opt['tmpls'][1]['qfmt'] = "{{^Add Reverse}}{{Back}}{{/Add Reverse}}" + assert opt["req"][1] == [1, "any", [1, 2]] + # testing None + 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() diff --git a/tests/test_schedv1.py b/tests/test_schedv1.py index 4a2c00b3b..ad426164d 100644 --- a/tests/test_schedv1.py +++ b/tests/test_schedv1.py @@ -5,35 +5,41 @@ import copy from anki.consts import STARTING_FACTOR from tests.shared import getEmptyCol as getEmptyColOrig -from anki.utils import intTime +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: + 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,12 +136,12 @@ 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 - assert c.left%1000 == 3 - assert c.left//1000 == 3 + assert c.left % 1000 == 3 + assert c.left // 1000 == 3 # it should by due in 30 seconds t = round(c.due - time.time()) assert t >= 25 and t <= 40 @@ -139,8 +149,8 @@ def test_learn(): d.sched.answerCard(c, 2) # it should by due in 3 minutes assert round(c.due - time.time()) in (179, 180) - assert c.left%1000 == 2 - assert c.left//1000 == 2 + assert c.left % 1000 == 2 + assert c.left // 1000 == 2 # check log is accurate log = d.db.first("select * from revlog order by id desc") assert log[3] == 2 @@ -150,8 +160,8 @@ def test_learn(): d.sched.answerCard(c, 2) # it should by due in 10 minutes assert round(c.due - time.time()) in (599, 600) - assert c.left%1000 == 1 - assert c.left//1000 == 1 + assert c.left % 1000 == 1 + assert c.left // 1000 == 1 # the next pass should graduate the card assert c.queue == 1 assert c.type == 1 @@ -159,7 +169,7 @@ def test_learn(): assert c.queue == 2 assert c.type == 2 # should be due tomorrow, with an interval of 1 - assert c.due == d.sched.today+1 + assert c.due == d.sched.today + 1 assert c.ivl == 1 # or normal removal c.type = 0 @@ -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,27 +225,28 @@ 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 - assert c.left%1000 == 3 - assert c.left//1000 == 1 + assert c.left % 1000 == 3 + assert c.left // 1000 == 1 assert d.sched.counts() == (0, 1, 0) c = d.sched.getCard() ni = d.sched.nextIvl assert ni(c, 2) == 86400 # answering it will place it in queue 3 d.sched.answerCard(c, 2) - assert c.due == d.sched.today+1 + assert c.due == d.sched.today + 1 assert c.queue == 3 assert not d.sched.getCard() # for testing, move it back a day @@ -244,7 +256,7 @@ def test_learn_day(): assert d.sched.counts() == (0, 1, 0) c = d.sched.getCard() # nextIvl should work - assert ni(c, 2) == 86400*2 + assert ni(c, 2) == 86400 * 2 # if we fail it, it should be back in the correct queue d.sched.answerCard(c, 1) assert c.queue == 1 @@ -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 @@ -313,7 +327,7 @@ def test_reviews(): # check ests. ni = d.sched.nextIvl assert ni(c, 1) == 120 - assert ni(c, 2) == 20*60 + assert ni(c, 2) == 20 * 60 # try again with an ease of 2 instead ################################################## c = copy.copy(cardcopy) @@ -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,44 +458,46 @@ 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 ################################################## ni = d.sched.nextIvl assert ni(c, 1) == 30 assert ni(c, 2) == 180 - assert ni(c, 3) == 4*86400 + assert ni(c, 3) == 4 * 86400 d.sched.answerCard(c, 1) # cards in learning ################################################## assert ni(c, 1) == 30 assert ni(c, 2) == 180 - assert ni(c, 3) == 4*86400 + assert ni(c, 3) == 4 * 86400 d.sched.answerCard(c, 2) assert ni(c, 1) == 30 assert ni(c, 2) == 600 - assert ni(c, 3) == 4*86400 + assert ni(c, 3) == 4 * 86400 d.sched.answerCard(c, 2) # normal graduation is tomorrow - assert ni(c, 2) == 1*86400 - assert ni(c, 3) == 4*86400 + assert ni(c, 2) == 1 * 86400 + assert ni(c, 3) == 4 * 86400 # lapsed cards ################################################## c.type = 2 c.ivl = 100 c.factor = STARTING_FACTOR assert ni(c, 1) == 60 - assert ni(c, 2) == 100*86400 - assert ni(c, 3) == 100*86400 + assert ni(c, 2) == 100 * 86400 + assert ni(c, 3) == 100 * 86400 # review cards ################################################## c.queue = 2 @@ -484,8 +506,8 @@ 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']=[] - assert ni(c, 1) == 1*86400 + d.sched._cardConf(c)["lapse"]["delays"] = [] + assert ni(c, 1) == 1 * 86400 # (* 100 1.2 86400)10368000.0 assert ni(c, 2) == 10368000 # (* 100 2.5 86400)21600000.0 @@ -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 @@ -566,7 +595,7 @@ def test_cram(): c.startTimer() c.flush() d.reset() - assert d.sched.counts() == (0,0,0) + assert d.sched.counts() == (0, 0, 0) cardcopy = copy.copy(c) # create a dynamic deck and refresh it did = d.decks.newDyn("Cram") @@ -575,18 +604,18 @@ def test_cram(): # should appear as new in the deck list assert sorted(d.sched.deckDueList())[0][4] == 1 # and should appear in the counts - assert d.sched.counts() == (1,0,0) + assert d.sched.counts() == (1, 0, 0) # grab it and check estimates c = d.sched.getCard() assert d.sched.answerButtons(c) == 2 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['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 - assert d.sched.nextIvl(c, 3) == 138*60*60*24 + assert d.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24 d.sched.answerCard(c, 2) # elapsed time was 75 days # factor = 2.5+1.2/2 = 1.85 @@ -595,12 +624,11 @@ 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 - assert d.sched.nextIvl(c, 3) == 138*60*60*24 + assert d.sched.nextIvl(c, 2) == 138 * 60 * 60 * 24 + assert d.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24 # when it graduates, due is updated c = d.sched.getCard() d.sched.answerCard(c, 2) @@ -616,7 +644,7 @@ def test_cram(): # check ivls again - passing should be idempotent assert d.sched.nextIvl(c, 1) == 60 assert d.sched.nextIvl(c, 2) == 600 - assert d.sched.nextIvl(c, 3) == 138*60*60*24 + assert d.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24 d.sched.answerCard(c, 2) assert c.ivl == 138 assert c.odue == 138 @@ -630,20 +658,20 @@ def test_cram(): assert len(d.sched.deckDueList()) == 1 c.load() assert c.ivl == 1 - assert c.due == d.sched.today+1 + assert c.due == d.sched.today + 1 # make it due d.reset() - assert d.sched.counts() == (0,0,0) + assert d.sched.counts() == (0, 0, 0) c.due = -5 c.ivl = 100 c.flush() d.reset() - assert d.sched.counts() == (0,0,1) + assert d.sched.counts() == (0, 0, 1) # cram again did = d.decks.newDyn("Cram") d.sched.rebuildDyn(did) d.reset() - assert d.sched.counts() == (0,0,1) + assert d.sched.counts() == (0, 0, 1) c.load() assert d.sched.answerButtons(c) == 4 # add a sibling so we can test minSpace, etc @@ -661,10 +689,11 @@ def test_cram(): # it should have been moved back to the original deck 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,75 +1024,80 @@ 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) + 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 + found = False # 50/50 chance of being reordered for i in range(20): d.sched.randomizeCards(1) if f.cards()[0].due != f.id: - found=True + found = True break assert found d.sched.orderCards(1) 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) @@ -1072,14 +1119,15 @@ def test_resched(): assert c.queue == c.type == 2 d.sched.reschedCards([c.id], 1, 1) c.load() - assert c.due == d.sched.today+1 + 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 diff --git a/tests/test_schedv2.py b/tests/test_schedv2.py index f3825027b..a1eb012cd 100644 --- a/tests/test_schedv2.py +++ b/tests/test_schedv2.py @@ -5,38 +5,45 @@ import copy from anki.consts import STARTING_FACTOR from tests.shared import getEmptyCol -from anki.utils import intTime +from anki.utils import intTime from anki.hooks import addHook # Between 2-4AM, shift the time back so test assumptions hold. 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 + return orig_time() - 60 * 60 * 2 + time.time = adjusted_time + def test_clock(): 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.") + 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,12 +140,12 @@ 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 - assert c.left%1000 == 3 - assert c.left//1000 == 3 + assert c.left % 1000 == 3 + assert c.left // 1000 == 3 # it should by due in 30 seconds t = round(c.due - time.time()) assert t >= 25 and t <= 40 @@ -142,9 +153,9 @@ def test_learn(): d.sched.answerCard(c, 3) # it should by due in 3 minutes dueIn = c.due - time.time() - assert 179 <= dueIn <= 180*1.25 - assert c.left%1000 == 2 - assert c.left//1000 == 2 + assert 179 <= dueIn <= 180 * 1.25 + assert c.left % 1000 == 2 + assert c.left // 1000 == 2 # check log is accurate log = d.db.first("select * from revlog order by id desc") assert log[3] == 3 @@ -154,9 +165,9 @@ def test_learn(): d.sched.answerCard(c, 3) # it should by due in 10 minutes dueIn = c.due - time.time() - assert 599 <= dueIn <= 600*1.25 - assert c.left%1000 == 1 - assert c.left//1000 == 1 + assert 599 <= dueIn <= 600 * 1.25 + assert c.left % 1000 == 1 + assert c.left // 1000 == 1 # the next pass should graduate the card assert c.queue == 1 assert c.type == 1 @@ -164,7 +175,7 @@ def test_learn(): assert c.queue == 2 assert c.type == 2 # should be due tomorrow, with an interval of 1 - assert c.due == d.sched.today+1 + assert c.due == d.sched.today + 1 assert c.ivl == 1 # or normal removal c.type = 0 @@ -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,27 +262,28 @@ 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 - assert c.left%1000 == 3 - assert c.left//1000 == 1 + assert c.left % 1000 == 3 + assert c.left // 1000 == 1 assert d.sched.counts() == (0, 1, 0) c = d.sched.getCard() ni = d.sched.nextIvl assert ni(c, 3) == 86400 # answering it will place it in queue 3 d.sched.answerCard(c, 3) - assert c.due == d.sched.today+1 + assert c.due == d.sched.today + 1 assert c.queue == 3 assert not d.sched.getCard() # for testing, move it back a day @@ -278,7 +293,7 @@ def test_learn_day(): assert d.sched.counts() == (0, 1, 0) c = d.sched.getCard() # nextIvl should work - assert ni(c, 3) == 86400*2 + assert ni(c, 3) == 86400 * 2 # if we fail it, it should be back in the correct queue d.sched.answerCard(c, 1) assert c.queue == 1 @@ -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 @@ -410,11 +431,11 @@ def test_review_limits(): tree = d.sched.deckDueTree() # (('Default', 1, 0, 0, 0, ()), ('parent', 1514457677462, 5, 0, 0, (('child', 1514457677463, 5, 0, 0, ()),))) - assert tree[1][2] == 5 # parent - assert tree[1][5][0][2] == 5 # child + assert tree[1][2] == 5 # parent + 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) @@ -424,24 +445,25 @@ def test_review_limits(): assert d.sched.counts() == (0, 0, 4) tree = d.sched.deckDueTree() - assert tree[1][2] == 4 # parent - assert tree[1][5][0][2] == 4 # child + assert tree[1][2] == 4 # parent + 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 tree = d.sched.deckDueTree() - assert tree[1][2] == 9 # parent - assert tree[1][5][0][2] == 4 # child + 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,47 +542,49 @@ 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 ################################################## ni = d.sched.nextIvl assert ni(c, 1) == 30 - assert ni(c, 2) == (30+180)//2 + assert ni(c, 2) == (30 + 180) // 2 assert ni(c, 3) == 180 - assert ni(c, 4) == 4*86400 + assert ni(c, 4) == 4 * 86400 d.sched.answerCard(c, 1) # cards in learning ################################################## assert ni(c, 1) == 30 - assert ni(c, 2) == (30+180)//2 + assert ni(c, 2) == (30 + 180) // 2 assert ni(c, 3) == 180 - assert ni(c, 4) == 4*86400 + assert ni(c, 4) == 4 * 86400 d.sched.answerCard(c, 3) assert ni(c, 1) == 30 - assert ni(c, 2) == (180+600)//2 + assert ni(c, 2) == (180 + 600) // 2 assert ni(c, 3) == 600 - assert ni(c, 4) == 4*86400 + assert ni(c, 4) == 4 * 86400 d.sched.answerCard(c, 3) # normal graduation is tomorrow - assert ni(c, 3) == 1*86400 - assert ni(c, 4) == 4*86400 + assert ni(c, 3) == 1 * 86400 + assert ni(c, 4) == 4 * 86400 # lapsed cards ################################################## c.type = 2 c.ivl = 100 c.factor = STARTING_FACTOR assert ni(c, 1) == 60 - assert ni(c, 3) == 100*86400 - assert ni(c, 4) == 101*86400 + assert ni(c, 3) == 100 * 86400 + assert ni(c, 4) == 101 * 86400 # review cards ################################################## c.queue = 2 @@ -566,8 +593,8 @@ 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']=[] - assert ni(c, 1) == 1*86400 + d.sched._cardConf(c)["lapse"]["delays"] = [] + assert ni(c, 1) == 1 * 86400 # (* 100 1.2 86400)10368000.0 assert ni(c, 2) == 10368000 # (* 100 2.5 86400)21600000.0 @@ -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 @@ -671,7 +708,7 @@ def test_filt_reviewing_early_normal(): c.startTimer() c.flush() d.reset() - assert d.sched.counts() == (0,0,0) + assert d.sched.counts() == (0, 0, 0) # create a dynamic deck and refresh it did = d.decks.newDyn("Cram") d.sched.rebuildDyn(did) @@ -679,14 +716,14 @@ def test_filt_reviewing_early_normal(): # should appear as normal in the deck list assert sorted(d.sched.deckDueList())[0][2] == 1 # and should appear in the counts - assert d.sched.counts() == (0,0,1) + assert d.sched.counts() == (0, 0, 1) # grab it and check estimates c = d.sched.getCard() assert d.sched.answerButtons(c) == 4 assert d.sched.nextIvl(c, 1) == 600 - assert d.sched.nextIvl(c, 2) == int(75*1.2)*86400 - assert d.sched.nextIvl(c, 3) == int(75*2.5)*86400 - assert d.sched.nextIvl(c, 4) == int(75*2.5*1.15)*86400 + assert d.sched.nextIvl(c, 2) == int(75 * 1.2) * 86400 + assert d.sched.nextIvl(c, 3) == int(75 * 2.5) * 86400 + assert d.sched.nextIvl(c, 4) == int(75 * 2.5 * 1.15) * 86400 # answer 'good' d.sched.answerCard(c, 3) @@ -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 @@ -707,20 +743,21 @@ def test_filt_reviewing_early_normal(): d.reset() c = d.sched.getCard() - assert d.sched.nextIvl(c, 2) == 60*86400 - assert d.sched.nextIvl(c, 3) == 100*86400 - assert d.sched.nextIvl(c, 4) == 114*86400 + assert d.sched.nextIvl(c, 2) == 60 * 86400 + 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) @@ -744,30 +781,31 @@ def test_filt_keep_lrn_state(): # should be able to advance learning steps d.sched.answerCard(c, 3) # should be due at least an hour in the future - assert c.due - intTime() > 60*60 + assert c.due - intTime() > 60 * 60 # emptying the deck preserves learning state d.sched.emptyDyn(did) c.load() assert c.type == c.queue == 1 assert c.left == 1001 - assert c.due - intTime() > 60*60 + 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,75 +1047,80 @@ 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) + 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 + found = False # 50/50 chance of being reordered for i in range(20): d.sched.randomizeCards(1) if f.cards()[0].due != f.id: - found=True + found = True break assert found d.sched.orderCards(1) 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) @@ -1087,14 +1142,15 @@ def test_resched(): assert c.queue == c.type == 2 d.sched.reschedCards([c.id], 1, 1) c.load() - assert c.due == d.sched.today+1 + 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 - diff --git a/tests/test_stats.py b/tests/test_stats.py index b7212d1e5..587050878 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -1,12 +1,13 @@ # coding: utf-8 -import os -from tests.shared import getEmptyCol +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() diff --git a/tests/test_template.py b/tests/test_template.py index 7132c1240..5a61e11f8 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -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}} \]" + ) diff --git a/tests/test_undo.py b/tests/test_undo.py index 457112987..1fdfff8f7 100644 --- a/tests/test_undo.py +++ b/tests/test_undo.py @@ -1,16 +1,17 @@ # coding: utf-8 import time -from tests.shared import getEmptyCol +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() - - diff --git a/tests/test_utils.py b/tests/test_utils.py index 96d787a83..8ac52d4f1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -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"