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