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

237 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""The reusable terminal must work without a Tms2812Tab or its private state."""
import os
from pathlib import Path
import subprocess
import sys
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtTest import QTest
from PySide6.QtWidgets import QApplication, QTabWidget
from pysetterminal.panel import ByteWorkPanel, build_byte_page
class Protocol:
def __init__(self):
self.requests = []
self.callback = None
self.cancelled = 0
def request_async(self, address, value, controller, callback):
self.requests.append((address, value, controller))
self.callback = callback
def reply(self, success, result):
callback, self.callback = self.callback, None
callback(success, result)
def cancel_async(self):
self.cancelled += 1
if self.callback is not None:
self.reply(False, "cancelled")
def poke(self, address, value, controller):
self.requests.append((address, value, controller))
return True, {"raw": b"ack"}
def peek(self, address, controller):
self.requests.append((address, None, controller))
return True, {"value": 0x1234, "raw_response": {"raw": b"reply"}}
def send_raw(self, text):
return True, text
def receive_raw(self):
return False, "Нет данных"
@pytest.fixture(scope="module")
def app():
return QApplication.instance() or QApplication([])
@pytest.fixture
def page(app, tmp_path):
protocol = Protocol()
widget = ByteWorkPanel(protocol, device_address=7, macros_file=tmp_path / "macros.json")
yield widget, protocol
widget.close()
app.processEvents()
def test_shared_panel_imports_and_constructs_without_desktop_application(tmp_path):
templates = Path(__file__).resolve().parents[1]
code = """
import importlib.abc, sys
class BlockDesktop(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname.split('.')[0] == 'gui_desktop':
raise ImportError('Unexpected desktop dependency')
sys.meta_path.insert(0, BlockDesktop())
from PySide6.QtWidgets import QApplication, QTabWidget
from pysetterminal.panel import build_byte_page
from pysetterminal.rawprotocol import RawProtocol
app = QApplication([])
tabs = QTabWidget()
page = build_byte_page(RawProtocol(), macros_file='macros.json')
tabs.addTab(page, 'Работа с байтом')
assert tabs.widget(0) is page
assert 'gui_desktop' not in sys.modules
assert page.bus_widget and page.raw_widget
page.close()
"""
result = subprocess.run([sys.executable, "-c", code], cwd=tmp_path,
env=dict(os.environ, PYTHONPATH=str(templates)),
capture_output=True, text=True, timeout=30)
assert result.returncode == 0, result.stderr
def test_multiple_tabs_keep_controls_status_and_macros_separate(app, tmp_path):
calls = []
tabs = QTabWidget()
first = build_byte_page(Protocol(), start_word=lambda *args: calls.append(args),
macros_file=tmp_path / "first.json")
second = ByteWorkPanel(Protocol(), macros_file=tmp_path / "second.json")
tabs.addTab(first, "Первый")
tabs.addTab(second, "Второй")
first.byte_write_value.setText("0301")
first.byte_write_value.textEdited.emit("0301")
first.byte_write_address.setText("0x1234")
first.byte_write_button.click()
first.set_read_value(0x1234, 0x0301)
first.set_status("Готово")
first.bus_widget.macros = [{"name": "Первый", "commands": []}]
first.bus_widget.save_macros()
assert calls == [(True, 0x1234, 0x0301)]
assert first.byte_write_binary.text() == "0000 0011 0000 0001"
assert second.byte_write_value.text() == "0000"
assert second.byte_read_value.text() == ""
assert second.status.text() == "Нет данных от контроллера"
second.bus_widget.load_macros()
assert second.bus_widget.macros == []
first.close()
second.close()
tabs.close()
@pytest.mark.parametrize("address", ["4096", "$1000", "0x1000"])
def test_read_accepts_address_formats_and_displays_both_values(page, address):
widget, protocol = page
widget.byte_read_address.setText(address)
widget.byte_read_button.click()
QTest.qWait(15) # Доставляем назначенный шаг общей очереди через события Qt без синхронной блокировки GUI.
assert protocol.requests == [(0x1000, None, 7)]
assert widget.byte_read_button.isEnabled() # Ручная кнопка принимает новые запросы, пока текущее чтение ожидает ответ.
protocol.reply(True, {"value": 0xABCD})
QTest.qWait(15) # Доставляем назначенный шаг общей очереди через события Qt без синхронной блокировки GUI.
assert widget.byte_read_address.text() == "$1000"
assert widget.byte_read_value.text() == "ABCD"
assert widget.byte_read_binary.text() == "1010 1011 1100 1101"
assert widget.byte_read_button.isEnabled()
@pytest.mark.parametrize("address,value,binary", [
("-1", "0000", "0"),
("$100000000", "0000", "0"),
("invalid", "0000", "0"),
("$1000", "10000", "10000000000000000"),
("$1000", "0001", "0"),
("$1000", "GGGG", "0"),
("$1000", "0000", "invalid"),
])
def test_invalid_inputs_do_not_send(page, address, value, binary):
widget, protocol = page
widget.byte_write_address.setText(address)
widget.byte_write_value.setText(value)
widget.byte_write_binary.setText(binary)
widget.byte_write_button.click()
QTest.qWait(15) # Доставляем назначенный шаг общей очереди через события Qt без синхронной блокировки GUI.
assert protocol.requests == []
assert "Проверьте" in widget.status.text()
@pytest.mark.parametrize("readback,confirmed", [(0x0301, True), (0x0302, False)])
def test_write_is_verified_and_controller_is_captured_for_whole_operation(page, readback, confirmed):
widget, protocol = page
controller = [13]
widget.device_address = lambda: controller[0]
widget.byte_write_value.setText("0301")
widget.sync_byte_value("0301", 16)
widget.byte_write_button.click()
QTest.qWait(15) # Доставляем назначенный шаг общей очереди через события Qt без синхронной блокировки GUI.
assert protocol.requests == [(0x1000, 0x0301, 13)]
controller[0] = 21
protocol.reply(True, {"raw": b"ack"})
QTest.qWait(15) # Доставляем назначенный шаг общей очереди через события Qt без синхронной блокировки GUI.
assert protocol.requests[-1] == (0x1000, None, 13)
protocol.reply(True, {"value": readback})
QTest.qWait(15) # Доставляем назначенный шаг общей очереди через события Qt без синхронной блокировки GUI.
assert widget.byte_read_value.text() == f"{readback:04X}"
assert ("Запись проверена" in widget.status.text()) == confirmed
if not confirmed:
assert "не подтверждена" in widget.status.text()
assert widget.byte_write_button.isEnabled()
def test_failure_and_cancellation_restore_controls_and_ignore_late_reply(page):
widget, protocol = page
widget.byte_read_button.click()
QTest.qWait(15) # Доставляем назначенный шаг общей очереди через события Qt без синхронной блокировки GUI.
protocol.reply(False, "CRC ответа не совпадает")
QTest.qWait(15) # Доставляем назначенный шаг общей очереди через события Qt без синхронной блокировки GUI.
assert widget.status.text() == "CRC ответа не совпадает"
assert widget.byte_read_button.isEnabled()
widget.byte_read_button.click()
QTest.qWait(15) # Доставляем назначенный шаг общей очереди через события Qt без синхронной блокировки GUI.
late_reply = protocol.callback
widget.stop()
assert protocol.cancelled == 1
widget.byte_read_button.click()
QTest.qWait(15) # Доставляем назначенный шаг общей очереди через события Qt без синхронной блокировки GUI.
late_reply(True, {"value": 0x9999})
assert widget.byte_read_value.text() == ""
assert widget.byte_read_button.isEnabled() # Ручная кнопка принимает новые запросы, пока текущее чтение ожидает ответ.
protocol.reply(True, {"value": 0x1234})
QTest.qWait(15) # Доставляем назначенный шаг общей очереди через события Qt без синхронной блокировки GUI.
assert widget.byte_read_value.text() == "1234"
assert widget.byte_read_button.isEnabled()
def test_controller_provider_is_used_by_bus_raw_memory_and_macros(app, tmp_path):
protocol = Protocol()
controller = [21]
widget = ByteWorkPanel(protocol, device_address=lambda: controller[0],
macros_file=tmp_path / "address.json")
try:
widget.raw_widget.memory_write() # Первая операция сохраняет контроллер до ожидания общей очереди.
widget.raw_widget.memory_read() # Второе нажатие принимается до завершения первой записи.
QTest.qWait(15) # Запись получает владение общим соединением.
protocol.reply(True, {"raw": b"ack"}) # Подтверждаем текущую запись перед следующим чтением.
QTest.qWait(15) # Следующая ручная операция начинает PEEK.
protocol.reply(True, {"value": 0x1234}) # Возвращаем слово ручного чтения памяти.
QTest.qWait(15) # Освобождаем очередь после завершения второго действия.
assert protocol.requests == [(0x2000, 0, 21), (0x2000, None, 21)] # Оба нажатия используют контроллер своего исходного снимка.
widget.bus_widget.read_from_bus() # Последовательное чтение также выполняется асинхронно через общую очередь.
QTest.qWait(15) # Отправляем подготовку SBUS.
protocol.reply(True, {"raw": b"ack"}) # Подтверждение позволяет перейти к буферу SBUS.
QTest.qWait(15) # Отправляем чтение существующего регистра результата.
protocol.reply(True, {"value": 0x1234, "raw_response": {"raw": b"reply"}}) # Возвращаем слово последовательной шины.
QTest.qWait(15) # Завершаем логическую операцию перед явным макросом.
assert protocol.requests[-2:] == [(0x200B, 0, 21), (0x200F, None, 21)] # Адреса и формат существующей SBUS остаются прежними.
widget.bus_widget.execute_macro_commands([{"type": "read", "tetrad": 3, "register": 5}])
controller[0] = 22
QTest.qWait(20)
assert protocol.requests[-1] == (0x200B, 0x35, 21)
protocol.reply(True, {"raw": b"ack"})
QTest.qWait(15) # Доставляем назначенный шаг общей очереди через события Qt без синхронной блокировки GUI.
QTest.qWait(20)
assert protocol.requests[-1] == (0x200F, None, 21)
protocol.reply(True, {"value": 0x4321, "raw_response": {"raw": b"reply"}})
QTest.qWait(15) # Доставляем назначенный шаг общей очереди через события Qt без синхронной блокировки GUI.
QTest.qWait(20)
assert not widget.bus_widget._macro_running
assert widget.bus_widget.bus_read_value_label.text() == "0x4321 (17185)"
finally:
widget.close()