From 96a784b58bcdece87908ef597e134f1196d9868f Mon Sep 17 00:00:00 2001 From: Peter Surda Date: Fri, 16 Oct 2020 14:51:02 +0200 Subject: [PATCH] Merged PeterSurda/teardown-test1: - reset BMConfigParser before reading config file - enable threads name check on posix systems - check singleton.lock in TestProcessProto.setUpClass() --- src/bmconfigparser.py | 9 +++ src/tests/test_config.py | 1 + src/tests/test_process.py | 131 ++++++++++++++++++++++++++++++-------- 3 files changed, 113 insertions(+), 28 deletions(-) diff --git a/src/bmconfigparser.py b/src/bmconfigparser.py index 328cf0c7..cd826bb4 100644 --- a/src/bmconfigparser.py +++ b/src/bmconfigparser.py @@ -124,7 +124,16 @@ class BMConfigParser(ConfigParser.SafeConfigParser): return [ x for x in BMConfigParser().sections() if x.startswith('BM-')] + def _reset(self): + """Reset current config. There doesn't appear to be a built in + method for this""" + sections = self.sections() + for x in sections: + self.remove_section(x) + def read(self, filenames): + """Read config and populate defaults""" + self._reset() ConfigParser.ConfigParser.read(self, filenames) for section in self.sections(): for option in self.options(section): diff --git a/src/tests/test_config.py b/src/tests/test_config.py index 508c4394..fcf8c498 100644 --- a/src/tests/test_config.py +++ b/src/tests/test_config.py @@ -47,6 +47,7 @@ class TestProcessConfig(TestProcessProto): def test_config_defaults(self): """Test settings in the generated config""" self._stop_process() + self._kill_process() config = BMConfigParser() config.read(os.path.join(self.home, 'keys.dat')) diff --git a/src/tests/test_process.py b/src/tests/test_process.py index 36e95b2a..da5d92eb 100644 --- a/src/tests/test_process.py +++ b/src/tests/test_process.py @@ -5,6 +5,7 @@ Common reusable code for tests and tests for pybitmessage process. import os import signal import subprocess # nosec +import sys import tempfile import time import unittest @@ -25,7 +26,26 @@ class TestProcessProto(unittest.TestCase): it starts pybitmessage in setUpClass() and stops it in tearDownClass() """ _process_cmd = ['pybitmessage', '-d'] - _threads_count = 15 + _threads_count_min = 15 + _threads_count_max = 16 + _threads_names = [ + 'PyBitmessage', + 'addressGenerato', + 'singleWorker', + 'SQL', + 'objectProcessor', + 'singleCleaner', + 'singleAPI', + 'Asyncore', + 'ReceiveQueue_0', + 'ReceiveQueue_1', + 'ReceiveQueue_2', + 'Announcer', + 'InvBroadcaster', + 'AddrBroadcaster', + 'Downloader', + 'Uploader' + ] _files = ( 'keys.dat', 'debug.log', 'messages.dat', 'knownnodes.dat', '.api_started', 'unittest.lock' @@ -41,15 +61,28 @@ class TestProcessProto(unittest.TestCase): cls._cleanup_files() os.environ['BITMESSAGE_HOME'] = cls.home put_signal_file(cls.home, 'unittest.lock') + starttime = int(time.time()) subprocess.Popen( cls._process_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) # nosec - time.sleep(5) - try: - cls.pid = int(cls._get_readline('singleton.lock')) - except TypeError: - cls.flag = True - return + timeout = starttime + 30 + while time.time() <= timeout: + try: + if os.path.exists(os.path.join(cls.home, + 'singleton.lock')): + pstat = os.stat(os.path.join(cls.home, 'singleton.lock')) + if starttime <= pstat.st_mtime and pstat.st_size > 0: + break + except OSError: + break + time.sleep(1) + if time.time() >= timeout: + raise psutil.TimeoutExpired( + "Timeout starting {}".format(cls._process_cmd)) + # wait a bit for the program to fully start + # 10 sec should be enough + time.sleep(10) + cls.pid = int(cls._get_readline('singleton.lock')) cls.process = psutil.Process(cls.pid) def setUp(self): @@ -73,6 +106,18 @@ class TestProcessProto(unittest.TestCase): return False return True + @classmethod + def _kill_process(cls, timeout=5): + try: + cls.process.send_signal(signal.SIGKILL) + cls.process.wait(timeout) + # Windows or already dead + except (AttributeError, psutil.NoSuchProcess): + return True + # except psutil.TimeoutExpired propagates, it means something is very + # wrong + return True + @classmethod def _cleanup_files(cls): cleanup(cls.home, cls._files) @@ -82,7 +127,6 @@ class TestProcessProto(unittest.TestCase): """Ensures that pybitmessage stopped and removes files""" try: if not cls._stop_process(): - print(open(os.path.join(cls.home, 'debug.log'), 'rb').read()) cls.process.kill() except (psutil.NoSuchProcess, AttributeError): pass @@ -90,25 +134,49 @@ class TestProcessProto(unittest.TestCase): cls._cleanup_files() def _test_threads(self): - # only count for now - # because of https://github.com/giampaolo/psutil/issues/613 - # PyBitmessage - # - addressGenerator - # - singleWorker - # - SQL - # - objectProcessor - # - singleCleaner - # - singleAPI - # - Asyncore - # - ReceiveQueue_0 - # - ReceiveQueue_1 - # - ReceiveQueue_2 - # - Announcer - # - InvBroadcaster - # - AddrBroadcaster - # - Downloader - self.assertEqual( - len(self.process.threads()), self._threads_count) + """Test number and names of threads""" + + # pylint: disable=invalid-name + self.longMessage = True + + try: + # using ps for posix platforms + # because of https://github.com/giampaolo/psutil/issues/613 + thread_names = subprocess.check_output([ + "ps", "-L", "-o", "comm=", "--pid", + str(self.process.pid) + ]).split() + except: # pylint: disable=bare-except + thread_names = [] + + running_threads = len(thread_names) + if 0 < running_threads < 30: # adequacy check + extra_threads = [] + missing_threads = [] + for thread_name in thread_names: + if thread_name not in self._threads_names: + extra_threads.append(thread_name) + for thread_name in self._threads_names: + if thread_name not in thread_names: + missing_threads.append(thread_name) + + msg = "Missing threads: {}, Extra threads: {}".format( + ",".join(missing_threads), ",".join(extra_threads)) + else: + running_threads = self.process.num_threads() + if sys.platform.startswith('win'): + running_threads -= 1 # one extra thread on Windows! + msg = "Unexpected running thread count" + + self.assertGreaterEqual( + running_threads, + self._threads_count_min, + msg) + + self.assertLessEqual( + running_threads, + self._threads_count_max, + msg) class TestProcessShutdown(TestProcessProto): @@ -123,7 +191,14 @@ class TestProcessShutdown(TestProcessProto): @classmethod def tearDownClass(cls): """Special teardown because pybitmessage is already stopped""" - cls._cleanup_files() + try: + if cls.process.is_running(): + cls.process.kill() + cls.process.wait(5) + except (psutil.TimeoutExpired, psutil.NoSuchProcess): + pass + finally: + cls._cleanup_files() class TestProcess(TestProcessProto):