← Back to team overview

duplicity-team team mailing list archive

[Merge] lp:~mterry/duplicity/more-test-reorg into lp:duplicity

 

Michael Terry has proposed merging lp:~mterry/duplicity/more-test-reorg into lp:duplicity.

Requested reviews:
  duplicity-team (duplicity-team)

For more details, see:
https://code.launchpad.net/~mterry/duplicity/more-test-reorg/+merge/216545

Here's another test reorganization / modernization branch.  It does the following things:

- Drop duplicity/misc.py.  It is confusing to have both misc.py and util.py, and most of the code in misc.py was no longer used.  I moved the one function that was still used into util.py.

- Consolidated the various ways to run tests into just one.  I made tox runs go through ./setup.py test, rather than nosetests.  And I made the ./testing/run-tests scripts just call tox.  Now we no longer need nosetests as a test dependency (although you can still use it if you want).

- Added two more code quality automated tests: a pep8 one and a pylint one.  I disabled almost all checks in each program that gave a warning.  These tests just establish a baseline for future improvement.

- Moved the test helper code into TestCase subclasses that all tests can use.  And used more code sharing and setUp/tearDown cleverness to remove duplicated code.

- Reorganized the tests in ./testing/tests into ./testing/functional and ./testing/unit -- for whether they drive duplicity as a subprocess or whether they import and test code directly.  Each dir can have specialized TestCase subclasses now.

- Renamed the files in ./testing/unit to more clearly indicate which file in ./duplicity they are unit testing.

- Added some helper methods for tests to set environment and globals.* parameters more safely (i.e. without affecting other tests) by automatically cleaning up any such changes during test tearDown.

- Removed test_unicode.py, since it is kind of dumb.  It used to be more useful, but now with py2.6, we are just testing that one line of code in it is actually there.
-- 
https://code.launchpad.net/~mterry/duplicity/more-test-reorg/+merge/216545
Your team duplicity-team is requested to review the proposed merge of lp:~mterry/duplicity/more-test-reorg into lp:duplicity.
=== modified file 'duplicity/backend.py'
--- duplicity/backend.py	2014-04-17 20:50:57 +0000
+++ duplicity/backend.py	2014-04-20 14:58:57 +0000
@@ -355,7 +355,7 @@
 
     return _retry_fatal
 
-class Backend:
+class Backend(object):
     """
     Represents a generic duplicity backend, capable of storing and
     retrieving files.

=== modified file 'duplicity/gpg.py'
--- duplicity/gpg.py	2014-04-17 21:13:48 +0000
+++ duplicity/gpg.py	2014-04-20 14:58:57 +0000
@@ -27,10 +27,10 @@
 
 import os, types, tempfile, re, gzip, locale
 
-from duplicity import misc
 from duplicity import globals
 from duplicity import gpginterface
 from duplicity import tempdir
+from duplicity import util
 
 try:
     from hashlib import sha1
@@ -313,7 +313,7 @@
         >> largest block size).
         """
         incompressible_fp = open(filename, "rb")
-        assert misc.copyfileobj(incompressible_fp, file.gpg_input, bytes) == bytes
+        assert util.copyfileobj(incompressible_fp, file.gpg_input, bytes) == bytes
         incompressible_fp.close()
 
     def get_current_size():

=== removed file 'duplicity/misc.py'
--- duplicity/misc.py	2013-12-27 06:39:00 +0000
+++ duplicity/misc.py	1970-01-01 00:00:00 +0000
@@ -1,192 +0,0 @@
-# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
-#
-# Copyright 2002 Ben Escoto <ben@xxxxxxxxxxx>
-# Copyright 2007 Kenneth Loafman <kenneth@xxxxxxxxxxx>
-#
-# This file is part of duplicity.
-#
-# Duplicity is free software; you can redistribute it and/or modify it
-# under the terms of the GNU General Public License as published by the
-# Free Software Foundation; either version 2 of the License, or (at your
-# option) any later version.
-#
-# Duplicity is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with duplicity; if not, write to the Free Software Foundation,
-# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-
-"""Miscellaneous classes and methods"""
-
-import os
-
-from duplicity import log
-from duplicity import util
-
-
-class MiscError(Exception):
-    """Signifies a miscellaneous error..."""
-    pass
-
-
-class FileVolumeWriter:
-    """Split up an incoming fileobj into multiple volumes on disk
-
-    This class can also be used as an iterator.  It returns the
-    filenames of the files it writes.
-
-    """
-    volume_size = 50 * 1024 * 1024
-    blocksize = 64 * 1024
-    def __init__(self, infp, file_prefix):
-        """FileVolumeWriter initializer
-
-        infp is a file object opened for reading.  It will be closed
-        at end.  file_prefix is the full path of the volumes that will
-        be written.  If more than one is required, it will be appended
-        with .1, .2, etc.
-
-        """
-        self.infp = infp
-        self.prefix = file_prefix
-        self.current_index = 1
-        self.finished = None # set to true when completely done
-        self.buffer = "" # holds data that belongs in next volume
-
-    def get_initial_buf(self):
-        """Get first value of buffer, from self.buffer or infp"""
-        if self.buffer:
-            buf = self.buffer
-            self.buffer = ""
-            return buf
-        else:
-            return self.infp.read(self.blocksize)
-
-    def write_volume(self, outfp):
-        """Write self.volume_size bytes from self.infp to outfp
-
-        Return None if we have reached end of infp without reaching
-        volume size, and false otherwise.
-
-        """
-        bytes_written, buf = 0, self.get_initial_buf()
-        while len(buf) + bytes_written <= self.volume_size:
-            if not buf:
-                # reached end of input
-                outfp.close()
-                return None
-            if len(buf) + bytes_written > self.volume_size:
-                break
-            outfp.write(buf)
-            bytes_written += len(buf)
-            buf = self.infp.read(self.blocksize)
-
-        remainder = self.volume_size - bytes_written
-        assert remainder < len(buf)
-        outfp.write(buf[:remainder])
-        outfp.close()
-        self.buffer = buf[remainder:]
-        return 1
-
-    def next(self):
-        """Write next file, return filename"""
-        if self.finished:
-            raise StopIteration
-
-        filename = "%s.%d" % (self.prefix, self.current_index)
-        log.Info(_("Starting to write %s") % util.ufn(filename))
-        outfp = open(filename, "wb")
-
-        if not self.write_volume(outfp):
-            # end of input
-            self.finished = 1
-            if self.current_index == 1:
-                # special case first index
-                log.Notice(_("One only volume required.\n"
-                             "Renaming %s to %s") % (util.ufn(filename), util.ufn(self.prefix)))
-                os.rename(filename, self.prefix)
-                return self.prefix
-        else:
-            self.current_index += 1
-        return filename
-
-    def __iter__(self):
-        return self
-
-
-class BufferedFile:
-    """Buffer file open for reading, so reads will happen in fixed sizes
-
-    This is currently used to buffer a GzipFile, because that class
-    apparently doesn't respond well to arbitrary read sizes.
-
-    """
-    def __init__(self, fileobj, blocksize = 32 * 1024):
-        self.fileobj = fileobj
-        self.buffer = ""
-        self.blocksize = blocksize
-
-    def read(self, length = -1):
-        """Return length bytes, or all if length < 0"""
-        if length < 0:
-            while 1:
-                buf = self.fileobj.read(self.blocksize)
-                if not buf:
-                    break
-                self.buffer += buf
-            real_length = len(self.buffer)
-        else:
-            while len(self.buffer) < length:
-                buf = self.fileobj.read(self.blocksize)
-                if not buf:
-                    break
-                self.buffer += buf
-            real_length = min(length, len(self.buffer))
-        result = self.buffer[:real_length]
-        self.buffer = self.buffer[real_length:]
-        return result
-
-    def close(self):
-        self.fileobj.close()
-
-
-def copyfileobj(infp, outfp, byte_count = -1):
-    """Copy byte_count bytes from infp to outfp, or all if byte_count < 0
-
-    Returns the number of bytes actually written (may be less than
-    byte_count if find eof.  Does not close either fileobj.
-
-    """
-    blocksize = 64 * 1024
-    bytes_written = 0
-    if byte_count < 0:
-        while 1:
-            buf = infp.read(blocksize)
-            if not buf:
-                break
-            bytes_written += len(buf)
-            outfp.write(buf)
-    else:
-        while bytes_written + blocksize <= byte_count:
-            buf = infp.read(blocksize)
-            if not buf:
-                break
-            bytes_written += len(buf)
-            outfp.write(buf)
-        buf = infp.read(byte_count - bytes_written)
-        bytes_written += len(buf)
-        outfp.write(buf)
-    return bytes_written
-
-def copyfileobj_close(infp, outfp):
-    """Copy infp to outfp, closing afterwards"""
-    copyfileobj(infp, outfp)
-    if infp.close():
-        raise MiscError("Error closing input file")
-    if outfp.close():
-        raise MiscError("Error closing output file")
-
-

=== modified file 'duplicity/patchdir.py'
--- duplicity/patchdir.py	2014-04-17 20:50:57 +0000
+++ duplicity/patchdir.py	2014-04-20 14:58:57 +0000
@@ -28,7 +28,6 @@
 from duplicity import librsync #@UnusedImport
 from duplicity import log #@UnusedImport
 from duplicity import diffdir
-from duplicity import misc
 from duplicity import selection
 from duplicity import tempdir
 from duplicity import util #@UnusedImport
@@ -478,7 +477,7 @@
             by using the duplicity.tempdir to tell us where.
             """
             tempfp = tempfile.TemporaryFile( dir=tempdir.default().dir() )
-            misc.copyfileobj( current_file, tempfp )
+            util.copyfileobj( current_file, tempfp )
             assert not current_file.close()
             tempfp.seek( 0 )
             current_file = tempfp

=== modified file 'duplicity/progress.py'
--- duplicity/progress.py	2014-04-17 21:46:00 +0000
+++ duplicity/progress.py	2014-04-20 14:58:57 +0000
@@ -32,7 +32,9 @@
 This is a forecast based on gathered evidence.
 """
 
+from __future__ import absolute_import
 
+import collections as sys_collections
 import math
 import threading
 import time
@@ -42,30 +44,6 @@
 import pickle
 import os
 
-def import_non_local(name, custom_name=None):
-    """
-    This function is needed to play a trick... as there exists a local
-    "collections" module, that is named the same as a system module
-    """
-    import imp, sys
-
-    custom_name = custom_name or name
-
-    f, pathname, desc = imp.find_module(name, sys.path[1:])
-    module = imp.load_module(custom_name, f, pathname, desc)
-    f.close()
-
-    return module
-
-"""
-Import non-local module, use a custom name to differentiate it from local
-This name is only used internally for identifying the module. We decide
-the name in the local scope by assigning it to the variable sys_collections.
-"""
-sys_collections = import_non_local('collections','sys_collections')
-
-
-
 tracker = None
 progress_thread = None
 

