← Back to team overview

duplicity-team team mailing list archive

[Merge] lp:~mterry/duplicity/modern-testing into lp:duplicity

 

Michael Terry has proposed merging lp:~mterry/duplicity/modern-testing into lp:duplicity with lp:~mterry/duplicity/consolidate-tests as a prerequisite.

Requested reviews:
  duplicity-team (duplicity-team)

For more details, see:
https://code.launchpad.net/~mterry/duplicity/modern-testing/+merge/215994

Enable/use more modern testing tools like nosetests and tox as well as more common setup.py hooks like test and sdist.

Specifically:
 * move setup.py to toplevel where most tools and users expect it
 * Move and adjust test files to work when running "nosetests" in toplevel directory.  Specifically, do a lot more of the test setup in tests/__init__.py rather than the run-tests scripts
 * Add small tox.ini file for using tox, which is a wrapper for using virtualenv.  Only enable for py26 and py27 right now, since modern setuptools dropped support for <2.6 (and tox 1.7 also recently dropped <2.6)
 * Add setup.py hooks for test and sdist which are both standard targets (sdist just outsources to dist/makedist right now)
-- 
https://code.launchpad.net/~mterry/duplicity/modern-testing/+merge/215994
Your team duplicity-team is requested to review the proposed merge of lp:~mterry/duplicity/modern-testing into lp:duplicity.
=== modified file 'bin/duplicity'
--- bin/duplicity	2014-03-10 14:17:08 +0000
+++ bin/duplicity	2014-04-16 02:52:51 +0000
@@ -33,10 +33,6 @@
 from datetime import datetime
 from lockfile import FileLock
 
-pwd = os.path.abspath(os.path.dirname(sys.argv[0]))
-if os.path.exists(os.path.join(pwd, "../duplicity")):
-    sys.path.insert(0, os.path.abspath(os.path.join(pwd, "../.")))
-
 from duplicity import log
 log.setup()
 

=== modified file 'dist/makedist'
--- dist/makedist	2011-11-25 17:47:57 +0000
+++ dist/makedist	2014-04-16 02:52:51 +0000
@@ -23,7 +23,6 @@
 import os, re, shutil, time, sys, subprocess
 
 SourceDir = "duplicity"
-DistDir = "dist"
 
 # Various details about the files must also be specified by the rpm
 # spec template.
@@ -89,7 +88,7 @@
                   os.path.join(tardir, "bin", "duplicity.1"))
     VersionedCopy(os.path.join("bin", "rdiffdir.1"),
                   os.path.join(tardir, "bin", "rdiffdir.1"))
-    VersionedCopy(os.path.join(DistDir, "setup.py"),
+    VersionedCopy("setup.py",
                   os.path.join(tardir, "setup.py"))
 
     # make sure executables are

=== renamed file 'dist/setup.py' => 'setup.py'
--- dist/setup.py	2011-11-25 17:47:57 +0000
+++ setup.py	2014-04-16 02:52:51 +0000
@@ -21,7 +21,10 @@
 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
 import sys, os
-from distutils.core import setup, Extension
+from setuptools import setup, Extension
+from setuptools.command.test import test
+from setuptools.command.install import install
+from setuptools.command.sdist import sdist
 
 version_string = "$version"
 
@@ -55,8 +58,9 @@
                 'CHANGELOG']),
               ]
 
-assert os.path.exists("po"), "Missing 'po' directory."
-for root, dirs, files in os.walk("po"):
+top_dir = os.path.dirname(os.path.abspath(__file__))
+assert os.path.exists(os.path.join(top_dir, "po")), "Missing 'po' directory."
+for root, dirs, files in os.walk(os.path.join(top_dir, "po")):
     for file in files:
         path = os.path.join(root, file)
         if path.endswith("duplicity.mo"):
@@ -65,6 +69,60 @@
                 ('share/locale/%s/LC_MESSAGES' % lang,
                  ["po/%s/duplicity.mo" % lang]))
 
