Добавить очереди операций PBUS/SBUS, общую консоль, настройки Candle и SLCAN, декодеры DSView и обработку плотных графиков. Обновить протокол Бальзам и проверки общих компонентов.
323 lines
40 KiB
Python
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() # Стандартный запуск выдаёт количество проверок и подробности ошибок.
|