=== modified file 'duplicity/tempdir.py'
--- duplicity/tempdir.py	2014-04-17 22:03:10 +0000
+++ duplicity/tempdir.py	2014-04-20 14:58:57 +0000
@@ -54,7 +54,7 @@
 
     _defaultLock.acquire()
     try:
-        if _defaultInstance is None:
+        if _defaultInstance is None or _defaultInstance.dir() is None:
             _defaultInstance = TemporaryDirectory(temproot = globals.temproot)
         return _defaultInstance
     finally:

=== modified file 'duplicity/util.py'
--- duplicity/util.py	2014-04-17 20:50:57 +0000
+++ duplicity/util.py	2014-04-20 14:58:57 +0000
@@ -145,3 +145,30 @@
         except UnlockError:
             pass
 
+def copyfileobj(infp, outfp, byte_count = -1):
+    """Copy byte_count bytes from infp to outfp, or all if byte_count < 0
+
+    Returns the number of bytes actually written (may be less than
+    byte_count if find eof.  Does not close either fileobj.
+
+    """
+    blocksize = 64 * 1024
+    bytes_written = 0
+    if byte_count < 0:
+        while 1:
+            buf = infp.read(blocksize)
+            if not buf:
+                break
+            bytes_written += len(buf)
+            outfp.write(buf)
+    else:
+        while bytes_written + blocksize <= byte_count:
+            buf = infp.read(blocksize)
+            if not buf:
+                break
+            bytes_written += len(buf)
+            outfp.write(buf)
+        buf = infp.read(byte_count - bytes_written)
+        bytes_written += len(buf)
+        outfp.write(buf)
+    return bytes_written

=== modified file 'po/POTFILES.in'
--- po/POTFILES.in	2014-04-18 14:32:30 +0000
+++ po/POTFILES.in	2014-04-20 14:58:57 +0000
@@ -15,7 +15,6 @@
 duplicity/collections.py
 duplicity/log.py
 duplicity/robust.py
-duplicity/misc.py
 duplicity/diffdir.py
 duplicity/lazy.py
 duplicity/backends/_cf_pyrax.py

=== modified file 'setup.py'
--- setup.py	2014-04-17 19:34:23 +0000
+++ setup.py	2014-04-20 14:58:57 +0000
@@ -79,7 +79,7 @@
 
         # make symlinks for test data
         if build_cmd.build_lib != top_dir:
-            for path in ['testfiles.tar.gz', 'testtar.tar', 'gnupg']:
+            for path in ['testfiles.tar.gz', 'gnupg']:
                 src = os.path.join(top_dir, 'testing', path)
                 target = os.path.join(build_cmd.build_lib, 'testing', path)
                 try:
@@ -133,8 +133,9 @@
       packages = ['duplicity',
                   'duplicity.backends',
                   'testing',
-                  'testing.helpers',
-                  'testing.tests'],
+                  'testing.functional',
+                  'testing.overrides',
+                  'testing.unit'],
       package_dir = {"duplicity" : "duplicity",
                      "duplicity.backends" : "duplicity/backends",},
       ext_modules = [Extension("duplicity._librsync",
@@ -144,8 +145,8 @@
                                libraries=["rsync"])],
       scripts = ['bin/rdiffdir', 'bin/duplicity'],
       data_files = data_files,
-      tests_require = ['lockfile', 'mock', 'nose', 'pexpect'],
-      test_suite = 'nose.collector',
+      tests_require = ['lockfile', 'mock', 'pexpect'],
+      test_suite = 'testing',
       cmdclass={'test': TestCommand,
                 'install': InstallCommand,
                 'sdist': SDistCommand},

=== removed file 'testing/__init__.py'
=== renamed file 'testing/tests/__init__.py' => 'testing/__init__.py'
--- testing/tests/__init__.py	2014-04-16 20:45:09 +0000
+++ testing/__init__.py	2014-04-20 14:58:57 +0000
@@ -21,18 +21,21 @@
 import os
 import sys
 import time
-
-_this_dir = os.path.dirname(os.path.abspath(__file__))
-_testing_dir = os.path.dirname(_this_dir)
+import unittest
+
+from duplicity import backend
+from duplicity import globals
+from duplicity import log
+
+_testing_dir = os.path.dirname(os.path.abspath(__file__))
 _top_dir = os.path.dirname(_testing_dir)
-_helpers_dir = os.path.join(_testing_dir, 'helpers')
 _overrides_dir = os.path.join(_testing_dir, 'overrides')
 
-# Adjust python path for duplicity and helper modules
-sys.path = [_overrides_dir, _top_dir, _helpers_dir] + sys.path
+# Adjust python path for duplicity and override modules
+sys.path = [_overrides_dir, _top_dir] + sys.path
 
 # Also set PYTHONPATH for any subprocesses
-os.environ['PYTHONPATH'] = _overrides_dir + ":" + _top_dir
+os.environ['PYTHONPATH'] = _overrides_dir + ":" + _top_dir + ":" + os.environ.get('PYTHONPATH', '')
 
 # Now set some variables that help standardize test behavior
 os.environ['LANG'] = ''
@@ -41,3 +44,55 @@
 # Standardize time
 os.environ['TZ'] = 'US/Central'
 time.tzset()
+
+
+class DuplicityTestCase(unittest.TestCase):
+
+    sign_key = '56538CCF'
+    sign_passphrase = 'test'
+    encrypt_key1 = 'B5FA894F'
+    encrypt_key2 = '9B736B2A'
+
+    def setUp(self):
+        super(DuplicityTestCase, self).setUp()
+        self.savedEnviron = {}
+        self.savedGlobals = {}
+
+        # TODO: remove these lines
+        log.setup()
+        log.setverbosity(log.WARNING)
+        self.set_global('print_statistics', 0)
+        backend.import_backends()
+
+        # Have all file references in tests relative to our testing dir
+        os.chdir(_testing_dir)
+
+    def tearDown(self):
+        for key in self.savedEnviron:
+            self._update_env(key, self.savedEnviron[key])
+        for key in self.savedGlobals:
+            setattr(globals, key, self.savedGlobals[key])
+        assert not os.system("rm -rf testfiles")
+        super(DuplicityTestCase, self).tearDown()
+
+    def unpack_testfiles(self):
+        assert not os.system("rm -rf testfiles")
+        assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
+        assert not os.system("mkdir testfiles/output testfiles/cache")
+
+    def _update_env(self, key, value):
+        if value is not None:
+            os.environ[key] = value
+        elif key in os.environ:
+            del os.environ[key]
+
+    def set_environ(self, key, value):
+        if key not in self.savedEnviron:
+            self.savedEnviron[key] = os.environ.get(key)
+        self._update_env(key, value)
+
+    def set_global(self, key, value):
+        assert hasattr(globals, key)
+        if key not in self.savedGlobals:
+            self.savedGlobals[key] = getattr(globals, key)
+        setattr(globals, key, value)

=== added directory 'testing/functional'
=== added file 'testing/functional/__init__.py'
--- testing/functional/__init__.py	1970-01-01 00:00:00 +0000
+++ testing/functional/__init__.py	2014-04-20 14:58:57 +0000
@@ -0,0 +1,147 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright 2012 Canonical Ltd
+#
+# This file is part of duplicity.
+#
+# Duplicity is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# Duplicity is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with duplicity; if not, write to the Free Software Foundation,
+# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import os
+import pexpect
+import time
+import unittest
+
+from duplicity import backend
+from .. import DuplicityTestCase
+
+
+class CmdError(Exception):
+    """Indicates an error running an external command"""
+    def __init__(self, code):
+        Exception.__init__(self, code)
+        self.exit_status = code
+
+
+class FunctionalTestCase(DuplicityTestCase):
+
+    def setUp(self):
+        super(FunctionalTestCase, self).setUp()
+
+        self.unpack_testfiles()
+
+        self.class_args = []
+        self.backend_url = "file://testfiles/output"
+        self.last_backup = None
+        self.set_environ('PASSPHRASE', self.sign_passphrase)
+        self.set_environ("SIGN_PASSPHRASE", self.sign_passphrase)
+
+        backend_inst = backend.get_backend(self.backend_url)
+        bl = backend_inst.list()
+        if bl:
+            backend_inst.delete(backend_inst.list())
+        backend_inst.close()
+
+    def run_duplicity(self, options=[], current_time=None, fail=None,
+                      passphrase_input=[]):
+        """
+        Run duplicity binary with given arguments and options
+        """
+        # We run under setsid and take input from /dev/null (below) because
+        # this way we force a failure if duplicity tries to read from the
+        # console unexpectedly (like for gpg password or such).
+        cmd_list = ["setsid", "duplicity"]
+        cmd_list.extend(options)
+        cmd_list.extend(["-v0"])
+        cmd_list.extend(["--no-print-statistics"])
+        cmd_list.extend(["--allow-source-mismatch"])
+        cmd_list.extend(["--archive-dir=testfiles/cache"])
+        if current_time:
+            cmd_list.extend(["--current-time", current_time])
+        cmd_list.extend(self.class_args)
+        if fail:
+            cmd_list.extend(["--fail", str(fail)])
+        cmdline = " ".join(map(lambda x: '"%s"' % x, cmd_list))
+
+        if not passphrase_input:
+            cmdline += " < /dev/null"
+        child = pexpect.spawn('/bin/sh', ['-c', cmdline])
+        for passphrase in passphrase_input:
+            child.expect('passphrase.*:')
+            child.sendline(passphrase)
+        child.wait()
+        return_val = child.exitstatus
+
+        #print "Ran duplicity command: ", cmdline, "\n with return_val: ", child.exitstatus
+        if fail:
+            self.assertEqual(30, child.exitstatus)
+        elif return_val:
+            raise CmdError(child.exitstatus)
+
+    def backup(self, type, input_dir, options=[], **kwargs):
+        """Run duplicity backup to default directory"""
+        options = [type, input_dir, self.backend_url, "--volsize", "1"] + options
+        before_files = self.get_backend_files()
+
+        # If a chain ends with time X and the next full chain begins at time X,
+        # we may trigger an assert in collections.py.  If needed, sleep to
+        # avoid such problems
+        if self.last_backup == int(time.time()):
+            time.sleep(1)
+
+        result = self.run_duplicity(options=options, **kwargs)
+        self.last_backup = int(time.time())
+
+        after_files = self.get_backend_files()
+        return after_files - before_files
+
+    def restore(self, file_to_restore=None, time=None, options=[], **kwargs):
+        assert not os.system("rm -rf testfiles/restore_out")
+        options = [self.backend_url, "testfiles/restore_out"] + options
+        if file_to_restore:
+            options.extend(['--file-to-restore', file_to_restore])
+        if time:
+            options.extend(['--restore-time', str(time)])
+        self.run_duplicity(options=options, **kwargs)
+
+    def verify(self, dirname, file_to_verify=None, time=None, options=[],
+               **kwargs):
+        options = ["verify", self.backend_url, dirname] + options
+        if file_to_verify:
+            options.extend(['--file-to-restore', file_to_verify])
+        if time:
+            options.extend(['--restore-time', str(time)])
+        self.run_duplicity(options=options, **kwargs)
+
+    def cleanup(self, options=[]):
+        """
+        Run duplicity cleanup to default directory
+        """
+        options = ["cleanup", self.backend_url, "--force"] + options
+        self.run_duplicity(options=options)
+
+    def get_backend_files(self):
+        backend_inst = backend.get_backend(self.backend_url)
+        bl = backend_inst.list()
+        backend_inst.close()
+        return set(bl)
+
+    def make_largefiles(self, count=3, size=2):
+        """
+        Makes a number of large files in testfiles/largefiles that each are
+        the specified number of megabytes.
+        """
+        assert not os.system("mkdir testfiles/largefiles")
+        for n in range(count):
+            assert not os.system("dd if=/dev/urandom of=testfiles/largefiles/file%d bs=1024 count=%d > /dev/null 2>&1" % (n + 1, size * 1024))

