]> arthur.barton.de Git - bup.git/blobdiff - lib/bup/t/tresolve.py
tresolve: accommodate python 3 and test there
[bup.git] / lib / bup / t / tresolve.py
index 533bf8750b079caf23a315bc691c1f54ec8ae34d..f2e29d4bc5d5a85541b44e188f40fe9f2f9a84c6 100644 (file)
@@ -1,23 +1,23 @@
 
 from __future__ import absolute_import, print_function
+from binascii import unhexlify
 from errno import ELOOP, ENOTDIR
-from os import environ, symlink
+from os import symlink
 from stat import S_IFDIR
 from sys import stderr
 from time import localtime, strftime
 
 from wvtest import *
 
-from bup import git, vfs
+from bup import git, path, vfs
+from bup.compat import environ
+from bup.io import path_msg
 from bup.metadata import Metadata
-from bup.repo import LocalRepo
+from bup.repo import LocalRepo, RemoteRepo
 from bup.test.vfs import tree_dict
 from buptest import ex, exo, no_lingering_errors, test_tempdir
 
-top_dir = '../../..'
-bup_tmp = os.path.realpath('../../../t/tmp')
-bup_path = top_dir + '/bup'
-start_dir = os.getcwd()
+bup_path = path.exe()
 
 ## The clear_cache() calls below are to make sure that the test starts
 ## from a known state since at the moment the cache entry for a given
@@ -25,294 +25,310 @@ start_dir = os.getcwd()
 ## be promoted from a mode to a Metadata instance once the tree it
 ## refers to is traversed.
 
-@wvtest
-def test_resolve():
+def prep_and_test_repo(name, create_repo, test_repo):
     with no_lingering_errors():
-        with test_tempdir('bup-tvfs-') as tmpdir:
-            resolve = vfs.resolve
-            bup_dir = tmpdir + '/bup'
-            environ['GIT_DIR'] = bup_dir
-            environ['BUP_DIR'] = bup_dir
+        with test_tempdir(b'bup-t' + name) as tmpdir:
+            bup_dir = tmpdir + b'/bup'
+            environ[b'GIT_DIR'] = bup_dir
+            environ[b'BUP_DIR'] = bup_dir
+            ex((bup_path, b'init'))
             git.repodir = bup_dir
-            data_path = tmpdir + '/src'
-            save_time = 100000
-            save_time_str = strftime('%Y-%m-%d-%H%M%S', localtime(save_time))
-            os.mkdir(data_path)
-            os.mkdir(data_path + '/dir')
-            with open(data_path + '/file', 'w+') as tmpfile:
-                print('canary', file=tmpfile)
-            symlink('file', data_path + '/file-symlink')
-            symlink('dir', data_path + '/dir-symlink')
-            symlink('not-there', data_path + '/bad-symlink')
-            ex((bup_path, 'init'))
-            ex((bup_path, 'index', '-v', data_path))
-            ex((bup_path, 'save', '-d', str(save_time), '-tvvn', 'test',
-                '--strip', data_path))
-            ex((bup_path, 'tag', 'test-tag', 'test'))
-            repo = LocalRepo()
+            with create_repo(bup_dir) as repo:
+                test_repo(repo, tmpdir)
 
-            tip_hash = exo(('git', 'show-ref', 'refs/heads/test'))[0]
-            tip_oidx = tip_hash.strip().split()[0]
-            tip_oid = tip_oidx.decode('hex')
-            tip_tree_oidx = exo(('git', 'log', '--pretty=%T', '-n1',
-                                 tip_oidx))[0].strip()
-            tip_tree_oid = tip_tree_oidx.decode('hex')
-            tip_tree = tree_dict(repo, tip_tree_oid)
-            test_revlist_w_meta = vfs.RevList(meta=tip_tree['.'].meta,
-                                              oid=tip_oid)
-            expected_latest_item = vfs.Commit(meta=S_IFDIR | 0o755,
-                                              oid=tip_tree_oid,
-                                              coid=tip_oid)
-            expected_latest_item_w_meta = vfs.Commit(meta=tip_tree['.'].meta,
-                                                     oid=tip_tree_oid,
-                                                     coid=tip_oid)
-            expected_latest_link = vfs.FakeLink(meta=vfs.default_symlink_mode,
-                                                target=save_time_str)
-            expected_test_tag_item = expected_latest_item
+# Currently, we just test through the repos since LocalRepo resolve is
+# just a straight redirection to vfs.resolve.
 
