/ src / tests / test_process.py
test_process.py
  1  """
  2  Common reusable code for tests and tests for pybitmessage process.
  3  """
  4  
  5  import os
  6  import signal
  7  import subprocess  # nosec
  8  import sys
  9  import tempfile
 10  import time
 11  import unittest
 12  
 13  import psutil
 14  
 15  from .common import cleanup, put_signal_file, skip_python3
 16  
 17  
 18  skip_python3()
 19  
 20  
 21  class TestProcessProto(unittest.TestCase):
 22      """Test case implementing common logic for external testing:
 23      it starts pybitmessage in setUpClass() and stops it in tearDownClass()
 24      """
 25      _process_cmd = ['pybitmessage', '-d']
 26      _threads_count_min = 15
 27      _threads_count_max = 16
 28      _threads_names = [
 29          'PyBitmessage',
 30          'addressGenerato',
 31          'singleWorker',
 32          'SQL',
 33          'objectProcessor',
 34          'singleCleaner',
 35          'singleAPI',
 36          'Asyncore',
 37          'ReceiveQueue_0',
 38          'ReceiveQueue_1',
 39          'ReceiveQueue_2',
 40          'Announcer',
 41          'InvBroadcaster',
 42          'AddrBroadcaster',
 43          'Downloader',
 44          'Uploader'
 45      ]
 46      _files = (
 47          'keys.dat', 'debug.log', 'messages.dat', 'knownnodes.dat',
 48          '.api_started', 'unittest.lock'
 49      )
 50      home = None
 51  
 52      @classmethod
 53      def setUpClass(cls):
 54          """Setup environment and start pybitmessage"""
 55          cls.flag = False
 56          if not cls.home:
 57              cls.home = tempfile.gettempdir()
 58              cls._cleanup_files()
 59          os.environ['BITMESSAGE_HOME'] = cls.home
 60          put_signal_file(cls.home, 'unittest.lock')
 61          starttime = int(time.time()) - 0.5
 62          cls.process = psutil.Popen(
 63              cls._process_cmd, stderr=subprocess.STDOUT)  # nosec
 64  
 65          pidfile = os.path.join(cls.home, 'singleton.lock')
 66          for _ in range(10):
 67              time.sleep(1)
 68              try:
 69                  pstat = os.stat(pidfile)
 70                  if starttime <= pstat.st_mtime and pstat.st_size > 0:
 71                      break  # the pidfile is suitable
 72              except OSError:
 73                  continue
 74  
 75          try:
 76              pid = int(cls._get_readline('singleton.lock'))
 77              cls.process = psutil.Process(pid)
 78              time.sleep(5)
 79          except (psutil.NoSuchProcess, TypeError):
 80              cls.flag = True
 81  
 82      def setUp(self):
 83          if self.flag:
 84              self.fail("%s is not started ):" % self._process_cmd)
 85  
 86      @classmethod
 87      def _get_readline(cls, pfile):
 88          pfile = os.path.join(cls.home, pfile)
 89          try:
 90              with open(pfile, 'rb') as p:
 91                  return p.readline().strip()
 92          except (OSError, IOError):
 93              pass
 94  
 95      @classmethod
 96      def _stop_process(cls, timeout=5):
 97          cls.process.send_signal(signal.SIGTERM)
 98          try:
 99              cls.process.wait(timeout)
100          except psutil.TimeoutExpired:
101              return False
102          return True
103  
104      @classmethod
105      def _kill_process(cls, timeout=5):
106          try:
107              cls.process.send_signal(signal.SIGKILL)
108              cls.process.wait(timeout)
109          # Windows or already dead
110          except (AttributeError, psutil.NoSuchProcess):
111              return True
112          # except psutil.TimeoutExpired propagates, it means something is very
113          # wrong
114          return True
115  
116      @classmethod
117      def _cleanup_files(cls):
118          cleanup(cls.home, cls._files)
119  
120      @classmethod
121      def tearDownClass(cls):
122          """Ensures that pybitmessage stopped and removes files"""
123          try:
124              if not cls._stop_process(10):
125                  processes = cls.process.children(recursive=True)
126                  processes.append(cls.process)
127                  for p in processes:
128                      try:
129                          p.kill()
130                      except psutil.NoSuchProcess:
131                          pass
132          except psutil.NoSuchProcess:
133              pass
134          finally:
135              cls._cleanup_files()
136  
137      def _test_threads(self):
138          """Test number and names of threads"""
139  
140          # pylint: disable=invalid-name
141          self.longMessage = True
142  
143          try:
144              # using ps for posix platforms
145              # because of https://github.com/giampaolo/psutil/issues/613
146              thread_names = subprocess.check_output([
147                  "ps", "-L", "-o", "comm=", "--pid",
148                  str(self.process.pid)
149              ]).split()
150          except subprocess.CalledProcessError:
151              thread_names = []
152          except:  # noqa:E722
153              thread_names = []
154  
155          running_threads = len(thread_names)
156          if 0 < running_threads < 30:  # adequacy check
157              extra_threads = []
158              missing_threads = []
159              for thread_name in thread_names:
160                  if thread_name not in self._threads_names:
161                      extra_threads.append(thread_name)
162              for thread_name in self._threads_names:
163                  if thread_name not in thread_names:
164                      missing_threads.append(thread_name)
165  
166              msg = "Missing threads: {}, Extra threads: {}".format(
167                  ",".join(missing_threads), ",".join(extra_threads))
168          else:
169              running_threads = self.process.num_threads()
170              if sys.platform.startswith('win'):
171                  running_threads -= 1  # one extra thread on Windows!
172              msg = "Unexpected running thread count"
173  
174          self.assertGreaterEqual(
175              running_threads,
176              self._threads_count_min,
177              msg)
178  
179          self.assertLessEqual(
180              running_threads,
181              self._threads_count_max,
182              msg)
183  
184  
185  class TestProcessShutdown(TestProcessProto):
186      """Separate test case for SIGTERM"""
187      def test_shutdown(self):
188          """Send to pybitmessage SIGTERM and ensure it stopped"""
189          # longer wait time because it's not a benchmark
190          self.assertTrue(
191              self._stop_process(20),
192              '%s has not stopped in 20 sec' % ' '.join(self._process_cmd))
193  
194  
195  class TestProcess(TestProcessProto):
196      """A test case for pybitmessage process"""
197      @unittest.skipIf(sys.platform[:5] != 'linux', 'probably needs prctl')
198      def test_process_name(self):
199          """Check PyBitmessage process name"""
200          self.assertEqual(self.process.name(), 'PyBitmessage')
201  
202      @unittest.skipIf(psutil.version_info < (4, 0), 'psutil is too old')
203      def test_home(self):
204          """Ensure BITMESSAGE_HOME is used by process"""
205          self.assertEqual(
206              self.process.environ().get('BITMESSAGE_HOME'), self.home)
207  
208      @unittest.skipIf(
209          os.getenv('WINEPREFIX'), "process.connections() doesn't work on wine")
210      def test_listening(self):
211          """Check that pybitmessage listens on port 8444"""
212          for c in self.process.connections():
213              if c.status == 'LISTEN':
214                  self.assertEqual(c.laddr[1], 8444)
215                  break
216  
217      def test_files(self):
218          """Check existence of PyBitmessage files"""
219          for pfile in self._files:
220              if pfile.startswith('.'):
221                  continue
222              self.assertIsNot(
223                  self._get_readline(pfile), None,
224                  'Failed to read file %s' % pfile
225              )
226  
227      def test_threads(self):
228          """Testing PyBitmessage threads"""
229          self._test_threads()