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