=== renamed file 'testing/tests/test_badupload.py' => 'testing/functional/test_badupload.py'
--- testing/tests/test_badupload.py	2014-04-17 20:50:57 +0000
+++ testing/functional/test_badupload.py	2014-04-20 14:58:57 +0000
@@ -22,10 +22,10 @@
 
 import unittest
 
-from helper import CmdError, DuplicityTestCase
-
-
-class BadUploadTest(DuplicityTestCase):
+from . import CmdError, FunctionalTestCase
+
+
+class BadUploadTest(FunctionalTestCase):
     """
     Test missing volume upload using duplicity binary
     """

=== renamed file 'testing/tests/test_cleanup.py' => 'testing/functional/test_cleanup.py'
--- testing/tests/test_cleanup.py	2014-04-16 02:43:43 +0000
+++ testing/functional/test_cleanup.py	2014-04-20 14:58:57 +0000
@@ -21,10 +21,10 @@
 
 import unittest
 
-from helper import DuplicityTestCase
-
-
-class CleanupTest(DuplicityTestCase):
+from . import FunctionalTestCase
+
+
+class CleanupTest(FunctionalTestCase):
     """
     Test cleanup using duplicity binary
     """

=== renamed file 'testing/tests/test_final.py' => 'testing/functional/test_final.py'
--- testing/tests/test_final.py	2014-04-16 02:43:43 +0000
+++ testing/functional/test_final.py	2014-04-20 14:58:57 +0000
@@ -23,10 +23,10 @@
 import unittest
 
 from duplicity import path
-from helper import CmdError, DuplicityTestCase
-
-
-class FinalTest(DuplicityTestCase):
+from . import CmdError, FunctionalTestCase
+
+
+class FinalTest(FunctionalTestCase):
     """
     Test backup/restore using duplicity binary
     """
@@ -165,17 +165,17 @@
 
 
 class OldFilenamesFinalTest(FinalTest):
-    @classmethod
-    def setUpClass(cls):
-        super(OldFilenamesFinalTest, cls).setUpClass()
-        cls.class_args.extend(["--old-filenames"])
+
+    def setUp(self):
+        super(OldFilenamesFinalTest, self).setUp()
+        self.class_args.extend(["--old-filenames"])
 
 
 class ShortFilenamesFinalTest(FinalTest):
-    @classmethod
-    def setUpClass(cls):
-        super(ShortFilenamesFinalTest, cls).setUpClass()
-        cls.class_args.extend(["--short-filenames"])
+
+    def setUp(self):
+        super(ShortFilenamesFinalTest, self).setUp()
+        self.class_args.extend(["--short-filenames"])
 
 if __name__ == "__main__":
     unittest.main()

=== renamed file 'testing/tests/test_log.py' => 'testing/functional/test_log.py'
--- testing/tests/test_log.py	2014-04-16 02:43:43 +0000
+++ testing/functional/test_log.py	2014-04-20 14:58:57 +0000
@@ -21,14 +21,19 @@
 import unittest
 import os
 
-class LogTest(unittest.TestCase):
+from . import FunctionalTestCase
+
+
+class LogTest(FunctionalTestCase):
     """Test machine-readable functions/classes in log.py"""
 
     def setUp(self):
+        super(LogTest, self).setUp()
         assert not os.system("rm -f /tmp/duplicity.log")
 
     def tearDown(self):
         assert not os.system("rm -f /tmp/duplicity.log")
+        super(LogTest, self).tearDown()
 
     def test_command_line_error(self):
         """Check notification of a simple error code"""

=== renamed file 'testing/tests/test_rdiffdir.py' => 'testing/functional/test_rdiffdir.py'
--- testing/tests/test_rdiffdir.py	2014-04-16 02:43:43 +0000
+++ testing/functional/test_rdiffdir.py	2014-04-20 14:58:57 +0000
@@ -19,29 +19,18 @@
 # along with duplicity; if not, write to the Free Software Foundation,
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-import helper
-import sys, unittest, os
+import unittest, os
 
 from duplicity import path
-
-helper.setup()
-
-class RdiffdirTest(unittest.TestCase):
+from . import FunctionalTestCase
+
+
+class RdiffdirTest(FunctionalTestCase):
     """Test rdiffdir command line program"""
-    def setUp(self):
-        assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
-
-    def tearDown(self):
-        assert not os.system("rm -rf testfiles tempdir temp2.tar")
 
     def run_cmd(self, command):
         assert not os.system(command)
 
-    def del_tmp(self):
-        """Make new testfiles/output dir"""
-        self.run_cmd("rm -rf testfiles/output")
-        os.mkdir("testfiles/output")
-
     def run_rdiffdir(self, argstring):
         """Run rdiffdir with given arguments"""
         self.run_cmd("rdiffdir " + argstring)
@@ -49,7 +38,6 @@
     def run_cycle(self, dirname_list):
         """Run diff/patch cycle on directories in dirname_list"""
         assert len(dirname_list) >= 2
-        self.del_tmp()
 
         seq_path = path.Path("testfiles/output/sequence")
         new_path = path.Path(dirname_list[0])

=== renamed file 'testing/tests/test_restart.py' => 'testing/functional/test_restart.py'
--- testing/tests/test_restart.py	2014-04-17 21:49:37 +0000
+++ testing/functional/test_restart.py	2014-04-20 14:58:57 +0000
@@ -24,10 +24,10 @@
 import subprocess
 import unittest
 
-from helper import DuplicityTestCase
-
-
-class RestartTest(DuplicityTestCase):
+from . import FunctionalTestCase
+
+
+class RestartTest(FunctionalTestCase):
     """
     Test checkpoint/restart using duplicity binary
     """
@@ -205,7 +205,6 @@
         # 'restart' the backup
         self.backup("full", source, options=["--volsize=5", "--name=backup1"])
         # Confirm we actually resumed the previous backup
-        print os.listdir("testfiles/output")
         self.assertEqual(len(os.listdir("testfiles/output")), 4)
         # Now make sure everything is byte-for-byte the same once restored
         self.restore()
@@ -292,10 +291,10 @@
 
 # Note that this class duplicates all the tests in RestartTest
 class RestartTestWithoutEncryption(RestartTest):
