]> arthur.barton.de Git - bup.git/commitdiff
tests: convert python-based tests to pytest
authorJohannes Berg <johannes@sipsolutions.net>
Thu, 28 May 2020 20:50:43 +0000 (22:50 +0200)
committerRob Browning <rlb@defaultvalue.org>
Thu, 26 Nov 2020 21:53:09 +0000 (15:53 -0600)
This converts the existing test scripts that are written
in python already to pytest based tests.

Signed-off-by: Johannes Berg <johannes@sipsolutions.net>
[rlb@defaultvalue.org: change wvtest imports to wvpytest; remove
 wvtest.py here; drop buptest.test_tempdir here]

17 files changed:
Makefile
dev/echo-argv-bytes
dev/hardlink-sets
dev/ns-timestamp-resolutions
dev/sparse-test-data
dev/subtree-hash
test/ext/test-argv [deleted file]
test/ext/test-ftp [deleted file]
test/ext/test-get [deleted file]
test/ext/test-prune-older [deleted file]
test/ext/test_argv.py [new file with mode: 0644]
test/ext/test_ftp.py [new file with mode: 0644]
test/ext/test_get.py [new file with mode: 0644]
test/ext/test_prune_older.py [new file with mode: 0644]
test/int/test_compat.py
test/lib/buptest/__init__.py
wvtest.py [deleted file]

index 270653c777153b787d9dda39c30134ad4d8126ad..722b7f720fad39d79deeb57bc1cad5ace09bd2d1 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -166,14 +166,12 @@ runtests-python: all test/tmp
 
 cmdline_tests := \
   test/ext/test.sh \
-  test/ext/test-argv \
   test/ext/test-cat-file.sh \
   test/ext/test-command-without-init-fails.sh \
   test/ext/test-compression.sh \
   test/ext/test-drecurse.sh \
   test/ext/test-fsck.sh \
   test/ext/test-fuse.sh \
-  test/ext/test-ftp \
   test/ext/test-help \
   test/ext/test-web.sh \
   test/ext/test-gc.sh \
@@ -189,7 +187,6 @@ cmdline_tests := \
   test/ext/test-meta.sh \
   test/ext/test-on.sh \
   test/ext/test-packsizelimit \
-  test/ext/test-prune-older \
   test/ext/test-redundant-saves.sh \
   test/ext/test-restore-map-owner.sh \
   test/ext/test-restore-single-file.sh \
@@ -206,19 +203,6 @@ cmdline_tests := \
   test/ext/test-tz.sh \
   test/ext/test-xdev.sh
 
-tmp-target-run-test-get-%: all test/tmp
-       $(pf); cd $$(pwd -P); TMPDIR="$(test_tmp)" \
-         test/ext/test-get $* 2>&1 | tee -a test/tmp/test-log/$$$$.log
-
-test_get_targets += \
-  tmp-target-run-test-get-replace \
-  tmp-target-run-test-get-universal \
-  tmp-target-run-test-get-ff \
-  tmp-target-run-test-get-append \
-  tmp-target-run-test-get-pick \
-  tmp-target-run-test-get-new-tag \
-  tmp-target-run-test-get-unnamed
-
 # For parallel runs.
 # The "pwd -P" here may not be appropriate in the long run, but we
 # need it until we settle the relevant drecurse/exclusion questions:
@@ -227,7 +211,7 @@ tmp-target-run-test%: all test/tmp
        $(pf); cd $$(pwd -P); TMPDIR="$(test_tmp)" \
          test/ext/test$* 2>&1 | tee -a test/tmp/test-log/$$$$.log
 
-runtests-cmdline: $(test_get_targets) $(subst test/ext/test,tmp-target-run-test,$(cmdline_tests))
+runtests-cmdline: $(subst test/ext/test,tmp-target-run-test,$(cmdline_tests))
 
 stupid:
        PATH=/bin:/usr/bin $(MAKE) test
index 1be728866d7098a57f22614a5830604f7a331a66..d49c26cc751e5e908a24a97f4878670b9c1195c9 100755 (executable)
@@ -20,7 +20,7 @@ from sys import stdout
 import os, sys
 
 script_home = abspath(dirname(__file__))
-sys.path[:0] = [abspath(script_home + '/../lib'), abspath(script_home + '/..')]
+sys.path[:0] = [abspath(script_home + '/../../lib'), abspath(script_home + '/../..')]
 
 from bup import compat
 
index a0329f80b334fd249c2bd17a8351f33d294bdf2e..e1be7424b8c2d00670d6f397e8f6b4fa97f8afd9 100755 (executable)
@@ -16,6 +16,8 @@ exec "$bup_python" "$0"
 from __future__ import absolute_import, print_function
 import os, stat, sys
 
+sys.path[:0] = [os.path.dirname(os.path.realpath(__file__)) + '/../lib']
+
 from bup import compat
 from bup.io import byte_stream
 
index 1938e78887cf0f0157a0b47f2e12043fe2f1da18..f5b296c0d364fb444427d53debc72ea5da4cbf5c 100755 (executable)
@@ -16,7 +16,7 @@ exec "$bup_python" "$0"
 from __future__ import absolute_import
 import os.path, sys
 
-sys.path[:0] = [os.path.dirname(os.path.realpath(__file__)) + '/../lib']
+sys.path[:0] = [os.path.dirname(os.path.realpath(__file__)) + '/../../lib']
 
 from bup.compat import argv_bytes
 from bup.helpers import handle_ctrl_c, saved_errors
index 002e52594e4835e4d6097310234cfbfe0401d7a1..34bb117840ab4d174cb08af0adbe52cfd6c24454 100755 (executable)
@@ -9,6 +9,8 @@ from random import randint
 from sys import stderr, stdout
 import os, sys
 
+sys.path[:0] = [os.path.dirname(os.path.realpath(__file__)) + '/../lib']
+
 from bup.io import byte_stream
 
 def smaller_region(max_offset):
index 84f168204864ee9715331b788aa22f1282add849..e3468fb57093e2fb54e0ada1f1d845ed450c5bb6 100755 (executable)
@@ -8,6 +8,8 @@ exec "$bup_python" "$0" ${1+"$@"}
 from __future__ import absolute_import, print_function
 import os.path, sys
 
+sys.path[:0] = [os.path.dirname(os.path.realpath(__file__)) + '/../lib']
+
 from bup.compat import argv_bytes
 from bup.helpers import handle_ctrl_c, readpipe
 from bup.io import byte_stream
