Anki/tests/test_sync.py
Damien Elmes 667b89ecc5 support partial syncs of arbitrary size
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.
2011-09-24 12:42:02 +09:00

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()