-            wvstart('resolve: /')
-            vfs.clear_cache()
-            res = resolve(repo, '/')
-            wvpasseq(1, len(res))
-            wvpasseq((('', vfs._root),), res)
-            ignore, root_item = res[0]
-            root_content = frozenset(vfs.contents(repo, root_item))
-            wvpasseq(frozenset([('.', root_item),
-                                ('.tag', vfs._tags),
-                                ('test', test_revlist_w_meta)]),
-                     root_content)
-            for path in ('//', '/.', '/./', '/..', '/../',
-                         '/test/latest/dir/../../..',
-                         '/test/latest/dir/../../../',
-                         '/test/latest/dir/../../../.',
-                         '/test/latest/dir/../../..//',
-                         '/test//latest/dir/../../..',
-                         '/test/./latest/dir/../../..',
-                         '/test/././latest/dir/../../..',
-                         '/test/.//./latest/dir/../../..',
-                         '/test//.//.//latest/dir/../../..'
-                         '/test//./latest/dir/../../..'):
-                wvstart('resolve: ' + path)
-                vfs.clear_cache()
-                res = resolve(repo, path)
-                wvpasseq((('', vfs._root),), res)
+def test_resolve(repo, tmpdir):
+        data_path = tmpdir + b'/src'
+        resolve = repo.resolve
+        save_time = 100000
+        save_time_str = strftime('%Y-%m-%d-%H%M%S', localtime(save_time)).encode('ascii')
+        os.mkdir(data_path)
+        os.mkdir(data_path + b'/dir')
+        with open(data_path + b'/file', 'wb+') as tmpfile:
+            tmpfile.write(b'canary\n')
+        symlink(b'file', data_path + b'/file-symlink')
+        symlink(b'dir', data_path + b'/dir-symlink')
+        symlink(b'not-there', data_path + b'/bad-symlink')
+        ex((bup_path, b'index', b'-v', data_path))
+        ex((bup_path, b'save', b'-d', b'%d' % save_time, b'-tvvn', b'test',
+            b'--strip', data_path))
+        ex((bup_path, b'tag', b'test-tag', b'test'))
 
-            wvstart('resolve: /.tag')
-            vfs.clear_cache()
-            res = resolve(repo, '/.tag')
-            wvpasseq(2, len(res))
-            wvpasseq((('', vfs._root), ('.tag', vfs._tags)),
-                     res)
-            ignore, tag_item = res[1]
-            tag_content = frozenset(vfs.contents(repo, tag_item))
-            wvpasseq(frozenset([('.', tag_item),
-                                ('test-tag', expected_test_tag_item)]),
-                     tag_content)
+        tip_hash = exo((b'git', b'show-ref', b'refs/heads/test'))[0]
+        tip_oidx = tip_hash.strip().split()[0]
+        tip_oid = unhexlify(tip_oidx)
+        tip_tree_oidx = exo((b'git', b'log', b'--pretty=%T', b'-n1',
+                             tip_oidx))[0].strip()
+        tip_tree_oid = unhexlify(tip_tree_oidx)
+        tip_tree = tree_dict(repo, tip_tree_oid)
+        test_revlist_w_meta = vfs.RevList(meta=tip_tree[b'.'].meta,
+                                          oid=tip_oid)
+        expected_latest_item = vfs.Commit(meta=S_IFDIR | 0o755,
+                                          oid=tip_tree_oid,
+                                          coid=tip_oid)
+        expected_latest_item_w_meta = vfs.Commit(meta=tip_tree[b'.'].meta,
+                                                 oid=tip_tree_oid,
+                                                 coid=tip_oid)
+        expected_latest_link = vfs.FakeLink(meta=vfs.default_symlink_mode,
+                                            target=save_time_str)
+        expected_test_tag_item = expected_latest_item
 