diff --git a/test/ext/test-argv b/test/ext/test-argv
deleted file mode 100755 (executable)
index 6fc3249..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/bin/sh
-"""": # -*-python-*-
-bup_python="$(dirname "$0")/../../config/bin/python" || exit $?
-exec "$bup_python" "$0" ${1+"$@"}
-"""
-# end of bup preamble
-
-from __future__ import absolute_import, print_function
-
-from os.path import abspath, dirname
-from random import randint
-from subprocess import check_output
-from sys import stderr, stdout
-import sys
-
-script_home = abspath(dirname(__file__))
-sys.path[:0] = [abspath(script_home + '/../../lib'), abspath(script_home + '/../..')]
-
-from wvtest import wvcheck, wvfail, wvmsg, wvpass, wvpasseq, wvpassne, wvstart
-
-wvstart('command line arguments are not mangled')
-
-def rand_bytes(n):
-    return bytes([randint(1, 255) for x in range(n)])
-
-for trial in range(100):
-    cmd = [b'dev/echo-argv-bytes', rand_bytes(randint(1, 32))]
-    out = check_output(cmd)
-    wvpasseq(b'\0\n'.join(cmd) + b'\0\n', out)
diff --git a/test/ext/test-ftp b/test/ext/test-ftp
deleted file mode 100755 (executable)
index 0f9591b..0000000
+++ /dev/null
@@ -1,141 +0,0 @@
-#!/bin/sh
-"""": # -*-python-*-
-bup_python="$(dirname "$0")/../../dev/bup-python" || exit $?
-exec "$bup_python" "$0" ${1+"$@"}
-"""
-# end of bup preamble
-
-from __future__ import absolute_import, print_function
-from os import chdir, mkdir, symlink, unlink
-from os.path import abspath, dirname
-from subprocess import PIPE
-from time import localtime, strftime, tzset
-import os, sys
-
-# For buptest, wvtest, ...
-sys.path[:0] = (abspath(os.path.dirname(__file__) + '/../..'),)
-sys.path[:0] = (abspath(os.path.dirname(__file__) + '/../../test/lib'),)
-sys.path[:0] = [os.path.dirname(os.path.realpath(__file__)) + '/../../lib']
-
-from buptest import ex, exo, logcmd, test_tempdir
-from wvtest import wvfail, wvpass, wvpasseq, wvpassne, wvstart
-
-from bup.compat import environ
-from bup.helpers import unlink as unlink_if_exists
-import bup.path
-
-bup_cmd = bup.path.exe()
-
-def bup(*args, **kwargs):
-    if 'stdout' not in kwargs:
-        return exo((bup_cmd,) + args, **kwargs)
-    return ex((bup_cmd,) + args, **kwargs)
-
-def jl(*lines):
-    return b''.join(line + b'\n' for line in lines)
-
-environ[b'GIT_AUTHOR_NAME'] = b'bup test'
-environ[b'GIT_COMMITTER_NAME'] = b'bup test'
-environ[b'GIT_AUTHOR_EMAIL'] = b'bup@a425bc70a02811e49bdf73ee56450e6f'
-environ[b'GIT_COMMITTER_EMAIL'] = b'bup@a425bc70a02811e49bdf73ee56450e6f'
-
-with test_tempdir(b'ftp-') as tmpdir:
-    environ[b'BUP_DIR'] = tmpdir + b'/repo'
-    environ[b'GIT_DIR'] = tmpdir + b'/repo'
-    environ[b'TZ'] = b'UTC'
-    tzset()
-
-    chdir(tmpdir)
-    mkdir(b'src')
-    chdir(b'src')
-    mkdir(b'dir')
-    with open(b'file-1', 'wb') as f:
-        f.write(b'excitement!\n')
-    with open(b'dir/file-2', 'wb') as f:
-        f.write(b'more excitement!\n')
-    symlink(b'file-1', b'file-symlink')
-    symlink(b'dir', b'dir-symlink')
-    symlink(b'not-there', b'bad-symlink')
-
-    chdir(tmpdir)    
-    bup(b'init')
-    bup(b'index', b'src')
-    bup(b'save', b'-n', b'src', b'--strip', b'src')
-    save_utc = int(exo((b'git', b'show',
-                        b'-s', b'--format=%at', b'src')).out.strip())
-    save_name = strftime('%Y-%m-%d-%H%M%S', localtime(save_utc)).encode('ascii')
-    
-    wvstart('help')
-    wvpasseq(b'Commands: ls cd pwd cat get mget help quit\n',
-             exo((bup_cmd, b'ftp'), input=b'help\n', stderr=PIPE).out)
-
-    wvstart('pwd/cd')
-    wvpasseq(b'/\n', bup(b'ftp', input=b'pwd\n').out)
-    wvpasseq(b'', bup(b'ftp', input=b'cd src\n').out)
-    wvpasseq(b'/src\n', bup(b'ftp', input=jl(b'cd src', b'pwd')).out)
-    wvpasseq(b'/src\n/\n', bup(b'ftp', input=jl(b'cd src', b'pwd',
-                                                b'cd ..', b'pwd')).out)
-    wvpasseq(b'/src\n/\n', bup(b'ftp', input=jl(b'cd src', b'pwd',
-                                                b'cd ..', b'cd ..',
-                                                b'pwd')).out)
-    wvpasseq(b'/src/%s/dir\n' % save_name,
-             bup(b'ftp', input=jl(b'cd src/latest/dir-symlink', b'pwd')).out)
-    wvpasseq(b'/src/%s/dir\n' % save_name,
-             bup(b'ftp', input=jl(b'cd src latest dir-symlink', b'pwd')).out)
-    wvpassne(0, bup(b'ftp',
-                    input=jl(b'cd src/latest/bad-symlink', b'pwd'),
-                    check=False, stdout=None).rc)
-    wvpassne(0, bup(b'ftp',
-                    input=jl(b'cd src/latest/not-there', b'pwd'),
-                    check=False, stdout=None).rc)
-
-    wvstart('ls')
-    # FIXME: elaborate
-    wvpasseq(b'src\n', bup(b'ftp', input=b'ls\n').out)
-    wvpasseq(save_name + b'\nlatest\n',
-             bup(b'ftp', input=b'ls src\n').out)
-
-    wvstart('cat')
-    wvpasseq(b'excitement!\n',
-             bup(b'ftp', input=b'cat src/latest/file-1\n').out)
-    wvpasseq(b'excitement!\nmore excitement!\n',
-             bup(b'ftp',
-                 input=b'cat src/latest/file-1 src/latest/dir/file-2\n').out)
-    
-    wvstart('get')
-    bup(b'ftp', input=jl(b'get src/latest/file-1 dest'))
-    with open(b'dest', 'rb') as f:
-        wvpasseq(b'excitement!\n', f.read())
-    unlink(b'dest')
-    bup(b'ftp', input=jl(b'get src/latest/file-symlink dest'))
-    with open(b'dest', 'rb') as f:
-        wvpasseq(b'excitement!\n', f.read())
-    unlink(b'dest')
-    wvpassne(0, bup(b'ftp',
-                    input=jl(b'get src/latest/bad-symlink dest'),
-                    check=False, stdout=None).rc)
-    wvpassne(0, bup(b'ftp',
-                    input=jl(b'get src/latest/not-there'),
-                    check=False, stdout=None).rc)
-    
-    wvstart('mget')
-    unlink_if_exists(b'file-1')
-    bup(b'ftp', input=jl(b'mget src/latest/file-1'))
-    with open(b'file-1', 'rb') as f:
-        wvpasseq(b'excitement!\n', f.read())
-    unlink_if_exists(b'file-1')
-    unlink_if_exists(b'file-2')
-    bup(b'ftp', input=jl(b'mget src/latest/file-1 src/latest/dir/file-2'))
-    with open(b'file-1', 'rb') as f:
-        wvpasseq(b'excitement!\n', f.read())
-    with open(b'file-2', 'rb') as f:
-        wvpasseq(b'more excitement!\n', f.read())
-    unlink_if_exists(b'file-symlink')
-    bup(b'ftp', input=jl(b'mget src/latest/file-symlink'))
-    with open(b'file-symlink', 'rb') as f:
-        wvpasseq(b'excitement!\n', f.read())
-    wvpassne(0, bup(b'ftp',
-                    input=jl(b'mget src/latest/bad-symlink dest'),
-                    check=False, stdout=None).rc)
-    # bup mget currently always does pattern matching
-    bup(b'ftp', input=b'mget src/latest/not-there\n')
diff --git a/test/ext/test-get b/test/ext/test-get
deleted file mode 100755 (executable)
index 1dad1d4..0000000
+++ /dev/null
@@ -1,1010 +0,0 @@
-#!/bin/sh
-"""": # -*-python-*-
-# https://sourceware.org/bugzilla/show_bug.cgi?id=26034
-export "BUP_ARGV_0"="$0"
-arg_i=1
-for arg in "$@"; do
-    export "BUP_ARGV_${arg_i}"="$arg"
-    shift
-    arg_i=$((arg_i + 1))
-done
-bup_python="$(dirname "$0")/../../dev/bup-python" || exit $?
-exec "$bup_python" "$0"
-"""
-# end of bup preamble
-
-from __future__ import print_function
-from errno import ENOENT
-from os import chdir, mkdir, rename
-from os.path import abspath, dirname
-from shutil import rmtree
-from subprocess import PIPE
-import os, re, sys
-
-# For buptest, wvtest, ...
-sys.path[:0] = (abspath(os.path.dirname(__file__) + '/../..'),)
-sys.path[:0] = (abspath(os.path.dirname(__file__) + '/../../test/lib'),)
-sys.path[:0] = [os.path.dirname(os.path.realpath(__file__)) + '/../../lib']
-
-from bup import compat, path
-from bup.compat import environ, getcwd, items
-from bup.helpers import bquote, merge_dict, unlink
-from bup.io import byte_stream
-from buptest import ex, exo, test_tempdir
-from wvtest import wvcheck, wvfail, wvmsg, wvpass, wvpasseq, wvpassne, wvstart
-import bup.path
-
-sys.stdout.flush()
-stdout = byte_stream(sys.stdout)
-
-# FIXME: per-test function
-environ[b'GIT_AUTHOR_NAME'] = b'bup test-get'
-environ[b'GIT_COMMITTER_NAME'] = b'bup test-get'
-environ[b'GIT_AUTHOR_EMAIL'] = b'bup@85430dcca2b611e4b2c3-8f5691723476'
-environ[b'GIT_COMMITTER_EMAIL'] = b'bup@85430dcca2b611e4b2c3-8f5691723476'
-
-# The clean-repo test can probably be applied more broadly.  It was
-# initially just applied to test-pick to catch a bug.
-
-top = getcwd()
-bup_cmd = bup.path.exe()
-
-def rmrf(path):
-    err = []  # because python's scoping mess...
-    def onerror(function, path, excinfo):
-        err.append((function, path, excinfo))
-    rmtree(path, onerror=onerror)
-    if err:
-        function, path, excinfo = err[0]
-        ex_type, ex, traceback = excinfo
-        if (not isinstance(ex, OSError)) or ex.errno != ENOENT:
-            raise ex
-
-def verify_trees_match(path1, path2):
-    global top
-    exr = exo((top + b'/dev/compare-trees', b'-c', path1, path2), check=False)
-    stdout.write(exr.out)
-    sys.stdout.flush()
-    wvcheck(exr.rc == 0, 'process exit %d == 0' % exr.rc)
-
-def verify_rcz(cmd, **kwargs):
-    assert not kwargs.get('check')
-    kwargs['check'] = False
-    result = exo(cmd, **kwargs)
-    stdout.write(result.out)
-    rc = result.proc.returncode
-    wvcheck(rc == 0, 'process exit %d == 0' % rc)
-    return result
-
-# FIXME: multline, or allow opts generally?
-
-def verify_rx(rx, string):
-    wvcheck(re.search(rx, string), 'rx %r matches %r' % (rx, string))
-
-def verify_nrx(rx, string):
-    wvcheck(not re.search(rx, string), "rx %r doesn't match %r" % (rx, string))
-
-def validate_clean_repo():
-    out = verify_rcz((b'git', b'--git-dir', b'get-dest', b'fsck')).out
-    verify_nrx(br'dangling|mismatch|missing|unreachable', out)
-    
-def validate_blob(src_id, dest_id):
-    global top
-    rmrf(b'restore-src')
-    rmrf(b'restore-dest')
-    cat_tree = top + b'/dev/git-cat-tree'
-    src_blob = verify_rcz((cat_tree, b'--git-dir', b'get-src', src_id)).out
-    dest_blob = verify_rcz((cat_tree, b'--git-dir', b'get-src', src_id)).out
-    wvpasseq(src_blob, dest_blob)
-
-def validate_tree(src_id, dest_id):
-
-    rmrf(b'restore-src')
-    rmrf(b'restore-dest')
-    mkdir(b'restore-src')
-    mkdir(b'restore-dest')
-    
-    commit_env = merge_dict(environ, {b'GIT_COMMITTER_DATE': b'2014-01-01 01:01'})
-
-    # Create a commit so the archive contents will have matching timestamps.
-    src_c = exo((b'git', b'--git-dir', b'get-src',
-                 b'commit-tree', b'-m', b'foo', src_id),
-                env=commit_env).out.strip()
-    dest_c = exo((b'git', b'--git-dir', b'get-dest',
-                  b'commit-tree', b'-m', b'foo', dest_id),
-                 env=commit_env).out.strip()
-    exr = verify_rcz(b'git --git-dir get-src archive %s | tar xvf - -C restore-src'
-                     % bquote(src_c),
-                     shell=True)
-    if exr.rc != 0: return False
-    exr = verify_rcz(b'git --git-dir get-dest archive %s | tar xvf - -C restore-dest'
-                     % bquote(dest_c),
-                     shell=True)
-    if exr.rc != 0: return False
-    
-    # git archive doesn't include an entry for ./.
-    unlink(b'restore-src/pax_global_header')
-    unlink(b'restore-dest/pax_global_header')
-    ex((b'touch', b'-r', b'restore-src', b'restore-dest'))
-    verify_trees_match(b'restore-src/', b'restore-dest/')
-    rmrf(b'restore-src')
-    rmrf(b'restore-dest')
-
-def validate_commit(src_id, dest_id):
-    exr = verify_rcz((b'git', b'--git-dir', b'get-src', b'cat-file', b'commit', src_id))
-    if exr.rc != 0: return False
-    src_cat = exr.out
-    exr = verify_rcz((b'git', b'--git-dir', b'get-dest', b'cat-file', b'commit', dest_id))
-    if exr.rc != 0: return False
-    dest_cat = exr.out
-    wvpasseq(src_cat, dest_cat)
-    if src_cat != dest_cat: return False
-    
-    rmrf(b'restore-src')
-    rmrf(b'restore-dest')
-    mkdir(b'restore-src')
-    mkdir(b'restore-dest')
-    qsrc = bquote(src_id)
-    qdest = bquote(dest_id)
-    exr = verify_rcz((b'git --git-dir get-src archive ' + qsrc
-                      + b' | tar xf - -C restore-src'),
-                     shell=True)
-    if exr.rc != 0: return False
-    exr = verify_rcz((b'git --git-dir get-dest archive ' + qdest +
-                      b' | tar xf - -C restore-dest'),
-                     shell=True)
-    if exr.rc != 0: return False
-    
-    # git archive doesn't include an entry for ./.
-    ex((b'touch', b'-r', b'restore-src', b'restore-dest'))
-    verify_trees_match(b'restore-src/', b'restore-dest/')
-    rmrf(b'restore-src')
-    rmrf(b'restore-dest')
-
-def _validate_save(orig_dir, save_path, commit_id, tree_id):
-    global bup_cmd
-    rmrf(b'restore')
-    exr = verify_rcz((bup_cmd, b'-d', b'get-dest',
-                      b'restore', b'-C', b'restore', save_path + b'/.'))
-    if exr.rc: return False
-    verify_trees_match(orig_dir + b'/', b'restore/')
-    if tree_id:
-        # FIXME: double check that get-dest is correct
-        exr = verify_rcz((b'git', b'--git-dir', b'get-dest', b'ls-tree', tree_id))
-        if exr.rc: return False
-        cat = verify_rcz((b'git', b'--git-dir', b'get-dest',
-                          b'cat-file', b'commit', commit_id))
-        if cat.rc: return False
-        wvpasseq(b'tree ' + tree_id, cat.out.splitlines()[0])
-
-# FIXME: re-merge save and new_save?
-        
-def validate_save(dest_name, restore_subpath, commit_id, tree_id, orig_value,
-                  get_out):
-    out = get_out.splitlines()
-    print('blarg: out', repr(out), file=sys.stderr)
-    wvpasseq(2, len(out))
-    get_tree_id = out[0]
-    get_commit_id = out[1]
-    wvpasseq(tree_id, get_tree_id)
-    wvpasseq(commit_id, get_commit_id)
-    _validate_save(orig_value, dest_name + restore_subpath, commit_id, tree_id)
-
-def validate_new_save(dest_name, restore_subpath, commit_id, tree_id, orig_value,
-                      get_out):
-    out = get_out.splitlines()
-    wvpasseq(2, len(out))
-    get_tree_id = out[0]
-    get_commit_id = out[1]
-    wvpasseq(tree_id, get_tree_id)
-    wvpassne(commit_id, get_commit_id)
-    _validate_save(orig_value, dest_name + restore_subpath, get_commit_id, tree_id)
-        
-def validate_tagged_save(tag_name, restore_subpath,
-                         commit_id, tree_id, orig_value, get_out):
-    out = get_out.splitlines()
-    wvpasseq(1, len(out))
-    get_tag_id = out[0]
-    wvpasseq(commit_id, get_tag_id)
-    # Make sure tmp doesn't already exist.
-    exr = exo((b'git', b'--git-dir', b'get-dest', b'show-ref', b'tmp-branch-for-tag'),
-              check=False)
-    wvpasseq(1, exr.rc)
-
-    ex((b'git', b'--git-dir', b'get-dest', b'branch', b'tmp-branch-for-tag',
-        b'refs/tags/' + tag_name))
-    _validate_save(orig_value, b'tmp-branch-for-tag/latest' + restore_subpath,
-                   commit_id, tree_id)
-    ex((b'git', b'--git-dir', b'get-dest', b'branch', b'-D', b'tmp-branch-for-tag'))
-
-def validate_new_tagged_commit(tag_name, commit_id, tree_id, get_out):
-    out = get_out.splitlines()
-    wvpasseq(1, len(out))
-    get_tag_id = out[0]
-    wvpassne(commit_id, get_tag_id)
-    validate_tree(tree_id, tag_name + b':')
-
-
-get_cases_tested = 0
-
-def _run_get(disposition, method, what):
-    print('run_get:', repr((disposition, method, what)), file=sys.stderr)
-    global bup_cmd
-
-    if disposition == 'get':
-        get_cmd = (bup_cmd, b'-d', b'get-dest',
-                   b'get', b'-vvct', b'--print-tags', b'-s', b'get-src')
-    elif disposition == 'get-on':
-        get_cmd = (bup_cmd, b'-d', b'get-dest',
-                   b'on', b'-', b'get', b'-vvct', b'--print-tags', b'-s', b'get-src')
-    elif disposition == 'get-to':
-        get_cmd = (bup_cmd, b'-d', b'get-dest',
-                   b'get', b'-vvct', b'--print-tags', b'-s', b'get-src',
-                   b'-r', b'-:' + getcwd() + b'/get-dest')
-    else:
-        raise Exception('error: unexpected get disposition ' + repr(disposition))
-    
-    global get_cases_tested
-    if isinstance(what, bytes):
-        cmd = get_cmd + (method, what)
-    else:
-        assert not isinstance(what, str)  # python 3 sanity check
-        if method in (b'--ff', b'--append', b'--pick', b'--force-pick', b'--new-tag',
-                      b'--replace'):
-            method += b':'
-        src, dest = what
-        cmd = get_cmd + (method, src, dest)
-    result = exo(cmd, check=False, stderr=PIPE)
-    get_cases_tested += 1
-    fsck = ex((bup_cmd, b'-d', b'get-dest', b'fsck'), check=False)
-    wvpasseq(0, fsck.rc)
-    return result
-
-def run_get(disposition, method, what=None, given=None):
-    global bup_cmd
-    rmrf(b'get-dest')
-    ex((bup_cmd, b'-d', b'get-dest', b'init'))
-
-    if given:
-        # FIXME: replace bup-get with independent commands as is feasible
-        exr = _run_get(disposition, b'--replace', given)
-        assert not exr.rc
-    return _run_get(disposition, method, what)
-
-def test_universal_behaviors(get_disposition):
-    methods = (b'--ff', b'--append', b'--pick', b'--force-pick', b'--new-tag',
-               b'--replace', b'--unnamed')
-    for method in methods:
-        mmsg = method.decode('ascii')
-        wvstart(get_disposition + ' ' + mmsg + ', missing source, fails')
-        exr = run_get(get_disposition, method, b'not-there')
-        wvpassne(0, exr.rc)
-        verify_rx(br'cannot find source', exr.err)
-    for method in methods:
-        mmsg = method.decode('ascii')
-        wvstart(get_disposition + ' ' + mmsg + ' / fails')
-        exr = run_get(get_disposition, method, b'/')
-        wvpassne(0, exr.rc)
-        verify_rx(b'cannot fetch entire repository', exr.err)
-
-def verify_only_refs(**kwargs):
-    for kind, refs in items(kwargs):
-        if kind == 'heads':
-            abs_refs = [b'refs/heads/' + ref for ref in refs]
-            karg = b'--heads'
-        elif kind == 'tags':
-            abs_refs = [b'refs/tags/' + ref for ref in refs]
-            karg = b'--tags'
-        else:
-            raise TypeError('unexpected keyword argument %r' % kind)
-        if abs_refs:
-            verify_rcz([b'git', b'--git-dir', b'get-dest',
-                        b'show-ref', b'--verify', karg] + abs_refs)
-            exr = exo((b'git', b'--git-dir', b'get-dest', b'show-ref', karg),
-                      check=False)
-            wvpasseq(0, exr.rc)
-            expected_refs = sorted(abs_refs)
-            repo_refs = sorted([x.split()[1] for x in exr.out.splitlines()])
-            wvpasseq(expected_refs, repo_refs)
-        else:
-            # FIXME: can we just check "git show-ref --heads == ''"?
-            exr = exo((b'git', b'--git-dir', b'get-dest', b'show-ref', karg),
-                      check=False)
-            wvpasseq(1, exr.rc)
-            wvpasseq(b'', exr.out.strip())
-
-def test_replace(get_disposition, src_info):
-    print('blarg:', repr(src_info), file=sys.stderr)
-
-    wvstart(get_disposition + ' --replace to root fails')
-    for item in (b'.tag/tinyfile',
-                 b'src/latest' + src_info['tinyfile-path'],
-                 b'.tag/subtree',
-                 b'src/latest' + src_info['subtree-vfs-path'],
-                 b'.tag/commit-1',
-                 b'src/latest',
-                 b'src'):
-        exr = run_get(get_disposition, b'--replace', (item, b'/'))
-        wvpassne(0, exr.rc)
-        verify_rx(br'impossible; can only overwrite branch or tag', exr.err)
-
-    tinyfile_id = src_info['tinyfile-id']
-    tinyfile_path = src_info['tinyfile-path']
-    subtree_vfs_path = src_info['subtree-vfs-path']
-    subtree_id = src_info['subtree-id']
-    commit_2_id = src_info['commit-2-id']
-    tree_2_id = src_info['tree-2-id']
-
-    # Anything to tag
-    existing_items = {'nothing' : None,
-                      'blob' : (b'.tag/tinyfile', b'.tag/obj'),
-                      'tree' : (b'.tag/tree-1', b'.tag/obj'),
-                      'commit': (b'.tag/commit-1', b'.tag/obj')}
-    for ex_type, ex_ref in items(existing_items):
-        wvstart(get_disposition + ' --replace ' + ex_type + ' with blob tag')
-        for item in (b'.tag/tinyfile', b'src/latest' + tinyfile_path):
-            exr = run_get(get_disposition, b'--replace', (item ,b'.tag/obj'),
-                          given=ex_ref)
-            wvpasseq(0, exr.rc)        
-            validate_blob(tinyfile_id, tinyfile_id)
-            verify_only_refs(heads=[], tags=(b'obj',))
-        wvstart(get_disposition + ' --replace ' + ex_type + ' with tree tag')
-        for item in (b'.tag/subtree',  b'src/latest' + subtree_vfs_path):
-            exr = run_get(get_disposition, b'--replace', (item, b'.tag/obj'),
-                          given=ex_ref)
-            validate_tree(subtree_id, subtree_id)
-            verify_only_refs(heads=[], tags=(b'obj',))
-        wvstart(get_disposition + ' --replace ' + ex_type + ' with commitish tag')
-        for item in (b'.tag/commit-2', b'src/latest', b'src'):
-            exr = run_get(get_disposition, b'--replace', (item, b'.tag/obj'),
-                          given=ex_ref)
-            validate_tagged_save(b'obj', getcwd() + b'/src',
-                                 commit_2_id, tree_2_id, b'src-2', exr.out)
-            verify_only_refs(heads=[], tags=(b'obj',))
-
-        # Committish to branch.
-        existing_items = (('nothing', None),
-                          ('branch', (b'.tag/commit-1', b'obj')))
-        for ex_type, ex_ref in existing_items:
-            for item_type, item in (('commit', b'.tag/commit-2'),
-                                    ('save', b'src/latest'),
-                                    ('branch', b'src')):
-                wvstart(get_disposition + ' --replace '
-                        + ex_type + ' with ' + item_type)
-                exr = run_get(get_disposition, b'--replace', (item, b'obj'),
-                              given=ex_ref)
-                validate_save(b'obj/latest', getcwd() + b'/src',
-                              commit_2_id, tree_2_id, b'src-2', exr.out)
-                verify_only_refs(heads=(b'obj',), tags=[])
-
-        # Not committish to branch
-        existing_items = (('nothing', None),
-                          ('branch', (b'.tag/commit-1', b'obj')))
-        for ex_type, ex_ref in existing_items:
-            for item_type, item in (('blob', b'.tag/tinyfile'),
-                                    ('blob', b'src/latest' + tinyfile_path),
-                                    ('tree', b'.tag/subtree'),
-                                    ('tree', b'src/latest' + subtree_vfs_path)):
-                wvstart(get_disposition + ' --replace branch with '
-                        + item_type + ' given ' + ex_type + ' fails')
-
-                exr = run_get(get_disposition, b'--replace', (item, b'obj'),
-                              given=ex_ref)
-                wvpassne(0, exr.rc)
-                verify_rx(br'cannot overwrite branch with .+ for', exr.err)
-
-        wvstart(get_disposition + ' --replace, implicit destinations')
-
-        exr = run_get(get_disposition, b'--replace', b'src')
-        validate_save(b'src/latest', getcwd() + b'/src',
-                      commit_2_id, tree_2_id, b'src-2', exr.out)
-        verify_only_refs(heads=(b'src',), tags=[])
-
-        exr = run_get(get_disposition, b'--replace', b'.tag/commit-2')
-        validate_tagged_save(b'commit-2', getcwd() + b'/src',
-                             commit_2_id, tree_2_id, b'src-2', exr.out)
-        verify_only_refs(heads=[], tags=(b'commit-2',))
-
-def test_ff(get_disposition, src_info):
-
-    wvstart(get_disposition + ' --ff to root fails')
-    tinyfile_path = src_info['tinyfile-path']
-    for item in (b'.tag/tinyfile', b'src/latest' + tinyfile_path):
-        exr = run_get(get_disposition, b'--ff', (item, b'/'))
-        wvpassne(0, exr.rc)
-        verify_rx(br'source for .+ must be a branch, save, or commit', exr.err)
-    subtree_vfs_path = src_info['subtree-vfs-path']
-    for item in (b'.tag/subtree', b'src/latest' + subtree_vfs_path):
-        exr = run_get(get_disposition, b'--ff', (item, b'/'))
-        wvpassne(0, exr.rc)
-        verify_rx(br'is impossible; can only --append a tree to a branch',
-                  exr.err)    
-    for item in (b'.tag/commit-1', b'src/latest', b'src'):
-        exr = run_get(get_disposition, b'--ff', (item, b'/'))
-        wvpassne(0, exr.rc)
-        verify_rx(br'destination for .+ is a root, not a branch', exr.err)
-
-    wvstart(get_disposition + ' --ff of not-committish fails')
-    for src in (b'.tag/tinyfile', b'src/latest' + tinyfile_path):
-        # FIXME: use get_item elsewhere?
-        for given, get_item in ((None, (src, b'obj')),
-                                (None, (src, b'.tag/obj')),
-                                ((b'.tag/tinyfile', b'.tag/obj'), (src, b'.tag/obj')),
-                                ((b'.tag/tree-1', b'.tag/obj'), (src, b'.tag/obj')),
-                                ((b'.tag/commit-1', b'.tag/obj'), (src, b'.tag/obj')),
-                                ((b'.tag/commit-1', b'obj'), (src, b'obj'))):
-            exr = run_get(get_disposition, b'--ff', get_item, given=given)
-            wvpassne(0, exr.rc)
-            verify_rx(br'must be a branch, save, or commit', exr.err)
-    for src in (b'.tag/subtree', b'src/latest' + subtree_vfs_path):
-        for given, get_item in ((None, (src, b'obj')),
-                                (None, (src, b'.tag/obj')),
-                                ((b'.tag/tinyfile', b'.tag/obj'), (src, b'.tag/obj')),
-                                ((b'.tag/tree-1', b'.tag/obj'), (src, b'.tag/obj')),
-                                ((b'.tag/commit-1', b'.tag/obj'), (src, b'.tag/obj')),
-                                ((b'.tag/commit-1', b'obj'), (src, b'obj'))):
-            exr = run_get(get_disposition, b'--ff', get_item, given=given)
-            wvpassne(0, exr.rc)
-            verify_rx(br'can only --append a tree to a branch', exr.err)
-
-    wvstart(get_disposition + ' --ff committish, ff possible')
-    save_2 = src_info['save-2']
-    for src in (b'.tag/commit-2', b'src/' + save_2, b'src'):
-        for given, get_item, complaint in \
-            ((None, (src, b'.tag/obj'),
-              br'destination .+ must be a valid branch name'),
-             ((b'.tag/tinyfile', b'.tag/obj'), (src, b'.tag/obj'),
-              br'destination .+ is a blob, not a branch'),
-             ((b'.tag/tree-1', b'.tag/obj'), (src, b'.tag/obj'),
-              br'destination .+ is a tree, not a branch'),
-             ((b'.tag/commit-1', b'.tag/obj'), (src, b'.tag/obj'),
-              br'destination .+ is a tagged commit, not a branch'),
-             ((b'.tag/commit-2', b'.tag/obj'), (src, b'.tag/obj'),
-              br'destination .+ is a tagged commit, not a branch')):
-            exr = run_get(get_disposition, b'--ff', get_item, given=given)
-            wvpassne(0, exr.rc)
-            verify_rx(complaint, exr.err)
-    # FIXME: use src or item and given or existing consistently in loops...
-    commit_2_id = src_info['commit-2-id']
-    tree_2_id = src_info['tree-2-id']
-    for src in (b'.tag/commit-2', b'src/' + save_2, b'src'):
-        for given in (None, (b'.tag/commit-1', b'obj'), (b'.tag/commit-2', b'obj')):
-            exr = run_get(get_disposition, b'--ff', (src, b'obj'), given=given)
-            wvpasseq(0, exr.rc)
-            validate_save(b'obj/latest', getcwd() + b'/src',
-                          commit_2_id, tree_2_id, b'src-2', exr.out)
-            verify_only_refs(heads=(b'obj',), tags=[])
-            
-    wvstart(get_disposition + ' --ff, implicit destinations')
-    for item in (b'src', b'src/latest'):
-        exr = run_get(get_disposition, b'--ff', item)
-        wvpasseq(0, exr.rc)
-
-        ex((b'find', b'get-dest/refs'))
-        ex((bup_cmd, b'-d', b'get-dest', b'ls'))
-
-        validate_save(b'src/latest', getcwd() + b'/src',
-                     commit_2_id, tree_2_id, b'src-2', exr.out)
-        #verify_only_refs(heads=('src',), tags=[])
-
-    wvstart(get_disposition + ' --ff, ff impossible')
-    for given, get_item in (((b'unrelated-branch', b'src'), b'src'),
-                            ((b'.tag/commit-2', b'src'), (b'.tag/commit-1', b'src'))):
-        exr = run_get(get_disposition, b'--ff', get_item, given=given)
-        wvpassne(0, exr.rc)
-        verify_rx(br'destination is not an ancestor of source', exr.err)
-
-def test_append(get_disposition, src_info):
-    tinyfile_path = src_info['tinyfile-path']
-    subtree_vfs_path = src_info['subtree-vfs-path']
-
-    wvstart(get_disposition + ' --append to root fails')
-    for item in (b'.tag/tinyfile', b'src/latest' + tinyfile_path):
-        exr = run_get(get_disposition, b'--append', (item, b'/'))
-        wvpassne(0, exr.rc)
-        verify_rx(br'source for .+ must be a branch, save, commit, or tree',
-                  exr.err)
-    for item in (b'.tag/subtree', b'src/latest' + subtree_vfs_path,
-                 b'.tag/commit-1', b'src/latest', b'src'):
-        exr = run_get(get_disposition, b'--append', (item, b'/'))
-        wvpassne(0, exr.rc)
-        verify_rx(br'destination for .+ is a root, not a branch', exr.err)
-
-    wvstart(get_disposition + ' --append of not-treeish fails')
-    for src in (b'.tag/tinyfile', b'src/latest' + tinyfile_path):
-        for given, item in ((None, (src, b'obj')),
-                            (None, (src, b'.tag/obj')),
-                            ((b'.tag/tinyfile', b'.tag/obj'), (src, b'.tag/obj')),
-                            ((b'.tag/tree-1', b'.tag/obj'), (src, b'.tag/obj')),
-                            ((b'.tag/commit-1', b'.tag/obj'), (src, b'.tag/obj')),
-                            ((b'.tag/commit-1', b'obj'), (src, b'obj'))):
-            exr = run_get(get_disposition, b'--append', item, given=given)
-            wvpassne(0, exr.rc)
-            verify_rx(br'must be a branch, save, commit, or tree', exr.err)
-
-    wvstart(get_disposition + ' --append committish failure cases')
-    save_2 = src_info['save-2']
-    for src in (b'.tag/subtree', b'src/latest' + subtree_vfs_path,
-                b'.tag/commit-2', b'src/' + save_2, b'src'):
-        for given, item, complaint in \
-            ((None, (src, b'.tag/obj'),
-              br'destination .+ must be a valid branch name'),
-             ((b'.tag/tinyfile', b'.tag/obj'), (src, b'.tag/obj'),
-              br'destination .+ is a blob, not a branch'),
-             ((b'.tag/tree-1', b'.tag/obj'), (src, b'.tag/obj'),
-              br'destination .+ is a tree, not a branch'),
-             ((b'.tag/commit-1', b'.tag/obj'), (src, b'.tag/obj'),
-              br'destination .+ is a tagged commit, not a branch'),
-             ((b'.tag/commit-2', b'.tag/obj'), (src, b'.tag/obj'),
-              br'destination .+ is a tagged commit, not a branch')):
-            exr = run_get(get_disposition, b'--append', item, given=given)
-            wvpassne(0, exr.rc)
-            verify_rx(complaint, exr.err)
-
-    wvstart(get_disposition + ' --append committish')
-    commit_2_id = src_info['commit-2-id']
-    tree_2_id = src_info['tree-2-id']
-    for item in (b'.tag/commit-2', b'src/' + save_2, b'src'):
-        for existing in (None, (b'.tag/commit-1', b'obj'),
-                         (b'.tag/commit-2', b'obj'),
-                         (b'unrelated-branch', b'obj')):
-            exr = run_get(get_disposition, b'--append', (item, b'obj'),
-                          given=existing)
-            wvpasseq(0, exr.rc)
-            validate_new_save(b'obj/latest', getcwd() + b'/src',
-                              commit_2_id, tree_2_id, b'src-2', exr.out)
-            verify_only_refs(heads=(b'obj',), tags=[])
-    # Append ancestor
-    save_1 = src_info['save-1']
-    commit_1_id = src_info['commit-1-id']
-    tree_1_id = src_info['tree-1-id']
-    for item in (b'.tag/commit-1',  b'src/' + save_1, b'src-1'):
-        exr = run_get(get_disposition, b'--append', (item, b'obj'),
-                      given=(b'.tag/commit-2', b'obj'))
-        wvpasseq(0, exr.rc)
-        validate_new_save(b'obj/latest', getcwd() + b'/src',
-                          commit_1_id, tree_1_id, b'src-1', exr.out)
-        verify_only_refs(heads=(b'obj',), tags=[])
-
-    wvstart(get_disposition + ' --append tree')
-    subtree_path = src_info['subtree-path']
-    subtree_id = src_info['subtree-id']
-    for item in (b'.tag/subtree', b'src/latest' + subtree_vfs_path):
-        for existing in (None,
-                         (b'.tag/commit-1', b'obj'),
-                         (b'.tag/commit-2', b'obj')):
-            exr = run_get(get_disposition, b'--append', (item, b'obj'),
-                          given=existing)
-            wvpasseq(0, exr.rc)
-            validate_new_save(b'obj/latest', b'/', None, subtree_id, subtree_path,
-                              exr.out)
-            verify_only_refs(heads=(b'obj',), tags=[])
-
-    wvstart(get_disposition + ' --append, implicit destinations')
-
-    for item in (b'src', b'src/latest'):
-        exr = run_get(get_disposition, b'--append', item)
-        wvpasseq(0, exr.rc)
-        validate_new_save(b'src/latest', getcwd() + b'/src', commit_2_id, tree_2_id,
-                          b'src-2', exr.out)
-        verify_only_refs(heads=(b'src',), tags=[])
-
-def test_pick(get_disposition, src_info, force=False):
-    flavor = b'--force-pick' if force else b'--pick'
-    flavormsg = flavor.decode('ascii')
-    tinyfile_path = src_info['tinyfile-path']
-    subtree_vfs_path = src_info['subtree-vfs-path']
-    
-    wvstart(get_disposition + ' ' + flavormsg + ' to root fails')
-    for item in (b'.tag/tinyfile', b'src/latest' + tinyfile_path, b'src'):
-        exr = run_get(get_disposition, flavor, (item, b'/'))
-        wvpassne(0, exr.rc)
-        verify_rx(br'can only pick a commit or save', exr.err)
-    for item in (b'.tag/commit-1', b'src/latest'):
-        exr = run_get(get_disposition, flavor, (item, b'/'))
-        wvpassne(0, exr.rc)
-        verify_rx(br'destination is not a tag or branch', exr.err)
-    for item in (b'.tag/subtree', b'src/latest' + subtree_vfs_path):
-        exr = run_get(get_disposition, flavor, (item, b'/'))
-        wvpassne(0, exr.rc)
-        verify_rx(br'is impossible; can only --append a tree', exr.err)
-
-    wvstart(get_disposition + ' ' + flavormsg + ' of blob or branch fails')
-    for item in (b'.tag/tinyfile', b'src/latest' + tinyfile_path, b'src'):
-        for given, get_item in ((None, (item, b'obj')),
-                                (None, (item, b'.tag/obj')),
-                                ((b'.tag/tinyfile', b'.tag/obj'), (item, b'.tag/obj')),
-                                ((b'.tag/tree-1', b'.tag/obj'), (item, b'.tag/obj')),
-                                ((b'.tag/commit-1', b'.tag/obj'), (item, b'.tag/obj')),
-                                ((b'.tag/commit-1', b'obj'), (item, b'obj'))):
-            exr = run_get(get_disposition, flavor, get_item, given=given)
-            wvpassne(0, exr.rc)
-            verify_rx(br'impossible; can only pick a commit or save', exr.err)
-
-    wvstart(get_disposition + ' ' + flavormsg + ' of tree fails')
-    for item in (b'.tag/subtree', b'src/latest' + subtree_vfs_path):
-        for given, get_item in ((None, (item, b'obj')),
-                                (None, (item, b'.tag/obj')),
-                                ((b'.tag/tinyfile', b'.tag/obj'), (item, b'.tag/obj')),
-                                ((b'.tag/tree-1', b'.tag/obj'), (item, b'.tag/obj')),
-                                ((b'.tag/commit-1', b'.tag/obj'), (item, b'.tag/obj')),
-                                ((b'.tag/commit-1', b'obj'), (item, b'obj'))):
-            exr = run_get(get_disposition, flavor, get_item, given=given)
-            wvpassne(0, exr.rc)
-            verify_rx(br'impossible; can only --append a tree', exr.err)
-
-    save_2 = src_info['save-2']
-    commit_2_id = src_info['commit-2-id']
-    tree_2_id = src_info['tree-2-id']
-    # FIXME: these two wvstart texts?
-    if force:
-        wvstart(get_disposition + ' ' + flavormsg + ' commit/save to existing tag')
-        for item in (b'.tag/commit-2', b'src/' + save_2):
-            for given in ((b'.tag/tinyfile', b'.tag/obj'),
-                          (b'.tag/tree-1', b'.tag/obj'),
-                          (b'.tag/commit-1', b'.tag/obj')):
-                exr = run_get(get_disposition, flavor, (item, b'.tag/obj'),
-                              given=given)
-                wvpasseq(0, exr.rc)
-                validate_new_tagged_commit(b'obj', commit_2_id, tree_2_id,
-                                           exr.out)
-                verify_only_refs(heads=[], tags=(b'obj',))
-    else: # --pick
-        wvstart(get_disposition + ' ' + flavormsg
-                + ' commit/save to existing tag fails')
-        for item in (b'.tag/commit-2', b'src/' + save_2):
-            for given in ((b'.tag/tinyfile', b'.tag/obj'),
-                          (b'.tag/tree-1', b'.tag/obj'),
-                          (b'.tag/commit-1', b'.tag/obj')):
-                exr = run_get(get_disposition, flavor, (item, b'.tag/obj'), given=given)
-                wvpassne(0, exr.rc)
-                verify_rx(br'cannot overwrite existing tag', exr.err)
-            
-    wvstart(get_disposition + ' ' + flavormsg + ' commit/save to tag')
-    for item in (b'.tag/commit-2', b'src/' + save_2):
-        exr = run_get(get_disposition, flavor, (item, b'.tag/obj'))
-        wvpasseq(0, exr.rc)
-        validate_clean_repo()
-        validate_new_tagged_commit(b'obj', commit_2_id, tree_2_id, exr.out)
-        verify_only_refs(heads=[], tags=(b'obj',))
-         
-    wvstart(get_disposition + ' ' + flavormsg + ' commit/save to branch')
-    for item in (b'.tag/commit-2', b'src/' + save_2):
-        for given in (None, (b'.tag/commit-1', b'obj'), (b'.tag/commit-2', b'obj')):
-            exr = run_get(get_disposition, flavor, (item, b'obj'), given=given)
-            wvpasseq(0, exr.rc)
-            validate_clean_repo()
-            validate_new_save(b'obj/latest', getcwd() + b'/src',
-                              commit_2_id, tree_2_id, b'src-2', exr.out)
-            verify_only_refs(heads=(b'obj',), tags=[])
-
-    wvstart(get_disposition + ' ' + flavormsg
-            + ' commit/save unrelated commit to branch')
-    for item in(b'.tag/commit-2', b'src/' + save_2):
-        exr = run_get(get_disposition, flavor, (item, b'obj'),
-                      given=(b'unrelated-branch', b'obj'))
-        wvpasseq(0, exr.rc)
-        validate_clean_repo()
-        validate_new_save(b'obj/latest', getcwd() + b'/src',
-                          commit_2_id, tree_2_id, b'src-2', exr.out)
-        verify_only_refs(heads=(b'obj',), tags=[])
-
-    wvstart(get_disposition + ' ' + flavormsg + ' commit/save ancestor to branch')
-    save_1 = src_info['save-1']
-    commit_1_id = src_info['commit-1-id']
-    tree_1_id = src_info['tree-1-id']
-    for item in (b'.tag/commit-1', b'src/' + save_1):
-        exr = run_get(get_disposition, flavor, (item, b'obj'),
-                      given=(b'.tag/commit-2', b'obj'))
-        wvpasseq(0, exr.rc)
-        validate_clean_repo()
-        validate_new_save(b'obj/latest', getcwd() + b'/src',
-                          commit_1_id, tree_1_id, b'src-1', exr.out)
-        verify_only_refs(heads=(b'obj',), tags=[])
-
-
-    wvstart(get_disposition + ' ' + flavormsg + ', implicit destinations')
-    exr = run_get(get_disposition, flavor, b'.tag/commit-2')
-    wvpasseq(0, exr.rc)
-    validate_clean_repo()
-    validate_new_tagged_commit(b'commit-2', commit_2_id, tree_2_id, exr.out)
-    verify_only_refs(heads=[], tags=(b'commit-2',))
-
-    exr = run_get(get_disposition, flavor, b'src/latest')
-    wvpasseq(0, exr.rc)
-    validate_clean_repo()
-    validate_new_save(b'src/latest', getcwd() + b'/src',
-                      commit_2_id, tree_2_id, b'src-2', exr.out)
-    verify_only_refs(heads=(b'src',), tags=[])
-
-def test_new_tag(get_disposition, src_info):
-    tinyfile_id = src_info['tinyfile-id']
-    tinyfile_path = src_info['tinyfile-path']
-    commit_2_id = src_info['commit-2-id']
-    tree_2_id = src_info['tree-2-id']
-    subtree_id = src_info['subtree-id']
-    subtree_vfs_path = src_info['subtree-vfs-path']
-
-    wvstart(get_disposition + ' --new-tag to root fails')
-    for item in (b'.tag/tinyfile',
-                 b'src/latest' + tinyfile_path,
-                 b'.tag/subtree',
-                 b'src/latest' + subtree_vfs_path,
-                 b'.tag/commit-1',
-                 b'src/latest',
-                 b'src'):
-        exr = run_get(get_disposition, b'--new-tag', (item, b'/'))
-        wvpassne(0, exr.rc)
-        verify_rx(br'destination for .+ must be a VFS tag', exr.err)
-
-    # Anything to new tag.
-    wvstart(get_disposition + ' --new-tag, blob tag')
-    for item in (b'.tag/tinyfile', b'src/latest' + tinyfile_path):
-        exr = run_get(get_disposition, b'--new-tag', (item, b'.tag/obj'))
-        wvpasseq(0, exr.rc)        
-        validate_blob(tinyfile_id, tinyfile_id)
-        verify_only_refs(heads=[], tags=(b'obj',))
-
-    wvstart(get_disposition + ' --new-tag, tree tag')
-    for item in (b'.tag/subtree', b'src/latest' + subtree_vfs_path):
-        exr = run_get(get_disposition, b'--new-tag', (item, b'.tag/obj'))
-        wvpasseq(0, exr.rc)        
-        validate_tree(subtree_id, subtree_id)
-        verify_only_refs(heads=[], tags=(b'obj',))
-        
-    wvstart(get_disposition + ' --new-tag, committish tag')
-    for item in (b'.tag/commit-2', b'src/latest', b'src'):
-        exr = run_get(get_disposition, b'--new-tag', (item, b'.tag/obj'))
-        wvpasseq(0, exr.rc)        
-        validate_tagged_save(b'obj', getcwd() + b'/src/', commit_2_id, tree_2_id,
-                             b'src-2', exr.out)
-        verify_only_refs(heads=[], tags=(b'obj',))
-
-    # Anything to existing tag (fails).
-    for ex_type, ex_tag in (('blob', (b'.tag/tinyfile', b'.tag/obj')),
-                            ('tree', (b'.tag/tree-1', b'.tag/obj')),
-                            ('commit', (b'.tag/commit-1', b'.tag/obj'))):
-        for item_type, item in (('blob tag', b'.tag/tinyfile'),
-                                ('blob path', b'src/latest' + tinyfile_path),
-                                ('tree tag', b'.tag/subtree'),
-                                ('tree path', b'src/latest' + subtree_vfs_path),
-                                ('commit tag', b'.tag/commit-2'),
-                                ('save', b'src/latest'),
-                                ('branch', b'src')):
-            wvstart(get_disposition + ' --new-tag of ' + item_type
-                    + ', given existing ' + ex_type + ' tag, fails')
-            exr = run_get(get_disposition, b'--new-tag', (item, b'.tag/obj'),
-                          given=ex_tag)
-            wvpassne(0, exr.rc)
-            verify_rx(br'cannot overwrite existing tag .* \(requires --replace\)',
-                      exr.err)
-
-    # Anything to branch (fails).
-    for ex_type, ex_tag in (('nothing', None),
-                            ('blob', (b'.tag/tinyfile', b'.tag/obj')),
-                            ('tree', (b'.tag/tree-1', b'.tag/obj')),
-                            ('commit', (b'.tag/commit-1', b'.tag/obj'))):
-        for item_type, item in (('blob tag', b'.tag/tinyfile'),
-                ('blob path', b'src/latest' + tinyfile_path),
-                ('tree tag', b'.tag/subtree'),
-                ('tree path', b'src/latest' + subtree_vfs_path),
-                ('commit tag', b'.tag/commit-2'),
-                ('save', b'src/latest'),
-                ('branch', b'src')):
-            wvstart(get_disposition + ' --new-tag to branch of ' + item_type
-                    + ', given existing ' + ex_type + ' tag, fails')
-            exr = run_get(get_disposition, b'--new-tag', (item, b'obj'),
-                          given=ex_tag)
-            wvpassne(0, exr.rc)
-            verify_rx(br'destination for .+ must be a VFS tag', exr.err)
-
-    wvstart(get_disposition + ' --new-tag, implicit destinations')
-    exr = run_get(get_disposition, b'--new-tag', b'.tag/commit-2')
-    wvpasseq(0, exr.rc)        
-    validate_tagged_save(b'commit-2', getcwd() + b'/src/', commit_2_id, tree_2_id,
-                         b'src-2', exr.out)
-    verify_only_refs(heads=[], tags=(b'commit-2',))
-
-def test_unnamed(get_disposition, src_info):
-    tinyfile_id = src_info['tinyfile-id']
-    tinyfile_path = src_info['tinyfile-path']
-    subtree_vfs_path = src_info['subtree-vfs-path']
-    wvstart(get_disposition + ' --unnamed to root fails')
-    for item in (b'.tag/tinyfile',
-                 b'src/latest' + tinyfile_path,
-                 b'.tag/subtree',
-                 b'src/latest' + subtree_vfs_path,
-                 b'.tag/commit-1',
-                 b'src/latest',
-                 b'src'):
-        for ex_ref in (None, (item, b'.tag/obj')):
-            exr = run_get(get_disposition, b'--unnamed', (item, b'/'),
-                          given=ex_ref)
-            wvpassne(0, exr.rc)
-            verify_rx(br'usage: bup get ', exr.err)
-
-    wvstart(get_disposition + ' --unnamed file')
-    for item in (b'.tag/tinyfile', b'src/latest' + tinyfile_path):
-        exr = run_get(get_disposition, b'--unnamed', item)
-        wvpasseq(0, exr.rc)        
-        validate_blob(tinyfile_id, tinyfile_id)
-        verify_only_refs(heads=[], tags=[])
-
-        exr = run_get(get_disposition, b'--unnamed', item,
-                      given=(item, b'.tag/obj'))
-        wvpasseq(0, exr.rc)        
-        validate_blob(tinyfile_id, tinyfile_id)
-        verify_only_refs(heads=[], tags=(b'obj',))
-
-    wvstart(get_disposition + ' --unnamed tree')
-    subtree_id = src_info['subtree-id']
-    for item in (b'.tag/subtree', b'src/latest' + subtree_vfs_path):
-        exr = run_get(get_disposition, b'--unnamed', item)
-        wvpasseq(0, exr.rc)        
-        validate_tree(subtree_id, subtree_id)
-        verify_only_refs(heads=[], tags=[])
-        
-        exr = run_get(get_disposition, b'--unnamed', item,
-                      given=(item, b'.tag/obj'))
-        wvpasseq(0, exr.rc)        
-        validate_tree(subtree_id, subtree_id)
-        verify_only_refs(heads=[], tags=(b'obj',))
-        
-    wvstart(get_disposition + ' --unnamed committish')
-    save_2 = src_info['save-2']
-    commit_2_id = src_info['commit-2-id']
-    for item in (b'.tag/commit-2', b'src/' + save_2, b'src'):
-        exr = run_get(get_disposition, b'--unnamed', item)
-        wvpasseq(0, exr.rc)        
-        validate_commit(commit_2_id, commit_2_id)
-        verify_only_refs(heads=[], tags=[])
-
-        exr = run_get(get_disposition, b'--unnamed', item,
-                      given=(item, b'.tag/obj'))
-        wvpasseq(0, exr.rc)        
-        validate_commit(commit_2_id, commit_2_id)
-        verify_only_refs(heads=[], tags=(b'obj',))
-
-def create_get_src():
-    global bup_cmd, src_info
-    wvstart('preparing')
-    ex((bup_cmd, b'-d', b'get-src', b'init'))
-
-    mkdir(b'src')
-    open(b'src/unrelated', 'a').close()
-    ex((bup_cmd, b'-d', b'get-src', b'index', b'src'))
-    ex((bup_cmd, b'-d', b'get-src', b'save', b'-tcn', b'unrelated-branch', b'src'))
-
-    ex((bup_cmd, b'-d', b'get-src', b'index', b'--clear'))
-    rmrf(b'src')
-    mkdir(b'src')
-    open(b'src/zero', 'a').close()
-    ex((bup_cmd, b'-d', b'get-src', b'index', b'src'))
-    exr = exo((bup_cmd, b'-d', b'get-src', b'save', b'-tcn', b'src', b'src'))
-    out = exr.out.splitlines()
-    tree_0_id = out[0]
-    commit_0_id = out[-1]
-    exr = exo((bup_cmd, b'-d', b'get-src', b'ls', b'src'))
-    save_0 = exr.out.splitlines()[0]
-    ex((b'git', b'--git-dir', b'get-src', b'branch', b'src-0', b'src'))
-    ex((b'cp', b'-RPp', b'src', b'src-0'))
-    
-    rmrf(b'src')
-    mkdir(b'src')
-    mkdir(b'src/x')
-    mkdir(b'src/x/y')
-    ex((bup_cmd + b' -d get-src random 1k > src/1'), shell=True)
-    ex((bup_cmd + b' -d get-src random 1k > src/x/2'), shell=True)
-    ex((bup_cmd, b'-d', b'get-src', b'index', b'src'))
-    exr = exo((bup_cmd, b'-d', b'get-src', b'save', b'-tcn', b'src', b'src'))
-    out = exr.out.splitlines()
-    tree_1_id = out[0]
-    commit_1_id = out[-1]
-    exr = exo((bup_cmd, b'-d', b'get-src', b'ls', b'src'))
-    save_1 = exr.out.splitlines()[1]
-    ex((b'git', b'--git-dir', b'get-src', b'branch', b'src-1', b'src'))
-    ex((b'cp', b'-RPp', b'src', b'src-1'))
-    
-    # Make a copy the current state of src so we'll have an ancestor.
-    ex((b'cp', b'-RPp',
-         b'get-src/refs/heads/src', b'get-src/refs/heads/src-ancestor'))
-
-    with open(b'src/tiny-file', 'ab') as f: f.write(b'xyzzy')
-    ex((bup_cmd, b'-d', b'get-src', b'index', b'src'))
-    ex((bup_cmd, b'-d', b'get-src', b'tick'))  # Ensure the save names differ
-    exr = exo((bup_cmd, b'-d', b'get-src', b'save', b'-tcn', b'src', b'src'))
-    out = exr.out.splitlines()
-    tree_2_id = out[0]
-    commit_2_id = out[-1]
-    exr = exo((bup_cmd, b'-d', b'get-src', b'ls', b'src'))
-    save_2 = exr.out.splitlines()[2]
-    rename(b'src', b'src-2')
-
-    src_root = getcwd() + b'/src'
-
-    subtree_path = b'src-2/x'
-    subtree_vfs_path = src_root + b'/x'
-
-    # No support for "ls -d", so grep...
-    exr = exo((bup_cmd, b'-d', b'get-src', b'ls', b'-s', b'src/latest' + src_root))
-    out = exr.out.splitlines()
-    subtree_id = None
-    for line in out:
-        if b'x' in line:
-            subtree_id = line.split()[0]
-    assert(subtree_id)
-
-    # With a tiny file, we'll get a single blob, not a chunked tree
-    tinyfile_path = src_root + b'/tiny-file'
-    exr = exo((bup_cmd, b'-d', b'get-src', b'ls', b'-s', b'src/latest' + tinyfile_path))
-    tinyfile_id = exr.out.splitlines()[0].split()[0]
-
-    ex((bup_cmd, b'-d', b'get-src', b'tag', b'tinyfile', tinyfile_id))
-    ex((bup_cmd, b'-d', b'get-src', b'tag', b'subtree', subtree_id))
-    ex((bup_cmd, b'-d', b'get-src', b'tag', b'tree-0', tree_0_id))
-    ex((bup_cmd, b'-d', b'get-src', b'tag', b'tree-1', tree_1_id))
-    ex((bup_cmd, b'-d', b'get-src', b'tag', b'tree-2', tree_2_id))
-    ex((bup_cmd, b'-d', b'get-src', b'tag', b'commit-0', commit_0_id))
-    ex((bup_cmd, b'-d', b'get-src', b'tag', b'commit-1', commit_1_id))
-    ex((bup_cmd, b'-d', b'get-src', b'tag', b'commit-2', commit_2_id))
-    ex((b'git', b'--git-dir', b'get-src', b'branch', b'commit-1', commit_1_id))
-    ex((b'git', b'--git-dir', b'get-src', b'branch', b'commit-2', commit_2_id))
-
-    return {'tinyfile-path' : tinyfile_path,
-            'tinyfile-id' : tinyfile_id,
-            'subtree-id' : subtree_id,
-            'tree-0-id' : tree_0_id,
-            'tree-1-id' : tree_1_id,
-            'tree-2-id' : tree_2_id,
-            'commit-0-id' : commit_0_id,
-            'commit-1-id' : commit_1_id,
-            'commit-2-id' : commit_2_id,
-            'save-1' : save_1,
-            'save-2' : save_2,
-            'subtree-path' : subtree_path,
-            'subtree-vfs-path' : subtree_vfs_path}
-    
-# FIXME: this fails in a strange way:
-#   WVPASS given nothing get --ff not-there
-
-dispositions_to_test = ('get',)
-
-if int(environ.get(b'BUP_TEST_LEVEL', b'0')) >= 11:
-    dispositions_to_test += ('get-on', 'get-to')
-
-if len(compat.argv) == 1:
-    categories = ('replace', 'universal', 'ff', 'append', 'pick', 'new-tag',
-             'unnamed')
-else:
-    categories = compat.argv[1:]
-    
-with test_tempdir(b'get-') as tmpdir:
-    chdir(tmpdir)
-    try:
-        src_info = create_get_src()
-        for category in categories:
-            for disposition in dispositions_to_test:
-                # given=FOO depends on --replace, so test it early
-                if category == 'replace':
-                    test_replace(disposition, src_info)
-                elif category == 'universal':
-                    test_universal_behaviors(disposition)
-                elif category == 'ff':
-                    test_ff(disposition, src_info)
-                elif category == 'append':
-                    test_append(disposition, src_info)
-                elif category == 'pick':
-                    test_pick(disposition, src_info, force=False)
-                    test_pick(disposition, src_info, force=True)
-                elif category == 'new-tag':
-                    test_new_tag(disposition, src_info)
-                elif category == 'unnamed':
-                    test_unnamed(disposition, src_info)
-                else:
-                    raise Exception('unrecognized get test category')
-    except Exception as ex:
-        chdir(top)
-        raise
-    chdir(top)
-
-wvmsg('checked %d cases' % get_cases_tested)
diff --git a/test/ext/test-prune-older b/test/ext/test-prune-older
deleted file mode 100755 (executable)
index 0c8b623..0000000
+++ /dev/null
@@ -1,230 +0,0 @@
-#!/bin/sh
-"""": # -*-python-*-
-bup_python="$(dirname "$0")/../../dev/bup-python" || exit $?
-exec "$bup_python" "$0" ${1+"$@"}
-"""
-# end of bup preamble
-
-from __future__ import absolute_import, print_function
-from collections import defaultdict
-from itertools import chain, dropwhile, groupby, takewhile
-from os import chdir
-from os.path import abspath, dirname
-from random import choice, randint
-from shutil import copytree, rmtree
-from subprocess import PIPE
-from sys import stderr
-from time import localtime, strftime, time
-import os, random, sys
-
-if sys.version_info[:2] >= (3, 5):
-    from difflib import diff_bytes, unified_diff
-else:
-    from difflib import unified_diff
-
-# For buptest, wvtest, ...
-sys.path[:0] = (abspath(os.path.dirname(__file__) + '/../..'),)
-sys.path[:0] = (abspath(os.path.dirname(__file__) + '/../../test/lib'),)
-sys.path[:0] = [os.path.dirname(os.path.realpath(__file__)) + '/../../lib']
-
-from buptest import ex, exo, test_tempdir
-from wvtest import wvfail, wvpass, wvpasseq, wvpassne, wvstart
-
-from bup import compat
-from bup.compat import environ
-from bup.helpers import partition, period_as_secs, readpipe
-from bup.io import byte_stream
-import bup.path
-
-if sys.version_info[:2] < (3, 5):
-    def diff_bytes(_, *args):
-        return unified_diff(*args)
-
-def create_older_random_saves(n, start_utc, end_utc):
-    with open(b'foo', 'wb') as f:
-        pass
-    ex([b'git', b'add', b'foo'])
-    utcs = set()
-    while len(utcs) != n:
-        utcs.add(randint(start_utc, end_utc))
-    utcs = sorted(utcs)
-    for utc in utcs:
-        with open(b'foo', 'wb') as f:
-            f.write(b'%d\n' % utc)
-        ex([b'git', b'commit', b'--date', b'%d' % utc, b'-qam', b'%d' % utc])
-    ex([b'git', b'gc', b'--aggressive'])
-    return utcs
-
-# There is corresponding code in bup for some of this, but the
-# computation method is different here, in part so that the test can
-# provide a more effective cross-check.
-
-period_kinds = [b'all', b'dailies', b'monthlies', b'yearlies']
-period_scale = {b's': 1,
-                b'min': 60,
-                b'h': 60 * 60,
-                b'd': 60 * 60 * 24,
-                b'w': 60 * 60 * 24 * 7,
-                b'm': 60 * 60 * 24 * 31,
-                b'y': 60 * 60 * 24 * 366}
-period_scale_kinds = list(period_scale.keys())
-
-def expected_retentions(utcs, utc_start, spec):
-    if not spec:
-        return utcs
-    utcs = sorted(utcs, reverse=True)
-    period_start = dict(spec)
-    for kind, duration in compat.items(period_start):
-        period_start[kind] = utc_start - period_as_secs(duration)
-    period_start = defaultdict(lambda: float('inf'), period_start)
-
-    all = list(takewhile(lambda x: x >= period_start[b'all'], utcs))
-    utcs = list(dropwhile(lambda x: x >= period_start[b'all'], utcs))
-
-    matches = takewhile(lambda x: x >= period_start[b'dailies'], utcs)
-    dailies = [max(day_utcs) for yday, day_utcs
-               in groupby(matches, lambda x: localtime(x).tm_yday)]
-    utcs = list(dropwhile(lambda x: x >= period_start[b'dailies'], utcs))
-
-    matches = takewhile(lambda x: x >= period_start[b'monthlies'], utcs)
-    monthlies = [max(month_utcs) for month, month_utcs
-                 in groupby(matches, lambda x: localtime(x).tm_mon)]
-    utcs = dropwhile(lambda x: x >= period_start[b'monthlies'], utcs)
-
-    matches = takewhile(lambda x: x >= period_start[b'yearlies'], utcs)
-    yearlies = [max(year_utcs) for year, year_utcs
-                in groupby(matches, lambda x: localtime(x).tm_year)]
-
-    return chain(all, dailies, monthlies, yearlies)
-
-def period_spec(start_utc, end_utc):
-    global period_kinds, period_scale, period_scale_kinds
-    result = []
-    desired_specs = randint(1, 2 * len(period_kinds))
-    assert(desired_specs >= 1)  # At least one --keep argument is required
-    while len(result) < desired_specs:
-        period = None
-        if randint(1, 100) <= 5:
-            period = b'forever'
-        else:
-            assert(end_utc > start_utc)
-            period_secs = randint(1, end_utc - start_utc)
-            scale = choice(period_scale_kinds)
-            mag = int(float(period_secs) / period_scale[scale])
-            if mag != 0:
-                period = (b'%d' % mag) + scale
-        if period:
-            result += [(choice(period_kinds), period)]
-    return tuple(result)
-
-def unique_period_specs(n, start_utc, end_utc):
-    invocations = set()
-    while len(invocations) < n:
-        invocations.add(period_spec(start_utc, end_utc))
-    return tuple(invocations)
-
-def period_spec_to_period_args(spec):
-    return tuple(chain(*((b'--keep-' + kind + b'-for', period)
-                         for kind, period in spec)))
-
-def result_diffline(x):
-    return (b'%d %s\n'
-            % (x, strftime(' %Y-%m-%d-%H%M%S', localtime(x)).encode('ascii')))
-
-def check_prune_result(expected):
-    actual = sorted([int(x)
-                     for x in exo([b'git', b'log',
-                                   b'--pretty=format:%at']).out.splitlines()])
-
-    if expected != actual:
-        for x in expected:
-            print('ex:', x, strftime('%Y-%m-%d-%H%M%S', localtime(x)),
-                  file=stderr)
-        for line in diff_bytes(unified_diff,
-                               [result_diffline(x) for x in expected],
-                               [result_diffline(x) for x in actual],
-                               fromfile=b'expected', tofile=b'actual'):
-            sys.stderr.flush()
-            byte_stream(sys.stderr).write(line)
-    wvpass(expected == actual)
-
-
-environ[b'GIT_AUTHOR_NAME'] = b'bup test'
-environ[b'GIT_COMMITTER_NAME'] = b'bup test'
-environ[b'GIT_AUTHOR_EMAIL'] = b'bup@a425bc70a02811e49bdf73ee56450e6f'
-environ[b'GIT_COMMITTER_EMAIL'] = b'bup@a425bc70a02811e49bdf73ee56450e6f'
-
-seed = int(environ.get(b'BUP_TEST_SEED', time()))
-random.seed(seed)
-print('random seed:', seed, file=stderr)
-
-save_population = int(environ.get(b'BUP_TEST_PRUNE_OLDER_SAVES', 2000))
-prune_cycles = int(environ.get(b'BUP_TEST_PRUNE_OLDER_CYCLES', 20))
-prune_gc_cycles = int(environ.get(b'BUP_TEST_PRUNE_OLDER_GC_CYCLES', 10))
-
-bup_cmd = bup.path.exe()
-
-with test_tempdir(b'prune-older-') as tmpdir:
-    environ[b'BUP_DIR'] = tmpdir + b'/work/.git'
-    environ[b'GIT_DIR'] = tmpdir + b'/work/.git'
-    now = int(time())
-    three_years_ago = now - (60 * 60 * 24 * 366 * 3)
-    chdir(tmpdir)
-    ex([b'git', b'init', b'work'])
-    ex([b'git', b'config', b'gc.autoDetach', b'false'])
-
-    wvstart('generating ' + str(save_population) + ' random saves')
-    chdir(tmpdir + b'/work')
-    save_utcs = create_older_random_saves(save_population, three_years_ago, now)
-    chdir(tmpdir)
-    test_set_hash = exo([b'git', b'show-ref', b'-s', b'master']).out.rstrip()
-    ls_saves = exo((bup_cmd, b'ls', b'master')).out.splitlines()
-    wvpasseq(save_population + 1, len(ls_saves))
-
-    wvstart('ensure everything kept, if no keep arguments')
-    ex([b'git', b'reset', b'--hard', test_set_hash])
-    proc = ex((bup_cmd,
-               b'prune-older', b'-v', b'--unsafe', b'--no-gc',
-               b'--wrt', b'%d' % now) \
-              + (b'master',),
-              stdout=None, stderr=PIPE, check=False)
-    wvpassne(proc.rc, 0)
-    wvpass(b'at least one keep argument is required' in proc.err)
-    check_prune_result(save_utcs)
-
-
-    wvstart('running %d generative no-gc tests on %d saves' % (prune_cycles,
-                                                               save_population))
-    for spec in unique_period_specs(prune_cycles,
-                                    # Make it more likely we'll have
-                                    # some outside the save range.
-                                    three_years_ago - period_scale[b'm'],
-                                    now):
-        ex([b'git', b'reset', b'--hard', test_set_hash])
-        expected = sorted(expected_retentions(save_utcs, now, spec))
-        ex((bup_cmd,
-            b'prune-older', b'-v', b'--unsafe', b'--no-gc', b'--wrt',
-            b'%d' % now) \
-           + period_spec_to_period_args(spec) \
-           + (b'master',))
-        check_prune_result(expected)
-
-
-    # More expensive because we have to recreate the repo each time
-    wvstart('running %d generative gc tests on %d saves' % (prune_gc_cycles,
-                                                            save_population))
-    ex([b'git', b'reset', b'--hard', test_set_hash])
-    copytree(b'work/.git', b'clean-test-repo', symlinks=True)
-    for spec in unique_period_specs(prune_gc_cycles,
-                                    # Make it more likely we'll have
-                                    # some outside the save range.
-                                    three_years_ago - period_scale[b'm'],
-                                    now):
-        rmtree(b'work/.git')
-        copytree(b'clean-test-repo', b'work/.git')
-        expected = sorted(expected_retentions(save_utcs, now, spec))
-        ex((bup_cmd,
-            b'prune-older', b'-v', b'--unsafe', b'--wrt', b'%d' % now) \
-           + period_spec_to_period_args(spec) \
-           + (b'master',))
-        check_prune_result(expected)
diff --git a/test/ext/test_argv.py b/test/ext/test_argv.py
new file mode 100644 (file)
index 0000000..cc13fd7
--- /dev/null
@@ -0,0 +1,18 @@
+
+from __future__ import absolute_import, print_function
+
+from random import randint
+from subprocess import CalledProcessError, check_output
+from sys import stderr, stdout
+
+
+from test.lib.wvpytest import wvpasseq
+
+def rand_bytes(n):
+    return bytes([randint(1, 255) for x in range(n)])
+
+def test_argv():
+    for trial in range(100):
+        cmd = [b'dev/echo-argv-bytes', rand_bytes(randint(1, 32))]
+        out = check_output(cmd)
+        wvpasseq(b'\0\n'.join(cmd) + b'\0\n', out)
diff --git a/test/ext/test_ftp.py b/test/ext/test_ftp.py
new file mode 100644 (file)
index 0000000..d6bb273
--- /dev/null
@@ -0,0 +1,129 @@
+
+from __future__ import absolute_import, print_function
+from os import chdir, mkdir, symlink, unlink
+from subprocess import PIPE
+from time import localtime, strftime, tzset
+
+from bup.compat import environ
+from bup.helpers import unlink as unlink_if_exists
+from buptest import ex, exo
+from wvpytest import wvfail, wvpass, wvpasseq, wvpassne, wvstart
+import bup.path
+
+bup_cmd = bup.path.exe()
+
+def bup(*args, **kwargs):
+    if 'stdout' not in kwargs:
+        return exo((bup_cmd,) + args, **kwargs)
+    return ex((bup_cmd,) + args, **kwargs)
+
+def jl(*lines):
+    return b''.join(line + b'\n' for line in lines)
+
+environ[b'GIT_AUTHOR_NAME'] = b'bup test'
+environ[b'GIT_COMMITTER_NAME'] = b'bup test'
+environ[b'GIT_AUTHOR_EMAIL'] = b'bup@a425bc70a02811e49bdf73ee56450e6f'
+environ[b'GIT_COMMITTER_EMAIL'] = b'bup@a425bc70a02811e49bdf73ee56450e6f'
+
+import subprocess
+
+def test_ftp(tmpdir):
+    environ[b'BUP_DIR'] = tmpdir + b'/repo'
+    environ[b'GIT_DIR'] = tmpdir + b'/repo'
+    environ[b'TZ'] = b'UTC'
+    tzset()
+
+    chdir(tmpdir)
+    mkdir(b'src')
+    chdir(b'src')
+    mkdir(b'dir')
+    with open(b'file-1', 'wb') as f:
+        f.write(b'excitement!\n')
+    with open(b'dir/file-2', 'wb') as f:
+        f.write(b'more excitement!\n')
+    symlink(b'file-1', b'file-symlink')
+    symlink(b'dir', b'dir-symlink')
+    symlink(b'not-there', b'bad-symlink')
+
+    chdir(tmpdir)    
+    bup(b'init')
+    bup(b'index', b'src')
+    bup(b'save', b'-n', b'src', b'--strip', b'src')
+    save_utc = int(exo((b'git', b'show',
+                        b'-s', b'--format=%at', b'src')).out.strip())
+    save_name = strftime('%Y-%m-%d-%H%M%S', localtime(save_utc)).encode('ascii')
+    
+    wvstart('help')
+    wvpasseq(b'Commands: ls cd pwd cat get mget help quit\n',
+             exo((bup_cmd, b'ftp'), input=b'help\n', stderr=PIPE).out)
+
+    wvstart('pwd/cd')
+    wvpasseq(b'/\n', bup(b'ftp', input=b'pwd\n').out)
+    wvpasseq(b'', bup(b'ftp', input=b'cd src\n').out)
+    wvpasseq(b'/src\n', bup(b'ftp', input=jl(b'cd src', b'pwd')).out)
+    wvpasseq(b'/src\n/\n', bup(b'ftp', input=jl(b'cd src', b'pwd',
+                                                b'cd ..', b'pwd')).out)
+    wvpasseq(b'/src\n/\n', bup(b'ftp', input=jl(b'cd src', b'pwd',
+                                                b'cd ..', b'cd ..',
+                                                b'pwd')).out)
+    wvpasseq(b'/src/%s/dir\n' % save_name,
+             bup(b'ftp', input=jl(b'cd src/latest/dir-symlink', b'pwd')).out)
+    wvpasseq(b'/src/%s/dir\n' % save_name,
+             bup(b'ftp', input=jl(b'cd src latest dir-symlink', b'pwd')).out)
+    wvpassne(0, bup(b'ftp',
+                    input=jl(b'cd src/latest/bad-symlink', b'pwd'),
+                    check=False, stdout=None).rc)
+    wvpassne(0, bup(b'ftp',
+                    input=jl(b'cd src/latest/not-there', b'pwd'),
+                    check=False, stdout=None).rc)
+
+    wvstart('ls')
+    # FIXME: elaborate
+    wvpasseq(b'src\n', bup(b'ftp', input=b'ls\n').out)
+    wvpasseq(save_name + b'\nlatest\n',
+             bup(b'ftp', input=b'ls src\n').out)
+
+    wvstart('cat')
+    wvpasseq(b'excitement!\n',
+             bup(b'ftp', input=b'cat src/latest/file-1\n').out)
+    wvpasseq(b'excitement!\nmore excitement!\n',
+             bup(b'ftp',
+                 input=b'cat src/latest/file-1 src/latest/dir/file-2\n').out)
+    
+    wvstart('get')
+    bup(b'ftp', input=jl(b'get src/latest/file-1 dest'))
+    with open(b'dest', 'rb') as f:
+        wvpasseq(b'excitement!\n', f.read())
+    unlink(b'dest')
+    bup(b'ftp', input=jl(b'get src/latest/file-symlink dest'))
+    with open(b'dest', 'rb') as f:
+        wvpasseq(b'excitement!\n', f.read())
+    unlink(b'dest')
+    wvpassne(0, bup(b'ftp',
+                    input=jl(b'get src/latest/bad-symlink dest'),
+                    check=False, stdout=None).rc)
+    wvpassne(0, bup(b'ftp',
+                    input=jl(b'get src/latest/not-there'),
+                    check=False, stdout=None).rc)
+    
+    wvstart('mget')
+    unlink_if_exists(b'file-1')
+    bup(b'ftp', input=jl(b'mget src/latest/file-1'))
+    with open(b'file-1', 'rb') as f:
+        wvpasseq(b'excitement!\n', f.read())
+    unlink_if_exists(b'file-1')
+    unlink_if_exists(b'file-2')
+    bup(b'ftp', input=jl(b'mget src/latest/file-1 src/latest/dir/file-2'))
+    with open(b'file-1', 'rb') as f:
+        wvpasseq(b'excitement!\n', f.read())
+    with open(b'file-2', 'rb') as f:
+        wvpasseq(b'more excitement!\n', f.read())
+    unlink_if_exists(b'file-symlink')
+    bup(b'ftp', input=jl(b'mget src/latest/file-symlink'))
+    with open(b'file-symlink', 'rb') as f:
+        wvpasseq(b'excitement!\n', f.read())
+    wvpassne(0, bup(b'ftp',
+                    input=jl(b'mget src/latest/bad-symlink dest'),
+                    check=False, stdout=None).rc)
+    # bup mget currently always does pattern matching
+    bup(b'ftp', input=b'mget src/latest/not-there\n')
diff --git a/test/ext/test_get.py b/test/ext/test_get.py
new file mode 100644 (file)
index 0000000..86cc756
--- /dev/null
@@ -0,0 +1,968 @@
+
+from __future__ import print_function
+from errno import ENOENT
+from itertools import product
+from os import chdir, mkdir, rename
+from shutil import rmtree
+from subprocess import PIPE
+import pytest, re, sys
+
+from bup import compat, path
+from bup.compat import environ, getcwd, items
+from bup.helpers import bquote, merge_dict, unlink
+from bup.io import byte_stream
+from buptest import ex, exo
+from wvpytest import wvcheck, wvfail, wvmsg, wvpass, wvpasseq, wvpassne, wvstart
+import bup.path
+
+
+sys.stdout.flush()
+stdout = byte_stream(sys.stdout)
+
+# FIXME: per-test function
+environ[b'GIT_AUTHOR_NAME'] = b'bup test-get'
+environ[b'GIT_COMMITTER_NAME'] = b'bup test-get'
+environ[b'GIT_AUTHOR_EMAIL'] = b'bup@85430dcca2b611e4b2c3-8f5691723476'
+environ[b'GIT_COMMITTER_EMAIL'] = b'bup@85430dcca2b611e4b2c3-8f5691723476'
+
+# The clean-repo test can probably be applied more broadly.  It was
+# initially just applied to test-pick to catch a bug.
+
+top = getcwd()
+bup_cmd = bup.path.exe()
+
+def rmrf(path):
+    err = []  # because python's scoping mess...
+    def onerror(function, path, excinfo):
+        err.append((function, path, excinfo))
+    rmtree(path, onerror=onerror)
+    if err:
+        function, path, excinfo = err[0]
+        ex_type, ex, traceback = excinfo
+        if (not isinstance(ex, OSError)) or ex.errno != ENOENT:
+            raise ex
+
+def verify_trees_match(path1, path2):
+    global top
+    exr = exo((top + b'/dev/compare-trees', b'-c', path1, path2), check=False)
+    stdout.write(exr.out)
+    sys.stdout.flush()
+    wvcheck(exr.rc == 0, 'process exit %d == 0' % exr.rc)
+
+def verify_rcz(cmd, **kwargs):
+    assert not kwargs.get('check')
+    kwargs['check'] = False
+    result = exo(cmd, **kwargs)
+    stdout.write(result.out)
+    rc = result.proc.returncode
+    wvcheck(rc == 0, 'process exit %d == 0' % rc)
+    return result
+
+# FIXME: multline, or allow opts generally?
+
+def verify_rx(rx, string):
+    wvcheck(re.search(rx, string), 'rx %r matches %r' % (rx, string))
+
+def verify_nrx(rx, string):
+    wvcheck(not re.search(rx, string), "rx %r doesn't match %r" % (rx, string))
+
+def validate_clean_repo():
+    out = verify_rcz((b'git', b'--git-dir', b'get-dest', b'fsck')).out
+    verify_nrx(br'dangling|mismatch|missing|unreachable', out)
+    
+def validate_blob(src_id, dest_id):
+    global top
+    rmrf(b'restore-src')
+    rmrf(b'restore-dest')
+    cat_tree = top + b'/dev/git-cat-tree'
+    src_blob = verify_rcz((cat_tree, b'--git-dir', b'get-src', src_id)).out
+    dest_blob = verify_rcz((cat_tree, b'--git-dir', b'get-src', src_id)).out
+    wvpasseq(src_blob, dest_blob)
+
+def validate_tree(src_id, dest_id):
+
+    rmrf(b'restore-src')
+    rmrf(b'restore-dest')
+    mkdir(b'restore-src')
+    mkdir(b'restore-dest')
+    
+    commit_env = merge_dict(environ, {b'GIT_COMMITTER_DATE': b'2014-01-01 01:01'})
+
+    # Create a commit so the archive contents will have matching timestamps.
+    src_c = exo((b'git', b'--git-dir', b'get-src',
+                 b'commit-tree', b'-m', b'foo', src_id),
+                env=commit_env).out.strip()
+    dest_c = exo((b'git', b'--git-dir', b'get-dest',
+                  b'commit-tree', b'-m', b'foo', dest_id),
+                 env=commit_env).out.strip()
+    exr = verify_rcz(b'git --git-dir get-src archive %s | tar xvf - -C restore-src'
+                     % bquote(src_c),
+                     shell=True)
+    if exr.rc != 0: return False
+    exr = verify_rcz(b'git --git-dir get-dest archive %s | tar xvf - -C restore-dest'
+                     % bquote(dest_c),
+                     shell=True)
+    if exr.rc != 0: return False
+    
+    # git archive doesn't include an entry for ./.
+    unlink(b'restore-src/pax_global_header')
+    unlink(b'restore-dest/pax_global_header')
+    ex((b'touch', b'-r', b'restore-src', b'restore-dest'))
+    verify_trees_match(b'restore-src/', b'restore-dest/')
+    rmrf(b'restore-src')
+    rmrf(b'restore-dest')
+
+def validate_commit(src_id, dest_id):
+    exr = verify_rcz((b'git', b'--git-dir', b'get-src', b'cat-file', b'commit', src_id))
+    if exr.rc != 0: return False
+    src_cat = exr.out
+    exr = verify_rcz((b'git', b'--git-dir', b'get-dest', b'cat-file', b'commit', dest_id))
+    if exr.rc != 0: return False
+    dest_cat = exr.out
+    wvpasseq(src_cat, dest_cat)
+    if src_cat != dest_cat: return False
+    
+    rmrf(b'restore-src')
+    rmrf(b'restore-dest')
+    mkdir(b'restore-src')
+    mkdir(b'restore-dest')
+    qsrc = bquote(src_id)
+    qdest = bquote(dest_id)
+    exr = verify_rcz((b'git --git-dir get-src archive ' + qsrc
+                      + b' | tar xf - -C restore-src'),
+                     shell=True)
+    if exr.rc != 0: return False
+    exr = verify_rcz((b'git --git-dir get-dest archive ' + qdest +
+                      b' | tar xf - -C restore-dest'),
+                     shell=True)
+    if exr.rc != 0: return False
+    
+    # git archive doesn't include an entry for ./.
+    ex((b'touch', b'-r', b'restore-src', b'restore-dest'))
+    verify_trees_match(b'restore-src/', b'restore-dest/')
+    rmrf(b'restore-src')
+    rmrf(b'restore-dest')
+
+def _validate_save(orig_dir, save_path, commit_id, tree_id):
+    global bup_cmd
+    rmrf(b'restore')
+    exr = verify_rcz((bup_cmd, b'-d', b'get-dest',
+                      b'restore', b'-C', b'restore', save_path + b'/.'))
+    if exr.rc: return False
+    verify_trees_match(orig_dir + b'/', b'restore/')
+    if tree_id:
+        # FIXME: double check that get-dest is correct
+        exr = verify_rcz((b'git', b'--git-dir', b'get-dest', b'ls-tree', tree_id))
+        if exr.rc: return False
+        cat = verify_rcz((b'git', b'--git-dir', b'get-dest',
+                          b'cat-file', b'commit', commit_id))
+        if cat.rc: return False
+        wvpasseq(b'tree ' + tree_id, cat.out.splitlines()[0])
+
+# FIXME: re-merge save and new_save?
+        
+def validate_save(dest_name, restore_subpath, commit_id, tree_id, orig_value,
+                  get_out):
+    out = get_out.splitlines()
+    print('blarg: out', repr(out), file=sys.stderr)
+    wvpasseq(2, len(out))
+    get_tree_id = out[0]
+    get_commit_id = out[1]
+    wvpasseq(tree_id, get_tree_id)
+    wvpasseq(commit_id, get_commit_id)
+    _validate_save(orig_value, dest_name + restore_subpath, commit_id, tree_id)
+
+def validate_new_save(dest_name, restore_subpath, commit_id, tree_id, orig_value,
+                      get_out):
+    out = get_out.splitlines()
+    wvpasseq(2, len(out))
+    get_tree_id = out[0]
+    get_commit_id = out[1]
+    wvpasseq(tree_id, get_tree_id)
+    wvpassne(commit_id, get_commit_id)
+    _validate_save(orig_value, dest_name + restore_subpath, get_commit_id, tree_id)
+        
+def validate_tagged_save(tag_name, restore_subpath,
+                         commit_id, tree_id, orig_value, get_out):
+    out = get_out.splitlines()
+    wvpasseq(1, len(out))
+    get_tag_id = out[0]
+    wvpasseq(commit_id, get_tag_id)
+    # Make sure tmp doesn't already exist.
+    exr = exo((b'git', b'--git-dir', b'get-dest', b'show-ref', b'tmp-branch-for-tag'),
+              check=False)
+    wvpasseq(1, exr.rc)
+
+    ex((b'git', b'--git-dir', b'get-dest', b'branch', b'tmp-branch-for-tag',
+        b'refs/tags/' + tag_name))
+    _validate_save(orig_value, b'tmp-branch-for-tag/latest' + restore_subpath,
+                   commit_id, tree_id)
+    ex((b'git', b'--git-dir', b'get-dest', b'branch', b'-D', b'tmp-branch-for-tag'))
+
+def validate_new_tagged_commit(tag_name, commit_id, tree_id, get_out):
+    out = get_out.splitlines()
+    wvpasseq(1, len(out))
+    get_tag_id = out[0]
+    wvpassne(commit_id, get_tag_id)
+    validate_tree(tree_id, tag_name + b':')
+
+
+def _run_get(disposition, method, what):
+    print('run_get:', repr((disposition, method, what)), file=sys.stderr)
+    global bup_cmd
+
+    if disposition == 'get':
+        get_cmd = (bup_cmd, b'-d', b'get-dest',
+                   b'get', b'-vvct', b'--print-tags', b'-s', b'get-src')
+    elif disposition == 'get-on':
+        get_cmd = (bup_cmd, b'-d', b'get-dest',
+                   b'on', b'-', b'get', b'-vvct', b'--print-tags', b'-s', b'get-src')
+    elif disposition == 'get-to':
+        get_cmd = (bup_cmd, b'-d', b'get-dest',
+                   b'get', b'-vvct', b'--print-tags', b'-s', b'get-src',
+                   b'-r', b'-:' + getcwd() + b'/get-dest')
+    else:
+        raise Exception('error: unexpected get disposition ' + repr(disposition))
+    
+    if isinstance(what, bytes):
+        cmd = get_cmd + (method, what)
+    else:
+        assert not isinstance(what, str)  # python 3 sanity check
+        if method in (b'--ff', b'--append', b'--pick', b'--force-pick', b'--new-tag',
+                      b'--replace'):
+            method += b':'
+        src, dest = what
+        cmd = get_cmd + (method, src, dest)
+    result = exo(cmd, check=False, stderr=PIPE)
+    fsck = ex((bup_cmd, b'-d', b'get-dest', b'fsck'), check=False)
+    wvpasseq(0, fsck.rc)
+    return result
+
+def run_get(disposition, method, what=None, given=None):
+    global bup_cmd
+    rmrf(b'get-dest')
+    ex((bup_cmd, b'-d', b'get-dest', b'init'))
+
+    if given:
+        # FIXME: replace bup-get with independent commands as is feasible
+        exr = _run_get(disposition, b'--replace', given)
+        assert not exr.rc
+    return _run_get(disposition, method, what)
+
+def _test_universal(get_disposition, src_info):
+    methods = (b'--ff', b'--append', b'--pick', b'--force-pick', b'--new-tag',
+               b'--replace', b'--unnamed')
+    for method in methods:
+        mmsg = method.decode('ascii')
+        wvstart(get_disposition + ' ' + mmsg + ', missing source, fails')
+        exr = run_get(get_disposition, method, b'not-there')
+        wvpassne(0, exr.rc)
+        verify_rx(br'cannot find source', exr.err)
+    for method in methods:
+        mmsg = method.decode('ascii')
+        wvstart(get_disposition + ' ' + mmsg + ' / fails')
+        exr = run_get(get_disposition, method, b'/')
+        wvpassne(0, exr.rc)
+        verify_rx(b'cannot fetch entire repository', exr.err)
+
+def verify_only_refs(**kwargs):
+    for kind, refs in items(kwargs):
+        if kind == 'heads':
+            abs_refs = [b'refs/heads/' + ref for ref in refs]
+            karg = b'--heads'
+        elif kind == 'tags':
+            abs_refs = [b'refs/tags/' + ref for ref in refs]
+            karg = b'--tags'
+        else:
+            raise TypeError('unexpected keyword argument %r' % kind)
+        if abs_refs:
+            verify_rcz([b'git', b'--git-dir', b'get-dest',
+                        b'show-ref', b'--verify', karg] + abs_refs)
+            exr = exo((b'git', b'--git-dir', b'get-dest', b'show-ref', karg),
+                      check=False)
+            wvpasseq(0, exr.rc)
+            expected_refs = sorted(abs_refs)
+            repo_refs = sorted([x.split()[1] for x in exr.out.splitlines()])
+            wvpasseq(expected_refs, repo_refs)
+        else:
+            # FIXME: can we just check "git show-ref --heads == ''"?
+            exr = exo((b'git', b'--git-dir', b'get-dest', b'show-ref', karg),
+                      check=False)
+            wvpasseq(1, exr.rc)
+            wvpasseq(b'', exr.out.strip())
+
+def _test_replace(get_disposition, src_info):
+    print('blarg:', repr(src_info), file=sys.stderr)
+
+    wvstart(get_disposition + ' --replace to root fails')
+    for item in (b'.tag/tinyfile',
+                 b'src/latest' + src_info['tinyfile-path'],
+                 b'.tag/subtree',
+                 b'src/latest' + src_info['subtree-vfs-path'],
+                 b'.tag/commit-1',
+                 b'src/latest',
+                 b'src'):
+        exr = run_get(get_disposition, b'--replace', (item, b'/'))
+        wvpassne(0, exr.rc)
+        verify_rx(br'impossible; can only overwrite branch or tag', exr.err)
+
+    tinyfile_id = src_info['tinyfile-id']
+    tinyfile_path = src_info['tinyfile-path']
+    subtree_vfs_path = src_info['subtree-vfs-path']
+    subtree_id = src_info['subtree-id']
+    commit_2_id = src_info['commit-2-id']
+    tree_2_id = src_info['tree-2-id']
+
+    # Anything to tag
+    existing_items = {'nothing' : None,
+                      'blob' : (b'.tag/tinyfile', b'.tag/obj'),
+                      'tree' : (b'.tag/tree-1', b'.tag/obj'),
+                      'commit': (b'.tag/commit-1', b'.tag/obj')}
+    for ex_type, ex_ref in items(existing_items):
+        wvstart(get_disposition + ' --replace ' + ex_type + ' with blob tag')
+        for item in (b'.tag/tinyfile', b'src/latest' + tinyfile_path):
+            exr = run_get(get_disposition, b'--replace', (item ,b'.tag/obj'),
+                          given=ex_ref)
+            wvpasseq(0, exr.rc)        
+            validate_blob(tinyfile_id, tinyfile_id)
+            verify_only_refs(heads=[], tags=(b'obj',))
+        wvstart(get_disposition + ' --replace ' + ex_type + ' with tree tag')
+        for item in (b'.tag/subtree',  b'src/latest' + subtree_vfs_path):
+            exr = run_get(get_disposition, b'--replace', (item, b'.tag/obj'),
+                          given=ex_ref)
+            validate_tree(subtree_id, subtree_id)
+            verify_only_refs(heads=[], tags=(b'obj',))
+        wvstart(get_disposition + ' --replace ' + ex_type + ' with commitish tag')
+        for item in (b'.tag/commit-2', b'src/latest', b'src'):
+            exr = run_get(get_disposition, b'--replace', (item, b'.tag/obj'),
+                          given=ex_ref)
+            validate_tagged_save(b'obj', getcwd() + b'/src',
+                                 commit_2_id, tree_2_id, b'src-2', exr.out)
+            verify_only_refs(heads=[], tags=(b'obj',))
+
+        # Committish to branch.
+        existing_items = (('nothing', None),
+                          ('branch', (b'.tag/commit-1', b'obj')))
+        for ex_type, ex_ref in existing_items:
+            for item_type, item in (('commit', b'.tag/commit-2'),
+                                    ('save', b'src/latest'),
+                                    ('branch', b'src')):
+                wvstart(get_disposition + ' --replace '
+                        + ex_type + ' with ' + item_type)
+                exr = run_get(get_disposition, b'--replace', (item, b'obj'),
+                              given=ex_ref)
+                validate_save(b'obj/latest', getcwd() + b'/src',
+                              commit_2_id, tree_2_id, b'src-2', exr.out)
+                verify_only_refs(heads=(b'obj',), tags=[])
+
+        # Not committish to branch
+        existing_items = (('nothing', None),
+                          ('branch', (b'.tag/commit-1', b'obj')))
+        for ex_type, ex_ref in existing_items:
+            for item_type, item in (('blob', b'.tag/tinyfile'),
+                                    ('blob', b'src/latest' + tinyfile_path),
+                                    ('tree', b'.tag/subtree'),
+                                    ('tree', b'src/latest' + subtree_vfs_path)):
+                wvstart(get_disposition + ' --replace branch with '
+                        + item_type + ' given ' + ex_type + ' fails')
+
+                exr = run_get(get_disposition, b'--replace', (item, b'obj'),
+                              given=ex_ref)
+                wvpassne(0, exr.rc)
+                verify_rx(br'cannot overwrite branch with .+ for', exr.err)
+
+        wvstart(get_disposition + ' --replace, implicit destinations')
+
+        exr = run_get(get_disposition, b'--replace', b'src')
+        validate_save(b'src/latest', getcwd() + b'/src',
+                      commit_2_id, tree_2_id, b'src-2', exr.out)
+        verify_only_refs(heads=(b'src',), tags=[])
+
+        exr = run_get(get_disposition, b'--replace', b'.tag/commit-2')
+        validate_tagged_save(b'commit-2', getcwd() + b'/src',
+                             commit_2_id, tree_2_id, b'src-2', exr.out)
+        verify_only_refs(heads=[], tags=(b'commit-2',))
+
+def _test_ff(get_disposition, src_info):
+
+    wvstart(get_disposition + ' --ff to root fails')
+    tinyfile_path = src_info['tinyfile-path']
+    for item in (b'.tag/tinyfile', b'src/latest' + tinyfile_path):
+        exr = run_get(get_disposition, b'--ff', (item, b'/'))
+        wvpassne(0, exr.rc)
+        verify_rx(br'source for .+ must be a branch, save, or commit', exr.err)
+    subtree_vfs_path = src_info['subtree-vfs-path']
+    for item in (b'.tag/subtree', b'src/latest' + subtree_vfs_path):
+        exr = run_get(get_disposition, b'--ff', (item, b'/'))
+        wvpassne(0, exr.rc)
+        verify_rx(br'is impossible; can only --append a tree to a branch',
+                  exr.err)    
+    for item in (b'.tag/commit-1', b'src/latest', b'src'):
+        exr = run_get(get_disposition, b'--ff', (item, b'/'))
+        wvpassne(0, exr.rc)
+        verify_rx(br'destination for .+ is a root, not a branch', exr.err)
+
+    wvstart(get_disposition + ' --ff of not-committish fails')
+    for src in (b'.tag/tinyfile', b'src/latest' + tinyfile_path):
+        # FIXME: use get_item elsewhere?
+        for given, get_item in ((None, (src, b'obj')),
+                                (None, (src, b'.tag/obj')),
+                                ((b'.tag/tinyfile', b'.tag/obj'), (src, b'.tag/obj')),
+                                ((b'.tag/tree-1', b'.tag/obj'), (src, b'.tag/obj')),
+                                ((b'.tag/commit-1', b'.tag/obj'), (src, b'.tag/obj')),
+                                ((b'.tag/commit-1', b'obj'), (src, b'obj'))):
+            exr = run_get(get_disposition, b'--ff', get_item, given=given)
+            wvpassne(0, exr.rc)
+            verify_rx(br'must be a branch, save, or commit', exr.err)
+    for src in (b'.tag/subtree', b'src/latest' + subtree_vfs_path):
+        for given, get_item in ((None, (src, b'obj')),
+                                (None, (src, b'.tag/obj')),
+                                ((b'.tag/tinyfile', b'.tag/obj'), (src, b'.tag/obj')),
+                                ((b'.tag/tree-1', b'.tag/obj'), (src, b'.tag/obj')),
+                                ((b'.tag/commit-1', b'.tag/obj'), (src, b'.tag/obj')),
+                                ((b'.tag/commit-1', b'obj'), (src, b'obj'))):
+            exr = run_get(get_disposition, b'--ff', get_item, given=given)
+            wvpassne(0, exr.rc)
+            verify_rx(br'can only --append a tree to a branch', exr.err)
+
+    wvstart(get_disposition + ' --ff committish, ff possible')
+    save_2 = src_info['save-2']
+    for src in (b'.tag/commit-2', b'src/' + save_2, b'src'):
+        for given, get_item, complaint in \
+            ((None, (src, b'.tag/obj'),
+              br'destination .+ must be a valid branch name'),
+             ((b'.tag/tinyfile', b'.tag/obj'), (src, b'.tag/obj'),
+              br'destination .+ is a blob, not a branch'),
+             ((b'.tag/tree-1', b'.tag/obj'), (src, b'.tag/obj'),
+              br'destination .+ is a tree, not a branch'),
+             ((b'.tag/commit-1', b'.tag/obj'), (src, b'.tag/obj'),
+              br'destination .+ is a tagged commit, not a branch'),
+             ((b'.tag/commit-2', b'.tag/obj'), (src, b'.tag/obj'),
+              br'destination .+ is a tagged commit, not a branch')):
+            exr = run_get(get_disposition, b'--ff', get_item, given=given)
+            wvpassne(0, exr.rc)
+            verify_rx(complaint, exr.err)
+    # FIXME: use src or item and given or existing consistently in loops...
+    commit_2_id = src_info['commit-2-id']
+    tree_2_id = src_info['tree-2-id']
+    for src in (b'.tag/commit-2', b'src/' + save_2, b'src'):
+        for given in (None, (b'.tag/commit-1', b'obj'), (b'.tag/commit-2', b'obj')):
+            exr = run_get(get_disposition, b'--ff', (src, b'obj'), given=given)
+            wvpasseq(0, exr.rc)
+            validate_save(b'obj/latest', getcwd() + b'/src',
+                          commit_2_id, tree_2_id, b'src-2', exr.out)
+            verify_only_refs(heads=(b'obj',), tags=[])
+            
+    wvstart(get_disposition + ' --ff, implicit destinations')
+    for item in (b'src', b'src/latest'):
+        exr = run_get(get_disposition, b'--ff', item)
+        wvpasseq(0, exr.rc)
+
+        ex((b'find', b'get-dest/refs'))
+        ex((bup_cmd, b'-d', b'get-dest', b'ls'))
+
+        validate_save(b'src/latest', getcwd() + b'/src',
+                     commit_2_id, tree_2_id, b'src-2', exr.out)
+        #verify_only_refs(heads=('src',), tags=[])
+
+    wvstart(get_disposition + ' --ff, ff impossible')
+    for given, get_item in (((b'unrelated-branch', b'src'), b'src'),
+                            ((b'.tag/commit-2', b'src'), (b'.tag/commit-1', b'src'))):
+        exr = run_get(get_disposition, b'--ff', get_item, given=given)
+        wvpassne(0, exr.rc)
+        verify_rx(br'destination is not an ancestor of source', exr.err)
+
+def _test_append(get_disposition, src_info):
+    tinyfile_path = src_info['tinyfile-path']
+    subtree_vfs_path = src_info['subtree-vfs-path']
+
+    wvstart(get_disposition + ' --append to root fails')
+    for item in (b'.tag/tinyfile', b'src/latest' + tinyfile_path):
+        exr = run_get(get_disposition, b'--append', (item, b'/'))
+        wvpassne(0, exr.rc)
+        verify_rx(br'source for .+ must be a branch, save, commit, or tree',
+                  exr.err)
+    for item in (b'.tag/subtree', b'src/latest' + subtree_vfs_path,
+                 b'.tag/commit-1', b'src/latest', b'src'):
+        exr = run_get(get_disposition, b'--append', (item, b'/'))
+        wvpassne(0, exr.rc)
+        verify_rx(br'destination for .+ is a root, not a branch', exr.err)
+
+    wvstart(get_disposition + ' --append of not-treeish fails')
+    for src in (b'.tag/tinyfile', b'src/latest' + tinyfile_path):
+        for given, item in ((None, (src, b'obj')),
+                            (None, (src, b'.tag/obj')),
+                            ((b'.tag/tinyfile', b'.tag/obj'), (src, b'.tag/obj')),
+                            ((b'.tag/tree-1', b'.tag/obj'), (src, b'.tag/obj')),
+                            ((b'.tag/commit-1', b'.tag/obj'), (src, b'.tag/obj')),
+                            ((b'.tag/commit-1', b'obj'), (src, b'obj'))):
+            exr = run_get(get_disposition, b'--append', item, given=given)
+            wvpassne(0, exr.rc)
+            verify_rx(br'must be a branch, save, commit, or tree', exr.err)
+
+    wvstart(get_disposition + ' --append committish failure cases')
+    save_2 = src_info['save-2']
+    for src in (b'.tag/subtree', b'src/latest' + subtree_vfs_path,
+                b'.tag/commit-2', b'src/' + save_2, b'src'):
+        for given, item, complaint in \
+            ((None, (src, b'.tag/obj'),
+              br'destination .+ must be a valid branch name'),
+             ((b'.tag/tinyfile', b'.tag/obj'), (src, b'.tag/obj'),
+              br'destination .+ is a blob, not a branch'),
+             ((b'.tag/tree-1', b'.tag/obj'), (src, b'.tag/obj'),
+              br'destination .+ is a tree, not a branch'),
+             ((b'.tag/commit-1', b'.tag/obj'), (src, b'.tag/obj'),
+              br'destination .+ is a tagged commit, not a branch'),
+             ((b'.tag/commit-2', b'.tag/obj'), (src, b'.tag/obj'),
+              br'destination .+ is a tagged commit, not a branch')):
+            exr = run_get(get_disposition, b'--append', item, given=given)
+            wvpassne(0, exr.rc)
+            verify_rx(complaint, exr.err)
+
+    wvstart(get_disposition + ' --append committish')
+    commit_2_id = src_info['commit-2-id']
+    tree_2_id = src_info['tree-2-id']
+    for item in (b'.tag/commit-2', b'src/' + save_2, b'src'):
+        for existing in (None, (b'.tag/commit-1', b'obj'),
+                         (b'.tag/commit-2', b'obj'),
+                         (b'unrelated-branch', b'obj')):
+            exr = run_get(get_disposition, b'--append', (item, b'obj'),
+                          given=existing)
+            wvpasseq(0, exr.rc)
+            validate_new_save(b'obj/latest', getcwd() + b'/src',
+                              commit_2_id, tree_2_id, b'src-2', exr.out)
+            verify_only_refs(heads=(b'obj',), tags=[])
+    # Append ancestor
+    save_1 = src_info['save-1']
+    commit_1_id = src_info['commit-1-id']
+    tree_1_id = src_info['tree-1-id']
+    for item in (b'.tag/commit-1',  b'src/' + save_1, b'src-1'):
+        exr = run_get(get_disposition, b'--append', (item, b'obj'),
+                      given=(b'.tag/commit-2', b'obj'))
+        wvpasseq(0, exr.rc)
+        validate_new_save(b'obj/latest', getcwd() + b'/src',
+                          commit_1_id, tree_1_id, b'src-1', exr.out)
+        verify_only_refs(heads=(b'obj',), tags=[])
+
+    wvstart(get_disposition + ' --append tree')
+    subtree_path = src_info['subtree-path']
+    subtree_id = src_info['subtree-id']
+    for item in (b'.tag/subtree', b'src/latest' + subtree_vfs_path):
+        for existing in (None,
+                         (b'.tag/commit-1', b'obj'),
+                         (b'.tag/commit-2', b'obj')):
+            exr = run_get(get_disposition, b'--append', (item, b'obj'),
+                          given=existing)
+            wvpasseq(0, exr.rc)
+            validate_new_save(b'obj/latest', b'/', None, subtree_id, subtree_path,
+                              exr.out)
+            verify_only_refs(heads=(b'obj',), tags=[])
+
+    wvstart(get_disposition + ' --append, implicit destinations')
+
+    for item in (b'src', b'src/latest'):
+        exr = run_get(get_disposition, b'--append', item)
+        wvpasseq(0, exr.rc)
+        validate_new_save(b'src/latest', getcwd() + b'/src', commit_2_id, tree_2_id,
+                          b'src-2', exr.out)
+        verify_only_refs(heads=(b'src',), tags=[])
+
+def _test_pick_common(get_disposition, src_info, force=False):
+    flavor = b'--force-pick' if force else b'--pick'
+    flavormsg = flavor.decode('ascii')
+    tinyfile_path = src_info['tinyfile-path']
+    subtree_vfs_path = src_info['subtree-vfs-path']
+    
+    wvstart(get_disposition + ' ' + flavormsg + ' to root fails')
+    for item in (b'.tag/tinyfile', b'src/latest' + tinyfile_path, b'src'):
+        exr = run_get(get_disposition, flavor, (item, b'/'))
+        wvpassne(0, exr.rc)
+        verify_rx(br'can only pick a commit or save', exr.err)
+    for item in (b'.tag/commit-1', b'src/latest'):
+        exr = run_get(get_disposition, flavor, (item, b'/'))
+        wvpassne(0, exr.rc)
+        verify_rx(br'destination is not a tag or branch', exr.err)
+    for item in (b'.tag/subtree', b'src/latest' + subtree_vfs_path):
+        exr = run_get(get_disposition, flavor, (item, b'/'))
+        wvpassne(0, exr.rc)
+        verify_rx(br'is impossible; can only --append a tree', exr.err)
+
+    wvstart(get_disposition + ' ' + flavormsg + ' of blob or branch fails')
+    for item in (b'.tag/tinyfile', b'src/latest' + tinyfile_path, b'src'):
+        for given, get_item in ((None, (item, b'obj')),
+                                (None, (item, b'.tag/obj')),
+                                ((b'.tag/tinyfile', b'.tag/obj'), (item, b'.tag/obj')),
+                                ((b'.tag/tree-1', b'.tag/obj'), (item, b'.tag/obj')),
+                                ((b'.tag/commit-1', b'.tag/obj'), (item, b'.tag/obj')),
+                                ((b'.tag/commit-1', b'obj'), (item, b'obj'))):
+            exr = run_get(get_disposition, flavor, get_item, given=given)
+            wvpassne(0, exr.rc)
+            verify_rx(br'impossible; can only pick a commit or save', exr.err)
+
+    wvstart(get_disposition + ' ' + flavormsg + ' of tree fails')
+    for item in (b'.tag/subtree', b'src/latest' + subtree_vfs_path):
+        for given, get_item in ((None, (item, b'obj')),
+                                (None, (item, b'.tag/obj')),
+                                ((b'.tag/tinyfile', b'.tag/obj'), (item, b'.tag/obj')),
+                                ((b'.tag/tree-1', b'.tag/obj'), (item, b'.tag/obj')),
+                                ((b'.tag/commit-1', b'.tag/obj'), (item, b'.tag/obj')),
+                                ((b'.tag/commit-1', b'obj'), (item, b'obj'))):
+            exr = run_get(get_disposition, flavor, get_item, given=given)
+            wvpassne(0, exr.rc)
+            verify_rx(br'impossible; can only --append a tree', exr.err)
+
+    save_2 = src_info['save-2']
+    commit_2_id = src_info['commit-2-id']
+    tree_2_id = src_info['tree-2-id']
+    # FIXME: these two wvstart texts?
+    if force:
+        wvstart(get_disposition + ' ' + flavormsg + ' commit/save to existing tag')
+        for item in (b'.tag/commit-2', b'src/' + save_2):
+            for given in ((b'.tag/tinyfile', b'.tag/obj'),
+                          (b'.tag/tree-1', b'.tag/obj'),
+                          (b'.tag/commit-1', b'.tag/obj')):
+                exr = run_get(get_disposition, flavor, (item, b'.tag/obj'),
+                              given=given)
+                wvpasseq(0, exr.rc)
+                validate_new_tagged_commit(b'obj', commit_2_id, tree_2_id,
+                                           exr.out)
+                verify_only_refs(heads=[], tags=(b'obj',))
+    else: # --pick
+        wvstart(get_disposition + ' ' + flavormsg
+                + ' commit/save to existing tag fails')
+        for item in (b'.tag/commit-2', b'src/' + save_2):
+            for given in ((b'.tag/tinyfile', b'.tag/obj'),
+                          (b'.tag/tree-1', b'.tag/obj'),
+                          (b'.tag/commit-1', b'.tag/obj')):
+                exr = run_get(get_disposition, flavor, (item, b'.tag/obj'), given=given)
+                wvpassne(0, exr.rc)
+                verify_rx(br'cannot overwrite existing tag', exr.err)
+            
+    wvstart(get_disposition + ' ' + flavormsg + ' commit/save to tag')
+    for item in (b'.tag/commit-2', b'src/' + save_2):
+        exr = run_get(get_disposition, flavor, (item, b'.tag/obj'))
+        wvpasseq(0, exr.rc)
+        validate_clean_repo()
+        validate_new_tagged_commit(b'obj', commit_2_id, tree_2_id, exr.out)
+        verify_only_refs(heads=[], tags=(b'obj',))
+         
+    wvstart(get_disposition + ' ' + flavormsg + ' commit/save to branch')
+    for item in (b'.tag/commit-2', b'src/' + save_2):
+        for given in (None, (b'.tag/commit-1', b'obj'), (b'.tag/commit-2', b'obj')):
+            exr = run_get(get_disposition, flavor, (item, b'obj'), given=given)
+            wvpasseq(0, exr.rc)
+            validate_clean_repo()
+            validate_new_save(b'obj/latest', getcwd() + b'/src',
+                              commit_2_id, tree_2_id, b'src-2', exr.out)
+            verify_only_refs(heads=(b'obj',), tags=[])
+
+    wvstart(get_disposition + ' ' + flavormsg
+            + ' commit/save unrelated commit to branch')
+    for item in(b'.tag/commit-2', b'src/' + save_2):
+        exr = run_get(get_disposition, flavor, (item, b'obj'),
+                      given=(b'unrelated-branch', b'obj'))
+        wvpasseq(0, exr.rc)
+        validate_clean_repo()
+        validate_new_save(b'obj/latest', getcwd() + b'/src',
+                          commit_2_id, tree_2_id, b'src-2', exr.out)
+        verify_only_refs(heads=(b'obj',), tags=[])
+
+    wvstart(get_disposition + ' ' + flavormsg + ' commit/save ancestor to branch')
+    save_1 = src_info['save-1']
+    commit_1_id = src_info['commit-1-id']
+    tree_1_id = src_info['tree-1-id']
+    for item in (b'.tag/commit-1', b'src/' + save_1):
+        exr = run_get(get_disposition, flavor, (item, b'obj'),
+                      given=(b'.tag/commit-2', b'obj'))
+        wvpasseq(0, exr.rc)
+        validate_clean_repo()
+        validate_new_save(b'obj/latest', getcwd() + b'/src',
+                          commit_1_id, tree_1_id, b'src-1', exr.out)
+        verify_only_refs(heads=(b'obj',), tags=[])
+
+
+    wvstart(get_disposition + ' ' + flavormsg + ', implicit destinations')
+    exr = run_get(get_disposition, flavor, b'.tag/commit-2')
+    wvpasseq(0, exr.rc)
+    validate_clean_repo()
+    validate_new_tagged_commit(b'commit-2', commit_2_id, tree_2_id, exr.out)
+    verify_only_refs(heads=[], tags=(b'commit-2',))
+
+    exr = run_get(get_disposition, flavor, b'src/latest')
+    wvpasseq(0, exr.rc)
+    validate_clean_repo()
+    validate_new_save(b'src/latest', getcwd() + b'/src',
+                      commit_2_id, tree_2_id, b'src-2', exr.out)
+    verify_only_refs(heads=(b'src',), tags=[])
+
+def _test_pick_force(get_disposition, src_info):
+    _test_pick_common(get_disposition, src_info, force=True)
+
+def _test_pick_noforce(get_disposition, src_info):
+    _test_pick_common(get_disposition, src_info, force=False)
+
+def _test_new_tag(get_disposition, src_info):
+    tinyfile_id = src_info['tinyfile-id']
+    tinyfile_path = src_info['tinyfile-path']
+    commit_2_id = src_info['commit-2-id']
+    tree_2_id = src_info['tree-2-id']
+    subtree_id = src_info['subtree-id']
+    subtree_vfs_path = src_info['subtree-vfs-path']
+
+    wvstart(get_disposition + ' --new-tag to root fails')
+    for item in (b'.tag/tinyfile',
+                 b'src/latest' + tinyfile_path,
+                 b'.tag/subtree',
+                 b'src/latest' + subtree_vfs_path,
+                 b'.tag/commit-1',
+                 b'src/latest',
+                 b'src'):
+        exr = run_get(get_disposition, b'--new-tag', (item, b'/'))
+        wvpassne(0, exr.rc)
+        verify_rx(br'destination for .+ must be a VFS tag', exr.err)
+
+    # Anything to new tag.
+    wvstart(get_disposition + ' --new-tag, blob tag')
+    for item in (b'.tag/tinyfile', b'src/latest' + tinyfile_path):
+        exr = run_get(get_disposition, b'--new-tag', (item, b'.tag/obj'))
+        wvpasseq(0, exr.rc)        
+        validate_blob(tinyfile_id, tinyfile_id)
+        verify_only_refs(heads=[], tags=(b'obj',))
+
+    wvstart(get_disposition + ' --new-tag, tree tag')
+    for item in (b'.tag/subtree', b'src/latest' + subtree_vfs_path):
+        exr = run_get(get_disposition, b'--new-tag', (item, b'.tag/obj'))
+        wvpasseq(0, exr.rc)        
+        validate_tree(subtree_id, subtree_id)
+        verify_only_refs(heads=[], tags=(b'obj',))
+        
+    wvstart(get_disposition + ' --new-tag, committish tag')
+    for item in (b'.tag/commit-2', b'src/latest', b'src'):
+        exr = run_get(get_disposition, b'--new-tag', (item, b'.tag/obj'))
+        wvpasseq(0, exr.rc)        
+        validate_tagged_save(b'obj', getcwd() + b'/src/', commit_2_id, tree_2_id,
+                             b'src-2', exr.out)
+        verify_only_refs(heads=[], tags=(b'obj',))
+
+    # Anything to existing tag (fails).
+    for ex_type, ex_tag in (('blob', (b'.tag/tinyfile', b'.tag/obj')),
+                            ('tree', (b'.tag/tree-1', b'.tag/obj')),
+                            ('commit', (b'.tag/commit-1', b'.tag/obj'))):
+        for item_type, item in (('blob tag', b'.tag/tinyfile'),
+                                ('blob path', b'src/latest' + tinyfile_path),
+                                ('tree tag', b'.tag/subtree'),
+                                ('tree path', b'src/latest' + subtree_vfs_path),
+                                ('commit tag', b'.tag/commit-2'),
+                                ('save', b'src/latest'),
+                                ('branch', b'src')):
+            wvstart(get_disposition + ' --new-tag of ' + item_type
+                    + ', given existing ' + ex_type + ' tag, fails')
+            exr = run_get(get_disposition, b'--new-tag', (item, b'.tag/obj'),
+                          given=ex_tag)
+            wvpassne(0, exr.rc)
+            verify_rx(br'cannot overwrite existing tag .* \(requires --replace\)',
+                      exr.err)
+
+    # Anything to branch (fails).
+    for ex_type, ex_tag in (('nothing', None),
+                            ('blob', (b'.tag/tinyfile', b'.tag/obj')),
+                            ('tree', (b'.tag/tree-1', b'.tag/obj')),
+                            ('commit', (b'.tag/commit-1', b'.tag/obj'))):
+        for item_type, item in (('blob tag', b'.tag/tinyfile'),
+                ('blob path', b'src/latest' + tinyfile_path),
+                ('tree tag', b'.tag/subtree'),
+                ('tree path', b'src/latest' + subtree_vfs_path),
+                ('commit tag', b'.tag/commit-2'),
+                ('save', b'src/latest'),
+                ('branch', b'src')):
+            wvstart(get_disposition + ' --new-tag to branch of ' + item_type
+                    + ', given existing ' + ex_type + ' tag, fails')
+            exr = run_get(get_disposition, b'--new-tag', (item, b'obj'),
+                          given=ex_tag)
+            wvpassne(0, exr.rc)
+            verify_rx(br'destination for .+ must be a VFS tag', exr.err)
+
+    wvstart(get_disposition + ' --new-tag, implicit destinations')
+    exr = run_get(get_disposition, b'--new-tag', b'.tag/commit-2')
+    wvpasseq(0, exr.rc)        
+    validate_tagged_save(b'commit-2', getcwd() + b'/src/', commit_2_id, tree_2_id,
+                         b'src-2', exr.out)
+    verify_only_refs(heads=[], tags=(b'commit-2',))
+
+def _test_unnamed(get_disposition, src_info):
+    tinyfile_id = src_info['tinyfile-id']
+    tinyfile_path = src_info['tinyfile-path']
+    subtree_vfs_path = src_info['subtree-vfs-path']
+    wvstart(get_disposition + ' --unnamed to root fails')
+    for item in (b'.tag/tinyfile',
+                 b'src/latest' + tinyfile_path,
+                 b'.tag/subtree',
+                 b'src/latest' + subtree_vfs_path,
+                 b'.tag/commit-1',
+                 b'src/latest',
+                 b'src'):
+        for ex_ref in (None, (item, b'.tag/obj')):
+            exr = run_get(get_disposition, b'--unnamed', (item, b'/'),
+                          given=ex_ref)
+            wvpassne(0, exr.rc)
+            verify_rx(br'usage: bup get ', exr.err)
+
+    wvstart(get_disposition + ' --unnamed file')
+    for item in (b'.tag/tinyfile', b'src/latest' + tinyfile_path):
+        exr = run_get(get_disposition, b'--unnamed', item)
+        wvpasseq(0, exr.rc)        
+        validate_blob(tinyfile_id, tinyfile_id)
+        verify_only_refs(heads=[], tags=[])
+
+        exr = run_get(get_disposition, b'--unnamed', item,
+                      given=(item, b'.tag/obj'))
+        wvpasseq(0, exr.rc)        
+        validate_blob(tinyfile_id, tinyfile_id)
+        verify_only_refs(heads=[], tags=(b'obj',))
+
+    wvstart(get_disposition + ' --unnamed tree')
+    subtree_id = src_info['subtree-id']
+    for item in (b'.tag/subtree', b'src/latest' + subtree_vfs_path):
+        exr = run_get(get_disposition, b'--unnamed', item)
+        wvpasseq(0, exr.rc)        
+        validate_tree(subtree_id, subtree_id)
+        verify_only_refs(heads=[], tags=[])
+        
+        exr = run_get(get_disposition, b'--unnamed', item,
+                      given=(item, b'.tag/obj'))
+        wvpasseq(0, exr.rc)        
+        validate_tree(subtree_id, subtree_id)
+        verify_only_refs(heads=[], tags=(b'obj',))
+        
+    wvstart(get_disposition + ' --unnamed committish')
+    save_2 = src_info['save-2']
+    commit_2_id = src_info['commit-2-id']
+    for item in (b'.tag/commit-2', b'src/' + save_2, b'src'):
+        exr = run_get(get_disposition, b'--unnamed', item)
+        wvpasseq(0, exr.rc)        
+        validate_commit(commit_2_id, commit_2_id)
+        verify_only_refs(heads=[], tags=[])
+
+        exr = run_get(get_disposition, b'--unnamed', item,
+                      given=(item, b'.tag/obj'))
+        wvpasseq(0, exr.rc)        
+        validate_commit(commit_2_id, commit_2_id)
+        verify_only_refs(heads=[], tags=(b'obj',))
+
+def create_get_src():
+    global bup_cmd, src_info
+    wvstart('preparing')
+    ex((bup_cmd, b'-d', b'get-src', b'init'))
+
+    mkdir(b'src')
+    open(b'src/unrelated', 'a').close()
+    ex((bup_cmd, b'-d', b'get-src', b'index', b'src'))
+    ex((bup_cmd, b'-d', b'get-src', b'save', b'-tcn', b'unrelated-branch', b'src'))
+
+    ex((bup_cmd, b'-d', b'get-src', b'index', b'--clear'))
+    rmrf(b'src')
+    mkdir(b'src')
+    open(b'src/zero', 'a').close()
+    ex((bup_cmd, b'-d', b'get-src', b'index', b'src'))
+    exr = exo((bup_cmd, b'-d', b'get-src', b'save', b'-tcn', b'src', b'src'))
+    out = exr.out.splitlines()
+    tree_0_id = out[0]
+    commit_0_id = out[-1]
+    exr = exo((bup_cmd, b'-d', b'get-src', b'ls', b'src'))
+    save_0 = exr.out.splitlines()[0]
+    ex((b'git', b'--git-dir', b'get-src', b'branch', b'src-0', b'src'))
+    ex((b'cp', b'-RPp', b'src', b'src-0'))
+    
+    rmrf(b'src')
+    mkdir(b'src')
+    mkdir(b'src/x')
+    mkdir(b'src/x/y')
+    ex((bup_cmd + b' -d get-src random 1k > src/1'), shell=True)
+    ex((bup_cmd + b' -d get-src random 1k > src/x/2'), shell=True)
+    ex((bup_cmd, b'-d', b'get-src', b'index', b'src'))
+    exr = exo((bup_cmd, b'-d', b'get-src', b'save', b'-tcn', b'src', b'src'))
+    out = exr.out.splitlines()
+    tree_1_id = out[0]
+    commit_1_id = out[-1]
+    exr = exo((bup_cmd, b'-d', b'get-src', b'ls', b'src'))
+    save_1 = exr.out.splitlines()[1]
+    ex((b'git', b'--git-dir', b'get-src', b'branch', b'src-1', b'src'))
+    ex((b'cp', b'-RPp', b'src', b'src-1'))
+    
+    # Make a copy the current state of src so we'll have an ancestor.
+    ex((b'cp', b'-RPp',
+         b'get-src/refs/heads/src', b'get-src/refs/heads/src-ancestor'))
+
+    with open(b'src/tiny-file', 'ab') as f: f.write(b'xyzzy')
+    ex((bup_cmd, b'-d', b'get-src', b'index', b'src'))
+    ex((bup_cmd, b'-d', b'get-src', b'tick'))  # Ensure the save names differ
+    exr = exo((bup_cmd, b'-d', b'get-src', b'save', b'-tcn', b'src', b'src'))
+    out = exr.out.splitlines()
+    tree_2_id = out[0]
+    commit_2_id = out[-1]
+    exr = exo((bup_cmd, b'-d', b'get-src', b'ls', b'src'))
+    save_2 = exr.out.splitlines()[2]
+    rename(b'src', b'src-2')
+
+    src_root = getcwd() + b'/src'
+
+    subtree_path = b'src-2/x'
+    subtree_vfs_path = src_root + b'/x'
+
+    # No support for "ls -d", so grep...
+    exr = exo((bup_cmd, b'-d', b'get-src', b'ls', b'-s', b'src/latest' + src_root))
+    out = exr.out.splitlines()
+    subtree_id = None
+    for line in out:
+        if b'x' in line:
+            subtree_id = line.split()[0]
+    assert(subtree_id)
+
+    # With a tiny file, we'll get a single blob, not a chunked tree
+    tinyfile_path = src_root + b'/tiny-file'
+    exr = exo((bup_cmd, b'-d', b'get-src', b'ls', b'-s', b'src/latest' + tinyfile_path))
+    tinyfile_id = exr.out.splitlines()[0].split()[0]
+
+    ex((bup_cmd, b'-d', b'get-src', b'tag', b'tinyfile', tinyfile_id))
+    ex((bup_cmd, b'-d', b'get-src', b'tag', b'subtree', subtree_id))
+    ex((bup_cmd, b'-d', b'get-src', b'tag', b'tree-0', tree_0_id))
+    ex((bup_cmd, b'-d', b'get-src', b'tag', b'tree-1', tree_1_id))
+    ex((bup_cmd, b'-d', b'get-src', b'tag', b'tree-2', tree_2_id))
+    ex((bup_cmd, b'-d', b'get-src', b'tag', b'commit-0', commit_0_id))
+    ex((bup_cmd, b'-d', b'get-src', b'tag', b'commit-1', commit_1_id))
+    ex((bup_cmd, b'-d', b'get-src', b'tag', b'commit-2', commit_2_id))
+    ex((b'git', b'--git-dir', b'get-src', b'branch', b'commit-1', commit_1_id))
+    ex((b'git', b'--git-dir', b'get-src', b'branch', b'commit-2', commit_2_id))
+
+    return {'tinyfile-path' : tinyfile_path,
+            'tinyfile-id' : tinyfile_id,
+            'subtree-id' : subtree_id,
+            'tree-0-id' : tree_0_id,
+            'tree-1-id' : tree_1_id,
+            'tree-2-id' : tree_2_id,
+            'commit-0-id' : commit_0_id,
+            'commit-1-id' : commit_1_id,
+            'commit-2-id' : commit_2_id,
+            'save-1' : save_1,
+            'save-2' : save_2,
+            'subtree-path' : subtree_path,
+            'subtree-vfs-path' : subtree_vfs_path}
+    
+# FIXME: this fails in a strange way:
+#   WVPASS given nothing get --ff not-there
+
+dispositions_to_test = ('get',)
+
+if int(environ.get(b'BUP_TEST_LEVEL', b'0')) >= 11:
+    dispositions_to_test += ('get-on', 'get-to')
+
+categories = ('replace', 'universal', 'ff', 'append', 'pick_force', 'pick_noforce', 'new_tag', 'unnamed')
+
+@pytest.mark.parametrize("disposition,category", product(dispositions_to_test, categories))
+def test_get(tmpdir, disposition, category):
+    chdir(tmpdir)
+    try:
+        src_info = create_get_src()
+        globals().get('_test_' + category)(disposition, src_info)
+    finally:
+        chdir(top)
diff --git a/test/ext/test_prune_older.py b/test/ext/test_prune_older.py
new file mode 100644 (file)
index 0000000..fd44fdf
--- /dev/null
@@ -0,0 +1,217 @@
+
+from __future__ import absolute_import, print_function
+from collections import defaultdict
+from itertools import chain, dropwhile, groupby, takewhile
+from os import chdir
+from random import choice, randint
+from shutil import copytree, rmtree
+from subprocess import PIPE
+from sys import stderr
+from time import localtime, strftime, time, tzset
+import random, sys
+
+if sys.version_info[:2] >= (3, 5):
+    from difflib import diff_bytes, unified_diff
+else:
+    from difflib import unified_diff
+
+from bup import compat
+from bup.compat import environ
+from bup.helpers import partition, period_as_secs, readpipe
+from bup.io import byte_stream
+from buptest import ex, exo
+from wvpytest import wvfail, wvpass, wvpasseq, wvpassne, wvstart
+import bup.path
+
+if sys.version_info[:2] < (3, 5):
+    def diff_bytes(_, *args):
+        return unified_diff(*args)
+
+def create_older_random_saves(n, start_utc, end_utc):
+    with open(b'foo', 'wb') as f:
+        pass
+    ex([b'git', b'add', b'foo'])
+    utcs = set()
+    while len(utcs) != n:
+        utcs.add(randint(start_utc, end_utc))
+    utcs = sorted(utcs)
+    for utc in utcs:
+        with open(b'foo', 'wb') as f:
+            f.write(b'%d\n' % utc)
+        ex([b'git', b'commit', b'--date', b'%d' % utc, b'-qam', b'%d' % utc])
+    ex([b'git', b'gc', b'--aggressive'])
+    return utcs
+
+# There is corresponding code in bup for some of this, but the
+# computation method is different here, in part so that the test can
+# provide a more effective cross-check.
+
+period_kinds = [b'all', b'dailies', b'monthlies', b'yearlies']
+period_scale = {b's': 1,
+                b'min': 60,
+                b'h': 60 * 60,
+                b'd': 60 * 60 * 24,
+                b'w': 60 * 60 * 24 * 7,
+                b'm': 60 * 60 * 24 * 31,
+                b'y': 60 * 60 * 24 * 366}
+period_scale_kinds = list(period_scale.keys())
+
+def expected_retentions(utcs, utc_start, spec):
+    if not spec:
+        return utcs
+    utcs = sorted(utcs, reverse=True)
+    period_start = dict(spec)
+    for kind, duration in compat.items(period_start):
+        period_start[kind] = utc_start - period_as_secs(duration)
+    period_start = defaultdict(lambda: float('inf'), period_start)
+
+    all = list(takewhile(lambda x: x >= period_start[b'all'], utcs))
+    utcs = list(dropwhile(lambda x: x >= period_start[b'all'], utcs))
+
+    matches = takewhile(lambda x: x >= period_start[b'dailies'], utcs)
+    dailies = [max(day_utcs) for yday, day_utcs
+               in groupby(matches, lambda x: localtime(x).tm_yday)]
+    utcs = list(dropwhile(lambda x: x >= period_start[b'dailies'], utcs))
+
+    matches = takewhile(lambda x: x >= period_start[b'monthlies'], utcs)
+    monthlies = [max(month_utcs) for month, month_utcs
+                 in groupby(matches, lambda x: localtime(x).tm_mon)]
+    utcs = dropwhile(lambda x: x >= period_start[b'monthlies'], utcs)
+
+    matches = takewhile(lambda x: x >= period_start[b'yearlies'], utcs)
+    yearlies = [max(year_utcs) for year, year_utcs
+                in groupby(matches, lambda x: localtime(x).tm_year)]
+
+    return chain(all, dailies, monthlies, yearlies)
+
+def period_spec(start_utc, end_utc):
+    global period_kinds, period_scale, period_scale_kinds
+    result = []
+    desired_specs = randint(1, 2 * len(period_kinds))
+    assert(desired_specs >= 1)  # At least one --keep argument is required
+    while len(result) < desired_specs:
+        period = None
+        if randint(1, 100) <= 5:
+            period = b'forever'
+        else:
+            assert(end_utc > start_utc)
+            period_secs = randint(1, end_utc - start_utc)
+            scale = choice(period_scale_kinds)
+            mag = int(float(period_secs) / period_scale[scale])
+            if mag != 0:
+                period = (b'%d' % mag) + scale
+        if period:
+            result += [(choice(period_kinds), period)]
+    return tuple(result)
+
+def unique_period_specs(n, start_utc, end_utc):
+    invocations = set()
+    while len(invocations) < n:
+        invocations.add(period_spec(start_utc, end_utc))
+    return tuple(invocations)
+
+def period_spec_to_period_args(spec):
+    return tuple(chain(*((b'--keep-' + kind + b'-for', period)
+                         for kind, period in spec)))
+
+def result_diffline(x):
+    return (b'%d %s\n'
+            % (x, strftime(' %Y-%m-%d-%H%M%S', localtime(x)).encode('ascii')))
+
+def check_prune_result(expected):
+    actual = sorted([int(x)
+                     for x in exo([b'git', b'log',
+                                   b'--pretty=format:%at']).out.splitlines()])
+
+    if expected != actual:
+        for x in expected:
+            print('ex:', x, strftime('%Y-%m-%d-%H%M%S', localtime(x)),
+                  file=stderr)
+        for line in diff_bytes(unified_diff,
+                               [result_diffline(x) for x in expected],
+                               [result_diffline(x) for x in actual],
+                               fromfile=b'expected', tofile=b'actual'):
+            sys.stderr.flush()
+            byte_stream(sys.stderr).write(line)
+    wvpass(expected == actual)
+
+
+def test_prune_older(tmpdir):
+    environ[b'GIT_AUTHOR_NAME'] = b'bup test'
+    environ[b'GIT_COMMITTER_NAME'] = b'bup test'
+    environ[b'GIT_AUTHOR_EMAIL'] = b'bup@a425bc70a02811e49bdf73ee56450e6f'
+    environ[b'GIT_COMMITTER_EMAIL'] = b'bup@a425bc70a02811e49bdf73ee56450e6f'
+
+    seed = int(environ.get(b'BUP_TEST_SEED', time()))
+    random.seed(seed)
+    print('random seed:', seed, file=stderr)
+
+    save_population = int(environ.get(b'BUP_TEST_PRUNE_OLDER_SAVES', 2000))
+    prune_cycles = int(environ.get(b'BUP_TEST_PRUNE_OLDER_CYCLES', 20))
+    prune_gc_cycles = int(environ.get(b'BUP_TEST_PRUNE_OLDER_GC_CYCLES', 10))
+
+    bup_cmd = bup.path.exe()
+
+    environ[b'BUP_DIR'] = tmpdir + b'/work/.git'
+    environ[b'GIT_DIR'] = tmpdir + b'/work/.git'
+    now = int(time())
+    three_years_ago = now - (60 * 60 * 24 * 366 * 3)
+    chdir(tmpdir)
+    ex([b'git', b'init', b'work'])
+    ex([b'git', b'config', b'gc.autoDetach', b'false'])
+
+    wvstart('generating ' + str(save_population) + ' random saves')
+    chdir(tmpdir + b'/work')
+    save_utcs = create_older_random_saves(save_population, three_years_ago, now)
+    chdir(tmpdir)
+    test_set_hash = exo([b'git', b'show-ref', b'-s', b'master']).out.rstrip()
+    ls_saves = exo((bup_cmd, b'ls', b'master')).out.splitlines()
+    wvpasseq(save_population + 1, len(ls_saves))
+
+    wvstart('ensure everything kept, if no keep arguments')
+    ex([b'git', b'reset', b'--hard', test_set_hash])
+    proc = ex((bup_cmd,
+               b'prune-older', b'-v', b'--unsafe', b'--no-gc',
+               b'--wrt', b'%d' % now) \
+              + (b'master',),
+              stdout=None, stderr=PIPE, check=False)
+    wvpassne(proc.rc, 0)
+    wvpass(b'at least one keep argument is required' in proc.err)
+    check_prune_result(save_utcs)
+
+
+    wvstart('running %d generative no-gc tests on %d saves' % (prune_cycles,
+                                                               save_population))
+    for spec in unique_period_specs(prune_cycles,
+                                    # Make it more likely we'll have
+                                    # some outside the save range.
+                                    three_years_ago - period_scale[b'm'],
+                                    now):
+        ex([b'git', b'reset', b'--hard', test_set_hash])
+        expected = sorted(expected_retentions(save_utcs, now, spec))
+        ex((bup_cmd,
+            b'prune-older', b'-v', b'--unsafe', b'--no-gc', b'--wrt',
+            b'%d' % now) \
+           + period_spec_to_period_args(spec) \
+           + (b'master',))
+        check_prune_result(expected)
+
+
+    # More expensive because we have to recreate the repo each time
+    wvstart('running %d generative gc tests on %d saves' % (prune_gc_cycles,
+                                                            save_population))
+    ex([b'git', b'reset', b'--hard', test_set_hash])
+    copytree(b'work/.git', b'clean-test-repo', symlinks=True)
+    for spec in unique_period_specs(prune_gc_cycles,
+                                    # Make it more likely we'll have
+                                    # some outside the save range.
+                                    three_years_ago - period_scale[b'm'],
+                                    now):
+        rmtree(b'work/.git')
+        copytree(b'clean-test-repo', b'work/.git')
+        expected = sorted(expected_retentions(save_utcs, now, spec))
+        ex((bup_cmd,
+            b'prune-older', b'-v', b'--unsafe', b'--wrt', b'%d' % now) \
+           + period_spec_to_period_args(spec) \
+           + (b'master',))
+        check_prune_result(expected)
index 039eb12da0f5b62f57218df4deb6cd734bd71e2c..df2a03f9a14bebc1be6df1e2c9afb838dc09877d 100644 (file)
@@ -1,11 +1,9 @@
 
 from __future__ import absolute_import, print_function
 
