Extract reusable byte work panel with configurable terminal addressing
This commit is contained in:
213
python/tests/test_byte_work_panel.py
Normal file
213
python/tests/test_byte_work_panel.py
Normal 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()
|
||||
Reference in New Issue
Block a user