-            wvstart('resolve: /test')
+        wvstart('resolve: /')
+        vfs.clear_cache()
+        res = resolve(b'/')
+        wvpasseq(1, len(res))
+        wvpasseq(((b'', vfs._root),), res)
+        ignore, root_item = res[0]
+        root_content = frozenset(vfs.contents(repo, root_item))
+        wvpasseq(frozenset([(b'.', root_item),
+                            (b'.tag', vfs._tags),
+                            (b'test', test_revlist_w_meta)]),
+                 root_content)
+        for path in (b'//', b'/.', b'/./', b'/..', b'/../',
+                     b'/test/latest/dir/../../..',
+                     b'/test/latest/dir/../../../',
+                     b'/test/latest/dir/../../../.',
+                     b'/test/latest/dir/../../..//',
+                     b'/test//latest/dir/../../..',
+                     b'/test/./latest/dir/../../..',
+                     b'/test/././latest/dir/../../..',
+                     b'/test/.//./latest/dir/../../..',
+                     b'/test//.//.//latest/dir/../../..'
+                     b'/test//./latest/dir/../../..'):
+            wvstart('resolve: ' + path_msg(path))
             vfs.clear_cache()
-            res = resolve(repo, '/test')
-            wvpasseq(2, len(res))
-            wvpasseq((('', vfs._root), ('test', test_revlist_w_meta)), res)
-            ignore, test_item = res[1]
-            test_content = frozenset(vfs.contents(repo, test_item))
-            # latest has metadata here due to caching
-            wvpasseq(frozenset([('.', test_revlist_w_meta),
-                                (save_time_str, expected_latest_item_w_meta),
-                                ('latest', expected_latest_link)]),
-                     test_content)
+            res = resolve(path)
+            wvpasseq(((b'', vfs._root),), res)
 
-            wvstart('resolve: /test/latest')
-            vfs.clear_cache()
-            res = resolve(repo, '/test/latest')
-            wvpasseq(3, len(res))
-            expected_latest_item_w_meta = vfs.Commit(meta=tip_tree['.'].meta,
-                                                     oid=tip_tree_oid,
-                                                     coid=tip_oid)
-            expected = (('', vfs._root),
-                        ('test', test_revlist_w_meta),
-                        (save_time_str, expected_latest_item_w_meta))
-            wvpasseq(expected, res)
-            ignore, latest_item = res[2]
-            latest_content = frozenset(vfs.contents(repo, latest_item))
-            expected = frozenset((x.name, vfs.Item(oid=x.oid, meta=x.meta))
-                                 for x in (tip_tree[name]
-                                           for name in ('.',
-                                                        'bad-symlink',
-                                                        'dir',
-                                                        'dir-symlink',
-                                                        'file',
-                                                        'file-symlink')))
-            wvpasseq(expected, latest_content)
+        wvstart('resolve: /.tag')
+        vfs.clear_cache()
+        res = resolve(b'/.tag')
+        wvpasseq(2, len(res))
+        wvpasseq(((b'', vfs._root), (b'.tag', vfs._tags)),
+                 res)
+        ignore, tag_item = res[1]
+        tag_content = frozenset(vfs.contents(repo, tag_item))
+        wvpasseq(frozenset([(b'.', tag_item),
+                            (b'test-tag', expected_test_tag_item)]),
+                 tag_content)
 
-            wvstart('resolve: /test/latest/file')
-            vfs.clear_cache()
-            res = resolve(repo, '/test/latest/file')
-            wvpasseq(4, len(res))
-            expected_file_item_w_meta = vfs.Item(meta=tip_tree['file'].meta,
-                                                 oid=tip_tree['file'].oid)
-            expected = (('', vfs._root),
-                        ('test', test_revlist_w_meta),
-                        (save_time_str, expected_latest_item_w_meta),
-                        ('file', expected_file_item_w_meta))
-            wvpasseq(expected, res)
+        wvstart('resolve: /test')
+        vfs.clear_cache()
+        res = resolve(b'/test')
+        wvpasseq(2, len(res))
+        wvpasseq(((b'', vfs._root), (b'test', test_revlist_w_meta)), res)
+        ignore, test_item = res[1]
+        test_content = frozenset(vfs.contents(repo, test_item))
+        # latest has metadata here due to caching
+        wvpasseq(frozenset([(b'.', test_revlist_w_meta),
+                            (save_time_str, expected_latest_item_w_meta),
+                            (b'latest', expected_latest_link)]),
+                 test_content)
 