+
+class TestCommand(test):
+    def run(self):
+        # Make sure all modules are ready
+        build_cmd = self.get_finalized_command("build_py")
+        build_cmd.run()
+        # And make sure our scripts are ready
+        build_scripts_cmd = self.get_finalized_command("build_scripts")
+        build_scripts_cmd.run()
+
+        # make symlinks for test data
+        if build_cmd.build_lib != top_dir:
+            for path in ['testfiles.tar.gz', 'testtar.tar', 'gnupg']:
+                src = os.path.join(top_dir, 'testing', path)
+                target = os.path.join(build_cmd.build_lib, 'testing', path)
+                try:
+                    os.symlink(src, target)
+                except Exception:
+                    pass
+
+        os.environ['PATH'] = "%s:%s" % (
+            os.path.abspath(build_scripts_cmd.build_dir),
+            os.environ.get('PATH'))
+
+        test.run(self)
+
+
+class InstallCommand(install):
+    def run(self):
+        # Normally, install will call build().  But we want to delete the
+        # testing dir between building and installing.  So we manually build
+        # and mark ourselves to skip building when we run() for real.
+        self.run_command('build')
+        self.skip_build = True
+
+        # This should always be true, but just to make sure!
+        if self.build_lib != top_dir:
+            testing_dir = os.path.join(self.build_lib, 'testing')
+            os.system("rm -rf %s" % testing_dir)
+
+        install.run(self)
+
+
+# TODO: move logic from dist/makedist inline
+class SDistCommand(sdist):
+    def run(self):
+        version = version_string
+        if version[0] == '$':
+            version = "0"
+        os.system(os.path.join(top_dir, "dist", "makedist") + " " + version)
+        os.system("rm duplicity.spec")
+        os.system("mv duplicity-" + version + ".tar.gz " + self.dist_dir)
+
+
 setup(name="duplicity",
       version=version_string,
       description="Encrypted backup using rsync algorithm",
@@ -74,7 +132,10 @@
       maintainer_email="kenneth@xxxxxxxxxxx",
       url="http://duplicity.nongnu.org/index.html";,
       packages = ['duplicity',
-                  'duplicity.backends',],
+                  'duplicity.backends',
+                  'testing',
+                  'testing.helpers',
+                  'testing.tests'],
       package_dir = {"duplicity" : "duplicity",
                      "duplicity.backends" : "duplicity/backends",},
       ext_modules = [Extension("duplicity._librsync",
@@ -84,4 +145,9 @@
                                libraries=["rsync"])],
       scripts = ['bin/rdiffdir', 'bin/duplicity'],
       data_files = data_files,
+      tests_require = ['lockfile', 'nose'],
+      test_suite = 'nose.collector',
+      cmdclass={'test': TestCommand,
+                'install': InstallCommand,
+                'sdist': SDistCommand},
       )

=== added file 'testing/__init__.py'
--- testing/__init__.py	1970-01-01 00:00:00 +0000
+++ testing/__init__.py	2014-04-16 02:52:51 +0000
@@ -0,0 +1,3 @@
+import sys
+if sys.version_info < (2, 5,):
+	import tests

=== modified file 'testing/helpers/helper.py'
--- testing/helpers/helper.py	2014-04-16 02:52:51 +0000
+++ testing/helpers/helper.py	2014-04-16 02:52:51 +0000
@@ -33,14 +33,22 @@
 encrypt_key1 = 'B5FA894F'
 encrypt_key2 = '9B736B2A'
 
