summaryrefslogtreecommitdiff
path: root/test/test_runner.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/test_runner.py')
-rw-r--r--test/test_runner.py662
1 files changed, 662 insertions, 0 deletions
diff --git a/test/test_runner.py b/test/test_runner.py
new file mode 100644
index 0000000..11551ab
--- /dev/null
+++ b/test/test_runner.py
@@ -0,0 +1,662 @@
+# -*- coding: utf-8 -*-
+#
+# test/test_runner.py
+# Part of python-daemon, an implementation of PEP 3143.
+#
+# Copyright © 2009–2010 Ben Finney <ben+python@benfinney.id.au>
+#
+# This is free software: you may copy, modify, and/or distribute this work
+# under the terms of the Python Software Foundation License, version 2 or
+# later as published by the Python Software Foundation.
+# No warranty expressed or implied. See the file LICENSE.PSF-2 for details.
+
+""" Unit test for runner module.
+ """
+
+import __builtin__
+import os
+import sys
+import tempfile
+import errno
+import signal
+
+import scaffold
+from test_pidlockfile import (
+ FakeFileDescriptorStringIO,
+ setup_pidfile_fixtures,
+ make_pidlockfile_scenarios,
+ setup_lockfile_method_mocks,
+ )
+from test_daemon import (
+ setup_streams_fixtures,
+ )
+import daemon.daemon
+
+from daemon import pidlockfile
+from daemon import runner
+
+
+class Exception_TestCase(scaffold.Exception_TestCase):
+ """ Test cases for module exception classes. """
+
+ def __init__(self, *args, **kwargs):
+ """ Set up a new instance. """
+ super(Exception_TestCase, self).__init__(*args, **kwargs)
+
+ self.valid_exceptions = {
+ runner.DaemonRunnerError: dict(
+ min_args = 1,
+ types = (Exception,),
+ ),
+ runner.DaemonRunnerInvalidActionError: dict(
+ min_args = 1,
+ types = (runner.DaemonRunnerError, ValueError),
+ ),
+ runner.DaemonRunnerStartFailureError: dict(
+ min_args = 1,
+ types = (runner.DaemonRunnerError, RuntimeError),
+ ),
+ runner.DaemonRunnerStopFailureError: dict(
+ min_args = 1,
+ types = (runner.DaemonRunnerError, RuntimeError),
+ ),
+ }
+
+
+def make_runner_scenarios():
+ """ Make a collection of scenarios for testing DaemonRunner instances. """
+
+ pidlockfile_scenarios = make_pidlockfile_scenarios()
+
+ scenarios = {
+ 'simple': {
+ 'pidlockfile_scenario_name': 'simple',
+ },
+ 'pidfile-locked': {
+ 'pidlockfile_scenario_name': 'exist-other-pid-locked',
+ },
+ }
+
+ for scenario in scenarios.values():
+ if 'pidlockfile_scenario_name' in scenario:
+ pidlockfile_scenario = pidlockfile_scenarios.pop(
+ scenario['pidlockfile_scenario_name'])
+ scenario['pid'] = pidlockfile_scenario['pid']
+ scenario['pidfile_path'] = pidlockfile_scenario['path']
+ scenario['pidfile_timeout'] = 23
+ scenario['pidlockfile_scenario'] = pidlockfile_scenario
+
+ return scenarios
+
+
+def set_runner_scenario(testcase, scenario_name, clear_tracker=True):
+ """ Set the DaemonRunner test scenario for the test case. """
+ scenarios = testcase.runner_scenarios
+ testcase.scenario = scenarios[scenario_name]
+ set_pidlockfile_scenario(
+ testcase, testcase.scenario['pidlockfile_scenario_name'])
+ if clear_tracker:
+ testcase.mock_tracker.clear()
+
+
+def set_pidlockfile_scenario(testcase, scenario_name):
+ """ Set the PIDLockFile test scenario for the test case. """
+ scenarios = testcase.pidlockfile_scenarios
+ testcase.pidlockfile_scenario = scenarios[scenario_name]
+ setup_lockfile_method_mocks(
+ testcase, testcase.pidlockfile_scenario,
+ testcase.lockfile_class_name)
+
+
+def setup_runner_fixtures(testcase):
+ """ Set up common test fixtures for DaemonRunner test case. """
+ testcase.mock_tracker = scaffold.MockTracker()
+
+ setup_pidfile_fixtures(testcase)
+ setup_streams_fixtures(testcase)
+
+ testcase.runner_scenarios = make_runner_scenarios()
+
+ testcase.mock_stderr = FakeFileDescriptorStringIO()
+ scaffold.mock(
+ "sys.stderr",
+ mock_obj=testcase.mock_stderr,
+ tracker=testcase.mock_tracker)
+
+ simple_scenario = testcase.runner_scenarios['simple']
+
+ testcase.lockfile_class_name = "pidlockfile.TimeoutPIDLockFile"
+
+ testcase.mock_runner_lock = scaffold.Mock(
+ testcase.lockfile_class_name,
+ tracker=testcase.mock_tracker)
+ testcase.mock_runner_lock.path = simple_scenario['pidfile_path']
+
+ scaffold.mock(
+ testcase.lockfile_class_name,
+ returns=testcase.mock_runner_lock,
+ tracker=testcase.mock_tracker)
+
+ class TestApp(object):
+
+ def __init__(self):
+ self.stdin_path = testcase.stream_file_paths['stdin']
+ self.stdout_path = testcase.stream_file_paths['stdout']
+ self.stderr_path = testcase.stream_file_paths['stderr']
+ self.pidfile_path = simple_scenario['pidfile_path']
+ self.pidfile_timeout = simple_scenario['pidfile_timeout']
+
+ run = scaffold.Mock(
+ "TestApp.run",
+ tracker=testcase.mock_tracker)
+
+ testcase.TestApp = TestApp
+
+ scaffold.mock(
+ "daemon.runner.DaemonContext",
+ returns=scaffold.Mock(
+ "DaemonContext",
+ tracker=testcase.mock_tracker),
+ tracker=testcase.mock_tracker)
+
+ testcase.test_app = testcase.TestApp()
+
+ testcase.test_program_name = "bazprog"
+ testcase.test_program_path = (
+ "/foo/bar/%(test_program_name)s" % vars(testcase))
+ testcase.valid_argv_params = {
+ 'start': [testcase.test_program_path, 'start'],
+ 'stop': [testcase.test_program_path, 'stop'],
+ 'restart': [testcase.test_program_path, 'restart'],
+ }
+
+ def mock_open(filename, mode=None, buffering=None):
+ if filename in testcase.stream_files_by_path:
+ result = testcase.stream_files_by_path[filename]
+ else:
+ result = FakeFileDescriptorStringIO()
+ result.mode = mode
+ result.buffering = buffering
+ return result
+
+ scaffold.mock(
+ "__builtin__.open",
+ returns_func=mock_open,
+ tracker=testcase.mock_tracker)
+
+ scaffold.mock(
+ "os.kill",
+ tracker=testcase.mock_tracker)
+
+ scaffold.mock(
+ "sys.argv",
+ mock_obj=testcase.valid_argv_params['start'],
+ tracker=testcase.mock_tracker)
+
+ testcase.test_instance = runner.DaemonRunner(testcase.test_app)
+
+ testcase.scenario = NotImplemented
+
+
+class DaemonRunner_TestCase(scaffold.TestCase):
+ """ Test cases for DaemonRunner class. """
+
+ def setUp(self):
+ """ Set up test fixtures. """
+ setup_runner_fixtures(self)
+ set_runner_scenario(self, 'simple')
+
+ scaffold.mock(
+ "runner.DaemonRunner.parse_args",
+ tracker=self.mock_tracker)
+
+ self.test_instance = runner.DaemonRunner(self.test_app)
+
+ def tearDown(self):
+ """ Tear down test fixtures. """
+ scaffold.mock_restore()
+
+ def test_instantiate(self):
+ """ New instance of DaemonRunner should be created. """
+ self.failUnlessIsInstance(self.test_instance, runner.DaemonRunner)
+
+ def test_parses_commandline_args(self):
+ """ Should parse commandline arguments. """
+ expect_mock_output = """\
+ Called runner.DaemonRunner.parse_args()
+ ...
+ """
+ self.failUnlessMockCheckerMatch(expect_mock_output)
+
+ def test_has_specified_app(self):
+ """ Should have specified application object. """
+ self.failUnlessIs(self.test_app, self.test_instance.app)
+
+ def test_sets_pidfile_none_when_pidfile_path_is_none(self):
+ """ Should set ‘pidfile’ to ‘None’ when ‘pidfile_path’ is ‘None’. """
+ pidfile_path = None
+ self.test_app.pidfile_path = pidfile_path
+ expect_pidfile = None
+ instance = runner.DaemonRunner(self.test_app)
+ self.failUnlessIs(expect_pidfile, instance.pidfile)
+
+ def test_error_when_pidfile_path_not_string(self):
+ """ Should raise ValueError when PID file path not a string. """
+ pidfile_path = object()
+ self.test_app.pidfile_path = pidfile_path
+ expect_error = ValueError
+ self.failUnlessRaises(
+ expect_error,
+ runner.DaemonRunner, self.test_app)
+
+ def test_error_when_pidfile_path_not_absolute(self):
+ """ Should raise ValueError when PID file path not absolute. """
+ pidfile_path = "foo/bar.pid"
+ self.test_app.pidfile_path = pidfile_path
+ expect_error = ValueError
+ self.failUnlessRaises(
+ expect_error,
+ runner.DaemonRunner, self.test_app)
+
+ def test_creates_lock_with_specified_parameters(self):
+ """ Should create a TimeoutPIDLockFile with specified params. """
+ pidfile_path = self.scenario['pidfile_path']
+ pidfile_timeout = self.scenario['pidfile_timeout']
+ lockfile_class_name = self.lockfile_class_name
+ expect_mock_output = """\
+ ...
+ Called %(lockfile_class_name)s(
+ %(pidfile_path)r,
+ %(pidfile_timeout)r)
+ """ % vars()
+ scaffold.mock_restore()
+ self.failUnlessMockCheckerMatch(expect_mock_output)
+
+ def test_has_created_pidfile(self):
+ """ Should have new PID lock file as `pidfile` attribute. """
+ expect_pidfile = self.mock_runner_lock
+ instance = self.test_instance
+ self.failUnlessIs(
+ expect_pidfile, instance.pidfile)
+
+ def test_daemon_context_has_created_pidfile(self):
+ """ DaemonContext component should have new PID lock file. """
+ expect_pidfile = self.mock_runner_lock
+ daemon_context = self.test_instance.daemon_context
+ self.failUnlessIs(
+ expect_pidfile, daemon_context.pidfile)
+
+ def test_daemon_context_has_specified_stdin_stream(self):
+ """ DaemonContext component should have specified stdin file. """
+ test_app = self.test_app
+ expect_file = self.stream_files_by_name['stdin']
+ daemon_context = self.test_instance.daemon_context
+ self.failUnlessEqual(expect_file, daemon_context.stdin)
+
+ def test_daemon_context_has_stdin_in_read_mode(self):
+ """ DaemonContext component should open stdin file for read. """
+ expect_mode = 'r'
+ daemon_context = self.test_instance.daemon_context
+ self.failUnlessIn(daemon_context.stdin.mode, expect_mode)
+
+ def test_daemon_context_has_specified_stdout_stream(self):
+ """ DaemonContext component should have specified stdout file. """
+ test_app = self.test_app
+ expect_file = self.stream_files_by_name['stdout']
+ daemon_context = self.test_instance.daemon_context
+ self.failUnlessEqual(expect_file, daemon_context.stdout)
+
+ def test_daemon_context_has_stdout_in_append_mode(self):
+ """ DaemonContext component should open stdout file for append. """
+ expect_mode = 'w+'
+ daemon_context = self.test_instance.daemon_context
+ self.failUnlessIn(daemon_context.stdout.mode, expect_mode)
+
+ def test_daemon_context_has_specified_stderr_stream(self):
+ """ DaemonContext component should have specified stderr file. """
+ test_app = self.test_app
+ expect_file = self.stream_files_by_name['stderr']
+ daemon_context = self.test_instance.daemon_context
+ self.failUnlessEqual(expect_file, daemon_context.stderr)
+
+ def test_daemon_context_has_stderr_in_append_mode(self):
+ """ DaemonContext component should open stderr file for append. """
+ expect_mode = 'w+'
+ daemon_context = self.test_instance.daemon_context
+ self.failUnlessIn(daemon_context.stderr.mode, expect_mode)
+
+ def test_daemon_context_has_stderr_with_no_buffering(self):
+ """ DaemonContext component should open stderr file unbuffered. """
+ expect_buffering = 0
+ daemon_context = self.test_instance.daemon_context
+ self.failUnlessEqual(
+ expect_buffering, daemon_context.stderr.buffering)
+
+
+class DaemonRunner_usage_exit_TestCase(scaffold.TestCase):
+ """ Test cases for DaemonRunner.usage_exit method. """
+
+ def setUp(self):
+ """ Set up test fixtures. """
+ setup_runner_fixtures(self)
+ set_runner_scenario(self, 'simple')
+
+ def tearDown(self):
+ """ Tear down test fixtures. """
+ scaffold.mock_restore()
+
+ def test_raises_system_exit(self):
+ """ Should raise SystemExit exception. """
+ instance = self.test_instance
+ argv = [self.test_program_path]
+ self.failUnlessRaises(
+ SystemExit,
+ instance._usage_exit, argv)
+
+ def test_message_follows_conventional_format(self):
+ """ Should emit a conventional usage message. """
+ instance = self.test_instance
+ progname = self.test_program_name
+ argv = [self.test_program_path]
+ expect_stderr_output = """\
+ usage: %(progname)s ...
+ """ % vars()
+ self.failUnlessRaises(
+ SystemExit,
+ instance._usage_exit, argv)
+ self.failUnlessOutputCheckerMatch(
+ expect_stderr_output, self.mock_stderr.getvalue())
+
+
+class DaemonRunner_parse_args_TestCase(scaffold.TestCase):
+ """ Test cases for DaemonRunner.parse_args method. """
+
+ def setUp(self):
+ """ Set up test fixtures. """
+ setup_runner_fixtures(self)
+ set_runner_scenario(self, 'simple')
+
+ scaffold.mock(
+ "daemon.runner.DaemonRunner._usage_exit",
+ raises=NotImplementedError,
+ tracker=self.mock_tracker)
+
+ def tearDown(self):
+ """ Tear down test fixtures. """
+ scaffold.mock_restore()
+
+ def test_emits_usage_message_if_insufficient_args(self):
+ """ Should emit a usage message and exit if too few arguments. """
+ instance = self.test_instance
+ argv = [self.test_program_path]
+ expect_mock_output = """\
+ Called daemon.runner.DaemonRunner._usage_exit(%(argv)r)
+ """ % vars()
+ try:
+ instance.parse_args(argv)
+ except NotImplementedError:
+ pass
+ self.failUnlessMockCheckerMatch(expect_mock_output)
+
+ def test_emits_usage_message_if_unknown_action_arg(self):
+ """ Should emit a usage message and exit if unknown action. """
+ instance = self.test_instance
+ progname = self.test_program_name
+ argv = [self.test_program_path, 'bogus']
+ expect_mock_output = """\
+ Called daemon.runner.DaemonRunner._usage_exit(%(argv)r)
+ """ % vars()
+ try:
+ instance.parse_args(argv)
+ except NotImplementedError:
+ pass
+ self.failUnlessMockCheckerMatch(expect_mock_output)
+
+ def test_should_parse_system_argv_by_default(self):
+ """ Should parse sys.argv by default. """
+ instance = self.test_instance
+ expect_action = 'start'
+ argv = self.valid_argv_params['start']
+ scaffold.mock(
+ "sys.argv",
+ mock_obj=argv,
+ tracker=self.mock_tracker)
+ instance.parse_args()
+ self.failUnlessEqual(expect_action, instance.action)
+
+ def test_sets_action_from_first_argument(self):
+ """ Should set action from first commandline argument. """
+ instance = self.test_instance
+ for name, argv in self.valid_argv_params.items():
+ expect_action = name
+ instance.parse_args(argv)
+ self.failUnlessEqual(expect_action, instance.action)
+
+
+class DaemonRunner_do_action_TestCase(scaffold.TestCase):
+ """ Test cases for DaemonRunner.do_action method. """
+
+ def setUp(self):
+ """ Set up test fixtures. """
+ setup_runner_fixtures(self)
+ set_runner_scenario(self, 'simple')
+
+ def tearDown(self):
+ """ Tear down test fixtures. """
+ scaffold.mock_restore()
+
+ def test_raises_error_if_unknown_action(self):
+ """ Should emit a usage message and exit if action is unknown. """
+ instance = self.test_instance
+ instance.action = 'bogus'
+ expect_error = runner.DaemonRunnerInvalidActionError
+ self.failUnlessRaises(
+ expect_error,
+ instance.do_action)
+
+
+class DaemonRunner_do_action_start_TestCase(scaffold.TestCase):
+ """ Test cases for DaemonRunner.do_action method, action 'start'. """
+
+ def setUp(self):
+ """ Set up test fixtures. """
+ setup_runner_fixtures(self)
+ set_runner_scenario(self, 'simple')
+
+ self.test_instance.action = 'start'
+
+ def tearDown(self):
+ """ Tear down test fixtures. """
+ scaffold.mock_restore()
+
+ def test_raises_error_if_pidfile_locked(self):
+ """ Should raise error if PID file is locked. """
+ set_pidlockfile_scenario(self, 'exist-other-pid-locked')
+ instance = self.test_instance
+ instance.daemon_context.open.mock_raises = (
+ pidlockfile.AlreadyLocked)
+ pidfile_path = self.scenario['pidfile_path']
+ expect_error = runner.DaemonRunnerStartFailureError
+ expect_message_content = pidfile_path
+ try:
+ instance.do_action()
+ except expect_error, exc:
+ pass
+ else:
+ raise self.failureException(
+ "Failed to raise " + expect_error.__name__)
+ self.failUnlessIn(str(exc), expect_message_content)
+
+ def test_breaks_lock_if_no_such_process(self):
+ """ Should request breaking lock if PID file process is not running. """
+ set_runner_scenario(self, 'pidfile-locked')
+ instance = self.test_instance
+ self.mock_runner_lock.read_pid.mock_returns = (
+ self.scenario['pidlockfile_scenario']['pidfile_pid'])
+ pidfile_path = self.scenario['pidfile_path']
+ test_pid = self.scenario['pidlockfile_scenario']['pidfile_pid']
+ expect_signal = signal.SIG_DFL
+ error = OSError(errno.ESRCH, "Not running")
+ os.kill.mock_raises = error
+ lockfile_class_name = self.lockfile_class_name
+ expect_mock_output = """\
+ ...
+ Called os.kill(%(test_pid)r, %(expect_signal)r)
+ Called %(lockfile_class_name)s.break_lock()
+ ...
+ """ % vars()
+ instance.do_action()
+ scaffold.mock_restore()
+ self.failUnlessMockCheckerMatch(expect_mock_output)
+
+ def test_requests_daemon_context_open(self):
+ """ Should request the daemon context to open. """
+ instance = self.test_instance
+ expect_mock_output = """\
+ ...
+ Called DaemonContext.open()
+ ...
+ """
+ instance.do_action()
+ self.failUnlessMockCheckerMatch(expect_mock_output)
+
+ def test_emits_start_message_to_stderr(self):
+ """ Should emit start message to stderr. """
+ instance = self.test_instance
+ current_pid = self.scenario['pid']
+ expect_stderr = """\
+ started with pid %(current_pid)d
+ """ % vars()
+ instance.do_action()
+ self.failUnlessOutputCheckerMatch(
+ expect_stderr, self.mock_stderr.getvalue())
+
+ def test_requests_app_run(self):
+ """ Should request the application to run. """
+ instance = self.test_instance
+ expect_mock_output = """\
+ ...
+ Called TestApp.run()
+ """
+ instance.do_action()
+ self.failUnlessMockCheckerMatch(expect_mock_output)
+
+
+class DaemonRunner_do_action_stop_TestCase(scaffold.TestCase):
+ """ Test cases for DaemonRunner.do_action method, action 'stop'. """
+
+ def setUp(self):
+ """ Set up test fixtures. """
+ setup_runner_fixtures(self)
+ set_runner_scenario(self, 'pidfile-locked')
+
+ self.test_instance.action = 'stop'
+
+ self.mock_runner_lock.is_locked.mock_returns = True
+ self.mock_runner_lock.i_am_locking.mock_returns = False
+ self.mock_runner_lock.read_pid.mock_returns = (
+ self.scenario['pidlockfile_scenario']['pidfile_pid'])
+
+ def tearDown(self):
+ """ Tear down test fixtures. """
+ scaffold.mock_restore()
+
+ def test_raises_error_if_pidfile_not_locked(self):
+ """ Should raise error if PID file is not locked. """
+ set_runner_scenario(self, 'simple')
+ instance = self.test_instance
+ self.mock_runner_lock.is_locked.mock_returns = False
+ self.mock_runner_lock.i_am_locking.mock_returns = False
+ self.mock_runner_lock.read_pid.mock_returns = (
+ self.scenario['pidlockfile_scenario']['pidfile_pid'])
+ pidfile_path = self.scenario['pidfile_path']
+ expect_error = runner.DaemonRunnerStopFailureError
+ expect_message_content = pidfile_path
+ try:
+ instance.do_action()
+ except expect_error, exc:
+ pass
+ else:
+ raise self.failureException(
+ "Failed to raise " + expect_error.__name__)
+ scaffold.mock_restore()
+ self.failUnlessIn(str(exc), expect_message_content)
+
+ def test_breaks_lock_if_pidfile_stale(self):
+ """ Should break lock if PID file is stale. """
+ instance = self.test_instance
+ pidfile_path = self.scenario['pidfile_path']
+ test_pid = self.scenario['pidlockfile_scenario']['pidfile_pid']
+ expect_signal = signal.SIG_DFL
+ error = OSError(errno.ESRCH, "Not running")
+ os.kill.mock_raises = error
+ lockfile_class_name = self.lockfile_class_name
+ expect_mock_output = """\
+ ...
+ Called %(lockfile_class_name)s.break_lock()
+ """ % vars()
+ instance.do_action()
+ scaffold.mock_restore()
+ self.failUnlessMockCheckerMatch(expect_mock_output)
+
+ def test_sends_terminate_signal_to_process_from_pidfile(self):
+ """ Should send SIGTERM to the daemon process. """
+ instance = self.test_instance
+ test_pid = self.scenario['pidlockfile_scenario']['pidfile_pid']
+ expect_signal = signal.SIGTERM
+ expect_mock_output = """\
+ ...
+ Called os.kill(%(test_pid)r, %(expect_signal)r)
+ """ % vars()
+ instance.do_action()
+ scaffold.mock_restore()
+ self.failUnlessMockCheckerMatch(expect_mock_output)
+
+ def test_raises_error_if_cannot_send_signal_to_process(self):
+ """ Should raise error if cannot send signal to daemon process. """
+ instance = self.test_instance
+ test_pid = self.scenario['pidlockfile_scenario']['pidfile_pid']
+ pidfile_path = self.scenario['pidfile_path']
+ error = OSError(errno.EPERM, "Nice try")
+ os.kill.mock_raises = error
+ expect_error = runner.DaemonRunnerStopFailureError
+ expect_message_content = str(test_pid)
+ try:
+ instance.do_action()
+ except expect_error, exc:
+ pass
+ else:
+ raise self.failureException(
+ "Failed to raise " + expect_error.__name__)
+ self.failUnlessIn(str(exc), expect_message_content)
+
+
+class DaemonRunner_do_action_restart_TestCase(scaffold.TestCase):
+ """ Test cases for DaemonRunner.do_action method, action 'restart'. """
+
+ def setUp(self):
+ """ Set up test fixtures. """
+ setup_runner_fixtures(self)
+ set_runner_scenario(self, 'pidfile-locked')
+
+ self.test_instance.action = 'restart'
+
+ def tearDown(self):
+ """ Tear down test fixtures. """
+ scaffold.mock_restore()
+
+ def test_requests_stop_then_start(self):
+ """ Should request stop, then start. """
+ instance = self.test_instance
+ scaffold.mock(
+ "daemon.runner.DaemonRunner._start",
+ tracker=self.mock_tracker)
+ scaffold.mock(
+ "daemon.runner.DaemonRunner._stop",
+ tracker=self.mock_tracker)
+ expect_mock_output = """\
+ Called daemon.runner.DaemonRunner._stop()
+ Called daemon.runner.DaemonRunner._start()
+ """
+ instance.do_action()
+ self.failUnlessMockCheckerMatch(expect_mock_output)