import pytest from unittest.mock import MagicMock, patch, ANY import asyncio from src.controllers.measurement_controller import MeasurementController from src.models.device import DeviceStatus from src.models.cell import Cell, CellLimits, MeasureValues from src.services.mqtt_service import InsertedCell import logging @pytest.fixture def mock_services(): i2c_service = MagicMock() http_service = MagicMock() mqtt_service = MagicMock() return i2c_service, http_service, mqtt_service @pytest.fixture def config(): return { 'devices': [ { 'id': 1, 'i2c_address': 0x48, 'num_slots': 2 } ], 'mqtt': { 'subscribe_prefix': 'test/' }, 'i2c': { 'polling_interval_ms': 100 }, 'measurement': { 'min_voltage': 2.5, 'max_voltage': 4.2, 'c_rate': 0.5 } } @pytest.fixture def controller(mock_services, config): i2c_service, http_service, mqtt_service = mock_services return MeasurementController(config, i2c_service, http_service, mqtt_service) @pytest.mark.asyncio async def test_device_polling(controller, mock_services): i2c_service, _, _ = mock_services # Setup mock responses i2c_service.request_status_list.return_value = [DeviceStatus.EMPTY, DeviceStatus.EMPTY] # Start polling await controller.start_polling() await asyncio.sleep(0.2) # Allow some polling cycles await controller.stop_polling() # Verify I2C service was called assert i2c_service.request_status_list.called assert i2c_service.request_status_list.call_count >= 1 @pytest.mark.asyncio async def test_cell_insertion_flow(controller, mock_services): i2c_service, http_service, mqtt_service = mock_services # Setup mock responses http_service.fetch_cell_info.return_value = { 'cell_type': { 'min_voltage': 2.5, 'max_voltage': 4.2, 'capacity': 3000, 'name': 'Test Cell' } } # Simulate cell insertion insertion_info = InsertedCell(device_id=1, slot_id=0, cell_id=123) controller._update_inserted_cell(insertion_info) # Verify HTTP service was called to fetch cell info http_service.fetch_cell_info.assert_called_once_with(123) # Verify cell was inserted with correct parameters device = controller.devices[1] cell = device.slots[0].get_cell() assert cell is not None assert cell.id == 123 assert cell.nom_capacity == 3000 @pytest.mark.asyncio async def test_measurement_cycle(controller, mock_services): i2c_service, _, mqtt_service = mock_services capacity = 3000 # Setup initial state device = controller.devices[1] cell = Cell(123, CellLimits(2.5, 4.2, 1.5), capacity) device.slots[0].insert_cell(cell) # Setup mock responses for state transitions i2c_service.request_status_list.side_effect = [ [DeviceStatus.INSERTED, DeviceStatus.EMPTY], # Initial state [DeviceStatus.MEASURING, DeviceStatus.EMPTY], # Measuring [DeviceStatus.MEASURING, DeviceStatus.EMPTY], # Measuring [DeviceStatus.DONE, DeviceStatus.EMPTY], # Measurement complete ] i2c_service.request_measure_values.return_value = MeasureValues(4.2, 3.6, 1.5) # Start polling await controller.start_polling() await asyncio.sleep(1) # Allow state transitions await controller.stop_polling() # Verify measurement cycle assert i2c_service.send_cell_limits.called assert i2c_service.request_measure_values.called assert mqtt_service.cell_finished.called # Verify health calculation is called device_id, slot_id, cell_id, health, status = mqtt_service.cell_finished.call_args[0] # Verify health calculation and other parameters assert device_id == 1 assert slot_id == 0 assert cell_id == 123 assert health != -1.0 # Verify that health was actually calculated assert status == DeviceStatus.DONE @pytest.mark.asyncio async def test_error_handling(controller, mock_services): i2c_service, _, mqtt_service = mock_services # Setup initial state device = controller.devices[1] cell = Cell(123, CellLimits(2.5, 4.2, 1.5), 3000) device.slots[0].insert_cell(cell) # Simulate error condition i2c_service.request_status_list.return_value = [DeviceStatus.ERROR, DeviceStatus.ERROR] # Start polling await controller.start_polling() await asyncio.sleep(0.5) await controller.stop_polling() # Verify error handling mqtt_service.cell_finished.assert_called_with(1, 0, 123, 0.0, DeviceStatus.ERROR) assert device.slots[0].get_cell() is None # Cell should be removed after error @pytest.mark.asyncio async def test_invalid_status_list(controller, mock_services, caplog): i2c_service, _, _ = mock_services caplog.set_level(logging.ERROR) # Setup mock to return invalid status list i2c_service.request_status_list.return_value = [DeviceStatus.EMPTY] # Too short # Start polling await controller.start_polling() await asyncio.sleep(0.2) # Allow some polling cycles await controller.stop_polling() # Verify error was logged assert "Invalid status list length" in caplog.text # Add pytest.ini file to handle asyncio