33
|
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
|
|
16 from __future__ import (absolute_import, unicode_literals)
|
|
17
|
|
18 try:
|
|
19 # Python 3 standard library.
|
|
20 import builtins
|
|
21 except ImportError:
|
|
22 # Python 2 standard library.
|
|
23 import __builtin__ as builtins
|
|
24 import os
|
|
25 import os.path
|
|
26 import sys
|
|
27 import tempfile
|
|
28 import errno
|
|
29 import signal
|
|
30 import functools
|
|
31
|
|
32 import lockfile
|
|
33 import mock
|
|
34 import testtools
|
|
35
|
|
36 from . import scaffold
|
|
37 from .scaffold import (basestring, unicode)
|
|
38 from .test_pidfile import (
|
|
39 FakeFileDescriptorStringIO,
|
|
40 setup_pidfile_fixtures,
|
|
41 make_pidlockfile_scenarios,
|
|
42 apply_lockfile_method_mocks,
|
|
43 )
|
|
44 from .test_daemon import (
|
|
45 setup_streams_fixtures,
|
|
46 )
|
|
47
|
|
48 import daemon.daemon
|
|
49 import daemon.runner
|
|
50 import daemon.pidfile
|
|
51
|
|
52
|
|
53 class 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
|
|
80 def 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
|
|
114 def 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
|
|
132 def 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
|
|
230 class 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
|
|
241 class 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
|
|
362 class 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
|
|
388 class 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
|
|
438 try:
|
|
439 ProcessLookupError
|
|
440 except NameError:
|
|
441 # Python 2 uses OSError.
|
|
442 ProcessLookupError = functools.partial(OSError, errno.ESRCH)
|
|
443
|
|
444 class 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
|
|
457 class 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
|
|
518 class 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")
|
|
586 class 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")
|
|
608 class 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
|
|
632 class 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 :
|