3198: Implement rename() (efficient move within/between collections).
[arvados.git] / services / fuse / tests / test_mount.py
1 import arvados
2 import arvados.safeapi
3 import arvados_fuse as fuse
4 import glob
5 import json
6 import llfuse
7 import os
8 import shutil
9 import subprocess
10 import sys
11 import tempfile
12 import threading
13 import time
14 import unittest
15 import logging
16 import multiprocessing
17 import run_test_server
18
19 logger = logging.getLogger('arvados.arv-mount')
20
21 import threading, sys, traceback
22
23 def dumpstacks(signal, frame):
24     id2name = dict([(th.ident, th.name) for th in threading.enumerate()])
25     code = []
26     for threadId, stack in sys._current_frames().items():
27         code.append("\n# Thread: %s(%d)" % (id2name.get(threadId,""), threadId))
28         for filename, lineno, name, line in traceback.extract_stack(stack):
29             code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
30             if line:
31                 code.append("  %s" % (line.strip()))
32     print "\n".join(code)
33
34 import signal
35 signal.signal(signal.SIGUSR1, dumpstacks)
36
37 class MountTestBase(unittest.TestCase):
38     def setUp(self):
39         self.keeptmp = tempfile.mkdtemp()
40         os.environ['KEEP_LOCAL_STORE'] = self.keeptmp
41         self.mounttmp = tempfile.mkdtemp()
42         run_test_server.run()
43         run_test_server.authorize_with("admin")
44         self.api = arvados.safeapi.ThreadSafeApiCache(arvados.config.settings())
45
46     def make_mount(self, root_class, **root_kwargs):
47         operations = fuse.Operations(os.getuid(), os.getgid(), inode_cache=2)
48         operations.inodes.add_entry(root_class(
49             llfuse.ROOT_INODE, operations.inodes, self.api, 0, **root_kwargs))
50         llfuse.init(operations, self.mounttmp, ['debug'])
51         threading.Thread(None, llfuse.main).start()
52         # wait until the driver is finished initializing
53         operations.initlock.wait()
54         return operations.inodes[llfuse.ROOT_INODE]
55
56     def tearDown(self):
57         # llfuse.close is buggy, so use fusermount instead.
58         #llfuse.close(unmount=True)
59         count = 0
60         success = 1
61         while (count < 9 and success != 0):
62           success = subprocess.call(["fusermount", "-u", self.mounttmp])
63           time.sleep(0.5)
64           count += 1
65
66         os.rmdir(self.mounttmp)
67         shutil.rmtree(self.keeptmp)
68         run_test_server.reset()
69
70     def assertDirContents(self, subdir, expect_content):
71         path = self.mounttmp
72         if subdir:
73             path = os.path.join(path, subdir)
74         self.assertEqual(sorted(expect_content), sorted(llfuse.listdir(path)))
75
76
77 class FuseMountTest(MountTestBase):
78     def setUp(self):
79         super(FuseMountTest, self).setUp()
80
81         cw = arvados.CollectionWriter()
82
83         cw.start_new_file('thing1.txt')
84         cw.write("data 1")
85         cw.start_new_file('thing2.txt')
86         cw.write("data 2")
87         cw.start_new_stream('dir1')
88
89         cw.start_new_file('thing3.txt')
90         cw.write("data 3")
91         cw.start_new_file('thing4.txt')
92         cw.write("data 4")
93
94         cw.start_new_stream('dir2')
95         cw.start_new_file('thing5.txt')
96         cw.write("data 5")
97         cw.start_new_file('thing6.txt')
98         cw.write("data 6")
99
100         cw.start_new_stream('dir2/dir3')
101         cw.start_new_file('thing7.txt')
102         cw.write("data 7")
103
104         cw.start_new_file('thing8.txt')
105         cw.write("data 8")
106
107         cw.start_new_stream('edgecases')
108         for f in ":/./../.../-/*/\x01\\/ ".split("/"):
109             cw.start_new_file(f)
110             cw.write('x')
111
112         for f in ":/../.../-/*/\x01\\/ ".split("/"):
113             cw.start_new_stream('edgecases/dirs/' + f)
114             cw.start_new_file('x/x')
115             cw.write('x')
116
117         self.testcollection = cw.finish()
118         self.api.collections().create(body={"manifest_text":cw.manifest_text()}).execute()
119
120     def runTest(self):
121         self.make_mount(fuse.CollectionDirectory, collection_record=self.testcollection)
122
123         self.assertDirContents(None, ['thing1.txt', 'thing2.txt',
124                                       'edgecases', 'dir1', 'dir2'])
125         self.assertDirContents('dir1', ['thing3.txt', 'thing4.txt'])
126         self.assertDirContents('dir2', ['thing5.txt', 'thing6.txt', 'dir3'])
127         self.assertDirContents('dir2/dir3', ['thing7.txt', 'thing8.txt'])
128         self.assertDirContents('edgecases',
129                                "dirs/:/_/__/.../-/*/\x01\\/ ".split("/"))
130         self.assertDirContents('edgecases/dirs',
131                                ":/__/.../-/*/\x01\\/ ".split("/"))
132
133         files = {'thing1.txt': 'data 1',
134                  'thing2.txt': 'data 2',
135                  'dir1/thing3.txt': 'data 3',
136                  'dir1/thing4.txt': 'data 4',
137                  'dir2/thing5.txt': 'data 5',
138                  'dir2/thing6.txt': 'data 6',
139                  'dir2/dir3/thing7.txt': 'data 7',
140                  'dir2/dir3/thing8.txt': 'data 8'}
141
142         for k, v in files.items():
143             with open(os.path.join(self.mounttmp, k)) as f:
144                 self.assertEqual(v, f.read())
145
146
147 class FuseNoAPITest(MountTestBase):
148     def setUp(self):
149         super(FuseNoAPITest, self).setUp()
150         keep = arvados.keep.KeepClient(local_store=self.keeptmp)
151         self.file_data = "API-free text\n"
152         self.file_loc = keep.put(self.file_data)
153         self.coll_loc = keep.put(". {} 0:{}:api-free.txt\n".format(
154                 self.file_loc, len(self.file_data)))
155
156     def runTest(self):
157         self.make_mount(fuse.MagicDirectory)
158         self.assertDirContents(self.coll_loc, ['api-free.txt'])
159         with open(os.path.join(
160                 self.mounttmp, self.coll_loc, 'api-free.txt')) as keep_file:
161             actual = keep_file.read(-1)
162         self.assertEqual(self.file_data, actual)
163
164
165 class FuseMagicTest(MountTestBase):
166     def setUp(self):
167         super(FuseMagicTest, self).setUp()
168
169         cw = arvados.CollectionWriter()
170
171         cw.start_new_file('thing1.txt')
172         cw.write("data 1")
173
174         self.testcollection = cw.finish()
175         self.api.collections().create(body={"manifest_text":cw.manifest_text()}).execute()
176
177     def runTest(self):
178         self.make_mount(fuse.MagicDirectory)
179
180         mount_ls = llfuse.listdir(self.mounttmp)
181         self.assertIn('README', mount_ls)
182         self.assertFalse(any(arvados.util.keep_locator_pattern.match(fn) or
183                              arvados.util.uuid_pattern.match(fn)
184                              for fn in mount_ls),
185                          "new FUSE MagicDirectory lists Collection")
186         self.assertDirContents(self.testcollection, ['thing1.txt'])
187         self.assertDirContents(os.path.join('by_id', self.testcollection),
188                                ['thing1.txt'])
189         mount_ls = llfuse.listdir(self.mounttmp)
190         self.assertIn('README', mount_ls)
191         self.assertIn(self.testcollection, mount_ls)
192         self.assertIn(self.testcollection,
193                       llfuse.listdir(os.path.join(self.mounttmp, 'by_id')))
194
195         files = {}
196         files[os.path.join(self.mounttmp, self.testcollection, 'thing1.txt')] = 'data 1'
197
198         for k, v in files.items():
199             with open(os.path.join(self.mounttmp, k)) as f:
200                 self.assertEqual(v, f.read())
201
202
203 class FuseTagsTest(MountTestBase):
204     def runTest(self):
205         self.make_mount(fuse.TagsDirectory)
206
207         d1 = llfuse.listdir(self.mounttmp)
208         d1.sort()
209         self.assertEqual(['foo_tag'], d1)
210
211         d2 = llfuse.listdir(os.path.join(self.mounttmp, 'foo_tag'))
212         d2.sort()
213         self.assertEqual(['zzzzz-4zz18-fy296fx3hot09f7'], d2)
214
215         d3 = llfuse.listdir(os.path.join(self.mounttmp, 'foo_tag', 'zzzzz-4zz18-fy296fx3hot09f7'))
216         d3.sort()
217         self.assertEqual(['foo'], d3)
218
219
220 class FuseTagsUpdateTest(MountTestBase):
221     def tag_collection(self, coll_uuid, tag_name):
222         return self.api.links().create(
223             body={'link': {'head_uuid': coll_uuid,
224                            'link_class': 'tag',
225                            'name': tag_name,
226         }}).execute()
227
228     def runTest(self):
229         self.make_mount(fuse.TagsDirectory, poll_time=1)
230
231         self.assertIn('foo_tag', llfuse.listdir(self.mounttmp))
232
233         bar_uuid = run_test_server.fixture('collections')['bar_file']['uuid']
234         self.tag_collection(bar_uuid, 'fuse_test_tag')
235         time.sleep(1)
236         self.assertIn('fuse_test_tag', llfuse.listdir(self.mounttmp))
237         self.assertDirContents('fuse_test_tag', [bar_uuid])
238
239         baz_uuid = run_test_server.fixture('collections')['baz_file']['uuid']
240         l = self.tag_collection(baz_uuid, 'fuse_test_tag')
241         time.sleep(1)
242         self.assertDirContents('fuse_test_tag', [bar_uuid, baz_uuid])
243
244         self.api.links().delete(uuid=l['uuid']).execute()
245         time.sleep(1)
246         self.assertDirContents('fuse_test_tag', [bar_uuid])
247
248
249 class FuseSharedTest(MountTestBase):
250     def runTest(self):
251         self.make_mount(fuse.SharedDirectory,
252                         exclude=self.api.users().current().execute()['uuid'])
253
254         # shared_dirs is a list of the directories exposed
255         # by fuse.SharedDirectory (i.e. any object visible
256         # to the current user)
257         shared_dirs = llfuse.listdir(self.mounttmp)
258         shared_dirs.sort()
259         self.assertIn('FUSE User', shared_dirs)
260
261         # fuse_user_objs is a list of the objects owned by the FUSE
262         # test user (which present as files in the 'FUSE User'
263         # directory)
264         fuse_user_objs = llfuse.listdir(os.path.join(self.mounttmp, 'FUSE User'))
265         fuse_user_objs.sort()
266         self.assertEqual(['FUSE Test Project',                    # project owned by user
267                           'collection #1 owned by FUSE',          # collection owned by user
268                           'collection #2 owned by FUSE',          # collection owned by user
269                           'pipeline instance owned by FUSE.pipelineInstance',  # pipeline instance owned by user
270                       ], fuse_user_objs)
271
272         # test_proj_files is a list of the files in the FUSE Test Project.
273         test_proj_files = llfuse.listdir(os.path.join(self.mounttmp, 'FUSE User', 'FUSE Test Project'))
274         test_proj_files.sort()
275         self.assertEqual(['collection in FUSE project',
276                           'pipeline instance in FUSE project.pipelineInstance',
277                           'pipeline template in FUSE project.pipelineTemplate'
278                       ], test_proj_files)
279
280         # Double check that we can open and read objects in this folder as a file,
281         # and that its contents are what we expect.
282         pipeline_template_path = os.path.join(
283                 self.mounttmp,
284                 'FUSE User',
285                 'FUSE Test Project',
286                 'pipeline template in FUSE project.pipelineTemplate')
287         with open(pipeline_template_path) as f:
288             j = json.load(f)
289             self.assertEqual("pipeline template in FUSE project", j['name'])
290
291         # check mtime on template
292         st = os.stat(pipeline_template_path)
293         self.assertEqual(st.st_mtime, 1397493304)
294
295         # check mtime on collection
296         st = os.stat(os.path.join(
297                 self.mounttmp,
298                 'FUSE User',
299                 'collection #1 owned by FUSE'))
300         self.assertEqual(st.st_mtime, 1391448174)
301
302
303 class FuseHomeTest(MountTestBase):
304     def runTest(self):
305         self.make_mount(fuse.ProjectDirectory,
306                         project_object=self.api.users().current().execute())
307
308         d1 = llfuse.listdir(self.mounttmp)
309         self.assertIn('Unrestricted public data', d1)
310
311         d2 = llfuse.listdir(os.path.join(self.mounttmp, 'Unrestricted public data'))
312         public_project = run_test_server.fixture('groups')[
313             'anonymously_accessible_project']
314         found_in = 0
315         found_not_in = 0
316         for name, item in run_test_server.fixture('collections').iteritems():
317             if 'name' not in item:
318                 pass
319             elif item['owner_uuid'] == public_project['uuid']:
320                 self.assertIn(item['name'], d2)
321                 found_in += 1
322             else:
323                 # Artificial assumption here: there is no public
324                 # collection fixture with the same name as a
325                 # non-public collection.
326                 self.assertNotIn(item['name'], d2)
327                 found_not_in += 1
328         self.assertNotEqual(0, found_in)
329         self.assertNotEqual(0, found_not_in)
330
331         d3 = llfuse.listdir(os.path.join(self.mounttmp, 'Unrestricted public data', 'GNU General Public License, version 3'))
332         self.assertEqual(["GNU_General_Public_License,_version_3.pdf"], d3)
333
334 class FuseUpdateFileTest(MountTestBase):
335     def runTest(self):
336         collection = arvados.collection.Collection(api_client=self.api)
337         with collection.open("file1.txt", "w") as f:
338             f.write("blub")
339
340         collection.save_new()
341
342         m = self.make_mount(fuse.CollectionDirectory)
343         with llfuse.lock:
344             m.new_collection(None, collection)
345
346         d1 = llfuse.listdir(self.mounttmp)
347         self.assertEqual(["file1.txt"], d1)
348         with open(os.path.join(self.mounttmp, "file1.txt")) as f:
349             self.assertEqual("blub", f.read())
350
351         with collection.open("file1.txt", "w") as f:
352             f.write("plnp")
353
354         d1 = llfuse.listdir(self.mounttmp)
355         self.assertEqual(["file1.txt"], d1)
356         with open(os.path.join(self.mounttmp, "file1.txt")) as f:
357             self.assertEqual("plnp", f.read())
358
359 class FuseAddFileToCollectionTest(MountTestBase):
360     def runTest(self):
361         collection = arvados.collection.Collection(api_client=self.api)
362         with collection.open("file1.txt", "w") as f:
363             f.write("blub")
364
365         collection.save_new()
366
367         m = self.make_mount(fuse.CollectionDirectory)
368         with llfuse.lock:
369             m.new_collection(None, collection)
370
371         d1 = llfuse.listdir(self.mounttmp)
372         self.assertEqual(["file1.txt"], d1)
373
374         with collection.open("file2.txt", "w") as f:
375             f.write("plnp")
376
377         d1 = llfuse.listdir(self.mounttmp)
378         self.assertEqual(["file1.txt", "file2.txt"], sorted(d1))
379
380 class FuseRemoveFileFromCollectionTest(MountTestBase):
381     def runTest(self):
382         collection = arvados.collection.Collection(api_client=self.api)
383         with collection.open("file1.txt", "w") as f:
384             f.write("blub")
385
386         with collection.open("file2.txt", "w") as f:
387             f.write("plnp")
388
389         collection.save_new()
390
391         m = self.make_mount(fuse.CollectionDirectory)
392         with llfuse.lock:
393             m.new_collection(None, collection)
394
395         d1 = llfuse.listdir(self.mounttmp)
396         self.assertEqual(["file1.txt", "file2.txt"], sorted(d1))
397
398         collection.remove("file2.txt")
399
400         d1 = llfuse.listdir(self.mounttmp)
401         self.assertEqual(["file1.txt"], d1)
402
403 class FuseCreateFileTest(MountTestBase):
404     def runTest(self):
405         collection = arvados.collection.Collection(api_client=self.api)
406         collection.save_new()
407
408         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
409         self.assertEqual(collection2["manifest_text"], "")
410
411         collection.save_new()
412
413         m = self.make_mount(fuse.CollectionDirectory)
414         with llfuse.lock:
415             m.new_collection(None, collection)
416         self.assertTrue(m.writable())
417
418         self.assertNotIn("file1.txt", collection)
419
420         with open(os.path.join(self.mounttmp, "file1.txt"), "w") as f:
421             pass
422
423         self.assertIn("file1.txt", collection)
424
425         d1 = llfuse.listdir(self.mounttmp)
426         self.assertEqual(["file1.txt"], d1)
427
428         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
429         self.assertRegexpMatches(collection2["manifest_text"],
430             r'\. d41d8cd98f00b204e9800998ecf8427e\+0\+A[a-f0-9]{40}@[a-f0-9]{8} 0:0:file1\.txt$')
431
432 def fuseWriteFileTestHelper(mounttmp):
433     with open(os.path.join(mounttmp, "file1.txt"), "r") as f:
434         return f.read() == "Hello world!"
435
436 class FuseWriteFileTest(MountTestBase):
437     def runTest(self):
438         arvados.logger.setLevel(logging.DEBUG)
439
440         collection = arvados.collection.Collection(api_client=self.api)
441         collection.save_new()
442
443         m = self.make_mount(fuse.CollectionDirectory)
444         with llfuse.lock:
445             m.new_collection(None, collection)
446         self.assertTrue(m.writable())
447
448         self.assertNotIn("file1.txt", collection)
449
450         with open(os.path.join(self.mounttmp, "file1.txt"), "w") as f:
451             f.write("Hello world!")
452
453         with collection.open("file1.txt") as f:
454             self.assertEqual(f.read(), "Hello world!")
455
456         # We can't just open the collection for reading because the underlying
457         # C implementation of open() makes a fstat() syscall with the GIL still
458         # held.  When the GETATTR message comes back to llfuse (which in these
459         # tests is in the same interpreter process) it can't acquire the GIL,
460         # so it can't service the fstat() call, so it deadlocks.  The
461         # workaround is to run some of our test code in a separate process.
462         # Forturnately the multiprocessing module makes this relatively easy.
463         pool = multiprocessing.Pool(1)
464         self.assertTrue(pool.apply(fuseWriteFileTestHelper, (self.mounttmp,)))
465         pool.close()
466
467         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
468         self.assertRegexpMatches(collection2["manifest_text"],
469             r'\. 86fb269d190d2c85f6e0468ceca42a20\+12\+A[a-f0-9]{40}@[a-f0-9]{8} 0:12:file1\.txt$')
470
471 def fuseUpdateFileTestHelper1(mounttmp):
472     with open(os.path.join(mounttmp, "file1.txt"), "r+") as f:
473         fr = f.read()
474         if fr != "Hello world!":
475             raise Exception("Got %s expected 'Hello world!'" % fr)
476         f.seek(0)
477         f.write("Hola mundo!")
478         f.seek(0)
479         fr = f.read()
480         if fr != "Hola mundo!!":
481             raise Exception("Got %s expected 'Hola mundo!!'" % fr)
482         return True
483
484 def fuseUpdateFileTestHelper2(mounttmp):
485     with open(os.path.join(mounttmp, "file1.txt"), "r") as f:
486         return f.read() == "Hola mundo!!"
487
488 class FuseUpdateFileTest(MountTestBase):
489     def runTest(self):
490         arvados.logger.setLevel(logging.DEBUG)
491
492         collection = arvados.collection.Collection(api_client=self.api)
493         collection.save_new()
494
495         m = self.make_mount(fuse.CollectionDirectory)
496         with llfuse.lock:
497             m.new_collection(None, collection)
498         self.assertTrue(m.writable())
499
500         with open(os.path.join(self.mounttmp, "file1.txt"), "w") as f:
501             f.write("Hello world!")
502
503         # See note in FuseWriteFileTest
504         pool = multiprocessing.Pool(1)
505         self.assertTrue(pool.apply(fuseUpdateFileTestHelper1, (self.mounttmp,)))
506         self.assertTrue(pool.apply(fuseUpdateFileTestHelper2, (self.mounttmp,)))
507         pool.close()
508
509         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
510         self.assertRegexpMatches(collection2["manifest_text"],
511             r'\. daaef200ebb921e011e3ae922dd3266b\+11\+A[a-f0-9]{40}@[a-f0-9]{8} 86fb269d190d2c85f6e0468ceca42a20\+12\+A[a-f0-9]{40}@[a-f0-9]{8} 0:11:file1\.txt 22:1:file1\.txt$')
512
513
514 class FuseMkdirTest(MountTestBase):
515     def runTest(self):
516         arvados.logger.setLevel(logging.DEBUG)
517
518         collection = arvados.collection.Collection(api_client=self.api)
519         collection.save_new()
520
521         m = self.make_mount(fuse.CollectionDirectory)
522         with llfuse.lock:
523             m.new_collection(None, collection)
524         self.assertTrue(m.writable())
525
526         with self.assertRaises(IOError):
527             with open(os.path.join(self.mounttmp, "testdir", "file1.txt"), "w") as f:
528                 f.write("Hello world!")
529
530         os.mkdir(os.path.join(self.mounttmp, "testdir"))
531
532         with self.assertRaises(OSError):
533             os.mkdir(os.path.join(self.mounttmp, "testdir"))
534
535         d1 = llfuse.listdir(self.mounttmp)
536         self.assertEqual(["testdir"], d1)
537
538         with open(os.path.join(self.mounttmp, "testdir", "file1.txt"), "w") as f:
539             f.write("Hello world!")
540
541         d1 = llfuse.listdir(os.path.join(self.mounttmp, "testdir"))
542         self.assertEqual(["file1.txt"], d1)
543
544         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
545         self.assertRegexpMatches(collection2["manifest_text"],
546             r'\./testdir 86fb269d190d2c85f6e0468ceca42a20\+12\+A[a-f0-9]{40}@[a-f0-9]{8} 0:12:file1\.txt$')
547
548
549 class FuseRmTest(MountTestBase):
550     def runTest(self):
551         arvados.logger.setLevel(logging.DEBUG)
552
553         collection = arvados.collection.Collection(api_client=self.api)
554         collection.save_new()
555
556         m = self.make_mount(fuse.CollectionDirectory)
557         with llfuse.lock:
558             m.new_collection(None, collection)
559         self.assertTrue(m.writable())
560
561         os.mkdir(os.path.join(self.mounttmp, "testdir"))
562
563         with open(os.path.join(self.mounttmp, "testdir", "file1.txt"), "w") as f:
564             f.write("Hello world!")
565
566         # Starting manifest
567         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
568         self.assertRegexpMatches(collection2["manifest_text"],
569             r'\./testdir 86fb269d190d2c85f6e0468ceca42a20\+12\+A[a-f0-9]{40}@[a-f0-9]{8} 0:12:file1\.txt$')
570
571         # Can't delete because it's not empty
572         with self.assertRaises(OSError):
573             os.rmdir(os.path.join(self.mounttmp, "testdir"))
574
575         d1 = llfuse.listdir(os.path.join(self.mounttmp, "testdir"))
576         self.assertEqual(["file1.txt"], d1)
577
578         # Delete file
579         os.remove(os.path.join(self.mounttmp, "testdir", "file1.txt"))
580
581         # Make sure it's empty
582         d1 = llfuse.listdir(os.path.join(self.mounttmp, "testdir"))
583         self.assertEqual([], d1)
584
585         # Try to delete it again
586         with self.assertRaises(OSError):
587             os.remove(os.path.join(self.mounttmp, "testdir", "file1.txt"))
588
589         # Can't have empty directories :-( so manifest will be empty.
590         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
591         self.assertEqual(collection2["manifest_text"], "")
592
593         # Should be able to delete now that it is empty
594         os.rmdir(os.path.join(self.mounttmp, "testdir"))
595
596         # Make sure it's empty
597         d1 = llfuse.listdir(os.path.join(self.mounttmp))
598         self.assertEqual([], d1)
599
600         # Try to delete it again
601         with self.assertRaises(OSError):
602             os.rmdir(os.path.join(self.mounttmp, "testdir"))
603
604         # manifest should be empty now.
605         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
606         self.assertEqual(collection2["manifest_text"], "")
607
608
609 class FuseMvTest(MountTestBase):
610     def runTest(self):
611         arvados.logger.setLevel(logging.DEBUG)
612
613         collection = arvados.collection.Collection(api_client=self.api)
614         collection.save_new()
615
616         m = self.make_mount(fuse.CollectionDirectory)
617         with llfuse.lock:
618             m.new_collection(None, collection)
619         self.assertTrue(m.writable())
620
621         os.mkdir(os.path.join(self.mounttmp, "testdir"))
622
623         with open(os.path.join(self.mounttmp, "testdir", "file1.txt"), "w") as f:
624             f.write("Hello world!")
625
626         # Starting manifest
627         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
628         self.assertRegexpMatches(collection2["manifest_text"],
629             r'\./testdir 86fb269d190d2c85f6e0468ceca42a20\+12\+A[a-f0-9]{40}@[a-f0-9]{8} 0:12:file1\.txt$')
630
631         d1 = llfuse.listdir(os.path.join(self.mounttmp))
632         self.assertEqual(["testdir"], d1)
633         d1 = llfuse.listdir(os.path.join(self.mounttmp, "testdir"))
634         self.assertEqual(["file1.txt"], d1)
635
636         os.rename(os.path.join(self.mounttmp, "testdir", "file1.txt"), os.path.join(self.mounttmp, "file1.txt"))
637
638         d1 = llfuse.listdir(os.path.join(self.mounttmp))
639         self.assertEqual(["file1.txt", "testdir"], sorted(d1))
640         d1 = llfuse.listdir(os.path.join(self.mounttmp, "testdir"))
641         self.assertEqual([], d1)
642
643         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
644         self.assertRegexpMatches(collection2["manifest_text"],
645             r'\. 86fb269d190d2c85f6e0468ceca42a20\+12\+A[a-f0-9]{40}@[a-f0-9]{8} 0:12:file1\.txt$')
646
647
648 class FuseUnitTest(unittest.TestCase):
649     def test_sanitize_filename(self):
650         acceptable = [
651             "foo.txt",
652             ".foo",
653             "..foo",
654             "...",
655             "foo...",
656             "foo..",
657             "foo.",
658             "-",
659             "\x01\x02\x03",
660             ]
661         unacceptable = [
662             "f\00",
663             "\00\00",
664             "/foo",
665             "foo/",
666             "//",
667             ]
668         for f in acceptable:
669             self.assertEqual(f, fuse.sanitize_filename(f))
670         for f in unacceptable:
671             self.assertNotEqual(f, fuse.sanitize_filename(f))
672             # The sanitized filename should be the same length, though.
673             self.assertEqual(len(f), len(fuse.sanitize_filename(f)))
674         # Special cases
675         self.assertEqual("_", fuse.sanitize_filename(""))
676         self.assertEqual("_", fuse.sanitize_filename("."))
677         self.assertEqual("__", fuse.sanitize_filename(".."))