]> arthur.barton.de Git - bup.git/blob - lib/bup/t/tvfs.py
vfs: use None for unknown uid/gid
[bup.git] / lib / bup / t / tvfs.py
1
2 from __future__ import absolute_import, print_function
3 from binascii import unhexlify
4 from collections import namedtuple
5 from errno import ELOOP, ENOTDIR
6 from io import BytesIO
7 from os import symlink
8 from random import Random, randint
9 from stat import S_IFDIR, S_IFLNK, S_IFREG, S_ISDIR, S_ISREG
10 from sys import stderr
11 from time import localtime, strftime, tzset
12
13 from wvtest import *
14
15 from bup._helpers import write_random
16 from bup import git, metadata, vfs
17 from bup.compat import environ, fsencode, items, range
18 from bup.git import BUP_CHUNKED
19 from bup.helpers import exc, shstr
20 from bup.metadata import Metadata
21 from bup.repo import LocalRepo
22 from bup.test.vfs import tree_dict
23 from buptest import ex, exo, no_lingering_errors, test_tempdir
24
25 top_dir = b'../../..'
26 bup_tmp = os.path.realpath(b'../../../t/tmp')
27 bup_path = top_dir + b'/bup'
28 start_dir = os.getcwd()
29
30 def ex(cmd, **kwargs):
31     print(shstr(cmd), file=stderr)
32     return exc(cmd, **kwargs)
33
34 @wvtest
35 def test_default_modes():
36     wvpasseq(S_IFREG | 0o644, vfs.default_file_mode)
37     wvpasseq(S_IFDIR | 0o755, vfs.default_dir_mode)
38     wvpasseq(S_IFLNK | 0o755, vfs.default_symlink_mode)
39
40 @wvtest
41 def test_cache_behavior():
42     orig_max = vfs._cache_max_items
43     try:
44         vfs._cache_max_items = 2
45         vfs.clear_cache()
46         wvpasseq({}, vfs._cache)
47         wvpasseq([], vfs._cache_keys)
48         wvfail(vfs._cache_keys)
49         wvexcept(Exception, vfs.cache_notice, b'x', 1)
50         key_0 = b'itm:' + b'\0' * 20
51         key_1 = b'itm:' + b'\1' * 20
52         key_2 = b'itm:' + b'\2' * 20
53         vfs.cache_notice(key_0, b'something')
54         wvpasseq({key_0 : b'something'}, vfs._cache)
55         wvpasseq([key_0], vfs._cache_keys)
56         vfs.cache_notice(key_1, b'something else')
57         wvpasseq({key_0 : b'something', key_1 : b'something else'}, vfs._cache)
58         wvpasseq(frozenset([key_0, key_1]), frozenset(vfs._cache_keys))
59         vfs.cache_notice(key_2, b'and also')
60         wvpasseq(2, len(vfs._cache))
61         wvpass(frozenset(items(vfs._cache))
62                < frozenset(items({key_0 : b'something',
63                                   key_1 : b'something else',
64                                   key_2 : b'and also'})))
65         wvpasseq(2, len(vfs._cache_keys))
66         wvpass(frozenset(vfs._cache_keys) < frozenset([key_0, key_1, key_2]))
67         vfs.clear_cache()
68         wvpasseq({}, vfs._cache)
69         wvpasseq([], vfs._cache_keys)
70     finally:
71         vfs._cache_max_items = orig_max
72         vfs.clear_cache()
73
74 ## The clear_cache() calls below are to make sure that the test starts
75 ## from a known state since at the moment the cache entry for a given
76 ## item (like a commit) can change.  For example, its meta value might
77 ## be promoted from a mode to a Metadata instance once the tree it
78 ## refers to is traversed.
79
80 def run_augment_item_meta_tests(repo,
81                                 file_path, file_size,
82                                 link_path, link_target):
83     _, file_item = vfs.resolve(repo, file_path)[-1]
84     _, link_item = vfs.resolve(repo, link_path, follow=False)[-1]
85     wvpass(isinstance(file_item.meta, Metadata))
86     wvpass(isinstance(link_item.meta, Metadata))
87     # Note: normally, modifying item.meta values is forbidden
88     file_item.meta.size = file_item.meta.size or vfs.item_size(repo, file_item)
89     link_item.meta.size = link_item.meta.size or vfs.item_size(repo, link_item)
90
91     ## Ensure a fully populated item is left alone
92     augmented = vfs.augment_item_meta(repo, file_item)
93     wvpass(augmented is file_item)
94     wvpass(augmented.meta is file_item.meta)
95     augmented = vfs.augment_item_meta(repo, file_item, include_size=True)
96     wvpass(augmented is file_item)
97     wvpass(augmented.meta is file_item.meta)
98
99     ## Ensure a missing size is handled poperly
100     file_item.meta.size = None
101     augmented = vfs.augment_item_meta(repo, file_item)
102     wvpass(augmented is file_item)
103     wvpass(augmented.meta is file_item.meta)
104     augmented = vfs.augment_item_meta(repo, file_item, include_size=True)
105     wvpass(augmented is not file_item)
106     wvpasseq(file_size, augmented.meta.size)
107
108     ## Ensure a meta mode is handled properly
109     mode_item = file_item._replace(meta=vfs.default_file_mode)
110     augmented = vfs.augment_item_meta(repo, mode_item)
111     augmented_w_size = vfs.augment_item_meta(repo, mode_item, include_size=True)
112     for item in (augmented, augmented_w_size):
113         meta = item.meta
114         wvpass(item is not file_item)
115         wvpass(isinstance(meta, Metadata))
116         wvpasseq(vfs.default_file_mode, meta.mode)
117         wvpasseq((None, None, 0, 0, 0),
118                  (meta.uid, meta.gid, meta.atime, meta.mtime, meta.ctime))
119     wvpass(augmented.meta.size is None)
120     wvpasseq(file_size, augmented_w_size.meta.size)
121
122     ## Ensure symlinks are handled properly
123     mode_item = link_item._replace(meta=vfs.default_symlink_mode)
124     augmented = vfs.augment_item_meta(repo, mode_item)
125     wvpass(augmented is not mode_item)
126     wvpass(isinstance(augmented.meta, Metadata))
127     wvpasseq(link_target, augmented.meta.symlink_target)
128     wvpasseq(len(link_target), augmented.meta.size)
129     augmented = vfs.augment_item_meta(repo, mode_item, include_size=True)
130     wvpass(augmented is not mode_item)
131     wvpass(isinstance(augmented.meta, Metadata))
132     wvpasseq(link_target, augmented.meta.symlink_target)
133     wvpasseq(len(link_target), augmented.meta.size)
134
135
136 @wvtest
137 def test_item_mode():
138     with no_lingering_errors():
139         mode = S_IFDIR | 0o755
140         meta = metadata.from_path(b'.')
141         oid = b'\0' * 20
142         wvpasseq(mode, vfs.item_mode(vfs.Item(oid=oid, meta=mode)))
143         wvpasseq(meta.mode, vfs.item_mode(vfs.Item(oid=oid, meta=meta)))
144
145 @wvtest
146 def test_reverse_suffix_duplicates():
147     suffix = lambda x: tuple(vfs._reverse_suffix_duplicates(x))
148     wvpasseq((b'x',), suffix((b'x',)))
149     wvpasseq((b'x', b'y'), suffix((b'x', b'y')))
150     wvpasseq((b'x-1', b'x-0'), suffix((b'x',) * 2))
151     wvpasseq([b'x-%02d' % n for n in reversed(range(11))],
152              list(suffix((b'x',) * 11)))
153     wvpasseq((b'x-1', b'x-0', b'y'), suffix((b'x', b'x', b'y')))
154     wvpasseq((b'x', b'y-1', b'y-0'), suffix((b'x', b'y', b'y')))
155     wvpasseq((b'x', b'y-1', b'y-0', b'z'), suffix((b'x', b'y', b'y', b'z')))
156
157 @wvtest
158 def test_misc():
159     with no_lingering_errors():
160         with test_tempdir(b'bup-tvfs-') as tmpdir:
161             bup_dir = tmpdir + b'/bup'
162             environ[b'GIT_DIR'] = bup_dir
163             environ[b'BUP_DIR'] = bup_dir
164             git.repodir = bup_dir
165             data_path = tmpdir + b'/src'
166             os.mkdir(data_path)
167             with open(data_path + b'/file', 'wb+') as tmpfile:
168                 tmpfile.write(b'canary\n')
169             symlink(b'file', data_path + b'/symlink')
170             ex((bup_path, b'init'))
171             ex((bup_path, b'index', b'-v', data_path))
172             ex((bup_path, b'save', b'-d', b'100000', b'-tvvn', b'test',
173                 b'--strip', data_path))
174             repo = LocalRepo()
175
176             wvstart('readlink')
177             ls_tree = exo((b'git', b'ls-tree', b'test', b'symlink')).out
178             mode, typ, oidx, name = ls_tree.strip().split(None, 3)
179             assert name == b'symlink'
180             link_item = vfs.Item(oid=unhexlify(oidx), meta=int(mode, 8))
181             wvpasseq(b'file', vfs.readlink(repo, link_item))
182
183             ls_tree = exo((b'git', b'ls-tree', b'test', b'file')).out
184             mode, typ, oidx, name = ls_tree.strip().split(None, 3)
185             assert name == b'file'
186             file_item = vfs.Item(oid=unhexlify(oidx), meta=int(mode, 8))
187             wvexcept(Exception, vfs.readlink, repo, file_item)
188
189             wvstart('item_size')
190             wvpasseq(4, vfs.item_size(repo, link_item))
191             wvpasseq(7, vfs.item_size(repo, file_item))
192             meta = metadata.from_path(fsencode(__file__))
193             meta.size = 42
194             fake_item = file_item._replace(meta=meta)
195             wvpasseq(42, vfs.item_size(repo, fake_item))
196
197             _, fakelink_item = vfs.resolve(repo, b'/test/latest', follow=False)[-1]
198             wvpasseq(17, vfs.item_size(repo, fakelink_item))
199
200             wvstart('augment_item_meta')
201             run_augment_item_meta_tests(repo,
202                                         b'/test/latest/file', 7,
203                                         b'/test/latest/symlink', b'file')
204
205             wvstart('copy_item')
206             # FIXME: this caused StopIteration
207             #_, file_item = vfs.resolve(repo, '/file')[-1]
208             _, file_item = vfs.resolve(repo, b'/test/latest/file')[-1]
209             file_copy = vfs.copy_item(file_item)
210             wvpass(file_copy is not file_item)
211             wvpass(file_copy.meta is not file_item.meta)
212             wvpass(isinstance(file_copy, tuple))
213             wvpass(file_item.meta.user)
214             wvpass(file_copy.meta.user)
215             file_copy.meta.user = None
216             wvpass(file_item.meta.user)
217
218 def write_sized_random_content(parent_dir, size, seed):
219     verbose = 0
220     with open(b'%s/%d' % (parent_dir, size), 'wb') as f:
221         write_random(f.fileno(), size, seed, verbose)
222
223 def validate_vfs_streaming_read(repo, item, expected_path, read_sizes):
224     for read_size in read_sizes:
225         with open(expected_path, 'rb') as expected:
226             with vfs.fopen(repo, item) as actual:
227                 ex_buf = expected.read(read_size)
228                 act_buf = actual.read(read_size)
229                 while ex_buf and act_buf:
230                     wvpassge(read_size, len(ex_buf))
231                     wvpassge(read_size, len(act_buf))
232                     wvpasseq(len(ex_buf), len(act_buf))
233                     wvpass(ex_buf == act_buf)
234                     ex_buf = expected.read(read_size)
235                     act_buf = actual.read(read_size)
236                 wvpasseq(b'', ex_buf)
237                 wvpasseq(b'', act_buf)
238
239 def validate_vfs_seeking_read(repo, item, expected_path, read_sizes):
240     def read_act(act_pos):
241         with vfs.fopen(repo, item) as actual:
242             actual.seek(act_pos)
243             wvpasseq(act_pos, actual.tell())
244             act_buf = actual.read(read_size)
245             act_pos += len(act_buf)
246             wvpasseq(act_pos, actual.tell())
247             return act_pos, act_buf
248
249     for read_size in read_sizes:
250         with open(expected_path, 'rb') as expected:
251                 ex_buf = expected.read(read_size)
252                 act_buf = None
253                 act_pos = 0
254                 while ex_buf:
255                     act_pos, act_buf = read_act(act_pos)
256                     wvpassge(read_size, len(ex_buf))
257                     wvpassge(read_size, len(act_buf))
258                     wvpasseq(len(ex_buf), len(act_buf))
259                     wvpass(ex_buf == act_buf)
260                     if not act_buf:
261                         break
262                     ex_buf = expected.read(read_size)
263                 else:  # hit expected eof first
264                     act_pos, act_buf = read_act(act_pos)
265                 wvpasseq(b'', ex_buf)
266                 wvpasseq(b'', act_buf)
267
268 @wvtest
269 def test_read_and_seek():
270     # Write a set of randomly sized files containing random data whose
271     # names are their sizes, and then verify that what we get back
272     # from the vfs when seeking and reading with various block sizes
273     # matches the original content.
274     with no_lingering_errors():
275         with test_tempdir(b'bup-tvfs-read-') as tmpdir:
276             resolve = vfs.resolve
277             bup_dir = tmpdir + b'/bup'
278             environ[b'GIT_DIR'] = bup_dir
279             environ[b'BUP_DIR'] = bup_dir
280             git.repodir = bup_dir
281             repo = LocalRepo()
282             data_path = tmpdir + b'/src'
283             os.mkdir(data_path)
284             seed = randint(-(1 << 31), (1 << 31) - 1)
285             rand = Random()
286             rand.seed(seed)
287             print('test_read seed:', seed, file=sys.stderr)
288             max_size = 2 * 1024 * 1024
289             sizes = set((rand.randint(1, max_size) for _ in range(5)))
290             sizes.add(1)
291             sizes.add(max_size)
292             for size in sizes:
293                 write_sized_random_content(data_path, size, seed)
294             ex((bup_path, b'init'))
295             ex((bup_path, b'index', b'-v', data_path))
296             ex((bup_path, b'save', b'-d', b'100000', b'-tvvn', b'test',
297                 b'--strip', data_path))
298             read_sizes = set((rand.randint(1, max_size) for _ in range(10)))
299             sizes.add(1)
300             sizes.add(max_size)
301             print('test_read src sizes:', sizes, file=sys.stderr)
302             print('test_read read sizes:', read_sizes, file=sys.stderr)
303             for size in sizes:
304                 res = resolve(repo, b'/test/latest/' + str(size).encode('ascii'))
305                 _, item = res[-1]
306                 wvpasseq(size, vfs.item_size(repo, res[-1][1]))
307                 validate_vfs_streaming_read(repo, item,
308                                             b'%s/%d' % (data_path, size),
309                                             read_sizes)
310                 validate_vfs_seeking_read(repo, item,
311                                           b'%s/%d' % (data_path, size),
312                                           read_sizes)
313
314 @wvtest
315 def test_contents_with_mismatched_bupm_git_ordering():
316     with no_lingering_errors():
317         with test_tempdir(b'bup-tvfs-') as tmpdir:
318             bup_dir = tmpdir + b'/bup'
319             environ[b'GIT_DIR'] = bup_dir
320             environ[b'BUP_DIR'] = bup_dir
321             git.repodir = bup_dir
322             data_path = tmpdir + b'/src'
323             os.mkdir(data_path)
324             os.mkdir(data_path + b'/foo')
325             with open(data_path + b'/foo.', 'wb+') as tmpfile:
326                 tmpfile.write(b'canary\n')
327             ex((bup_path, b'init'))
328             ex((bup_path, b'index', b'-v', data_path))
329             save_utc = 100000
330             save_name = strftime('%Y-%m-%d-%H%M%S', localtime(save_utc)).encode('ascii')
331             ex((bup_path, b'save', b'-tvvn', b'test', b'-d', b'%d' % save_utc,
332                 b'--strip', data_path))
333             repo = LocalRepo()
334             tip_sref = exo((b'git', b'show-ref', b'refs/heads/test')).out
335             tip_oidx = tip_sref.strip().split()[0]
336             tip_tree_oidx = exo((b'git', b'log', b'--pretty=%T', b'-n1',
337                                  tip_oidx)).out.strip()
338             tip_tree_oid = unhexlify(tip_tree_oidx)
339             tip_tree = tree_dict(repo, tip_tree_oid)
340
341             name, item = vfs.resolve(repo, b'/test/latest')[2]
342             wvpasseq(save_name, name)
343             expected = frozenset((x.name, vfs.Item(oid=x.oid, meta=x.meta))
344                                  for x in (tip_tree[name]
345                                            for name in (b'.', b'foo', b'foo.')))
346             contents = tuple(vfs.contents(repo, item))
347             wvpasseq(expected, frozenset(contents))
348             # Spot check, in case tree_dict shares too much code with the vfs
349             name, item = next(((n, i) for n, i in contents if n == b'foo'))
350             wvpass(S_ISDIR(item.meta))
351             name, item = next(((n, i) for n, i in contents if n == b'foo.'))
352             wvpass(S_ISREG(item.meta.mode))
353
354 @wvtest
355 def test_duplicate_save_dates():
356     with no_lingering_errors():
357         with test_tempdir(b'bup-tvfs-') as tmpdir:
358             bup_dir = tmpdir + b'/bup'
359             environ[b'GIT_DIR'] = bup_dir
360             environ[b'BUP_DIR'] = bup_dir
361             environ[b'TZ'] = b'UTC'
362             tzset()
363             git.repodir = bup_dir
364             data_path = tmpdir + b'/src'
365             os.mkdir(data_path)
366             with open(data_path + b'/file', 'wb+') as tmpfile:
367                 tmpfile.write(b'canary\n')
368             ex((b'env',))
369             ex((bup_path, b'init'))
370             ex((bup_path, b'index', b'-v', data_path))
371             for i in range(11):
372                 ex((bup_path, b'save', b'-d', b'100000', b'-n', b'test',
373                     data_path))
374             repo = LocalRepo()
375             res = vfs.resolve(repo, b'/test')
376             wvpasseq(2, len(res))
377             name, revlist = res[-1]
378             wvpasseq(b'test', name)
379             wvpasseq((b'.',
380                       b'1970-01-02-034640-00',
381                       b'1970-01-02-034640-01',
382                       b'1970-01-02-034640-02',
383                       b'1970-01-02-034640-03',
384                       b'1970-01-02-034640-04',
385                       b'1970-01-02-034640-05',
386                       b'1970-01-02-034640-06',
387                       b'1970-01-02-034640-07',
388                       b'1970-01-02-034640-08',
389                       b'1970-01-02-034640-09',
390                       b'1970-01-02-034640-10',
391                       b'latest'),
392                      tuple(sorted(x[0] for x in vfs.contents(repo, revlist))))
393
394 @wvtest
395 def test_item_read_write():
396     with no_lingering_errors():
397         x = vfs.Root(meta=13)
398         stream = BytesIO()
399         vfs.write_item(stream, x)
400         print('stream:', repr(stream.getvalue()), stream.tell(), file=sys.stderr)
401         stream.seek(0)
402         wvpasseq(x, vfs.read_item(stream))