Files
aitbc/apps/blockchain-node/tests/test_gossip_network.py
aitbc e60cc3226c
Some checks failed
API Endpoint Tests / test-api-endpoints (push) Successful in 9s
Blockchain Synchronization Verification / sync-verification (push) Failing after 1s
CLI Tests / test-cli (push) Failing after 3s
Documentation Validation / validate-docs (push) Successful in 6s
Documentation Validation / validate-policies-strict (push) Successful in 2s
Integration Tests / test-service-integration (push) Successful in 40s
Multi-Node Blockchain Health Monitoring / health-check (push) Successful in 1s
P2P Network Verification / p2p-verification (push) Successful in 2s
Production Tests / Production Integration Tests (push) Successful in 21s
Python Tests / test-python (push) Successful in 13s
Security Scanning / security-scan (push) Failing after 46s
Smart Contract Tests / test-solidity (map[name:aitbc-token path:packages/solidity/aitbc-token]) (push) Successful in 17s
Smart Contract Tests / lint-solidity (push) Successful in 10s
Add sys import to test files and remove obsolete integration tests
- Add sys import to 29 test files across agent-coordinator, blockchain-event-bridge, blockchain-node, and coordinator-api
- Remove apps/blockchain-event-bridge/tests/test_integration.py (obsolete bridge integration tests)
- Remove apps/coordinator-api/tests/test_integration.py (obsolete API integration tests)
- Implement GPU registration in marketplace_gpu.py with GPURegistry model persistence
2026-04-23 16:43:17 +02:00

516 lines
19 KiB
Python

