"""Проверки общей консоли на настоящем Qt без физического устройства.""" # Проверяем сохранность истории, фильтры и очистку при асинхронном обновлении. import os # Виртуальная платформа выбирается до импорта Qt. os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") # Тесты не открывают окна на рабочем столе. import pytest # Фикстура даёт каждому сценарию независимую консоль. from set_devices.qt_ports.console import ConsoleView, QApplication # Используем реальный порт templates, а не имитацию обработчиков. @pytest.fixture # Создаём отдельный журнал для проверки независимости состояний. def console(): # Ссылка на приложение удерживается до завершения сценария. app = QApplication.instance() or QApplication([]) # Qt требует приложение перед созданием виджетов. view = ConsoleView(limit=3, payloads=True) # Малый предел проверяет вытеснение без большого массива данных. yield view # Сценарий управляет настоящими сигналами флажков и кнопок. view.panel.deleteLater() # Удаляется контейнер вместе с редактором и таймером. app.processEvents() # Отложенное удаление доставляется до следующего сценария. def test_pause_keeps_bounded_history_and_clear_cannot_resurrect(console): # Очистка на паузе должна удалять также невидимые данные. console.appendPlainText("первая") # Первая запись сразу видна на экране. console.pause.setChecked(True) # Замораживаем только представление. for index in range(5): console.appendPlainText("строка %d" % index) # На паузе поступает больше данных, чем помещается в истории. assert console.toPlainText() == "первая" and len(console.records) == 3 # Экран заморожен, память ограничена. console.pause.setChecked(False) # Возобновление показывает последние сохранённые строки. assert console.toPlainText().splitlines() == ["строка 2", "строка 3", "строка 4"] # Вытеснённые записи больше не появляются. console.appendPlainText("ожидает таймер") # Полный буфер ставит пакетное обновление в очередь. assert console.timer.isActive() # Проверяем именно очистку при ожидающем обновлении. console.pause.setChecked(True) # Очистка должна работать и при замороженном экране. console.clear_button.click() # Пользователь удаляет весь журнал через общую панель. console.pause.setChecked(False) # Возобновление не должно вернуть старые данные. assert not console.records and not console.toPlainText() and not console.timer.isActive() # Документ, история и очередь согласованы. def test_filters_restore_hidden_rows_and_preserve_service_errors(console): # Фильтры направлений не должны скрывать ошибки без метки TX/RX. console.appendPlainText("[12:00:00] TX команда") # Начальная метка после времени определяет направление. console.appendPlainText("12:00:01.123 RX ответ") # Поддерживаем существующий формат временных меток SETGUI. console.appendPlainText("#ERR RX timeout") # Слово RX внутри ошибки не делает её принятым пакетом. console.show_rx.setChecked(False) # Скрываем только входящие пакеты. console.show_tx.setChecked(False) # Оба направления можно выключить одновременно. assert console.toPlainText() == "#ERR RX timeout" # Служебная ошибка остаётся доступной оператору. console.search.setText("ОТВЕТ") # Регистр запроса не должен влиять на поиск. console.show_rx.setChecked(True) # Входящие данные восстанавливаются из сохранённой истории. assert console.toPlainText() == "12:00:01.123 RX ответ" # Поиск применяется к той же записи без повторного приёма. console.search.clear() # Снимаем текстовый фильтр. assert len(console.records) == 3 # Фильтры не удаляют события. def test_payload_and_utf8_export_use_visible_snapshot(console, tmp_path): # Сохранение и копирование соответствуют выбранному представлению. console.append_message("RX пакет <текст>", direction="RX", payload=b"A\x00\xff") # Payload содержит печатный, управляющий и не-ASCII байты. console.display.setCurrentText("HEX + ASCII") # Выбираем совместное представление байтов. assert console.toPlainText() == "RX пакет <текст> · 41 00 FF | A.." # Непечатные байты заменяются точками без потери HEX. path = tmp_path / "журнал.txt" # Проверяем кириллицу в имени и содержимом файла. console.save_to(path) # Экспорт выполняется без диалога, сохраняя тот же код записи. assert path.read_text(encoding="utf-8") == console.toPlainText() # UTF-8 сохраняет фактически видимый текст. console.copy_visible() # Кнопка копирует весь показанный журнал. assert QApplication.clipboard().text() == console.toPlainText() # Буфер обмена совпадает с экспортом. def test_multiline_html_and_snapshot_obey_limit(console): # Предел относится к строкам, а не к числу вызовов записи. console.appendPlainText("0\n1\n2\n3\n4") # Один пакет расшифровки превышает лимит документа и истории. console.refresh() # Применяем запланированное пакетное обновление. assert console.toPlainText() == "2\n3\n4" # Старые строки не возвращаются при смене фильтра. console.setPlainText("снимок") # Потребитель может передать результат повторного разбора кадров целиком. console.appendHtml('<ошибка>') # Проверяем совместимость прежнего цветного API SETGUI. console.search.setText("ошибка") # Поиск должен работать по тексту без тегов разметки. assert console.toPlainText() == "<ошибка>" # Спецсимволы устройства остаются обычным текстом. console.search.clear() # Возвращаем обе записи с исходным оформлением. assert console.toPlainText() == "снимок\n<ошибка>" # Фильтр не теряет предыдущий снимок истории. def test_clear_notifies_external_owner_once(console): # Владелец может очистить свой буфер и повторно вызвать clear без рекурсии. external = ["старый кадр"] # Имитируем отдельную историю протокольных кадров приложения. def clear_owner(): # Обработчик очищает внешний источник повторного форматирования. external.clear() # Старые кадры не вернутся после смены парсера. console.clear() # Повторный программный вызов не должен снова эмитировать пользовательский сигнал. console.userCleared.connect(clear_owner) # Подключаем тот же контракт, что консоли платы и TX/RX SETGUI. console.appendPlainText("строка") # До очистки журнал содержит данные. console.clear_button.click() # Нажимаем настоящую кнопку Qt. assert not external and not console.toPlainText() # Обе истории очищены одним действием.