-    @classmethod
-    def setUpClass(cls):
-        super(RestartTestWithoutEncryption, cls).setUpClass()
-        cls.class_args.extend(["--no-encryption"])
+
+    def setUp(self):
+        super(RestartTestWithoutEncryption, self).setUp()
+        self.class_args.extend(["--no-encryption"])
 
     def test_no_write_double_snapshot(self):
         """

=== removed directory 'testing/helpers'
=== removed file 'testing/helpers/helper.py'
--- testing/helpers/helper.py	2014-04-17 19:34:23 +0000
+++ testing/helpers/helper.py	1970-01-01 00:00:00 +0000
@@ -1,191 +0,0 @@
-# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
-#
-# Copyright 2002 Ben Escoto <ben@xxxxxxxxxxx>
-# Copyright 2007 Kenneth Loafman <kenneth@xxxxxxxxxxx>
-#
-# This file is part of duplicity.
-#
-# Duplicity is free software; you can redistribute it and/or modify it
-# under the terms of the GNU General Public License as published by the
-# Free Software Foundation; either version 2 of the License, or (at your
-# option) any later version.
-#
-# Duplicity is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with duplicity; if not, write to the Free Software Foundation,
-# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-
-import os
-import pexpect
-import time
-import unittest
-
-from duplicity import backend
-from duplicity import globals
-from duplicity import log
-
-sign_key = '56538CCF'
-sign_passphrase = 'test'
-encrypt_key1 = 'B5FA894F'
-encrypt_key2 = '9B736B2A'
-
-def setup():
-    """ setup for unit tests """
-    # TODO: remove these lines
-    log.setup()
-    log.setverbosity(log.WARNING)
-    globals.print_statistics = 0
-    backend.import_backends()
-
-    # Have all file references in tests relative to our testing dir
-    _testing_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-    os.chdir(_testing_dir)
-    # Make sure that PYTHONPATH is set right for subprocesses (setuptools
-    # may futz with it)
-    _top_dir = os.path.dirname(_testing_dir)
-    os.environ['PYTHONPATH'] = _top_dir
-
-
-class CmdError(Exception):
-    """Indicates an error running an external command"""
-    def __init__(self, code):
-        Exception.__init__(self, code)
-        self.exit_status = code
-
-
-class DuplicityTestCase(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        cls.class_args = []
-        cls.backend_url = "file://testfiles/output"
-        cls.sign_key = sign_key
-        cls.sign_passphrase = sign_passphrase
-        cls.encrypt_key1 = encrypt_key1
-        cls.encrypt_key2 = encrypt_key2
-        setup()
-
-    def setUp(self):
-        assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
-        assert not os.system("rm -rf testfiles/output testfiles/largefiles "
-                             "testfiles/restore_out testfiles/cache")
-        assert not os.system("mkdir testfiles/output testfiles/cache")
-
-        backend_inst = backend.get_backend(self.backend_url)
-        bl = backend_inst.list()
-        if bl:
-            backend_inst.delete(backend_inst.list())
-        backend_inst.close()
-
-        self.last_backup = None
-        self.set_environ('PASSPHRASE', self.sign_passphrase)
-        self.set_environ("SIGN_PASSPHRASE", self.sign_passphrase)
-
-    def tearDown(self):
-        self.set_environ("PASSPHRASE", None)
-        assert not os.system("rm -rf testfiles tempdir temp2.tar")
-
-    def set_environ(self, varname, value):
-        if value is not None:
-            os.environ[varname] = value
-        else:
-            try:
-                del os.environ[varname]
-            except Exception:
-                pass
-
-    def run_duplicity(self, options=[], current_time=None, fail=None,
-                      passphrase_input=[]):
-        """
-        Run duplicity binary with given arguments and options
-        """
-        # We run under setsid and take input from /dev/null (below) because
-        # this way we force a failure if duplicity tries to read from the
-        # console unexpectedly (like for gpg password or such).
-        cmd_list = ["setsid", "duplicity"]
-        cmd_list.extend(options)
-        cmd_list.extend(["-v0"])
-        cmd_list.extend(["--no-print-statistics"])
-        cmd_list.extend(["--allow-source-mismatch"])
-        cmd_list.extend(["--archive-dir=testfiles/cache"])
-        if current_time:
-            cmd_list.extend(["--current-time", current_time])
-        cmd_list.extend(self.class_args)
-        if fail:
-            cmd_list.extend(["--fail", str(fail)])
-        cmdline = " ".join(map(lambda x: '"%s"' % x, cmd_list))
-
-        if not passphrase_input:
-            cmdline += " < /dev/null"
-        child = pexpect.spawn('/bin/sh', ['-c', cmdline])
-        for passphrase in passphrase_input:
-            child.expect('passphrase.*:')
-            child.sendline(passphrase)
-        child.wait()
-        return_val = child.exitstatus
-
-        #print "Ran duplicity command: ", cmdline, "\n with return_val: ", child.exitstatus
-        if fail:
-            self.assertEqual(30, child.exitstatus)
-        elif return_val:
-            raise CmdError(child.exitstatus)
-
-    def backup(self, type, input_dir, options=[], **kwargs):
-        """Run duplicity backup to default directory"""
-        options = [type, input_dir, self.backend_url, "--volsize", "1"] + options
-        before_files = self.get_backend_files()
-
-        # If a chain ends with time X and the next full chain begins at time X,
-        # we may trigger an assert in collections.py.  If needed, sleep to
-        # avoid such problems
-        if self.last_backup == int(time.time()):
-            time.sleep(1)
-
-        result = self.run_duplicity(options=options, **kwargs)
-        self.last_backup = int(time.time())
-
-        after_files = self.get_backend_files()
-        return after_files - before_files
-
-    def restore(self, file_to_restore=None, time=None, options=[], **kwargs):
-        assert not os.system("rm -rf testfiles/restore_out")
-        options = [self.backend_url, "testfiles/restore_out"] + options
-        if file_to_restore:
-            options.extend(['--file-to-restore', file_to_restore])
-        if time:
-            options.extend(['--restore-time', str(time)])
-        self.run_duplicity(options=options, **kwargs)
-
-    def verify(self, dirname, file_to_verify=None, time=None, options=[],
-               **kwargs):
-        options = ["verify", self.backend_url, dirname] + options
-        if file_to_verify:
-            options.extend(['--file-to-restore', file_to_verify])
-        if time:
-            options.extend(['--restore-time', str(time)])
-        self.run_duplicity(options=options, **kwargs)
-
-    def cleanup(self, options=[]):
-        """
-        Run duplicity cleanup to default directory
-        """
-        options = ["cleanup", self.backend_url, "--force"] + options
-        self.run_duplicity(options=options)
-
-    def get_backend_files(self):
-        backend_inst = backend.get_backend(self.backend_url)
-        bl = backend_inst.list()
-        backend_inst.close()
-        return set(bl)
-
-    def make_largefiles(self, count=3, size=2):
-        """
-        Makes a number of large files in testfiles/largefiles that each are
-        the specified number of megabytes.
-        """
-        assert not os.system("mkdir testfiles/largefiles")
-        for n in range(count):
-            assert not os.system("dd if=/dev/urandom of=testfiles/largefiles/file%d bs=1024 count=%d > /dev/null 2>&1" % (n + 1, size * 1024))

=== renamed file 'testing/rootfiles.tar.gz' => 'testing/manual/rootfiles.tar.gz'
=== modified file 'testing/manual/roottest.py'
--- testing/manual/roottest.py	2012-02-29 19:21:10 +0000
+++ testing/manual/roottest.py	2014-04-20 14:58:57 +0000
@@ -39,7 +39,7 @@
         # make sure uid/gid match euid/egid
         os.setuid(os.geteuid())
         os.setgid(os.getegid())
-        assert not os.system("tar xzf rootfiles.tar.gz > /dev/null 2>&1")
+        assert not os.system("tar xzf manual/rootfiles.tar.gz > /dev/null 2>&1")
 
     def tearDown(self):
         assert not os.system("rm -rf testfiles tempdir temp2.tar")

=== modified file 'testing/overrides/gettext.py'
--- testing/overrides/gettext.py	2014-02-05 02:57:01 +0000
+++ testing/overrides/gettext.py	2014-04-20 14:58:57 +0000
@@ -22,13 +22,8 @@
 # always return a string with fancy unicode characters, which will notify us
 # if we ever get a unicode->ascii translation by accident.
 
-def translation(*args, **kwargs):
-    class Translation:
-        ZWSP = u"​" # ZERO WIDTH SPACE, basically an invisible space separator
-        def install(self, **kwargs):
-            import __builtin__
-            __builtin__.__dict__['_'] = lambda x: x + self.ZWSP
-        def ungettext(self, one, more, n):
-            if n == 1: return one + self.ZWSP
-            else:      return more + self.ZWSP
-    return Translation()
+def install(*args, **kwargs):
+    ZWSP = u"​" # ZERO WIDTH SPACE, basically an invisible space separator
+    import __builtin__
+    __builtin__.__dict__['_'] = lambda x: x + ZWSP
+    __builtin__.__dict__['ngettext'] = lambda one, more, n: one + ZWSP if n == 1 else more + ZWSP

=== modified file 'testing/run-tests'
--- testing/run-tests	2014-04-16 20:45:09 +0000
+++ testing/run-tests	2014-04-20 14:58:57 +0000
@@ -20,54 +20,5 @@
 # along with duplicity; if not, write to the Free Software Foundation,
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-# Go to directory housing this script
-cd $(dirname $0)
-
-THISDIR=$(pwd)
-export PATH="$(dirname $THISDIR)/bin:$PATH"
-
-TOP_TESTS=$*
-if [ -z "$TOP_TESTS" ]; then
-      TOP_TESTS="all"
-fi
-
-if [ "$TOP_TESTS" = "all" ]; then
-      TOP_TESTS="tests" # don't include manual tests unless they were asked for
-fi
-
-# Expand arguments if directories
-TESTS=
-for t in $TOP_TESTS; do
-      if [ -d "$t" ]; then
-        TESTS="$TESTS $(ls $t/*.py)"
-      else
-        TESTS="$TESTS $t"
-      fi
-done
-
-# run against all supported python versions
-for v in 2.6 2.7; do
-    type python$v >& /dev/null
-    if [ $? == 1 ]; then
-        echo "python$v not found on system"
-        continue
-    fi
-
-    echo "========== Compiling librsync for python$v =========="
-    pushd ../duplicity
-    python$v ./compilec.py
-    popd
-
-    for t in $TESTS; do
-        echo "========== Running $t for python$v =========="
-        pushd .
-        if ! python$v -u $t -v 2>&1; then
-          echo "Test failed"
-          exit 1
-        fi
-        popd
-        echo "========== Finished $t for python$v =========="
-        echo
-        echo
-    done
-done
+cd $(dirname $(dirname $0))
+tox

=== modified file 'testing/run-tests-ve'
--- testing/run-tests-ve	2014-04-16 20:45:09 +0000
+++ testing/run-tests-ve	2014-04-20 14:58:57 +0000
@@ -20,57 +20,5 @@
 # along with duplicity; if not, write to the Free Software Foundation,
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-# Go to directory housing this script
-cd $(dirname $0)
-
-THISDIR=$(pwd)
-export PATH="$(dirname $THISDIR)/bin:$PATH"
-
-TOP_TESTS=$*
-if [ -z "$TOP_TESTS" ]; then
-      TOP_TESTS="all"
-fi
-
-if [ "$TOP_TESTS" = "all" ]; then
-      TOP_TESTS="tests" # don't include manual tests unless they were asked for
-fi
-
-# Expand arguments if directories
-TESTS=
-for t in $TOP_TESTS; do
-      if [ -d "$t" ]; then
-        TESTS="$TESTS $(ls $t/*.py)"
-      else
-        TESTS="$TESTS $t"
-      fi
-done
-
-# run against all supported python versions
-for v in 2.6 2.7; do
-    ve=~/virtual$v
-    if [ $? == 1 ]; then
-        echo "virtual$v not found on system"
-        continue
-    fi
-    source $ve/bin/activate
-
-    echo "========== Compiling librsync for virtual$v =========="
-    pushd ../duplicity
-    python ./compilec.py
-    popd
-
-    for t in $TESTS; do
-        echo "========== Running $t for virtual$v =========="
-        pushd .
-        if ! python -u $t -v 2>&1; then
-          echo "Test failed"
-          exit 1
-        fi
-        popd
-        echo "========== Finished $t for virtual$v =========="
-        echo
-        echo
-    done
-
-    deactivate
-done
+cd $(dirname $(dirname $0))
+tox

=== renamed file 'testing/tests/test_python3.py' => 'testing/test_code.py'
--- testing/tests/test_python3.py	2014-04-17 22:26:39 +0000
+++ testing/test_code.py	2014-04-20 14:58:57 +0000
@@ -18,43 +18,107 @@
 # along with duplicity; if not, write to the Free Software Foundation,
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-import helper
 import os
 import subprocess
 import unittest
 
-helper.setup()
-
-
-class Python3ReadinessTest(unittest.TestCase):
+from . import _top_dir, DuplicityTestCase
+
+
+class CodeTest(DuplicityTestCase):
+
+    def run_checker(self, cmd):
+        process = subprocess.Popen(cmd,
+                                   stdout=subprocess.PIPE,
+                                   stderr=subprocess.PIPE)
+        output = process.communicate()[0]
+        self.assertEqual(0, process.returncode, output)
+        self.assertEqual("", output, output)
+
     def test_2to3(self):
-        _top_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)),
-                                "..", "..")
-
         # As we modernize the source code, we can remove more and more nofixes
-        process = subprocess.Popen(["2to3",
-                                    "--nofix=dict",
-                                    "--nofix=filter",
-                                    "--nofix=map",
-                                    "--nofix=next",
-                                    "--nofix=print",
-                                    "--nofix=types",
-                                    "--nofix=unicode",
-                                    "--nofix=xrange",
+        self.run_checker(["2to3",
+                          "--nofix=dict",
+                          "--nofix=filter",
+                          "--nofix=map",
+                          "--nofix=next",
+                          "--nofix=print",
+                          "--nofix=types",
+                          "--nofix=unicode",
+                          "--nofix=xrange",
         # The following fixes we don't want to remove, since they are false
         # positives, things we don't care about, or real incompatibilities
         # but which 2to3 can fix for us better automatically.
-                                    "--nofix=callable",
-                                    "--nofix=future",
-                                    "--nofix=imports",
-                                    "--nofix=raw_input",
-                                    "--nofix=urllib",
-                                    _top_dir],
-                                   stdout=subprocess.PIPE,
-                                   stderr=subprocess.PIPE)
-        output = process.communicate()[0]
-        self.assertEqual(0, process.returncode)
-        self.assertEqual("", output, output)
+                          "--nofix=callable",
+                          "--nofix=future",
+                          "--nofix=imports",
+                          "--nofix=raw_input",
+                          "--nofix=urllib",
+                          _top_dir])
+
+    def test_pylint(self):
+        self.run_checker(["pylint",
+                          "-E",
+                          "--msg-template={msg_id}: {line}: {msg}",
+                          "--disable=E0203",  # Access to member before its definition line
+                          "--disable=E0602",  # Undefined variable
+                          "--disable=E0611",  # No name in module
+                          "--disable=E1101",  # Has no member
+                          "--disable=E1103",  # Maybe has no member
+                          "--ignore=_librsync.so",
+                          os.path.join(_top_dir, 'duplicity'),
+                          os.path.join(_top_dir, 'bin/duplicity'),
+                          os.path.join(_top_dir, 'bin/rdiffdir')])
+
+    def test_pep8(self):
+        # All these ignores are just because when this test was added, I didn't
+        # want to fix all of the code.  Over time we can remove some of these
+        # and clean up the code.  But for now, let's at least get *some* pep8
+        # coverage.
+        ignores = ["E111",
+                   "E121",
+                   "E122",
+                   "E124",
+                   "E125",
+                   "E126",
+                   "E127",
+                   "E128",
+                   "E201",
+                   "E202",
+                   "E203",
+                   "E211",
+                   "E221",
+                   "E222",
+                   "E225",
+                   "E226",
+                   "E228",
+                   "E231",
+                   "E241",
+                   "E251",
+                   "E261",
+                   "E262",
+                   "E271",
+                   "E272",
+                   "E301",
+                   "E302",
+                   "E303",
+                   "E401",
+                   "E501",
+                   "E502",
+                   "E701",
+                   "E702",
+                   "E703",
+                   "E711",
+                   "E721",
+                   "W291",
+                   "W292",
+                   "W293",
+                   "W391"]
+        self.run_checker(["pep8",
+                          "--ignore=" + ','.join(ignores),
+                          os.path.join(_top_dir, 'duplicity'),
+                          os.path.join(_top_dir, 'bin/duplicity'),
+                          os.path.join(_top_dir, 'bin/rdiffdir')])
 
 
 if __name__ == "__main__":

=== removed directory 'testing/tests'
=== removed file 'testing/tests/test_misc.py'
--- testing/tests/test_misc.py	2014-04-16 02:43:43 +0000
+++ testing/tests/test_misc.py	1970-01-01 00:00:00 +0000
@@ -1,91 +0,0 @@
-# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
-#
-# Copyright 2002 Ben Escoto <ben@xxxxxxxxxxx>
-# Copyright 2007 Kenneth Loafman <kenneth@xxxxxxxxxxx>
-#
-# This file is part of duplicity.
-#
-# Duplicity is free software; you can redistribute it and/or modify it
-# under the terms of the GNU General Public License as published by the
-# Free Software Foundation; either version 2 of the License, or (at your
-# option) any later version.
-#
-# Duplicity is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with duplicity; if not, write to the Free Software Foundation,
-# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-
-import helper
-import sys, os, unittest, cStringIO
-
-from duplicity import misc
-
-helper.setup()
-
-class MiscTest(unittest.TestCase):
-    """Test functions/classes in misc.py"""
-    def setUp(self):
-        assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
-
-    def tearDown(self):
-        assert not os.system("rm -rf testfiles tempdir temp2.tar")
-
-    def deltmp(self):
-        assert not os.system("rm -rf testfiles/output")
-        os.mkdir("testfiles/output")
-
-    def test_file_volume_writer(self):
-        """Test FileVolumeWriter class"""
-        self.deltmp()
-        s = "hello" * 10000
-        assert len(s) == 50000
-        infp = cStringIO.StringIO(s)
-        fvw = misc.FileVolumeWriter(infp, "testfiles/output/volume")
-        fvw.volume_size = 20000
-        fvw.blocksize = 5000
-
-        l = []
-        for filename in fvw: l.append(filename)
-        assert l == ['testfiles/output/volume.1',
-                     'testfiles/output/volume.2',
-                     'testfiles/output/volume.3'], l
-
-        s2 = ""
-        for filename in l:
-            infp2 = open(filename, "rb")
-            s2 += infp2.read()
-            assert not infp2.close()
-
-        assert s2 == s
-
-    def test_file_volume_writer2(self):
-        """Test again but one volume this time"""
-        self.deltmp()
-        fvw = misc.FileVolumeWriter(cStringIO.StringIO("hello, world!"),
-                                    "testfiles/output/one_vol")
-        assert fvw.next() == "testfiles/output/one_vol"
-        self.assertRaises(StopIteration, fvw.next)
-
-    def test_file_volume_writer3(self):
-        """Test case when end of file falls exactly on volume boundary"""
-        self.deltmp()
-        s = "hello" * 10000
-        assert len(s) == 50000
-        infp = cStringIO.StringIO(s)
-        fvw = misc.FileVolumeWriter(infp, "testfiles/output/volume")
-        fvw.volume_size = 25000
-        fvw.blocksize = 5000
-
-        l = []
-        for filename in fvw: l.append(filename)
-        assert l == ['testfiles/output/volume.1',
-                     'testfiles/output/volume.2']
-
-
-
-if __name__ == "__main__":
-    unittest.main()

=== removed file 'testing/tests/test_unicode.py'
--- testing/tests/test_unicode.py	2014-04-16 20:45:09 +0000
+++ testing/tests/test_unicode.py	1970-01-01 00:00:00 +0000
@@ -1,39 +0,0 @@
-# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
-#
-# Copyright 2012 Canonical Ltd
-#
-# This file is part of duplicity.
-#
-# Duplicity is free software; you can redistribute it and/or modify it
-# under the terms of the GNU General Public License as published by the
-# Free Software Foundation; either version 2 of the License, or (at your
-# option) any later version.
-#
-# Duplicity is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with duplicity; if not, write to the Free Software Foundation,
-# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-
-import sys
-import unittest
-from mock import patch
-
-
-class UTF8Test(unittest.TestCase):
-
-    def setUp(self):
-        if 'duplicity' in sys.modules:
-            del(sys.modules["duplicity"])
-
-    @patch('gettext.install')
-    def test_module_install(self, gettext_mock):
-        """Make sure we convert translations to unicode"""
-        import duplicity
-        gettext_mock.assert_called_once_with('duplicity', unicode=True, names=['ngettext'])
-
-if __name__ == "__main__":
-    unittest.main()

=== added directory 'testing/unit'
=== added file 'testing/unit/__init__.py'
--- testing/unit/__init__.py	1970-01-01 00:00:00 +0000
+++ testing/unit/__init__.py	2014-04-20 14:58:57 +0000
@@ -0,0 +1,25 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright 2012 Canonical Ltd
+#
+# This file is part of duplicity.
+#
+# Duplicity is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# Duplicity is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with duplicity; if not, write to the Free Software Foundation,
+# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from .. import DuplicityTestCase
+
+
+class UnitTestCase(DuplicityTestCase):
+    pass

=== renamed file 'testing/tests/test_parsedurl.py' => 'testing/unit/test_backend.py'
--- testing/tests/test_parsedurl.py	2014-04-16 20:45:09 +0000
+++ testing/unit/test_backend.py	2014-04-20 14:58:57 +0000
@@ -19,16 +19,15 @@
 # along with duplicity; if not, write to the Free Software Foundation,
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-import helper
-import sys, unittest
+import unittest
 
 import duplicity.backend
 import duplicity.backends #@UnusedImport
 from duplicity.errors import * #@UnusedWildImport
-
-helper.setup()
-
-class ParsedUrlTest(unittest.TestCase):
+from . import UnitTestCase
+
+
+class ParsedUrlTest(UnitTestCase):
     """Test the ParsedUrl class"""
     def test_basic(self):
         """Test various url strings"""

=== renamed file 'testing/tests/test_collections.py' => 'testing/unit/test_collections.py'
--- testing/tests/test_collections.py	2014-04-17 21:49:37 +0000
+++ testing/unit/test_collections.py	2014-04-20 14:58:57 +0000
@@ -19,17 +19,15 @@
 # along with duplicity; if not, write to the Free Software Foundation,
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-import helper
 import os, sys, random, unittest
 
 from duplicity import collections
 from duplicity import backend
+from duplicity import globals
 from duplicity import path
 from duplicity import gpg
-from duplicity import globals
 from duplicity import dup_time
-
-helper.setup()
+from . import UnitTestCase
 
 filename_list1 = ["duplicity-full.2002-08-17T16:17:01-07:00.manifest.gpg",
                   "duplicity-full.2002-08-17T16:17:01-07:00.vol1.difftar.gpg",
@@ -71,15 +69,16 @@
                   "Extra stuff to be ignored"]
 
 
-class CollectionTest(unittest.TestCase):
+class CollectionTest(UnitTestCase):
     """Test collections"""
     def setUp(self):
-        assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
-        assert not os.system("mkdir testfiles/output")
+        super(CollectionTest, self).setUp()
+
+        self.unpack_testfiles()
 
         col_test_dir = path.Path("testfiles/collectionstest")
         archive_dir = col_test_dir.append("archive_dir")
-        globals.archive_dir = archive_dir
+        self.set_global('archive_dir', archive_dir)
         self.archive_dir_backend = backend.get_backend("file://testfiles/collectionstest"
                                                        "/archive_dir")
 
@@ -88,17 +87,9 @@
         self.output_dir = path.Path("testfiles/output") # used as a temp directory
         self.output_dir_backend = backend.get_backend("file://testfiles/output")
 
-    def tearDown(self):
-        assert not os.system("rm -rf testfiles tempdir temp2.tar")
-
-    def del_tmp(self):
-        """Reset the testfiles/output directory"""
-        self.output_dir.deltree()
-        self.output_dir.mkdir()
-
     def set_gpg_profile(self):
         """Set gpg profile to standard "foobar" sym"""
-        globals.gpg_profile = gpg.GPGProfile(passphrase = "foobar")
+        self.set_global('gpg_profile', gpg.GPGProfile(passphrase = "foobar"))
 
     def test_backup_chains(self):
         """Test basic backup chain construction"""
@@ -193,7 +184,6 @@
     def get_filelist2_cs(self):
         """Return set CollectionsStatus object from filelist 2"""
         # Set up testfiles/output with files from filename_list2
-        self.del_tmp()
         for filename in filename_list2:
             p = self.output_dir.append(filename)
             p.touch()

=== renamed file 'testing/tests/test_diffdir.py' => 'testing/unit/test_diffdir.py'
--- testing/tests/test_diffdir.py	2014-04-16 02:43:43 +0000
+++ testing/unit/test_diffdir.py	2014-04-20 14:58:57 +0000
@@ -19,7 +19,6 @@
 # along with duplicity; if not, write to the Free Software Foundation,
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-import helper
 import os, sys, unittest
 
 from duplicity.path import * #@UnusedWildImport
@@ -27,16 +26,14 @@
 from duplicity import selection
 from duplicity import util
 from duplicity import tarfile #@Reimport
-
-helper.setup()
-
-class DDTest(unittest.TestCase):
+from . import UnitTestCase
+
+
+class DDTest(UnitTestCase):
     """Test functions in diffdir.py"""
     def setUp(self):
-        assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
-
-    def tearDown(self):
-        assert not os.system("rm -rf testfiles tempdir temp2.tar")
+        super(DDTest, self).setUp()
+        self.unpack_testfiles()
 
     def copyfileobj(self, infp, outfp):
         """Copy in fileobj to out, closing afterwards"""
@@ -48,14 +45,8 @@
         assert not infp.close()
         assert not outfp.close()
 
-    def deltmp(self):
-        """Delete temporary directories"""
-        assert not os.system("rm -rf testfiles/output")
-        os.mkdir("testfiles/output")
-
     def testsig(self):
         """Test producing tar signature of various file types"""
-        self.deltmp()
         select = selection.Select(Path("testfiles/various_file_types"))
         select.set_iter()
         sigtar = diffdir.SigTarBlockIter(select)
@@ -68,7 +59,6 @@
 
     def empty_diff_schema(self, dirname):
         """Given directory name, make sure can tell when nothing changes"""
-        self.deltmp()
         select = selection.Select(Path(dirname))
         select.set_iter()
         sigtar = diffdir.SigTarBlockIter(select)
@@ -91,9 +81,7 @@
     def test_empty_diff(self):
         """Test producing a diff against same sig; should be len 0"""
         self.empty_diff_schema("testfiles/various_file_types")
-        return
 
-        self.deltmp()
         select = selection.Select(Path("testfiles/various_file_types"))
         select.set_iter()
         sigtar = diffdir.SigTarBlockIter(select)
@@ -114,7 +102,6 @@
 
     def test_diff(self):
         """Test making a diff"""
-        self.deltmp()
         sel1 = selection.Select(Path("testfiles/dir1"))
         diffdir.write_block_iter(diffdir.SigTarBlockIter(sel1.set_iter()),
                                  "testfiles/output/dir1.sigtar")
@@ -141,7 +128,6 @@
 
     def test_diff2(self):
         """Another diff test - this one involves multivol support"""
-        self.deltmp()
         sel1 = selection.Select(Path("testfiles/dir2"))
         diffdir.write_block_iter(diffdir.SigTarBlockIter(sel1.set_iter()),
                                  "testfiles/output/dir2.sigtar")
@@ -175,7 +161,6 @@
         those produced by DirDelta_WriteSig and other methods.
 
         """
