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()