mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 14:32:22 -04:00

The full sync threshold was a hack to ensure we synced the deck in a memory-efficient way if there was a lot of data to send. The problem is that it's not easy for the user to predict how many changes there are, and so it might come as a surprise to them when a sync suddenly switches to a full sync. In order to be able to send changes in chunks rather than all at once, some changes had to be made: - Clients now set usn=-1 when they modify an object, which allows us to distinguish between objects that have been modified on the server, and ones that have been modified on the client. If we don't do this, we would have to buffer the local changes in a temporary location before adding the server changes. - Before a client sends the objects to the server, it changes the usn to maxUsn both in the payload and the local storage. - We do deletions at the start - To determine which card or fact is newer, we have to fetch the modification time of the local version. We do this in batches rather than try to load the entire list in memory.
245 lines
7.6 KiB
Python
245 lines
7.6 KiB
Python
# coding: utf-8
|
|
|
|
import nose, os, tempfile, shutil, time
|
|
from tests.shared import assertException
|
|
|
|
from anki.errors import *
|
|
from anki import Deck
|
|
from anki.utils import intTime
|
|
from anki.sync import Syncer, LocalServer
|
|
from anki.facts import Fact
|
|
from anki.cards import Card
|
|
from tests.shared import getEmptyDeck
|
|
|
|
# Local tests
|
|
##########################################################################
|
|
|
|
deck1=None
|
|
deck2=None
|
|
client=None
|
|
server=None
|
|
|
|
def setup_basic(loadDecks=None):
|
|
global deck1, deck2, client, server
|
|
deck1 = getEmptyDeck()
|
|
# add a fact to deck 1
|
|
f = deck1.newFact()
|
|
f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = [u"foo"]
|
|
deck1.addFact(f)
|
|
# answer it
|
|
deck1.reset(); deck1.sched.answerCard(deck1.sched.getCard(), 4)
|
|
# repeat for deck2
|
|
deck2 = getEmptyDeck(server=True)
|
|
f = deck2.newFact()
|
|
f['Front'] = u"bar"; f['Back'] = u"bar"; f.tags = [u"bar"]
|
|
deck2.addFact(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 setup_modified():
|
|
setup_basic()
|
|
# mark deck1 as changed
|
|
deck1.save()
|
|
|
|
@nose.with_setup(setup_basic)
|
|
def test_nochange():
|
|
assert client.sync() == "noChanges"
|
|
|
|
@nose.with_setup(setup_modified)
|
|
def test_changedSchema():
|
|
deck1.scm += 1
|
|
assert client.sync() == "fullSync"
|
|
|
|
@nose.with_setup(setup_modified)
|
|
def test_sync():
|
|
def check(num):
|
|
for d in deck1, deck2:
|
|
for t in ("revlog", "facts", "cards", "fsums"):
|
|
assert d.db.scalar("select count() from %s" % t) == num
|
|
assert len(d.models.all()) == num*2
|
|
# the default group and config have an id of 1, so always 1
|
|
assert len(d.groups.all()) == 1
|
|
assert len(d.groups.gconf) == 1
|
|
assert len(d.tags.all()) == num
|
|
check(1)
|
|
origUsn = deck1.usn()
|
|
assert client.sync() == "success"
|
|
# last sync times and mod times should agree
|
|
assert deck1.mod == deck2.mod
|
|
assert deck1._usn == deck2._usn
|
|
assert deck1.mod == deck1.ls
|
|
assert deck1._usn != origUsn
|
|
# because everything was created separately it will be merged in. in
|
|
# actual use we use a full sync to ensure initial a common starting point.
|
|
check(2)
|
|
# repeating it does nothing
|
|
assert client.sync() == "noChanges"
|
|
# if we bump mod time, everything is copied across again because of the
|
|
# 600 second sync leeway. but the decks should remain the same.
|
|
deck1.save()
|
|
assert client.sync() == "success"
|
|
check(2)
|
|
|
|
@nose.with_setup(setup_modified)
|
|
def test_models():
|
|
test_sync()
|
|
# update model one
|
|
cm = deck1.models.current()
|
|
cm['name'] = "new"
|
|
time.sleep(1)
|
|
deck1.models.save(cm)
|
|
deck1.save()
|
|
assert deck2.models.get(cm['id'])['name'] == "Basic"
|
|
assert client.sync() == "success"
|
|
assert deck2.models.get(cm['id'])['name'] == "new"
|
|
# deleting triggers a full sync
|
|
deck1.scm = deck2.scm = 0
|
|
deck1.models.rem(cm)
|
|
deck1.save()
|
|
assert client.sync() == "fullSync"
|
|
|
|
@nose.with_setup(setup_modified)
|
|
def test_facts():
|
|
test_sync()
|
|
# modifications should be synced
|
|
fid = deck1.db.scalar("select id from facts")
|
|
fact = deck1.getFact(fid)
|
|
assert fact['Front'] != "abc"
|
|
fact['Front'] = "abc"
|
|
fact.flush()
|
|
deck1.save()
|
|
assert client.sync() == "success"
|
|
assert deck2.getFact(fid)['Front'] == "abc"
|
|
# deletions too
|
|
assert deck1.db.scalar("select 1 from facts where id = ?", fid)
|
|
deck1.remFacts([fid])
|
|
deck1.save()
|
|
assert client.sync() == "success"
|
|
assert not deck1.db.scalar("select 1 from facts where id = ?", fid)
|
|
assert not deck2.db.scalar("select 1 from facts where id = ?", fid)
|
|
|
|
@nose.with_setup(setup_modified)
|
|
def test_cards():
|
|
test_sync()
|
|
fid = deck1.db.scalar("select id from facts")
|
|
fact = deck1.getFact(fid)
|
|
card = fact.cards()[0]
|
|
# answer the card locally
|
|
card.startTimer()
|
|
deck1.sched.answerCard(card, 4)
|
|
assert card.reps == 2
|
|
deck1.save()
|
|
assert deck2.getCard(card.id).reps == 1
|
|
assert client.sync() == "success"
|
|
assert deck2.getCard(card.id).reps == 2
|
|
# if it's modified on both sides , later mod time should win
|
|
for test in ((deck1, deck2), (deck2, deck1)):
|
|
time.sleep(1)
|
|
c = test[0].getCard(card.id)
|
|
c.reps = 5; c.flush()
|
|
test[0].save()
|
|
time.sleep(1)
|
|
c = test[1].getCard(card.id)
|
|
c.reps = 3; c.flush()
|
|
test[1].save()
|
|
assert client.sync() == "success"
|
|
assert test[1].getCard(card.id).reps == 3
|
|
assert test[0].getCard(card.id).reps == 3
|
|
# removals should work too
|
|
deck1.remCards([card.id])
|
|
deck1.save()
|
|
assert deck2.db.scalar("select 1 from cards where id = ?", card.id)
|
|
assert client.sync() == "success"
|
|
assert not deck2.db.scalar("select 1 from cards where id = ?", card.id)
|
|
|
|
@nose.with_setup(setup_modified)
|
|
def test_tags():
|
|
test_sync()
|
|
assert deck1.tags.all() == deck2.tags.all()
|
|
deck1.tags.register(["abc"])
|
|
deck2.tags.register(["xyz"])
|
|
assert deck1.tags.all() != deck2.tags.all()
|
|
deck1.save()
|
|
deck2.save()
|
|
assert client.sync() == "success"
|
|
assert deck1.tags.all() == deck2.tags.all()
|
|
|
|
@nose.with_setup(setup_modified)
|
|
def test_groups():
|
|
test_sync()
|
|
assert len(deck1.groups.all()) == 1
|
|
assert len(deck1.groups.all()) == len(deck2.groups.all())
|
|
deck1.groups.id("new")
|
|
assert len(deck1.groups.all()) != len(deck2.groups.all())
|
|
time.sleep(0.1)
|
|
deck2.groups.id("new2")
|
|
deck1.save()
|
|
deck2.save()
|
|
assert client.sync() == "success"
|
|
assert deck1.tags.all() == deck2.tags.all()
|
|
assert len(deck1.groups.all()) == len(deck2.groups.all())
|
|
assert len(deck1.groups.all()) == 3
|
|
assert deck1.groups.conf(1)['maxTaken'] == 60
|
|
deck2.groups.conf(1)['maxTaken'] = 30
|
|
deck2.groups.save(deck2.groups.conf(1))
|
|
deck2.save()
|
|
assert client.sync() == "success"
|
|
assert deck1.groups.conf(1)['maxTaken'] == 30
|
|
|
|
@nose.with_setup(setup_modified)
|
|
def test_conf():
|
|
test_sync()
|
|
assert deck2.conf['topGroup'] == 1
|
|
deck1.conf['topGroup'] = 2
|
|
deck1.save()
|
|
assert client.sync() == "success"
|
|
assert deck2.conf['topGroup'] == 2
|
|
|
|
@nose.with_setup(setup_modified)
|
|
def test_threeway():
|
|
test_sync()
|
|
deck1.close(save=False)
|
|
d3path = deck1.path.replace(".anki", "2.anki")
|
|
shutil.copy2(deck1.path, d3path)
|
|
deck1.reopen()
|
|
deck3 = Deck(d3path)
|
|
client2 = Syncer(deck3, server)
|
|
assert client2.sync() == "noChanges"
|
|
# client 1 adds a card at time 1
|
|
time.sleep(1)
|
|
f = deck1.newFact()
|
|
f['Front'] = u"1";
|
|
deck1.addFact(f)
|
|
deck1.save()
|
|
# at time 2, client 2 syncs to server
|
|
time.sleep(1)
|
|
deck3.save()
|
|
assert client2.sync() == "success"
|
|
# at time 3, client 1 syncs, adding the older fact
|
|
time.sleep(1)
|
|
assert client.sync() == "success"
|
|
assert deck1.factCount() == deck2.factCount()
|
|
# syncing client2 should pick it up
|
|
assert client2.sync() == "success"
|
|
assert deck1.factCount() == deck2.factCount() == deck3.factCount()
|
|
|
|
def _test_speed():
|
|
t = time.time()
|
|
setup_basic([os.path.expanduser("~/rapid.anki"),
|
|
os.path.expanduser("~/rapid2.anki")])
|
|
print "load %d" % ((time.time() - t)*1000); t = time.time()
|
|
deck2.save()
|
|
# 3000 revlog entries: ~128ms
|
|
# 3000 cards: ~200ms
|
|
# 3000 facts: ~500ms
|
|
assert client.sync() != "fullSync"
|
|
print "sync %d" % ((time.time() - t)*1000); t = time.time()
|
|
|
|
|
|
|