def setup_basic(): global deck1, deck2, client, server deck1 = getEmptyDeck() # add a note to deck 1 f = deck1.newNote() f["Front"] = u"foo" f["Back"] = u"bar" f.tags = [u"foo"] deck1.addNote(f) # answer it deck1.reset() deck1.sched.answerCard(deck1.sched.getCard(), 4) # repeat for deck2 deck2 = getEmptyDeck(server=True) f = deck2.newNote() f["Front"] = u"bar" f["Back"] = u"bar" f.tags = [u"bar"] deck2.addNote(f) deck2.reset() deck2.sched.answerCard(deck2.sched.getCard(), 4) # start with same schema and sync time deck1.scm = deck2.scm = 0 # and same mod time, so sync does nothing t = intTime(1000) deck1.save(mod=t) deck2.save(mod=t) server = LocalServer(deck2) client = Syncer(deck1, server)
def test_counts(): d = getEmptyDeck() # add a second group assert d.groupId("new group") == 2 # for each card type for type in range(3): # and each of the groups for gid in (1,2): # create a new fact f = d.newFact() f['Front'] = u"one" d.addFact(f) c = f.cards()[0] # set type/gid c.type = type c.queue = type c.gid = gid c.due = 0 c.flush() d.reset() # with the default settings, there's no count limit assert d.sched.counts() == (2,2,2) # check limit to one group d.qconf['groups'] = [1] d.reset() assert d.sched.counts() == (1,1,1) # we don't need to build the queue to get the counts assert d.sched.allCounts() == (2,2,2) assert d.sched.selCounts() == (1,1,1) assert d.sched.allCounts() == (2,2,2)
def test_remove(): deck = getEmptyDeck() # can't remove the default deck assertException(AssertionError, lambda: deck.decks.rem(1)) # create a new deck, and add a note/card to it g1 = deck.decks.id("g1") f = deck.newNote() f['Front'] = u"1" f.model()['did'] = g1 deck.addNote(f) c = f.cards()[0] assert c.did == g1 # by default deleting the deck leaves the cards with an invalid did assert deck.cardCount() == 1 deck.decks.rem(g1) assert deck.cardCount() == 1 c.load() assert c.did == g1 # but if we try to get it, we get the default 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() # 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_delete(): deck = getEmptyDeck() f = deck.newFact() f['Front'] = u'1' f['Back'] = u'2' deck.addFact(f) cid = f.cards()[0].id deck.reset() deck.sched.answerCard(deck.sched.getCard(), 2) assert deck.db.scalar("select count() from revlog") == 1 deck.delCards([cid]) assert deck.cardCount() == 0 assert deck.factCount() == 0 assert deck.db.scalar("select count() from facts") == 0 assert deck.db.scalar("select count() from cards") == 0 assert deck.db.scalar("select count() from fsums") == 0 assert deck.db.scalar("select count() from revlog") == 0 assert deck.db.scalar("select count() from graves") == 0 # add the fact back deck.addFact(f) assert deck.cardCount() == 1 cid = f.cards()[0].id # delete again, this time with syncing enabled deck.syncName = "abc" deck.lastSync = time.time() deck.delCards([cid]) assert deck.cardCount() == 0 assert deck.factCount() == 0 assert deck.db.scalar("select count() from graves") != 0
def test_genrem(): d = getEmptyDeck() f = d.newNote() f['Front'] = u'1' f['Back'] = u'' 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'] = "" 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}}" 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.flush() assert len(f.cards()) == 2
def test_fieldChecksum(): deck = getEmptyDeck() f = deck.newFact() f['Front'] = u"new"; f['Back'] = u"new2" deck.addFact(f) assert deck.db.scalar( "select csum from fsums") == int("22af645d", 16) # empty field should have no checksum f['Front'] = u"" f.flush() assert deck.db.scalar( "select count() from fsums") == 0 # changing the val should change the checksum f['Front'] = u"newx" f.flush() assert deck.db.scalar( "select csum from fsums") == int("4b0e5a4c", 16) # turning off unique and modifying the fact should delete the sum m = f.model() m.fields[0]['uniq'] = False m.flush() f.flush() assert deck.db.scalar( "select count() from fsums") == 0 # and turning on both should ensure two checksums generated m.fields[0]['uniq'] = True m.fields[1]['uniq'] = True m.flush() f.flush() assert deck.db.scalar( "select count() from fsums") == 2
def test_suspend(): d = getEmptyDeck() f = d.newFact() f['Front'] = u"one" d.addFact(f) c = f.cards()[0] # suspending d.reset() assert d.sched.getCard() d.sched.suspendCards([c.id]) d.reset() assert not d.sched.getCard() # unsuspending d.sched.unsuspendCards([c.id]) d.reset() assert d.sched.getCard() # should cope with rev cards being relearnt 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) assert c.due >= time.time() assert c.queue == 1 assert c.type == 2 d.sched.suspendCards([c.id]) d.sched.unsuspendCards([c.id]) c.load() assert c.queue == 2 assert c.type == 2 assert c.due == 1
def test_fieldChecksum(): deck = getEmptyDeck() f = deck.newFact() f['Front'] = u"new" f['Back'] = u"new2" deck.addFact(f) assert deck.db.scalar("select csum from fsums") == int("22af645d", 16) # empty field should have no checksum f['Front'] = u"" f.flush() assert deck.db.scalar("select count() from fsums") == 0 # changing the val should change the checksum f['Front'] = u"newx" f.flush() assert deck.db.scalar("select csum from fsums") == int("4b0e5a4c", 16) # turning off unique and modifying the fact should delete the sum m = f.model() m.fields[0]['uniq'] = False m.flush() f.flush() assert deck.db.scalar("select count() from fsums") == 0 # and turning on both should ensure two checksums generated m.fields[0]['uniq'] = True m.fields[1]['uniq'] = True m.flush() f.flush() assert deck.db.scalar("select count() from fsums") == 2
def test_selective(): deck = getEmptyDeck() f = deck.newFact() f['Front'] = u"1" f.tags = ["one", "three"] deck.addFact(f) f = deck.newFact() f['Front'] = u"2" f.tags = ["two", "three", "four"] deck.addFact(f) f = deck.newFact() f['Front'] = u"3" f.tags = ["one", "two", "three", "four"] deck.addFact(f) assert len(deck.selTagFids(["one"], [])) == 2 assert len(deck.selTagFids(["three"], [])) == 3 assert len(deck.selTagFids([], ["three"])) == 0 assert len(deck.selTagFids(["one"], ["three"])) == 0 assert len(deck.selTagFids(["one"], ["two"])) == 1 assert len(deck.selTagFids(["two", "three"], [])) == 3 assert len(deck.selTagFids(["two", "three"], ["one"])) == 1 assert len(deck.selTagFids(["one", "three"], ["two", "four"])) == 1 deck.setGroupForTags(["three"], [], 3) assert deck.db.scalar("select count() from cards where gid = 3") == 3 deck.setGroupForTags(["one"], [], 2) assert deck.db.scalar("select count() from cards where gid = 2") == 2
def test_findDupes(): deck = getEmptyDeck() f = deck.newNote() f['Front'] = u'foo' f['Back'] = u'bar' deck.addNote(f) f2 = deck.newNote() f2['Front'] = u'baz' f2['Back'] = u'bar' deck.addNote(f2) f3 = deck.newNote() f3['Front'] = u'quux' f3['Back'] = u'bar' deck.addNote(f3) f4 = deck.newNote() f4['Front'] = u'quuux' f4['Back'] = u'nope' deck.addNote(f4) r = deck.findDupes("Back") assert r[0][0] == "bar" assert len(r[0][1]) == 3 # valid search r = deck.findDupes("Back", "bar") assert r[0][0] == "bar" assert len(r[0][1]) == 3 # excludes everything r = deck.findDupes("Back", "invalid") assert not r # front isn't dupe assert deck.findDupes("Front") == []
def test_chained_mods(): d = getEmptyDeck() d.models.setCurrent(d.models.byName("Cloze")) m = d.models.current() mm = d.models #We replace the default Cloze template t = mm.newTemplate("ChainedCloze") t['qfmt'] = "{{cloze:text:Text}}" t['afmt'] = "{{text:cloze:Text}}" #independent of the order of mods mm.addTemplate(m, t) mm.save(m) d.models.remTemplate(m, m['tmpls'][0]) f = d.newNote() q1 = '<span style=\"color:red\">phrase</span>' a1 = '<b>sentence</b>' q2 = '<span style=\"color:red\">en chaine</span>' a2 = '<i>chained</i>' f['Text'] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % ( q1, a1, q2, a2) assert d.addNote(f) == 1 assert "This <span class=cloze>[sentence]</span> demonstrates <span class=cloze>[chained]</span> clozes." in f.cards( )[0].q() assert "This <span class=cloze>phrase</span> demonstrates <span class=cloze>en chaine</span> clozes." in f.cards( )[0].a()
def test_basic(): deck = getEmptyDeck() # we start with a standard deck assert len(deck.decks.decks) == 1 # it should have an id of 1 assert deck.decks.name(1) # create a new deck parentId = deck.decks.id("new deck") assert parentId assert len(deck.decks.decks) == 2 # should get the same id assert deck.decks.id("new deck") == parentId # we start with the default deck selected assert deck.decks.selected() == 1 assert deck.decks.active() == [1] # we can select a different deck deck.decks.select(parentId) assert deck.decks.selected() == parentId assert deck.decks.active() == [parentId] # let's create a child childId = deck.decks.id("new deck::child") # it should have been added to the active list assert deck.decks.selected() == parentId assert deck.decks.active() == [parentId, childId] # we can select the child individually too deck.decks.select(childId) assert deck.decks.selected() == childId assert deck.decks.active() == [childId]
def test_anki2(): global srcNotes, srcCards # get the deck to import tmp = getUpgradeDeckPath() u = Upgrader() src = u.upgrade(tmp) srcpath = src.path srcNotes = src.noteCount() srcCards = src.cardCount() srcRev = src.db.scalar("select count() from revlog") # add a media file for testing open(os.path.join(src.media.dir(), "foo.jpg"), "w").write("foo") src.close() # create a new empty deck dst = getEmptyDeck() # import src into dst imp = Anki2Importer(dst, srcpath) imp.run() def check(): assert dst.noteCount() == srcNotes assert dst.cardCount() == srcCards assert srcRev == dst.db.scalar("select count() from revlog") mids = [int(x) for x in dst.models.models.keys()] assert not dst.db.scalar( "select count() from notes where mid not in "+ids2str(mids)) assert not dst.db.scalar( "select count() from cards where nid not in (select id from notes)") assert not dst.db.scalar( "select count() from revlog where cid not in (select id from cards)") assert dst.fixIntegrity()[0].startswith("Database rebuilt") check() # importing should be idempotent imp.run() check() assert len(os.listdir(dst.media.dir())) == 1
def test_ordcycle(): d = getEmptyDeck() # add two more templates and set second active m = d.models.current() mm = d.models t = mm.newTemplate("Reverse") t['qfmt'] = "{{Back}}" t['afmt'] = "{{Front}}" mm.addTemplate(m, t) t = mm.newTemplate("f2") 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" d.addNote(f) assert d.cardCount() == 3 d.reset() # ordinals should arrive in order assert d.sched.getCard().ord == 0 assert d.sched.getCard().ord == 1 assert d.sched.getCard().ord == 2
def test_anki1_diffmodels(): # create a new empty deck dst = getEmptyDeck() # import the 1 card version of the model tmp = getUpgradeDeckPath("diffmodels1.anki") imp = Anki1Importer(dst, tmp) imp.run() before = dst.noteCount() # repeating the process should do nothing imp = Anki1Importer(dst, tmp) imp.run() assert before == dst.noteCount() # then the 2 card version tmp = getUpgradeDeckPath("diffmodels2.anki") imp = Anki1Importer(dst, tmp) imp.run() after = dst.noteCount() # as the model schemas differ, should have been imported as new model assert after == before + 1 # repeating the process should do nothing beforeModels = len(dst.models.all()) imp = Anki1Importer(dst, tmp) imp.run() after = dst.noteCount() assert after == before + 1 assert beforeModels == len(dst.models.all())
def test_findReplace(): deck = getEmptyDeck() f = deck.newNote() f['Front'] = u'foo' f['Back'] = u'bar' deck.addNote(f) f2 = deck.newNote() f2['Front'] = u'baz' f2['Back'] = u'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" # single field replace assert deck.findReplace(nids, "qux", "foo", field="Front") == 1 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" assert deck.findReplace(nids, "B.r", "reg", regex=True) == 1 f.load() assert f['Back'] == "reg"
def test_counts(): d = getEmptyDeck() # add a second group assert d.groupId("new group") == 2 # for each card type for type in range(3): # and each of the groups for gid in (1, 2): # create a new fact f = d.newFact() f['Front'] = u"one" d.addFact(f) c = f.cards()[0] # set type/gid c.type = type c.queue = type c.gid = gid c.due = 0 c.flush() d.reset() # with the default settings, there's no count limit assert d.sched.counts() == (2, 2, 2) # check limit to one group d.qconf['groups'] = [1] d.reset() assert d.sched.counts() == (1, 1, 1) # we don't need to build the queue to get the counts assert d.sched.allCounts() == (2, 2, 2) assert d.sched.selCounts() == (1, 1, 1) assert d.sched.allCounts() == (2, 2, 2)
def test_gendeck(): d = getEmptyDeck() cloze = d.models.byName("Cloze") d.models.setCurrent(cloze) f = d.newNote() f['Text'] = u'{{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 d.models.save(cloze) # a newly generated card should share the first card's deck f['Text'] += u'{{c2::two}}' f.flush() assert f.cards()[1].did == 1 # and same with multiple cards f['Text'] += u'{{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 # model default c = f.cards()[1] c.did = newId c.flush() f['Text'] += u'{{c4::four}}' f.flush() assert f.cards()[3].did == newId
def test_findReplace(): deck = getEmptyDeck() f = deck.newFact() f['Front'] = u'foo' f['Back'] = u'bar' deck.addFact(f) f2 = deck.newFact() f2['Front'] = u'baz' f2['Back'] = u'foo' deck.addFact(f2) fids = [f.id, f2.id] # should do nothing assert deck.findReplace(fids, "abc", "123") == 0 # global replace assert deck.findReplace(fids, "foo", "qux") == 2 f.load(); assert f['Front'] == "qux" f2.load(); assert f2['Back'] == "qux" # single field replace assert deck.findReplace(fids, "qux", "foo", field="Front") == 1 f.load(); assert f['Front'] == "foo" f2.load(); assert f2['Back'] == "qux" # regex replace assert deck.findReplace(fids, "B.r", "reg") == 0 f.load(); assert f['Back'] != "reg" assert deck.findReplace(fids, "B.r", "reg", regex=True) == 1 f.load(); assert f['Back'] == "reg"
def test_templates(): d = getEmptyDeck() m = d.currentModel() m.templates[1]['actv'] = True m.flush() f = d.newFact() f['Front'] = u'1' f['Back'] = u'2' d.addFact(f) assert d.cardCount() == 2 (c, c2) = f.cards() # first card should have first ord assert c.ord == 0 assert c2.ord == 1 # switch templates m.moveTemplate(c.template(), 1) c.load(); c2.load() assert c.ord == 1 assert c2.ord == 0 # removing a template should delete its cards m.delTemplate(m.templates[0]) assert d.cardCount() == 1 # and should have updated the other cards' ordinals c = f.cards()[0] assert c.ord == 0 stripHTML(c.q()) == "2"
def test_anki2_updates(): # create a new empty deck dst = getEmptyDeck() tmp = getUpgradeDeckPath("update1.apkg") imp = AnkiPackageImporter(dst, tmp) imp.run() assert imp.dupes == 0 assert imp.added == 1 assert imp.updated == 0 # importing again should be idempotent imp = AnkiPackageImporter(dst, tmp) imp.run() assert imp.dupes == 1 assert imp.added == 0 assert imp.updated == 0 # importing a newer note should update assert dst.noteCount() == 1 assert dst.db.scalar("select flds from notes").startswith("hello") tmp = getUpgradeDeckPath("update2.apkg") imp = AnkiPackageImporter(dst, tmp) imp.run() assert imp.dupes == 1 assert imp.added == 0 assert imp.updated == 1 assert dst.noteCount() == 1 assert dst.db.scalar("select flds from notes").startswith("goodbye")
def test_genrem(): d = getEmptyDeck() f = d.newNote() f['Front'] = u'1' f['Back'] = u'' 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'] = "" 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}}" mm.save(m, templates=True) assert len(f.cards()) == 1 # if we add to the note, a card should be automatically generated f.load() f['Back'] = "1" f.flush() assert len(f.cards()) == 2 # deleteion calls a hook to let the user abort the delete. let's abort it: def abort(val, *args): return False addHook("remEmptyCards", abort) f['Back'] = "" f.flush() assert len(f.cards()) == 2
def test_remove(): deck = getEmptyDeck() # create a new deck, and add a note/card to it g1 = deck.decks.id("g1") f = deck.newNote() f['Front'] = u"1" f.model()['did'] = g1 deck.addNote(f) c = f.cards()[0] assert c.did == g1 # by default deleting the deck leaves the cards with an invalid did assert deck.cardCount() == 1 deck.decks.rem(g1) assert deck.cardCount() == 1 c.load() assert c.did == g1 # but if we try to get it, we get the default 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() # 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_ordcycle(): d = getEmptyDeck() # add two more templates and set second active m = d.models.current() mm = d.models t = mm.newTemplate("Reverse") t["qfmt"] = "{{Back}}" t["afmt"] = "{{Front}}" mm.addTemplate(m, t) t = mm.newTemplate("f2") 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" d.addNote(f) assert d.cardCount() == 3 d.reset() # ordinals should arrive in order assert d.sched.getCard().ord == 0 assert d.sched.getCard().ord == 1 assert d.sched.getCard().ord == 2
def test_learn_collapsed(): d = getEmptyDeck() # add 2 notes f = d.newNote() f['Front'] = u"1" f = d.addNote(f) f = d.newNote() f['Front'] = u"2" f = d.addNote(f) # set as a learn card and rebuild queues d.db.execute("update cards set queue=0, type=0") d.reset() # should get '1' first c = d.sched.getCard() assert c.q().endswith("1") # pass it so it's due in 10 minutes d.sched.answerCard(c, 2) # get the other card c = d.sched.getCard() assert c.q().endswith("2") # fail it so it's due in 1 minute d.sched.answerCard(c, 1) # we shouldn't get the same card again c = d.sched.getCard() assert not c.q().endswith("2")
def test_op(): d = getEmptyDeck() # should have no undo by default assert not d.undoName() # let's adjust a study option d.save("studyopts") d.conf['abc'] = 5 # it should be listed as undoable assert d.undoName() == "studyopts" # with about 5 minutes until it's clobbered assert time.time() - d._lastSave < 1 # undoing should restore the old value d.undo() assert not d.undoName() assert 'abc' not in d.conf # an (auto)save will clear the undo d.save("foo") assert d.undoName() == "foo" d.save() assert not d.undoName() # and a review will, too d.save("add") f = d.newNote() f['Front'] = u"one" d.addNote(f) d.reset() assert d.undoName() == "add" c = d.sched.getCard() d.sched.answerCard(c, 2) assert d.undoName() == "Review"
def test_newLimits(): d = getEmptyDeck() # add some notes g2 = d.decks.id("Default::foo") for i in range(30): f = d.newNote() f['Front'] = str(i) if i > 4: f.model()['did'] = g2 d.addNote(f) # give the child deck a different configuration c2 = d.decks.confId("new conf") d.decks.setConf(d.decks.get(g2), c2) d.reset() # both confs have defaulted to a limit of 20 assert d.sched.newCount == 20 # first card we get comes from parent c = d.sched.getCard() 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 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 d.reset() assert d.sched.newCount == 9
def test_timing(): d = getEmptyDeck() # add a few review cards, due today for i in range(5): f = d.newNote() f['Front'] = "num" + str(i) d.addNote(f) c = f.cards()[0] c.type = 2 c.queue = 2 c.due = 0 c.flush() # fail the first one 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.answerCard(c, 1) # the next card should be another review c = d.sched.getCard() assert c.queue == 2 # but if we wait for a second, the failed card should come back time.sleep(1) c = d.sched.getCard() assert c.queue == 1
def test_overdue_lapse(): # disabled in commit 3069729776990980f34c25be66410e947e9d51a2 return d = getEmptyDeck() # add a note f = d.newNote() f['Front'] = u"one" d.addNote(f) # simulate a review that was lapsed and is now due for its normal review c = f.cards()[0] c.type = 2 c.queue = 1 c.due = -1 c.odue = -1 c.factor = 2500 c.left = 2002 c.ivl = 0 c.flush() d.sched._clearOverdue = False # checkpoint d.save() d.sched.reset() assert d.sched.counts() == (0, 2, 0) c = d.sched.getCard() d.sched.answerCard(c, 3) # it should be due tomorrow assert c.due == d.sched.today + 1 # revert to before d.rollback() d.sched._clearOverdue = True # with the default settings, the overdue card should be removed from the # learning queue d.sched.reset() assert d.sched.counts() == (0, 0, 1)
def test_newLimits(): d = getEmptyDeck() # add some notes g2 = d.decks.id("Default::foo") for i in range(30): f = d.newNote() f['Front'] = str(i) if i > 4: f.did = g2 d.addNote(f) # give the child deck a different configuration c2 = d.decks.confId("new conf") d.decks.setConf(d.decks.get(g2), c2) d.reset() # both confs have defaulted to a limit of 20 assert d.sched.newCount == 20 # first card we get comes from parent c = d.sched.getCard() 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 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 d.reset() assert d.sched.newCount == 9
def test_templates(): d = getEmptyDeck() m = d.models.current(); mm = d.models t = mm.newTemplate("Reverse") t['qfmt'] = "{{Back}}" t['afmt'] = "{{Front}}" mm.addTemplate(m, t) mm.save(m) f = d.newNote() f['Front'] = u'1' f['Back'] = u'2' 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 # switch templates d.models.moveTemplate(m, c.template(), 1) 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.cardCount() == 1 # and should have updated the other cards' ordinals c = f.cards()[0] assert c.ord == 0 assert stripHTML(c.q()) == "1" # it shouldn't be possible to orphan notes by removing templates t = mm.newTemplate(m) mm.addTemplate(m, t) assert not d.models.remTemplate(m, m['tmpls'][0])
def test_availOrds(): d = getEmptyDeck() m = d.models.current(); mm = d.models t = m['tmpls'][0] f = d.newNote() f['Front'] = "1" # simple templates assert mm.availOrds(m, joinFields(f.fields)) == [0] t['qfmt'] = "{{Back}}" mm.save(m, templates=True) assert not mm.availOrds(m, joinFields(f.fields)) # AND 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}}" mm.save(m, templates=True) assert not mm.availOrds(m, joinFields(f.fields)) # OR t['qfmt'] = "{{Front}}\n{{Back}}" mm.save(m, templates=True) assert mm.availOrds(m, joinFields(f.fields)) == [0] t['Front'] = "" t['Back'] = "1" assert mm.availOrds(m, joinFields(f.fields)) == [0]
def test_timing(): d = getEmptyDeck() # add a few review cards, due today for i in range(5): f = d.newNote() f['Front'] = "num"+str(i) d.addNote(f) c = f.cards()[0] c.type = 2 c.queue = 2 c.due = 0 c.flush() # fail the first one 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.answerCard(c, 1) # the next card should be another review c = d.sched.getCard() assert c.queue == 2 # but if we wait for a second, the failed card should come back time.sleep(1) c = d.sched.getCard() assert c.queue == 1
def test_csv(): deck = getEmptyDeck() file = unicode(os.path.join(testDir, "support/text-2fields.txt")) i = TextImporter(deck, file) i.initMapping() i.run() # four problems - too many & too few fields, a missing front, and a # duplicate entry assert len(i.log) == 5 assert i.total == 5 # if we run the import again, it should update instead i.run() assert len(i.log) == 10 assert i.total == 5 # but importing should not clobber tags if they're unmapped n = deck.getNote(deck.db.scalar("select id from notes")) n.addTag("test") n.flush() i.run() n.load() assert n.tags == ['test'] # if add-only mode, count will be 0 i.importMode = 1 i.run() assert i.total == 0 # and if dupes mode, will reimport everything assert deck.cardCount() == 5 i.importMode = 2 i.run() # includes repeated field assert i.total == 6 assert deck.cardCount() == 11 deck.close()
def test_anki2_diffmodels(): # create a new empty deck dst = getEmptyDeck() # import the 1 card version of the model tmp = getUpgradeDeckPath("diffmodels2-1.apkg") imp = AnkiPackageImporter(dst, tmp) imp.run() before = dst.noteCount() # repeating the process should do nothing imp = AnkiPackageImporter(dst, tmp) imp.run() assert before == dst.noteCount() # then the 2 card version tmp = getUpgradeDeckPath("diffmodels2-2.apkg") imp = AnkiPackageImporter(dst, tmp) imp.run() after = dst.noteCount() # as the model schemas differ, should have been imported as new model assert after == before + 1 # and the new model should have both cards assert dst.cardCount() == 3 # repeating the process should do nothing imp = AnkiPackageImporter(dst, tmp) imp.run() after = dst.noteCount() assert after == before + 1 assert dst.cardCount() == 3
def test_overdue_lapse(): d = getEmptyDeck() # add a note f = d.newNote() f['Front'] = u"one" d.addNote(f) # simulate a review that was lapsed and is now due for its normal review c = f.cards()[0] c.type = 2 c.queue = 1 c.due = -1 c.odue = -1 c.factor = 2500 c.left = 2 c.ivl = 0 c.flush() d.sched._clearOverdue = False # checkpoint d.save() d.sched.reset() assert d.sched.counts() == (0, 2, 0) c = d.sched.getCard() d.sched.answerCard(c, 3) # it should be due tomorrow assert c.due == d.sched.today + 1 # revert to before d.rollback() d.sched._clearOverdue = True # with the default settings, the overdue card should be removed from the # learning queue d.sched.reset() assert d.sched.counts() == (0, 0, 1)
def test_anki2(): global srcNotes, srcCards # get the deck to import tmp = getUpgradeDeckPath() u = Upgrader() src = u.upgrade(tmp) srcpath = src.path srcNotes = src.noteCount() srcCards = src.cardCount() srcRev = src.db.scalar("select count() from revlog") # add a media file for testing open(os.path.join(src.media.dir(), "_foo.jpg"), "w").write("foo") src.close() # create a new empty deck dst = getEmptyDeck() # import src into dst imp = Anki2Importer(dst, srcpath) imp.run() def check(): assert dst.noteCount() == srcNotes assert dst.cardCount() == srcCards assert srcRev == dst.db.scalar("select count() from revlog") mids = [int(x) for x in dst.models.models.keys()] assert not dst.db.scalar( "select count() from notes where mid not in "+ids2str(mids)) assert not dst.db.scalar( "select count() from cards where nid not in (select id from notes)") assert not dst.db.scalar( "select count() from revlog where cid not in (select id from cards)") assert dst.fixIntegrity()[0].startswith("Database rebuilt") check() # importing should be idempotent imp.run() check() assert len(os.listdir(dst.media.dir())) == 1
def test_csv(): deck = getEmptyDeck() file = unicode(os.path.join(testDir, "support/text-2fields.txt")) i = TextImporter(deck, file) i.initMapping() i.run() # four problems - too many & too few fields, a missing front, and a # duplicate entry assert len(i.log) == 5 assert i.total == 5 # if we run the import again, it should update instead i.run() assert len(i.log) == 5 assert i.total == 5 # but importing should not clobber tags if they're unmapped n = deck.getNote(deck.db.scalar("select id from notes")) n.addTag("test") n.flush() i.run() n.load() assert n.tags == ['test'] # if add-only mode, count will be 0 i.importMode = 1 i.run() assert i.total == 0 # and if dupes mode, will reimport everything assert deck.cardCount() == 5 i.importMode = 2 i.run() # includes repeated field assert i.total == 6 assert deck.cardCount() == 11 deck.close()
def test_suspend(): d = getEmptyDeck() f = d.newNote() f['Front'] = u"one" d.addNote(f) c = f.cards()[0] # suspending d.reset() assert d.sched.getCard() d.sched.suspendCards([c.id]) d.reset() assert not d.sched.getCard() # unsuspending d.sched.unsuspendCards([c.id]) d.reset() assert d.sched.getCard() # should cope with rev cards being relearnt 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) assert c.due >= time.time() assert c.queue == 1 assert c.type == 2 d.sched.suspendCards([c.id]) d.sched.unsuspendCards([c.id]) c.load() assert c.queue == 2 assert c.type == 2 assert c.due == 1
def test_suspended(): # create a new empty deck dst = getEmptyDeck() # import the 1 card version of the model tmp = getUpgradeDeckPath("suspended12.anki") imp = Anki1Importer(dst, tmp) imp.run() assert dst.db.scalar("select due from cards") < 0
def test_deckTree(): d = getEmptyDeck() d.decks.id("new::b::c") d.decks.id("new2") # new should not appear twice in tree names = [x[0] for x in d.sched.deckDueTree()] names.remove("new") assert "new" not in names
def test_text(): d = getEmptyDeck() m = d.models.current() m['tmpls'][0]['qfmt'] = "{{text:Front}}" d.models.save(m) f = d.newNote() f['Front'] = u'hello<b>world' d.addNote(f) assert "helloworld" in f.cards()[0].q()
def test_modelDelete(): deck = getEmptyDeck() f = deck.newNote() f['Front'] = u'1' f['Back'] = u'2' deck.addNote(f) assert deck.cardCount() == 1 deck.models.rem(deck.models.current()) assert deck.cardCount() == 0
def test_learn_day(): d = getEmptyDeck() # add a note f = d.newNote() f['Front'] = u"one" f = d.addNote(f) d.sched.reset() c = d.sched.getCard() 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 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.queue == 3 assert not d.sched.getCard() # for testing, move it back a day c.due -= 1 c.flush() d.reset() assert d.sched.counts() == (0, 1, 0) c = d.sched.getCard() # nextIvl should work 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 d.undo() d.reset() c = d.sched.getCard() d.sched.answerCard(c, 2) # simulate the passing of another two days c.due -= 2 c.flush() d.reset() # the last pass should graduate it into a review card assert ni(c, 2) == 86400 d.sched.answerCard(c, 2) assert c.queue == c.type == 2 # if the lapse step is tomorrow, failing it should handle the counts # correctly c.due = 0 c.flush() d.reset() assert d.sched.counts() == (0, 0, 1) 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_mnemo(): deck = getEmptyDeck() file = unicode(os.path.join(testDir, "support/mnemo.db")) i = MnemosyneImporter(deck, file) i.run() assert deck.cardCount() == 7 assert "a_longer_tag" in deck.tags.all() assert deck.db.scalar("select count() from cards where type = 0") == 1 deck.close()