Extract reusable byte work panel with configurable terminal addressing

This commit is contained in:
2026-10-09 08:56:16 +03:00
parent fe9b5a246e
commit 86ece3ea90
6 changed files with 493 additions and 69 deletions

View File

@@ -0,0 +1,213 @@
"""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()
assert protocol.requests == [(0x1000, None, 7)]
assert not widget.byte_read_button.isEnabled()
protocol.reply(True, {"value": 0xABCD})
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()
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()
assert protocol.requests == [(0x1000, 0x0301, 13)]
controller[0] = 21
protocol.reply(True, {"raw": b"ack"})
assert protocol.requests[-1] == (0x1000, None, 13)
protocol.reply(True, {"value": readback})
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()
protocol.reply(False, "CRC ответа не совпадает")
assert widget.status.text() == "CRC ответа не совпадает"
assert widget.byte_read_button.isEnabled()
widget.byte_read_button.click()
late_reply = protocol.callback
widget.stop()
assert protocol.cancelled == 1
widget.byte_read_button.click()
late_reply(True, {"value": 0x9999})
assert widget.byte_read_value.text() == ""
assert not widget.byte_read_button.isEnabled()
protocol.reply(True, {"value": 0x1234})
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()
assert protocol.requests == [(0x2000, 0, 21), (0x2000, None, 21)]
widget.bus_widget.read_from_bus()
assert protocol.requests[-2:] == [(0x200B, 0, 21), (0x200F, None, 21)]
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(20)
assert protocol.requests[-1] == (0x200F, None, 21)
protocol.reply(True, {"value": 0x4321, "raw_response": {"raw": b"reply"}})
QTest.qWait(20)
assert not widget.bus_widget._macro_running
assert widget.bus_widget.bus_read_value_label.text() == "0x4321 (17185)"
finally:
widget.close()