-        self.deltmp()
         deltadir1 = Path("testfiles/output/dir.deltatar1") #@UnusedVariable
         deltadir2 = Path("testfiles/output/dir.deltatar2") #@UnusedVariable
         cur_full_sigs = Path("testfiles/output/fullsig.dir1")

=== renamed file 'testing/tests/test_temp.py' => 'testing/unit/test_dup_temp.py'
--- testing/tests/test_temp.py	2014-04-16 02:43:43 +0000
+++ testing/unit/test_dup_temp.py	2014-04-20 14:58:57 +0000
@@ -19,28 +19,16 @@
 # along with duplicity; if not, write to the Free Software Foundation,
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-import helper
-import sys, os, unittest, gzip
+import gzip
+import unittest
 
 from duplicity import dup_temp
 from duplicity import file_naming
-
-helper.setup()
-
-prefix = "testfiles/output"
-
-class TempTest(unittest.TestCase):
+from . import UnitTestCase
+
+
+class TempTest(UnitTestCase):
     """Test various temp files methods"""
-    def setUp(self):
-        assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
-
-    def tearDown(self):
-        assert not os.system("rm -rf testfiles tempdir temp2.tar")
-
-    def del_tmp(self):
-        """Delete testfiles/output and recreate"""
-        assert not os.system("rm -rf " + prefix)
-        assert not os.system("mkdir " + prefix)
 
     def test_temppath(self):
         """Allocate new temppath, try open_with_delete"""