-            wvstart('resolve: /test/latest/bad-symlink')
-            vfs.clear_cache()
-            res = resolve(repo, '/test/latest/bad-symlink')
-            wvpasseq(4, len(res))
-            expected = (('', vfs._root),
-                        ('test', test_revlist_w_meta),
-                        (save_time_str, expected_latest_item_w_meta),
-                        ('not-there', None))
-            wvpasseq(expected, res)
+        wvstart('resolve: /test/latest')
+        vfs.clear_cache()
+        res = resolve(b'/test/latest')
+        wvpasseq(3, len(res))
+        expected_latest_item_w_meta = vfs.Commit(meta=tip_tree[b'.'].meta,
+                                                 oid=tip_tree_oid,
+                                                 coid=tip_oid)
+        expected = ((b'', vfs._root),
+                    (b'test', test_revlist_w_meta),
+                    (save_time_str, expected_latest_item_w_meta))
+        wvpasseq(expected, res)
+        ignore, latest_item = res[2]
+        latest_content = frozenset(vfs.contents(repo, latest_item))
+        expected = frozenset((x.name, vfs.Item(oid=x.oid, meta=x.meta))
+                             for x in (tip_tree[name]
+                                       for name in (b'.',
+                                                    b'bad-symlink',
+                                                    b'dir',
+                                                    b'dir-symlink',
+                                                    b'file',
+                                                    b'file-symlink')))
+        wvpasseq(expected, latest_content)
 
-            wvstart('resolve nofollow: /test/latest/bad-symlink')
-            vfs.clear_cache()
-            res = resolve(repo, '/test/latest/bad-symlink', follow=False)
-            wvpasseq(4, len(res))
-            bad_symlink_value = tip_tree['bad-symlink']
-            expected_bad_symlink_item_w_meta = vfs.Item(meta=bad_symlink_value.meta,
-                                                        oid=bad_symlink_value.oid)
-            expected = (('', vfs._root),
-                        ('test', test_revlist_w_meta),
-                        (save_time_str, expected_latest_item_w_meta),
-                        ('bad-symlink', expected_bad_symlink_item_w_meta))
-            wvpasseq(expected, res)
+        wvstart('resolve: /test/latest/file')
+        vfs.clear_cache()
+        res = resolve(b'/test/latest/file')
+        wvpasseq(4, len(res))
+        expected_file_item_w_meta = vfs.Item(meta=tip_tree[b'file'].meta,
+                                             oid=tip_tree[b'file'].oid)
+        expected = ((b'', vfs._root),
+                    (b'test', test_revlist_w_meta),
+                    (save_time_str, expected_latest_item_w_meta),
+                    (b'file', expected_file_item_w_meta))
+        wvpasseq(expected, res)
 
-            wvstart('resolve: /test/latest/file-symlink')
-            vfs.clear_cache()
-            res = resolve(repo, '/test/latest/file-symlink')
-            wvpasseq(4, len(res))
-            expected = (('', vfs._root),
-                        ('test', test_revlist_w_meta),
-                        (save_time_str, expected_latest_item_w_meta),
-                        ('file', expected_file_item_w_meta))
-            wvpasseq(expected, res)
+        wvstart('resolve: /test/latest/bad-symlink')
+        vfs.clear_cache()
+        res = resolve(b'/test/latest/bad-symlink')
+        wvpasseq(4, len(res))
+        expected = ((b'', vfs._root),
+                    (b'test', test_revlist_w_meta),
+                    (save_time_str, expected_latest_item_w_meta),
+                    (b'not-there', None))
+        wvpasseq(expected, res)
 
