Files
templates/tools/dsview/tests/check_link_loss.cpp
T

126 lines
22 KiB
C++

#include <QApplication> // Настоящий цикл Qt выявляет модальные окна и повторный вход при сетевом таймауте.
#include <QAbstractButton> // Нажатие штатной кнопки OK проверяет очистку указателя уведомления.
#include <QLabel> // Проверяем фактическую видимость текста ошибки, а не только существование оболочки уведомления.
#include <QMouseEvent> // Кнопка закрытия получает обычные события мыши после проверки её видимой геометрии.
#include <QStackedWidget> // Две страницы воспроизводят переход из DSView на другую вкладку SETGUI.
#include <QVBoxLayout> // Родительская страница задаёт настоящую область встраивания дочернего окна.
#include <QElapsedTimer> // Монотонное время ограничивает ожидание ответа и проверку отсутствия повторов.
#include <QPointer> // Уведомление может удалиться после OK, поэтому тест не хранит висячий указатель.
#include <QTimer> // Проверки выполняются в GUI-потоке одновременно с настоящим драйвером захвата.
#include "pv/appcontrol.h" // Используем штатную инициализацию libsigrok и сессии DSView.
#include "pv/config/appconfig.h" // Каталог записи направляется в отдельную папку проверки.
#include "pv/log.h" // Инициализация журнала необходима до запуска аппаратного ядра DSView.
#include "pv/mainframe.h" // Проверяем закрытие настоящего главного окна вместе с предупреждением.
#include "pv/sigsession.h" // Режимы Single, Repetitive и Loop запускаются обычным методом сессии.
#include "pv/ui/langresource.h" // Ресурсы сообщений загружаются так же, как в рабочем приложении.
#include <windows.h> // Win32-проверка учитывает скрытие панели другим процессом, которое не меняет внутренний флаг видимости Qt.
int main(int argc, char **argv) { // Исполняемый тест линкуется с теми же объектами, что и DSView, без его main.cpp.
QApplication application(argc, argv); // Цикл событий нужен для обработки сетевого завершения и показа уведомления.
application.setProperty("embeddedHost", true); // Закрытие кадра завершает тест и не записывает пользовательскую геометрию окна.
application.setQuitOnLastWindowClosed(false); // Закрытие отдельного уведомления не должно завершать встроенный DSView.
dsv_log_init(); // Подготавливаем штатную инфраструктуру диагностики до создания устройств.
auto &config = AppConfig::Instance(); // Профиль тестового EXE расположен в отдельном runtime.
config.LoadAll(); // Загружаем обязательные значения оформления и захвата.
config.userHistory.saveDir = QString::fromUtf8(qgetenv("DSVIEW_CAPTURE_DIR")); // Все записи остаются в каталоге артефактов проверки.
config.frameOptions.style = "dark"; // Снимок предупреждения использует тему SETGUI со скриншота ошибки.
LangResource::Instance()->Load(config.frameOptions.language); // Штатные строки интерфейса нужны конструктору главного окна.
auto *control = AppControl::Instance(); // Тест пользуется настоящей сессией, а не подменой алгоритма повтора.
if (!control->Init()) return 2; // Ошибка зависимости отличима от регрессии потери связи.
ds_device_handle handle = 0; // Публичный API возвращает найденный прибор, обслуживаемый локальным UDP-эмулятором.
if (ds_add_zynq_device("127.0.0.1", &handle) != SR_OK) return 3; // Реальная плата и Ethernet-сеть для проверки не нужны.
int result = 1; // Успех фиксируется только после всех трёх обрывов и штатного закрытия.
{ // Главное окно должно уничтожиться до освобождения аппаратного ядра.
pv::MainFrame frame; // Обычный кадр создаёт те же обработчики ошибок и панели, что и DSView.exe.
control->Start(); // Запускаем обработку сообщений сессии после создания главного окна.
auto *session = control->GetSession(); // Публичные методы позволяют запускать реальные захваты без имитации кнопок.
if (!session->set_device(handle)) return 4; // Захват должен относиться к найденному сетевому прибору.
QWidget host; // Отдельное окно моделирует оболочку SETGUI с несколькими страницами.
auto *host_layout = new QVBoxLayout(&host); // Область DSView следует размеру родительского окна.
auto *pages = new QStackedWidget(&host); // Переключение страницы должно скрывать всё содержимое DSView, включая сообщение об ошибке.
auto *dsview_page = new QWidget(pages); // Нативный HWND первой страницы становится родителем настоящего окна DSView.
pages->addWidget(dsview_page); // Первая страница отводится встроенному анализатору.
pages->addWidget(new QLabel(QStringLiteral("Другая вкладка SETGUI"), pages)); // Вторая страница воспроизводит экран Zynq с пользовательского скриншота.
host_layout->addWidget(pages); // Родительский layout задаёт видимые границы страниц.
host.resize(1100, 700); // Размер в логических пикселях оставляет видимыми панели и предупреждение.
host.show(); // Для реальной проверки видимости нужен показанный нативный родитель.
if (!frame.ShowEmbedded(static_cast<quintptr>(dsview_page->winId()))) return 6; // Используем штатное Win32-встраивание, а не отдельное окно DSView.
QTimer timer; // Один периодический таймер отслеживает конечное состояние трёх сценариев.
QElapsedTimer elapsed; // Общий дедлайн исключает зависание CI на регрессии модального окна.
elapsed.start(); // Срок включает таймауты драйвера и ожидание возможного автоповтора.
int scenario = 0; // Сначала Repetitive без данных, затем Repetitive с данными, в конце Loop без данных.
qint64 observed_at = -1; // Отрицательное значение означает, что уведомление текущего захвата ещё не появилось.
QPointer<QWidget> notice; // Отслеживаем стабильность уведомления независимо от реализации его оформления.
auto start = [&]() { // Каждый сценарий начинается явным ручным запуском, как после восстановления линка.
session->set_collect_mode(scenario == 2 ? pv::COLLECT_LOOP : pv::COLLECT_REPEAT); // Повтор и кольцевой режим должны останавливаться при одном обрыве.
session->set_repeat_intvl(0.01); // Интервал 10 мс провоцирует прежнюю гонку между END и обработчиком ошибки GUI.
if (scenario == 1) { // Во втором сценарии проверяем также штатное завершение дисковой записи после обрыва.
const QByteArray path = (config.userHistory.saveDir + "/partial-rj2.dsl").toUtf8(); // Имя относится только к изолированным артефактам текущего запуска.
if (!session->get_device()->set_config_uint64(SR_CONF_ZYNQ_RECORD_SAMPLES, 1000000)) return false; // Миллион отсчётов превышает посланный префикс 65 536 и позволяет дождаться сетевого таймаута.
if (!session->get_device()->set_config_string(SR_CONF_ZYNQ_RECORD_FILE, path.constData())) return false; // Передаём путь тем же публичным API, который вызывает кнопка Run после выбора файла.
} // Остальные сценарии проверяют потерю связи ещё до появления первого отсчёта.
observed_at = -1; // Новый захват должен самостоятельно выдать новое уведомление.
return session->start_capture(false); // Обычный захват использует реальный приём UDP и таймаут драйвера.
}; // Повторный запуск проверяет восстановление после сброса обработанной ошибки.
QObject::connect(&timer, &QTimer::timeout, &frame, [&]() { // Все обращения к виджетам выполняются в их GUI-потоке.
auto fail = [&](const char *reason) { // Ошибка теста выводит причину и прекращает дальнейшие проверки.
fprintf(stderr, "FAIL: %s\n", reason); // Журнал запуска остаётся пригодным для диагностики без скриншота.
application.exit(1); // Даже открытый модальный диалог не должен подвесить завершение теста.
}; // Единственный путь неуспеха сохраняет изначальное значение result.
if (elapsed.elapsed() > 30000) { fail("link loss timeout"); return; } // Предел 30 секунд превышает три аппаратных таймаута и окна наблюдения.
QList<QWidget *> visible; // Скрытые закрывающиеся объекты не считаются пользовательскими уведомлениями.
for (auto *widget : frame.findChildren<QWidget *>(QStringLiteral("networkCaptureError"))) if (widget->isVisible()) visible.append(widget); // Счётчик выявляет несколько предупреждений одной сетевой сессии.
if (visible.isEmpty()) return; // До аппаратного таймаута уведомления быть не должно.
if (visible.size() != 1) { fail("duplicate packet error dialogs"); return; } // Любое второе окно означает возврат исходной ошибки.
if (QApplication::activeModalWidget()) { fail("link error blocks the application"); return; } // Предупреждение должно оставлять доступными отключение и закрытие.
if (session->is_working() || session->get_device()->is_collecting()) { fail("capture restarted after link loss"); return; } // Завершение обязано остановить и сессию, и драйвер.
auto *current = visible.front(); // Геометрию и содержимое проверяем у единственного показанного уведомления.
QLabel *message = nullptr; // Одного заголовка Packet Error недостаточно: пользователь должен видеть причину обрыва.
for (auto *label : current->findChildren<QLabel *>()) if (label->isVisible() && label->text().contains(QStringLiteral("RJ2"))) message = label; // Ищем настоящий отображаемый текст аппаратной диагностики.
if (!message || message->visibleRegion().isEmpty()) { fail("packet error details are invisible"); return; } // Проверка воспроизводит пустую оболочку с пользовательского скриншота.
QAbstractButton *dismiss = nullptr; // Кнопка должна быть доступна мышью, а не только программным вызовом click.
for (auto *button : current->findChildren<QAbstractButton *>()) if (button->isVisible() && button->isEnabled() && !button->visibleRegion().isEmpty()) dismiss = button; // Отбрасываем скрытые кнопки вложенного QMessageBox.
if (!dismiss) { fail("packet error dismiss button is invisible"); return; } // Отсутствующий OK нельзя считать успешной неблокирующей диагностикой.
if (current->isWindow()) { fail("packet error escapes its DSView panel"); return; } // Самостоятельное окно остаётся поверх посторонних вкладок после скрытия нативного родителя.
const QRect bounds(current->mapTo(frame.GetMainWindow(), QPoint()), current->size()); // Сравниваем видимую геометрию в общей системе координат панели DSView.
if (!frame.GetMainWindow()->rect().contains(bounds)) { fail("packet error lies outside the DSView panel"); return; } // Уведомление не должно выходить за границы встроенного анализатора.
if (observed_at < 0) { // Первый тик после обрыва запоминает существующее уведомление.
notice = visible.front(); // Следующие тики проверят, что окно не было заменено новым.
observed_at = elapsed.elapsed(); // От этого момента выдерживаем паузу длиннее таймаута драйвера.
pages->setCurrentIndex(1); // Переходим на соседнюю вкладку, как на втором пользовательском скриншоте.
if (IsWindowVisible(reinterpret_cast<HWND>(current->winId()))) { fail("packet error remains above another tab"); return; } // Проверяем фактическую Win32-видимость после скрытия нативного родителя.
pages->setCurrentIndex(0); // Возврат должен восстановить анализатор и доступную кнопку предупреждения.
if (!IsWindowVisible(reinterpret_cast<HWND>(current->winId()))) { fail("packet error did not return with DSView"); return; } // Уведомление сохраняется до явного закрытия пользователем.
current->grab().save(config.userHistory.saveDir + QStringLiteral("/notice-%1.png").arg(scenario)); // Сохраняем само предупреждение с текстом и кнопкой для визуальной проверки.
for (int queued = 0; queued < 3; ++queued) QMetaObject::invokeMethod(frame.GetMainWindow(), "on_session_error", Qt::QueuedConnection); // Запоздавшие сигналы не должны создавать пустые сообщения Undefined Error.
} // Очередь событий обрабатывается между тиками настоящего Qt.
if (notice != visible.front()) { fail("packet error dialog was replaced"); return; } // Обновление не должно порождать цепочку новых объектов.
if (elapsed.elapsed() - observed_at < 3500) return; // Пауза 3,5 секунды превышает сетевой таймаут и обнаруживает скрытый автоповтор.
fprintf(stdout, "PASS scenario %d: stopped, one nonmodal notice\n", scenario); // Отдельный результат подтверждает каждый вариант обрыва.
fflush(stdout); // Внешний сценарий получает сообщение даже при последующей ошибке завершения.
if (scenario < 2) { // Первые два обрыва проверяют ручной запуск без перезапуска приложения.
QMouseEvent press(QEvent::MouseButtonPress, dismiss->rect().center(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); // Нажимаем в центре фактически видимой кнопки закрытия.
QApplication::sendEvent(dismiss, &press); // Используем штатный обработчик мыши кнопки, а не прямой вызов click.
QMouseEvent release(QEvent::MouseButtonRelease, dismiss->rect().center(), Qt::LeftButton, Qt::NoButton, Qt::NoModifier); // Отпускание в той же точке завершает пользовательское нажатие.
QApplication::sendEvent(dismiss, &release); // Закрытие предупреждения должно освободить его объект перед следующим обрывом.
++scenario; // Следующий сценарий меняет условия эмулятора по номеру новой UDP-сессии.
if (!start()) fail("manual restart after link loss failed"); // Восстановленный пользовательский запуск не должен блокироваться старой ошибкой.
return; // Новое уведомление ожидается на следующих тиках таймера.
} // В последнем сценарии предупреждение остаётся открытым до закрытия приложения.
frame.grab().save(QString::fromUtf8(qgetenv("DSVIEW_CAPTURE_DIR")) + "/link-loss.png"); // Сохраняем настоящий кадр остановленной сессии.
session->clear_store_confirm_flag(); // Принятый в предыдущем сценарии фрагмент уже проверяется на диске; тест подтверждает отказ от дополнительного сохранения.
timer.stop(); // Повторный тик не должен обращаться к закрывающемуся окну.
result = frame.close() ? 0 : 1; // Закрытие с открытым предупреждением обязано пройти штатный closeEvent.
application.exit(result); // Возвращаем проверенный результат, не ожидая принудительного завершения процесса.
}); // Обработчик живёт не дольше главного окна проверки.
timer.start(50); // Период 50 мс проверяет состояние интерфейса без занятого ожидания.
QTimer::singleShot(0, &frame, [&]() { if (!start()) application.exit(5); }); // Запуск откладывается до начала обычной обработки GUI-событий.
const int loop_result = application.exec(); // Нативные сетевые ошибки проходят через настоящий цикл событий DSView.
if (loop_result != 0) result = loop_result; // Код ошибки дедлайна или модальности не может превратиться в успешный результат.
control->Stop(); // Освобождаем аппаратную сессию до уничтожения дочерних виджетов.
} // Деструкторы проверяют также время жизни неблокирующего уведомления.
control->UnInit(); // Освобождаем декодеры и libsigrok после закрытия окон.
dsv_log_uninit(); // Закрываем штатный журнал перед возвратом результата теста.
return result; // Нулевой код подтверждает остановку, восстановление и штатное закрытие.
} // Тест не включается в пользовательский EXE и не добавляет в него скрытые команды.