2 from __future__ import absolute_import, print_function
3 from binascii import unhexlify
4 from collections import namedtuple
5 from errno import ELOOP, ENOTDIR
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
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 buptest import ex, exo, no_lingering_errors, test_tempdir
23 from buptest.vfs import tree_dict
26 bup_path = top_dir + b'/bup'
27 start_dir = os.getcwd()
29 def ex(cmd, **kwargs):
30 print(shstr(cmd), file=stderr)
31 return exc(cmd, **kwargs)
34 def test_default_modes():
35 wvpasseq(S_IFREG | 0o644, vfs.default_file_mode)
36 wvpasseq(S_IFDIR | 0o755, vfs.default_dir_mode)
37 wvpasseq(S_IFLNK | 0o755, vfs.default_symlink_mode)
40 def test_cache_behavior():
41 orig_max = vfs._cache_max_items
43 vfs._cache_max_items = 2
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]))
67 wvpasseq({}, vfs._cache)
68 wvpasseq([], vfs._cache_keys)
70 vfs._cache_max_items = orig_max
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.
79 def run_augment_item_meta_tests(repo,
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)
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)
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)
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):
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)
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)
136 def test_item_mode():
137 with no_lingering_errors():
138 mode = S_IFDIR | 0o755
139 meta = metadata.from_path(b'.')
141 wvpasseq(mode, vfs.item_mode(vfs.Item(oid=oid, meta=mode)))
142 wvpasseq(meta.mode, vfs.item_mode(vfs.Item(oid=oid, meta=meta)))
145 def test_reverse_suffix_duplicates():
146 suffix = lambda x: tuple(vfs._reverse_suffix_duplicates(x))
147 wvpasseq((b'x',), suffix((b'x',)))
148 wvpasseq((b'x', b'y'), suffix((b'x', b'y')))
149 wvpasseq((b'x-1', b'x-0'), suffix((b'x',) * 2))
150 wvpasseq([b'x-%02d' % n for n in reversed(range(11))],
151 list(suffix((b'x',) * 11)))
152 wvpasseq((b'x-1', b'x-0', b'y'), suffix((b'x', b'x', b'y')))
153 wvpasseq((b'x', b'y-1', b'y-0'), suffix((b'x', b'y', b'y')))
154 wvpasseq((b'x', b'y-1', b'y-0', b'z'), suffix((b'x', b'y', b'y', b'z')))
158 with no_lingering_errors():
159 with test_tempdir(b'bup-tvfs-') as tmpdir:
160 bup_dir = tmpdir + b'/bup'
161 environ[b'GIT_DIR'] = bup_dir
162 environ[b'BUP_DIR'] = bup_dir
163 git.repodir = bup_dir
164 data_path = tmpdir + b'/src'
166 with open(data_path + b'/file', 'wb+') as tmpfile:
167 tmpfile.write(b'canary\n')
168 symlink(b'file', data_path + b'/symlink')
169 ex((bup_path, b'init'))
170 ex((bup_path, b'index', b'-v', data_path))
171 ex((bup_path, b'save', b'-d', b'100000', b'-tvvn', b'test',
172 b'--strip', data_path))
176 ls_tree = exo((b'git', b'ls-tree', b'test', b'symlink')).out
177 mode, typ, oidx, name = ls_tree.strip().split(None, 3)
178 assert name == b'symlink'
179 link_item = vfs.Item(oid=unhexlify(oidx), meta=int(mode, 8))
180 wvpasseq(b'file', vfs.readlink(repo, link_item))
182 ls_tree = exo((b'git', b'ls-tree', b'test', b'file')).out
183 mode, typ, oidx, name = ls_tree.strip().split(None, 3)
184 assert name == b'file'
185 file_item = vfs.Item(oid=unhexlify(oidx), meta=int(mode, 8))
186 wvexcept(Exception, vfs.readlink, repo, file_item)
189 wvpasseq(4, vfs.item_size(repo, link_item))
190 wvpasseq(7, vfs.item_size(repo, file_item))
191 meta = metadata.from_path(fsencode(__file__))
193 fake_item = file_item._replace(meta=meta)
194 wvpasseq(42, vfs.item_size(repo, fake_item))
196 _, fakelink_item = vfs.resolve(repo, b'/test/latest', follow=False)[-1]
197 wvpasseq(17, vfs.item_size(repo, fakelink_item))
199 wvstart('augment_item_meta')
200 run_augment_item_meta_tests(repo,
201 b'/test/latest/file', 7,
202 b'/test/latest/symlink', b'file')
205 # FIXME: this caused StopIteration
206 #_, file_item = vfs.resolve(repo, '/file')[-1]
207 _, file_item = vfs.resolve(repo, b'/test/latest/file')[-1]
208 file_copy = vfs.copy_item(file_item)
209 wvpass(file_copy is not file_item)
210 wvpass(file_copy.meta is not file_item.meta)
211 wvpass(isinstance(file_copy, tuple))
212 wvpass(file_item.meta.user)
213 wvpass(file_copy.meta.user)
214 file_copy.meta.user = None
215 wvpass(file_item.meta.user)
217 def write_sized_random_content(parent_dir, size, seed):
219 with open(b'%s/%d' % (parent_dir, size), 'wb') as f:
220 write_random(f.fileno(), size, seed, verbose)
222 def validate_vfs_streaming_read(repo, item, expected_path, read_sizes):
223 for read_size in read_sizes:
224 with open(expected_path, 'rb') as expected:
225 with vfs.fopen(repo, item) as actual:
226 ex_buf = expected.read(read_size)
227 act_buf = actual.read(read_size)
228 while ex_buf and act_buf:
229 wvpassge(read_size, len(ex_buf))
230 wvpassge(read_size, len(act_buf))
231 wvpasseq(len(ex_buf), len(act_buf))
232 wvpass(ex_buf == act_buf)
233 ex_buf = expected.read(read_size)
234 act_buf = actual.read(read_size)
235 wvpasseq(b'', ex_buf)
236 wvpasseq(b'', act_buf)
238 def validate_vfs_seeking_read(repo, item, expected_path, read_sizes):
239 def read_act(act_pos):
240 with vfs.fopen(repo, item) as actual:
242 wvpasseq(act_pos, actual.tell())
243 act_buf = actual.read(read_size)
244 act_pos += len(act_buf)
245 wvpasseq(act_pos, actual.tell())
246 return act_pos, act_buf
248 for read_size in read_sizes:
249 with open(expected_path, 'rb') as expected:
250 ex_buf = expected.read(read_size)
254 act_pos, act_buf = read_act(act_pos)
255 wvpassge(read_size, len(ex_buf))
256 wvpassge(read_size, len(act_buf))
257 wvpasseq(len(ex_buf), len(act_buf))
258 wvpass(ex_buf == act_buf)
261 ex_buf = expected.read(read_size)
262 else: # hit expected eof first
263 act_pos, act_buf = read_act(act_pos)
264 wvpasseq(b'', ex_buf)
265 wvpasseq(b'', act_buf)
268 def test_read_and_seek():
269 # Write a set of randomly sized files containing random data whose
270 # names are their sizes, and then verify that what we get back
271 # from the vfs when seeking and reading with various block sizes
272 # matches the original content.
273 with no_lingering_errors():
274 with test_tempdir(b'bup-tvfs-read-') as tmpdir:
275 resolve = vfs.resolve
276 bup_dir = tmpdir + b'/bup'
277 environ[b'GIT_DIR'] = bup_dir
278 environ[b'BUP_DIR'] = bup_dir
279 git.repodir = bup_dir
281 data_path = tmpdir + b'/src'
283 seed = randint(-(1 << 31), (1 << 31) - 1)
286 print('test_read seed:', seed, file=sys.stderr)
287 max_size = 2 * 1024 * 1024
288 sizes = set((rand.randint(1, max_size) for _ in range(5)))
292 write_sized_random_content(data_path, size, seed)
293 ex((bup_path, b'init'))
294 ex((bup_path, b'index', b'-v', data_path))
295 ex((bup_path, b'save', b'-d', b'100000', b'-tvvn', b'test',
296 b'--strip', data_path))
297 read_sizes = set((rand.randint(1, max_size) for _ in range(10)))
300 print('test_read src sizes:', sizes, file=sys.stderr)
301 print('test_read read sizes:', read_sizes, file=sys.stderr)
303 res = resolve(repo, b'/test/latest/' + str(size).encode('ascii'))
305 wvpasseq(size, vfs.item_size(repo, res[-1][1]))
306 validate_vfs_streaming_read(repo, item,
307 b'%s/%d' % (data_path, size),
309 validate_vfs_seeking_read(repo, item,
310 b'%s/%d' % (data_path, size),
314 def test_contents_with_mismatched_bupm_git_ordering():
315 with no_lingering_errors():
316 with test_tempdir(b'bup-tvfs-') as tmpdir:
317 bup_dir = tmpdir + b'/bup'
318 environ[b'GIT_DIR'] = bup_dir
319 environ[b'BUP_DIR'] = bup_dir
320 git.repodir = bup_dir
321 data_path = tmpdir + b'/src'
323 os.mkdir(data_path + b'/foo')
324 with open(data_path + b'/foo.', 'wb+') as tmpfile:
325 tmpfile.write(b'canary\n')
326 ex((bup_path, b'init'))
327 ex((bup_path, b'index', b'-v', data_path))
329 save_name = strftime('%Y-%m-%d-%H%M%S', localtime(save_utc)).encode('ascii')
330 ex((bup_path, b'save', b'-tvvn', b'test', b'-d', b'%d' % save_utc,
331 b'--strip', data_path))
333 tip_sref = exo((b'git', b'show-ref', b'refs/heads/test')).out
334 tip_oidx = tip_sref.strip().split()[0]
335 tip_tree_oidx = exo((b'git', b'log', b'--pretty=%T', b'-n1',
336 tip_oidx)).out.strip()
337 tip_tree_oid = unhexlify(tip_tree_oidx)
338 tip_tree = tree_dict(repo, tip_tree_oid)
340 name, item = vfs.resolve(repo, b'/test/latest')[2]
341 wvpasseq(save_name, name)
342 expected = frozenset((x.name, vfs.Item(oid=x.oid, meta=x.meta))
343 for x in (tip_tree[name]
344 for name in (b'.', b'foo', b'foo.')))
345 contents = tuple(vfs.contents(repo, item))
346 wvpasseq(expected, frozenset(contents))
347 # Spot check, in case tree_dict shares too much code with the vfs
348 name, item = next(((n, i) for n, i in contents if n == b'foo'))
349 wvpass(S_ISDIR(item.meta))
350 name, item = next(((n, i) for n, i in contents if n == b'foo.'))
351 wvpass(S_ISREG(item.meta.mode))
354 def test_duplicate_save_dates():
355 with no_lingering_errors():
356 with test_tempdir(b'bup-tvfs-') as tmpdir:
357 bup_dir = tmpdir + b'/bup'
358 environ[b'GIT_DIR'] = bup_dir
359 environ[b'BUP_DIR'] = bup_dir
360 environ[b'TZ'] = b'UTC'
362 git.repodir = bup_dir
363 data_path = tmpdir + b'/src'
365 with open(data_path + b'/file', 'wb+') as tmpfile:
366 tmpfile.write(b'canary\n')
368 ex((bup_path, b'init'))
369 ex((bup_path, b'index', b'-v', data_path))
371 ex((bup_path, b'save', b'-d', b'100000', b'-n', b'test',
374 res = vfs.resolve(repo, b'/test')
375 wvpasseq(2, len(res))
376 name, revlist = res[-1]
377 wvpasseq(b'test', name)
379 b'1970-01-02-034640-00',
380 b'1970-01-02-034640-01',
381 b'1970-01-02-034640-02',
382 b'1970-01-02-034640-03',
383 b'1970-01-02-034640-04',
384 b'1970-01-02-034640-05',
385 b'1970-01-02-034640-06',
386 b'1970-01-02-034640-07',
387 b'1970-01-02-034640-08',
388 b'1970-01-02-034640-09',
389 b'1970-01-02-034640-10',
391 tuple(sorted(x[0] for x in vfs.contents(repo, revlist))))
394 def test_item_read_write():
395 with no_lingering_errors():
396 x = vfs.Root(meta=13)
398 vfs.write_item(stream, x)
399 print('stream:', repr(stream.getvalue()), stream.tell(), file=sys.stderr)
401 wvpasseq(x, vfs.read_item(stream))