#include // Настоящий цикл Qt выявляет модальные окна и повторный вход при сетевом таймауте. #include // Нажатие штатной кнопки OK проверяет очистку указателя уведомления. #include // Проверяем фактическую видимость текста ошибки, а не только существование оболочки уведомления. #include // Кнопка закрытия получает обычные события мыши после проверки её видимой геометрии. #include // Две страницы воспроизводят переход из DSView на другую вкладку SETGUI. #include // Родительская страница задаёт настоящую область встраивания дочернего окна. #include // Монотонное время ограничивает ожидание ответа и проверку отсутствия повторов. #include // Уведомление может удалиться после OK, поэтому тест не хранит висячий указатель. #include // Проверки выполняются в 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 // 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(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 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 visible; // Скрытые закрывающиеся объекты не считаются пользовательскими уведомлениями. for (auto *widget : frame.findChildren(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()) 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()) 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(current->winId()))) { fail("packet error remains above another tab"); return; } // Проверяем фактическую Win32-видимость после скрытия нативного родителя. pages->setCurrentIndex(0); // Возврат должен восстановить анализатор и доступную кнопку предупреждения. if (!IsWindowVisible(reinterpret_cast(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 и не добавляет в него скрытые команды.