EU-Utility/tests/unit/test_plugin_api.py

477 lines
15 KiB
Python

"""
Unit tests for Plugin API service.
Tests cover:
- Singleton pattern
- API registration and calling
- Service registration (OCR, Screenshot, Log, Audio, Nexus)
- Shared data management
- Utility functions (PED/PEC formatting, DPP calculation)
"""
import pytest
from unittest.mock import MagicMock, patch
from typing import Any
from core.plugin_api import PluginAPI, APIType, APIEndpoint, get_api
@pytest.mark.unit
class TestPluginAPISingleton:
"""Test PluginAPI singleton behavior."""
def test_singleton_instance(self, reset_singletons):
"""Test that PluginAPI is a proper singleton."""
api1 = get_api()
api2 = get_api()
assert api1 is api2
assert isinstance(api1, PluginAPI)
def test_singleton_initialized_once(self, reset_singletons):
"""Test that singleton is initialized only once."""
api1 = get_api()
api1._initialized = True
api1.custom_attr = "test"
api2 = get_api()
assert hasattr(api2, 'custom_attr')
assert api2.custom_attr == "test"
@pytest.mark.unit
class TestAPIRegistration:
"""Test API endpoint registration."""
def test_register_api(self, reset_singletons):
"""Test registering an API endpoint."""
api = get_api()
def handler_func():
return "test result"
endpoint = APIEndpoint(
name="test_api",
api_type=APIType.UTILITY,
description="Test API",
handler=handler_func,
plugin_id="test_plugin"
)
result = api.register_api(endpoint)
assert result is True
assert "test_plugin:test_api" in api.apis
def test_register_api_error_handling(self, reset_singletons):
"""Test API registration error handling."""
api = get_api()
# Create endpoint with bad handler
endpoint = APIEndpoint(
name="bad_api",
api_type=APIType.UTILITY,
description="Bad API",
handler=None, # This will cause issues
plugin_id="test_plugin"
)
# Should not raise, returns True on success
result = api.register_api(endpoint)
assert result is True
def test_unregister_api_single(self, reset_singletons):
"""Test unregistering a single API."""
api = get_api()
endpoint = APIEndpoint(
name="test_api",
api_type=APIType.UTILITY,
description="Test API",
handler=lambda: None,
plugin_id="test_plugin"
)
api.register_api(endpoint)
assert "test_plugin:test_api" in api.apis
api.unregister_api("test_plugin", "test_api")
assert "test_plugin:test_api" not in api.apis
def test_unregister_api_all_for_plugin(self, reset_singletons):
"""Test unregistering all APIs for a plugin."""
api = get_api()
# Register multiple APIs
for i in range(3):
endpoint = APIEndpoint(
name=f"api_{i}",
api_type=APIType.UTILITY,
description=f"API {i}",
handler=lambda: None,
plugin_id="test_plugin"
)
api.register_api(endpoint)
assert len(api.apis) == 3
# Unregister all for plugin
api.unregister_api("test_plugin")
assert len(api.apis) == 0
def test_call_api_success(self, reset_singletons):
"""Test calling a registered API."""
api = get_api()
def handler_func(arg1, arg2):
return f"Result: {arg1}, {arg2}"
endpoint = APIEndpoint(
name="test_api",
api_type=APIType.UTILITY,
description="Test API",
handler=handler_func,
plugin_id="test_plugin"
)
api.register_api(endpoint)
result = api.call_api("test_plugin", "test_api", "hello", "world")
assert result == "Result: hello, world"
def test_call_api_not_found(self, reset_singletons):
"""Test calling a non-existent API."""
api = get_api()
with pytest.raises(ValueError, match="API not found"):
api.call_api("nonexistent", "api")
def test_call_api_error_propagation(self, reset_singletons):
"""Test that API errors are propagated."""
api = get_api()
def error_handler():
raise ValueError("Test error")
endpoint = APIEndpoint(
name="error_api",
api_type=APIType.UTILITY,
description="Error API",
handler=error_handler,
plugin_id="test_plugin"
)
api.register_api(endpoint)
with pytest.raises(ValueError, match="Test error"):
api.call_api("test_plugin", "error_api")
def test_find_apis_by_type(self, reset_singletons):
"""Test finding APIs by type."""
api = get_api()
# Register APIs of different types
endpoint1 = APIEndpoint(
name="ocr_api",
api_type=APIType.OCR,
description="OCR API",
handler=lambda: None,
plugin_id="plugin1"
)
endpoint2 = APIEndpoint(
name="log_api",
api_type=APIType.LOG,
description="Log API",
handler=lambda: None,
plugin_id="plugin2"
)
endpoint3 = APIEndpoint(
name="another_ocr",
api_type=APIType.OCR,
description="Another OCR",
handler=lambda: None,
plugin_id="plugin3"
)
api.register_api(endpoint1)
api.register_api(endpoint2)
api.register_api(endpoint3)
ocr_apis = api.find_apis(APIType.OCR)
assert len(ocr_apis) == 2
all_apis = api.find_apis()
assert len(all_apis) == 3
@pytest.mark.unit
class TestServiceRegistration:
"""Test service registration and access."""
def test_register_ocr_service(self, reset_singletons):
"""Test OCR service registration."""
api = get_api()
mock_ocr = MagicMock()
mock_ocr.return_value = {"text": "test", "confidence": 0.9}
api.register_ocr_service(mock_ocr)
assert api.services['ocr'] is mock_ocr
def test_ocr_capture(self, reset_singletons):
"""Test OCR capture functionality."""
api = get_api()
mock_ocr = MagicMock()
mock_ocr.return_value = {"text": "captured", "confidence": 0.95}
api.register_ocr_service(mock_ocr)
result = api.ocr_capture(region=(0, 0, 100, 100))
assert result["text"] == "captured"
mock_ocr.assert_called_once_with((0, 0, 100, 100))
def test_ocr_capture_no_service(self, reset_singletons):
"""Test OCR capture when service not available."""
api = get_api()
with pytest.raises(RuntimeError, match="OCR service not available"):
api.ocr_capture()
def test_register_screenshot_service(self, reset_singletons):
"""Test screenshot service registration."""
api = get_api()
mock_screenshot = MagicMock()
mock_image = MagicMock()
mock_screenshot.capture.return_value = mock_image
mock_screenshot.capture_region.return_value = mock_image
api.register_screenshot_service(mock_screenshot)
# Test capture_screen
result = api.capture_screen(full_screen=True)
assert result is mock_image
mock_screenshot.capture.assert_called_once_with(full_screen=True)
# Test capture_region
result = api.capture_region(10, 20, 100, 200)
assert result is mock_image
mock_screenshot.capture_region.assert_called_once_with(10, 20, 100, 200)
def test_register_log_service(self, reset_singletons):
"""Test log service registration."""
api = get_api()
mock_log = MagicMock()
mock_log.return_value = ["line1", "line2"]
api.register_log_service(mock_log)
result = api.read_log(lines=10, filter_text="test")
assert result == ["line1", "line2"]
mock_log.assert_called_once_with(10, "test")
def test_register_audio_service(self, reset_singletons):
"""Test audio service registration."""
api = get_api()
mock_audio = MagicMock()
mock_audio.play_sound.return_value = True
mock_audio.get_volume.return_value = 0.8
mock_audio.is_muted.return_value = False
mock_audio.is_available.return_value = True
api.register_audio_service(mock_audio)
# Test play_sound
result = api.play_sound("hof")
assert result is True
mock_audio.play_sound.assert_called_once_with("hof", False)
# Test volume control
api.set_volume(0.5)
mock_audio.set_volume.assert_called_once_with(0.5)
assert api.get_volume() == 0.8
# Test mute
api.mute_audio()
mock_audio.mute.assert_called_once()
api.unmute_audio()
mock_audio.unmute.assert_called_once()
assert api.toggle_mute_audio() == False
assert api.is_audio_muted() == False
assert api.is_audio_available() == True
@pytest.mark.unit
class TestSharedData:
"""Test shared data management."""
def test_get_set_data(self, reset_singletons):
"""Test getting and setting shared data."""
api = get_api()
# Set data
api.set_data("test_key", "test_value")
# Get data
result = api.get_data("test_key")
assert result == "test_value"
def test_get_data_default(self, reset_singletons):
"""Test getting data with default value."""
api = get_api()
result = api.get_data("nonexistent_key", "default")
assert result == "default"
def test_get_data_none_default(self, reset_singletons):
"""Test getting data with None default."""
api = get_api()
result = api.get_data("nonexistent_key")
assert result is None
@pytest.mark.unit
class TestUtilityFunctions:
"""Test utility helper functions."""
def test_format_ped(self, reset_singletons):
"""Test PED formatting."""
api = get_api()
assert api.format_ped(10.5) == "10.50 PED"
assert api.format_ped(0.0) == "0.00 PED"
assert api.format_ped(100.123) == "100.12 PED"
def test_format_pec(self, reset_singletons):
"""Test PEC formatting."""
api = get_api()
assert api.format_pec(50.0) == "50 PEC"
assert api.format_pec(0.0) == "0 PEC"
assert api.format_pec(100.7) == "101 PEC"
def test_calculate_dpp(self, reset_singletons):
"""Test DPP calculation."""
api = get_api()
# Normal case
dpp = api.calculate_dpp(damage=100, ammo=50, decay=0.5)
# ammo_cost = 50 * 0.01 = 0.5 PEC
# total_cost = 0.5 + 0.5 = 1.0 PEC = 0.01 PED
# dpp = 100 / 0.01 = 10000
assert dpp == 10000.0
# Zero damage
assert api.calculate_dpp(damage=0, ammo=50, decay=0.5) == 0.0
# Zero cost
assert api.calculate_dpp(damage=100, ammo=0, decay=0) == 0.0
def test_calculate_markup(self, reset_singletons):
"""Test markup calculation."""
api = get_api()
# Normal case
assert api.calculate_markup(price=110, tt=100) == 110.0
# Zero TT
assert api.calculate_markup(price=110, tt=0) == 0.0
# Negative TT
assert api.calculate_markup(price=110, tt=-10) == 0.0
@pytest.mark.unit
class TestLegacyEventSystem:
"""Test legacy event system (backward compatibility)."""
def test_publish_event(self, reset_singletons):
"""Test publishing legacy events."""
api = get_api()
api.publish_event("test_event", {"key": "value"})
# Check data was stored
event_data = api.get_data("event:test_event")
assert event_data is not None
assert event_data['data'] == {"key": "value"}
assert 'timestamp' in event_data
def test_subscribe_and_receive_legacy(self, reset_singletons):
"""Test legacy event subscription."""
api = get_api()
received = []
def callback(data):
received.append(data)
api.subscribe("test_event", callback)
api.publish_event("test_event", {"message": "hello"})
# Note: Legacy subscription is synchronous
assert len(received) == 1
assert received[0] == {"message": "hello"}
@pytest.mark.unit
class TestTypedEventIntegration:
"""Test typed event system integration."""
def test_publish_typed(self, reset_singletons, fresh_event_bus):
"""Test publishing typed events."""
from core.event_bus import SkillGainEvent
api = get_api()
event = SkillGainEvent(skill_name="Rifle", skill_value=25.0, gain_amount=0.01)
# Should not raise
api.publish_typed(event)
def test_subscribe_typed(self, reset_singletons, fresh_event_bus):
"""Test subscribing to typed events."""
from core.event_bus import SkillGainEvent
api = get_api()
received = []
def handler(event):
received.append(event)
sub_id = api.subscribe_typed(SkillGainEvent, handler)
assert isinstance(sub_id, str)
assert len(sub_id) > 0
def test_get_recent_events(self, reset_singletons, fresh_event_bus):
"""Test getting recent events."""
from core.event_bus import SkillGainEvent
api = get_api()
# Publish some events
for i in range(5):
api.publish_typed(SkillGainEvent(skill_name=f"Skill{i}", skill_value=float(i), gain_amount=0.01))
# Get recent events
events = api.get_recent_events(SkillGainEvent, count=3)
assert len(events) <= 3
def test_get_event_stats(self, reset_singletons, fresh_event_bus):
"""Test getting event statistics."""
from core.event_bus import SkillGainEvent
api = get_api()
# Publish an event
api.publish_typed(SkillGainEvent(skill_name="Rifle", skill_value=25.0, gain_amount=0.01))
stats = api.get_event_stats()
assert 'total_published' in stats
assert 'total_delivered' in stats