=== renamed file 'testing/tests/test_time.py' => 'testing/unit/test_dup_time.py'
--- testing/tests/test_time.py	2014-04-16 02:43:43 +0000
+++ testing/unit/test_dup_time.py	2014-04-20 14:58:57 +0000
@@ -19,13 +19,10 @@
 # along with duplicity; if not, write to the Free Software Foundation,
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-import helper
 import sys, unittest, time, types
-from copy import copy
-from duplicity import globals
 from duplicity import dup_time
+from . import UnitTestCase
 
-helper.setup()
 
 class TimeTest:
     def testConversion(self):
@@ -41,12 +38,8 @@
 
     def testConversion_separator(self):
         """Same as testConversion, but change time Separator"""
-        prev_sep = copy(globals.time_separator)
-        try:
-            globals.time_separator = "_"
-            self.testConversion()
-        finally:
-            globals.time_separator = prev_sep
+        self.set_global('time_separator', "_")
+        self.testConversion()
 
     def testCmp(self):
         """Test time comparisons"""
@@ -64,19 +57,15 @@
 
     def testCmp_separator(self):
         """Like testCmp but with new separator"""
-        prev_sep = copy(globals.time_separator)
-        try:
-            globals.time_separator = "_"
-            cmp = dup_time.cmp
-            assert cmp(1,2) == -1
-            assert cmp(2,2) == 0
-            assert cmp(5,1) == 1
-            assert cmp("2001-09-01T21_49_04Z", "2001-08-01T21_49_04Z") == 1
-            assert cmp("2001-09-01T04_49_04+03_23", "2001-09-01T21_49_04Z") == -1
-            assert cmp("2001-09-01T12_00_00Z", "2001-09-01T04_00_00-08_00") == 0
-            assert cmp("2001-09-01T12_00_00-08_00", "2001-09-01T12_00_00-07_00") == 1
-        finally:
-            globals.time_separator = prev_sep
+        self.set_global('time_separator', "_")
+        cmp = dup_time.cmp
+        assert cmp(1,2) == -1
+        assert cmp(2,2) == 0
+        assert cmp(5,1) == 1
+        assert cmp("2001-09-01T21_49_04Z", "2001-08-01T21_49_04Z") == 1
+        assert cmp("2001-09-01T04_49_04+03_23", "2001-09-01T21_49_04Z") == -1
+        assert cmp("2001-09-01T12_00_00Z", "2001-09-01T04_00_00-08_00") == 0
+        assert cmp("2001-09-01T12_00_00-08_00", "2001-09-01T12_00_00-07_00") == 1
 
     def testStringtotime(self):
         """Test converting string to time"""
@@ -144,15 +133,19 @@
         t = int(time.time())
         assert dup_time.stringtotime(dup_time.timetostring(t)) == t
 
-class TimeTest1(TimeTest, unittest.TestCase):
-    
-    def setUp(self):
-        globals.old_filenames = False
-
-class TimeTest2(TimeTest, unittest.TestCase):
-    
-    def setUp(self):
-        globals.old_filenames = True
+
+class TimeTest1(TimeTest, UnitTestCase):
+    
+    def setUp(self):
+        super(TimeTest1, self).setUp()
+        self.set_global('old_filenames', False)
+
+
+class TimeTest2(TimeTest, UnitTestCase):
+    
+    def setUp(self):
+        super(TimeTest2, self).setUp()
+        self.set_global('old_filenames', True)
 
 if __name__ == '__main__':
     unittest.main()

=== renamed file 'testing/tests/test_filenaming.py' => 'testing/unit/test_file_naming.py'
--- testing/tests/test_filenaming.py	2014-04-17 21:49:37 +0000
+++ testing/unit/test_file_naming.py	2014-04-20 14:58:57 +0000
@@ -19,17 +19,16 @@
 # along with duplicity; if not, write to the Free Software Foundation,
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-import helper
-import sys, unittest
+import unittest
 
 from duplicity import dup_time
 from duplicity import file_naming
 from duplicity import log
 from duplicity import globals
