2023-08-01 23:15:00 +02:00
|
|
|
"""Blind tests, starting the minode process"""
|
2024-07-21 00:58:23 +02:00
|
|
|
import os
|
2021-03-07 22:39:37 +01:00
|
|
|
import signal
|
2021-03-11 18:37:23 +01:00
|
|
|
import socket
|
2021-03-07 22:39:37 +01:00
|
|
|
import subprocess
|
2021-08-02 15:45:25 +02:00
|
|
|
import sys
|
2021-03-07 22:39:37 +01:00
|
|
|
import tempfile
|
|
|
|
import time
|
2024-07-21 00:58:23 +02:00
|
|
|
import unittest
|
2021-03-07 22:39:37 +01:00
|
|
|
|
|
|
|
import psutil
|
|
|
|
|
2024-07-29 02:53:28 +02:00
|
|
|
from minode.i2p import util
|
2023-08-27 02:12:17 +02:00
|
|
|
from minode.structure import NetAddrNoPrefix
|
|
|
|
|
2021-03-11 18:37:23 +01:00
|
|
|
try:
|
|
|
|
socket.socket().bind(('127.0.0.1', 7656))
|
|
|
|
i2p_port_free = True
|
|
|
|
except (OSError, socket.error):
|
|
|
|
i2p_port_free = False
|
|
|
|
|
2021-03-07 22:39:37 +01:00
|
|
|
|
|
|
|
class TestProcessProto(unittest.TestCase):
|
|
|
|
"""Test process attributes, common flow"""
|
|
|
|
_process_cmd = ['minode']
|
2024-07-08 23:39:41 +02:00
|
|
|
_connection_limit = 4 if sys.platform.startswith('win') else 8
|
2022-09-23 03:25:24 +02:00
|
|
|
_listen = False
|
2021-03-07 22:39:37 +01:00
|
|
|
_listening_port = None
|
|
|
|
|
|
|
|
home = None
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def setUpClass(cls):
|
|
|
|
if not cls.home:
|
|
|
|
cls.home = tempfile.gettempdir()
|
|
|
|
cmd = cls._process_cmd + [
|
|
|
|
'--data-dir', cls.home,
|
|
|
|
'--connection-limit', str(cls._connection_limit)
|
|
|
|
]
|
2022-09-23 03:25:24 +02:00
|
|
|
if not cls._listen:
|
2021-03-07 22:39:37 +01:00
|
|
|
cmd += ['--no-incoming']
|
2022-09-23 03:25:24 +02:00
|
|
|
elif cls._listening_port:
|
|
|
|
cmd += ['-p', str(cls._listening_port)]
|
2021-03-07 22:39:37 +01:00
|
|
|
cls.process = psutil.Popen(cmd, stderr=subprocess.STDOUT) # nosec
|
|
|
|
|
|
|
|
@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
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def tearDownClass(cls):
|
2023-08-01 23:15:00 +02:00
|
|
|
"""Ensures that process stopped and removes files"""
|
2021-03-07 22:39:37 +01:00
|
|
|
try:
|
|
|
|
if not cls._stop_process(10):
|
|
|
|
try:
|
|
|
|
cls.process.kill()
|
|
|
|
except psutil.NoSuchProcess:
|
|
|
|
pass
|
|
|
|
except psutil.NoSuchProcess:
|
|
|
|
pass
|
|
|
|
|
2023-08-02 19:53:36 +02:00
|
|
|
def connections(self):
|
|
|
|
"""All process' established connections"""
|
|
|
|
return [
|
|
|
|
c for c in self.process.connections()
|
|
|
|
if c.status == 'ESTABLISHED']
|
|
|
|
|
2021-03-07 22:39:37 +01:00
|
|
|
|
|
|
|
class TestProcessShutdown(TestProcessProto):
|
|
|
|
"""Separate test case for SIGTERM"""
|
2024-06-17 19:47:54 +02:00
|
|
|
_wait_time = 30
|
|
|
|
# longer wait time because it's not a benchmark
|
|
|
|
|
2021-03-07 22:39:37 +01:00
|
|
|
def test_shutdown(self):
|
|
|
|
"""Send to minode SIGTERM and ensure it stopped"""
|
|
|
|
self.assertTrue(
|
2024-06-17 19:47:54 +02:00
|
|
|
self._stop_process(self._wait_time),
|
|
|
|
'%s has not stopped in %i sec' % (
|
|
|
|
' '.join(self._process_cmd), self._wait_time))
|
2021-03-07 22:39:37 +01:00
|
|
|
|
|
|
|
|
|
|
|
class TestProcess(TestProcessProto):
|
|
|
|
"""The test case for minode process"""
|
2024-07-08 23:39:41 +02:00
|
|
|
_wait_time = 180
|
2022-09-23 06:18:39 +02:00
|
|
|
_check_limit = False
|
|
|
|
|
2021-03-07 22:39:37 +01:00
|
|
|
def test_connections(self):
|
|
|
|
"""Check minode process connections"""
|
|
|
|
_started = time.time()
|
2022-09-23 03:25:24 +02:00
|
|
|
|
2022-09-23 06:18:39 +02:00
|
|
|
def continue_check_limit(extra_time):
|
2023-08-02 04:17:58 +02:00
|
|
|
for _ in range(extra_time * 2):
|
2022-09-23 06:18:39 +02:00
|
|
|
self.assertLessEqual(
|
2023-08-02 19:53:36 +02:00
|
|
|
len(self.connections()),
|
2022-09-23 06:18:39 +02:00
|
|
|
# shared.outgoing_connections, one listening
|
|
|
|
# TODO: find the cause of one extra
|
|
|
|
(min(self._connection_limit, 8) if not self._listen
|
|
|
|
else self._connection_limit) + 1,
|
|
|
|
'Opened more connections than required'
|
|
|
|
' by --connection-limit')
|
|
|
|
time.sleep(1)
|
|
|
|
|
2023-08-02 04:17:58 +02:00
|
|
|
for _ in range(self._wait_time * 2):
|
2024-07-08 23:39:41 +02:00
|
|
|
if len(self.connections()) >= self._connection_limit / 2:
|
2021-03-07 22:39:37 +01:00
|
|
|
_time_to_connect = round(time.time() - _started)
|
|
|
|
break
|
2023-08-27 02:12:17 +02:00
|
|
|
if '--i2p' not in self._process_cmd:
|
|
|
|
groups = []
|
|
|
|
for c in self.connections():
|
|
|
|
group = NetAddrNoPrefix.network_group(c.raddr[0])
|
|
|
|
self.assertNotIn(group, groups)
|
|
|
|
groups.append(group)
|
2021-03-07 22:39:37 +01:00
|
|
|
time.sleep(0.5)
|
|
|
|
else:
|
|
|
|
self.fail(
|
2024-07-08 23:39:41 +02:00
|
|
|
'Failed to establish at least %i connections in %s sec'
|
2024-06-17 19:47:54 +02:00
|
|
|
% (int(self._connection_limit / 2), self._wait_time))
|
2022-09-23 06:18:39 +02:00
|
|
|
|
|
|
|
if self._check_limit:
|
|
|
|
continue_check_limit(_time_to_connect)
|
2022-09-23 03:25:24 +02:00
|
|
|
|
|
|
|
for c in self.process.connections():
|
2021-03-07 22:39:37 +01:00
|
|
|
if c.status == 'LISTEN':
|
|
|
|
if self._listen is False:
|
2023-08-14 05:05:29 +02:00
|
|
|
self.fail('Listening while started with --no-incoming')
|
|
|
|
return
|
2021-03-07 22:39:37 +01:00
|
|
|
self.assertEqual(c.laddr[1], self._listening_port or 8444)
|
|
|
|
break
|
2022-09-23 03:25:24 +02:00
|
|
|
else:
|
|
|
|
if self._listen:
|
|
|
|
self.fail('No listening connection found')
|
2021-03-11 18:37:23 +01:00
|
|
|
|
|
|
|
|
|
|
|
@unittest.skipIf(i2p_port_free, 'No running i2pd detected')
|
|
|
|
class TestProcessI2P(TestProcess):
|
|
|
|
"""Test minode process with --i2p and no IP"""
|
|
|
|
_process_cmd = ['minode', '--i2p', '--no-ip']
|
|
|
|
_listen = True
|
|
|
|
_listening_port = 8448
|
2023-08-02 19:53:36 +02:00
|
|
|
|
2024-07-21 00:58:23 +02:00
|
|
|
@classmethod
|
|
|
|
def setUpClass(cls):
|
|
|
|
cls.freezed = False
|
|
|
|
cls.keyfile = os.path.join(cls.home, 'i2p_dest.pub')
|
|
|
|
saved = os.path.isfile(cls.keyfile)
|
|
|
|
super().setUpClass()
|
|
|
|
for _ in range(cls._wait_time):
|
|
|
|
if saved:
|
|
|
|
if cls.process.num_threads() > 3:
|
|
|
|
break
|
|
|
|
elif os.path.isfile(cls.keyfile):
|
|
|
|
break
|
|
|
|
time.sleep(1)
|
|
|
|
else:
|
|
|
|
cls.freezed = True
|
|
|
|
|
2024-07-29 02:53:28 +02:00
|
|
|
def setUp(self):
|
2024-07-29 04:33:18 +02:00
|
|
|
"""Skip any test if I2PController freezed"""
|
2024-07-21 00:58:23 +02:00
|
|
|
if self.freezed:
|
2024-07-29 04:33:18 +02:00
|
|
|
raise unittest.SkipTest(
|
|
|
|
'I2PController has probably failed to start')
|
2024-07-29 02:53:28 +02:00
|
|
|
|
|
|
|
def test_saved_keys(self):
|
|
|
|
"""Check saved i2p keys"""
|
|
|
|
with open(self.keyfile, 'br') as src:
|
|
|
|
i2p_dest_pub = src.read()
|
|
|
|
with open(os.path.join(self.home, 'i2p_dest_priv.key'), 'br') as src:
|
|
|
|
i2p_dest_priv = src.read()
|
|
|
|
self.assertEqual(util.pub_from_priv(i2p_dest_priv), i2p_dest_pub)
|
|
|
|
|
|
|
|
def test_connections(self):
|
|
|
|
"""Ensure all connections are I2P"""
|
2023-08-02 19:53:36 +02:00
|
|
|
super().test_connections()
|
|
|
|
for c in self.connections():
|
|
|
|
self.assertEqual(c.raddr[0], '127.0.0.1')
|
|
|
|
self.assertEqual(c.raddr[1], 7656)
|
2023-08-10 02:55:48 +02:00
|
|
|
|
|
|
|
|
|
|
|
@unittest.skipUnless(i2p_port_free, 'Detected running i2pd')
|
|
|
|
class TestProcessNoI2P(TestProcessShutdown):
|
|
|
|
"""Test minode process shutdown with --i2p and no IP"""
|
|
|
|
_process_cmd = ['minode', '--i2p', '--no-ip']
|