-from wvtest import *
-
 from bup.compat import pending_raise
+from wvpytest import wvpasseq
 
-@wvtest
 def test_pending_raise():
     outer = Exception('outer')
     inner = Exception('inner')
@@ -17,8 +15,8 @@ def test_pending_raise():
             with pending_raise(ex):
                 pass
     except Exception as ex:
-        WVPASSEQ(outer, ex)
-        WVPASSEQ(None, getattr(outer, '__context__', None))
+        wvpasseq(outer, ex)
+        wvpasseq(None, getattr(outer, '__context__', None))
 
     try:
         try:
@@ -27,6 +25,6 @@ def test_pending_raise():
             with pending_raise(ex):
                 raise inner
     except Exception as ex:
-        WVPASSEQ(inner, ex)
-        WVPASSEQ(None, getattr(outer, '__context__', None))
-        WVPASSEQ(outer, getattr(inner, '__context__', None))
+        wvpasseq(inner, ex)
+        wvpasseq(None, getattr(outer, '__context__', None))
+        wvpasseq(outer, getattr(inner, '__context__', None))
index a83181259d40fb86ea51876a07725c229fb0745f..d9aeaaed56b53c022092cefb89d2e080773030e5 100644 (file)
@@ -8,32 +8,11 @@ from subprocess import PIPE, Popen
 from traceback import extract_stack
 import errno, os, subprocess, sys, tempfile
 
