]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/test/test_magic_folder.py
c58736594acfcccdfd7b9742f4bc2d6f9a5fa9c4
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / test / test_magic_folder.py
1
2 import os, sys
3
4 from twisted.trial import unittest
5 from twisted.internet import defer, task
6
7 from allmydata.interfaces import IDirectoryNode
8 from allmydata.util.assertutil import precondition
9
10 from allmydata.util import fake_inotify, fileutil
11 from allmydata.util.encodingutil import get_filesystem_encoding, to_filepath
12 from allmydata.util.consumer import download_to_data
13 from allmydata.test.no_network import GridTestMixin
14 from allmydata.test.common_util import ReallyEqualMixin, NonASCIIPathMixin
15 from allmydata.test.common import ShouldFailMixin
16 from .test_cli_magic_folder import MagicFolderCLITestMixin
17
18 from allmydata.frontends import magic_folder
19 from allmydata.frontends.magic_folder import MagicFolder, Downloader, WriteFileMixin
20 from allmydata import magicfolderdb, magicpath
21 from allmydata.util.fileutil import abspath_expanduser_unicode
22 from allmydata.immutable.upload import Data
23
24
25 class MagicFolderTestMixin(MagicFolderCLITestMixin, ShouldFailMixin, ReallyEqualMixin, NonASCIIPathMixin):
26     """
27     These tests will be run both with a mock notifier, and (on platforms that support it)
28     with the real INotify.
29     """
30
31     def setUp(self):
32         GridTestMixin.setUp(self)
33         temp = self.mktemp()
34         self.basedir = abspath_expanduser_unicode(temp.decode(get_filesystem_encoding()))
35         self.magicfolder = None
36         self.patch(Downloader, 'REMOTE_SCAN_INTERVAL', 0)
37
38     def _get_count(self, name, client=None):
39         counters = (client or self.get_client()).stats_provider.get_stats()["counters"]
40         return counters.get('magic_folder.%s' % (name,), 0)
41
42     def _createdb(self):
43         dbfile = abspath_expanduser_unicode(u"magicfolderdb.sqlite", base=self.basedir)
44         mdb = magicfolderdb.get_magicfolderdb(dbfile, create_version=(magicfolderdb.SCHEMA_v1, 1))
45         self.failUnless(mdb, "unable to create magicfolderdb from %r" % (dbfile,))
46         self.failUnlessEqual(mdb.VERSION, 1)
47         return mdb
48
49     def _restart_client(self, ign):
50         #print "_restart_client"
51         d = self.restart_client()
52         d.addCallback(self._wait_until_started)
53         return d
54
55     def _wait_until_started(self, ign):
56         #print "_wait_until_started"
57         self.magicfolder = self.get_client().getServiceNamed('magic-folder')
58         return self.magicfolder.ready()
59
60     def test_db_basic(self):
61         fileutil.make_dirs(self.basedir)
62         self._createdb()
63
64     def test_db_persistence(self):
65         """Test that a file upload creates an entry in the database."""
66
67         fileutil.make_dirs(self.basedir)
68         db = self._createdb()
69
70         relpath1 = u"myFile1"
71         pathinfo = fileutil.PathInfo(isdir=False, isfile=True, islink=False,
72                                      exists=True, size=1, mtime=123, ctime=456)
73         db.did_upload_version(relpath1, 0, 'URI:LIT:1', 'URI:LIT:0', 0, pathinfo)
74
75         c = db.cursor
76         c.execute("SELECT size, mtime, ctime"
77                   " FROM local_files"
78                   " WHERE path=?",
79                   (relpath1,))
80         row = c.fetchone()
81         self.failUnlessEqual(row, (pathinfo.size, pathinfo.mtime, pathinfo.ctime))
82
83         # Second test uses magic_folder.is_new_file instead of SQL query directly
84         # to confirm the previous upload entry in the db.
85         relpath2 = u"myFile2"
86         path2 = os.path.join(self.basedir, relpath2)
87         fileutil.write(path2, "meow\n")
88         pathinfo = fileutil.get_pathinfo(path2)
89         db.did_upload_version(relpath2, 0, 'URI:LIT:2', 'URI:LIT:1', 0, pathinfo)
90         db_entry = db.get_db_entry(relpath2)
91         self.failUnlessFalse(magic_folder.is_new_file(pathinfo, db_entry))
92
93         different_pathinfo = fileutil.PathInfo(isdir=False, isfile=True, islink=False,
94                                                exists=True, size=0, mtime=pathinfo.mtime, ctime=pathinfo.ctime)
95         self.failUnlessTrue(magic_folder.is_new_file(different_pathinfo, db_entry))
96
97     def test_magicfolder_start_service(self):
98         self.set_up_grid()
99
100         self.local_dir = abspath_expanduser_unicode(self.unicode_or_fallback(u"l\u00F8cal_dir", u"local_dir"),
101                                                     base=self.basedir)
102         self.mkdir_nonascii(self.local_dir)
103
104         d = defer.succeed(None)
105         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.dirs_monitored'), 0))
106
107         d.addCallback(lambda ign: self.create_invite_join_magic_folder(u"Alice", self.local_dir))
108         d.addCallback(self._restart_client)
109
110         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.dirs_monitored'), 1))
111         d.addBoth(self.cleanup)
112         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.dirs_monitored'), 0))
113         return d
114
115     def test_scan_once_on_startup(self):
116         self.set_up_grid()
117         self.local_dir = abspath_expanduser_unicode(u"test_scan_once_on_startup", base=self.basedir)
118         self.mkdir_nonascii(self.local_dir)
119         self.collective_dircap = ""
120
121         alice_clock = task.Clock()
122         bob_clock = task.Clock()
123         d = self.setup_alice_and_bob(alice_clock, bob_clock)
124
125         def upload_stuff(ignore):
126             uploadable = Data("", self.alice_magicfolder._client.convergence)
127             return self.alice_magicfolder._client.upload(uploadable)
128         d.addCallback(upload_stuff)
129         def check_is_upload(ignore):
130             alice_clock.advance(99)
131             d.addCallback(lambda ign: self._check_uploader_count('files_uploaded', 0, magic=self.alice_magicfolder))
132             d.addCallback(lambda ign: self._check_uploader_count('objects_queued', 0, magic=self.alice_magicfolder))
133             d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 0, magic=self.alice_magicfolder))
134             d.addCallback(lambda ign: self._check_uploader_count('objects_succeeded', 0, magic=self.alice_magicfolder))
135             d.addCallback(lambda ign: self._check_downloader_count('objects_failed', 0, magic=self.alice_magicfolder))
136             d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 0, magic=self.alice_magicfolder))
137
138         d.addCallback(check_is_upload)
139         def _cleanup(ign, magicfolder, clock):
140             if magicfolder is not None:
141                 d2 = magicfolder.finish()
142                 clock.advance(0)
143                 return d2
144         def cleanup_Alice_and_Bob(result):
145             print "cleanup alice bob test\n"
146             d = defer.succeed(None)
147             d.addCallback(_cleanup, self.alice_magicfolder, alice_clock)
148             d.addCallback(_cleanup, self.bob_magicfolder, bob_clock)
149             d.addCallback(lambda ign: result)
150             return d
151
152         d.addBoth(cleanup_Alice_and_Bob)
153         return d
154
155     def test_move_tree(self):
156         self.set_up_grid()
157
158         self.local_dir = abspath_expanduser_unicode(self.unicode_or_fallback(u"l\u00F8cal_dir", u"local_dir"),
159                                                     base=self.basedir)
160         self.mkdir_nonascii(self.local_dir)
161
162         empty_tree_name = self.unicode_or_fallback(u"empty_tr\u00EAe", u"empty_tree")
163         empty_tree_dir = abspath_expanduser_unicode(empty_tree_name, base=self.basedir)
164         new_empty_tree_dir = abspath_expanduser_unicode(empty_tree_name, base=self.local_dir)
165
166         small_tree_name = self.unicode_or_fallback(u"small_tr\u00EAe", u"empty_tree")
167         small_tree_dir = abspath_expanduser_unicode(small_tree_name, base=self.basedir)
168         new_small_tree_dir = abspath_expanduser_unicode(small_tree_name, base=self.local_dir)
169
170         d = self.create_invite_join_magic_folder(u"Alice", self.local_dir)
171         d.addCallback(self._restart_client)
172
173         def _check_move_empty_tree(res):
174             print "_check_move_empty_tree"
175             uploaded_d = self.magicfolder.uploader.set_hook('processed')
176             self.mkdir_nonascii(empty_tree_dir)
177             os.rename(empty_tree_dir, new_empty_tree_dir)
178             self.notify(to_filepath(new_empty_tree_dir), self.inotify.IN_MOVED_TO)
179
180             return uploaded_d
181         d.addCallback(_check_move_empty_tree)
182         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_failed'), 0))
183         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_succeeded'), 1))
184         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.files_uploaded'), 0))
185         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_queued'), 0))
186         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.directories_created'), 1))
187
188         def _check_move_small_tree(res):
189             print "_check_move_small_tree"
190             uploaded_d = self.magicfolder.uploader.set_hook('processed', ignore_count=1)
191             self.mkdir_nonascii(small_tree_dir)
192             what_path = abspath_expanduser_unicode(u"what", base=small_tree_dir)
193             fileutil.write(what_path, "say when")
194             os.rename(small_tree_dir, new_small_tree_dir)
195             self.notify(to_filepath(new_small_tree_dir), self.inotify.IN_MOVED_TO)
196
197             return uploaded_d
198         d.addCallback(_check_move_small_tree)
199         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_failed'), 0))
200         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_succeeded'), 3))
201         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.files_uploaded'), 1))
202         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_queued'), 0))
203         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.directories_created'), 2))
204
205         def _check_moved_tree_is_watched(res):
206             print "_check_moved_tree_is_watched"
207             uploaded_d = self.magicfolder.uploader.set_hook('processed')
208             another_path = abspath_expanduser_unicode(u"another", base=new_small_tree_dir)
209             fileutil.write(another_path, "file")
210             self.notify(to_filepath(another_path), self.inotify.IN_CLOSE_WRITE)
211
212             return uploaded_d
213         d.addCallback(_check_moved_tree_is_watched)
214         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_failed'), 0))
215         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_succeeded'), 4))
216         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.files_uploaded'), 2))
217         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_queued'), 0))
218         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.directories_created'), 2))
219
220         # Files that are moved out of the upload directory should no longer be watched.
221         #def _move_dir_away(ign):
222         #    os.rename(new_empty_tree_dir, empty_tree_dir)
223         #    # Wuh? Why don't we get this event for the real test?
224         #    #self.notify(to_filepath(new_empty_tree_dir), self.inotify.IN_MOVED_FROM)
225         #d.addCallback(_move_dir_away)
226         #def create_file(val):
227         #    test_file = abspath_expanduser_unicode(u"what", base=empty_tree_dir)
228         #    fileutil.write(test_file, "meow")
229         #    #self.notify(...)
230         #    return
231         #d.addCallback(create_file)
232         #d.addCallback(lambda ign: time.sleep(1))  # XXX ICK
233         #d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_failed'), 0))
234         #d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_succeeded'), 4))
235         #d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.files_uploaded'), 2))
236         #d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_queued'), 0))
237         #d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.directories_created'), 2))
238
239         d.addBoth(self.cleanup)
240         return d
241
242     def test_persistence(self):
243         """
244         Perform an upload of a given file and then stop the client.
245         Start a new client and magic-folder service... and verify that the file is NOT uploaded
246         a second time. This test is meant to test the database persistence along with
247         the startup and shutdown code paths of the magic-folder service.
248         """
249         self.set_up_grid()
250         self.local_dir = abspath_expanduser_unicode(u"test_persistence", base=self.basedir)
251         self.mkdir_nonascii(self.local_dir)
252         self.collective_dircap = ""
253
254         d = defer.succeed(None)
255         d.addCallback(lambda ign: self.create_invite_join_magic_folder(u"Alice", self.local_dir))
256         d.addCallback(self._restart_client)
257
258         def create_test_file(filename):
259             d2 = self.magicfolder.uploader.set_hook('processed')
260             test_file = abspath_expanduser_unicode(filename, base=self.local_dir)
261             fileutil.write(test_file, "meow %s" % filename)
262             self.notify(to_filepath(test_file), self.inotify.IN_CLOSE_WRITE)
263             return d2
264         d.addCallback(lambda ign: create_test_file(u"what1"))
265         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_failed'), 0))
266         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_succeeded'), 1))
267         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_queued'), 0))
268         d.addCallback(self.cleanup)
269
270         d.addCallback(self._restart_client)
271         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_failed'), 0))
272         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_succeeded'), 1))
273         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_queued'), 0))
274         d.addCallback(lambda ign: create_test_file(u"what2"))
275         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_failed'), 0))
276         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_succeeded'), 2))
277         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_queued'), 0))
278         d.addBoth(self.cleanup)
279         return d
280
281     @defer.inlineCallbacks
282     def test_delete(self):
283         self.set_up_grid()
284         self.local_dir = os.path.join(self.basedir, u"local_dir")
285         self.mkdir_nonascii(self.local_dir)
286
287         yield self.create_invite_join_magic_folder(u"Alice\u0101", self.local_dir)
288         yield self._restart_client(None)
289
290         try:
291             # create a file
292             up_proc = self.magicfolder.uploader.set_hook('processed')
293             # down_proc = self.magicfolder.downloader.set_hook('processed')
294             path = os.path.join(self.local_dir, u'foo')
295             fileutil.write(path, 'foo\n')
296             self.notify(to_filepath(path), self.inotify.IN_CLOSE_WRITE)
297             yield up_proc
298             self.assertTrue(os.path.exists(path))
299
300             # the real test part: delete the file
301             up_proc = self.magicfolder.uploader.set_hook('processed')
302             os.unlink(path)
303             self.notify(to_filepath(path), self.inotify.IN_DELETE)
304             yield up_proc
305             self.assertFalse(os.path.exists(path))
306
307             # ensure we still have a DB entry, and that the version is 1
308             node, metadata = yield self.magicfolder.downloader._get_collective_latest_file(u'foo')
309             self.assertTrue(node is not None, "Failed to find %r in DMD" % (path,))
310             self.failUnlessEqual(metadata['version'], 1)
311
312         finally:
313             yield self.cleanup(None)
314
315     @defer.inlineCallbacks
316     def test_delete_and_restore(self):
317         self.set_up_grid()
318         self.local_dir = os.path.join(self.basedir, u"local_dir")
319         self.mkdir_nonascii(self.local_dir)
320
321         yield self.create_invite_join_magic_folder(u"Alice\u0101", self.local_dir)
322         yield self._restart_client(None)
323
324         try:
325             # create a file
326             up_proc = self.magicfolder.uploader.set_hook('processed')
327             # down_proc = self.magicfolder.downloader.set_hook('processed')
328             path = os.path.join(self.local_dir, u'foo')
329             fileutil.write(path, 'foo\n')
330             self.notify(to_filepath(path), self.inotify.IN_CLOSE_WRITE)
331             yield up_proc
332             self.assertTrue(os.path.exists(path))
333
334             # delete the file
335             up_proc = self.magicfolder.uploader.set_hook('processed')
336             os.unlink(path)
337             self.notify(to_filepath(path), self.inotify.IN_DELETE)
338             yield up_proc
339             self.assertFalse(os.path.exists(path))
340
341             # ensure we still have a DB entry, and that the version is 1
342             node, metadata = yield self.magicfolder.downloader._get_collective_latest_file(u'foo')
343             self.assertTrue(node is not None, "Failed to find %r in DMD" % (path,))
344             self.failUnlessEqual(metadata['version'], 1)
345
346             # restore the file, with different contents
347             up_proc = self.magicfolder.uploader.set_hook('processed')
348             path = os.path.join(self.local_dir, u'foo')
349             fileutil.write(path, 'bar\n')
350             self.notify(to_filepath(path), self.inotify.IN_CLOSE_WRITE)
351             yield up_proc
352
353             # ensure we still have a DB entry, and that the version is 2
354             node, metadata = yield self.magicfolder.downloader._get_collective_latest_file(u'foo')
355             self.assertTrue(node is not None, "Failed to find %r in DMD" % (path,))
356             self.failUnlessEqual(metadata['version'], 2)
357
358         finally:
359             yield self.cleanup(None)
360
361     @defer.inlineCallbacks
362     def test_alice_delete_bob_restore(self):
363         alice_clock = task.Clock()
364         bob_clock = task.Clock()
365         yield self.setup_alice_and_bob(alice_clock, bob_clock)
366         alice_dir = self.alice_magicfolder.uploader._local_path_u
367         bob_dir = self.bob_magicfolder.uploader._local_path_u
368         alice_fname = os.path.join(alice_dir, 'blam')
369         bob_fname = os.path.join(bob_dir, 'blam')
370
371         try:
372             # alice creates a file, bob downloads it
373             alice_proc = self.alice_magicfolder.uploader.set_hook('processed')
374             bob_proc = self.bob_magicfolder.downloader.set_hook('processed')
375
376             fileutil.write(alice_fname, 'contents0\n')
377             self.notify(to_filepath(alice_fname), self.inotify.IN_CLOSE_WRITE, magic=self.alice_magicfolder)
378
379             alice_clock.advance(0)
380             yield alice_proc  # alice uploads
381
382             bob_clock.advance(0)
383             yield bob_proc    # bob downloads
384
385             # check the state
386             yield self._check_version_in_dmd(self.alice_magicfolder, u"blam", 1)
387             yield self._check_version_in_local_db(self.alice_magicfolder, u"blam", 0)
388             yield self._check_version_in_dmd(self.bob_magicfolder, u"blam", 1)
389             yield self._check_version_in_local_db(self.bob_magicfolder, u"blam", 0)
390             yield self.failUnlessReallyEqual(
391                 self._get_count('downloader.objects_failed', client=self.bob_magicfolder._client),
392                 0
393             )
394             yield self.failUnlessReallyEqual(
395                 self._get_count('downloader.objects_downloaded', client=self.bob_magicfolder._client),
396                 1
397             )
398
399             print("BOB DELETE")
400             # now bob deletes it (bob should upload, alice download)
401             bob_proc = self.bob_magicfolder.uploader.set_hook('processed')
402             alice_proc = self.alice_magicfolder.downloader.set_hook('processed')
403             os.unlink(bob_fname)
404             self.notify(to_filepath(bob_fname), self.inotify.IN_DELETE, magic=self.bob_magicfolder)
405
406             bob_clock.advance(0)
407             yield bob_proc
408             alice_clock.advance(0)
409             yield alice_proc
410
411             # check versions
412             node, metadata = yield self.alice_magicfolder.downloader._get_collective_latest_file(u'blam')
413             self.assertTrue(metadata['deleted'])
414             yield self._check_version_in_dmd(self.bob_magicfolder, u"blam", 1)
415             yield self._check_version_in_local_db(self.bob_magicfolder, u"blam", 1)
416             yield self._check_version_in_dmd(self.alice_magicfolder, u"blam", 1)
417             yield self._check_version_in_local_db(self.alice_magicfolder, u"blam", 1)
418
419             print("ALICE RESTORE")
420             # now alice restores it (alice should upload, bob download)
421             alice_proc = self.alice_magicfolder.uploader.set_hook('processed')
422             bob_proc = self.bob_magicfolder.downloader.set_hook('processed')
423             fileutil.write(alice_fname, 'new contents\n')
424             self.notify(to_filepath(alice_fname), self.inotify.IN_CLOSE_WRITE, magic=self.alice_magicfolder)
425
426             alice_clock.advance(0)
427             yield alice_proc
428             bob_clock.advance(0)
429             yield bob_proc
430
431             # check versions
432             node, metadata = yield self.alice_magicfolder.downloader._get_collective_latest_file(u'blam')
433             self.assertTrue('deleted' not in metadata or not metadata['deleted'])
434             yield self._check_version_in_dmd(self.bob_magicfolder, u"blam", 2)
435             yield self._check_version_in_local_db(self.bob_magicfolder, u"blam", 2)
436             yield self._check_version_in_dmd(self.alice_magicfolder, u"blam", 2)
437             yield self._check_version_in_local_db(self.alice_magicfolder, u"blam", 2)
438
439         finally:
440             # cleanup
441             d0 = self.alice_magicfolder.finish()
442             alice_clock.advance(0)
443             yield d0
444
445             d1 = self.bob_magicfolder.finish()
446             bob_clock.advance(0)
447             yield d1
448
449     @defer.inlineCallbacks
450     def test_alice_sees_bobs_delete_with_error(self):
451         # alice creates a file, bob deletes it -- and we also arrange
452         # for Alice's file to have "gone missing" as well.
453         alice_clock = task.Clock()
454         bob_clock = task.Clock()
455         yield self.setup_alice_and_bob(alice_clock, bob_clock)
456         alice_dir = self.alice_magicfolder.uploader._local_path_u
457         bob_dir = self.bob_magicfolder.uploader._local_path_u
458         alice_fname = os.path.join(alice_dir, 'blam')
459         bob_fname = os.path.join(bob_dir, 'blam')
460
461         try:
462             # alice creates a file, bob downloads it
463             alice_proc = self.alice_magicfolder.uploader.set_hook('processed')
464             bob_proc = self.bob_magicfolder.downloader.set_hook('processed')
465
466             fileutil.write(alice_fname, 'contents0\n')
467             self.notify(to_filepath(alice_fname), self.inotify.IN_CLOSE_WRITE, magic=self.alice_magicfolder)
468
469             alice_clock.advance(0)
470             yield alice_proc  # alice uploads
471
472             bob_clock.advance(0)
473             yield bob_proc    # bob downloads
474
475             # check the state
476             yield self._check_version_in_dmd(self.alice_magicfolder, u"blam", 1)
477             yield self._check_version_in_local_db(self.alice_magicfolder, u"blam", 0)
478             yield self._check_version_in_dmd(self.bob_magicfolder, u"blam", 1)
479             yield self._check_version_in_local_db(self.bob_magicfolder, u"blam", 0)
480             yield self.failUnlessReallyEqual(
481                 self._get_count('downloader.objects_failed', client=self.bob_magicfolder._client),
482                 0
483             )
484             yield self.failUnlessReallyEqual(
485                 self._get_count('downloader.objects_downloaded', client=self.bob_magicfolder._client),
486                 1
487             )
488
489             # now bob deletes it (bob should upload, alice download)
490             bob_proc = self.bob_magicfolder.uploader.set_hook('processed')
491             alice_proc = self.alice_magicfolder.downloader.set_hook('processed')
492             os.unlink(bob_fname)
493             self.notify(to_filepath(bob_fname), self.inotify.IN_DELETE, magic=self.bob_magicfolder)
494             # just after notifying bob, we also delete alice's,
495             # covering the 'except' flow in _rename_deleted_file()
496             os.unlink(alice_fname)
497
498             bob_clock.advance(0)
499             yield bob_proc
500             alice_clock.advance(0)
501             yield alice_proc
502
503             # check versions
504             node, metadata = yield self.alice_magicfolder.downloader._get_collective_latest_file(u'blam')
505             self.assertTrue(metadata['deleted'])
506             yield self._check_version_in_dmd(self.bob_magicfolder, u"blam", 1)
507             yield self._check_version_in_local_db(self.bob_magicfolder, u"blam", 1)
508             yield self._check_version_in_dmd(self.alice_magicfolder, u"blam", 1)
509             yield self._check_version_in_local_db(self.alice_magicfolder, u"blam", 1)
510
511         finally:
512             # cleanup
513             d0 = self.alice_magicfolder.finish()
514             alice_clock.advance(0)
515             yield d0
516
517             d1 = self.bob_magicfolder.finish()
518             bob_clock.advance(0)
519             yield d1
520
521     @defer.inlineCallbacks
522     def test_alice_create_bob_update(self):
523         alice_clock = task.Clock()
524         bob_clock = task.Clock()
525         yield self.setup_alice_and_bob(alice_clock, bob_clock)
526         alice_dir = self.alice_magicfolder.uploader._local_path_u
527         bob_dir = self.bob_magicfolder.uploader._local_path_u
528         alice_fname = os.path.join(alice_dir, 'blam')
529         bob_fname = os.path.join(bob_dir, 'blam')
530
531         try:
532             # alice creates a file, bob downloads it
533             alice_proc = self.alice_magicfolder.uploader.set_hook('processed')
534             bob_proc = self.bob_magicfolder.downloader.set_hook('processed')
535
536             fileutil.write(alice_fname, 'contents0\n')
537             self.notify(to_filepath(alice_fname), self.inotify.IN_CLOSE_WRITE, magic=self.alice_magicfolder)
538
539             alice_clock.advance(0)
540             yield alice_proc  # alice uploads
541
542             bob_clock.advance(0)
543             yield bob_proc    # bob downloads
544
545             # check the state
546             yield self._check_version_in_dmd(self.alice_magicfolder, u"blam", 1)
547             yield self._check_version_in_local_db(self.alice_magicfolder, u"blam", 0)
548             yield self._check_version_in_dmd(self.bob_magicfolder, u"blam", 1)
549             yield self._check_version_in_local_db(self.bob_magicfolder, u"blam", 0)
550             yield self.failUnlessReallyEqual(
551                 self._get_count('downloader.objects_failed', client=self.bob_magicfolder._client),
552                 0
553             )
554             yield self.failUnlessReallyEqual(
555                 self._get_count('downloader.objects_downloaded', client=self.bob_magicfolder._client),
556                 1
557             )
558
559             # now bob updates it (bob should upload, alice download)
560             bob_proc = self.bob_magicfolder.uploader.set_hook('processed')
561             alice_proc = self.alice_magicfolder.downloader.set_hook('processed')
562             fileutil.write(bob_fname, 'bob wuz here\n')
563             self.notify(to_filepath(bob_fname), self.inotify.IN_CLOSE_WRITE, magic=self.bob_magicfolder)
564
565             bob_clock.advance(0)
566             yield bob_proc
567             alice_clock.advance(0)
568             yield alice_proc
569
570             # check the state
571             yield self._check_version_in_dmd(self.bob_magicfolder, u"blam", 1)
572             yield self._check_version_in_local_db(self.bob_magicfolder, u"blam", 1)
573             yield self._check_version_in_dmd(self.alice_magicfolder, u"blam", 1)
574             yield self._check_version_in_local_db(self.alice_magicfolder, u"blam", 1)
575
576         finally:
577             # cleanup
578             d0 = self.alice_magicfolder.finish()
579             alice_clock.advance(0)
580             try:
581                 yield d0
582             except Exception as e:
583                 print("WARNING:", e)
584
585             d1 = self.bob_magicfolder.finish()
586             bob_clock.advance(0)
587             try:
588                 yield d1
589             except Exception as e:
590                 print("WARNING:", e)
591
592     @defer.inlineCallbacks
593     def test_alice_delete_and_restore(self):
594         alice_clock = task.Clock()
595         bob_clock = task.Clock()
596         yield self.setup_alice_and_bob(alice_clock, bob_clock)
597         alice_dir = self.alice_magicfolder.uploader._local_path_u
598         bob_dir = self.bob_magicfolder.uploader._local_path_u
599         alice_fname = os.path.join(alice_dir, 'blam')
600         bob_fname = os.path.join(bob_dir, 'blam')
601
602         try:
603             # alice creates a file, bob downloads it
604             alice_proc = self.alice_magicfolder.uploader.set_hook('processed')
605             bob_proc = self.bob_magicfolder.downloader.set_hook('processed')
606
607             fileutil.write(alice_fname, 'contents0\n')
608             self.notify(to_filepath(alice_fname), self.inotify.IN_CLOSE_WRITE, magic=self.alice_magicfolder)
609
610             alice_clock.advance(0)
611             yield alice_proc  # alice uploads
612
613             bob_clock.advance(0)
614             yield bob_proc    # bob downloads
615
616             # check the state
617             yield self._check_version_in_dmd(self.alice_magicfolder, u"blam", 1)
618             yield self._check_version_in_local_db(self.alice_magicfolder, u"blam", 0)
619             yield self._check_version_in_dmd(self.bob_magicfolder, u"blam", 1)
620             yield self._check_version_in_local_db(self.bob_magicfolder, u"blam", 0)
621             yield self.failUnlessReallyEqual(
622                 self._get_count('downloader.objects_failed', client=self.bob_magicfolder._client),
623                 0
624             )
625             yield self.failUnlessReallyEqual(
626                 self._get_count('downloader.objects_downloaded', client=self.bob_magicfolder._client),
627                 1
628             )
629             self.failUnless(os.path.exists(bob_fname))
630
631             # now alice deletes it (alice should upload, bob download)
632             alice_proc = self.alice_magicfolder.uploader.set_hook('processed')
633             bob_proc = self.bob_magicfolder.downloader.set_hook('processed')
634             os.unlink(alice_fname)
635             self.notify(to_filepath(alice_fname), self.inotify.IN_DELETE, magic=self.alice_magicfolder)
636
637             alice_clock.advance(0)
638             yield alice_proc
639             bob_clock.advance(0)
640             yield bob_proc
641
642             # check the state
643             yield self._check_version_in_dmd(self.bob_magicfolder, u"blam", 1)
644             yield self._check_version_in_local_db(self.bob_magicfolder, u"blam", 1)
645             yield self._check_version_in_dmd(self.alice_magicfolder, u"blam", 1)
646             yield self._check_version_in_local_db(self.alice_magicfolder, u"blam", 1)
647             self.failIf(os.path.exists(bob_fname))
648
649             # now alice restores the file (with new contents)
650             alice_proc = self.alice_magicfolder.uploader.set_hook('processed')
651             bob_proc = self.bob_magicfolder.downloader.set_hook('processed')
652             fileutil.write(alice_fname, 'alice wuz here\n')
653             self.notify(to_filepath(alice_fname), self.inotify.IN_CLOSE_WRITE, magic=self.alice_magicfolder)
654
655             alice_clock.advance(0)
656             yield alice_proc
657             bob_clock.advance(0)
658             yield bob_proc
659
660             # check the state
661             yield self._check_version_in_dmd(self.bob_magicfolder, u"blam", 2)
662             yield self._check_version_in_local_db(self.bob_magicfolder, u"blam", 2)
663             yield self._check_version_in_dmd(self.alice_magicfolder, u"blam", 2)
664             yield self._check_version_in_local_db(self.alice_magicfolder, u"blam", 2)
665             self.failUnless(os.path.exists(bob_fname))
666
667         finally:
668             # cleanup
669             d0 = self.alice_magicfolder.finish()
670             alice_clock.advance(0)
671             yield d0
672
673             d1 = self.bob_magicfolder.finish()
674             bob_clock.advance(0)
675             yield d1
676
677     def test_magic_folder(self):
678         self.set_up_grid()
679         self.local_dir = os.path.join(self.basedir, self.unicode_or_fallback(u"loc\u0101l_dir", u"local_dir"))
680         self.mkdir_nonascii(self.local_dir)
681
682         d = self.create_invite_join_magic_folder(u"Alice\u0101", self.local_dir)
683         d.addCallback(self._restart_client)
684
685         # Write something short enough for a LIT file.
686         d.addCallback(lambda ign: self._check_file(u"short", "test"))
687
688         # Write to the same file again with different data.
689         d.addCallback(lambda ign: self._check_file(u"short", "different"))
690
691         # Test that temporary files are not uploaded.
692         d.addCallback(lambda ign: self._check_file(u"tempfile", "test", temporary=True))
693
694         # Test creation of a subdirectory.
695         d.addCallback(lambda ign: self._check_mkdir(u"directory"))
696
697         # Write something longer, and also try to test a Unicode name if the fs can represent it.
698         name_u = self.unicode_or_fallback(u"l\u00F8ng", u"long")
699         d.addCallback(lambda ign: self._check_file(name_u, "test"*100))
700
701         # TODO: test that causes an upload failure.
702         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_failed'), 0))
703
704         d.addBoth(self.cleanup)
705         return d
706
707     def _check_mkdir(self, name_u):
708         return self._check_file(name_u + u"/", "", directory=True)
709
710     def _check_file(self, name_u, data, temporary=False, directory=False):
711         precondition(not (temporary and directory), temporary=temporary, directory=directory)
712
713         print "%r._check_file(%r, %r, temporary=%r, directory=%r)" % (self, name_u, data, temporary, directory)
714         previously_uploaded = self._get_count('uploader.objects_succeeded')
715         previously_disappeared = self._get_count('uploader.objects_disappeared')
716
717         d = self.magicfolder.uploader.set_hook('processed')
718
719         path_u = abspath_expanduser_unicode(name_u, base=self.local_dir)
720         path = to_filepath(path_u)
721
722         if directory:
723             os.mkdir(path_u)
724             event_mask = self.inotify.IN_CREATE | self.inotify.IN_ISDIR
725         else:
726             # We don't use FilePath.setContent() here because it creates a temporary file that
727             # is renamed into place, which causes events that the test is not expecting.
728             f = open(path_u, "wb")
729             try:
730                 if temporary and sys.platform != "win32":
731                     os.unlink(path_u)
732                 f.write(data)
733             finally:
734                 f.close()
735             if temporary and sys.platform == "win32":
736                 os.unlink(path_u)
737                 self.notify(path, self.inotify.IN_DELETE, flush=False)
738             event_mask = self.inotify.IN_CLOSE_WRITE
739
740         self.notify(path, event_mask)
741         encoded_name_u = magicpath.path2magic(name_u)
742
743         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_failed'), 0))
744         if temporary:
745             d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_disappeared'),
746                                                                  previously_disappeared + 1))
747         else:
748             def _here(res, n):
749                 print "here %r %r" % (n, res)
750                 return res
751             d.addBoth(_here, 1)
752             d.addCallback(lambda ign: self.upload_dirnode.list())
753             d.addBoth(_here, 1.5)
754             d.addCallback(lambda ign: self.upload_dirnode.get(encoded_name_u))
755             d.addBoth(_here, 2)
756             d.addCallback(download_to_data)
757             d.addBoth(_here, 3)
758             d.addCallback(lambda actual_data: self.failUnlessReallyEqual(actual_data, data))
759             d.addBoth(_here, 4)
760             d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_succeeded'),
761                                                                  previously_uploaded + 1))
762
763         d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('uploader.objects_queued'), 0))
764         return d
765
766     def _check_version_in_dmd(self, magicfolder, relpath_u, expected_version):
767         encoded_name_u = magicpath.path2magic(relpath_u)
768         d = magicfolder.downloader._get_collective_latest_file(encoded_name_u)
769         def check_latest(result):
770             if result[0] is not None:
771                 node, metadata = result
772                 d.addCallback(lambda ign: self.failUnlessEqual(metadata['version'], expected_version))
773         d.addCallback(check_latest)
774         return d
775
776     def _check_version_in_local_db(self, magicfolder, relpath_u, expected_version):
777         db_entry = magicfolder._db.get_db_entry(relpath_u)
778         if db_entry is not None:
779             #print "_check_version_in_local_db: %r has version %s" % (relpath_u, version)
780             self.failUnlessEqual(db_entry.version, expected_version)
781
782     def _check_file_gone(self, magicfolder, relpath_u):
783         path = os.path.join(magicfolder.uploader._local_path_u, relpath_u)
784         self.assertTrue(not os.path.exists(path))
785
786     def _check_uploader_count(self, name, expected, magic=None):
787         self.failUnlessReallyEqual(self._get_count('uploader.'+name, client=(magic or self.alice_magicfolder)._client),
788                                    expected)
789
790     def _check_downloader_count(self, name, expected, magic=None):
791         self.failUnlessReallyEqual(self._get_count('downloader.'+name, client=(magic or self.bob_magicfolder)._client),
792                                    expected)
793
794     def test_alice_bob(self):
795         alice_clock = task.Clock()
796         bob_clock = task.Clock()
797         d = self.setup_alice_and_bob(alice_clock, bob_clock)
798         def _cleanup(ign, magicfolder, clock):
799             if magicfolder is not None:
800                 d2 = magicfolder.finish()
801                 clock.advance(0)
802                 return d2
803
804         def cleanup_Alice_and_Bob(result):
805             print "cleanup alice bob test\n"
806             d = defer.succeed(None)
807             d.addCallback(_cleanup, self.alice_magicfolder, alice_clock)
808             d.addCallback(_cleanup, self.bob_magicfolder, bob_clock)
809             d.addCallback(lambda ign: result)
810             return d
811         # callback version is added at the end...
812         d.addErrback(cleanup_Alice_and_Bob)
813
814         def _wait_for_Alice(ign, downloaded_d):
815             print "Now waiting for Alice to download\n"
816             alice_clock.advance(0)
817             return downloaded_d
818
819         def _wait_for_Bob(ign, downloaded_d):
820             print "Now waiting for Bob to download\n"
821             bob_clock.advance(0)
822             return downloaded_d
823
824         def _wait_for(ign, something_to_do, alice=True):
825             if alice:
826                 downloaded_d = self.bob_magicfolder.downloader.set_hook('processed')
827                 uploaded_d = self.alice_magicfolder.uploader.set_hook('processed')
828             else:
829                 downloaded_d = self.alice_magicfolder.downloader.set_hook('processed')
830                 uploaded_d = self.bob_magicfolder.uploader.set_hook('processed')
831             something_to_do()
832             if alice:
833                 print "Waiting for Alice to upload\n"
834                 alice_clock.advance(0)
835                 uploaded_d.addCallback(_wait_for_Bob, downloaded_d)
836             else:
837                 print "Waiting for Bob to upload\n"
838                 bob_clock.advance(0)
839                 uploaded_d.addCallback(_wait_for_Alice, downloaded_d)
840             return uploaded_d
841
842         def Alice_to_write_a_file():
843             print "Alice writes a file\n"
844             self.file_path = abspath_expanduser_unicode(u"file1", base=self.alice_magicfolder.uploader._local_path_u)
845             fileutil.write(self.file_path, "meow, meow meow. meow? meow meow! meow.")
846             self.notify(to_filepath(self.file_path), self.inotify.IN_CLOSE_WRITE, magic=self.alice_magicfolder)
847         d.addCallback(_wait_for, Alice_to_write_a_file)
848
849         d.addCallback(lambda ign: self._check_version_in_dmd(self.alice_magicfolder, u"file1", 0))
850         d.addCallback(lambda ign: self._check_version_in_local_db(self.alice_magicfolder, u"file1", 0))
851         d.addCallback(lambda ign: self._check_uploader_count('objects_failed', 0))
852         d.addCallback(lambda ign: self._check_uploader_count('objects_succeeded', 1))
853         d.addCallback(lambda ign: self._check_uploader_count('files_uploaded', 1))
854         d.addCallback(lambda ign: self._check_uploader_count('objects_queued', 0))
855         d.addCallback(lambda ign: self._check_uploader_count('directories_created', 0))
856         d.addCallback(lambda ign: self._check_uploader_count('objects_conflicted', 0))
857         d.addCallback(lambda ign: self._check_uploader_count('objects_conflicted', 0, magic=self.bob_magicfolder))
858
859         d.addCallback(lambda ign: self._check_version_in_local_db(self.bob_magicfolder, u"file1", 0))
860         d.addCallback(lambda ign: self._check_downloader_count('objects_failed', 0))
861         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 1))
862         d.addCallback(lambda ign: self._check_uploader_count('objects_succeeded', 0, magic=self.bob_magicfolder))
863
864         def Alice_to_delete_file():
865             print "Alice deletes the file!\n"
866             os.unlink(self.file_path)
867             self.notify(to_filepath(self.file_path), self.inotify.IN_DELETE, magic=self.alice_magicfolder)
868         d.addCallback(_wait_for, Alice_to_delete_file)
869
870         def notify_bob_moved(ign):
871             d0 = self.bob_magicfolder.uploader.set_hook('processed')
872             p = abspath_expanduser_unicode(u"file1", base=self.bob_magicfolder.uploader._local_path_u)
873             self.notify(to_filepath(p), self.inotify.IN_MOVED_FROM, magic=self.bob_magicfolder, flush=False)
874             self.notify(to_filepath(p + u'.backup'), self.inotify.IN_MOVED_TO, magic=self.bob_magicfolder)
875             bob_clock.advance(0)
876             return d0
877         d.addCallback(notify_bob_moved)
878
879         d.addCallback(lambda ign: self._check_version_in_dmd(self.alice_magicfolder, u"file1", 1))
880         d.addCallback(lambda ign: self._check_version_in_local_db(self.alice_magicfolder, u"file1", 1))
881         d.addCallback(lambda ign: self._check_uploader_count('objects_failed', 0))
882         d.addCallback(lambda ign: self._check_uploader_count('objects_succeeded', 2))
883         d.addCallback(lambda ign: self._check_uploader_count('objects_not_uploaded', 1, magic=self.bob_magicfolder))
884         d.addCallback(lambda ign: self._check_uploader_count('objects_succeeded', 1, magic=self.bob_magicfolder))
885
886         d.addCallback(lambda ign: self._check_version_in_local_db(self.bob_magicfolder, u"file1", 1))
887         d.addCallback(lambda ign: self._check_version_in_dmd(self.bob_magicfolder, u"file1", 1))
888         d.addCallback(lambda ign: self._check_file_gone(self.bob_magicfolder, u"file1"))
889         d.addCallback(lambda ign: self._check_downloader_count('objects_failed', 0))
890         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 2))
891
892         def Alice_to_rewrite_file():
893             print "Alice rewrites file\n"
894             self.file_path = abspath_expanduser_unicode(u"file1", base=self.alice_magicfolder.uploader._local_path_u)
895             fileutil.write(self.file_path, "Alice suddenly sees the white rabbit running into the forest.")
896             self.notify(to_filepath(self.file_path), self.inotify.IN_CLOSE_WRITE, magic=self.alice_magicfolder)
897         d.addCallback(_wait_for, Alice_to_rewrite_file)
898
899         d.addCallback(lambda ign: self._check_version_in_dmd(self.alice_magicfolder, u"file1", 2))
900         d.addCallback(lambda ign: self._check_version_in_local_db(self.alice_magicfolder, u"file1", 2))
901         d.addCallback(lambda ign: self._check_uploader_count('objects_failed', 0))
902         d.addCallback(lambda ign: self._check_uploader_count('objects_succeeded', 3))
903         d.addCallback(lambda ign: self._check_uploader_count('files_uploaded', 3))
904         d.addCallback(lambda ign: self._check_uploader_count('objects_queued', 0))
905         d.addCallback(lambda ign: self._check_uploader_count('directories_created', 0))
906         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 0, magic=self.alice_magicfolder))
907         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 0))
908
909         d.addCallback(lambda ign: self._check_version_in_dmd(self.bob_magicfolder, u"file1", 2))
910         d.addCallback(lambda ign: self._check_version_in_local_db(self.bob_magicfolder, u"file1", 2))
911         d.addCallback(lambda ign: self._check_downloader_count('objects_failed', 0))
912         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 3))
913
914         path_u = u"/tmp/magic_folder_test"
915         encoded_path_u = magicpath.path2magic(u"/tmp/magic_folder_test")
916
917         def Alice_tries_to_p0wn_Bob(ign):
918             print "Alice tries to p0wn Bob\n"
919             processed_d = self.bob_magicfolder.downloader.set_hook('processed')
920
921             # upload a file that would provoke the security bug from #2506
922             uploadable = Data("", self.alice_magicfolder._client.convergence)
923             alice_dmd = self.alice_magicfolder.uploader._upload_dirnode
924
925             d2 = alice_dmd.add_file(encoded_path_u, uploadable, metadata={"version": 0}, overwrite=True)
926             d2.addCallback(lambda ign: self.failUnless(alice_dmd.has_child(encoded_path_u)))
927             d2.addCallback(_wait_for_Bob, processed_d)
928             return d2
929         d.addCallback(Alice_tries_to_p0wn_Bob)
930
931         d.addCallback(lambda ign: self.failIf(os.path.exists(path_u)))
932         d.addCallback(lambda ign: self._check_version_in_local_db(self.bob_magicfolder, encoded_path_u, None))
933         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 3))
934         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 0, magic=self.alice_magicfolder))
935         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 0))
936
937         def Bob_to_rewrite_file():
938             print "Bob rewrites file\n"
939             self.file_path = abspath_expanduser_unicode(u"file1", base=self.bob_magicfolder.uploader._local_path_u)
940             print "---- bob's file is %r" % (self.file_path,)
941             fileutil.write(self.file_path, "No white rabbit to be found.")
942             self.magicfolder = self.bob_magicfolder
943             self.notify(to_filepath(self.file_path), self.inotify.IN_CLOSE_WRITE)
944         d.addCallback(lambda ign: _wait_for(None, Bob_to_rewrite_file, alice=False))
945
946         d.addCallback(lambda ign: self._check_version_in_dmd(self.bob_magicfolder, u"file1", 3))
947         d.addCallback(lambda ign: self._check_version_in_local_db(self.bob_magicfolder, u"file1", 3))
948         d.addCallback(lambda ign: self._check_uploader_count('objects_failed', 0, magic=self.bob_magicfolder))
949         d.addCallback(lambda ign: self._check_uploader_count('objects_succeeded', 2, magic=self.bob_magicfolder))
950         d.addCallback(lambda ign: self._check_uploader_count('files_uploaded', 1, magic=self.bob_magicfolder))
951         d.addCallback(lambda ign: self._check_uploader_count('objects_queued', 0, magic=self.bob_magicfolder))
952         d.addCallback(lambda ign: self._check_uploader_count('directories_created', 0, magic=self.bob_magicfolder))
953         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 0))
954
955         d.addCallback(lambda ign: self._check_version_in_dmd(self.alice_magicfolder, u"file1", 3))
956         d.addCallback(lambda ign: self._check_version_in_local_db(self.alice_magicfolder, u"file1", 3))
957         d.addCallback(lambda ign: self._check_downloader_count('objects_failed', 0, magic=self.alice_magicfolder))
958         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 1, magic=self.alice_magicfolder))
959         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 0, magic=self.alice_magicfolder))
960
961         def Alice_conflicts_with_Bobs_last_downloaded_uri():
962             print "Alice conflicts with Bob\n"
963             downloaded_d = self.bob_magicfolder.downloader.set_hook('processed')
964             uploadable = Data("do not follow the white rabbit", self.alice_magicfolder._client.convergence)
965             alice_dmd = self.alice_magicfolder.uploader._upload_dirnode
966             d2 = alice_dmd.add_file(u"file1", uploadable,
967                                     metadata={"version": 5,
968                                               "last_downloaded_uri" : "URI:LIT:" },
969                                     overwrite=True)
970             print "Waiting for Alice to upload\n"
971             d2.addCallback(lambda ign: bob_clock.advance(6))
972             d2.addCallback(lambda ign: downloaded_d)
973             d2.addCallback(lambda ign: self.failUnless(alice_dmd.has_child(encoded_path_u)))
974             return d2
975
976         d.addCallback(lambda ign: Alice_conflicts_with_Bobs_last_downloaded_uri())
977         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 4))
978         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 1))
979         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 1, magic=self.alice_magicfolder))
980         d.addCallback(lambda ign: self._check_downloader_count('objects_failed', 0, magic=self.alice_magicfolder))
981         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 0, magic=self.alice_magicfolder))
982
983         # prepare to perform another conflict test
984         def Alice_to_write_file2():
985             print "Alice writes a file\n"
986             self.file_path = abspath_expanduser_unicode(u"file2", base=self.alice_magicfolder.uploader._local_path_u)
987             fileutil.write(self.file_path, "something")
988             self.notify(to_filepath(self.file_path), self.inotify.IN_CLOSE_WRITE, magic=self.alice_magicfolder)
989         d.addCallback(_wait_for, Alice_to_write_file2)
990         d.addCallback(lambda ign: self._check_version_in_dmd(self.alice_magicfolder, u"file2", 0))
991         d.addCallback(lambda ign: self._check_downloader_count('objects_failed', 0, magic=self.alice_magicfolder))
992         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 0, magic=self.alice_magicfolder))
993
994         def Bob_to_rewrite_file2():
995             print "Bob rewrites file\n"
996             self.file_path = abspath_expanduser_unicode(u"file2", base=self.bob_magicfolder.uploader._local_path_u)
997             print "---- bob's file is %r" % (self.file_path,)
998             fileutil.write(self.file_path, "roger roger. what vector?")
999             self.magicfolder = self.bob_magicfolder
1000             self.notify(to_filepath(self.file_path), self.inotify.IN_CLOSE_WRITE)
1001         d.addCallback(lambda ign: _wait_for(None, Bob_to_rewrite_file2, alice=False))
1002         d.addCallback(lambda ign: self._check_version_in_dmd(self.bob_magicfolder, u"file2", 1))
1003         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 5))
1004         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 1))
1005         d.addCallback(lambda ign: self._check_uploader_count('objects_failed', 0, magic=self.bob_magicfolder))
1006         d.addCallback(lambda ign: self._check_uploader_count('objects_succeeded', 3, magic=self.bob_magicfolder))
1007         d.addCallback(lambda ign: self._check_uploader_count('files_uploaded', 2, magic=self.bob_magicfolder))
1008         d.addCallback(lambda ign: self._check_uploader_count('objects_queued', 0, magic=self.bob_magicfolder))
1009         d.addCallback(lambda ign: self._check_uploader_count('directories_created', 0, magic=self.bob_magicfolder))
1010         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 0, magic=self.alice_magicfolder))
1011         d.addCallback(lambda ign: self._check_downloader_count('objects_failed', 0, magic=self.alice_magicfolder))
1012         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 2, magic=self.alice_magicfolder))
1013
1014         # XXX here we advance the clock and then test again to make sure no values are monotonically increasing
1015         # with each queue turn ;-p
1016         alice_clock.advance(6)
1017         bob_clock.advance(6)
1018         d.addCallback(lambda ign: self._check_version_in_dmd(self.bob_magicfolder, u"file2", 1))
1019         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 5))
1020         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 1))
1021         d.addCallback(lambda ign: self._check_uploader_count('objects_failed', 0, magic=self.bob_magicfolder))
1022         d.addCallback(lambda ign: self._check_uploader_count('objects_succeeded', 3, magic=self.bob_magicfolder))
1023         d.addCallback(lambda ign: self._check_uploader_count('files_uploaded', 2, magic=self.bob_magicfolder))
1024         d.addCallback(lambda ign: self._check_uploader_count('objects_queued', 0, magic=self.bob_magicfolder))
1025         d.addCallback(lambda ign: self._check_uploader_count('directories_created', 0, magic=self.bob_magicfolder))
1026         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 0, magic=self.alice_magicfolder))
1027         d.addCallback(lambda ign: self._check_downloader_count('objects_failed', 0, magic=self.alice_magicfolder))
1028         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 2, magic=self.alice_magicfolder))
1029
1030         def Alice_conflicts_with_Bobs_last_uploaded_uri():
1031             print "Alice conflicts with Bob\n"
1032             encoded_path_u = magicpath.path2magic(u"file2")
1033             downloaded_d = self.bob_magicfolder.downloader.set_hook('processed')
1034             uploadable = Data("rabbits with sharp fangs", self.alice_magicfolder._client.convergence)
1035             alice_dmd = self.alice_magicfolder.uploader._upload_dirnode
1036             d2 = alice_dmd.add_file(u"file2", uploadable,
1037                                     metadata={"version": 5,
1038                                               "last_uploaded_uri" : "URI:LIT:" },
1039                                     overwrite=True)
1040             print "Waiting for Alice to upload\n"
1041             d2.addCallback(lambda ign: bob_clock.advance(6))
1042             d2.addCallback(lambda ign: downloaded_d)
1043             d2.addCallback(lambda ign: self.failUnless(alice_dmd.has_child(encoded_path_u)))
1044             return d2
1045         d.addCallback(lambda ign: Alice_conflicts_with_Bobs_last_uploaded_uri())
1046         d.addCallback(lambda ign: self._check_version_in_dmd(self.bob_magicfolder, u"file2", 5))
1047         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 6))
1048         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 2))
1049         d.addCallback(lambda ign: self._check_uploader_count('objects_failed', 0, magic=self.bob_magicfolder))
1050         d.addCallback(lambda ign: self._check_uploader_count('objects_succeeded', 3, magic=self.bob_magicfolder))
1051         d.addCallback(lambda ign: self._check_uploader_count('files_uploaded', 2, magic=self.bob_magicfolder))
1052         d.addCallback(lambda ign: self._check_uploader_count('objects_queued', 0, magic=self.bob_magicfolder))
1053         d.addCallback(lambda ign: self._check_uploader_count('directories_created', 0, magic=self.bob_magicfolder))
1054         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 0, magic=self.alice_magicfolder))
1055         d.addCallback(lambda ign: self._check_downloader_count('objects_failed', 0, magic=self.alice_magicfolder))
1056         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 2, magic=self.alice_magicfolder))
1057
1058         alice_clock.advance(6)
1059         bob_clock.advance(6)
1060         alice_clock.advance(6)
1061         bob_clock.advance(6)
1062
1063         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 2, magic=self.alice_magicfolder))
1064         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 0, magic=self.alice_magicfolder))
1065         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 2))
1066         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 6))
1067
1068         # prepare to perform another conflict test
1069         def Alice_to_write_file3():
1070             print "Alice writes a file\n"
1071             self.file_path = abspath_expanduser_unicode(u"file3", base=self.alice_magicfolder.uploader._local_path_u)
1072             fileutil.write(self.file_path, "something")
1073             self.notify(to_filepath(self.file_path), self.inotify.IN_CLOSE_WRITE, magic=self.alice_magicfolder)
1074         d.addCallback(_wait_for, Alice_to_write_file3)
1075         d.addCallback(lambda ign: self._check_version_in_dmd(self.alice_magicfolder, u"file3", 0))
1076         d.addCallback(lambda ign: self._check_downloader_count('objects_failed', 0, magic=self.alice_magicfolder))
1077         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 7))
1078         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 2, magic=self.alice_magicfolder))
1079         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 2))
1080         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 0, magic=self.alice_magicfolder))
1081
1082         def Bob_to_rewrite_file3():
1083             print "Bob rewrites file\n"
1084             self.file_path = abspath_expanduser_unicode(u"file3", base=self.bob_magicfolder.uploader._local_path_u)
1085             print "---- bob's file is %r" % (self.file_path,)
1086             fileutil.write(self.file_path, "roger roger")
1087             self.magicfolder = self.bob_magicfolder
1088             self.notify(to_filepath(self.file_path), self.inotify.IN_CLOSE_WRITE)
1089         d.addCallback(lambda ign: _wait_for(None, Bob_to_rewrite_file3, alice=False))
1090         d.addCallback(lambda ign: self._check_version_in_dmd(self.bob_magicfolder, u"file3", 1))
1091         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 7))
1092         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 2))
1093         d.addCallback(lambda ign: self._check_uploader_count('objects_failed', 0, magic=self.bob_magicfolder))
1094         d.addCallback(lambda ign: self._check_uploader_count('objects_succeeded', 4, magic=self.bob_magicfolder))
1095         d.addCallback(lambda ign: self._check_uploader_count('files_uploaded', 3, magic=self.bob_magicfolder))
1096         d.addCallback(lambda ign: self._check_uploader_count('objects_queued', 0, magic=self.bob_magicfolder))
1097         d.addCallback(lambda ign: self._check_uploader_count('directories_created', 0, magic=self.bob_magicfolder))
1098         d.addCallback(lambda ign: self._check_downloader_count('objects_conflicted', 0, magic=self.alice_magicfolder))
1099         d.addCallback(lambda ign: self._check_downloader_count('objects_failed', 0, magic=self.alice_magicfolder))
1100         d.addCallback(lambda ign: self._check_downloader_count('objects_downloaded', 3, magic=self.alice_magicfolder))
1101
1102         d.addCallback(cleanup_Alice_and_Bob)
1103         return d
1104
1105
1106 class MockTest(MagicFolderTestMixin, unittest.TestCase):
1107     """This can run on any platform, and even if twisted.internet.inotify can't be imported."""
1108
1109     def setUp(self):
1110         MagicFolderTestMixin.setUp(self)
1111         self.inotify = fake_inotify
1112         self.patch(magic_folder, 'get_inotify_module', lambda: self.inotify)
1113
1114     def notify(self, path, mask, magic=None, flush=True):
1115         if magic is None:
1116             magic = self.magicfolder
1117         magic.uploader._notifier.event(path, mask)
1118         # no flush for the mock test.
1119
1120     def test_errors(self):
1121         self.set_up_grid()
1122
1123         errors_dir = abspath_expanduser_unicode(u"errors_dir", base=self.basedir)
1124         os.mkdir(errors_dir)
1125         not_a_dir = abspath_expanduser_unicode(u"NOT_A_DIR", base=self.basedir)
1126         fileutil.write(not_a_dir, "")
1127         magicfolderdb = abspath_expanduser_unicode(u"magicfolderdb", base=self.basedir)
1128         doesnotexist  = abspath_expanduser_unicode(u"doesnotexist", base=self.basedir)
1129
1130         client = self.g.clients[0]
1131         d = client.create_dirnode()
1132         def _check_errors(n):
1133             self.failUnless(IDirectoryNode.providedBy(n))
1134             upload_dircap = n.get_uri()
1135             readonly_dircap = n.get_readonly_uri()
1136
1137             self.shouldFail(AssertionError, 'nonexistent local.directory', 'there is no directory',
1138                             MagicFolder, client, upload_dircap, '', doesnotexist, magicfolderdb, 0077)
1139             self.shouldFail(AssertionError, 'non-directory local.directory', 'is not a directory',
1140                             MagicFolder, client, upload_dircap, '', not_a_dir, magicfolderdb, 0077)
1141             self.shouldFail(AssertionError, 'bad upload.dircap', 'does not refer to a directory',
1142                             MagicFolder, client, 'bad', '', errors_dir, magicfolderdb, 0077)
1143             self.shouldFail(AssertionError, 'non-directory upload.dircap', 'does not refer to a directory',
1144                             MagicFolder, client, 'URI:LIT:foo', '', errors_dir, magicfolderdb, 0077)
1145             self.shouldFail(AssertionError, 'readonly upload.dircap', 'is not a writecap to a directory',
1146                             MagicFolder, client, readonly_dircap, '', errors_dir, magicfolderdb, 0077)
1147             self.shouldFail(AssertionError, 'collective dircap', 'is not a readonly cap to a directory',
1148                             MagicFolder, client, upload_dircap, upload_dircap, errors_dir, magicfolderdb, 0077)
1149
1150             def _not_implemented():
1151                 raise NotImplementedError("blah")
1152             self.patch(magic_folder, 'get_inotify_module', _not_implemented)
1153             self.shouldFail(NotImplementedError, 'unsupported', 'blah',
1154                             MagicFolder, client, upload_dircap, '', errors_dir, magicfolderdb, 0077)
1155         d.addCallback(_check_errors)
1156         return d
1157
1158     def test_write_downloaded_file(self):
1159         workdir = u"cli/MagicFolder/write-downloaded-file"
1160         local_file = fileutil.abspath_expanduser_unicode(os.path.join(workdir, "foobar"))
1161
1162         class TestWriteFileMixin(WriteFileMixin):
1163             def _log(self, msg):
1164                 pass
1165
1166         writefile = TestWriteFileMixin()
1167         writefile._umask = 0077
1168
1169         # create a file with name "foobar" with content "foo"
1170         # write downloaded file content "bar" into "foobar" with is_conflict = False
1171         fileutil.make_dirs(workdir)
1172         fileutil.write(local_file, "foo")
1173
1174         # if is_conflict is False, then the .conflict file shouldn't exist.
1175         writefile._write_downloaded_file(local_file, "bar", False, None)
1176         conflicted_path = local_file + u".conflict"
1177         self.failIf(os.path.exists(conflicted_path))
1178
1179         # At this point, the backup file should exist with content "foo"
1180         backup_path = local_file + u".backup"
1181         self.failUnless(os.path.exists(backup_path))
1182         self.failUnlessEqual(fileutil.read(backup_path), "foo")
1183
1184         # .tmp file shouldn't exist
1185         self.failIf(os.path.exists(local_file + u".tmp"))
1186
1187         # .. and the original file should have the new content
1188         self.failUnlessEqual(fileutil.read(local_file), "bar")
1189
1190         # now a test for conflicted case
1191         writefile._write_downloaded_file(local_file, "bar", True, None)
1192         self.failUnless(os.path.exists(conflicted_path))
1193
1194         # .tmp file shouldn't exist
1195         self.failIf(os.path.exists(local_file + u".tmp"))
1196
1197
1198 class RealTest(MagicFolderTestMixin, unittest.TestCase):
1199     """This is skipped unless both Twisted and the platform support inotify."""
1200
1201     def setUp(self):
1202         MagicFolderTestMixin.setUp(self)
1203         self.inotify = magic_folder.get_inotify_module()
1204
1205     def notify(self, path, mask, magic=None, flush=True):
1206         # Writing to the filesystem causes the notification.
1207         # However, flushing filesystem buffers may be necessary on Windows.
1208         if flush:
1209             fileutil.flush_volume(path.path)
1210
1211 try:
1212     magic_folder.get_inotify_module()
1213 except NotImplementedError:
1214     RealTest.skip = "Magic Folder support can only be tested for-real on an OS that supports inotify or equivalent."