1fc46f261SDan Klein# -*- coding: utf-8 -*-
2fc46f261SDan Klein#
3fc46f261SDan Klein# test/test_runner.py
4fc46f261SDan Klein# Part of ‘python-daemon’, an implementation of PEP 3143.
5fc46f261SDan Klein#
6fc46f261SDan Klein# Copyright © 2009–2015 Ben Finney <ben+python@benfinney.id.au>
7fc46f261SDan Klein#
8fc46f261SDan Klein# This is free software: you may copy, modify, and/or distribute this work
9fc46f261SDan Klein# under the terms of the Apache License, version 2.0 as published by the
10fc46f261SDan Klein# Apache Software Foundation.
11fc46f261SDan Klein# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details.
12fc46f261SDan Klein
13fc46f261SDan Klein""" Unit test for ‘runner’ module.
14fc46f261SDan Klein    """
15fc46f261SDan Klein
16fc46f261SDan Kleinfrom __future__ import (absolute_import, unicode_literals)
17fc46f261SDan Klein
18fc46f261SDan Kleintry:
19fc46f261SDan Klein    # Python 3 standard library.
20fc46f261SDan Klein    import builtins
21fc46f261SDan Kleinexcept ImportError:
22fc46f261SDan Klein    # Python 2 standard library.
23fc46f261SDan Klein    import __builtin__ as builtins
24fc46f261SDan Kleinimport os
25fc46f261SDan Kleinimport os.path
26fc46f261SDan Kleinimport sys
27fc46f261SDan Kleinimport tempfile
28fc46f261SDan Kleinimport errno
29fc46f261SDan Kleinimport signal
30fc46f261SDan Kleinimport functools
31fc46f261SDan Klein
32fc46f261SDan Kleinimport lockfile
33fc46f261SDan Kleinimport mock
34fc46f261SDan Kleinimport testtools
35fc46f261SDan Klein
36fc46f261SDan Kleinfrom . import scaffold
37fc46f261SDan Kleinfrom .scaffold import (basestring, unicode)
38fc46f261SDan Kleinfrom .test_pidfile import (
39fc46f261SDan Klein        FakeFileDescriptorStringIO,
40fc46f261SDan Klein        setup_pidfile_fixtures,
41fc46f261SDan Klein        make_pidlockfile_scenarios,
42fc46f261SDan Klein        apply_lockfile_method_mocks,
43fc46f261SDan Klein        )
44fc46f261SDan Kleinfrom .test_daemon import (
45fc46f261SDan Klein        setup_streams_fixtures,
46fc46f261SDan Klein        )
47fc46f261SDan Klein
48fc46f261SDan Kleinimport daemon.daemon
49fc46f261SDan Kleinimport daemon.runner
50fc46f261SDan Kleinimport daemon.pidfile
51fc46f261SDan Klein
52fc46f261SDan Klein
53fc46f261SDan Kleinclass ModuleExceptions_TestCase(scaffold.Exception_TestCase):
54fc46f261SDan Klein    """ Test cases for module exception classes. """
55fc46f261SDan Klein
56fc46f261SDan Klein    scenarios = scaffold.make_exception_scenarios([
57fc46f261SDan Klein            ('daemon.runner.DaemonRunnerError', dict(
58fc46f261SDan Klein                exc_type = daemon.runner.DaemonRunnerError,
59fc46f261SDan Klein                min_args = 1,
60fc46f261SDan Klein                types = [Exception],
61fc46f261SDan Klein                )),
62fc46f261SDan Klein            ('daemon.runner.DaemonRunnerInvalidActionError', dict(
63fc46f261SDan Klein                exc_type = daemon.runner.DaemonRunnerInvalidActionError,
64fc46f261SDan Klein                min_args = 1,
65fc46f261SDan Klein                types = [daemon.runner.DaemonRunnerError, ValueError],
66fc46f261SDan Klein                )),
67fc46f261SDan Klein            ('daemon.runner.DaemonRunnerStartFailureError', dict(
68fc46f261SDan Klein                exc_type = daemon.runner.DaemonRunnerStartFailureError,
69fc46f261SDan Klein                min_args = 1,
70fc46f261SDan Klein                types = [daemon.runner.DaemonRunnerError, RuntimeError],
71fc46f261SDan Klein                )),
72fc46f261SDan Klein            ('daemon.runner.DaemonRunnerStopFailureError', dict(
73fc46f261SDan Klein                exc_type = daemon.runner.DaemonRunnerStopFailureError,
74fc46f261SDan Klein                min_args = 1,
75fc46f261SDan Klein                types = [daemon.runner.DaemonRunnerError, RuntimeError],
76fc46f261SDan Klein                )),
77fc46f261SDan Klein            ])
78fc46f261SDan Klein
79fc46f261SDan Klein
80fc46f261SDan Kleindef make_runner_scenarios():
81fc46f261SDan Klein    """ Make a collection of scenarios for testing `DaemonRunner` instances.
82fc46f261SDan Klein
83fc46f261SDan Klein        :return: A collection of scenarios for tests involving
84fc46f261SDan Klein            `DaemonRunner` instances.
85fc46f261SDan Klein
86fc46f261SDan Klein        The collection is a mapping from scenario name to a dictionary of
87fc46f261SDan Klein        scenario attributes.
88fc46f261SDan Klein
89fc46f261SDan Klein        """
90fc46f261SDan Klein
91fc46f261SDan Klein    pidlockfile_scenarios = make_pidlockfile_scenarios()
92fc46f261SDan Klein
93fc46f261SDan Klein    scenarios = {
94fc46f261SDan Klein            'simple': {
95fc46f261SDan Klein                'pidlockfile_scenario_name': 'simple',
96fc46f261SDan Klein                },
97fc46f261SDan Klein            'pidfile-locked': {
98fc46f261SDan Klein                'pidlockfile_scenario_name': 'exist-other-pid-locked',
99fc46f261SDan Klein                },
100fc46f261SDan Klein            }
101fc46f261SDan Klein
102fc46f261SDan Klein    for scenario in scenarios.values():
103fc46f261SDan Klein        if 'pidlockfile_scenario_name' in scenario:
104fc46f261SDan Klein            pidlockfile_scenario = pidlockfile_scenarios.pop(
105fc46f261SDan Klein                    scenario['pidlockfile_scenario_name'])
106fc46f261SDan Klein        scenario['pid'] = pidlockfile_scenario['pid']
107fc46f261SDan Klein        scenario['pidfile_path'] = pidlockfile_scenario['pidfile_path']
108fc46f261SDan Klein        scenario['pidfile_timeout'] = 23
109fc46f261SDan Klein        scenario['pidlockfile_scenario'] = pidlockfile_scenario
110fc46f261SDan Klein
111fc46f261SDan Klein    return scenarios
112fc46f261SDan Klein
113fc46f261SDan Klein
114fc46f261SDan Kleindef set_runner_scenario(testcase, scenario_name):
115fc46f261SDan Klein    """ Set the DaemonRunner test scenario for the test case.
116fc46f261SDan Klein
117fc46f261SDan Klein        :param testcase: The `TestCase` instance to decorate.
118fc46f261SDan Klein        :param scenario_name: The name of the scenario to use.
119fc46f261SDan Klein
120fc46f261SDan Klein        Set the `DaemonRunner` test scenario name and decorate the
121fc46f261SDan Klein        `testcase` with the corresponding scenario fixtures.
122fc46f261SDan Klein
123fc46f261SDan Klein        """
124fc46f261SDan Klein    scenarios = testcase.runner_scenarios
125fc46f261SDan Klein    testcase.scenario = scenarios[scenario_name]
126fc46f261SDan Klein    apply_lockfile_method_mocks(
127fc46f261SDan Klein            testcase.mock_runner_lockfile,
128fc46f261SDan Klein            testcase,
129fc46f261SDan Klein            testcase.scenario['pidlockfile_scenario'])
130fc46f261SDan Klein
131fc46f261SDan Klein
132fc46f261SDan Kleindef setup_runner_fixtures(testcase):
133fc46f261SDan Klein    """ Set up common fixtures for `DaemonRunner` test cases.
134fc46f261SDan Klein
135fc46f261SDan Klein        :param testcase: A `TestCase` instance to decorate.
136fc46f261SDan Klein
137fc46f261SDan Klein        Decorate the `testcase` with attributes to be fixtures for tests
138fc46f261SDan Klein        involving `DaemonRunner` instances.
139fc46f261SDan Klein
140fc46f261SDan Klein        """
141fc46f261SDan Klein    setup_pidfile_fixtures(testcase)
142fc46f261SDan Klein    setup_streams_fixtures(testcase)
143fc46f261SDan Klein
144fc46f261SDan Klein    testcase.runner_scenarios = make_runner_scenarios()
145fc46f261SDan Klein
146fc46f261SDan Klein    patcher_stderr = mock.patch.object(
147fc46f261SDan Klein            sys, "stderr",
148fc46f261SDan Klein            new=FakeFileDescriptorStringIO())
149fc46f261SDan Klein    testcase.fake_stderr = patcher_stderr.start()
150fc46f261SDan Klein    testcase.addCleanup(patcher_stderr.stop)
151fc46f261SDan Klein
152fc46f261SDan Klein    simple_scenario = testcase.runner_scenarios['simple']
153fc46f261SDan Klein
154fc46f261SDan Klein    testcase.mock_runner_lockfile = mock.MagicMock(
155fc46f261SDan Klein            spec=daemon.pidfile.TimeoutPIDLockFile)
156fc46f261SDan Klein    apply_lockfile_method_mocks(
157fc46f261SDan Klein            testcase.mock_runner_lockfile,
158fc46f261SDan Klein            testcase,
159fc46f261SDan Klein            simple_scenario['pidlockfile_scenario'])
160fc46f261SDan Klein    testcase.mock_runner_lockfile.path = simple_scenario['pidfile_path']
161fc46f261SDan Klein
162fc46f261SDan Klein    patcher_lockfile_class = mock.patch.object(
163fc46f261SDan Klein            daemon.pidfile, "TimeoutPIDLockFile",
164fc46f261SDan Klein            return_value=testcase.mock_runner_lockfile)
165fc46f261SDan Klein    patcher_lockfile_class.start()
166fc46f261SDan Klein    testcase.addCleanup(patcher_lockfile_class.stop)
167fc46f261SDan Klein
168fc46f261SDan Klein    class TestApp(object):
169fc46f261SDan Klein
170fc46f261SDan Klein        def __init__(self):
171fc46f261SDan Klein            self.stdin_path = testcase.stream_file_paths['stdin']
172fc46f261SDan Klein            self.stdout_path = testcase.stream_file_paths['stdout']
173fc46f261SDan Klein            self.stderr_path = testcase.stream_file_paths['stderr']
174fc46f261SDan Klein            self.pidfile_path = simple_scenario['pidfile_path']
175fc46f261SDan Klein            self.pidfile_timeout = simple_scenario['pidfile_timeout']
176fc46f261SDan Klein
177fc46f261SDan Klein        run = mock.MagicMock(name="TestApp.run")
178fc46f261SDan Klein
179fc46f261SDan Klein    testcase.TestApp = TestApp
180fc46f261SDan Klein
181fc46f261SDan Klein    patcher_runner_daemoncontext = mock.patch.object(
182fc46f261SDan Klein            daemon.runner, "DaemonContext", autospec=True)
183fc46f261SDan Klein    patcher_runner_daemoncontext.start()
184fc46f261SDan Klein    testcase.addCleanup(patcher_runner_daemoncontext.stop)
185fc46f261SDan Klein
186fc46f261SDan Klein    testcase.test_app = testcase.TestApp()
187fc46f261SDan Klein
188fc46f261SDan Klein    testcase.test_program_name = "bazprog"
189fc46f261SDan Klein    testcase.test_program_path = os.path.join(
190fc46f261SDan Klein            "/foo/bar", testcase.test_program_name)
191fc46f261SDan Klein    testcase.valid_argv_params = {
192fc46f261SDan Klein            'start': [testcase.test_program_path, 'start'],
193fc46f261SDan Klein            'stop': [testcase.test_program_path, 'stop'],
194fc46f261SDan Klein            'restart': [testcase.test_program_path, 'restart'],
195fc46f261SDan Klein            }
196fc46f261SDan Klein
197fc46f261SDan Klein    def fake_open(filename, mode=None, buffering=None):
198fc46f261SDan Klein        if filename in testcase.stream_files_by_path:
199fc46f261SDan Klein            result = testcase.stream_files_by_path[filename]
200fc46f261SDan Klein        else:
201fc46f261SDan Klein            result = FakeFileDescriptorStringIO()
202fc46f261SDan Klein        result.mode = mode
203fc46f261SDan Klein        result.buffering = buffering
204fc46f261SDan Klein        return result
205fc46f261SDan Klein
206fc46f261SDan Klein    mock_open = mock.mock_open()
207fc46f261SDan Klein    mock_open.side_effect = fake_open
208fc46f261SDan Klein
209fc46f261SDan Klein    func_patcher_builtin_open = mock.patch.object(
210fc46f261SDan Klein            builtins, "open",
211fc46f261SDan Klein            new=mock_open)
212fc46f261SDan Klein    func_patcher_builtin_open.start()
213fc46f261SDan Klein    testcase.addCleanup(func_patcher_builtin_open.stop)
214fc46f261SDan Klein
215fc46f261SDan Klein    func_patcher_os_kill = mock.patch.object(os, "kill")
216fc46f261SDan Klein    func_patcher_os_kill.start()
217fc46f261SDan Klein    testcase.addCleanup(func_patcher_os_kill.stop)
218fc46f261SDan Klein
219fc46f261SDan Klein    patcher_sys_argv = mock.patch.object(
220fc46f261SDan Klein            sys, "argv",
221fc46f261SDan Klein            new=testcase.valid_argv_params['start'])
222fc46f261SDan Klein    patcher_sys_argv.start()
223fc46f261SDan Klein    testcase.addCleanup(patcher_sys_argv.stop)
224fc46f261SDan Klein
225fc46f261SDan Klein    testcase.test_instance = daemon.runner.DaemonRunner(testcase.test_app)
226fc46f261SDan Klein
227fc46f261SDan Klein    testcase.scenario = NotImplemented
228fc46f261SDan Klein
229fc46f261SDan Klein
230fc46f261SDan Kleinclass DaemonRunner_BaseTestCase(scaffold.TestCase):
231fc46f261SDan Klein    """ Base class for DaemonRunner test case classes. """
232fc46f261SDan Klein
233fc46f261SDan Klein    def setUp(self):
234fc46f261SDan Klein        """ Set up test fixtures. """
235fc46f261SDan Klein        super(DaemonRunner_BaseTestCase, self).setUp()
236fc46f261SDan Klein
237fc46f261SDan Klein        setup_runner_fixtures(self)
238fc46f261SDan Klein        set_runner_scenario(self, 'simple')
239fc46f261SDan Klein
240fc46f261SDan Klein
241fc46f261SDan Kleinclass DaemonRunner_TestCase(DaemonRunner_BaseTestCase):
242fc46f261SDan Klein    """ Test cases for DaemonRunner class. """
243fc46f261SDan Klein
244fc46f261SDan Klein    def setUp(self):
245fc46f261SDan Klein        """ Set up test fixtures. """
246fc46f261SDan Klein        super(DaemonRunner_TestCase, self).setUp()
247fc46f261SDan Klein
248fc46f261SDan Klein        func_patcher_parse_args = mock.patch.object(
249fc46f261SDan Klein                daemon.runner.DaemonRunner, "parse_args")
250fc46f261SDan Klein        func_patcher_parse_args.start()
251fc46f261SDan Klein        self.addCleanup(func_patcher_parse_args.stop)
252fc46f261SDan Klein
253fc46f261SDan Klein        # Create a new instance now with our custom patches.
254fc46f261SDan Klein        self.test_instance = daemon.runner.DaemonRunner(self.test_app)
255fc46f261SDan Klein
256fc46f261SDan Klein    def test_instantiate(self):
257fc46f261SDan Klein        """ New instance of DaemonRunner should be created. """
258fc46f261SDan Klein        self.assertIsInstance(self.test_instance, daemon.runner.DaemonRunner)
259fc46f261SDan Klein
260fc46f261SDan Klein    def test_parses_commandline_args(self):
261fc46f261SDan Klein        """ Should parse commandline arguments. """
262fc46f261SDan Klein        self.test_instance.parse_args.assert_called_with()
263fc46f261SDan Klein
264fc46f261SDan Klein    def test_has_specified_app(self):
265fc46f261SDan Klein        """ Should have specified application object. """
266fc46f261SDan Klein        self.assertIs(self.test_app, self.test_instance.app)
267fc46f261SDan Klein
268fc46f261SDan Klein    def test_sets_pidfile_none_when_pidfile_path_is_none(self):
269fc46f261SDan Klein        """ Should set ‘pidfile’ to ‘None’ when ‘pidfile_path’ is ‘None’. """
270fc46f261SDan Klein        pidfile_path = None
271fc46f261SDan Klein        self.test_app.pidfile_path = pidfile_path
272fc46f261SDan Klein        expected_pidfile = None
273fc46f261SDan Klein        instance = daemon.runner.DaemonRunner(self.test_app)
274fc46f261SDan Klein        self.assertIs(expected_pidfile, instance.pidfile)
275fc46f261SDan Klein
276fc46f261SDan Klein    def test_error_when_pidfile_path_not_string(self):
277fc46f261SDan Klein        """ Should raise ValueError when PID file path not a string. """
278fc46f261SDan Klein        pidfile_path = object()
279fc46f261SDan Klein        self.test_app.pidfile_path = pidfile_path
280fc46f261SDan Klein        expected_error = ValueError
281fc46f261SDan Klein        self.assertRaises(
282fc46f261SDan Klein                expected_error,
283fc46f261SDan Klein                daemon.runner.DaemonRunner, self.test_app)
284fc46f261SDan Klein
285fc46f261SDan Klein    def test_error_when_pidfile_path_not_absolute(self):
286fc46f261SDan Klein        """ Should raise ValueError when PID file path not absolute. """
287fc46f261SDan Klein        pidfile_path = "foo/bar.pid"
288fc46f261SDan Klein        self.test_app.pidfile_path = pidfile_path
289fc46f261SDan Klein        expected_error = ValueError
290fc46f261SDan Klein        self.assertRaises(
291fc46f261SDan Klein                expected_error,
292fc46f261SDan Klein                daemon.runner.DaemonRunner, self.test_app)
293fc46f261SDan Klein
294fc46f261SDan Klein    def test_creates_lock_with_specified_parameters(self):
295fc46f261SDan Klein        """ Should create a TimeoutPIDLockFile with specified params. """
296fc46f261SDan Klein        pidfile_path = self.scenario['pidfile_path']
297fc46f261SDan Klein        pidfile_timeout = self.scenario['pidfile_timeout']
298fc46f261SDan Klein        daemon.pidfile.TimeoutPIDLockFile.assert_called_with(
299fc46f261SDan Klein                pidfile_path, pidfile_timeout)
300fc46f261SDan Klein
301fc46f261SDan Klein    def test_has_created_pidfile(self):
302fc46f261SDan Klein        """ Should have new PID lock file as `pidfile` attribute. """
303fc46f261SDan Klein        expected_pidfile = self.mock_runner_lockfile
304fc46f261SDan Klein        instance = self.test_instance
305fc46f261SDan Klein        self.assertIs(
306fc46f261SDan Klein                expected_pidfile, instance.pidfile)
307fc46f261SDan Klein
308fc46f261SDan Klein    def test_daemon_context_has_created_pidfile(self):
309fc46f261SDan Klein        """ DaemonContext component should have new PID lock file. """
310fc46f261SDan Klein        expected_pidfile = self.mock_runner_lockfile
311fc46f261SDan Klein        daemon_context = self.test_instance.daemon_context
312fc46f261SDan Klein        self.assertIs(
313fc46f261SDan Klein                expected_pidfile, daemon_context.pidfile)
314fc46f261SDan Klein
315fc46f261SDan Klein    def test_daemon_context_has_specified_stdin_stream(self):
316fc46f261SDan Klein        """ DaemonContext component should have specified stdin file. """
317fc46f261SDan Klein        test_app = self.test_app
318fc46f261SDan Klein        expected_file = self.stream_files_by_name['stdin']
319fc46f261SDan Klein        daemon_context = self.test_instance.daemon_context
320fc46f261SDan Klein        self.assertEqual(expected_file, daemon_context.stdin)
321fc46f261SDan Klein
322fc46f261SDan Klein    def test_daemon_context_has_stdin_in_read_mode(self):
323fc46f261SDan Klein        """ DaemonContext component should open stdin file for read. """
324fc46f261SDan Klein        expected_mode = 'rt'
325fc46f261SDan Klein        daemon_context = self.test_instance.daemon_context
326fc46f261SDan Klein        self.assertIn(expected_mode, daemon_context.stdin.mode)
327fc46f261SDan Klein
328fc46f261SDan Klein    def test_daemon_context_has_specified_stdout_stream(self):
329fc46f261SDan Klein        """ DaemonContext component should have specified stdout file. """
330fc46f261SDan Klein        test_app = self.test_app
331fc46f261SDan Klein        expected_file = self.stream_files_by_name['stdout']
332fc46f261SDan Klein        daemon_context = self.test_instance.daemon_context
333fc46f261SDan Klein        self.assertEqual(expected_file, daemon_context.stdout)
334fc46f261SDan Klein
335fc46f261SDan Klein    def test_daemon_context_has_stdout_in_append_mode(self):
336fc46f261SDan Klein        """ DaemonContext component should open stdout file for append. """
337fc46f261SDan Klein        expected_mode = 'w+t'
338fc46f261SDan Klein        daemon_context = self.test_instance.daemon_context
339fc46f261SDan Klein        self.assertIn(expected_mode, daemon_context.stdout.mode)
340fc46f261SDan Klein
341fc46f261SDan Klein    def test_daemon_context_has_specified_stderr_stream(self):
342fc46f261SDan Klein        """ DaemonContext component should have specified stderr file. """
343fc46f261SDan Klein        test_app = self.test_app
344fc46f261SDan Klein        expected_file = self.stream_files_by_name['stderr']
345fc46f261SDan Klein        daemon_context = self.test_instance.daemon_context
346fc46f261SDan Klein        self.assertEqual(expected_file, daemon_context.stderr)
347fc46f261SDan Klein
348fc46f261SDan Klein    def test_daemon_context_has_stderr_in_append_mode(self):
349fc46f261SDan Klein        """ DaemonContext component should open stderr file for append. """
350fc46f261SDan Klein        expected_mode = 'w+t'
351fc46f261SDan Klein        daemon_context = self.test_instance.daemon_context
352fc46f261SDan Klein        self.assertIn(expected_mode, daemon_context.stderr.mode)
353fc46f261SDan Klein
354fc46f261SDan Klein    def test_daemon_context_has_stderr_with_no_buffering(self):
355fc46f261SDan Klein        """ DaemonContext component should open stderr file unbuffered. """
356fc46f261SDan Klein        expected_buffering = 0
357fc46f261SDan Klein        daemon_context = self.test_instance.daemon_context
358fc46f261SDan Klein        self.assertEqual(
359fc46f261SDan Klein                expected_buffering, daemon_context.stderr.buffering)
360fc46f261SDan Klein
361fc46f261SDan Klein
362fc46f261SDan Kleinclass DaemonRunner_usage_exit_TestCase(DaemonRunner_BaseTestCase):
363fc46f261SDan Klein    """ Test cases for DaemonRunner.usage_exit method. """
364fc46f261SDan Klein
365fc46f261SDan Klein    def test_raises_system_exit(self):
366fc46f261SDan Klein        """ Should raise SystemExit exception. """
367fc46f261SDan Klein        instance = self.test_instance
368fc46f261SDan Klein        argv = [self.test_program_path]
369fc46f261SDan Klein        self.assertRaises(
370fc46f261SDan Klein                SystemExit,
371fc46f261SDan Klein                instance._usage_exit, argv)
372fc46f261SDan Klein
373fc46f261SDan Klein    def test_message_follows_conventional_format(self):
374fc46f261SDan Klein        """ Should emit a conventional usage message. """
375fc46f261SDan Klein        instance = self.test_instance
376fc46f261SDan Klein        argv = [self.test_program_path]
377fc46f261SDan Klein        expected_stderr_output = """\
378fc46f261SDan Klein                usage: {progname} ...
379fc46f261SDan Klein                """.format(
380fc46f261SDan Klein                    progname=self.test_program_name)
381fc46f261SDan Klein        self.assertRaises(
382fc46f261SDan Klein                SystemExit,
383fc46f261SDan Klein                instance._usage_exit, argv)
384fc46f261SDan Klein        self.assertOutputCheckerMatch(
385fc46f261SDan Klein                expected_stderr_output, self.fake_stderr.getvalue())
386fc46f261SDan Klein
387fc46f261SDan Klein
388fc46f261SDan Kleinclass DaemonRunner_parse_args_TestCase(DaemonRunner_BaseTestCase):
389fc46f261SDan Klein    """ Test cases for DaemonRunner.parse_args method. """
390fc46f261SDan Klein
391fc46f261SDan Klein    def setUp(self):
392fc46f261SDan Klein        """ Set up test fixtures. """
393fc46f261SDan Klein        super(DaemonRunner_parse_args_TestCase, self).setUp()
394fc46f261SDan Klein
395fc46f261SDan Klein        func_patcher_usage_exit = mock.patch.object(
396fc46f261SDan Klein                daemon.runner.DaemonRunner, "_usage_exit",
397fc46f261SDan Klein                side_effect=NotImplementedError)
398fc46f261SDan Klein        func_patcher_usage_exit.start()
399fc46f261SDan Klein        self.addCleanup(func_patcher_usage_exit.stop)
400fc46f261SDan Klein
401fc46f261SDan Klein    def test_emits_usage_message_if_insufficient_args(self):
402fc46f261SDan Klein        """ Should emit a usage message and exit if too few arguments. """
403fc46f261SDan Klein        instance = self.test_instance
404fc46f261SDan Klein        argv = [self.test_program_path]
405fc46f261SDan Klein        exc = self.assertRaises(
406fc46f261SDan Klein                NotImplementedError,
407fc46f261SDan Klein                instance.parse_args, argv)
408fc46f261SDan Klein        daemon.runner.DaemonRunner._usage_exit.assert_called_with(argv)
409fc46f261SDan Klein
410fc46f261SDan Klein    def test_emits_usage_message_if_unknown_action_arg(self):
411fc46f261SDan Klein        """ Should emit a usage message and exit if unknown action. """
412fc46f261SDan Klein        instance = self.test_instance
413fc46f261SDan Klein        progname = self.test_program_name
414fc46f261SDan Klein        argv = [self.test_program_path, 'bogus']
415fc46f261SDan Klein        exc = self.assertRaises(
416fc46f261SDan Klein                NotImplementedError,
417fc46f261SDan Klein                instance.parse_args, argv)
418fc46f261SDan Klein        daemon.runner.DaemonRunner._usage_exit.assert_called_with(argv)
419fc46f261SDan Klein
420fc46f261SDan Klein    def test_should_parse_system_argv_by_default(self):
421fc46f261SDan Klein        """ Should parse sys.argv by default. """
422fc46f261SDan Klein        instance = self.test_instance
423fc46f261SDan Klein        expected_action = 'start'
424fc46f261SDan Klein        argv = self.valid_argv_params['start']
425fc46f261SDan Klein        with mock.patch.object(sys, "argv", new=argv):
426fc46f261SDan Klein            instance.parse_args()
427fc46f261SDan Klein        self.assertEqual(expected_action, instance.action)
428fc46f261SDan Klein
429fc46f261SDan Klein    def test_sets_action_from_first_argument(self):
430fc46f261SDan Klein        """ Should set action from first commandline argument. """
431fc46f261SDan Klein        instance = self.test_instance
432fc46f261SDan Klein        for name, argv in self.valid_argv_params.items():
433fc46f261SDan Klein            expected_action = name
434fc46f261SDan Klein            instance.parse_args(argv)
435fc46f261SDan Klein            self.assertEqual(expected_action, instance.action)
436fc46f261SDan Klein
437fc46f261SDan Klein
438fc46f261SDan Kleintry:
439fc46f261SDan Klein    ProcessLookupError
440fc46f261SDan Kleinexcept NameError:
441fc46f261SDan Klein    # Python 2 uses OSError.
442fc46f261SDan Klein    ProcessLookupError = functools.partial(OSError, errno.ESRCH)
443fc46f261SDan Klein
444fc46f261SDan Kleinclass DaemonRunner_do_action_TestCase(DaemonRunner_BaseTestCase):
445fc46f261SDan Klein    """ Test cases for DaemonRunner.do_action method. """
446fc46f261SDan Klein
447fc46f261SDan Klein    def test_raises_error_if_unknown_action(self):
448fc46f261SDan Klein        """ Should emit a usage message and exit if action is unknown. """
449fc46f261SDan Klein        instance = self.test_instance
450fc46f261SDan Klein        instance.action = 'bogus'
451fc46f261SDan Klein        expected_error = daemon.runner.DaemonRunnerInvalidActionError
452fc46f261SDan Klein        self.assertRaises(
453fc46f261SDan Klein                expected_error,
454fc46f261SDan Klein                instance.do_action)
455fc46f261SDan Klein
456fc46f261SDan Klein
457fc46f261SDan Kleinclass DaemonRunner_do_action_start_TestCase(DaemonRunner_BaseTestCase):
458fc46f261SDan Klein    """ Test cases for DaemonRunner.do_action method, action 'start'. """
459fc46f261SDan Klein
460fc46f261SDan Klein    def setUp(self):
461fc46f261SDan Klein        """ Set up test fixtures. """
462fc46f261SDan Klein        super(DaemonRunner_do_action_start_TestCase, self).setUp()
463fc46f261SDan Klein
464fc46f261SDan Klein        self.test_instance.action = 'start'
465fc46f261SDan Klein
466fc46f261SDan Klein    def test_raises_error_if_pidfile_locked(self):
467fc46f261SDan Klein        """ Should raise error if PID file is locked. """
468fc46f261SDan Klein
469fc46f261SDan Klein        instance = self.test_instance
470fc46f261SDan Klein        instance.daemon_context.open.side_effect = lockfile.AlreadyLocked
471fc46f261SDan Klein        pidfile_path = self.scenario['pidfile_path']
472fc46f261SDan Klein        expected_error = daemon.runner.DaemonRunnerStartFailureError
473fc46f261SDan Klein        expected_message_content = pidfile_path
474fc46f261SDan Klein        exc = self.assertRaises(
475fc46f261SDan Klein                expected_error,
476fc46f261SDan Klein                instance.do_action)
477fc46f261SDan Klein        self.assertIn(expected_message_content, unicode(exc))
478fc46f261SDan Klein
479fc46f261SDan Klein    def test_breaks_lock_if_no_such_process(self):
480fc46f261SDan Klein        """ Should request breaking lock if PID file process is not running. """
481fc46f261SDan Klein        set_runner_scenario(self, 'pidfile-locked')
482fc46f261SDan Klein        instance = self.test_instance
483fc46f261SDan Klein        self.mock_runner_lockfile.read_pid.return_value = (
484fc46f261SDan Klein                self.scenario['pidlockfile_scenario']['pidfile_pid'])
485fc46f261SDan Klein        pidfile_path = self.scenario['pidfile_path']
486fc46f261SDan Klein        test_pid = self.scenario['pidlockfile_scenario']['pidfile_pid']
487fc46f261SDan Klein        expected_signal = signal.SIG_DFL
488fc46f261SDan Klein        test_error = ProcessLookupError("Not running")
489fc46f261SDan Klein        os.kill.side_effect = test_error
490fc46f261SDan Klein        instance.do_action()
491fc46f261SDan Klein        os.kill.assert_called_with(test_pid, expected_signal)
492fc46f261SDan Klein        self.mock_runner_lockfile.break_lock.assert_called_with()
493fc46f261SDan Klein
494fc46f261SDan Klein    def test_requests_daemon_context_open(self):
495fc46f261SDan Klein        """ Should request the daemon context to open. """
496fc46f261SDan Klein        instance = self.test_instance
497fc46f261SDan Klein        instance.do_action()
498fc46f261SDan Klein        instance.daemon_context.open.assert_called_with()
499fc46f261SDan Klein
500fc46f261SDan Klein    def test_emits_start_message_to_stderr(self):
501fc46f261SDan Klein        """ Should emit start message to stderr. """
502fc46f261SDan Klein        instance = self.test_instance
503fc46f261SDan Klein        expected_stderr = """\
504fc46f261SDan Klein                started with pid {pid:d}
505fc46f261SDan Klein                """.format(
506fc46f261SDan Klein                    pid=self.scenario['pid'])
507fc46f261SDan Klein        instance.do_action()
508fc46f261SDan Klein        self.assertOutputCheckerMatch(
509fc46f261SDan Klein                expected_stderr, self.fake_stderr.getvalue())
510fc46f261SDan Klein
511fc46f261SDan Klein    def test_requests_app_run(self):
512fc46f261SDan Klein        """ Should request the application to run. """
513fc46f261SDan Klein        instance = self.test_instance
514fc46f261SDan Klein        instance.do_action()
515fc46f261SDan Klein        self.test_app.run.assert_called_with()
516fc46f261SDan Klein
517fc46f261SDan Klein
518fc46f261SDan Kleinclass DaemonRunner_do_action_stop_TestCase(DaemonRunner_BaseTestCase):
519fc46f261SDan Klein    """ Test cases for DaemonRunner.do_action method, action 'stop'. """
520fc46f261SDan Klein
521fc46f261SDan Klein    def setUp(self):
522fc46f261SDan Klein        """ Set up test fixtures. """
523fc46f261SDan Klein        super(DaemonRunner_do_action_stop_TestCase, self).setUp()
524fc46f261SDan Klein
525fc46f261SDan Klein        set_runner_scenario(self, 'pidfile-locked')
526fc46f261SDan Klein
527fc46f261SDan Klein        self.test_instance.action = 'stop'
528fc46f261SDan Klein
529fc46f261SDan Klein        self.mock_runner_lockfile.is_locked.return_value = True
530fc46f261SDan Klein        self.mock_runner_lockfile.i_am_locking.return_value = False
531fc46f261SDan Klein        self.mock_runner_lockfile.read_pid.return_value = (
532fc46f261SDan Klein                self.scenario['pidlockfile_scenario']['pidfile_pid'])
533fc46f261SDan Klein
534fc46f261SDan Klein    def test_raises_error_if_pidfile_not_locked(self):
535fc46f261SDan Klein        """ Should raise error if PID file is not locked. """
536fc46f261SDan Klein        set_runner_scenario(self, 'simple')
537fc46f261SDan Klein        instance = self.test_instance
538fc46f261SDan Klein        self.mock_runner_lockfile.is_locked.return_value = False
539fc46f261SDan Klein        self.mock_runner_lockfile.i_am_locking.return_value = False
540fc46f261SDan Klein        self.mock_runner_lockfile.read_pid.return_value = (
541fc46f261SDan Klein                self.scenario['pidlockfile_scenario']['pidfile_pid'])
542fc46f261SDan Klein        pidfile_path = self.scenario['pidfile_path']
543fc46f261SDan Klein        expected_error = daemon.runner.DaemonRunnerStopFailureError
544fc46f261SDan Klein        expected_message_content = pidfile_path
545fc46f261SDan Klein        exc = self.assertRaises(
546fc46f261SDan Klein                expected_error,
547fc46f261SDan Klein                instance.do_action)
548fc46f261SDan Klein        self.assertIn(expected_message_content, unicode(exc))
549fc46f261SDan Klein
550fc46f261SDan Klein    def test_breaks_lock_if_pidfile_stale(self):
551fc46f261SDan Klein        """ Should break lock if PID file is stale. """
552fc46f261SDan Klein        instance = self.test_instance
553fc46f261SDan Klein        pidfile_path = self.scenario['pidfile_path']
554fc46f261SDan Klein        test_pid = self.scenario['pidlockfile_scenario']['pidfile_pid']
555fc46f261SDan Klein        expected_signal = signal.SIG_DFL
556fc46f261SDan Klein        test_error = OSError(errno.ESRCH, "Not running")
557fc46f261SDan Klein        os.kill.side_effect = test_error
558fc46f261SDan Klein        instance.do_action()
559fc46f261SDan Klein        self.mock_runner_lockfile.break_lock.assert_called_with()
560fc46f261SDan Klein
561fc46f261SDan Klein    def test_sends_terminate_signal_to_process_from_pidfile(self):
562fc46f261SDan Klein        """ Should send SIGTERM to the daemon process. """
563fc46f261SDan Klein        instance = self.test_instance
564fc46f261SDan Klein        test_pid = self.scenario['pidlockfile_scenario']['pidfile_pid']
565fc46f261SDan Klein        expected_signal = signal.SIGTERM
566fc46f261SDan Klein        instance.do_action()
567fc46f261SDan Klein        os.kill.assert_called_with(test_pid, expected_signal)
568fc46f261SDan Klein
569fc46f261SDan Klein    def test_raises_error_if_cannot_send_signal_to_process(self):
570fc46f261SDan Klein        """ Should raise error if cannot send signal to daemon process. """
571fc46f261SDan Klein        instance = self.test_instance
572fc46f261SDan Klein        test_pid = self.scenario['pidlockfile_scenario']['pidfile_pid']
573fc46f261SDan Klein        pidfile_path = self.scenario['pidfile_path']
574fc46f261SDan Klein        test_error = OSError(errno.EPERM, "Nice try")
575fc46f261SDan Klein        os.kill.side_effect = test_error
576fc46f261SDan Klein        expected_error = daemon.runner.DaemonRunnerStopFailureError
577fc46f261SDan Klein        expected_message_content = unicode(test_pid)
578fc46f261SDan Klein        exc = self.assertRaises(
579fc46f261SDan Klein                expected_error,
580fc46f261SDan Klein                instance.do_action)
581fc46f261SDan Klein        self.assertIn(expected_message_content, unicode(exc))
582fc46f261SDan Klein
583fc46f261SDan Klein
584fc46f261SDan Klein@mock.patch.object(daemon.runner.DaemonRunner, "_start")
585fc46f261SDan Klein@mock.patch.object(daemon.runner.DaemonRunner, "_stop")
586fc46f261SDan Kleinclass DaemonRunner_do_action_restart_TestCase(DaemonRunner_BaseTestCase):
587fc46f261SDan Klein    """ Test cases for DaemonRunner.do_action method, action 'restart'. """
588fc46f261SDan Klein
589fc46f261SDan Klein    def setUp(self):
590fc46f261SDan Klein        """ Set up test fixtures. """
591fc46f261SDan Klein        super(DaemonRunner_do_action_restart_TestCase, self).setUp()
592fc46f261SDan Klein
593fc46f261SDan Klein        set_runner_scenario(self, 'pidfile-locked')
594fc46f261SDan Klein
595fc46f261SDan Klein        self.test_instance.action = 'restart'
596fc46f261SDan Klein
597fc46f261SDan Klein    def test_requests_stop_then_start(
598fc46f261SDan Klein            self,
599fc46f261SDan Klein            mock_func_daemonrunner_start, mock_func_daemonrunner_stop):
600fc46f261SDan Klein        """ Should request stop, then start. """
601fc46f261SDan Klein        instance = self.test_instance
602fc46f261SDan Klein        instance.do_action()
603fc46f261SDan Klein        mock_func_daemonrunner_start.assert_called_with()
604fc46f261SDan Klein        mock_func_daemonrunner_stop.assert_called_with()
605fc46f261SDan Klein
606fc46f261SDan Klein
607fc46f261SDan Klein@mock.patch.object(sys, "stderr")
608fc46f261SDan Kleinclass emit_message_TestCase(scaffold.TestCase):
609fc46f261SDan Klein    """ Test cases for ‘emit_message’ function. """
610fc46f261SDan Klein
611fc46f261SDan Klein    def test_writes_specified_message_to_stream(self, mock_stderr):
612fc46f261SDan Klein        """ Should write specified message to stream. """
613fc46f261SDan Klein        test_message = self.getUniqueString()
614fc46f261SDan Klein        expected_content = "{message}\n".format(message=test_message)
615fc46f261SDan Klein        daemon.runner.emit_message(test_message, stream=mock_stderr)
616fc46f261SDan Klein        mock_stderr.write.assert_called_with(expected_content)
617fc46f261SDan Klein
618fc46f261SDan Klein    def test_writes_to_specified_stream(self, mock_stderr):
619fc46f261SDan Klein        """ Should write message to specified stream. """
620fc46f261SDan Klein        test_message = self.getUniqueString()
621fc46f261SDan Klein        mock_stream = mock.MagicMock()
622fc46f261SDan Klein        daemon.runner.emit_message(test_message, stream=mock_stream)
623fc46f261SDan Klein        mock_stream.write.assert_called_with(mock.ANY)
624fc46f261SDan Klein
625fc46f261SDan Klein    def test_writes_to_stderr_by_default(self, mock_stderr):
626fc46f261SDan Klein        """ Should write message to ‘sys.stderr’ by default. """
627fc46f261SDan Klein        test_message = self.getUniqueString()
628fc46f261SDan Klein        daemon.runner.emit_message(test_message)
629fc46f261SDan Klein        mock_stderr.write.assert_called_with(mock.ANY)
630fc46f261SDan Klein
631fc46f261SDan Klein
632fc46f261SDan Kleinclass is_pidfile_stale_TestCase(scaffold.TestCase):
633fc46f261SDan Klein    """ Test cases for ‘is_pidfile_stale’ function. """
634fc46f261SDan Klein
635fc46f261SDan Klein    def setUp(self):
636fc46f261SDan Klein        """ Set up test fixtures. """
637fc46f261SDan Klein        super(is_pidfile_stale_TestCase, self).setUp()
638fc46f261SDan Klein
639fc46f261SDan Klein        func_patcher_os_kill = mock.patch.object(os, "kill")
640fc46f261SDan Klein        func_patcher_os_kill.start()
641fc46f261SDan Klein        self.addCleanup(func_patcher_os_kill.stop)
642fc46f261SDan Klein        os.kill.return_value = None
643fc46f261SDan Klein
644fc46f261SDan Klein        self.test_pid = self.getUniqueInteger()
645fc46f261SDan Klein        self.test_pidfile = mock.MagicMock(daemon.pidfile.TimeoutPIDLockFile)
646fc46f261SDan Klein        self.test_pidfile.read_pid.return_value = self.test_pid
647fc46f261SDan Klein
648fc46f261SDan Klein    def test_returns_false_if_no_pid_in_file(self):
649fc46f261SDan Klein        """ Should return False if the pidfile contains no PID. """
650fc46f261SDan Klein        self.test_pidfile.read_pid.return_value = None
651fc46f261SDan Klein        expected_result = False
652fc46f261SDan Klein        result = daemon.runner.is_pidfile_stale(self.test_pidfile)
653fc46f261SDan Klein        self.assertEqual(expected_result, result)
654fc46f261SDan Klein
655fc46f261SDan Klein    def test_returns_false_if_process_exists(self):
656fc46f261SDan Klein        """ Should return False if the process with its PID exists. """
657fc46f261SDan Klein        expected_result = False
658fc46f261SDan Klein        result = daemon.runner.is_pidfile_stale(self.test_pidfile)
659fc46f261SDan Klein        self.assertEqual(expected_result, result)
660fc46f261SDan Klein
661fc46f261SDan Klein    def test_returns_true_if_process_does_not_exist(self):
662fc46f261SDan Klein        """ Should return True if the process does not exist. """
663fc46f261SDan Klein        test_error = ProcessLookupError("No such process")
664fc46f261SDan Klein        del os.kill.return_value
665fc46f261SDan Klein        os.kill.side_effect = test_error
666fc46f261SDan Klein        expected_result = True
667fc46f261SDan Klein        result = daemon.runner.is_pidfile_stale(self.test_pidfile)
668fc46f261SDan Klein        self.assertEqual(expected_result, result)
669fc46f261SDan Klein
670fc46f261SDan Klein
671fc46f261SDan Klein# Local variables:
672fc46f261SDan Klein# coding: utf-8
673fc46f261SDan Klein# mode: python
674fc46f261SDan Klein# End:
675fc46f261SDan Klein# vim: fileencoding=utf-8 filetype=python :
676