-# TODO: remove this method
 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"""
@@ -57,7 +65,7 @@
         cls.sign_key = sign_key
         cls.sign_passphrase = sign_passphrase
         cls.encrypt_key1 = encrypt_key1
-        cls.encrypt_key2 = encrypt_key2        
+        cls.encrypt_key2 = encrypt_key2
         setup()
 
     def setUp(self):

=== modified file 'testing/run-tests'
--- testing/run-tests	2014-02-05 02:57:01 +0000
+++ testing/run-tests	2014-04-16 02:52:51 +0000
@@ -24,11 +24,6 @@
 cd $(dirname $0)
 
 THISDIR=$(pwd)
-export TZ=US/Central
-export LANG=en_US.UTF-8
-# up for 'duplicity' module and here for 'helper.py'
-export PYTHONPATH="$THISDIR/overrides:$(dirname $THISDIR):$THISDIR/helpers"
-export GNUPGHOME="$THISDIR/gnupg"
 export PATH="$(dirname $THISDIR)/bin:$PATH"
 
 TOP_TESTS=$*

=== modified file 'testing/run-tests-ve'
--- testing/run-tests-ve	2011-11-25 19:11:39 +0000
+++ testing/run-tests-ve	2014-04-16 02:52:51 +0000
@@ -24,11 +24,6 @@
 cd $(dirname $0)
 
 THISDIR=$(pwd)
-export TZ=US/Central
-export LANG=
-# up for 'duplicity' module and here for 'helper.py'
-export PYTHONPATH="$(dirname $THISDIR):$THISDIR/helpers"
-export GNUPGHOME="$THISDIR/gnupg"
 export PATH="$(dirname $THISDIR)/bin:$PATH"
 
 TOP_TESTS=$*

=== added file 'testing/tests/__init__.py'
--- testing/tests/__init__.py	1970-01-01 00:00:00 +0000
+++ testing/tests/__init__.py	2014-04-16 02:52:51 +0000
@@ -0,0 +1,52 @@
+# -*- 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 sys
+import time
+
+_this_dir = os.path.dirname(os.path.abspath(__file__))
+_testing_dir = os.path.dirname(_this_dir)
+_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
+
+# Also set PYTHONPATH for any subprocesses
+os.environ['PYTHONPATH'] = _overrides_dir + ":" + _top_dir
+
+# Now set some variables that help standardize test behavior
+os.environ['LANG'] = ''
+os.environ['GNUPGHOME'] = os.path.join(_testing_dir, 'gnupg')
+
+# Standardize time
+os.environ['TZ'] = 'US/Central'
+time.tzset()
+
+# Automatically add all submodules into this namespace.  Helps python2.4
+# unittest work.
+if sys.version_info < (2, 5,):
+    for module in os.listdir(_this_dir):
+        if module == '__init__.py' or module[-3:] != '.py':
+            continue
+        __import__(module[:-3], locals(), globals())
+    del module

=== renamed file 'testing/tests/GnuPGInterfacetest.py' => 'testing/tests/test_GnuPGInterface.py'
=== renamed file 'testing/tests/badupload.py' => 'testing/tests/test_badupload.py'
=== renamed file 'testing/tests/cleanuptest.py' => 'testing/tests/test_cleanup.py'
=== renamed file 'testing/tests/collectionstest.py' => 'testing/tests/test_collections.py'
--- testing/tests/collectionstest.py	2011-11-04 04:07:59 +0000
+++ testing/tests/test_collections.py	2014-04-16 02:52:51 +0000
@@ -70,20 +70,6 @@
                   "duplicity-inc.2000-08-17T16:17:01-07:00.to.2000-08-18T00:04:30-07:00.vol1.difftar.gpg",
                   "Extra stuff to be ignored"]
 
-assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
-
-col_test_dir = path.Path("testfiles/collectionstest")
-archive_dir = col_test_dir.append("archive_dir")
-globals.archive_dir = archive_dir
-archive_dir_backend = backend.get_backend("file://testfiles/collectionstest"
-                                           "/archive_dir")
-
-dummy_backend = None
-real_backend = backend.get_backend("file://%s/%s" %
-                                   (col_test_dir.name, "remote_dir"))
-output_dir = path.Path("testfiles/output") # used as a temp directory
-output_dir_backend = backend.get_backend("file://testfiles/output")
-
 
 class CollectionTest(unittest.TestCase):
     """Test collections"""
@@ -91,13 +77,24 @@
         assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
         assert not os.system("mkdir testfiles/output")
 
+        col_test_dir = path.Path("testfiles/collectionstest")
+        archive_dir = col_test_dir.append("archive_dir")
+        globals.archive_dir = archive_dir
+        self.archive_dir_backend = backend.get_backend("file://testfiles/collectionstest"
+                                                       "/archive_dir")
+
+        self.real_backend = backend.get_backend("file://%s/%s" %
+                                                (col_test_dir.name, "remote_dir"))
+        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"""
-        output_dir.deltree()
-        output_dir.mkdir()
+        self.output_dir.deltree()
+        self.output_dir.mkdir()
 
     def set_gpg_profile(self):
         """Set gpg profile to standard "foobar" sym"""