-from wvtest import WVPASSEQ, wvfailure_count
-
 from bup import helpers
 from bup.compat import fsencode, str_type
 from bup.io import byte_stream
 
 
-# Assumes (of course) this file is at the top-level of the source tree
-_bup_tmp = realpath(dirname(fsencode(__file__))) + b'/test/tmp'
-try:
-    os.makedirs(_bup_tmp)
-except OSError as e:
-    if e.errno != errno.EEXIST:
-        raise
-
-
-@contextmanager
-def test_tempdir(prefix):
-    initial_failures = wvfailure_count()
-    tmpdir = tempfile.mkdtemp(dir=_bup_tmp, prefix=prefix)
-    yield tmpdir
-    if wvfailure_count() == initial_failures:
-        subprocess.call(['chmod', '-R', 'u+rwX', tmpdir])
-        subprocess.call(['rm', '-rf', tmpdir])
-
-
 ex_res = namedtuple('SubprocResult', ['out', 'err', 'proc', 'rc'])
 
 def run(cmd, check=True, input=None, **kwargs):
diff --git a/wvtest.py b/wvtest.py
deleted file mode 100755 (executable)
index 0f284bf..0000000
--- a/wvtest.py
+++ /dev/null
@@ -1,255 +0,0 @@
-#!/bin/sh
-"""": # -*-python-*-
-bup_python="$(dirname "$0")/dev/bup-python"
-exec "$bup_python" "$0" ${1+"$@"}
-"""
-# end of bup preamble
-
-#
-# WvTest:
-#   Copyright (C)2007-2012 Versabanq Innovations Inc. and contributors.
-#       Licensed under the GNU Library General Public License, version 2.
-#       See the included file named LICENSE for license information.
-#       You can get wvtest from: http://github.com/apenwarr/wvtest
-#
-
-from __future__ import absolute_import, print_function
-
-import os
-import random
-
-_wvtest_random_seed = os.environ.get('BUP_TEST_SEED')
-if _wvtest_random_seed:
-    random.seed(int(_wvtest_random_seed))
-
-from os.path import relpath
-import atexit
-import inspect
-import re
-import sys
-import traceback
-
-sys.path[:0] = [os.path.realpath('test/lib')]
-
-_start_dir = os.getcwd()
-
-# NOTE
-# Why do we do we need the "!= main" check?  Because if you run
-# wvtest.py as a main program and it imports your test files, then
-# those test files will try to import the wvtest module recursively.
-# That actually *works* fine, because we don't run this main program
-# when we're imported as a module.  But you end up with two separate
-# wvtest modules, the one that gets imported, and the one that's the
-# main program.  Each of them would have duplicated global variables
-# (most importantly, wvtest._registered), and so screwy things could
-# happen.  Thus, we make the main program module *totally* different
-# from the imported module.  Then we import wvtest (the module) into
-# wvtest (the main program) here and make sure to refer to the right
-# versions of global variables.
-#
-# All this is done just so that wvtest.py can be a single file that's
-# easy to import into your own applications.
-if __name__ != '__main__':   # we're imported as a module
-    _registered = []
-    _tests = 0
-    _fails = 0
-
-    def wvtest(func):
-        """ Use this decorator (@wvtest) in front of any function you want to
-            run as part of the unit test suite.  Then run:
-                python wvtest.py path/to/yourtest.py [other test.py files...]
-            to run all the @wvtest functions in the given file(s).
-        """
-        _registered.append(func)
-        return func
-
-
-    def _result(msg, tb, code):
-        global _tests, _fails
-        _tests += 1
-        if code != 'ok':
-            _fails += 1
-        (filename, line, func, text) = tb
-        filename = os.path.basename(filename)
-        msg = re.sub(r'\s+', ' ', str(msg))
-        sys.stderr.flush()
-        print('! %-70s %s' % ('%s:%-4d %s' % (filename, line, msg),
-                              code))
-        sys.stdout.flush()
-
-
-    def _caller_stack(wv_call_depth):
-        # Without the chdir, the source text lookup may fail
-        orig = os.getcwd()
-        os.chdir(_start_dir)
-        try:
-            return traceback.extract_stack()[-(wv_call_depth + 2)]
-        finally:
-            os.chdir(orig)
-
-
-    def _check(cond, msg = 'unknown', tb = None):
-        if tb == None: tb = _caller_stack(2)
-        if cond:
-            _result(msg, tb, 'ok')
-        else:
-            _result(msg, tb, 'FAILED')
-        return cond
-
-    def wvcheck(cond, msg, tb = None):
-        if tb == None: tb = _caller_stack(2)
-        if cond:
-            _result(msg, tb, 'ok')
-        else:
-            _result(msg, tb, 'FAILED')
-        return cond
-
-    _code_rx = re.compile(r'^\w+\((.*)\)(\s*#.*)?$')
-    def _code():
-        text = _caller_stack(2)[3]
-        return _code_rx.sub(r'\1', text)
-
-    def WVSTART(message):
-        filename = _caller_stack(1)[0]
-        sys.stderr.write('Testing \"' + message + '\" in ' + filename + ':\n')
-
-    def WVMSG(message):
-        ''' Issues a notification. '''
-        return _result(message, _caller_stack(1), 'ok')
-
-    def WVPASS(cond = True):
-        ''' Counts a test failure unless cond is true. '''
-        return _check(cond, _code())
-
-    def WVFAIL(cond = True):
-        ''' Counts a test failure  unless cond is false. '''
-        return _check(not cond, 'NOT(%s)' % _code())
-
-    def WVPASSEQ(a, b):
-        ''' Counts a test failure unless a == b. '''
-        return _check(a == b, '%s == %s' % (repr(a), repr(b)))
-
-    def WVPASSNE(a, b):
-        ''' Counts a test failure unless a != b. '''
-        return _check(a != b, '%s != %s' % (repr(a), repr(b)))
-
-    def WVPASSLT(a, b):
-        ''' Counts a test failure unless a < b. '''
-        return _check(a < b, '%s < %s' % (repr(a), repr(b)))
-
-    def WVPASSLE(a, b):
-        ''' Counts a test failure unless a <= b. '''
-        return _check(a <= b, '%s <= %s' % (repr(a), repr(b)))
-
-    def WVPASSGT(a, b):
-        ''' Counts a test failure unless a > b. '''
-        return _check(a > b, '%s > %s' % (repr(a), repr(b)))
-
-    def WVPASSGE(a, b):
-        ''' Counts a test failure unless a >= b. '''
-        return _check(a >= b, '%s >= %s' % (repr(a), repr(b)))
-
-    def WVEXCEPT(etype, func, *args, **kwargs):
-        ''' Counts a test failure unless func throws an 'etype' exception.
-            You have to spell out the function name and arguments, rather than
-            calling the function yourself, so that WVEXCEPT can run before
-            your test code throws an exception.
-        '''
-        try:
-            func(*args, **kwargs)
-        except etype as e:
-            return _check(True, 'EXCEPT(%s)' % _code())
-        except:
-            _check(False, 'EXCEPT(%s)' % _code())
-            raise
-        else:
-            return _check(False, 'EXCEPT(%s)' % _code())
-
-    wvstart = WVSTART
-    wvmsg = WVMSG
-    wvpass = WVPASS
-    wvfail = WVFAIL
-    wvpasseq = WVPASSEQ
-    wvpassne = WVPASSNE
-    wvpaslt = WVPASSLT
-    wvpassle = WVPASSLE
-    wvpassgt = WVPASSGT
-    wvpassge = WVPASSGE
-    wvexcept = WVEXCEPT
-
-    def wvfailure_count():
-        return _fails
-
-    def _check_unfinished():
-        if _registered:
-            for func in _registered:
-                print('WARNING: not run: %r' % (func,))
-            WVFAIL('wvtest_main() not called')
-        if _fails:
-            sys.exit(1)
-
-    atexit.register(_check_unfinished)
-
-
-def _run_in_chdir(path, func, *args, **kwargs):
-    oldwd = os.getcwd()
-    oldpath = sys.path
-    try:
-        os.chdir(path)
-        sys.path += [path, os.path.split(path)[0]]
-        return func(*args, **kwargs)
-    finally:
-        os.chdir(oldwd)
-        sys.path = oldpath
-
-
-def _runtest(fname, f):
-    mod = inspect.getmodule(f)
-    rpath = relpath(mod.__file__, os.getcwd()).replace('.pyc', '.py')
-    print()
-    print('Testing "%s" in %s:' % (fname, rpath))
-    sys.stdout.flush()
-    try:
-        _run_in_chdir(os.path.split(mod.__file__)[0], f)
-    except Exception as e:
-        print()
-        print(traceback.format_exc())
-        tb = sys.exc_info()[2]
-        wvtest._result(e, traceback.extract_tb(tb)[1], 'EXCEPTION')
-
-
-def _run_registered_tests():
-    import wvtest as _wvtestmod
-    while _wvtestmod._registered:
-        t = _wvtestmod._registered.pop(0)
-        _runtest(t.__name__, t)
-        print()
-
-
-def wvtest_main(extra_testfiles=tuple()):
-    import wvtest as _wvtestmod
-    _run_registered_tests()
-    for modname in extra_testfiles:
-        if not os.path.exists(modname):
-            print('Skipping: %s' % modname)
-            continue
-        if modname.endswith('.py'):
-            modname = modname[:-3]
-        print('Importing: %s' % modname)
-        path, mod = os.path.split(os.path.abspath(modname))
-        nicename = modname.replace(os.path.sep, '.')
-        while nicename.startswith('.'):
-            nicename = modname[1:]
-        _run_in_chdir(path, __import__, nicename, None, None, [])
-        _run_registered_tests()
-    print()
-    print('WvTest: %d tests, %d failures.' % (_wvtestmod._tests,
-                                              _wvtestmod._fails))
-
-
-if __name__ == '__main__':
-    import wvtest as _wvtestmod
-    sys.modules['wvtest'] = _wvtestmod
-    sys.modules['wvtest.wvtest'] = _wvtestmod
-    wvtest = _wvtestmod
-    wvtest_main(sys.argv[1:])