2018-04-16 20:19:30 +02:00
|
|
|
"""
|
|
|
|
Common reusable code for tests and tests for pybitmessage process.
|
|
|
|
"""
|
|
|
|
|
2018-04-12 16:29:22 +02:00
|
|
|
import os
|
|
|
|
import signal
|
2018-05-08 16:39:07 +02:00
|
|
|
import subprocess # nosec
|
2020-10-16 14:51:02 +02:00
|
|
|
import sys
|
2018-04-12 16:29:22 +02:00
|
|
|
import tempfile
|
2018-04-16 09:00:23 +02:00
|
|
|
import time
|
2018-04-16 20:19:30 +02:00
|
|
|
import unittest
|
2018-04-12 16:29:22 +02:00
|
|
|
|
|
|
|
import psutil
|
|
|
|
|
2020-10-28 17:01:55 +01:00
|
|
|
from common import cleanup
|
|
|
|
|
2018-04-12 16:29:22 +02:00
|
|
|
|
2018-04-16 09:00:23 +02:00
|
|
|
def put_signal_file(path, filename):
|
2018-04-16 20:19:30 +02:00
|
|
|
"""Creates file, presence of which is a signal about some event."""
|
2018-04-16 09:00:23 +02:00
|
|
|
with open(os.path.join(path, filename), 'wb') as outfile:
|
|
|
|
outfile.write(str(time.time()))
|
|
|
|
|
|
|
|
|
2018-04-12 16:29:22 +02:00
|
|
|
class TestProcessProto(unittest.TestCase):
|
2018-04-16 20:19:30 +02:00
|
|
|
"""Test case implementing common logic for external testing:
|
|
|
|
it starts pybitmessage in setUpClass() and stops it in tearDownClass()
|
|
|
|
"""
|
2018-04-12 16:29:22 +02:00
|
|
|
_process_cmd = ['pybitmessage', '-d']
|
2020-10-16 14:51:02 +02:00
|
|
|
_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'
|
|
|
|
]
|
2018-04-16 09:00:23 +02:00
|
|
|
_files = (
|
|
|
|
'keys.dat', 'debug.log', 'messages.dat', 'knownnodes.dat',
|
|
|
|
'.api_started', 'unittest.lock'
|
|
|
|
)
|
2020-11-02 11:27:02 +01:00
|
|
|
home = None
|
2018-04-12 16:29:22 +02:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def setUpClass(cls):
|
2018-04-16 20:19:30 +02:00
|
|
|
"""Setup environment and start pybitmessage"""
|
2020-02-19 11:43:23 +01:00
|
|
|
cls.flag = False
|
2020-11-02 11:27:02 +01:00
|
|
|
if not cls.home:
|
|
|
|
cls.home = tempfile.gettempdir()
|
|
|
|
cls._cleanup_files()
|
|
|
|
os.environ['BITMESSAGE_HOME'] = cls.home
|
2018-04-16 09:00:23 +02:00
|
|
|
put_signal_file(cls.home, 'unittest.lock')
|
2020-10-16 14:51:02 +02:00
|
|
|
starttime = int(time.time())
|
2020-02-20 13:14:25 +01:00
|
|
|
subprocess.Popen(
|
|
|
|
cls._process_cmd,
|
|
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT) # nosec
|
2020-10-16 14:51:02 +02:00
|
|
|
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'))
|
2018-04-12 16:29:22 +02:00
|
|
|
cls.process = psutil.Process(cls.pid)
|
|
|
|
|
2020-02-19 11:43:23 +01:00
|
|
|
def setUp(self):
|
|
|
|
if self.flag:
|
|
|
|
self.fail("%s is not started ):" % self._process_cmd)
|
|
|
|
|
2018-04-12 16:29:22 +02:00
|
|
|
@classmethod
|
|
|
|
def _get_readline(cls, pfile):
|
|
|
|
pfile = os.path.join(cls.home, pfile)
|
|
|
|
try:
|
|
|
|
return open(pfile, 'rb').readline().strip()
|
|
|
|
except (OSError, IOError):
|
|
|
|
pass
|
|
|
|
|
2019-03-13 16:56:26 +01:00
|
|
|
@classmethod
|
|
|
|
def _stop_process(cls, timeout=5):
|
|
|
|
cls.process.send_signal(signal.SIGTERM)
|
|
|
|
try:
|
|
|
|
cls.process.wait(timeout)
|
|
|
|
except psutil.TimeoutExpired:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
2020-10-16 14:51:02 +02:00
|
|
|
@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
|
|
|
|
|
2018-04-12 16:29:22 +02:00
|
|
|
@classmethod
|
|
|
|
def _cleanup_files(cls):
|
2020-10-28 17:01:55 +01:00
|
|
|
cleanup(cls.home, cls._files)
|
2018-04-12 16:29:22 +02:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def tearDownClass(cls):
|
2018-04-16 20:19:30 +02:00
|
|
|
"""Ensures that pybitmessage stopped and removes files"""
|
2018-04-12 16:29:22 +02:00
|
|
|
try:
|
2019-03-13 16:56:26 +01:00
|
|
|
if not cls._stop_process():
|
|
|
|
cls.process.kill()
|
2020-02-19 11:43:23 +01:00
|
|
|
except (psutil.NoSuchProcess, AttributeError):
|
2019-03-13 16:56:26 +01:00
|
|
|
pass
|
2018-04-12 16:29:22 +02:00
|
|
|
finally:
|
|
|
|
cls._cleanup_files()
|
|
|
|
|
|
|
|
def _test_threads(self):
|
2020-10-16 14:51:02 +02:00
|
|
|
"""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)
|
2018-04-12 16:29:22 +02:00
|
|
|
|
|
|
|
|
2019-03-07 11:18:45 +01:00
|
|
|
class TestProcessShutdown(TestProcessProto):
|
|
|
|
"""Separate test case for SIGTERM"""
|
|
|
|
def test_shutdown(self):
|
|
|
|
"""Send to pybitmessage SIGTERM and ensure it stopped"""
|
2019-03-13 16:56:26 +01:00
|
|
|
# longer wait time because it's not a benchmark
|
|
|
|
self.assertTrue(
|
|
|
|
self._stop_process(20),
|
|
|
|
'%s has not stopped in 20 sec' % ' '.join(self._process_cmd))
|
2019-03-07 11:18:45 +01:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def tearDownClass(cls):
|
|
|
|
"""Special teardown because pybitmessage is already stopped"""
|
2020-10-16 14:51:02 +02:00
|
|
|
try:
|
|
|
|
if cls.process.is_running():
|
|
|
|
cls.process.kill()
|
|
|
|
cls.process.wait(5)
|
|
|
|
except (psutil.TimeoutExpired, psutil.NoSuchProcess):
|
|
|
|
pass
|
|
|
|
finally:
|
|
|
|
cls._cleanup_files()
|
2019-03-07 11:18:45 +01:00
|
|
|
|
|
|
|
|
2018-04-12 16:29:22 +02:00
|
|
|
class TestProcess(TestProcessProto):
|
2018-04-16 20:19:30 +02:00
|
|
|
"""A test case for pybitmessage process"""
|
2018-04-12 16:29:22 +02:00
|
|
|
def test_process_name(self):
|
|
|
|
"""Check PyBitmessage process name"""
|
|
|
|
self.assertEqual(self.process.name(), 'PyBitmessage')
|
|
|
|
|
|
|
|
def test_files(self):
|
|
|
|
"""Check existence of PyBitmessage files"""
|
|
|
|
for pfile in self._files:
|
2018-04-16 09:00:23 +02:00
|
|
|
if pfile.startswith('.'):
|
|
|
|
continue
|
2018-04-12 16:29:22 +02:00
|
|
|
self.assertIsNot(
|
|
|
|
self._get_readline(pfile), None,
|
|
|
|
'Failed to read file %s' % pfile
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_threads(self):
|
|
|
|
"""Testing PyBitmessage threads"""
|
|
|
|
self._test_threads()
|