@@ -106,7 +103,7 @@
     def test_backup_chains(self):
         """Test basic backup chain construction"""
         random.shuffle(filename_list1)
-        cs = collections.CollectionsStatus(dummy_backend, archive_dir)
+        cs = collections.CollectionsStatus(None, globals.archive_dir)
         chains, orphaned, incomplete = cs.get_backup_chains(filename_list1) #@UnusedVariable
         if len(chains) != 1 or len(orphaned) != 0:
             print chains
@@ -127,26 +124,26 @@
             assert cs.matched_chain_pair[0].end_time == 1029826800L
             assert len(cs.all_backup_chains) == 1, cs.all_backup_chains
 
-        cs = collections.CollectionsStatus(real_backend, archive_dir).set_values()
+        cs = collections.CollectionsStatus(self.real_backend, globals.archive_dir).set_values()
         check_cs(cs)
         assert cs.matched_chain_pair[0].islocal()
 
     def test_sig_chain(self):
         """Test a single signature chain"""
-        chain = collections.SignatureChain(1, archive_dir)
+        chain = collections.SignatureChain(1, globals.archive_dir)
         for filename in local_sigchain_filename_list:
             assert chain.add_filename(filename)
         assert not chain.add_filename("duplicity-new-signatures.2002-08-18T00:04:30-07:00.to.2002-08-20T00:00:00-07:00.sigtar.gpg")
 
     def test_sig_chains(self):
         """Test making signature chains from filename list"""
-        cs = collections.CollectionsStatus(dummy_backend, archive_dir)
+        cs = collections.CollectionsStatus(None, globals.archive_dir)
         chains, orphaned_paths = cs.get_signature_chains(local = 1)
         self.sig_chains_helper(chains, orphaned_paths)
 
     def test_sig_chains2(self):
         """Test making signature chains from filename list on backend"""
-        cs = collections.CollectionsStatus(archive_dir_backend, archive_dir)
+        cs = collections.CollectionsStatus(self.archive_dir_backend, globals.archive_dir)
         chains, orphaned_paths = cs.get_signature_chains(local = None)
         self.sig_chains_helper(chains, orphaned_paths)
 
@@ -161,16 +158,16 @@
     def sigchain_fileobj_get(self, local):
         """Return chain, local if local is true with filenames added"""
         if local:
-            chain = collections.SignatureChain(1, archive_dir)
+            chain = collections.SignatureChain(1, globals.archive_dir)
             for filename in local_sigchain_filename_list:
                 assert chain.add_filename(filename)
         else:
-            chain = collections.SignatureChain(None, real_backend)
+            chain = collections.SignatureChain(None, self.real_backend)
             for filename in remote_sigchain_filename_list:
                 assert chain.add_filename(filename)
         return chain
 
