Files
templates/python/tests/test_gas_slots.py
T
Andrey 876b58517c feat(set-protocol): Раздели GAS и добавь пакетный сбор регистров
Сохрани адресацию отдельных регистров GAS 0x3 и выдели слотам тип 0x4 с маской Word0. Добавь единое C-ядро GAS_WATCH2, тонкие Python/JNI-порты и проверки общих эталонных байтов.

Совместимость: тип 0x4 больше не обслуживает Modbus Coil. Потребителям PCAN_MSG_MODBUS_COIL и ProtoCanMsgToModbusCoil нужно перейти на GAS_SLOT и отдельный API слотов; регистровый GAS остаётся на 0x3.
2026-10-23 14:14:48 +03:00

64 lines
9.1 KiB
Python

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-кодека отражается стандартным статусом тестов.