import json # Общие машинные эталоны независимы от клиентского построителя кадра. from pathlib import Path # Находим спецификацию относительно репозитория. import unittest # Проверяем реальные ctypes-привязки без заглушки протокольного ядра. from protocan.gas_slots import GasSlots # Все правила wire-формата выполняет C, а не тестовая модель. from protocan import protocan as pc # Проверяем штатный CAN-декодер, который вызывают GUI и журналы. class GasSlotTests(unittest.TestCase): # Интеграционные проверки обоих типов GAS и границ адресного API. def setUp(self): # Каждая проверка использует настоящую собранную shared-библиотеку. self.codec = GasSlots() # Отсутствующая библиотека не заменяется Python fallback. def test_vectors(self): # Проверяем ID, маску и три значения по нормативным независимым байтам. path = Path(__file__).resolve().parents[2] / 'doc/setcan/protocan/examples/gas-slot-vectors.json' # Те же примеры опубликованы в документации. vectors = json.loads(path.read_text(encoding='utf-8')) # UTF-8 сохраняет названия и описание профиля. for item in vectors['vectors']: # Охватываем обе группы адресов и неполный последний слот PERIPH. words = item.get('words', vectors['words']) # Последний слот имеет только два действующих значения. expected = item.get('payload_hex', vectors['payload_hex']) # Его отдельный эталон не включает несуществующее третье значение. can_id, data = self.codec.frame(item['mode'], item['local_slot']) # Пустой запрос не несёт служебной маски. self.assertEqual((can_id, data), (int(item['request_id'], 16), b'')) # Тип 0x4 и адрес должны совпасть с фиксированным эталоном. reply, payload = self.codec.frame(item['mode'], item['local_slot'], words, route=1) # Ответ явно содержит маску достоверности. self.assertEqual(reply, int(item['response_id'], 16)) # Направление не меняет узел или слот. self.assertEqual(payload.hex().upper(), expected) # Проверяем все восемь октетов, включая расположение Word0. self.assertEqual(self.codec.decode(reply, payload), (item['mode'], item['local_slot'], tuple(words))) # Полный round-trip сохраняет маску отдельно от трёх значений. def test_invalid(self): # Неверные координаты, маски и длины не должны стать другой командой. for mode, slot in ((32, 0), (5, 67), (-1, 0), (2**32, 0)): # Верхняя граница страницы после маски — LocalSlot 66. with self.assertRaises(ValueError): self.codec.frame(mode, slot) # Ошибка не маскируется до допустимого адреса. for payload in (b'\0', b'\0' * 6, b'\0' * 9, b'\0' * 8): # В том числе запрещена полная запись с пустой маской. with self.assertRaises(ValueError): self.codec.decode(0x19540500, payload) # Точный размер и Word0 проверяет C. for mask in (0, 1, 0x10, 0xFFFF): # Бит 0 и старшие биты не адресуют данные слота. with self.assertRaises(ValueError): self.codec.frame(5, 0, [mask, 1, 2, 3]) # Не заменяем отвергнутую маску полной записью. with self.assertRaises(ValueError): self.codec.frame(5, 66, [14, 1, 2, 3]) # Третье значение неполного слота не существует. with self.assertRaises(ValueError): self.codec.frame(5, 0, [2, 65536, 0, 0]) # Переполнение u16 не превращается в допустимый ноль. can_id, payload = self.codec.frame(5, 0, [4, 0, 0x1234, 0], route=1) # Корректный частичный ответ для проверки транспортных флагов. with self.assertRaises(ValueError): self.codec.decode(can_id, payload, flags=3) # RTR отвергается до обращения к provider. def test_types_are_unambiguous(self): # Один и тот же payload получает разный смысл только по номеру типа. can_id, payload = self.codec.frame(5, 0, [4, 0, 0x1234, 0], route=1) # Word2 выбран, Word1/Word3 передаются в фиксированных позициях и игнорируются. slot = pc.decode(can_id, payload) # Никакого параметра gas_profile для типа 0x4 больше нет. self.assertIn('slot=0x0500', slot.summary) # Адрес показывается в единицах слота. self.assertIn('mask=0x0004', slot.summary) # В CAN-журнале видна частичная запись. self.assertIsNone(slot.registers) # Старые потребители не получают фиктивный последовательный список регистров. self.assertTrue(any('Word2' in name for name, value in slot.fields)) # Только выбранная позиция отображается как достоверное значение. self.assertFalse(any('Word1' in name or 'Word3' in name for name, value in slot.fields)) # Неотмеченные поля не публикуются как измерения. register = pc.decode(0x19530500, payload) # Тип 0x3 всегда интерпретируется как обычные последовательные слова. self.assertEqual(register.registers, [(0x0500, 4), (0x0501, 0), (0x0502, 0x1234), (0x0503, 0)]) # Нулевое слово payload не является маской регистрового GAS. self.assertTrue(pc.decode(can_id, payload + b'\0').warnings) # Девятый октет не теряется до проверки слотного формата. overflow = pc.decode(0x1953FFFF, b'\x01\x00\x02\x00') # По максимальному адресу нельзя разместить два последовательных регистра. self.assertTrue(overflow.warnings) # Ошибка диапазона должна быть видна в журнале. self.assertIsNone(overflow.registers) # Некорректный адрес 0x10000 не становится источником тренда. def test_generic_address_and_analyzer(self): # Тип 0x4 должен работать также за пределами PERIPH и его HEX-страниц. can_id, payload = self.codec.frame_address(7, 14, 0xFFFF, [2, 0x5678, 0xDEAD, 0], route=1) # Неотмеченное значение сохраняется как обычный заполнитель и не является командой записи. self.assertEqual(can_id, 0x1FE4FFFF) # Полный адрес FFFF принадлежит отдельному инвертору 7/E. self.assertEqual(self.codec.decode_address(can_id, payload), (2, 0x5678, 0xDEAD, 0)) # Общий декодер проверяет маску, не навязывая страницу mode. self.assertIn('slot=0xFFFF', pc.decode(can_id, payload).summary) # Штатный GUI-декодер использует общий адресный API. with self.assertRaises(ValueError): self.codec.frame_address(7, 14, 0x10000) # Не допускаем оборачивания в слот ноль. from logic_analyzer.decoders.set_can import legacy # Проверяем тот же публичный путь анализа аппаратного захвата. decoded = legacy(can_id, payload) # Анализатор вызывает общий C-кодек слотного формата. self.assertEqual(decoded['slot_words'], [(1, 0x5678)]) # Служебная маска и невыбранные слова не становятся данными анализатора. self.assertNotIn('registers', decoded) # Не смешиваем координаты слота с плоским регистровым адресом. if __name__ == '__main__': # Поддерживаем прямой запуск без pytest. unittest.main() # Любой отказ native-кодека отражается стандартным статусом тестов.