-    def sigchain_fileobj_testlist(self, chain):
+    def sigchain_fileobj_check_list(self, chain):
         """Make sure the list of file objects in chain has right contents
 
         The contents of the testfiles/collectiontest/remote_dir have
@@ -190,18 +187,18 @@
     def test_sigchain_fileobj(self):
         """Test getting signature chain fileobjs from archive_dir"""
         self.set_gpg_profile()
-        self.sigchain_fileobj_testlist(self.sigchain_fileobj_get(1))
-        self.sigchain_fileobj_testlist(self.sigchain_fileobj_get(None))
+        self.sigchain_fileobj_check_list(self.sigchain_fileobj_get(1))
+        self.sigchain_fileobj_check_list(self.sigchain_fileobj_get(None))
 
     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 = output_dir.append(filename)
+            p = self.output_dir.append(filename)
             p.touch()
 
-        cs = collections.CollectionsStatus(output_dir_backend, archive_dir)
+        cs = collections.CollectionsStatus(self.output_dir_backend, globals.archive_dir)
         cs.set_values()
         return cs
 

=== renamed file 'testing/tests/diffdirtest.py' => 'testing/tests/test_diffdir.py'
=== renamed file 'testing/tests/file_namingtest.py' => 'testing/tests/test_filenaming.py'
=== renamed file 'testing/tests/finaltest.py' => 'testing/tests/test_final.py'
=== renamed file 'testing/tests/gpgtest.py' => 'testing/tests/test_gpg.py'
--- testing/tests/gpgtest.py	2013-01-06 18:57:01 +0000
+++ testing/tests/test_gpg.py	2014-04-16 02:52:51 +0000
@@ -27,12 +27,11 @@
 
 helper.setup()
 
-default_profile = gpg.GPGProfile(passphrase = "foobar")
-
 class GPGTest(unittest.TestCase):
     """Test GPGFile"""
     def setUp(self):
         assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
+        self.default_profile = gpg.GPGProfile(passphrase = "foobar")
 
     def tearDown(self):
         assert not os.system("rm -rf testfiles tempdir temp2.tar")
@@ -47,7 +46,7 @@
         self.deltmp()
         epath = path.Path("testfiles/output/encrypted_file")
         if not profile:
-            profile = default_profile
+            profile = self.default_profile
         encrypted_file = gpg.GPGFile(1, epath, profile)
         encrypted_file.write(s)
         encrypted_file.close()

=== renamed file 'testing/tests/lazytest.py' => 'testing/tests/test_lazy.py'
=== renamed file 'testing/tests/logtest.py' => 'testing/tests/test_log.py'
=== renamed file 'testing/tests/manifesttest.py' => 'testing/tests/test_manifest.py'
=== renamed file 'testing/tests/misctest.py' => 'testing/tests/test_misc.py'
=== renamed file 'testing/tests/parsedurltest.py' => 'testing/tests/test_parsedurl.py'
=== renamed file 'testing/tests/patchdirtest.py' => 'testing/tests/test_patchdir.py'
=== renamed file 'testing/tests/pathtest.py' => 'testing/tests/test_path.py'
=== renamed file 'testing/tests/rdiffdirtest.py' => 'testing/tests/test_rdiffdir.py'
=== renamed file 'testing/tests/restarttest.py' => 'testing/tests/test_restart.py'
=== renamed file 'testing/tests/selectiontest.py' => 'testing/tests/test_selection.py'
=== renamed file 'testing/tests/statictest.py' => 'testing/tests/test_static.py'
=== renamed file 'testing/tests/statisticstest.py' => 'testing/tests/test_statistics.py'
=== renamed file 'testing/tests/dup_temptest.py' => 'testing/tests/test_temp.py'
=== renamed file 'testing/tests/tempdirtest.py' => 'testing/tests/test_tempdir.py'
=== renamed file 'testing/tests/dup_timetest.py' => 'testing/tests/test_time.py'
=== renamed file 'testing/tests/unicode.py' => 'testing/tests/test_unicode.py'
=== added file 'tox.ini'
--- tox.ini	1970-01-01 00:00:00 +0000
+++ tox.ini	2014-04-16 02:52:51 +0000
@@ -0,0 +1,7 @@
+[tox]
+envlist=py26,py27
+
+[testenv]
+deps=lockfile
+     nose
+commands=nosetests


Follow ups