fix(tests): fix stale asyncio.sleep patches and missing tarpit guards in service isolation tests
After the ingester._sleep alias fix, three tests in test_service_isolation.py
still patched `decnet.web.ingester.asyncio.sleep` (the old global-singleton
path). The ingester now calls `_sleep` directly, so those patches no longer
controlled the ingester's sleep — the worker looped with real asyncio.sleep
and the tests hung indefinitely.
Also: four API lifespan tests had no tarpit_watcher_worker patch, letting the
real tarpit task start. And test_api_survives_db_init_failure patched
`decnet.web.api.asyncio.sleep` (the singleton) instead of the existing
`_retry_sleep` alias.
Fixes:
- patch("decnet.web.ingester._sleep", ...) in the three ingester tests
- add tarpit_watcher_worker patch to all four api lifespan tests
- patch("decnet.web.api._retry_sleep", ...) in db_init_failure test
This commit is contained in:
@@ -110,7 +110,7 @@ class TestIngesterIsolation:
|
|||||||
raise asyncio.CancelledError()
|
raise asyncio.CancelledError()
|
||||||
|
|
||||||
with patch.dict(os.environ, {"DECNET_INGEST_LOG_FILE": "/tmp/nonexistent-decnet-test.log"}):
|
with patch.dict(os.environ, {"DECNET_INGEST_LOG_FILE": "/tmp/nonexistent-decnet-test.log"}):
|
||||||
with patch("decnet.web.ingester.asyncio.sleep", side_effect=_controlled_sleep):
|
with patch("decnet.web.ingester._sleep", side_effect=_controlled_sleep):
|
||||||
task = asyncio.create_task(log_ingestion_worker(mock_repo))
|
task = asyncio.create_task(log_ingestion_worker(mock_repo))
|
||||||
with pytest.raises(asyncio.CancelledError):
|
with pytest.raises(asyncio.CancelledError):
|
||||||
await task
|
await task
|
||||||
@@ -153,7 +153,7 @@ class TestIngesterIsolation:
|
|||||||
raise asyncio.CancelledError()
|
raise asyncio.CancelledError()
|
||||||
|
|
||||||
with patch.dict(os.environ, {"DECNET_INGEST_LOG_FILE": str(tmp_path / "test.log")}):
|
with patch.dict(os.environ, {"DECNET_INGEST_LOG_FILE": str(tmp_path / "test.log")}):
|
||||||
with patch("decnet.web.ingester.asyncio.sleep", side_effect=_controlled_sleep):
|
with patch("decnet.web.ingester._sleep", side_effect=_controlled_sleep):
|
||||||
task = asyncio.create_task(log_ingestion_worker(mock_repo))
|
task = asyncio.create_task(log_ingestion_worker(mock_repo))
|
||||||
with pytest.raises(asyncio.CancelledError):
|
with pytest.raises(asyncio.CancelledError):
|
||||||
await task
|
await task
|
||||||
@@ -345,9 +345,11 @@ class TestApiLifespanIsolation:
|
|||||||
side_effect=Exception("attacker exploded")):
|
side_effect=Exception("attacker exploded")):
|
||||||
with patch("decnet.sniffer.sniffer_worker",
|
with patch("decnet.sniffer.sniffer_worker",
|
||||||
side_effect=Exception("sniffer exploded")):
|
side_effect=Exception("sniffer exploded")):
|
||||||
# API should still start
|
with patch("decnet.web.api.tarpit_watcher_worker",
|
||||||
async with lifespan(mock_app):
|
return_value=asyncio.sleep(0)):
|
||||||
mock_repo.initialize.assert_awaited_once()
|
# API should still start
|
||||||
|
async with lifespan(mock_app):
|
||||||
|
mock_repo.initialize.assert_awaited_once()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_api_survives_db_init_failure(self):
|
async def test_api_survives_db_init_failure(self):
|
||||||
@@ -359,13 +361,15 @@ class TestApiLifespanIsolation:
|
|||||||
mock_repo.initialize = AsyncMock(side_effect=Exception("DB locked"))
|
mock_repo.initialize = AsyncMock(side_effect=Exception("DB locked"))
|
||||||
|
|
||||||
with patch("decnet.web.api.repo", mock_repo):
|
with patch("decnet.web.api.repo", mock_repo):
|
||||||
with patch("decnet.web.api.asyncio.sleep", new_callable=AsyncMock):
|
with patch("decnet.web.api._retry_sleep", new_callable=AsyncMock):
|
||||||
with patch("decnet.web.api.log_ingestion_worker", return_value=asyncio.sleep(0)):
|
with patch("decnet.web.api.log_ingestion_worker", return_value=asyncio.sleep(0)):
|
||||||
with patch("decnet.web.api.log_collector_worker", return_value=asyncio.sleep(0)):
|
with patch("decnet.web.api.log_collector_worker", return_value=asyncio.sleep(0)):
|
||||||
with patch("decnet.web.api.attacker_profile_worker", return_value=asyncio.sleep(0)):
|
with patch("decnet.web.api.attacker_profile_worker", return_value=asyncio.sleep(0)):
|
||||||
async with lifespan(mock_app):
|
with patch("decnet.web.api.tarpit_watcher_worker",
|
||||||
# DB init failed 5 times but API is running
|
return_value=asyncio.sleep(0)):
|
||||||
assert mock_repo.initialize.await_count == 5
|
async with lifespan(mock_app):
|
||||||
|
# DB init failed 5 times but API is running
|
||||||
|
assert mock_repo.initialize.await_count == 5
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_api_survives_sniffer_import_failure(self):
|
async def test_api_survives_sniffer_import_failure(self):
|
||||||
@@ -376,22 +380,23 @@ class TestApiLifespanIsolation:
|
|||||||
mock_repo = MagicMock()
|
mock_repo = MagicMock()
|
||||||
mock_repo.initialize = AsyncMock()
|
mock_repo.initialize = AsyncMock()
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
real_import = builtins.__import__
|
||||||
|
|
||||||
|
def _mock_import(name, *args, **kwargs):
|
||||||
|
if name == "decnet.sniffer":
|
||||||
|
raise ImportError("No module named 'scapy'")
|
||||||
|
return real_import(name, *args, **kwargs)
|
||||||
|
|
||||||
with patch("decnet.web.api.repo", mock_repo):
|
with patch("decnet.web.api.repo", mock_repo):
|
||||||
with patch("decnet.web.api.log_ingestion_worker", return_value=asyncio.sleep(0)):
|
with patch("decnet.web.api.log_ingestion_worker", return_value=asyncio.sleep(0)):
|
||||||
with patch("decnet.web.api.log_collector_worker", return_value=asyncio.sleep(0)):
|
with patch("decnet.web.api.log_collector_worker", return_value=asyncio.sleep(0)):
|
||||||
with patch("decnet.web.api.attacker_profile_worker", return_value=asyncio.sleep(0)):
|
with patch("decnet.web.api.attacker_profile_worker", return_value=asyncio.sleep(0)):
|
||||||
# Simulate sniffer import failure
|
with patch("decnet.web.api.tarpit_watcher_worker",
|
||||||
import builtins
|
return_value=asyncio.sleep(0)):
|
||||||
real_import = builtins.__import__
|
with patch("builtins.__import__", side_effect=_mock_import):
|
||||||
|
async with lifespan(mock_app):
|
||||||
def _mock_import(name, *args, **kwargs):
|
mock_repo.initialize.assert_awaited_once()
|
||||||
if name == "decnet.sniffer":
|
|
||||||
raise ImportError("No module named 'scapy'")
|
|
||||||
return real_import(name, *args, **kwargs)
|
|
||||||
|
|
||||||
with patch("builtins.__import__", side_effect=_mock_import):
|
|
||||||
async with lifespan(mock_app):
|
|
||||||
mock_repo.initialize.assert_awaited_once()
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_shutdown_handles_already_dead_tasks(self):
|
async def test_shutdown_handles_already_dead_tasks(self):
|
||||||
@@ -410,10 +415,12 @@ class TestApiLifespanIsolation:
|
|||||||
with patch("decnet.web.api.log_ingestion_worker", side_effect=_instant_worker):
|
with patch("decnet.web.api.log_ingestion_worker", side_effect=_instant_worker):
|
||||||
with patch("decnet.web.api.log_collector_worker", side_effect=_instant_worker):
|
with patch("decnet.web.api.log_collector_worker", side_effect=_instant_worker):
|
||||||
with patch("decnet.web.api.attacker_profile_worker", side_effect=_instant_worker):
|
with patch("decnet.web.api.attacker_profile_worker", side_effect=_instant_worker):
|
||||||
async with lifespan(mock_app):
|
with patch("decnet.web.api.tarpit_watcher_worker",
|
||||||
# Let tasks finish
|
side_effect=_instant_worker):
|
||||||
await asyncio.sleep(0.05)
|
async with lifespan(mock_app):
|
||||||
# Shutdown should handle already-done tasks gracefully
|
# Let tasks finish
|
||||||
|
await asyncio.sleep(0.05)
|
||||||
|
# Shutdown should handle already-done tasks gracefully
|
||||||
|
|
||||||
|
|
||||||
# ─── Cross-service cascade tests ────────────────────────────────────────────
|
# ─── Cross-service cascade tests ────────────────────────────────────────────
|
||||||
@@ -442,7 +449,7 @@ class TestCascadeIsolation:
|
|||||||
raise asyncio.CancelledError()
|
raise asyncio.CancelledError()
|
||||||
|
|
||||||
with patch.dict(os.environ, {"DECNET_INGEST_LOG_FILE": str(tmp_path / "cascade.log")}):
|
with patch.dict(os.environ, {"DECNET_INGEST_LOG_FILE": str(tmp_path / "cascade.log")}):
|
||||||
with patch("decnet.web.ingester.asyncio.sleep", side_effect=_controlled_sleep):
|
with patch("decnet.web.ingester._sleep", side_effect=_controlled_sleep):
|
||||||
task = asyncio.create_task(log_ingestion_worker(mock_repo))
|
task = asyncio.create_task(log_ingestion_worker(mock_repo))
|
||||||
with pytest.raises(asyncio.CancelledError):
|
with pytest.raises(asyncio.CancelledError):
|
||||||
await task
|
await task
|
||||||
|
|||||||
Reference in New Issue
Block a user