1
0
Files
electrum/tests/__init__.py
SomberNight 5d1df96020 tests: clear util.callback_mgr between test cases
util.callback_mgr.callbacks was not getting properly cleared between tests.
Every time an Abstract_Wallet or an LNWorker (or many other subclasses of EventListener) is instantiated,
self.register_callbacks() is called in __init__, which puts callbacks into util.callback_mgr.callbacks.
These are only cleaned up if we explicitly call Abstract_Wallet.stop() or LNWorker.stop() later, which we usually do not do in the tests.

As a result, when running multiple unit tests in a row, lots of objects created in a given testcase are never GC-ed and leak into subsequent tests. This is not only a memory leak, but wastes compute too: when events are triggered and cbs get called, these old objects also have their cbs called.

After running all (~1061) unit tests, I observe util.callback_mgr.callbacks had 30 events with a total of 3156 callbacks stored.

On my laptop, running all unit tests previously took ~115 sec, and now it takes ~73 sec.
2025-09-26 15:53:41 +00:00

117 lines
3.9 KiB
Python

import asyncio
import os
import unittest
import threading
import tempfile
import shutil
import functools
import electrum
import electrum.logging
from electrum import constants
from electrum import util
from electrum.logging import Logger
from electrum.wallet import restore_wallet_from_text
# Set this locally to make the test suite run faster.
# If set, unit tests that would normally test functions with multiple implementations,
# will only be run once, using the fastest implementation.
# e.g. libsecp256k1 vs python-ecdsa. pycryptodomex vs pyaes.
FAST_TESTS = False
electrum.logging._configure_stderr_logging(verbosity="*")
electrum.util.AS_LIB_USER_I_WANT_TO_MANAGE_MY_OWN_ASYNCIO_LOOP = True
class ElectrumTestCase(unittest.IsolatedAsyncioTestCase, Logger):
"""Base class for our unit tests."""
TESTNET = False
REGTEST = False
TEST_ANCHOR_CHANNELS = False
# maxDiff = None # for debugging
# some unit tests are modifying globals... so we run sequentially:
_test_lock = threading.Lock()
def __init__(self, *args, **kwargs):
Logger.__init__(self)
unittest.IsolatedAsyncioTestCase.__init__(self, *args, **kwargs)
@classmethod
def setUpClass(cls):
super().setUpClass()
assert not (cls.REGTEST and cls.TESTNET), "regtest and testnet are mutually exclusive"
if cls.REGTEST:
constants.BitcoinRegtest.set_as_network()
elif cls.TESTNET:
constants.BitcoinTestnet.set_as_network()
@classmethod
def tearDownClass(cls):
super().tearDownClass()
if cls.TESTNET or cls.REGTEST:
constants.BitcoinMainnet.set_as_network()
def setUp(self):
have_lock = self._test_lock.acquire(timeout=0.1)
if not have_lock:
# This can happen when trying to run the tests in parallel,
# or if a prior test raised during `setUp` or `asyncSetUp` and never released the lock.
raise Exception("timed out waiting for test_lock")
super().setUp()
self.electrum_path = tempfile.mkdtemp(prefix="electrum-unittest-base-")
assert util._asyncio_event_loop is None, "global event loop already set?!"
async def asyncSetUp(self):
await super().asyncSetUp()
loop = util.get_asyncio_loop()
# IsolatedAsyncioTestCase creates event loops with debug=True, which makes the tests take ~4x time
if not (os.environ.get("PYTHONASYNCIODEBUG") or os.environ.get("PYTHONDEVMODE")):
loop.set_debug(False)
util._asyncio_event_loop = loop
def tearDown(self):
util.callback_mgr.clear_all_callbacks()
shutil.rmtree(self.electrum_path)
super().tearDown()
util._asyncio_event_loop = None # cleared here, at the ~last possible moment. asyncTearDown is too early.
self._test_lock.release()
def as_testnet(func):
"""Function decorator to run a single unit test in testnet mode.
NOTE: this is inherently sequential; tests running in parallel would break things
"""
old_net = constants.net
if asyncio.iscoroutinefunction(func):
async def run_test(*args, **kwargs):
try:
constants.BitcoinTestnet.set_as_network()
return await func(*args, **kwargs)
finally:
constants.net = old_net
else:
def run_test(*args, **kwargs):
try:
constants.BitcoinTestnet.set_as_network()
return func(*args, **kwargs)
finally:
constants.net = old_net
return run_test
@functools.wraps(restore_wallet_from_text)
def restore_wallet_from_text__for_unittest(*args, gap_limit=2, gap_limit_for_change=1, **kwargs):
"""much lower default gap limits (to save compute time)"""
return restore_wallet_from_text(
*args,
gap_limit=gap_limit,
gap_limit_for_change=gap_limit_for_change,
**kwargs,
)