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 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
516 lines
19 KiB
Python
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()
|