-            wvstart('resolve nofollow: /test/latest/file-symlink')
-            vfs.clear_cache()
-            res = resolve(repo, '/test/latest/file-symlink', follow=False)
-            wvpasseq(4, len(res))
-            file_symlink_value = tip_tree['file-symlink']
-            expected_file_symlink_item_w_meta = vfs.Item(meta=file_symlink_value.meta,
-                                                         oid=file_symlink_value.oid)
-            expected = (('', vfs._root),
-                        ('test', test_revlist_w_meta),
-                        (save_time_str, expected_latest_item_w_meta),
-                        ('file-symlink', expected_file_symlink_item_w_meta))
-            wvpasseq(expected, res)
+        wvstart('resolve nofollow: /test/latest/bad-symlink')
+        vfs.clear_cache()
+        res = resolve(b'/test/latest/bad-symlink', follow=False)
+        wvpasseq(4, len(res))
+        bad_symlink_value = tip_tree[b'bad-symlink']
+        expected_bad_symlink_item_w_meta = vfs.Item(meta=bad_symlink_value.meta,
+                                                    oid=bad_symlink_value.oid)
+        expected = ((b'', vfs._root),
+                    (b'test', test_revlist_w_meta),
+                    (save_time_str, expected_latest_item_w_meta),
+                    (b'bad-symlink', expected_bad_symlink_item_w_meta))
+        wvpasseq(expected, res)
 
-            wvstart('resolve: /test/latest/missing')
-            vfs.clear_cache()
-            res = resolve(repo, '/test/latest/missing')
-            wvpasseq(4, len(res))
-            name, item = res[-1]
-            wvpasseq('missing', name)
-            wvpass(item is None)
+        wvstart('resolve: /test/latest/file-symlink')
+        vfs.clear_cache()
+        res = resolve(b'/test/latest/file-symlink')
+        wvpasseq(4, len(res))
+        expected = ((b'', vfs._root),
+                    (b'test', test_revlist_w_meta),
+                    (save_time_str, expected_latest_item_w_meta),
+                    (b'file', expected_file_item_w_meta))
+        wvpasseq(expected, res)
 
-            for path in ('/test/latest/file/',
-                         '/test/latest/file/.',
-                         '/test/latest/file/..',
-                         '/test/latest/file/../',
-                         '/test/latest/file/../.',
-                         '/test/latest/file/../..',
-                         '/test/latest/file/foo'):
-                wvstart('resolve: ' + path)
-                vfs.clear_cache()
-                try:
-                    resolve(repo, path)
-                except vfs.IOError as res_ex:
-                    wvpasseq(ENOTDIR, res_ex.errno)
-                    wvpasseq(['', 'test', save_time_str, 'file'],
-                             [name for name, item in res_ex.terminus])
+        wvstart('resolve nofollow: /test/latest/file-symlink')
+        vfs.clear_cache()
+        res = resolve(b'/test/latest/file-symlink', follow=False)
+        wvpasseq(4, len(res))
+        file_symlink_value = tip_tree[b'file-symlink']
+        expected_file_symlink_item_w_meta = vfs.Item(meta=file_symlink_value.meta,
+                                                     oid=file_symlink_value.oid)
+        expected = ((b'', vfs._root),
+                    (b'test', test_revlist_w_meta),
+                    (save_time_str, expected_latest_item_w_meta),
+                    (b'file-symlink', expected_file_symlink_item_w_meta))
+        wvpasseq(expected, res)
 
-            for path in ('/test/latest/file-symlink/',
-                         '/test/latest/file-symlink/.',
-                         '/test/latest/file-symlink/..',
-                         '/test/latest/file-symlink/../',
-                         '/test/latest/file-symlink/../.',
-                         '/test/latest/file-symlink/../..'):
-                wvstart('resolve nofollow: ' + path)
-                vfs.clear_cache()
-                try:
-                    resolve(repo, path, follow=False)
-                except vfs.IOError as res_ex:
-                    wvpasseq(ENOTDIR, res_ex.errno)
-                    wvpasseq(['', 'test', save_time_str, 'file'],
-                             [name for name, item in res_ex.terminus])
+        wvstart('resolve: /test/latest/missing')
+        vfs.clear_cache()
+        res = resolve(b'/test/latest/missing')
+        wvpasseq(4, len(res))
+        name, item = res[-1]
+        wvpasseq(b'missing', name)
+        wvpass(item is None)
 
