Skip to content

Commit

Permalink
Release 0.8: Don't raise exception when notify-send fails
Browse files Browse the repository at this point in the history
Also: Move test helpers to humanfriendly.testing and more

The improvements to the test suite uncovered the notify-send
exception that I decided to fix `upstream' in the `proc' package.
  • Loading branch information
xolox committed Jun 24, 2017
1 parent 33ae63a commit 9140451
Show file tree
Hide file tree
Showing 3 changed files with 24 additions and 101 deletions.
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
coloredlogs >= 6.1
executor >= 17.1
humanfriendly >= 3.1
humanfriendly >= 3.3
linux-utils >= 0.4
proc >= 0.12
proc >= 0.14
property-manager >= 2.0
rotate-backups >= 4.4
4 changes: 2 additions & 2 deletions rsync_system_backup/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# rsync-system-backup: Linux system backups powered by rsync.
#
# Author: Peter Odding <peter@peterodding.com>
# Last Change: June 23, 2017
# Last Change: June 24, 2017
# URL: https://github.com/xolox/python-rsync-system-backup

"""
Expand Down Expand Up @@ -46,7 +46,7 @@
)

# Semi-standard module versioning.
__version__ = '0.7'
__version__ = '0.8'

# Initialize a logger for this module.
logger = logging.getLogger(__name__)
Expand Down
117 changes: 20 additions & 97 deletions rsync_system_backup/tests.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Test suite for the `rsync-system-backup' Python package.
#
# Author: Peter Odding <peter@peterodding.com>
# Last Change: June 23, 2017
# Last Change: June 24, 2017
# URL: https://github.com/xolox/python-rsync-system-backup

"""Test suite for the `rsync-system-backup` package."""
Expand All @@ -10,15 +10,12 @@
import contextlib
import logging
import os
import shutil
import sys
import tempfile
import unittest

# External dependencies.
import coloredlogs
from executor import ExternalCommandFailed, execute, get_search_path, which
from humanfriendly import Timer, compact
from executor import ExternalCommandFailed, execute
from humanfriendly import Timer
from humanfriendly.testing import MockedProgram, TemporaryDirectory, TestCase
from linux_utils.luks import (
create_encrypted_filesystem,
create_image_file,
Expand Down Expand Up @@ -51,57 +48,11 @@
KEY_FILE = '/tmp/rsync-system-backup.key'
MOUNT_POINT = '/mnt/rsync-system-backup'

# Global runtime state.
TEMPORARY_DIRECTORIES = []


def setUpModule():
"""Create a fake ``notify-send`` program that will keep silent."""
# Create a temporary directory where we can create a fake notify-send
# program that is guaranteed to exist and will run successfully, but
# without actually bothering the user with interactive notifications.
directory = tempfile.mkdtemp(prefix='rsync-system-backup-', suffix='-fake-path')
TEMPORARY_DIRECTORIES.append(directory)
fake_program = os.path.join(directory, 'notify-send')
candidates = which('true')
os.symlink(candidates[0], fake_program)
# Add the directory to the $PATH.
path = get_search_path()
path.insert(0, directory)
os.environ['PATH'] = os.pathsep.join(path)


def tearDownModule():
"""Clean temporary directories created by the test suite."""
while TEMPORARY_DIRECTORIES:
directory = TEMPORARY_DIRECTORIES.pop(0)
shutil.rmtree(directory)


class RsyncSystemBackupsTestCase(unittest.TestCase):
class RsyncSystemBackupsTestCase(TestCase):

""":mod:`unittest` compatible container for `rsync-system-backup` tests."""

def setUp(self):
"""Enable verbose logging and reset it after each test."""
coloredlogs.install(level='DEBUG')

def skipTest(self, text, *args, **kw):
"""
Enable backwards compatible "marking of tests to skip".
By calling this method from a return statement in the test to be
skipped the test can be marked as skipped when possible, without
breaking the test suite when unittest.TestCase.skipTest() isn't
available.
"""
reason = compact(text, *args, **kw)
try:
super(RsyncSystemBackupsTestCase, self).skipTest(reason)
except AttributeError:
# unittest.TestCase.skipTest() isn't available in Python 2.6.
logger.warning("%s", reason)

def test_usage(self):
"""Test the usage message."""
# Make sure the usage message is shown when no arguments
Expand Down Expand Up @@ -186,14 +137,18 @@ def test_destination_context(self):

def test_notifications(self):
"""Test the desktop notification functionality."""
timer = Timer()
program = RsyncSystemBackup(destination='/backups/system')
# Right now we just make sure the Python code doesn't contain any silly
# mistakes. It would be nice to have a more thorough test though, e.g.
# make sure that `notify-send' is called and make sure that we don't
# fail when `notify-send' does fail.
program.notify_starting()
program.notify_finished(Timer())
program.notify_failed(Timer())
# The happy path.
with MockedProgram('notify-send', returncode=0):
program.notify_starting()
program.notify_finished(timer)
program.notify_failed(timer)
# The sad path (should not raise exceptions).
with MockedProgram('notify-send', returncode=1):
program.notify_starting()
program.notify_finished(timer)
program.notify_failed(timer)

def test_simple_backup(self):
"""Test a backup of an alternative source directory to a local destination."""
Expand Down Expand Up @@ -352,6 +307,7 @@ def test_missing_crypto_device(self):
crypto_device=CRYPTO_NAME,
destination=os.path.join(MOUNT_POINT, 'latest'),
mount_point=MOUNT_POINT,
notifications_enabled=False,
)
self.assertRaises(MissingBackupDiskError, program.execute)

Expand All @@ -364,6 +320,7 @@ def test_mount_failure(self):
crypto_device=CRYPTO_NAME,
destination=os.path.join(MOUNT_POINT, 'latest'),
mount_point=MOUNT_POINT,
notifications_enabled=False,
)
# When `mount' fails it should exit with a nonzero exit code,
# thereby causing executor to raise an ExternalCommandFailed
Expand All @@ -382,13 +339,15 @@ def test_invalid_destination_directory(self):
crypto_device=CRYPTO_NAME,
destination='/some/random/directory',
mount_point=MOUNT_POINT,
notifications_enabled=False,
)
self.assertRaises(InvalidDestinationDirectory, program.transfer_changes)

def test_backup_failure(self):
"""Test that an exception is raised when ``rsync`` fails."""
program = RsyncSystemBackup(
destination='0.0.0.0::module/directory',
notifications_enabled=False,
sudo_enabled=False,
)
self.assertRaises(ExternalCommandFailed, program.execute)
Expand Down Expand Up @@ -478,39 +437,3 @@ def run_cli(*arguments):
sys.stderr = saved_stderr
sys.stdout = saved_stdout
return exit_code, fake_stdout.getvalue()


class TemporaryDirectory(object):

"""
Easy temporary directory creation & cleanup using the :keyword:`with` statement.
Here's an example of how to use this:
.. code-block:: python
with TemporaryDirectory() as directory:
# Do something useful here.
assert os.path.isdir(directory)
"""

def __init__(self, **options):
"""
Initialize context manager that manages creation & cleanup of temporary directory.
:param options: Any keyword arguments are passed on to
:func:`tempfile.mkdtemp()`.
"""
self.options = options

def __enter__(self):
"""Create the temporary directory."""
self.temporary_directory = tempfile.mkdtemp(**self.options)
logger.debug("Created temporary directory: %s", self.temporary_directory)
return self.temporary_directory

def __exit__(self, exc_type, exc_value, traceback):
"""Destroy the temporary directory."""
logger.debug("Cleaning up temporary directory: %s", self.temporary_directory)
shutil.rmtree(self.temporary_directory)
del self.temporary_directory

0 comments on commit 9140451

Please sign in to comment.