PageRenderTime 29ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/src/mailman/commands/tests/test_control.py

https://gitlab.com/noc0lour/mailman
Python | 192 lines | 125 code | 26 blank | 41 comment | 18 complexity | e34b832a5fd316c9ea7b9fb1aa63afb2 MD5 | raw file
  1. # Copyright (C) 2011-2016 by the Free Software Foundation, Inc.
  2. #
  3. # This file is part of GNU Mailman.
  4. #
  5. # GNU Mailman is free software: you can redistribute it and/or modify it under
  6. # the terms of the GNU General Public License as published by the Free
  7. # Software Foundation, either version 3 of the License, or (at your option)
  8. # any later version.
  9. #
  10. # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
  11. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  12. # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
  13. # more details.
  14. #
  15. # You should have received a copy of the GNU General Public License along with
  16. # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
  17. """Test some additional corner cases for starting/stopping."""
  18. import os
  19. import sys
  20. import time
  21. import errno
  22. import shutil
  23. import signal
  24. import socket
  25. import unittest
  26. from contextlib import ExitStack, suppress
  27. from datetime import datetime, timedelta
  28. from mailman.commands.cli_control import Start, kill_watcher
  29. from mailman.config import Configuration, config
  30. from mailman.testing.helpers import configuration
  31. from mailman.testing.layers import ConfigLayer
  32. from tempfile import TemporaryDirectory
  33. SEP = '|'
  34. def make_config():
  35. # All we care about is the master process; normally it starts a bunch of
  36. # runners, but we don't care about any of them, so write a test
  37. # configuration file for the master that disables all the runners.
  38. new_config = 'no-runners.cfg'
  39. config_file = os.path.join(os.path.dirname(config.filename), new_config)
  40. shutil.copyfile(config.filename, config_file)
  41. with open(config_file, 'a') as fp:
  42. for runner_config in config.runner_configs:
  43. print('[{}]\nstart:no\n'.format(runner_config.name), file=fp)
  44. return config_file
  45. def find_master():
  46. # See if the master process is still running.
  47. until = timedelta(seconds=10) + datetime.now()
  48. while datetime.now() < until:
  49. time.sleep(0.1)
  50. with suppress(FileNotFoundError, ValueError, ProcessLookupError):
  51. with open(config.PID_FILE) as fp:
  52. pid = int(fp.read().strip())
  53. os.kill(pid, 0)
  54. return pid
  55. return None
  56. class FakeArgs:
  57. force = None
  58. run_as_user = None
  59. quiet = True
  60. config = None
  61. class FakeParser:
  62. def __init__(self):
  63. self.message = None
  64. def error(self, message):
  65. self.message = message
  66. sys.exit(1)
  67. class TestStart(unittest.TestCase):
  68. """Test various starting scenarios."""
  69. layer = ConfigLayer
  70. def setUp(self):
  71. self.command = Start()
  72. self.command.parser = FakeParser()
  73. self.args = FakeArgs()
  74. self.args.config = make_config()
  75. def tearDown(self):
  76. try:
  77. with open(config.PID_FILE) as fp:
  78. master_pid = int(fp.read())
  79. except OSError as error:
  80. if error.errno != errno.ENOENT:
  81. raise
  82. # There is no master, so just ignore this.
  83. return
  84. kill_watcher(signal.SIGTERM)
  85. os.waitpid(master_pid, 0)
  86. def test_force_stale_lock(self):
  87. # Fake an acquisition of the master lock by another process, which
  88. # subsequently goes stale. Start by finding a free process id. Yes,
  89. # this could race, but given that we're starting with our own PID and
  90. # searching downward, it's less likely.
  91. fake_pid = os.getpid() - 1
  92. while fake_pid > 1:
  93. try:
  94. os.kill(fake_pid, 0)
  95. except OSError as error:
  96. if error.errno == errno.ESRCH:
  97. break
  98. fake_pid -= 1
  99. else:
  100. raise RuntimeError('Cannot find free PID')
  101. # Lock acquisition logic taken from flufl.lock.
  102. claim_file = SEP.join((
  103. config.LOCK_FILE,
  104. socket.getfqdn(),
  105. str(fake_pid),
  106. '0'))
  107. with open(config.LOCK_FILE, 'w') as fp:
  108. fp.write(claim_file)
  109. os.link(config.LOCK_FILE, claim_file)
  110. expiration_date = datetime.now() - timedelta(minutes=2)
  111. t = time.mktime(expiration_date.timetuple())
  112. os.utime(claim_file, (t, t))
  113. # Start without --force; no master will be running.
  114. with suppress(SystemExit):
  115. self.command.process(self.args)
  116. self.assertIsNone(find_master())
  117. self.assertIn('--force', self.command.parser.message)
  118. # Start again, this time with --force.
  119. self.args.force = True
  120. self.command.process(self.args)
  121. pid = find_master()
  122. self.assertIsNotNone(pid)
  123. class TestBinDir(unittest.TestCase):
  124. """Test issues related to bin_dir, e.g. issue #3"""
  125. layer = ConfigLayer
  126. def setUp(self):
  127. self.command = Start()
  128. self.command.parser = FakeParser()
  129. self.args = FakeArgs()
  130. self.args.config = make_config()
  131. def test_master_is_elsewhere(self):
  132. with ExitStack() as resources:
  133. # Patch os.fork() so that we can record the failing child process's
  134. # id. We need to wait on the child exiting in either case, and
  135. # when it fails, no master.pid will be written.
  136. bin_dir = resources.enter_context(TemporaryDirectory())
  137. old_master = os.path.join(config.BIN_DIR, 'master')
  138. new_master = os.path.join(bin_dir, 'master')
  139. shutil.move(old_master, new_master)
  140. resources.callback(shutil.move, new_master, old_master)
  141. # Starting mailman should fail because 'master' can't be found.
  142. # XXX This will print Errno 2 on the console because we're not
  143. # silencing the child process's stderr.
  144. self.command.process(self.args)
  145. # There should be no pid file.
  146. args_config = Configuration()
  147. args_config.load(self.args.config)
  148. self.assertFalse(os.path.exists(args_config.PID_FILE))
  149. os.wait()
  150. def test_master_is_elsewhere_and_findable(self):
  151. with ExitStack() as resources:
  152. bin_dir = resources.enter_context(TemporaryDirectory())
  153. old_master = os.path.join(config.BIN_DIR, 'master')
  154. new_master = os.path.join(bin_dir, 'master')
  155. shutil.move(old_master, new_master)
  156. resources.enter_context(
  157. configuration('paths.testing', bin_dir=bin_dir))
  158. resources.callback(shutil.move, new_master, old_master)
  159. # Starting mailman should find master in the new bin_dir.
  160. self.command.process(self.args)
  161. # There should a pid file and the process it describes should be
  162. # killable. We might have to wait until the process has started.
  163. master_pid = find_master()
  164. self.assertIsNotNone(master_pid, 'master did not start')
  165. os.kill(master_pid, signal.SIGTERM)
  166. os.waitpid(master_pid, 0)