"""Test suite for Gossip Network - Message broadcasting and network topology."""
from __future__ import annotations
import sys
import pytest
import asyncio
from typing import Generator, Any
from unittest.mock import AsyncMock, patch
from aitbc_chain.gossip.broker import (
GossipBackend,
InMemoryGossipBackend,
BroadcastGossipBackend,
TopicSubscription
)
class TestInMemoryGossipBackend:
"""Test in-memory gossip backend functionality."""
@pytest.fixture
def backend(self) -> InMemoryGossipBackend:
"""Create an in-memory gossip backend."""
return InMemoryGossipBackend()
@pytest.mark.asyncio
async def test_backend_initialization(self, backend: InMemoryGossipBackend) -> None:
"""Test backend initialization."""
assert backend._topics == {}
assert backend._lock is not None
@pytest.mark.asyncio
async def test_publish_no_subscribers(self, backend: InMemoryGossipBackend) -> None:
"""Test publishing when no subscribers exist."""
# Should not raise an exception
await backend.publish("test_topic", {"message": "test"})
# Topics should remain empty
assert backend._topics == {}
@pytest.mark.asyncio
async def test_subscribe_single_subscriber(self, backend: InMemoryGossipBackend) -> None:
"""Test subscribing to a topic."""
subscription = await backend.subscribe("test_topic")
assert subscription.topic == "test_topic"
assert subscription.queue is not None
assert subscription._unsubscribe is not None
# Topic should be registered
assert "test_topic" in backend._topics
assert len(backend._topics["test_topic"]) == 1
@pytest.mark.asyncio
async def test_subscribe_multiple_subscribers(self, backend: InMemoryGossipBackend) -> None:
"""Test multiple subscribers to the same topic."""
sub1 = await backend.subscribe("test_topic")
sub2 = await backend.subscribe("test_topic")
sub3 = await backend.subscribe("test_topic")
# All should be registered
assert len(backend._topics["test_topic"]) == 3
assert sub1.queue in backend._topics["test_topic"]
assert sub2.queue in backend._topics["test_topic"]
assert sub3.queue in backend._topics["test_topic"]
@pytest.mark.asyncio
async def test_publish_to_single_subscriber(self, backend: InMemoryGossipBackend) -> None:
"""Test publishing to a single subscriber."""
subscription = await backend.subscribe("test_topic")
message = {"data": "test_message"}
await backend.publish("test_topic", message)
# Message should be in queue
received = await subscription.queue.get()
assert received == message
@pytest.mark.asyncio
async def test_publish_to_multiple_subscribers(self, backend: InMemoryGossipBackend) -> None:
"""Test publishing to multiple subscribers."""
sub1 = await backend.subscribe("test_topic")
sub2 = await backend.subscribe("test_topic")
sub3 = await backend.subscribe("test_topic")
message = {"data": "broadcast_message"}
await backend.publish("test_topic", message)
# All subscribers should receive the message
received1 = await sub1.queue.get()
received2 = await sub2.queue.get()
received3 = await sub3.queue.get()
assert received1 == message
assert received2 == message
assert received3 == message
@pytest.mark.asyncio
async def test_publish_different_topics(self, backend: InMemoryGossipBackend) -> None:
"""Test publishing to different topics."""
sub1 = await backend.subscribe("topic1")
sub2 = await backend.subscribe("topic2")
message1 = {"topic": "topic1", "data": "message1"}
message2 = {"topic": "topic2", "data": "message2"}
await backend.publish("topic1", message1)
await backend.publish("topic2", message2)
# Each subscriber should only receive their topic's message
received1 = await sub1.queue.get()
received2 = await sub2.queue.get()
assert received1 == message1
assert received2 == message2
# Queues should be empty now
assert sub1.queue.empty()
assert sub2.queue.empty()
@pytest.mark.asyncio
async def test_unsubscribe_single_subscriber(self, backend: InMemoryGossipBackend) -> None:
"""Test unsubscribing a single subscriber."""
subscription = await backend.subscribe("test_topic")
# Verify subscription exists
assert len(backend._topics["test_topic"]) == 1
# Unsubscribe
subscription.close()
# Give time for async cleanup
await asyncio.sleep(0.01)
# Topic should be removed when no subscribers left
assert "test_topic" not in backend._topics
@pytest.mark.asyncio
async def test_unsubscribe_multiple_subscribers(self, backend: InMemoryGossipBackend) -> None:
"""Test unsubscribing when multiple subscribers exist."""
sub1 = await backend.subscribe("test_topic")
sub2 = await backend.subscribe("test_topic")
sub3 = await backend.subscribe("test_topic")
# Verify all subscriptions exist
assert len(backend._topics["test_topic"]) == 3
# Unsubscribe one
sub2.close()
await asyncio.sleep(0.01)
# Should still have 2 subscribers
assert len(backend._topics["test_topic"]) == 2
assert sub1.queue in backend._topics["test_topic"]
assert sub3.queue in backend._topics["test_topic"]
# Unsubscribe another
sub1.close()
await asyncio.sleep(0.01)
# Should still have 1 subscriber
assert len(backend._topics["test_topic"]) == 1
assert sub3.queue in backend._topics["test_topic"]
# Unsubscribe last one
sub3.close()
await asyncio.sleep(0.01)
# Topic should be removed
assert "test_topic" not in backend._topics
@pytest.mark.asyncio
async def test_queue_size_limit(self, backend: InMemoryGossipBackend) -> None:
"""Test queue size limits."""
# Create subscription with small queue
subscription = await backend.subscribe("test_topic", max_queue_size=2)
# Fill queue to capacity
await backend.publish("test_topic", "message1")
await backend.publish("test_topic", "message2")
# Queue should be full
assert subscription.queue.full()
# Third message should be handled (depends on queue behavior)
# This test verifies the queue limit is respected
await backend.publish("test_topic", "message3")
# Should be able to get first two messages
assert await subscription.queue.get() == "message1"
assert await subscription.queue.get() == "message2"
@pytest.mark.asyncio
async def test_shutdown(self, backend: InMemoryGossipBackend) -> None:
"""Test backend shutdown."""
# Add some subscriptions
sub1 = await backend.subscribe("topic1")
sub2 = await backend.subscribe("topic2")
sub3 = await backend.subscribe("topic2")
assert len(backend._topics) == 2
# Shutdown
await backend.shutdown()
# All topics should be cleared
assert len(backend._topics) == 0
@pytest.mark.asyncio
async def test_concurrent_publish_subscribe(self, backend: InMemoryGossipBackend) -> None:
"""Test concurrent publishing and subscribing."""
subscriptions = []
# Create multiple subscribers
for i in range(5):
sub = await backend.subscribe("concurrent_topic")
subscriptions.append(sub)
# Publish messages concurrently
messages = [f"message_{i}" for i in range(10)]
publish_tasks = [
backend.publish("concurrent_topic", msg)
for msg in messages
]
await asyncio.gather(*publish_tasks)
# Each subscriber should receive all messages
for sub in subscriptions:
received_messages = []
# Collect all messages (with timeout)
for _ in range(10):
try:
msg = await asyncio.wait_for(sub.queue.get(), timeout=0.1)
received_messages.append(msg)
except asyncio.TimeoutError:
break
assert len(received_messages) == 10
assert set(received_messages) == set(messages)
class TestTopicSubscription:
"""Test topic subscription functionality."""
@pytest.mark.asyncio
async def test_subscription_iteration(self) -> None:
"""Test iterating over subscription messages."""
backend = InMemoryGossipBackend()
subscription = await backend.subscribe("iter_test")
# Publish some messages
messages = ["msg1", "msg2", "msg3"]
for msg in messages:
await backend.publish("iter_test", msg)
# Iterate through messages
received = []
async for message in subscription:
received.append(message)
if len(received) >= len(messages):
break
assert received == messages
@pytest.mark.asyncio
async def test_subscription_context_manager(self) -> None:
"""Test subscription as context manager."""
backend = InMemoryGossipBackend()
async with await backend.subscribe("context_test") as subscription:
assert subscription.topic == "context_test"
assert subscription.queue is not None
# After context exit, subscription should be cleaned up
await asyncio.sleep(0.01)
assert "context_test" not in backend._topics
class TestBroadcastGossipBackend:
"""Test broadcast gossip backend functionality."""
@pytest.fixture
def mock_broadcast(self) -> AsyncMock:
"""Create a mock broadcast instance."""
broadcast = AsyncMock()
broadcast.connect = AsyncMock()
broadcast.publish = AsyncMock()
broadcast.subscribe = AsyncMock()
return broadcast
@pytest.mark.asyncio
async def test_backend_initialization(self, mock_broadcast: AsyncMock) -> None:
"""Test backend initialization with mock broadcast."""
with patch('aitbc_chain.gossip.broker.Broadcast', return_value=mock_broadcast):
backend = BroadcastGossipBackend("redis://localhost:6379")
assert backend._broadcast == mock_broadcast
assert backend._tasks == set()
assert backend._lock is not None
assert backend._running is False
@pytest.mark.asyncio
async def test_start_stop(self, mock_broadcast: AsyncMock) -> None:
"""Test starting and stopping the backend."""
with patch('aitbc_chain.gossip.broker.Broadcast', return_value=mock_broadcast):
backend = BroadcastGossipBackend("redis://localhost:6379")
# Start
await backend.start()
assert backend._running is True
mock_broadcast.connect.assert_called_once()
# Stop (shutdown)
await backend.shutdown()
assert backend._running is False
@pytest.mark.asyncio
async def test_publish_when_not_running(self, mock_broadcast: AsyncMock) -> None:
"""Test publishing when backend is not started."""
with patch('aitbc_chain.gossip.broker.Broadcast', return_value=mock_broadcast):
backend = BroadcastGossipBackend("redis://localhost:6379")
# Should raise error when not running
with pytest.raises(RuntimeError, match="Broadcast backend not started"):
await backend.publish("test_topic", {"message": "test"})
@pytest.mark.asyncio
async def test_publish_when_running(self, mock_broadcast: AsyncMock) -> None:
"""Test publishing when backend is running."""
with patch('aitbc_chain.gossip.broker.Broadcast', return_value=mock_broadcast):
backend = BroadcastGossipBackend("redis://localhost:6379")
# Start backend
await backend.start()
# Publish message
message = {"data": "test_message"}
await backend.publish("test_topic", message)
# Should call broadcast.publish
mock_broadcast.publish.assert_called_once()
# Clean up
await backend.shutdown()
@pytest.mark.asyncio
async def test_subscribe_when_not_running(self, mock_broadcast: AsyncMock) -> None:
"""Test subscribing when backend is not started."""
with patch('aitbc_chain.gossip.broker.Broadcast', return_value=mock_broadcast):
backend = BroadcastGossipBackend("redis://localhost:6379")
# Should raise error when not running
with pytest.raises(RuntimeError, match="Broadcast backend not started"):
await backend.subscribe("test_topic")
@pytest.mark.asyncio
async def test_in_process_broadcast_fallback(self) -> None:
"""Test fallback to in-process broadcast when Broadcast is missing."""
with patch('aitbc_chain.gossip.broker.Broadcast', None):
backend = BroadcastGossipBackend("redis://localhost:6379")
# Should use _InProcessBroadcast
assert hasattr(backend._broadcast, 'publish')
assert hasattr(backend._broadcast, 'subscribe')
class TestGossipMetrics:
"""Test gossip network metrics."""
@pytest.mark.asyncio
async def test_publication_metrics(self) -> None:
"""Test that publication metrics are recorded."""
with patch('aitbc_chain.gossip.broker.metrics_registry') as mock_metrics:
backend = InMemoryGossipBackend()
await backend.subscribe("test_topic")
# Publish message
await backend.publish("test_topic", {"data": "test"})
# Should increment metrics
mock_metrics.increment.assert_called()
# Check specific calls
calls = mock_metrics.increment.call_args_list
assert any("gossip_publications_total" in str(call) for call in calls)
assert any("gossip_publications_topic_test_topic" in str(call) for call in calls)
@pytest.mark.asyncio
async def test_queue_metrics(self) -> None:
"""Test that queue metrics are recorded."""
with patch('aitbc_chain.gossip.broker.metrics_registry') as mock_metrics:
backend = InMemoryGossipBackend()
await backend.subscribe("test_topic")
# Publish message
await backend.publish("test_topic", {"data": "test"})
# Should set gauge for queue size
mock_metrics.set_gauge.assert_called()
# Check specific calls
calls = mock_metrics.set_gauge.call_args_list
assert any("gossip_queue_size_test_topic" in str(call) for call in calls)
@pytest.mark.asyncio
async def test_subscriber_metrics(self) -> None:
"""Test that subscriber metrics are recorded."""
with patch('aitbc_chain.gossip.broker.metrics_registry') as mock_metrics:
backend = InMemoryGossipBackend()
# Add multiple subscribers
await backend.subscribe("topic1")
await backend.subscribe("topic1")
await backend.subscribe("topic2")
# Should update subscriber metrics
mock_metrics.set_gauge.assert_called()
# Check for subscriber count metric
calls = mock_metrics.set_gauge.call_args_list
assert any("gossip_subscribers_total" in str(call) for call in calls)
class TestGossipIntegration:
"""Test gossip network integration scenarios."""
@pytest.mark.asyncio
async def test_multi_topic_broadcast(self) -> None:
"""Test broadcasting across multiple topics."""
backend = InMemoryGossipBackend()
# Create subscribers for different topics
block_sub = await backend.subscribe("blocks")
tx_sub = await backend.subscribe("transactions")
peer_sub = await backend.subscribe("peers")
# Publish different message types
block_msg = {"type": "block", "height": 100, "hash": "0xabc"}
tx_msg = {"type": "transaction", "hash": "0xdef", "amount": 1000}
peer_msg = {"type": "peer", "address": "node1.example.com"}
await backend.publish("blocks", block_msg)
await backend.publish("transactions", tx_msg)
await backend.publish("peers", peer_msg)
# Each subscriber should receive only their topic's message
assert await block_sub.queue.get() == block_msg
assert await tx_sub.queue.get() == tx_msg
assert await peer_sub.queue.get() == peer_msg
@pytest.mark.asyncio
async def test_high_volume_messaging(self) -> None:
"""Test handling high volume messages."""
backend = InMemoryGossipBackend()
subscription = await backend.subscribe("high_volume")
# Send many messages
message_count = 100
messages = [f"message_{i}" for i in range(message_count)]
# Publish all messages
for msg in messages:
await backend.publish("high_volume", msg)
# Receive all messages
received = []
for _ in range(message_count):
msg = await subscription.queue.get()
received.append(msg)
assert len(received) == message_count
assert set(received) == set(messages)
@pytest.mark.asyncio
async def test_dynamic_subscriber_management(self) -> None:
"""Test adding and removing subscribers dynamically."""
backend = InMemoryGossipBackend()
# Start with some subscribers
sub1 = await backend.subscribe("dynamic")
sub2 = await backend.subscribe("dynamic")
# Send message
await backend.publish("dynamic", "msg1")
# Add new subscriber
sub3 = await backend.subscribe("dynamic")
# Send another message
await backend.publish("dynamic", "msg2")
# Remove first subscriber
sub1.close()
await asyncio.sleep(0.01)
# Send final message
await backend.publish("dynamic", "msg3")
# Check message distribution
# sub1 should only receive msg1
assert await sub1.queue.get() == "msg1"
assert sub1.queue.empty()
# sub2 should receive all messages
assert await sub2.queue.get() == "msg1"
assert await sub2.queue.get() == "msg2"
assert await sub2.queue.get() == "msg3"
# sub3 should receive msg2 and msg3
assert await sub3.queue.get() == "msg2"
assert await sub3.queue.get() == "msg3"
assert sub3.queue.empty()