-
-helper.setup()
-
-class Test36(unittest.TestCase):
+from . import UnitTestCase
+
+
+class Test36(UnitTestCase):
     def test_base36(self):
         """Test conversion to/from base 36"""
         numlist = [0, 1, 10, 1313, 34233, 872338, 2342889,
@@ -124,23 +123,28 @@
         assert pr.time == 1036954144, repr(pr.time)
 
 
-class FileNamingLong(unittest.TestCase, FileNamingBase):
+class FileNamingLong(UnitTestCase, FileNamingBase):
     """Test long filename parsing and generation"""
     def setUp(self):
-        globals.short_filenames = 0
-
-class FileNamingShort(unittest.TestCase, FileNamingBase):
+        super(FileNamingLong, self).setUp()
+        self.set_global('short_filenames', 0)
+
+
+class FileNamingShort(UnitTestCase, FileNamingBase):
     """Test short filename parsing and generation"""
     def setUp(self):
-        globals.short_filenames = 1
+        super(FileNamingShort, self).setUp()
+        self.set_global('short_filenames', 1)
+
         
-class FileNamingPrefixes(unittest.TestCase, FileNamingBase):
+class FileNamingPrefixes(UnitTestCase, FileNamingBase):
     """Test filename parsing and generation with prefixes"""
     def setUp(self):
-        globals.file_prefix = "global-"
-        globals.file_prefix_manifest = "mani-"
-        globals.file_prefix_signature = "sign-"
-        globals.file_prefix_archive = "arch-"
+        super(FileNamingPrefixes, self).setUp()
+        self.set_global('file_prefix', "global-")
+        self.set_global('file_prefix_manifest', "mani-")
+        self.set_global('file_prefix_signature', "sign-")
+        self.set_global('file_prefix_archive', "arch-")
 
 
 if __name__ == "__main__":

=== renamed file 'testing/tests/test_gpg.py' => 'testing/unit/test_gpg.py'
--- testing/tests/test_gpg.py	2014-04-16 02:43:43 +0000
+++ testing/unit/test_gpg.py	2014-04-20 14:58:57 +0000
@@ -19,31 +19,22 @@
 # along with duplicity; if not, write to the Free Software Foundation,
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-import helper
 import sys, os, unittest, random
 
 from duplicity import gpg
 from duplicity import path
-
-helper.setup()
-
-class GPGTest(unittest.TestCase):
+from . import UnitTestCase
+
+
+class GPGTest(UnitTestCase):
     """Test GPGFile"""
     def setUp(self):
-        assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
+        super(GPGTest, self).setUp()
+        self.unpack_testfiles()
         self.default_profile = gpg.GPGProfile(passphrase = "foobar")
 
-    def tearDown(self):
-        assert not os.system("rm -rf testfiles tempdir temp2.tar")
-
-    def deltmp(self):
-        """Delete testfiles/output and recreate"""
-        assert not os.system("rm -rf testfiles/output")
-        assert not os.system("mkdir testfiles/output")
-
     def gpg_cycle(self, s, profile = None):
         """Test encryption/decryption cycle on string s"""
-        self.deltmp()
         epath = path.Path("testfiles/output/encrypted_file")
         if not profile:
             profile = self.default_profile
@@ -77,34 +68,33 @@
 
     def test_gpg_asym(self):
         """Test GPG asymmetric encryption"""
-        profile = gpg.GPGProfile(passphrase = helper.sign_passphrase,
-                                 recipients = [helper.encrypt_key1,
-                                               helper.encrypt_key2])
+        profile = gpg.GPGProfile(passphrase = self.sign_passphrase,
+                                 recipients = [self.encrypt_key1,
+                                               self.encrypt_key2])
         self.gpg_cycle("aoensutha aonetuh saoe", profile)
 
-        profile2 = gpg.GPGProfile(passphrase = helper.sign_passphrase,
-                                  recipients = [helper.encrypt_key1])
+        profile2 = gpg.GPGProfile(passphrase = self.sign_passphrase,
+                                  recipients = [self.encrypt_key1])
         self.gpg_cycle("aoeu" * 10000, profile2)
 
     def test_gpg_hidden_asym(self):
         """Test GPG asymmetric encryption with hidden key id"""
-        profile = gpg.GPGProfile(passphrase = helper.sign_passphrase,
-                                 hidden_recipients = [helper.encrypt_key1,
-                                               helper.encrypt_key2])
+        profile = gpg.GPGProfile(passphrase = self.sign_passphrase,
+                                 hidden_recipients = [self.encrypt_key1,
+                                                      self.encrypt_key2])
         self.gpg_cycle("aoensutha aonetuh saoe", profile)
 
-        profile2 = gpg.GPGProfile(passphrase = helper.sign_passphrase,
-                                  hidden_recipients = [helper.encrypt_key1])
+        profile2 = gpg.GPGProfile(passphrase = self.sign_passphrase,
+                                  hidden_recipients = [self.encrypt_key1])
         self.gpg_cycle("aoeu" * 10000, profile2)
 
     def test_gpg_signing(self):
         """Test to make sure GPG reports the proper signature key"""
-        self.deltmp()
         plaintext = "hello" * 50000
 
-        signing_profile = gpg.GPGProfile(passphrase = helper.sign_passphrase,
-                                         sign_key = helper.sign_key,
-                                         recipients = [helper.encrypt_key1])
+        signing_profile = gpg.GPGProfile(passphrase = self.sign_passphrase,
+                                         sign_key = self.sign_key,
+                                         recipients = [self.encrypt_key1])
 
         epath = path.Path("testfiles/output/encrypted_file")
         encrypted_signed_file = gpg.GPGFile(1, epath, signing_profile)
@@ -115,16 +105,15 @@
         assert decrypted_file.read() == plaintext
         decrypted_file.close()
         sig = decrypted_file.get_signature()
-        assert sig == helper.sign_key, sig
+        assert sig == self.sign_key, sig
 
     def test_gpg_signing_and_hidden_encryption(self):
         """Test to make sure GPG reports the proper signature key even with hidden encryption key id"""
-        self.deltmp()
         plaintext = "hello" * 50000
 
-        signing_profile = gpg.GPGProfile(passphrase = helper.sign_passphrase,
-                                         sign_key = helper.sign_key,
-                                         hidden_recipients = [helper.encrypt_key1])
+        signing_profile = gpg.GPGProfile(passphrase = self.sign_passphrase,
+                                         sign_key = self.sign_key,
+                                         hidden_recipients = [self.encrypt_key1])
 
         epath = path.Path("testfiles/output/encrypted_file")
         encrypted_signed_file = gpg.GPGFile(1, epath, signing_profile)
@@ -135,11 +124,10 @@
         assert decrypted_file.read() == plaintext
         decrypted_file.close()
         sig = decrypted_file.get_signature()
-        assert sig == helper.sign_key, sig
+        assert sig == self.sign_key, sig
 
     def test_GPGWriteFile(self):
         """Test GPGWriteFile"""
-        self.deltmp()
         size = 400 * 1000
         gwfh = GPGWriteFile_Helper()
         profile = gpg.GPGProfile(passphrase = "foobar")
@@ -155,7 +143,6 @@
 
     def test_GzipWriteFile(self):
         """Test GzipWriteFile"""
-        self.deltmp()
         size = 400 * 1000
         gwfh = GPGWriteFile_Helper()
         for i in range(10): #@UnusedVariable
@@ -171,6 +158,7 @@
 class GPGWriteHelper2:
     def __init__(self, data): self.data = data
 
+
 class GPGWriteFile_Helper:
     """Used in test_GPGWriteFile above"""
     def __init__(self):
@@ -203,13 +191,11 @@
         return "e" * random.randrange(0, 15000)
 
 
-class SHATest(unittest.TestCase):
+class SHATest(UnitTestCase):
     """Test making sha signatures"""
     def setUp(self):
-        assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
-
-    def tearDown(self):
-        assert not os.system("rm -rf testfiles tempdir temp2.tar")
+        super(SHATest, self).setUp()
+        self.unpack_testfiles()
 
     def test_sha(self):
         hash = gpg.get_hash("SHA1", path.Path("testfiles/various_file_types/regular_file"))

=== renamed file 'testing/tests/test_GnuPGInterface.py' => 'testing/unit/test_gpginterface.py'
=== renamed file 'testing/tests/test_lazy.py' => 'testing/unit/test_lazy.py'
--- testing/tests/test_lazy.py	2014-04-17 21:15:36 +0000
+++ testing/unit/test_lazy.py	2014-04-20 14:58:57 +0000
@@ -19,22 +19,21 @@
 # along with duplicity; if not, write to the Free Software Foundation,
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-import helper
 import unittest, pickle, sys
 from functools import reduce
 
 from duplicity.lazy import * #@UnusedWildImport
-
-helper.setup()
-
-class Iterators(unittest.TestCase):
+from . import UnitTestCase
+
+
+class Iterators(UnitTestCase):
     one_to_100 = lambda s: iter(range(1, 101))
     evens = lambda s: iter(range(2, 101, 2))
     odds = lambda s: iter(range(1, 100, 2))
     empty = lambda s: iter([])
 
     def __init__(self, *args):
-        unittest.TestCase.__init__(self, *args)
+        super(Iterators, self).__init__(*args)
         self.falseerror = self.falseerror_maker()
         self.trueerror = self.trueerror_maker()
         self.emptygen = self.emptygen_maker()
@@ -275,8 +274,10 @@
         #print "Adding branch ", subinstance.total
         self.total += subinstance.total
 
-class TreeReducerTest(unittest.TestCase):
+class TreeReducerTest(UnitTestCase):
     def setUp(self):
+        super(TreeReducerTest, self).setUp()
+
         self.i1 = [(), (1,), (2,), (3,)]
         self.i2 = [(0,), (0,1), (0,1,0), (0,1,1), (0,2), (0,2,1), (0,3)]
 

=== renamed file 'testing/tests/test_manifest.py' => 'testing/unit/test_manifest.py'
--- testing/tests/test_manifest.py	2014-04-16 02:43:43 +0000
+++ testing/unit/test_manifest.py	2014-04-20 14:58:57 +0000
@@ -19,16 +19,16 @@
 # along with duplicity; if not, write to the Free Software Foundation,
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-import helper
-import sys, unittest, types
+import types
+import unittest
 
 from duplicity import manifest
-from duplicity import globals
 from duplicity import path
-
-helper.setup()
-
-class VolumeInfoTest(unittest.TestCase):
+from . import UnitTestCase
+
+
+
+class VolumeInfoTest(UnitTestCase):
     """Test VolumeInfo"""
     def test_basic(self):
         """Basic VolumeInfoTest"""
@@ -75,7 +75,7 @@
         assert not vi3.contains(("3",), recursive = 0)
 
 
-class ManifestTest(unittest.TestCase):
+class ManifestTest(UnitTestCase):
     """Test Manifest class"""
     def test_basic(self):
         vi1 = manifest.VolumeInfo()
@@ -87,7 +87,7 @@
         m = manifest.Manifest()
         for vi in [vi1, vi2, vi3]: m.add_volume_info(vi)
 
-        globals.local_path = path.Path("Foobar")
+        self.set_global('local_path', path.Path("Foobar"))
         m.set_dirinfo()
 
         s = m.to_string()

=== renamed file 'testing/tests/test_patchdir.py' => 'testing/unit/test_patchdir.py'
--- testing/tests/test_patchdir.py	2014-04-17 21:49:37 +0000
+++ testing/unit/test_patchdir.py	2014-04-20 14:58:57 +0000
@@ -19,7 +19,6 @@
 # along with duplicity; if not, write to the Free Software Foundation,
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-import helper
 import sys, cStringIO, unittest
 
 from duplicity import diffdir
@@ -29,16 +28,14 @@
 from duplicity import tarfile #@UnusedImport
 from duplicity import librsync #@UnusedImport
 from duplicity.path import * #@UnusedWildImport
-
-helper.setup()
-
-class PatchingTest(unittest.TestCase):
+from . import UnitTestCase
+
+
+class PatchingTest(UnitTestCase):
     """Test patching"""
     def setUp(self):
-        assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
-
-    def tearDown(self):
-        assert not os.system("rm -rf testfiles tempdir temp2.tar")
+        super(PatchingTest, self).setUp()
+        self.unpack_testfiles()
 
     def copyfileobj(self, infp, outfp):
         """Copy in fileobj to out, closing afterwards"""
@@ -50,11 +47,6 @@
         assert not infp.close()
         assert not outfp.close()
 
-    def deltmp(self):
-        """Delete temporary directories"""
-        assert not os.system("rm -rf testfiles/output")
-        os.mkdir("testfiles/output")
-
     def test_total(self):
         """Test cycle on dirx"""
         self.total_sequence(['testfiles/dir1',
@@ -68,7 +60,6 @@
     def total_sequence(self, filelist):
         """Test signatures, diffing, and patching on directory list"""
         assert len(filelist) >= 2
-        self.deltmp()
         sig = Path("testfiles/output/sig.tar")
         diff = Path("testfiles/output/diff.tar")
         seq_path = Path("testfiles/output/sequence")
@@ -107,7 +98,6 @@
 
     def test_doubledot_hole(self):
         """Test for the .. bug that lets tar overwrite parent dir"""
-        self.deltmp()
 
         def make_bad_tar(filename):
             """Write attack tarfile to filename"""
@@ -124,7 +114,6 @@
 
             tf.close()
 
-        self.deltmp()
         make_bad_tar("testfiles/output/bad.tar")
         os.mkdir("testfiles/output/temp")
 
@@ -139,12 +128,10 @@
     def __init__(self, index):
         self.index = index
 
-class CollateItersTest(unittest.TestCase):
+class CollateItersTest(UnitTestCase):
     def setUp(self):
-        assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
-
-    def tearDown(self):
-        assert not os.system("rm -rf testfiles tempdir temp2.tar")
+        super(CollateItersTest, self).setUp()
+        self.unpack_testfiles()
 
     def test_collate(self):
         """Test collate_iters function"""
@@ -193,15 +180,13 @@
         assert c == 3
 
 
-class TestInnerFuncs(unittest.TestCase):
+class TestInnerFuncs(UnitTestCase):
     """Test some other functions involved in patching"""
     def setUp(self):
-        assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
+        super(TestInnerFuncs, self).setUp()
+        self.unpack_testfiles()
         self.check_output()
 
-    def tearDown(self):
-        assert not os.system("rm -rf testfiles tempdir temp2.tar")
-
     def check_output(self):
         """Make sure testfiles/output exists"""
         out = Path("testfiles/output")

=== renamed file 'testing/tests/test_path.py' => 'testing/unit/test_path.py'
--- testing/tests/test_path.py	2014-04-16 02:43:43 +0000
+++ testing/unit/test_path.py	2014-04-20 14:58:57 +0000
@@ -19,25 +19,21 @@
 # along with duplicity; if not, write to the Free Software Foundation,
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-import helper
 import sys, unittest
 
 from duplicity import log #@UnusedImport
 from duplicity.path import * #@UnusedWildImport
-
-helper.setup()
-
-class PathTest(unittest.TestCase):
+from . import UnitTestCase
+
+
+class PathTest(UnitTestCase):
     """Test basic path functions"""
     def setUp(self):
-        assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
-
-    def tearDown(self):
-        assert not os.system("rm -rf testfiles tempdir temp2.tar")
+        super(PathTest, self).setUp()
+        self.unpack_testfiles()
 
     def test_deltree(self):
         """Test deleting a tree"""
-        assert not os.system("rm -rf testfiles/output")
         assert not os.system("cp -pR testfiles/deltree testfiles/output")
         p = Path("testfiles/output")
         assert p.isdir()
@@ -48,7 +44,6 @@
     # will never be equal (they don't implement __cmp__ or __eq__)
     #def test_compare(self):
     #    """Test directory comparisons"""
-    #    assert not os.system("rm -rf testfiles/output")
     #    assert not os.system("cp -pR testfiles/dir1 testfiles/output")
     #    assert Path("testfiles/dir1").compare_recursive(Path("testfiles/output"), 1)
     #    assert not Path("testfiles/dir1").compare_recursive(Path("testfiles/dir2"), 1)

=== renamed file 'testing/tests/test_selection.py' => 'testing/unit/test_selection.py'
--- testing/tests/test_selection.py	2014-04-16 02:43:43 +0000
+++ testing/unit/test_selection.py	2014-04-20 14:58:57 +0000
@@ -20,24 +20,22 @@
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
 import types
-import helper
 import StringIO, unittest, sys
 
 from duplicity.selection import * #@UnusedWildImport
 from duplicity.lazy import * #@UnusedWildImport
-
-helper.setup()
-
-class MatchingTest(unittest.TestCase):
+from . import UnitTestCase
+
+
+
+class MatchingTest(UnitTestCase):
     """Test matching of file names against various selection functions"""
     def setUp(self):
-        assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
+        super(MatchingTest, self).setUp()
+        self.unpack_testfiles()
         self.root = Path("testfiles/select")
         self.Select = Select(self.root)
 
-    def tearDown(self):
-        assert not os.system("rm -rf testfiles tempdir temp2.tar")
-
     def makeext(self, path):
         return self.root.new_index(tuple(path.split("/")))
 
@@ -122,7 +120,7 @@
     def testFilelistIncludeNullSep(self):
         """Test included filelist but with null_separator set"""
         fp = StringIO.StringIO("""\0testfiles/select/1/2\0testfiles/select/1\0testfiles/select/1/2/3\0testfiles/select/3/3/2\0testfiles/select/hello\nthere\0""")
-        globals.null_separator = 1
+        self.set_global('null_separator', 1)
         sf = self.Select.filelist_get_sf(fp, 1, "test")
         assert sf(self.root) == 1
         assert sf(self.makeext("1")) == 1
@@ -133,7 +131,6 @@
         assert sf(self.makeext("3/3")) == 1
         assert sf(self.makeext("3/3/3")) == None
         assert sf(self.makeext("hello\nthere")) == 1
-        globals.null_separator = 0
 
     def testFilelistExclude(self):
         """Test included filelist"""
@@ -271,15 +268,13 @@
         assert sf(Path("/proc")) == sfval, \
                "Assumption: /proc is on a different filesystem"
 
-class ParseArgsTest(unittest.TestCase):
+class ParseArgsTest(UnitTestCase):
     """Test argument parsing"""
     def setUp(self):
-        assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
-
-    def tearDown(self):
-        assert not os.system("rm -rf testfiles tempdir temp2.tar")
-
-    root = None
+        super(ParseArgsTest, self).setUp()
+        self.unpack_testfiles()
+        self.root = None
+
     def ParseTest(self, tuplelist, indicies, filelists = []):
         """No error if running select on tuple goes over indicies"""
         if not self.root:

=== renamed file 'testing/tests/test_statistics.py' => 'testing/unit/test_statistics.py'
--- testing/tests/test_statistics.py	2014-04-16 02:43:43 +0000
+++ testing/unit/test_statistics.py	2014-04-20 14:58:57 +0000
@@ -19,21 +19,18 @@
 # along with duplicity; if not, write to the Free Software Foundation,
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-import helper
 import sys, unittest
 
 from duplicity.statistics import * #@UnusedWildImport
 from duplicity import path
-
-helper.setup()
-
-class StatsObjTest(unittest.TestCase):
+from . import UnitTestCase
+
+
+class StatsObjTest(UnitTestCase):
     """Test StatsObj class"""
     def setUp(self):
-        assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
-
-    def tearDown(self):
-        assert not os.system("rm -rf testfiles tempdir temp2.tar")
+        super(StatsObjTest, self).setUp()
+        self.unpack_testfiles()
 
     def set_obj(self, s):
         """Set values of s's statistics"""

=== renamed file 'testing/tests/test_tarfile.py' => 'testing/unit/test_tarfile.py'
--- testing/tests/test_tarfile.py	2014-04-16 20:45:09 +0000
+++ testing/unit/test_tarfile.py	2014-04-20 14:58:57 +0000
@@ -18,15 +18,13 @@
 # along with duplicity; if not, write to the Free Software Foundation,
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-import helper
 import unittest
 from duplicity import cached_ops
 from duplicity import tarfile
-
-helper.setup()
-
-
-class TarfileTest(unittest.TestCase):
+from . import UnitTestCase
+
+
+class TarfileTest(UnitTestCase):
     def test_cached_ops(self):
         self.assertTrue(tarfile.grp is cached_ops)
         self.assertTrue(tarfile.pwd is cached_ops)

=== renamed file 'testing/tests/test_tempdir.py' => 'testing/unit/test_tempdir.py'
--- testing/tests/test_tempdir.py	2014-04-16 02:43:43 +0000
+++ testing/unit/test_tempdir.py	2014-04-20 14:58:57 +0000
@@ -19,14 +19,14 @@
 # along with duplicity; if not, write to the Free Software Foundation,
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-import helper
-import sys, os, unittest
+import os
+import unittest
 
 from duplicity import tempdir
-
-helper.setup()
-
-class TempDirTest(unittest.TestCase):
+from . import UnitTestCase
+
+
+class TempDirTest(UnitTestCase):
     def test_all(self):
         td = tempdir.default()
 
@@ -48,4 +48,3 @@
 
 if __name__ == "__main__":
     unittest.main()
-

=== modified file 'tox.ini'
--- tox.ini	2014-04-17 19:34:23 +0000
+++ tox.ini	2014-04-20 14:58:57 +0000
@@ -4,6 +4,5 @@
 [testenv]
 deps=lockfile
      mock
-     nose
      pexpect
-commands=nosetests
+commands={envpython} ./setup.py test


Follow ups