Files
templates/tools/dsview/tests/test_plates_buses.py
T
Andrey a5e46d4627 Расширить обмен по шинам, CAN-адаптеры и обработку графиков
Добавить очереди операций PBUS/SBUS, общую консоль, настройки Candle и SLCAN, декодеры DSView и обработку плотных графиков. Обновить протокол Бальзам и проверки общих компонентов.
2026-10-17 14:36:14 +03:00

323 lines
40 KiB
Python

"""Проверки потоковых декодеров на цифровых трассах без установленного sigrokdecode.""" # Подмена только среды позволяет исполнять настоящий decode().
import ast # Проверка синтаксиса сохраняет совместимость с Python 3.6 в DSView.
import copy # Выходы фиксируются в момент put(), как при передаче данных в C-библиотеку.
import importlib # Пакеты загружаются тем же экспортируемым именем Decoder, что и в DSView.
from pathlib import Path # Пути вычисляются относительно теста, без зависимости от установки пользователя.
import sys # Временная подмена sigrokdecode нужна только на время импорта пакетов.
import types # Модуль-заглушка содержит минимальную поверхность API среды.
import unittest # Тесты запускаются стандартным Python без внешних зависимостей.
# Конец входных данных прерывает бесконечный decode(), как завершение потока в среде.
class EndOfCapture(Exception): # Отдельный тип не скрывает реальные ошибки декодеров.
pass # Дополнительных полей исключению не требуется.
# Эта среда реализует ожидание фронтов и таймеров, но не знает формата PBus или SBus.
class Runtime: # Декодеры наследуют её вместо встроенного C-класса только в тестах.
def register(self, output): # Идентификаторы выходов достаточно различать по их типу.
return output # Каждый декодер регистрирует один выход каждого используемого типа.
def has_channel(self, index): # Подключённость каналов задаётся конкретным тестом.
return index in self.connected # Необязательный PBCLK можно оставить неподключённым.
def put(self, start, end, output, data): # Проверяем контракт выходов независимо от содержимого кадров.
assert 0 <= start <= end < len(self.samples), (start, end, len(self.samples)) # Аннотации не должны уходить за границы захвата.
if output == 0: # В заглушке ноль соответствует OUTPUT_ANN.
assert 0 <= data[0] < len(self.annotations) and data[1] # Класс должен быть объявлен и иметь хотя бы один текст.
self.result.append((start, end, output, copy.deepcopy(data))) # Дальнейшая работа автомата не меняет уже выведенные пакеты.
def wait(self, conditions): # Поддерживаем только документированные условия, реально используемые декодерами.
if conditions == {}: # Пустое условие получает исходный отсчёт без фиктивного фронта.
self.samplenum = 0 # Первый отсчёт соответствует началу трассы.
return self.samples[0] # Возвращаются все логические каналы в порядке объявления.
conditions = conditions if isinstance(conditions, list) else [conditions] # Список условий означает логическое ИЛИ.
origin = self.samplenum # skip отсчитывается от последней точки возврата wait().
for sample in range(origin + 1, len(self.samples)): # Фронт не должен повторно срабатывать на прежнем отсчёте.
matched = 0 # Маска совпадений соответствует поведению libsigrokdecode4DSL.
for index, condition in enumerate(conditions): # Условие-словарь объединяет свои элементы через И.
valid = True # Пустое множество ограничений не запрещает совпадение.
for channel, edge in condition.items(): # Проверяем таймер либо изменение канала.
if channel == 'skip': # Временное условие задаётся числом отсчётов.
valid = valid and sample >= origin + edge # Таймер может совпасть с фронтом в той же точке.
else: # Цифровой канал имеет уровень 0 или 1.
before, after = self.samples[sample - 1][channel], self.samples[sample][channel] # Сравниваются соседние отсчёты, а не соседние пробуждения.
valid = valid and before != after and (edge == 'e' or (edge == 'r' and after == 1) or (edge == 'f' and after == 0)) # Разрешены оба фронта или отдельное направление.
if valid: # Несколько альтернатив могут совпасть одновременно.
matched |= 1 << index # DSView представляет matched целочисленной маской.
if matched: # Возвращаем ближайшее подходящее событие, не пропуская таймеры.
self.samplenum, self.matched = sample, matched # Атрибуты обновляются до возврата уровней.
return self.samples[sample] # Декодер видит уровни именно в точке события.
raise EndOfCapture() # Отсутствие событий до конца захвата завершает поток.
# Импорт не должен оставлять ложный sigrokdecode в окружении остальных тестов проекта.
def load_decoders(): # Возвращает модули для доступа к классам и проверяемому CRC.
root = Path(__file__).resolve().parents[1] / 'decoders' # Оба независимых пакета лежат в соседней папке decoders.
stub = types.ModuleType('sigrokdecode') # Создаём подмену только для импорта зависимых классов.
stub.Decoder, stub.OUTPUT_ANN, stub.OUTPUT_PYTHON, stub.SRD_CONF_SAMPLERATE = Runtime, 0, 1, 2 # Значения констант согласованы с Runtime.
original = sys.modules.get('sigrokdecode') # Ранее импортированный настоящий модуль нужно сохранить.
sys.modules['sigrokdecode'] = stub # Классы декодеров получают тестовую реализацию базового API.
sys.path.insert(0, str(root)) # Пакеты импортируются без копирования исходников.
try: # Восстановление окружения обязательно и при ошибке импорта.
return importlib.import_module('plates_sbus.pd'), importlib.import_module('plates_pbus.pd') # Проверяется и экспорт из __init__.py.
finally: # Другие тесты не должны зависеть от нашей подмены.
sys.path.remove(str(root)) # Убираем только путь, добавленный этим импортом.
if original is None: # При отсутствии настоящего модуля восстанавливаем его отсутствие.
del sys.modules['sigrokdecode'] # Ссылки внутри декодеров уже удерживают тестовую среду.
else: # В окружении с sigrokdecode возвращаем исходный объект.
sys.modules['sigrokdecode'] = original # Чужие импорты продолжают работать штатно.
SBUS, PBUS = load_decoders() # Все проверки исполняют рабочие файлы, а не копии алгоритмов.
# Трасса хранит уровни на каждом отсчёте, поэтому среда независимо обнаруживает реальные фронты.
class Trace: # Частота тестовых трасс SBus равна 100 МГц: один отсчёт — 10 нс.
def __init__(self, levels): # Начальные уровни задаются до первого сохранённого отсчёта.
self.levels, self.samples = list(levels), [] # Текущие уровни отделены от уже записанного захвата.
def hold(self, count, updates=None): # Удерживаем сигналы count отсчётов после применения изменений.
for channel, level in (updates or {}).items(): # Незаданные сигналы сохраняют прежнее состояние.
self.levels[channel] = level # Канал индексируется в порядке объявления декодера.
self.samples.extend([tuple(self.levels)] * count) # Кортежи исключают случайное изменение истории при следующем фронте.
# Кадры задаются фиксированными полями и CRC, без обращения к реализации расчёта в декодере.
def serial_frame(trace, direction=1, address=0x32, value=0x1234, crc=1, ack=0, limit=29, tail=1000, tail_clock=0): # Полный формат содержит 29 нарастающих фронтов.
frame = (direction << 28) | (address << 20) | (value << 4) | crc # Временная разметка независима от внутреннего буфера декодера.
for index, shift in enumerate(range(28, 28 - limit, -1)): # limit позволяет оборвать кадр на любом поле.
bit = (frame >> shift) & 1 # Каждый бит передаётся старшим вперёд.
tx, rx = (bit, 1) if index < 9 or direction else (1, bit) # При чтении TX заполнен единицами, чтобы обнаружить выбор неверной линии.
trace.hold(14, {0: 0, 1: tx, 2: rx}) # Ведомое устанавливает ответ на спаде CLK за 140 нс до выборки.
trace.hold(14, {0: 1}) # Полупериод 140 нс совпадает с существующим HDL-тестом.
if tail: # Отсутствие хвоста моделирует конец файла непосредственно после последнего бита.
trace.hold(tail, {0: tail_clock, 2: ack if direction else trace.levels[2]}) # ACK появляется после последнего CRC, а затем CLK может оставаться неподвижным.
# Формирование PBus не использует состояния декодера: задаётся только последовательность физических уровней.
def parallel_frame(trace, address=0x32, value=0x1234, inverse=None, bad_address=False, phases=3, clock_ok=True, simultaneous=False): # phases ограничивает число полученных подтверждений.
word = (address << 8) | ((address ^ 255) ^ int(bad_address)) # Адрес защищён побитовой инверсией младшего байта.
data = {2 + bit: (word >> bit) & 1 for bit in range(16)} # D0…D15 записываются на отдельные физические каналы.
trace.hold(10, data) # Адрес устанавливается до разрешения CE.
trace.hold(10, {0: 0, 1: 0 if simultaneous and phases else 1, 18: 1}) # Возможен одновременный спад CE и адресного ACK в одном отсчёте.
if phases: # Адресный ACK опускается только при наличии ответа.
trace.hold(10, {1: 0}) # Первый низкий ACK подтверждает приём адреса.
if phases >= 2: # Прямые данные передаются только после адресного подтверждения.
trace.hold(10, {2 + bit: (value >> bit) & 1 for bit in range(16)}) # Данные становятся стабильны заранее.
trace.hold(10, {1: 1, 18: 0 if clock_ok else 1}) # Высокий ACK подтверждает прямые данные при низком CLK.
if phases >= 3: # Полное чтение должно получить независимое инверсное слово.
inverse = (value ^ 65535) if inverse is None else inverse # Аргумент позволяет повредить контрольную копию.
trace.hold(10, {2 + bit: (inverse >> bit) & 1 for bit in range(16)}) # Инверсия устанавливается до своего ACK.
trace.hold(10, {1: 0, 18: 1 if clock_ok else 0}) # Последний низкий ACK отмечает готовность контрольного слова.
trace.hold(10, {0: 1, 1: 1}) # CE возвращается в неактивное состояние, ведомое отпускает ACK.
# Настройка окружения одинакова для всех трасс и не зависит от алгоритма протокола.
def run_decoder(module, trace, options=None, connected=None, samplerate=100000000): # Временная шкала по умолчанию соответствует 10 нс на отсчёт.
decoder = module.Decoder() # Исполняется штатный конструктор и reset().
decoder.samples, decoder.result, decoder.samplenum = trace.samples, [], 0 # Состояние тестовой среды хранится отдельно от автомата шины.
decoder.connected = set(range(len(trace.levels))) if connected is None else set(connected) # При необходимости можно отключать отдельные входы.
decoder.options = {item['id']: item['default'] for item in getattr(decoder, 'options', ())} # Среда применяет объявленные значения по умолчанию.
decoder.options.update(options or {}) # Тест может изменить интервалы выборки.
decoder.start() # Выходы должны быть зарегистрированы до первой аннотации.
if samplerate is not None and hasattr(decoder, 'metadata'): # PBus не требует частоту для выборки по ACK.
decoder.metadata(2, samplerate) # Константа 2 соответствует SRD_CONF_SAMPLERATE заглушки.
try: # Только штатный конец захвата перехватывается как успешное завершение.
decoder.decode() # Проверяется настоящий цикл wait(), а не отдельно вызванные внутренние методы.
except EndOfCapture: # Любое другое исключение делает тест ошибочным.
decoder.last_samplenum = len(trace.samples) - 1 # Последний отсчёт нужен для корректного end().
decoder.end() # Незавершённые операции должны быть выведены при конце файла.
packets = [data[1] for _, _, output, data in decoder.result if output == 1] # Извлекаем структурированные FRAME-пакеты.
warnings = [data[1][0] for _, _, output, data in decoder.result if output == 0 and data[0] == len(decoder.annotations) - 1] # Последний класс обоих декодеров содержит предупреждения.
return packets, warnings # Проверяем одновременно данные операций и явную диагностику.
# Набор проверяет содержимое кадров, повторную синхронизацию, границы и настройку каналов.
class DecoderTests(unittest.TestCase): # unittest позволяет запускать тесты без GUI и библиотек DSView.
def serial(self): # Готовим заведомо достаточную паузу перед первым запросом.
trace = Trace((0, 1, 1)) # Мастер держит низкий CLK, обе линии данных отпущены в единицу.
trace.hold(1000) # Десять микросекунд превышают 3,6 мкс reset в HDL.
return trace # Дальнейшие фронты добавляются конкретным тестом.
def parallel(self): # PBus стартует в неактивной фазе CE.
trace = Trace((1, 1) + (0,) * 16 + (1,)) # CE и ACK высокие, D0…D15 нулевые, CLK высокий.
trace.hold(10) # Первый wait() увидит исходные уровни до спада CE.
return trace # Физический запрос добавляется независимо от декодера.
def test_write_and_delayed_ack_without_more_clocks(self): # Проверяем отдельный таймер ACK после последнего такта.
trace = self.serial() # Начальная пауза обеспечивает синхронизацию.
serial_frame(trace, tail_clock=1) # После CRC CLK остаётся высоким без новых фронтов.
packets, warnings = run_decoder(SBUS, trace) # Декодируется весь цифровой поток.
self.assertEqual((packets[0]['dev'], packets[0]['reg'], packets[0]['value']), (3, 2, 0x1234)) # Поля должны совпасть с запросом.
self.assertTrue(packets[0]['crc_ok'] and packets[0]['ack']) # Хороший CRC и низкий ACK проверяются независимо.
self.assertEqual(warnings, []) # Полный корректный кадр не содержит предупреждений.
def test_read_uses_response_line(self): # TX после заголовка специально не содержит полезных данных чтения.
trace = self.serial() # Пауза позволяет принять первый бит направления.
serial_frame(trace, direction=0, value=0xCAFE, crc=7) # Контрольный вектор ответа отличается от всех единиц на TX.
packets, warnings = run_decoder(SBUS, trace) # Читаем по нарастающим фронтам после установки RX на спаде.
self.assertEqual((packets[0]['operation'], packets[0]['value'], packets[0]['ack']), ('read', 0xCAFE, None)) # У чтения нет ACK записи.
self.assertTrue(packets[0]['crc_ok']) # Контрольная сумма включает заголовок с TX и данные с RX.
self.assertEqual(warnings, []) # Правильное переключение линий не создаёт ошибок.
def test_repeated_writes_and_both_idle_levels(self): # Повтор одинакового значения остаётся отдельной командой.
trace = self.serial() # Первый кадр начинается после начальной паузы.
serial_frame(trace, tail_clock=1) # Первая межкадровая пауза держит CLK высоким.
serial_frame(trace, tail_clock=0) # Вторая пауза держит CLK низким.
packets, warnings = run_decoder(SBUS, trace) # Оба уровня паузы должны разрешать синхронизацию.
self.assertEqual([packet['value'] for packet in packets], [0x1234, 0x1234]) # Никакой дедупликации по значению быть не должно.
self.assertEqual(warnings, []) # Повторная команда и высокий idle являются допустимыми.
def test_crc_error_does_not_hide_ack(self): # ACK не заменяет проверку целостности принятого кадра.
trace = self.serial() # Создаём синхронизированный поток.
serial_frame(trace, crc=0, ack=0) # Один бит CRC повреждён, но ответ линии низкий.
packets, warnings = run_decoder(SBUS, trace) # Кадр должен остаться доступным для диагностики.
self.assertFalse(packets[0]['crc_ok']) # Расчёт CRC обнаруживает повреждение.
self.assertTrue(packets[0]['ack']) # Наблюдавшийся ACK сохраняется независимо от CRC.
self.assertIn('CRC mismatch', warnings) # Ошибка присутствует в отдельной строке.
def test_high_ack_is_not_success(self): # Высокий RX может означать отказ или отсутствие ведомого.
trace = self.serial() # Подготавливаем запрос с правильным CRC.
serial_frame(trace, ack=1) # Плата не подтверждает запись низким уровнем.
packets, warnings = run_decoder(SBUS, trace) # Разбор не должен путать CRC и успешность ответа.
self.assertTrue(packets[0]['crc_ok']) # Данные на запросе не повреждены.
self.assertFalse(packets[0]['ack']) # Высокий ответ не считается подтверждением.
self.assertIn('No low write ACK', warnings) # Причина обмена отмечается явно без предположений о плате.
def test_partial_frame_resynchronizes_after_gap(self): # Потерянные биты не должны сдвигать все последующие кадры.
trace = self.serial() # Создаём начало корректно синхронизированного кадра.
serial_frame(trace, limit=12) # Обрываем кадр после заголовка и части данных.
serial_frame(trace) # Следующий кадр начинается после достаточной паузы.
packets, warnings = run_decoder(SBUS, trace) # Автомат должен восстановиться независимо от остатка первого запроса.
self.assertEqual(len(packets), 1) # Частичный кадр не выдаётся за полную команду.
self.assertEqual(packets[0]['value'], 0x1234) # Следующий кадр читается без смещения.
self.assertTrue(any('12/29' in text for text in warnings)) # Обрыв первого кадра остаётся виден.
def test_capture_starting_mid_frame_waits_for_gap(self): # В начале файла может отсутствовать заголовок.
trace = Trace((0, 1, 1)) # Предварительной паузы здесь намеренно нет.
trace.hold(1) # Захват начинается непосредственно перед неизвестным тактом.
serial_frame(trace, limit=15) # Эти биты нельзя надёжно считать первым заголовком.
serial_frame(trace) # После паузы можно декодировать полную операцию.
packets, warnings = run_decoder(SBUS, trace) # Начальный фрагмент игнорируется до синхронизации.
self.assertEqual(len(packets), 1) # Ложный первый кадр не создаётся.
self.assertEqual(warnings, []) # Непроверяемый начальный фрагмент не объявляется ошибкой протокола.
def test_capture_ends_in_data(self): # end() должен диагностировать обрыв без следующего события.
trace = self.serial() # Первый кадр начнётся после паузы.
serial_frame(trace, limit=20, tail=0) # Захват заканчивается внутри 16-битных данных.
packets, warnings = run_decoder(SBUS, trace) # Единственный способ закрыть фрагмент — end().
self.assertEqual(packets, []) # Частичное значение не публикуется как полное.
self.assertTrue(any('20/29' in text for text in warnings)) # Сообщение содержит фактическое количество наблюдавшихся битов.
def test_capture_ends_before_ack_sampling(self): # Нельзя считать неизвестный ACK ни положительным, ни отрицательным.
trace = self.serial() # Кадр полностью помещается после паузы.
serial_frame(trace, tail=0) # Конец захвата наступает раньше таймера ACK.
packets, warnings = run_decoder(SBUS, trace) # end() сохраняет полный запрос.
self.assertIsNone(packets[0]['ack']) # Подтверждение осталось ненаблюдавшимся.
self.assertIn('Write ACK not sampled: capture ended', warnings) # Причина неизвестности указана явно.
def test_serial_bad_configuration(self): # Ошибки настройки должны выявляться до разбора потока.
for options in ({'ack_us': 0}, {'ack_us': 4}, {'idle_us': float('nan')}): # Проверяем нулевой, чрезмерный и нечисловой интервалы.
with self.subTest(options=options), self.assertRaises(ValueError): # Каждый неверный набор обязан быть отвергнут.
run_decoder(SBUS, self.serial(), options=options) # Не требуется наличие команды на входе.
with self.assertRaises(ValueError): # Отсутствие RX нельзя подменять единицей либо нулём.
run_decoder(SBUS, self.serial(), connected=(0, 1)) # Чтение и ACK требуют физического ответного канала.
with self.assertRaises(ValueError): # Без частоты захвата микросекунды не переводятся в отсчёты.
run_decoder(SBUS, self.serial(), samplerate=None) # Произвольные предполагаемые частоты недопустимы.
with self.assertRaises(ValueError): # Слишком редкие отсчёты сливают ACK и межкадровую паузу.
run_decoder(SBUS, self.serial(), samplerate=100000) # При 100 кГц один отсчёт равен 10 мкс.
def test_crc_fixed_vectors(self): # Дополнительные контрольные значения охватывают направления и крайние адреса.
for direction, address, value, expected in ((1, 0, 0, 1), (0, 0, 0, 12), (1, 255, 65535, 1), (0, 255, 65535, 12), (0, 0x32, 0xCAFE, 7), (1, 0x57, 0, 7)): # Векторы фиксированы и не вычисляются генератором трассы.
with self.subTest(direction=direction, address=address, value=value): # Ошибка укажет конкретный вектор.
self.assertEqual(SBUS.frame_crc(direction, address, value), expected) # Проверяем в том числе четыре завершающие единицы HDL.
def test_parallel_full_words_and_optional_clock(self): # Проверяем края диапазонов и сохранение старшего разряда данных.
for address, value in ((0, 0), (255, 65535), (0x32, 0x1234), (0x8F, 0x8001)): # Адрес и данные не должны превращаться в знаковые числа.
for connected in (range(18), range(19)): # PBCLK может отсутствовать либо участвовать в проверке фаз.
with self.subTest(address=address, value=value, clock=len(connected) == 19): # Каждый вариант проверяется независимо.
trace = self.parallel() # Готовим неактивные исходные уровни.
parallel_frame(trace, address=address, value=value) # На провод подаётся полная последовательность ACK.
packets, warnings = run_decoder(PBUS, trace, connected=connected) # Выборка идёт по реальным переходам ACK.
self.assertEqual((packets[0]['dev'], packets[0]['reg'], packets[0]['value']), (address >> 4, address & 15, value)) # Разделение адреса должно сохранять все биты.
self.assertTrue(packets[0]['ok'] and packets[0]['complete']) # Успешность требует полноты и правильной защиты.
self.assertEqual(warnings, []) # Корректный обмен не должен вызывать ложные ошибки.
def test_parallel_bad_address_with_and_without_ack(self): # Повреждение адреса видно и при молчании ведомого.
for phases in (0, 3): # Проверяем отсутствие ответа и ошибочно подтверждённый адрес.
trace = self.parallel() # Начинаем отдельный захват для каждого случая.
parallel_frame(trace, bad_address=True, phases=phases) # Инвертируем один бит защитной копии адреса.
packets, warnings = run_decoder(PBUS, trace) # Декодер сохраняет наблюдавшийся запрос.
self.assertFalse(packets[0]['address_ok'] or packets[0]['ok']) # Хорошие данные ответа не исправляют неверный запрос.
self.assertIn('Address complement mismatch', warnings) # Ошибка защиты адреса должна остаться явной.
def test_parallel_bad_inverse(self): # Последний ACK сам по себе не доказывает целостности данных.
trace = self.parallel() # Подготавливаем обычный адрес и прямые данные.
parallel_frame(trace, inverse=0) # Контрольное слово намеренно не равно инверсии 0x1234.
packets, warnings = run_decoder(PBUS, trace) # Все фазы обмена всё же присутствуют.
self.assertTrue(packets[0]['complete']) # Полнота и правильность являются разными свойствами.
self.assertFalse(packets[0]['inverse_ok'] or packets[0]['ok']) # Повреждённая копия делает обмен ошибочным.
self.assertIn('Data complement mismatch', warnings) # Есть понятная причина ошибки.
def test_parallel_missing_each_ack(self): # Место обрыва различается по ожидаемой фазе.
for phases, missing in ((0, 'address'), (1, 'data'), (2, 'inverse')): # Проверяем все три точки ожидания.
with self.subTest(phases=phases): # Диагностика должна назвать нужную фазу.
trace = self.parallel() # Каждый запрос имеет свой неактивный префикс.
parallel_frame(trace, phases=phases) # CE снимается до завершения операции.
packets, warnings = run_decoder(PBUS, trace) # Закрытие по CE обязано выдать частичный результат.
self.assertFalse(packets[0]['complete'] or packets[0]['ok']) # Неполная операция никогда не успешна.
self.assertIn('Incomplete PBus: missing %s ACK' % missing, warnings) # Указана конкретная отсутствующая фаза.
def test_parallel_capture_ends_before_inverse(self): # Отсутствие возврата CE не должно терять частичный запрос.
trace = self.parallel() # Создаём начало запроса и два подтверждения.
parallel_frame(trace, phases=2) # Генератор завершает CE, но ниже этот хвост удаляется.
trace.samples = trace.samples[:-10] # Обрываем файл при ещё активном CE.
packets, warnings = run_decoder(PBUS, trace) # end() должен закрыть ожидаемую инверсию.
self.assertEqual(packets[0]['value'], 0x1234) # Наблюдавшиеся прямые данные сохраняются.
self.assertIn('Incomplete PBus: missing inverse ACK', warnings) # Отсутствие последнего подтверждения остаётся явным.
def test_parallel_phase_checks_only_with_clock(self): # Необязательный CLK усиливает диагностику, не меняя данные.
trace = self.parallel() # Одинаковые уровни используются в двух конфигурациях каналов.
parallel_frame(trace, clock_ok=False) # ACK данных и инверсии приходят при неверных уровнях CLK.
packets, warnings = run_decoder(PBUS, trace) # Подключённый CLK должен выявить обе ошибки.
self.assertFalse(packets[0]['ok']) # Данные правильные, но протокол фаз нарушен.
self.assertEqual(len(warnings), 2) # Ошибки прямой и инверсной фазы независимы.
packets, warnings = run_decoder(PBUS, trace, connected=range(18)) # Без захвата CLK его уровень не интерпретируется.
self.assertTrue(packets[0]['ok']) # Доступные проверки CE, ACK и содержимого проходят.
self.assertEqual(warnings, []) # Нельзя жаловаться на незахваченный сигнал.
def test_parallel_repeats_and_simultaneous_enable_ack(self): # Фронты CE и ACK могут попасть в один отсчёт анализатора.
trace = self.parallel() # До первого запроса видны исходные высокие CE и ACK.
parallel_frame(trace, simultaneous=True) # Адресный ACK совпадает со спадом CE.
parallel_frame(trace) # Повтор того же чтения обязан оставаться отдельной операцией.
packets, warnings = run_decoder(PBUS, trace) # Оба способа начала проходят один автомат.
self.assertEqual(len(packets), 2) # Ни один запрос не теряется и не дублируется.
self.assertTrue(all(packet['ok'] for packet in packets)) # Оба кадра содержат полные корректные проверки.
self.assertEqual(warnings, []) # Совпадение фронтов само по себе не ошибка.
def test_parallel_start_in_active_transaction(self): # Начальная активная CE не доказывает наличие полного адреса.
trace = self.parallel() # Сначала строим полный физический обмен.
parallel_frame(trace) # Часть начала будет удалена из файла захвата.
trace.samples = trace.samples[45:] # Первый видимый отсчёт находится внутри активной операции.
parallel_frame(trace) # Следующий полный запрос должен декодироваться нормально.
packets, warnings = run_decoder(PBUS, trace) # Начальный фрагмент игнорируется до следующего спада CE.
self.assertEqual(len(packets), 1) # Частичное начало не порождает ложной операции.
self.assertEqual(warnings, []) # Нельзя диагностировать отсутствующий в файле адрес.
def test_parallel_requires_every_data_bit(self): # Один незахваченный разряд делает 16-битное слово недостоверным.
with self.assertRaises(ValueError): # Декодер обязан отвергнуть неполное назначение каналов.
run_decoder(PBUS, self.parallel(), connected=range(17)) # Здесь отсутствует обязательный D15.
def test_python36_syntax_and_line_comments(self): # Стиль проекта требует комментарий к каждой строке нового кода.
for path in Path(__file__).resolve().parents[1].glob('decoders/plates_*/*.py'): # Проверяются устанавливаемые Python-файлы обеих шин, без чужих декодеров при переносе в SETGUI.
source = path.read_text(encoding='utf-8') # Некорректная кодировка должна сразу приводить к ошибке.
ast.parse(source, filename=str(path), feature_version=(3, 6)) # DSView использует Python 3.6, поэтому более новый синтаксис запрещён.
for number, line in enumerate(source.splitlines(), 1): # Номер строки помогает исправить нарушение стиля.
if line.strip() and not line.lstrip().startswith('#'): # Пустые строки и отдельные комментарии не содержат исполняемый код.
self.assertIn('#', line, '%s:%d' % (path, number)) # Каждая строка кода сопровождается пояснением.
if __name__ == '__main__': # Разрешаем запуск как файла и через unittest discovery.
unittest.main() # Стандартный запуск выдаёт количество проверок и подробности ошибок.