Files
templates/python/tests/test_modbus_unified.py
T

39 lines
5.8 KiB
Python

import unittest # Стандартный тестовый раннер проверяет wire-контракт без зависимости от GUI.
from protocan import protocan as pc # Используем публичный построитель и декодер CAN-сообщений.
from logic_analyzer.decoders.set_can import legacy # Тот же формат должен распознаваться при анализе аппаратного захвата.
class ModbusTests(unittest.TestCase): # Проверяем объединение банков, битовую упаковку и отбраковку неверных окон.
def test_functions_and_counts(self): # Все шестнадцать сочетаний должны сохранять функцию, количество и значения.
for function in range(1, 5): # Номера 1…4 соответствуют Coil, Discrete, Holding и Input.
for count in range(1, 5): # Количество ограничено 1…4 независимо от размера элемента банка.
body = pc.merge_modbus(0x123, count, function) # Адрес выбран так, чтобы старшие биты отличались от младших служебных полей.
self.assertEqual(body, 0x1230 | ((function - 1) << 2) | (count - 1)) # Эталон явно задаёт адрес [15:4], функцию [3:2] и количество [1:0].
self.assertEqual(pc.split_modbus(body), (0x123, count, function)) # Обратное преобразование возвращает физические значения 1…4.
values = [index % 2 for index in range(count)] if function <= 2 else [0x1234 + index for index in range(count)] # Битовые и регистровые банки содержат различимые последовательности.
data = bytes([sum(value << index for index, value in enumerate(values))]) if function <= 2 else b''.join(value.to_bytes(2, 'little') for value in values) # Биты начинаются с младшего, слова передаются little-endian.
identifier = pc.ProtoCanId.build(1, 1, 7, 3, pc.MsgType.MODBUS, body) # Каждый ответ имеет единый MsgType=0x5 и направление от устройства.
decoded = pc.decode(identifier, data) # Проверяем штатный путь CAN-журнала приложения.
self.assertFalse(decoded.warnings, decoded.warnings) # Допустимое окно не должно сопровождаться предупреждениями.
self.assertEqual(decoded.registers, list(zip(range(0x123, 0x123 + count), values))) # Для каждого бита и слова публикуется точный адрес.
captured = legacy(identifier, data) # Проверяем согласованность анализатора захватов с протокольной библиотекой.
self.assertEqual((captured['function'], captured['count'], captured['registers']), (function, count, decoded.registers)) # Оба публичных пути возвращают одинаковые физические значения.
def test_invalid_inputs_and_payloads(self): # Ошибки должны отклоняться до превращения в другой допустимый wire-код.
for args in ((0, 0, 1), (0, 5, 1), (0, 1, 0), (0, 1, 5), (-1, 1, 1), (4096, 1, 1), (4095, 2, 4)): # Охватываем обе границы полей и переполнение окна.
with self.assertRaises(ValueError): pc.merge_modbus(*args) # Неверные значения не маскируются и не оборачиваются.
self.assertEqual(pc.merge_modbus(4095, 1, 4), 0xFFFC) # Последний адрес остаётся доступным для одного регистра Input.
for body, data in ((0x1233, b'\x10'), (0x123B, b'\x01\x02'), (0xFFFF, b'\0' * 8), (0x123B, b'\0' * 9)): # Проверяем лишний бит, короткие данные, выход окна за 0xFFF и запрет молчаливой обрезки девятого октета.
decoded = pc.decode(0x1F350000 | body, data) # Формируем ответ устройства с единым типом 0x5.
self.assertTrue(decoded.warnings) # Каждая ошибка должна быть видима в журнале.
self.assertFalse(decoded.registers) # Некорректный payload не становится источником значений для тренда.
with self.assertRaises(ValueError): legacy(0x1F350000 | body, data) # Анализатор захвата использует те же проверки.
self.assertFalse(pc.decode(0x1735123F, b'').warnings) # Запрос ПМ с функцией 4 и количеством 4 не содержит данных.
self.assertTrue(pc.decode(0x1F35123F, b'').warnings) # Пустой ответ прибора является ошибкой протокола.
for msg_type in (6, 7): # Бывшие отдельные типы Modbus освобождены.
self.assertTrue(pc.decode(0x1F300000 | (msg_type << 16), b'').warnings) # Декодер не трактует старые MsgType как единый Modbus.
if __name__ == '__main__': # Допускаем прямой запуск теста на хосте.
unittest.main() # Стандартный код завершения отражает результат проверок.