14965: Simplifies previous fix to FuseMagicTest
[arvados.git] / services / fuse / tests / test_mount.py
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 from __future__ import absolute_import
6 from future.utils import viewitems
7 from builtins import str
8 from builtins import object
9 import json
10 import llfuse
11 import logging
12 import mock
13 import os
14 import subprocess
15 import time
16 import unittest
17
18 import arvados
19 import arvados_fuse as fuse
20 from . import run_test_server
21
22 from .mount_test_base import MountTestBase
23
24 logger = logging.getLogger('arvados.arv-mount')
25
26
27 class AssertWithTimeout(object):
28     """Allow some time for an assertion to pass."""
29
30     def __init__(self, timeout=0):
31         self.timeout = timeout
32
33     def __iter__(self):
34         self.deadline = time.time() + self.timeout
35         self.done = False
36         return self
37
38     def __next__(self):
39         if self.done:
40             raise StopIteration
41         return self.attempt
42
43     def attempt(self, fn, *args, **kwargs):
44         try:
45             fn(*args, **kwargs)
46         except AssertionError:
47             if time.time() > self.deadline:
48                 raise
49             time.sleep(0.1)
50         else:
51             self.done = True
52
53
54 class FuseMountTest(MountTestBase):
55     def setUp(self):
56         super(FuseMountTest, self).setUp()
57
58         cw = arvados.CollectionWriter()
59
60         cw.start_new_file('thing1.txt')
61         cw.write("data 1")
62         cw.start_new_file('thing2.txt')
63         cw.write("data 2")
64
65         cw.start_new_stream('dir1')
66         cw.start_new_file('thing3.txt')
67         cw.write("data 3")
68         cw.start_new_file('thing4.txt')
69         cw.write("data 4")
70
71         cw.start_new_stream('dir2')
72         cw.start_new_file('thing5.txt')
73         cw.write("data 5")
74         cw.start_new_file('thing6.txt')
75         cw.write("data 6")
76
77         cw.start_new_stream('dir2/dir3')
78         cw.start_new_file('thing7.txt')
79         cw.write("data 7")
80
81         cw.start_new_file('thing8.txt')
82         cw.write("data 8")
83
84         cw.start_new_stream('edgecases')
85         for f in ":/.../-/*/ ".split("/"):
86             cw.start_new_file(f)
87             cw.write('x')
88
89         for f in ":/.../-/*/ ".split("/"):
90             cw.start_new_stream('edgecases/dirs/' + f)
91             cw.start_new_file('x/x')
92             cw.write('x')
93
94         self.testcollection = cw.finish()
95         self.api.collections().create(body={"manifest_text":cw.manifest_text()}).execute()
96
97     def runTest(self):
98         self.make_mount(fuse.CollectionDirectory, collection_record=self.testcollection)
99
100         self.assertDirContents(None, ['thing1.txt', 'thing2.txt',
101                                       'edgecases', 'dir1', 'dir2'])
102         self.assertDirContents('dir1', ['thing3.txt', 'thing4.txt'])
103         self.assertDirContents('dir2', ['thing5.txt', 'thing6.txt', 'dir3'])
104         self.assertDirContents('dir2/dir3', ['thing7.txt', 'thing8.txt'])
105         self.assertDirContents('edgecases',
106                                "dirs/:/.../-/*/ ".split("/"))
107         self.assertDirContents('edgecases/dirs',
108                                ":/.../-/*/ ".split("/"))
109
110         files = {'thing1.txt': 'data 1',
111                  'thing2.txt': 'data 2',
112                  'dir1/thing3.txt': 'data 3',
113                  'dir1/thing4.txt': 'data 4',
114                  'dir2/thing5.txt': 'data 5',
115                  'dir2/thing6.txt': 'data 6',
116                  'dir2/dir3/thing7.txt': 'data 7',
117                  'dir2/dir3/thing8.txt': 'data 8'}
118
119         for k, v in viewitems(files):
120             with open(os.path.join(self.mounttmp, k), 'rb') as f:
121                 self.assertEqual(v, f.read().decode())
122
123
124 class FuseMagicTest(MountTestBase):
125     def setUp(self, api=None):
126         super(FuseMagicTest, self).setUp(api=api)
127
128         self.test_project = run_test_server.fixture('groups')['aproject']['uuid']
129         self.non_project_group = run_test_server.fixture('groups')['public']['uuid']
130         self.collection_in_test_project = run_test_server.fixture('collections')['foo_collection_in_aproject']['name']
131
132         cw = arvados.CollectionWriter()
133
134         cw.start_new_file('thing1.txt')
135         cw.write("data 1")
136
137         self.testcollection = cw.finish()
138         self.test_manifest = cw.manifest_text()
139         coll = self.api.collections().create(body={"manifest_text":self.test_manifest}).execute()
140         self.test_manifest_pdh = coll['portable_data_hash']
141
142     def runTest(self):
143         self.make_mount(fuse.MagicDirectory)
144
145         mount_ls = llfuse.listdir(self.mounttmp)
146         self.assertIn('README', mount_ls)
147         self.assertFalse(any(arvados.util.keep_locator_pattern.match(fn) or
148                              arvados.util.uuid_pattern.match(fn)
149                              for fn in mount_ls),
150                          "new FUSE MagicDirectory has no collections or projects")
151         self.assertDirContents(self.testcollection, ['thing1.txt'])
152         self.assertDirContents(os.path.join('by_id', self.testcollection),
153                                ['thing1.txt'])
154         self.assertIn(self.collection_in_test_project,
155                       llfuse.listdir(os.path.join(self.mounttmp, self.test_project)))
156         self.assertIn(self.collection_in_test_project,
157                       llfuse.listdir(os.path.join(self.mounttmp, 'by_id', self.test_project)))
158
159         mount_ls = llfuse.listdir(self.mounttmp)
160         self.assertIn('README', mount_ls)
161         self.assertIn(self.testcollection, mount_ls)
162         self.assertIn(self.testcollection,
163                       llfuse.listdir(os.path.join(self.mounttmp, 'by_id')))
164         self.assertIn(self.test_project, mount_ls)
165         self.assertIn(self.test_project,
166                       llfuse.listdir(os.path.join(self.mounttmp, 'by_id')))
167
168         with self.assertRaises(OSError):
169             llfuse.listdir(os.path.join(self.mounttmp, 'by_id', self.non_project_group))
170
171         files = {}
172         files[os.path.join(self.mounttmp, self.testcollection, 'thing1.txt')] = 'data 1'
173
174         for k, v in viewitems(files):
175             with open(os.path.join(self.mounttmp, k), 'rb') as f:
176                 self.assertEqual(v, f.read().decode())
177
178
179 class FuseTagsTest(MountTestBase):
180     def runTest(self):
181         self.make_mount(fuse.TagsDirectory)
182
183         d1 = llfuse.listdir(self.mounttmp)
184         d1.sort()
185         self.assertEqual(['foo_tag'], d1)
186
187         d2 = llfuse.listdir(os.path.join(self.mounttmp, 'foo_tag'))
188         d2.sort()
189         self.assertEqual(['zzzzz-4zz18-fy296fx3hot09f7'], d2)
190
191         d3 = llfuse.listdir(os.path.join(self.mounttmp, 'foo_tag', 'zzzzz-4zz18-fy296fx3hot09f7'))
192         d3.sort()
193         self.assertEqual(['foo'], d3)
194
195
196 class FuseTagsUpdateTest(MountTestBase):
197     def tag_collection(self, coll_uuid, tag_name):
198         return self.api.links().create(
199             body={'link': {'head_uuid': coll_uuid,
200                            'link_class': 'tag',
201                            'name': tag_name,
202         }}).execute()
203
204     def runTest(self):
205         self.make_mount(fuse.TagsDirectory, poll_time=1)
206
207         self.assertIn('foo_tag', llfuse.listdir(self.mounttmp))
208
209         bar_uuid = run_test_server.fixture('collections')['bar_file']['uuid']
210         self.tag_collection(bar_uuid, 'fuse_test_tag')
211         for attempt in AssertWithTimeout(10):
212             attempt(self.assertIn, 'fuse_test_tag', llfuse.listdir(self.mounttmp))
213         self.assertDirContents('fuse_test_tag', [bar_uuid])
214
215         baz_uuid = run_test_server.fixture('collections')['baz_file']['uuid']
216         l = self.tag_collection(baz_uuid, 'fuse_test_tag')
217         for attempt in AssertWithTimeout(10):
218             attempt(self.assertDirContents, 'fuse_test_tag', [bar_uuid, baz_uuid])
219
220         self.api.links().delete(uuid=l['uuid']).execute()
221         for attempt in AssertWithTimeout(10):
222             attempt(self.assertDirContents, 'fuse_test_tag', [bar_uuid])
223
224
225 def fuseSharedTestHelper(mounttmp):
226     class Test(unittest.TestCase):
227         def runTest(self):
228             # Double check that we can open and read objects in this folder as a file,
229             # and that its contents are what we expect.
230             baz_path = os.path.join(
231                 mounttmp,
232                 'FUSE User',
233                 'FUSE Test Project',
234                 'collection in FUSE project',
235                 'baz')
236             with open(baz_path) as f:
237                 self.assertEqual("baz", f.read())
238
239             # check mtime on collection
240             st = os.stat(baz_path)
241             try:
242                 mtime = st.st_mtime_ns // 1000000000
243             except AttributeError:
244                 mtime = st.st_mtime
245             self.assertEqual(mtime, 1391448174)
246
247             # shared_dirs is a list of the directories exposed
248             # by fuse.SharedDirectory (i.e. any object visible
249             # to the current user)
250             shared_dirs = llfuse.listdir(mounttmp)
251             shared_dirs.sort()
252             self.assertIn('FUSE User', shared_dirs)
253
254             # fuse_user_objs is a list of the objects owned by the FUSE
255             # test user (which present as files in the 'FUSE User'
256             # directory)
257             fuse_user_objs = llfuse.listdir(os.path.join(mounttmp, 'FUSE User'))
258             fuse_user_objs.sort()
259             self.assertEqual(['FUSE Test Project',                    # project owned by user
260                               'collection #1 owned by FUSE',          # collection owned by user
261                               'collection #2 owned by FUSE'          # collection owned by user
262                           ], fuse_user_objs)
263
264             # test_proj_files is a list of the files in the FUSE Test Project.
265             test_proj_files = llfuse.listdir(os.path.join(mounttmp, 'FUSE User', 'FUSE Test Project'))
266             test_proj_files.sort()
267             self.assertEqual(['collection in FUSE project'
268                           ], test_proj_files)
269
270
271     Test().runTest()
272
273 class FuseSharedTest(MountTestBase):
274     def runTest(self):
275         self.make_mount(fuse.SharedDirectory,
276                         exclude=self.api.users().current().execute()['uuid'])
277         keep = arvados.keep.KeepClient()
278         keep.put("baz")
279
280         self.pool.apply(fuseSharedTestHelper, (self.mounttmp,))
281
282
283 class FuseHomeTest(MountTestBase):
284     def runTest(self):
285         self.make_mount(fuse.ProjectDirectory,
286                         project_object=self.api.users().current().execute())
287
288         d1 = llfuse.listdir(self.mounttmp)
289         self.assertIn('Unrestricted public data', d1)
290
291         d2 = llfuse.listdir(os.path.join(self.mounttmp, 'Unrestricted public data'))
292         public_project = run_test_server.fixture('groups')[
293             'anonymously_accessible_project']
294         found_in = 0
295         found_not_in = 0
296         for name, item in viewitems(run_test_server.fixture('collections')):
297             if 'name' not in item:
298                 pass
299             elif item['owner_uuid'] == public_project['uuid']:
300                 self.assertIn(item['name'], d2)
301                 found_in += 1
302             else:
303                 # Artificial assumption here: there is no public
304                 # collection fixture with the same name as a
305                 # non-public collection.
306                 self.assertNotIn(item['name'], d2)
307                 found_not_in += 1
308         self.assertNotEqual(0, found_in)
309         self.assertNotEqual(0, found_not_in)
310
311         d3 = llfuse.listdir(os.path.join(self.mounttmp, 'Unrestricted public data', 'GNU General Public License, version 3'))
312         self.assertEqual(["GNU_General_Public_License,_version_3.pdf"], d3)
313
314
315 def fuseModifyFileTestHelperReadStartContents(mounttmp):
316     class Test(unittest.TestCase):
317         def runTest(self):
318             d1 = llfuse.listdir(mounttmp)
319             self.assertEqual(["file1.txt"], d1)
320             with open(os.path.join(mounttmp, "file1.txt")) as f:
321                 self.assertEqual("blub", f.read())
322     Test().runTest()
323
324 def fuseModifyFileTestHelperReadEndContents(mounttmp):
325     class Test(unittest.TestCase):
326         def runTest(self):
327             d1 = llfuse.listdir(mounttmp)
328             self.assertEqual(["file1.txt"], d1)
329             with open(os.path.join(mounttmp, "file1.txt")) as f:
330                 self.assertEqual("plnp", f.read())
331     Test().runTest()
332
333 class FuseModifyFileTest(MountTestBase):
334     def runTest(self):
335         collection = arvados.collection.Collection(api_client=self.api)
336         with collection.open("file1.txt", "w") as f:
337             f.write("blub")
338
339         collection.save_new()
340
341         m = self.make_mount(fuse.CollectionDirectory)
342         with llfuse.lock:
343             m.new_collection(collection.api_response(), collection)
344
345         self.pool.apply(fuseModifyFileTestHelperReadStartContents, (self.mounttmp,))
346
347         with collection.open("file1.txt", "w") as f:
348             f.write("plnp")
349
350         self.pool.apply(fuseModifyFileTestHelperReadEndContents, (self.mounttmp,))
351
352
353 class FuseAddFileToCollectionTest(MountTestBase):
354     def runTest(self):
355         collection = arvados.collection.Collection(api_client=self.api)
356         with collection.open("file1.txt", "w") as f:
357             f.write("blub")
358
359         collection.save_new()
360
361         m = self.make_mount(fuse.CollectionDirectory)
362         with llfuse.lock:
363             m.new_collection(collection.api_response(), collection)
364
365         d1 = llfuse.listdir(self.mounttmp)
366         self.assertEqual(["file1.txt"], d1)
367
368         with collection.open("file2.txt", "w") as f:
369             f.write("plnp")
370
371         d1 = llfuse.listdir(self.mounttmp)
372         self.assertEqual(["file1.txt", "file2.txt"], sorted(d1))
373
374
375 class FuseRemoveFileFromCollectionTest(MountTestBase):
376     def runTest(self):
377         collection = arvados.collection.Collection(api_client=self.api)
378         with collection.open("file1.txt", "w") as f:
379             f.write("blub")
380
381         with collection.open("file2.txt", "w") as f:
382             f.write("plnp")
383
384         collection.save_new()
385
386         m = self.make_mount(fuse.CollectionDirectory)
387         with llfuse.lock:
388             m.new_collection(collection.api_response(), collection)
389
390         d1 = llfuse.listdir(self.mounttmp)
391         self.assertEqual(["file1.txt", "file2.txt"], sorted(d1))
392
393         collection.remove("file2.txt")
394
395         d1 = llfuse.listdir(self.mounttmp)
396         self.assertEqual(["file1.txt"], d1)
397
398
399 def fuseCreateFileTestHelper(mounttmp):
400     class Test(unittest.TestCase):
401         def runTest(self):
402             with open(os.path.join(mounttmp, "file1.txt"), "w") as f:
403                 pass
404     Test().runTest()
405
406 class FuseCreateFileTest(MountTestBase):
407     def runTest(self):
408         collection = arvados.collection.Collection(api_client=self.api)
409         collection.save_new()
410
411         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
412         self.assertEqual(collection2["manifest_text"], "")
413
414         collection.save_new()
415
416         m = self.make_mount(fuse.CollectionDirectory)
417         with llfuse.lock:
418             m.new_collection(collection.api_response(), collection)
419         self.assertTrue(m.writable())
420
421         self.assertNotIn("file1.txt", collection)
422
423         self.pool.apply(fuseCreateFileTestHelper, (self.mounttmp,))
424
425         self.assertIn("file1.txt", collection)
426
427         d1 = llfuse.listdir(self.mounttmp)
428         self.assertEqual(["file1.txt"], d1)
429
430         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
431         self.assertRegexpMatches(collection2["manifest_text"],
432             r'\. d41d8cd98f00b204e9800998ecf8427e\+0\+A\S+ 0:0:file1\.txt$')
433
434
435 def fuseWriteFileTestHelperWriteFile(mounttmp):
436     class Test(unittest.TestCase):
437         def runTest(self):
438             with open(os.path.join(mounttmp, "file1.txt"), "w") as f:
439                 f.write("Hello world!")
440     Test().runTest()
441
442 def fuseWriteFileTestHelperReadFile(mounttmp):
443     class Test(unittest.TestCase):
444         def runTest(self):
445             with open(os.path.join(mounttmp, "file1.txt"), "r") as f:
446                 self.assertEqual(f.read(), "Hello world!")
447     Test().runTest()
448
449 class FuseWriteFileTest(MountTestBase):
450     def runTest(self):
451         collection = arvados.collection.Collection(api_client=self.api)
452         collection.save_new()
453
454         m = self.make_mount(fuse.CollectionDirectory)
455         with llfuse.lock:
456             m.new_collection(collection.api_response(), collection)
457         self.assertTrue(m.writable())
458
459         self.assertNotIn("file1.txt", collection)
460
461         self.assertEqual(0, self.operations.write_counter.get())
462         self.pool.apply(fuseWriteFileTestHelperWriteFile, (self.mounttmp,))
463         self.assertEqual(12, self.operations.write_counter.get())
464
465         with collection.open("file1.txt") as f:
466             self.assertEqual(f.read(), "Hello world!")
467
468         self.assertEqual(0, self.operations.read_counter.get())
469         self.pool.apply(fuseWriteFileTestHelperReadFile, (self.mounttmp,))
470         self.assertEqual(12, self.operations.read_counter.get())
471
472         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
473         self.assertRegexpMatches(collection2["manifest_text"],
474             r'\. 86fb269d190d2c85f6e0468ceca42a20\+12\+A\S+ 0:12:file1\.txt$')
475
476
477 def fuseUpdateFileTestHelper(mounttmp):
478     class Test(unittest.TestCase):
479         def runTest(self):
480             with open(os.path.join(mounttmp, "file1.txt"), "w") as f:
481                 f.write("Hello world!")
482
483             with open(os.path.join(mounttmp, "file1.txt"), "r+") as f:
484                 fr = f.read()
485                 self.assertEqual(fr, "Hello world!")
486                 f.seek(0)
487                 f.write("Hola mundo!")
488                 f.seek(0)
489                 fr = f.read()
490                 self.assertEqual(fr, "Hola mundo!!")
491
492             with open(os.path.join(mounttmp, "file1.txt"), "r") as f:
493                 self.assertEqual(f.read(), "Hola mundo!!")
494
495     Test().runTest()
496
497 class FuseUpdateFileTest(MountTestBase):
498     def runTest(self):
499         collection = arvados.collection.Collection(api_client=self.api)
500         collection.save_new()
501
502         m = self.make_mount(fuse.CollectionDirectory)
503         with llfuse.lock:
504             m.new_collection(collection.api_response(), collection)
505         self.assertTrue(m.writable())
506
507         # See note in MountTestBase.setUp
508         self.pool.apply(fuseUpdateFileTestHelper, (self.mounttmp,))
509
510         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
511         self.assertRegexpMatches(collection2["manifest_text"],
512             r'\. daaef200ebb921e011e3ae922dd3266b\+11\+A\S+ 86fb269d190d2c85f6e0468ceca42a20\+12\+A\S+ 0:11:file1\.txt 22:1:file1\.txt$')
513
514
515 def fuseMkdirTestHelper(mounttmp):
516     class Test(unittest.TestCase):
517         def runTest(self):
518             with self.assertRaises(IOError):
519                 with open(os.path.join(mounttmp, "testdir", "file1.txt"), "w") as f:
520                     f.write("Hello world!")
521
522             os.mkdir(os.path.join(mounttmp, "testdir"))
523
524             with self.assertRaises(OSError):
525                 os.mkdir(os.path.join(mounttmp, "testdir"))
526
527             d1 = llfuse.listdir(mounttmp)
528             self.assertEqual(["testdir"], d1)
529
530             with open(os.path.join(mounttmp, "testdir", "file1.txt"), "w") as f:
531                 f.write("Hello world!")
532
533             d1 = llfuse.listdir(os.path.join(mounttmp, "testdir"))
534             self.assertEqual(["file1.txt"], d1)
535
536     Test().runTest()
537
538 class FuseMkdirTest(MountTestBase):
539     def runTest(self):
540         collection = arvados.collection.Collection(api_client=self.api)
541         collection.save_new()
542
543         m = self.make_mount(fuse.CollectionDirectory)
544         with llfuse.lock:
545             m.new_collection(collection.api_response(), collection)
546         self.assertTrue(m.writable())
547
548         self.pool.apply(fuseMkdirTestHelper, (self.mounttmp,))
549
550         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
551         self.assertRegexpMatches(collection2["manifest_text"],
552             r'\./testdir 86fb269d190d2c85f6e0468ceca42a20\+12\+A\S+ 0:12:file1\.txt$')
553
554
555 def fuseRmTestHelperWriteFile(mounttmp):
556     class Test(unittest.TestCase):
557         def runTest(self):
558             os.mkdir(os.path.join(mounttmp, "testdir"))
559
560             with open(os.path.join(mounttmp, "testdir", "file1.txt"), "w") as f:
561                 f.write("Hello world!")
562
563     Test().runTest()
564
565 def fuseRmTestHelperDeleteFile(mounttmp):
566     class Test(unittest.TestCase):
567         def runTest(self):
568             # Can't delete because it's not empty
569             with self.assertRaises(OSError):
570                 os.rmdir(os.path.join(mounttmp, "testdir"))
571
572             d1 = llfuse.listdir(os.path.join(mounttmp, "testdir"))
573             self.assertEqual(["file1.txt"], d1)
574
575             # Delete file
576             os.remove(os.path.join(mounttmp, "testdir", "file1.txt"))
577
578             # Make sure it's empty
579             d1 = llfuse.listdir(os.path.join(mounttmp, "testdir"))
580             self.assertEqual([], d1)
581
582             # Try to delete it again
583             with self.assertRaises(OSError):
584                 os.remove(os.path.join(mounttmp, "testdir", "file1.txt"))
585
586     Test().runTest()
587
588 def fuseRmTestHelperRmdir(mounttmp):
589     class Test(unittest.TestCase):
590         def runTest(self):
591             # Should be able to delete now that it is empty
592             os.rmdir(os.path.join(mounttmp, "testdir"))
593
594             # Make sure it's empty
595             d1 = llfuse.listdir(os.path.join(mounttmp))
596             self.assertEqual([], d1)
597
598             # Try to delete it again
599             with self.assertRaises(OSError):
600                 os.rmdir(os.path.join(mounttmp, "testdir"))
601
602     Test().runTest()
603
604 class FuseRmTest(MountTestBase):
605     def runTest(self):
606         collection = arvados.collection.Collection(api_client=self.api)
607         collection.save_new()
608
609         m = self.make_mount(fuse.CollectionDirectory)
610         with llfuse.lock:
611             m.new_collection(collection.api_response(), collection)
612         self.assertTrue(m.writable())
613
614         self.pool.apply(fuseRmTestHelperWriteFile, (self.mounttmp,))
615
616         # Starting manifest
617         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
618         self.assertRegexpMatches(collection2["manifest_text"],
619             r'\./testdir 86fb269d190d2c85f6e0468ceca42a20\+12\+A\S+ 0:12:file1\.txt$')
620         self.pool.apply(fuseRmTestHelperDeleteFile, (self.mounttmp,))
621
622         # Empty directories are represented by an empty file named "."
623         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
624         self.assertRegexpMatches(collection2["manifest_text"],
625                                  r'./testdir d41d8cd98f00b204e9800998ecf8427e\+0\+A\S+ 0:0:\\056\n')
626
627         self.pool.apply(fuseRmTestHelperRmdir, (self.mounttmp,))
628
629         # manifest should be empty now.
630         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
631         self.assertEqual(collection2["manifest_text"], "")
632
633
634 def fuseMvFileTestHelperWriteFile(mounttmp):
635     class Test(unittest.TestCase):
636         def runTest(self):
637             os.mkdir(os.path.join(mounttmp, "testdir"))
638
639             with open(os.path.join(mounttmp, "testdir", "file1.txt"), "w") as f:
640                 f.write("Hello world!")
641
642     Test().runTest()
643
644 def fuseMvFileTestHelperMoveFile(mounttmp):
645     class Test(unittest.TestCase):
646         def runTest(self):
647             d1 = llfuse.listdir(os.path.join(mounttmp))
648             self.assertEqual(["testdir"], d1)
649             d1 = llfuse.listdir(os.path.join(mounttmp, "testdir"))
650             self.assertEqual(["file1.txt"], d1)
651
652             os.rename(os.path.join(mounttmp, "testdir", "file1.txt"), os.path.join(mounttmp, "file1.txt"))
653
654             d1 = llfuse.listdir(os.path.join(mounttmp))
655             self.assertEqual(["file1.txt", "testdir"], sorted(d1))
656             d1 = llfuse.listdir(os.path.join(mounttmp, "testdir"))
657             self.assertEqual([], d1)
658
659     Test().runTest()
660
661 class FuseMvFileTest(MountTestBase):
662     def runTest(self):
663         collection = arvados.collection.Collection(api_client=self.api)
664         collection.save_new()
665
666         m = self.make_mount(fuse.CollectionDirectory)
667         with llfuse.lock:
668             m.new_collection(collection.api_response(), collection)
669         self.assertTrue(m.writable())
670
671         self.pool.apply(fuseMvFileTestHelperWriteFile, (self.mounttmp,))
672
673         # Starting manifest
674         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
675         self.assertRegexpMatches(collection2["manifest_text"],
676             r'\./testdir 86fb269d190d2c85f6e0468ceca42a20\+12\+A\S+ 0:12:file1\.txt$')
677
678         self.pool.apply(fuseMvFileTestHelperMoveFile, (self.mounttmp,))
679
680         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
681         self.assertRegexpMatches(collection2["manifest_text"],
682             r'\. 86fb269d190d2c85f6e0468ceca42a20\+12\+A\S+ 0:12:file1\.txt\n\./testdir d41d8cd98f00b204e9800998ecf8427e\+0\+A\S+ 0:0:\\056\n')
683
684
685 def fuseRenameTestHelper(mounttmp):
686     class Test(unittest.TestCase):
687         def runTest(self):
688             os.mkdir(os.path.join(mounttmp, "testdir"))
689
690             with open(os.path.join(mounttmp, "testdir", "file1.txt"), "w") as f:
691                 f.write("Hello world!")
692
693     Test().runTest()
694
695 class FuseRenameTest(MountTestBase):
696     def runTest(self):
697         collection = arvados.collection.Collection(api_client=self.api)
698         collection.save_new()
699
700         m = self.make_mount(fuse.CollectionDirectory)
701         with llfuse.lock:
702             m.new_collection(collection.api_response(), collection)
703         self.assertTrue(m.writable())
704
705         self.pool.apply(fuseRenameTestHelper, (self.mounttmp,))
706
707         # Starting manifest
708         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
709         self.assertRegexpMatches(collection2["manifest_text"],
710             r'\./testdir 86fb269d190d2c85f6e0468ceca42a20\+12\+A\S+ 0:12:file1\.txt$')
711
712         d1 = llfuse.listdir(os.path.join(self.mounttmp))
713         self.assertEqual(["testdir"], d1)
714         d1 = llfuse.listdir(os.path.join(self.mounttmp, "testdir"))
715         self.assertEqual(["file1.txt"], d1)
716
717         os.rename(os.path.join(self.mounttmp, "testdir"), os.path.join(self.mounttmp, "testdir2"))
718
719         d1 = llfuse.listdir(os.path.join(self.mounttmp))
720         self.assertEqual(["testdir2"], sorted(d1))
721         d1 = llfuse.listdir(os.path.join(self.mounttmp, "testdir2"))
722         self.assertEqual(["file1.txt"], d1)
723
724         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
725         self.assertRegexpMatches(collection2["manifest_text"],
726             r'\./testdir2 86fb269d190d2c85f6e0468ceca42a20\+12\+A\S+ 0:12:file1\.txt$')
727
728
729 class FuseUpdateFromEventTest(MountTestBase):
730     def runTest(self):
731         collection = arvados.collection.Collection(api_client=self.api)
732         collection.save_new()
733
734         m = self.make_mount(fuse.CollectionDirectory)
735         with llfuse.lock:
736             m.new_collection(collection.api_response(), collection)
737
738         self.operations.listen_for_events()
739
740         d1 = llfuse.listdir(os.path.join(self.mounttmp))
741         self.assertEqual([], sorted(d1))
742
743         with arvados.collection.Collection(collection.manifest_locator(), api_client=self.api) as collection2:
744             with collection2.open("file1.txt", "w") as f:
745                 f.write("foo")
746
747         for attempt in AssertWithTimeout(10):
748             attempt(self.assertEqual, ["file1.txt"], llfuse.listdir(os.path.join(self.mounttmp)))
749
750
751 class FuseDeleteProjectEventTest(MountTestBase):
752     def runTest(self):
753
754         aproject = self.api.groups().create(body={
755             "name": "aproject",
756             "group_class": "project"
757         }).execute()
758
759         bproject = self.api.groups().create(body={
760             "name": "bproject",
761             "group_class": "project",
762             "owner_uuid": aproject["uuid"]
763         }).execute()
764
765         self.make_mount(fuse.ProjectDirectory,
766                         project_object=self.api.users().current().execute())
767
768         self.operations.listen_for_events()
769
770         d1 = llfuse.listdir(os.path.join(self.mounttmp, "aproject"))
771         self.assertEqual(["bproject"], sorted(d1))
772
773         self.api.groups().delete(uuid=bproject["uuid"]).execute()
774
775         for attempt in AssertWithTimeout(10):
776             attempt(self.assertEqual, [], llfuse.listdir(os.path.join(self.mounttmp, "aproject")))
777
778
779 def fuseFileConflictTestHelper(mounttmp):
780     class Test(unittest.TestCase):
781         def runTest(self):
782             with open(os.path.join(mounttmp, "file1.txt"), "w") as f:
783                 f.write("bar")
784
785             d1 = sorted(llfuse.listdir(os.path.join(mounttmp)))
786             self.assertEqual(len(d1), 2)
787
788             with open(os.path.join(mounttmp, "file1.txt"), "r") as f:
789                 self.assertEqual(f.read(), "bar")
790
791             self.assertRegexpMatches(d1[1],
792                 r'file1\.txt~\d\d\d\d\d\d\d\d-\d\d\d\d\d\d~conflict~')
793
794             with open(os.path.join(mounttmp, d1[1]), "r") as f:
795                 self.assertEqual(f.read(), "foo")
796
797     Test().runTest()
798
799 class FuseFileConflictTest(MountTestBase):
800     def runTest(self):
801         collection = arvados.collection.Collection(api_client=self.api)
802         collection.save_new()
803
804         m = self.make_mount(fuse.CollectionDirectory)
805         with llfuse.lock:
806             m.new_collection(collection.api_response(), collection)
807
808         d1 = llfuse.listdir(os.path.join(self.mounttmp))
809         self.assertEqual([], sorted(d1))
810
811         with arvados.collection.Collection(collection.manifest_locator(), api_client=self.api) as collection2:
812             with collection2.open("file1.txt", "w") as f:
813                 f.write("foo")
814
815         # See note in MountTestBase.setUp
816         self.pool.apply(fuseFileConflictTestHelper, (self.mounttmp,))
817
818
819 def fuseUnlinkOpenFileTest(mounttmp):
820     class Test(unittest.TestCase):
821         def runTest(self):
822             with open(os.path.join(mounttmp, "file1.txt"), "w+") as f:
823                 f.write("foo")
824
825                 d1 = llfuse.listdir(os.path.join(mounttmp))
826                 self.assertEqual(["file1.txt"], sorted(d1))
827
828                 os.remove(os.path.join(mounttmp, "file1.txt"))
829
830                 d1 = llfuse.listdir(os.path.join(mounttmp))
831                 self.assertEqual([], sorted(d1))
832
833                 f.seek(0)
834                 self.assertEqual(f.read(), "foo")
835                 f.write("bar")
836
837                 f.seek(0)
838                 self.assertEqual(f.read(), "foobar")
839
840     Test().runTest()
841
842 class FuseUnlinkOpenFileTest(MountTestBase):
843     def runTest(self):
844         collection = arvados.collection.Collection(api_client=self.api)
845         collection.save_new()
846
847         m = self.make_mount(fuse.CollectionDirectory)
848         with llfuse.lock:
849             m.new_collection(collection.api_response(), collection)
850
851         # See note in MountTestBase.setUp
852         self.pool.apply(fuseUnlinkOpenFileTest, (self.mounttmp,))
853
854         self.assertEqual(collection.manifest_text(), "")
855
856
857 def fuseMvFileBetweenCollectionsTest1(mounttmp, uuid1, uuid2):
858     class Test(unittest.TestCase):
859         def runTest(self):
860             with open(os.path.join(mounttmp, uuid1, "file1.txt"), "w") as f:
861                 f.write("Hello world!")
862
863             d1 = os.listdir(os.path.join(mounttmp, uuid1))
864             self.assertEqual(["file1.txt"], sorted(d1))
865             d1 = os.listdir(os.path.join(mounttmp, uuid2))
866             self.assertEqual([], sorted(d1))
867
868     Test().runTest()
869
870 def fuseMvFileBetweenCollectionsTest2(mounttmp, uuid1, uuid2):
871     class Test(unittest.TestCase):
872         def runTest(self):
873             os.rename(os.path.join(mounttmp, uuid1, "file1.txt"), os.path.join(mounttmp, uuid2, "file2.txt"))
874
875             d1 = os.listdir(os.path.join(mounttmp, uuid1))
876             self.assertEqual([], sorted(d1))
877             d1 = os.listdir(os.path.join(mounttmp, uuid2))
878             self.assertEqual(["file2.txt"], sorted(d1))
879
880     Test().runTest()
881
882 class FuseMvFileBetweenCollectionsTest(MountTestBase):
883     def runTest(self):
884         collection1 = arvados.collection.Collection(api_client=self.api)
885         collection1.save_new()
886
887         collection2 = arvados.collection.Collection(api_client=self.api)
888         collection2.save_new()
889
890         m = self.make_mount(fuse.MagicDirectory)
891
892         # See note in MountTestBase.setUp
893         self.pool.apply(fuseMvFileBetweenCollectionsTest1, (self.mounttmp,
894                                                   collection1.manifest_locator(),
895                                                   collection2.manifest_locator()))
896
897         collection1.update()
898         collection2.update()
899
900         self.assertRegexpMatches(collection1.manifest_text(), r"\. 86fb269d190d2c85f6e0468ceca42a20\+12\+A\S+ 0:12:file1\.txt$")
901         self.assertEqual(collection2.manifest_text(), "")
902
903         self.pool.apply(fuseMvFileBetweenCollectionsTest2, (self.mounttmp,
904                                                   collection1.manifest_locator(),
905                                                   collection2.manifest_locator()))
906
907         collection1.update()
908         collection2.update()
909
910         self.assertEqual(collection1.manifest_text(), "")
911         self.assertRegexpMatches(collection2.manifest_text(), r"\. 86fb269d190d2c85f6e0468ceca42a20\+12\+A\S+ 0:12:file2\.txt$")
912
913         collection1.stop_threads()
914         collection2.stop_threads()
915
916
917 def fuseMvDirBetweenCollectionsTest1(mounttmp, uuid1, uuid2):
918     class Test(unittest.TestCase):
919         def runTest(self):
920             os.mkdir(os.path.join(mounttmp, uuid1, "testdir"))
921             with open(os.path.join(mounttmp, uuid1, "testdir", "file1.txt"), "w") as f:
922                 f.write("Hello world!")
923
924             d1 = os.listdir(os.path.join(mounttmp, uuid1))
925             self.assertEqual(["testdir"], sorted(d1))
926             d1 = os.listdir(os.path.join(mounttmp, uuid1, "testdir"))
927             self.assertEqual(["file1.txt"], sorted(d1))
928
929             d1 = os.listdir(os.path.join(mounttmp, uuid2))
930             self.assertEqual([], sorted(d1))
931
932     Test().runTest()
933
934
935 def fuseMvDirBetweenCollectionsTest2(mounttmp, uuid1, uuid2):
936     class Test(unittest.TestCase):
937         def runTest(self):
938             os.rename(os.path.join(mounttmp, uuid1, "testdir"), os.path.join(mounttmp, uuid2, "testdir2"))
939
940             d1 = os.listdir(os.path.join(mounttmp, uuid1))
941             self.assertEqual([], sorted(d1))
942
943             d1 = os.listdir(os.path.join(mounttmp, uuid2))
944             self.assertEqual(["testdir2"], sorted(d1))
945             d1 = os.listdir(os.path.join(mounttmp, uuid2, "testdir2"))
946             self.assertEqual(["file1.txt"], sorted(d1))
947
948             with open(os.path.join(mounttmp, uuid2, "testdir2", "file1.txt"), "r") as f:
949                 self.assertEqual(f.read(), "Hello world!")
950
951     Test().runTest()
952
953 class FuseMvDirBetweenCollectionsTest(MountTestBase):
954     def runTest(self):
955         collection1 = arvados.collection.Collection(api_client=self.api)
956         collection1.save_new()
957
958         collection2 = arvados.collection.Collection(api_client=self.api)
959         collection2.save_new()
960
961         m = self.make_mount(fuse.MagicDirectory)
962
963         # See note in MountTestBase.setUp
964         self.pool.apply(fuseMvDirBetweenCollectionsTest1, (self.mounttmp,
965                                                   collection1.manifest_locator(),
966                                                   collection2.manifest_locator()))
967
968         collection1.update()
969         collection2.update()
970
971         self.assertRegexpMatches(collection1.manifest_text(), r"\./testdir 86fb269d190d2c85f6e0468ceca42a20\+12\+A\S+ 0:12:file1\.txt$")
972         self.assertEqual(collection2.manifest_text(), "")
973
974         self.pool.apply(fuseMvDirBetweenCollectionsTest2, (self.mounttmp,
975                                                   collection1.manifest_locator(),
976                                                   collection2.manifest_locator()))
977
978         collection1.update()
979         collection2.update()
980
981         self.assertEqual(collection1.manifest_text(), "")
982         self.assertRegexpMatches(collection2.manifest_text(), r"\./testdir2 86fb269d190d2c85f6e0468ceca42a20\+12\+A\S+ 0:12:file1\.txt$")
983
984         collection1.stop_threads()
985         collection2.stop_threads()
986
987 def fuseProjectMkdirTestHelper1(mounttmp):
988     class Test(unittest.TestCase):
989         def runTest(self):
990             os.mkdir(os.path.join(mounttmp, "testcollection"))
991             with self.assertRaises(OSError):
992                 os.mkdir(os.path.join(mounttmp, "testcollection"))
993     Test().runTest()
994
995 def fuseProjectMkdirTestHelper2(mounttmp):
996     class Test(unittest.TestCase):
997         def runTest(self):
998             with open(os.path.join(mounttmp, "testcollection", "file1.txt"), "w") as f:
999                 f.write("Hello world!")
1000             with self.assertRaises(OSError):
1001                 os.rmdir(os.path.join(mounttmp, "testcollection"))
1002             os.remove(os.path.join(mounttmp, "testcollection", "file1.txt"))
1003             with self.assertRaises(OSError):
1004                 os.remove(os.path.join(mounttmp, "testcollection"))
1005             os.rmdir(os.path.join(mounttmp, "testcollection"))
1006     Test().runTest()
1007
1008 class FuseProjectMkdirRmdirTest(MountTestBase):
1009     def runTest(self):
1010         self.make_mount(fuse.ProjectDirectory,
1011                         project_object=self.api.users().current().execute())
1012
1013         d1 = llfuse.listdir(self.mounttmp)
1014         self.assertNotIn('testcollection', d1)
1015
1016         self.pool.apply(fuseProjectMkdirTestHelper1, (self.mounttmp,))
1017
1018         d1 = llfuse.listdir(self.mounttmp)
1019         self.assertIn('testcollection', d1)
1020
1021         self.pool.apply(fuseProjectMkdirTestHelper2, (self.mounttmp,))
1022
1023         d1 = llfuse.listdir(self.mounttmp)
1024         self.assertNotIn('testcollection', d1)
1025
1026
1027 def fuseProjectMvTestHelper1(mounttmp):
1028     class Test(unittest.TestCase):
1029         def runTest(self):
1030             d1 = llfuse.listdir(mounttmp)
1031             self.assertNotIn('testcollection', d1)
1032
1033             os.mkdir(os.path.join(mounttmp, "testcollection"))
1034
1035             d1 = llfuse.listdir(mounttmp)
1036             self.assertIn('testcollection', d1)
1037
1038             with self.assertRaises(OSError):
1039                 os.rename(os.path.join(mounttmp, "testcollection"), os.path.join(mounttmp, 'Unrestricted public data'))
1040
1041             os.rename(os.path.join(mounttmp, "testcollection"), os.path.join(mounttmp, 'Unrestricted public data', 'testcollection'))
1042
1043             d1 = llfuse.listdir(mounttmp)
1044             self.assertNotIn('testcollection', d1)
1045
1046             d1 = llfuse.listdir(os.path.join(mounttmp, 'Unrestricted public data'))
1047             self.assertIn('testcollection', d1)
1048
1049     Test().runTest()
1050
1051 class FuseProjectMvTest(MountTestBase):
1052     def runTest(self):
1053         self.make_mount(fuse.ProjectDirectory,
1054                         project_object=self.api.users().current().execute())
1055
1056         self.pool.apply(fuseProjectMvTestHelper1, (self.mounttmp,))
1057
1058
1059 def fuseFsyncTestHelper(mounttmp, k):
1060     class Test(unittest.TestCase):
1061         def runTest(self):
1062             fd = os.open(os.path.join(mounttmp, k), os.O_RDONLY)
1063             os.fsync(fd)
1064             os.close(fd)
1065
1066     Test().runTest()
1067
1068 class FuseFsyncTest(FuseMagicTest):
1069     def runTest(self):
1070         self.make_mount(fuse.MagicDirectory)
1071         self.pool.apply(fuseFsyncTestHelper, (self.mounttmp, self.testcollection))
1072
1073
1074 class MagicDirApiError(FuseMagicTest):
1075     def setUp(self):
1076         api = mock.MagicMock()
1077         super(MagicDirApiError, self).setUp(api=api)
1078         api.collections().get().execute.side_effect = iter([
1079             Exception('API fail'),
1080             {
1081                 "manifest_text": self.test_manifest,
1082                 "portable_data_hash": self.test_manifest_pdh,
1083             },
1084         ])
1085         api.keep.get.side_effect = Exception('Keep fail')
1086
1087     def runTest(self):
1088         self.make_mount(fuse.MagicDirectory)
1089
1090         self.operations.inodes.inode_cache.cap = 1
1091         self.operations.inodes.inode_cache.min_entries = 2
1092
1093         with self.assertRaises(OSError):
1094             llfuse.listdir(os.path.join(self.mounttmp, self.testcollection))
1095
1096         llfuse.listdir(os.path.join(self.mounttmp, self.testcollection))
1097
1098
1099 class FuseUnitTest(unittest.TestCase):
1100     def test_sanitize_filename(self):
1101         acceptable = [
1102             "foo.txt",
1103             ".foo",
1104             "..foo",
1105             "...",
1106             "foo...",
1107             "foo..",
1108             "foo.",
1109             "-",
1110             "\x01\x02\x03",
1111             ]
1112         unacceptable = [
1113             "f\00",
1114             "\00\00",
1115             "/foo",
1116             "foo/",
1117             "//",
1118             ]
1119         for f in acceptable:
1120             self.assertEqual(f, fuse.sanitize_filename(f))
1121         for f in unacceptable:
1122             self.assertNotEqual(f, fuse.sanitize_filename(f))
1123             # The sanitized filename should be the same length, though.
1124             self.assertEqual(len(f), len(fuse.sanitize_filename(f)))
1125         # Special cases
1126         self.assertEqual("_", fuse.sanitize_filename(""))
1127         self.assertEqual("_", fuse.sanitize_filename("."))
1128         self.assertEqual("__", fuse.sanitize_filename(".."))
1129
1130
1131 class FuseMagicTestPDHOnly(MountTestBase):
1132     def setUp(self, api=None):
1133         super(FuseMagicTestPDHOnly, self).setUp(api=api)
1134
1135         cw = arvados.CollectionWriter()
1136
1137         cw.start_new_file('thing1.txt')
1138         cw.write("data 1")
1139
1140         self.testcollection = cw.finish()
1141         self.test_manifest = cw.manifest_text()
1142         created = self.api.collections().create(body={"manifest_text":self.test_manifest}).execute()
1143         self.testcollectionuuid = str(created['uuid'])
1144
1145     def verify_pdh_only(self, pdh_only=False, skip_pdh_only=False):
1146         if skip_pdh_only is True:
1147             self.make_mount(fuse.MagicDirectory)    # in this case, the default by_id applies
1148         else:
1149             self.make_mount(fuse.MagicDirectory, pdh_only=pdh_only)
1150
1151         mount_ls = llfuse.listdir(self.mounttmp)
1152         self.assertIn('README', mount_ls)
1153         self.assertFalse(any(arvados.util.keep_locator_pattern.match(fn) or
1154                              arvados.util.uuid_pattern.match(fn)
1155                              for fn in mount_ls),
1156                          "new FUSE MagicDirectory lists Collection")
1157
1158         # look up using pdh should succeed in all cases
1159         self.assertDirContents(self.testcollection, ['thing1.txt'])
1160         self.assertDirContents(os.path.join('by_id', self.testcollection),
1161                                ['thing1.txt'])
1162         mount_ls = llfuse.listdir(self.mounttmp)
1163         self.assertIn('README', mount_ls)
1164         self.assertIn(self.testcollection, mount_ls)
1165         self.assertIn(self.testcollection,
1166                       llfuse.listdir(os.path.join(self.mounttmp, 'by_id')))
1167
1168         files = {}
1169         files[os.path.join(self.mounttmp, self.testcollection, 'thing1.txt')] = b'data 1'
1170
1171         for k, v in viewitems(files):
1172             with open(os.path.join(self.mounttmp, k), 'rb') as f:
1173                 self.assertEqual(v, f.read())
1174
1175         # look up using uuid should fail when pdh_only is set
1176         if pdh_only is True:
1177             with self.assertRaises(OSError):
1178                 self.assertDirContents(os.path.join('by_id', self.testcollectionuuid),
1179                                ['thing1.txt'])
1180         else:
1181             self.assertDirContents(os.path.join('by_id', self.testcollectionuuid),
1182                                ['thing1.txt'])
1183
1184     def test_with_pdh_only_true(self):
1185         self.verify_pdh_only(pdh_only=True)
1186
1187     def test_with_pdh_only_false(self):
1188         self.verify_pdh_only(pdh_only=False)
1189
1190     def test_with_default_by_id(self):
1191         self.verify_pdh_only(skip_pdh_only=True)