-            wvstart('resolve: non-directory parent')
+        for path in (b'/test/latest/file/',
+                     b'/test/latest/file/.',
+                     b'/test/latest/file/..',
+                     b'/test/latest/file/../',
+                     b'/test/latest/file/../.',
+                     b'/test/latest/file/../..',
+                     b'/test/latest/file/foo'):
+            wvstart('resolve: ' + path_msg(path))
             vfs.clear_cache()
-            file_res = resolve(repo, '/test/latest/file')
             try:
-                resolve(repo, 'foo', parent=file_res)
+                resolve(path)
             except vfs.IOError as res_ex:
                 wvpasseq(ENOTDIR, res_ex.errno)
-                wvpasseq(None, res_ex.terminus)
-
-            wvstart('resolve nofollow: /test/latest/dir-symlink')
-            vfs.clear_cache()
-            res = resolve(repo, '/test/latest/dir-symlink', follow=False)
-            wvpasseq(4, len(res))
-            dir_symlink_value = tip_tree['dir-symlink']
-            expected_dir_symlink_item_w_meta = vfs.Item(meta=dir_symlink_value.meta,
-                                                         oid=dir_symlink_value.oid)
-            expected = (('', vfs._root),
-                        ('test', test_revlist_w_meta),
-                        (save_time_str, expected_latest_item_w_meta),
-                        ('dir-symlink', expected_dir_symlink_item_w_meta))
-            wvpasseq(expected, res)
+                wvpasseq([b'', b'test', save_time_str, b'file'],
+                         [name for name, item in res_ex.terminus])
 
-            dir_value = tip_tree['dir']
-            expected_dir_item = vfs.Item(oid=dir_value.oid,
-                                         meta=tree_dict(repo, dir_value.oid)['.'].meta)
-            expected = (('', vfs._root),
-                        ('test', test_revlist_w_meta),
-                        (save_time_str, expected_latest_item_w_meta),
-                        ('dir', expected_dir_item))
-            def lresolve(*args, **keys):
-                return resolve(*args, **dict(keys, follow=False))
-            for resname, resolver in (('resolve', resolve),
-                                      ('resolve nofollow', lresolve)):
-                for path in ('/test/latest/dir-symlink/',
-                             '/test/latest/dir-symlink/.'):
-                    wvstart(resname + ': ' + path)
-                    vfs.clear_cache()
-                    res = resolver(repo, path)
-                    wvpasseq(4, len(res))
-                    wvpasseq(expected, res)
-            wvstart('resolve: /test/latest/dir-symlink')
+        for path in (b'/test/latest/file-symlink/',
+                     b'/test/latest/file-symlink/.',
+                     b'/test/latest/file-symlink/..',
+                     b'/test/latest/file-symlink/../',
+                     b'/test/latest/file-symlink/../.',
+                     b'/test/latest/file-symlink/../..'):
+            wvstart('resolve nofollow: ' + path_msg(path))
             vfs.clear_cache()
-            res = resolve(repo, path)
-            wvpasseq(4, len(res))
-            wvpasseq(expected, res)
-
-@wvtest
-def test_resolve_loop():
-    with no_lingering_errors():
-        with test_tempdir('bup-tvfs-resloop-') as tmpdir:
-            resolve = vfs.resolve
-            bup_dir = tmpdir + '/bup'
-            environ['GIT_DIR'] = bup_dir
-            environ['BUP_DIR'] = bup_dir
-            git.repodir = bup_dir
-            repo = LocalRepo()
-            data_path = tmpdir + '/src'
-            os.mkdir(data_path)
-            symlink('loop', data_path + '/loop')
-            ex((bup_path, 'init'))
-            ex((bup_path, 'index', '-v', data_path))
-            save_utc = 100000
-            ex((bup_path, 'save', '-d', str(save_utc), '-tvvn', 'test', '--strip',
-                data_path))
-            save_name = strftime('%Y-%m-%d-%H%M%S', localtime(save_utc))
             try:
-                wvpasseq('this call should never return',
-                         resolve(repo, '/test/%s/loop' % save_name))
+                resolve(path, follow=False)
             except vfs.IOError as res_ex:
