yellow team mailing list archive
-
yellow team
-
Mailing list archive
-
Message #01024
[Merge] lp:~frankban/lpsetup/bug-1023895-init-repo-no-checkout into lp:lpsetup
Francesco Banconi has proposed merging lp:~frankban/lpsetup/bug-1023895-init-repo-no-checkout into lp:lpsetup.
Requested reviews:
Yellow Squad (yellow)
Related bugs:
Bug #1023895 in lpsetup: "lpsetup: add --no-checkout option to init-repo"
https://bugs.launchpad.net/lpsetup/+bug/1023895
For more details, see:
https://code.launchpad.net/~frankban/lpsetup/bug-1023895-init-repo-no-checkout/+merge/115108
== Changes ==
*handle_directories* now handles relative paths. E.g. it is possible to use a relative path for the target repository *init-repo* command line argument. Also updated the relevant TestCase.
Added the *handle_branch_and_checkout* handler: it validates branch and checkout names.
s/CHECKOUT_DIR/LP_REPOSITORY_DIR everywhere.
Added `--no-checkout` option to *init-repo*: it creates a "normal" non-lightweight checkout.
The runtime errors that can be generated by `bzr` calls in *init-repo* are now handled and raise an *ExecutionError*. To avoid race conditions this error handling is done by the step itself and not by the handlers.
The *fetch()* step now tries a lot: it contains 3 similar try/except blocks that could be abstracted, for instance, with a code like this::
def bzrcall(args, error=''):
"""Execute bzr passing *args*.
If the bzr command fails, raise an ExecutionError containing the given
*error* + the original bzr failure message.
"""
cmd = ['bzr'] + list(args)
try:
call(*cmd)
except subprocess.CalledProcessError as err:
raise exceptions.ExecutionError(error + err.output)
I decided to leave the explicit calls in *fetch()*: please feel free to suggest another approach.
Fixed *setup_bzr_locations*: now the step correctly handles a custom branch name that can be passed as an argument.
Implemented an integration TestCase for *init-repo*. The tests create a fake test branch and use it as source bzr repository. The template directory used to create the fake branch is placed in `lpsetup/tests/test-branch/`: right now it contains only a test file, but in the future it can be used as a "mocked" Launchpad branch. 3 tests are currently implemented: init-repo with tree, without tree, and bazaar location checks.
Created a *BackupFile* context manager, useful to backup and restore a single file: in this branch it is used to preserve bazaar's `locations.conf`.
Added tests for the *capture* context manager.
Added the helper function *create_test_branch()*: create a temporary fake test branch.
`setup.cfg` excludes *create_test_branch* and includes *utils*.
--
https://code.launchpad.net/~frankban/lpsetup/bug-1023895-init-repo-no-checkout/+merge/115108
Your team Yellow Squad is requested to review the proposed merge of lp:~frankban/lpsetup/bug-1023895-init-repo-no-checkout into lp:lpsetup.
=== modified file 'lpsetup/handlers.py'
--- lpsetup/handlers.py 2012-07-13 16:01:13 +0000
+++ lpsetup/handlers.py 2012-07-16 10:50:29 +0000
@@ -234,9 +234,8 @@
err = ("argument directory cannot contain "
"spaces: '{0}'".format(directory))
raise ValidationError(err)
-
- directory = directory.replace('~', namespace.home_dir)
- directory = os.path.abspath(directory)
+ directory = os.path.abspath(
+ directory.replace('~', namespace.home_dir))
if not directory.startswith(namespace.home_dir + os.path.sep):
raise ValidationError(
'argument {0} does not reside under the home '
@@ -279,3 +278,33 @@
def handle_working_dir(namespace):
"""Handle path to the working directory."""
namespace.working_dir = normalize_path(namespace.working_dir)
+
+
+def handle_branch_and_checkout(namespace):
+ """Handle branch and checkout names.
+
+ Raise *ValidationError* if::
+
+ - branch/checkout name is '.' or '..'
+ - branch/checkout name contains '/'
+ - lightweight checkout is used and branch and checkout have the same name
+
+ The namespace must contain the following names::
+
+ - branch_name
+ - checkout_name
+
+ This handler does not modify the namespace.
+ """
+ branch_name = namespace.branch_name
+ checkout_name = namespace.checkout_name
+ for name in (branch_name, checkout_name):
+ if name in ('.', '..') or '/' in name:
+ raise ValidationError(
+ 'invalid name "{0}": branch or checkout names can not '
+ 'be "." or ".." and can not contain "/"'.format(name))
+ is_lightweight = not getattr(namespace, 'no_checkout', False)
+ if is_lightweight and (checkout_name == branch_name):
+ raise ValidationError(
+ 'branch and checkout: can not use the same name ({0}).'.format(
+ checkout_name))
=== modified file 'lpsetup/settings.py'
--- lpsetup/settings.py 2012-07-11 15:47:34 +0000
+++ lpsetup/settings.py 2012-07-16 10:50:29 +0000
@@ -15,7 +15,6 @@
)
# a2enmod requires apache2.2-common
BASE_PACKAGES = ['ssh', 'bzr', 'apache2.2-common']
-CHECKOUT_DIR = '~/launchpad/lp-branches'
DEPENDENCIES_DIR = '~/launchpad/lp-sourcedeps'
HOSTS_CONTENT = (
('127.0.0.88',
@@ -39,22 +38,23 @@
)
LP_BZR_LOCATIONS = {
'{repository}': {
- 'submit_branch': '{checkout_dir}',
+ 'submit_branch': '{branch_dir}',
'public_branch': 'bzr+ssh://bazaar.launchpad.net/~{lpuser}/launchpad',
'public_branch:policy': 'appendpath',
'push_location': 'lp:~{lpuser}/launchpad',
'push_location:policy': 'appendpath',
- 'merge_target': '{checkout_dir}',
+ 'merge_target': '{branch_dir}',
'submit_to': 'merge@xxxxxxxxxxxxxxxxxx',
},
- '{checkout_dir}': {
+ '{branch_dir}': {
'public_branch':
'bzr+ssh://bazaar.launchpad.net/~launchpad-pqm/launchpad/devel',
}
}
LP_BRANCH_NAME = 'devel'
LP_CHECKOUT_NAME = 'sandbox'
-LP_CODE_DIR = os.path.join(CHECKOUT_DIR, LP_CHECKOUT_NAME)
+LP_REPOSITORY_DIR = '~/launchpad/lp-branches'
+LP_CODE_DIR = os.path.join(LP_REPOSITORY_DIR, LP_CHECKOUT_NAME)
LP_PACKAGES = [
# "launchpad-database-dependencies-9.1" can be removed once 8.x is
# no longer an option in "launchpad-developer-dependencies.
=== modified file 'lpsetup/subcommands/initrepo.py'
--- lpsetup/subcommands/initrepo.py 2012-07-12 18:23:37 +0000
+++ lpsetup/subcommands/initrepo.py 2012-07-16 10:50:29 +0000
@@ -19,16 +19,20 @@
]
import os
+import subprocess
from shelltoolbox import mkdirs
-from lpsetup import argparser
-from lpsetup import handlers
+from lpsetup import (
+ argparser,
+ exceptions,
+ handlers,
+ )
from lpsetup.settings import (
- CHECKOUT_DIR,
LP_BRANCH_NAME,
LP_BZR_LOCATIONS,
LP_CHECKOUT_NAME,
+ LP_REPOSITORY_DIR,
)
from lpsetup.utils import (
call,
@@ -37,23 +41,42 @@
)
-def fetch(source, repository, branch_name, checkout_name):
+def fetch(source, repository, branch_name, checkout_name, no_checkout):
"""Create a repo for the Launchpad code and retrieve it."""
# XXX should we warn if root?
# Set up the repository.
mkdirs(repository)
- call('bzr', 'init-repo', repository, '--no-trees')
+ is_lightweight = not no_checkout
+ no_trees = '--no-trees' if is_lightweight else None
+ # Initialize the repository.
+ try:
+ call('bzr', 'init-repo', '--quiet', repository, no_trees)
+ except subprocess.CalledProcessError as err:
+ msg = 'Error: unable to initialize the repository: '
+ raise exceptions.ExecutionError(msg + err.output)
# Set up the codebase.
branch_dir = os.path.join(repository, branch_name)
- checkout_dir = os.path.join(repository, checkout_name)
- call('bzr', 'branch', source, branch_dir)
- call('bzr', 'co', '--lightweight', branch_dir, checkout_dir)
-
-
-def setup_bzr_locations(lpuser, repository, template=LP_BZR_LOCATIONS):
+ # Retrieve the branch.
+ try:
+ call('bzr', 'branch', source, branch_dir)
+ except subprocess.CalledProcessError as err:
+ msg = 'Error: unable to branch source: '
+ raise exceptions.ExecutionError(msg + err.output)
+ if is_lightweight:
+ checkout_dir = os.path.join(repository, checkout_name)
+ # Create a lightweight checkout.
+ try:
+ call('bzr', 'co', '--lightweight', branch_dir, checkout_dir)
+ except subprocess.CalledProcessError as err:
+ msg = 'Error: unable to create the lightweight checkout: '
+ raise exceptions.ExecutionError(msg + err.output)
+
+
+def setup_bzr_locations(
+ lpuser, repository, branch_name, template=LP_BZR_LOCATIONS):
"""Set up bazaar locations."""
context = {
- 'checkout_dir': os.path.join(repository, LP_BRANCH_NAME),
+ 'branch_dir': os.path.join(repository, branch_name),
'repository': repository,
'lpuser': lpuser,
}
@@ -78,8 +101,10 @@
"""Get the Launchpad code and dependent source code."""
steps = (
- (fetch, 'source', 'repository', 'branch_name', 'checkout_name'),
- (setup_bzr_locations, 'lpuser', 'repository'),
+ (fetch,
+ 'source', 'repository', 'branch_name', 'checkout_name',
+ 'no_checkout'),
+ (setup_bzr_locations, 'lpuser', 'repository', 'branch_name'),
)
help = __doc__
@@ -87,6 +112,7 @@
handlers.handle_user,
handlers.handle_lpuser_from_lplogin,
handlers.handle_directories,
+ handlers.handle_branch_and_checkout,
handlers.handle_source,
)
@@ -107,14 +133,19 @@
'Defaults to {0}.'.format(LP_BRANCH_NAME))
parser.add_argument(
'--checkout-name', default=LP_CHECKOUT_NAME,
- help='Create a checkout with the given name. Defaults to {0}.'
- .format(LP_CHECKOUT_NAME))
+ help='Create a checkout with the given name. '
+ 'Ignored if --no-checkout is specified. '
+ 'Defaults to {0}.'.format(LP_CHECKOUT_NAME))
parser.add_argument(
- '-r', '--repository', default=CHECKOUT_DIR,
- help='The directory of the Launchpad repository to be updated. '
+ '-r', '--repository', default=LP_REPOSITORY_DIR,
+ help='The directory of the Launchpad repository to be created. '
'The directory must reside under the home directory of the '
'given user (see -u argument). '
- '[DEFAULT={0}]'.format(CHECKOUT_DIR))
+ '[DEFAULT={0}]'.format(LP_REPOSITORY_DIR))
+ parser.add_argument(
+ '--no-checkout', action='store_true', dest='no_checkout',
+ default=False, help='Initialize a bzr repository with trees and '
+ 'do not create a lightweight checkout.')
def add_arguments(self, parser):
super(SubCommand, self).add_arguments(parser)
=== modified file 'lpsetup/tests/subcommands/test_initrepo.py'
--- lpsetup/tests/subcommands/test_initrepo.py 2012-07-05 18:38:51 +0000
+++ lpsetup/tests/subcommands/test_initrepo.py 2012-07-16 10:50:29 +0000
@@ -4,39 +4,52 @@
"""Tests for the initrepo subcommand."""
+import getpass
+import os
+import shutil
+import tempfile
import unittest
-from lpsetup import handlers
+from lpsetup import (
+ cli,
+ handlers,
+ )
from lpsetup.subcommands import initrepo
from lpsetup.tests.utils import (
+ BackupFile,
+ create_test_branch,
get_random_string,
StepsBasedSubCommandTestMixin,
)
+from lpsetup.utils import ConfigParser
fetch_step = (initrepo.fetch,
- ['source', 'repository', 'branch_name', 'checkout_name'])
+ ['source', 'repository', 'branch_name', 'checkout_name', 'no_checkout'])
setup_bzr_locations_step = (initrepo.setup_bzr_locations,
- ['lpuser', 'repository'])
+ ['lpuser', 'repository', 'branch_name'])
def get_arguments():
return (
'--source', get_random_string(),
+ '--repository', get_random_string(),
'--branch-name', get_random_string(),
'--checkout-name', get_random_string(),
+ '--no-checkout',
)
class InitrepoTest(StepsBasedSubCommandTestMixin, unittest.TestCase):
- sub_command_name = 'initrepo'
+ sub_command_name = 'init-repo'
sub_command_class = initrepo.SubCommand
expected_arguments = get_arguments()
expected_handlers = (
handlers.handle_user,
handlers.handle_lpuser_from_lplogin,
handlers.handle_directories,
+ handlers.handle_branch_and_checkout,
handlers.handle_source,
)
expected_steps = (
@@ -44,3 +57,100 @@
setup_bzr_locations_step,
)
needs_root = False
+
+
+class IntegrationTest(unittest.TestCase):
+
+ def setUp(self):
+ """Create a fake source bzr branch under `/tmp/`."""
+ # Create the source branch and schedule removing.
+ self.source = create_test_branch()
+ self.addCleanup(shutil.rmtree, self.source)
+ # The target repository (containing the branch and the checkout)
+ # must be placed inside the user home, otherwise init-repo exits
+ # with a ValidationError.
+ home = os.path.expanduser('~')
+ self.repository = tempfile.mktemp(dir=home)
+ branch_name = 'branch-' + get_random_string()
+ checkout_name = 'checkout-' + get_random_string()
+ self.cmd = (
+ 'init-repo',
+ '--source', self.source,
+ '--repository', self.repository,
+ '--branch-name', branch_name,
+ '--checkout-name', checkout_name,
+ )
+ self.branch = os.path.join(self.repository, branch_name)
+ self.checkout = os.path.join(self.repository, checkout_name)
+ # The command init-repo updates `~/.bazaar/locations.conf`:
+ # the tests need to preserve that file.
+ self.bazaar_locations = os.path.join(
+ home, '.bazaar', 'locations.conf')
+
+ def tearDown(self):
+ """Remove the target repository."""
+ if os.path.isdir(self.repository):
+ shutil.rmtree(self.repository)
+
+ def assertIsBranch(self, path, no_tree=False):
+ """Assert the given *path* is a valid bzr branch.
+
+ If *no_tree* is True, also check the *path* does not contain
+ a working tree.
+ If *no_tree* is False, also check the *path* contains the test file.
+ """
+ if not os.path.isdir(path):
+ self.fail(path + ' is not a directory')
+ contents = os.listdir(path)
+ if '.bzr' not in contents:
+ self.fail(path + ' is not a bzr repository')
+ if no_tree:
+ if len(contents) > 1:
+ self.fail(path + ' contains a working tree')
+ elif 'test-file' not in contents:
+ self.fail(path + ' does not contain a working tree')
+
+ def test_no_trees(self):
+ # Ensure a lightweight checkout is correctly created by init-repo.
+ with BackupFile(self.bazaar_locations):
+ retcode = cli.main(self.cmd)
+ self.assertIsNone(retcode)
+ # The branch is created without tree.
+ self.assertIsBranch(self.branch, no_tree=True)
+ # The checkout is created with tree.
+ self.assertIsBranch(self.checkout, no_tree=False)
+
+ def test_with_trees(self):
+ # Ensure a "normal" branch is correctly created by init-repo.
+ with BackupFile(self.bazaar_locations):
+ retcode = cli.main(self.cmd + ('--no-checkout',))
+ self.assertIsNone(retcode)
+ # The branch is created with tree.
+ self.assertIsBranch(self.branch, no_tree=False)
+
+ def test_bazaar_locations(self):
+ # Ensure the file `~/.bazaar/locations.conf` is correctly updated.
+ # Assume lpuser is the current user.
+ user = getpass.getuser()
+ expected_locations = {
+ self.repository: {
+ 'merge_target': self.branch,
+ 'public_branch': 'bzr+ssh://bazaar.launchpad.net/~{}/'
+ 'launchpad'.format(user),
+ 'public_branch:policy': 'appendpath',
+ 'push_location': 'lp:~{}/launchpad'.format(user),
+ 'push_location:policy': 'appendpath',
+ 'submit_branch': self.branch,
+ 'submit_to': 'merge@xxxxxxxxxxxxxxxxxx',
+ },
+ self.branch: {
+ 'public_branch': 'bzr+ssh://bazaar.launchpad.net/'
+ '~launchpad-pqm/launchpad/devel',
+ },
+ }
+ with BackupFile(self.bazaar_locations) as cm:
+ cli.main(self.cmd)
+ parser = ConfigParser()
+ parser.read(cm.path)
+ for section, options in expected_locations.items():
+ self.assertItemsEqual(options.items(), parser.items(section))
=== added directory 'lpsetup/tests/test-branch'
=== added file 'lpsetup/tests/test-branch/test-file'
=== modified file 'lpsetup/tests/test_handlers.py'
--- lpsetup/tests/test_handlers.py 2012-07-09 21:18:21 +0000
+++ lpsetup/tests/test_handlers.py 2012-07-16 10:50:29 +0000
@@ -7,11 +7,13 @@
import argparse
from contextlib import contextmanager
import getpass
+import os
import pwd
import unittest
from lpsetup.exceptions import ValidationError
from lpsetup.handlers import (
+ handle_branch_and_checkout,
handle_directories,
handle_lpuser_as_username,
handle_ssh_keys,
@@ -61,9 +63,48 @@
raise TypeError
+class HandleBranchAndCheckoutTest(HandlersTestMixin, unittest.TestCase):
+
+ def test_valid_arguments(self):
+ # The validation succeed if there are no conflicts in arguments.
+ namespace = argparse.Namespace(
+ branch_name='branch',
+ checkout_name='checkout')
+ handle_branch_and_checkout(namespace)
+
+ def test_checkout_equals_branch(self):
+ # The validation fails if checkout and branch have the same name.
+ namespace = argparse.Namespace(
+ branch_name='branch',
+ checkout_name='branch')
+ with self.assertNotValid('checkout'):
+ handle_branch_and_checkout(namespace)
+
+ def test_checkout_equals_branch_without_trees(self):
+ # The validation succeed if checkout and branch have the same name
+ # but `--no-checkout` is specified.
+ namespace = argparse.Namespace(
+ branch_name='branch',
+ checkout_name='branch',
+ no_checkout=True)
+ handle_branch_and_checkout(namespace)
+
+ def test_invalid_names(self):
+ # The validation fails if the branch/checkout name is '.' or '..',
+ # or if it contains '/'.
+ for kwargs in (
+ {'branch_name': '.', 'checkout_name': 'checkout'},
+ {'branch_name': 'branch', 'checkout_name': '..'},
+ {'branch_name': '../', 'checkout_name': 'checkout'},
+ {'branch_name': 'branch', 'checkout_name': 'check/out'},
+ ):
+ with self.assertNotValid('invalid name'):
+ handle_branch_and_checkout(argparse.Namespace(**kwargs))
+
+
class HandleDirectoriesTest(HandlersTestMixin, unittest.TestCase):
- home_dir = '/home/foo'
+ home_dir = os.path.expanduser('~')
dependencies_dir = '~/launchpad/deps'
def test_home_is_expanded(self):
@@ -72,9 +113,12 @@
repository='~/launchpad', home_dir=self.home_dir,
dependencies_dir=self.dependencies_dir)
handle_directories(namespace)
- self.assertEqual('/home/foo/launchpad', namespace.repository)
- self.assertEqual(
- '/home/foo/launchpad/deps', namespace.dependencies_dir)
+ self.assertEqual(
+ os.path.join(self.home_dir, 'launchpad'),
+ namespace.repository)
+ self.assertEqual(
+ os.path.join(self.home_dir, 'launchpad', 'deps'),
+ namespace.dependencies_dir)
def test_directory_not_in_home(self):
# The validation fails for directories not residing inside the home.
@@ -90,6 +134,27 @@
with self.assertNotValid('directory'):
handle_directories(namespace)
+ def test_relative_path(self):
+ # Ensure relative paths are correctly expanded.
+ namespace = argparse.Namespace(
+ repository='launchpad', home_dir=self.home_dir,
+ dependencies_dir='launchpad/../deps')
+ handle_directories(namespace)
+ cwd = os.getcwd()
+ self.assertEqual(
+ os.path.join(cwd, 'launchpad'),
+ namespace.repository)
+ self.assertEqual(
+ os.path.join(cwd, 'deps'),
+ namespace.dependencies_dir)
+
+ def test_invalid_relative_path(self):
+ # The validation fails if a relative path points outside the home.
+ namespace = argparse.Namespace(
+ repository='~/../', home_dir=self.home_dir)
+ with self.assertNotValid('repository'):
+ handle_directories(namespace)
+
class HandleLPUserTest(unittest.TestCase):
=== modified file 'lpsetup/tests/utils.py'
--- lpsetup/tests/utils.py 2012-06-27 09:20:50 +0000
+++ lpsetup/tests/utils.py 2012-07-16 10:50:29 +0000
@@ -15,13 +15,141 @@
from contextlib import contextmanager
from functools import partial
+import os
import random
+import shutil
import string
from StringIO import StringIO
import sys
+import tempfile
+import unittest
+
+from shelltoolbox import run
from lpsetup import argparser
from lpsetup.tests import examples
+from lpsetup.utils import call
+
+
+class BackupFile(object):
+ """A context manager to backup and restore a file.
+
+ E.g.::
+
+ with Backup('/path/to/file') as cm:
+ cm.exists # True if the file to backup actually exists
+ cm.original # the place where original file is temporarily stored
+ cm.path # the given file path
+
+ If the file exists, it is restored when the context manager exits.
+ Otherwise, if created during cm execution, it is deleted exiting.
+ """
+ def __init__(self, path):
+ self.path = path
+ self.original = tempfile.mktemp()
+ self.exists = False
+ self.saved = False
+ self.restored = False
+
+ def __enter__(self):
+ """Backup the original path."""
+ self.backup()
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ """Restore the original path."""
+ self.restore()
+
+ def backup(self):
+ """Actually copy the file in a temporary position if it exists."""
+ self.exists = os.path.exists(self.path)
+ if not self.saved and self.exists:
+ shutil.copy2(self.path, self.original)
+ self.saved = True
+
+ def restore(self):
+ """Restore the file in the original position if it existed."""
+ if self.saved and not self.restored:
+ if os.path.exists(self.path):
+ os.remove(self.path)
+ if self.exists:
+ shutil.move(self.original, self.path)
+ self.restored = True
+
+
+class BackupFileTest(unittest.TestCase):
+ """Tests for the *BackupFile* context manager."""
+
+ contents = 'contents'
+
+ def temp_file(self):
+ f = tempfile.NamedTemporaryFile()
+ f.write(self.contents)
+ f.flush()
+ return f
+
+ def test_existent_file_is_restored(self):
+ # Ensure an existing file is correctly restored by the cm.
+ with self.temp_file() as f:
+ with BackupFile(f.name) as cm:
+ self.assertTrue(cm.exists)
+ f.write('new contents')
+ f.close()
+ self.assertEqual(self.contents, open(f.name).read())
+
+ def test_non_existent_file_is_deleted(self):
+ # Ensure a non existent file, if created, is then deleted by the cm.
+ path = tempfile.mktemp()
+ with BackupFile(path) as cm:
+ self.assertFalse(cm.exists)
+ with open(path, 'w') as f:
+ f.write('contents')
+ self.assertFalse(os.path.exists(path))
+
+ def test_unexisting_file_not_created(self):
+ # Ensure nothing happen when a non existent file is not created
+ # during cm execution.
+ path = tempfile.mktemp()
+ with BackupFile(path):
+ pass
+ self.assertFalse(os.path.exists(path))
+
+ def test_saved_restored_flags(self):
+ # Ensure the context manager correctly store his state.
+ with BackupFile(tempfile.mktemp()) as cm:
+ self.assertTrue(cm.saved)
+ self.assertFalse(cm.restored)
+ self.assertTrue(cm.restored)
+
+ def test_multiple_brackup(self):
+ # Ensure backup is called only one time.
+ with self.temp_file() as f:
+ with BackupFile(f.name) as cm:
+ f.write('new contents')
+ f.flush()
+ # Try to execute the backup a second time.
+ cm.backup()
+ # And ensure the original contents are not modified.
+ self.assertEqual(self.contents, open(cm.original).read())
+
+ def test_multiple_restore(self):
+ # Ensure backup is called only one time.
+ with self.temp_file() as f:
+ with BackupFile(f.name) as cm:
+ pass
+ # Re-create the backup file with different contents.
+ with open(cm.original, 'w') as original:
+ original.write('new contents')
+ # Ensure another call to restore does not change the target file.
+ cm.restore()
+ self.assertEqual(self.contents, open(f.name).read())
+
+ def test_backup_is_deleted(self):
+ # Ensure the backup file is deleted when the context manager exits.
+ with self.temp_file() as f:
+ with BackupFile(f.name) as cm:
+ pass
+ self.assertFalse(os.path.exists(cm.original))
@contextmanager
@@ -39,6 +167,60 @@
capture_error = partial(capture, 'stderr')
+class CaptureTest(unittest.TestCase):
+ """Tests for the *capture* context manager."""
+
+ message = 'message'
+
+ def test_capture_output(self):
+ with capture_output() as stream:
+ print self.message
+ self.assertEqual(self.message + '\n', stream.getvalue())
+
+ def test_capture_error(self):
+ with capture_error() as stream:
+ print >> sys.stderr, self.message
+ self.assertEqual(self.message + '\n', stream.getvalue())
+
+
+def create_test_branch(template_dir=None):
+ """Create a temporary test branch containing files in *template_dir*.
+
+ If *template_dir* is None, the files in `lpsetup/tests/test-branch`
+ are used.
+
+ Return the path of the newly created branch.
+ """
+ if template_dir is None:
+ template_dir = os.path.join(os.path.dirname(__file__), 'test-branch')
+ branch_path = tempfile.mktemp()
+ shutil.copytree(template_dir, branch_path)
+ call('bzr', 'init', '--quiet', branch_path)
+ call('bzr', 'add', '--quiet', branch_path)
+ call('bzr', 'commit', branch_path, '-m', 'Initial commit.')
+ return branch_path
+
+
+class CreateTestBranchTest(unittest.TestCase):
+ """Tests for the *create_test_branch* helper function."""
+
+ def setUp(self):
+ self.branch_path = create_test_branch()
+
+ def tearDown(self):
+ shutil.rmtree(self.branch_path)
+
+ def test_revno(self):
+ # Ensure the bzr branch is correctly created and contains a revision.
+ revno = run('bzr', 'revno', self.branch_path)
+ self.assertEqual(1, int(revno.strip()))
+
+ def test_working_tree(self):
+ # Ensure the working tree is present in the newly created branch.
+ test_file_path = os.path.join(self.branch_path, 'test-file')
+ self.assertTrue(os.path.exists(test_file_path))
+
+
def get_random_string(size=10):
"""Return a random string to be used in tests."""
return ''.join(random.sample(string.ascii_letters, size))
=== modified file 'setup.cfg'
--- setup.cfg 2012-07-11 18:37:31 +0000
+++ setup.cfg 2012-07-16 10:50:29 +0000
@@ -1,10 +1,11 @@
[nosetests]
+# Specifying 'where' should not be required but it is a work-around
+# for a problem presented by having 'ignore-files'.
+where=lpsetup
+exclude=handle_testing|create_test_branch
+include=utils
detailed-errors=1
-exclude=handle_testing
with-coverage=1
cover-package=lpsetup
with-doctest=1
-# Specifying 'where' should not be required but it is a work-around
-# for a problem presented by having 'ignore-files'.
-where=lpsetup
ignore-files=disabled*