blob: 11551ab83f72ced6a6f5a89ed0728f55afd4293e [file] [log] [blame]
# -*- 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)