-                wvpasseq(ELOOP, res_ex.errno)
-                wvpasseq(['', 'test', save_name, 'loop'],
+                wvpasseq(ENOTDIR, res_ex.errno)
+                wvpasseq([b'', b'test', save_time_str, b'file'],
                          [name for name, item in res_ex.terminus])
 
+        wvstart('resolve: non-directory parent')
+        vfs.clear_cache()
+        file_res = resolve(b'/test/latest/file')
+        try:
+            resolve(b'foo', parent=file_res)
+        except vfs.IOError as res_ex:
+            wvpasseq(ENOTDIR, res_ex.errno)
+            wvpasseq(None, res_ex.terminus)
+
+        wvstart('resolve nofollow: /test/latest/dir-symlink')
+        vfs.clear_cache()
+        res = resolve(b'/test/latest/dir-symlink', follow=False)
+        wvpasseq(4, len(res))
+        dir_symlink_value = tip_tree[b'dir-symlink']
+        expected_dir_symlink_item_w_meta = vfs.Item(meta=dir_symlink_value.meta,
+                                                     oid=dir_symlink_value.oid)
+        expected = ((b'', vfs._root),
+                    (b'test', test_revlist_w_meta),
+                    (save_time_str, expected_latest_item_w_meta),
+                    (b'dir-symlink', expected_dir_symlink_item_w_meta))
+        wvpasseq(expected, res)
+
+        dir_value = tip_tree[b'dir']
+        expected_dir_item = vfs.Item(oid=dir_value.oid,
+                                     meta=tree_dict(repo, dir_value.oid)[b'.'].meta)
+        expected = ((b'', vfs._root),
+                    (b'test', test_revlist_w_meta),
+                    (save_time_str, expected_latest_item_w_meta),
+                    (b'dir', expected_dir_item))
+        def lresolve(*args, **keys):
+            return resolve(*args, **dict(keys, follow=False))
+        for resname, resolver in (('resolve', resolve),
+                                  ('resolve nofollow', lresolve)):
+            for path in (b'/test/latest/dir-symlink/',
+                         b'/test/latest/dir-symlink/.'):
+                wvstart(resname + ': ' + path_msg(path))
+                vfs.clear_cache()
+                res = resolver(path)
+                wvpasseq(4, len(res))
+                wvpasseq(expected, res)
+        wvstart('resolve: /test/latest/dir-symlink')
+        vfs.clear_cache()
+        res = resolve(path)
+        wvpasseq(4, len(res))
+        wvpasseq(expected, res)
+
+@wvtest
+def test_local_resolve():
+    prep_and_test_repo(b'local-vfs-resolve',
+                       lambda x: LocalRepo(repo_dir=x), test_resolve)
+
+@wvtest
+def test_remote_resolve():
+    prep_and_test_repo(b'remote-vfs-resolve',
+                       lambda x: RemoteRepo(x), test_resolve)
+
+def test_resolve_loop(repo, tmpdir):
+    data_path = tmpdir + b'/src'
+    os.mkdir(data_path)
+    symlink(b'loop', data_path + b'/loop')
+    ex((bup_path, b'init'))
+    ex((bup_path, b'index', b'-v', data_path))
+    save_utc = 100000
+    ex((bup_path, b'save', b'-d', b'%d' % save_utc, b'-tvvn', b'test', b'--strip',
+        data_path))
+    save_name = strftime('%Y-%m-%d-%H%M%S', localtime(save_utc)).encode('ascii')
+    try:
+        wvpasseq('this call should never return',
+                 repo.resolve(b'/test/%s/loop' % save_name))
+    except vfs.IOError as res_ex:
+        wvpasseq(ELOOP, res_ex.errno)
+        wvpasseq([b'', b'test', save_name, b'loop'],
+                 [name for name, item in res_ex.terminus])
+
+@wvtest
+def test_local_resolve_loop():
+    prep_and_test_repo(b'local-vfs-resolve-loop',
+                       lambda x: LocalRepo(x), test_resolve_loop)
+
+@wvtest
+def test_remote_resolve_loop():
+    prep_and_test_repo(b'remote-vfs-resolve-loop',
+                       lambda x: RemoteRepo(x), test_resolve_loop)
+
 # FIXME: add tests for the want_meta=False cases.