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