综合平台编程器项目的远程存储
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 
 

588 righe
25 KiB

  1. #include "domain/active_register_repository.h"
  2. #include "domain/project_storage.h"
  3. #include "domain/register_repository.h"
  4. #include "services/hmi_editor_service.h"
  5. #include "services/hmi_runtime_service.h"
  6. #include "services/logic_editor_service.h"
  7. #include "services/offline_simulation_service.h"
  8. #include "services/project_service.h"
  9. #include "services/runtime_mode_service.h"
  10. #include "services/register_monitor_service.h"
  11. #include "ui/hmi_editor_widget.h"
  12. #include "ui/free_monitor_widget.h"
  13. #include "ui/logic_editor_widget.h"
  14. #include "ui/main_window.h"
  15. #include "ui/plc_connection_dialog.h"
  16. #include <QAction>
  17. #include <QApplication>
  18. #include <QComboBox>
  19. #include <QDialog>
  20. #include <QDockWidget>
  21. #include <QGraphicsItem>
  22. #include <QGraphicsScene>
  23. #include <QLabel>
  24. #include <QLineEdit>
  25. #include <QListWidget>
  26. #include <QSpinBox>
  27. #include <QPushButton>
  28. #include <QTableWidget>
  29. #include <QTimer>
  30. #include <iostream>
  31. #include <stdexcept>
  32. #include <string>
  33. namespace {
  34. class TestProjectStorage final : public ProjectStorage
  35. {
  36. public:
  37. // 主窗口测试使用无副作用存储实现,隔离文件对 UI 行为的影响
  38. ProjectSaveResult save(const Project &, const std::string &) override
  39. {
  40. return {true, ProjectStorageError::None, {}};
  41. }
  42. ProjectLoadResult load(const std::string &) override
  43. {
  44. return {false, {}, ProjectStorageError::FileReadFailed, {}};
  45. }
  46. };
  47. class RepeatedStatusGateway final : public PlcCommunicationGateway
  48. {
  49. public:
  50. PlcCommunicationResult connectDevice(
  51. const PlcSerialConfiguration &) override
  52. {
  53. connection_state = PlcConnectionState::Connecting;
  54. notifyStateChanged();
  55. connection_state = PlcConnectionState::Connected;
  56. notifyStateChanged();
  57. notifyStateChanged();
  58. return {true, {}};
  59. }
  60. void disconnectDevice() override
  61. {
  62. connection_state = PlcConnectionState::Disconnected;
  63. notifyStateChanged();
  64. }
  65. void setPollAddresses(const std::vector<RegisterAddress> &) override {}
  66. PlcConnectionState state() const override { return connection_state; }
  67. bool initialReadCompleted() const override { return false; }
  68. const std::string &lastError() const override { return last_error; }
  69. void setCallbacks(
  70. std::function<void()> state_callback,
  71. std::function<void(bool)> initial_callback,
  72. std::function<void()> cache_callback,
  73. std::function<void(const std::string &)> error_callback) override
  74. {
  75. state_changed = std::move(state_callback);
  76. initial_read_changed = std::move(initial_callback);
  77. cache_updated = std::move(cache_callback);
  78. error_reported = std::move(error_callback);
  79. }
  80. private:
  81. void notifyStateChanged()
  82. {
  83. if (state_changed)
  84. {
  85. state_changed();
  86. }
  87. }
  88. PlcConnectionState connection_state = PlcConnectionState::Disconnected;
  89. std::string last_error;
  90. std::function<void()> state_changed;
  91. std::function<void(bool)> initial_read_changed;
  92. std::function<void()> cache_updated;
  93. std::function<void(const std::string &)> error_reported;
  94. };
  95. class OnlineReadyGateway final : public PlcCommunicationGateway
  96. {
  97. public:
  98. PlcCommunicationResult connectDevice(const PlcSerialConfiguration &) override
  99. {
  100. connection_state = PlcConnectionState::Connected;
  101. if (state_changed)
  102. {
  103. state_changed();
  104. }
  105. return {true, {}};
  106. }
  107. void disconnectDevice() override
  108. {
  109. connection_state = PlcConnectionState::Disconnected;
  110. }
  111. void setPollAddresses(const std::vector<RegisterAddress> &addresses) override
  112. {
  113. poll_addresses = addresses;
  114. }
  115. PlcConnectionState state() const override { return connection_state; }
  116. bool initialReadCompleted() const override { return initial_read; }
  117. const std::string &lastError() const override { return last_error; }
  118. void setCallbacks(
  119. std::function<void()> state_callback,
  120. std::function<void(bool)> initial_callback,
  121. std::function<void()> cache_callback,
  122. std::function<void(const std::string &)> error_callback) override
  123. {
  124. state_changed = std::move(state_callback);
  125. initial_read_changed = std::move(initial_callback);
  126. cache_updated = std::move(cache_callback);
  127. error_reported = std::move(error_callback);
  128. }
  129. void completeInitialRead()
  130. {
  131. initial_read = true;
  132. if (initial_read_changed)
  133. {
  134. initial_read_changed(true);
  135. }
  136. }
  137. PlcConnectionState connection_state = PlcConnectionState::Disconnected;
  138. bool initial_read = false;
  139. std::string last_error;
  140. std::vector<RegisterAddress> poll_addresses;
  141. std::function<void()> state_changed;
  142. std::function<void(bool)> initial_read_changed;
  143. std::function<void()> cache_updated;
  144. std::function<void(const std::string &)> error_reported;
  145. };
  146. void require(bool condition, const std::string &message)
  147. {
  148. if (!condition)
  149. {
  150. throw std::runtime_error(message);
  151. }
  152. }
  153. template<typename ObjectType>
  154. ObjectType *requiredChild(MainWindow &window, const char *name)
  155. {
  156. // 通过 objectName 取得 Designer 组件,缺失时给出明确测试失败信息
  157. ObjectType *child = window.findChild<ObjectType *>(QString::fromLatin1(name));
  158. require(child != nullptr, std::string("missing UI object ") + name);
  159. return child;
  160. }
  161. void testModeActionsControlEditingAvailability()
  162. {
  163. // 验证模式动作会同步禁用编辑入口,并在失败时恢复当前选择
  164. TestProjectStorage storage;
  165. ProjectService project_service(storage);
  166. HmiEditorService editor_service(project_service);
  167. LogicEditorService logic_editor_service(project_service);
  168. VirtualRegisterRepository repository;
  169. HmiRuntimeService runtime_service(repository);
  170. OfflineSimulationService simulation_service(repository);
  171. RuntimeModeService mode_service(project_service, simulation_service);
  172. RegisterMonitorService monitor_service(repository);
  173. MainWindow window(
  174. mode_service,
  175. project_service,
  176. editor_service,
  177. logic_editor_service,
  178. runtime_service,
  179. monitor_service);
  180. window.resize(1000, 640);
  181. window.show();
  182. QApplication::processEvents();
  183. require(window.findChild<QWidget *>(QStringLiteral("dataPointDock")) == nullptr,
  184. "the removed data point dock must not remain in the main window");
  185. require(window.findChild<QWidget *>(QStringLiteral("hmiDataPointComboBox")) == nullptr
  186. && window.findChild<QWidget *>(QStringLiteral("logicDataPointComboBox")) == nullptr,
  187. "HMI and ladder properties must use direct M/D address inputs");
  188. QAction *editing_action = requiredChild<QAction>(window, "editingModeAction");
  189. QAction *offline_action = requiredChild<QAction>(window, "offlineModeAction");
  190. QAction *online_action = requiredChild<QAction>(window, "onlineModeAction");
  191. QAction *add_button_action = requiredChild<QAction>(window, "addButtonAction");
  192. QAction *add_indicator_action = requiredChild<QAction>(window, "addIndicatorAction");
  193. QAction *delete_control_action = requiredChild<QAction>(window, "deleteControlAction");
  194. QAction *add_normally_open_action = requiredChild<QAction>(
  195. window, "addNormallyOpenAction");
  196. QAction *add_normal_coil_action = requiredChild<QAction>(
  197. window, "addNormalCoilAction");
  198. QAction *parallel_insert_action = requiredChild<QAction>(
  199. window, "parallelInsertAction");
  200. QDockWidget *project_dock = requiredChild<QDockWidget>(window, "projectDock");
  201. QDockWidget *properties_dock = requiredChild<QDockWidget>(window, "propertiesDock");
  202. QLabel *selection = requiredChild<QLabel>(window, "selectionValueLabel");
  203. QLineEdit *text_edit = requiredChild<QLineEdit>(window, "controlTextEdit");
  204. QComboBox *button_operation = requiredChild<QComboBox>(
  205. window, "buttonOperationComboBox");
  206. QPushButton *apply_properties = requiredChild<QPushButton>(
  207. window, "applyPropertiesButton");
  208. HmiEditorWidget *hmi_editor = requiredChild<HmiEditorWidget>(
  209. window, "hmiEditorWidget");
  210. QLabel *executor_status = requiredChild<QLabel>(window, "executorStatusLabel");
  211. QWidget *runtime_tab = requiredChild<QWidget>(window, "runtimeMonitorTab");
  212. QWidget *runtime_hmi = requiredChild<QWidget>(window, "runtimeHmiView");
  213. QWidget *runtime_logic = requiredChild<QWidget>(window, "runtimeLogicView");
  214. QWidget *free_monitor = requiredChild<QWidget>(window, "freeMonitorWidget");
  215. require(!runtime_tab->isVisible(),
  216. "runtime monitor workspace must be hidden while editing");
  217. const qreal compact_scale = hmi_editor->transform().m11();
  218. window.resize(1600, 900);
  219. QApplication::processEvents();
  220. require(hmi_editor->transform().m11() > compact_scale,
  221. "HMI page must refit when the window becomes larger");
  222. require(editing_action->isChecked(), "editing action must be selected initially");
  223. require(project_dock->isEnabled(), "project dock must be enabled while editing");
  224. require(properties_dock->isEnabled(), "properties dock must be enabled while editing");
  225. add_button_action->trigger();
  226. const std::string page_id = editor_service.firstPageId();
  227. require(editor_service.findPage(page_id)->controls.size() == 1,
  228. "adding a control must update the HMI page model");
  229. require(selection->text() == QStringLiteral("button-1(未绑定)"),
  230. "an unbound added control must be marked in the property panel");
  231. require(text_edit->text() == QStringLiteral("按钮"),
  232. "property panel must show the control text");
  233. require(button_operation->currentText() == QStringLiteral("瞬时 ON"),
  234. "new HMI buttons must default to momentary ON");
  235. const QList<QGraphicsItem *> unbound_items = hmi_editor->scene()->selectedItems();
  236. require(unbound_items.size() == 1 && unbound_items.front()->boundingRect().top() == 0,
  237. "an unbound HMI control must not reserve an address label area");
  238. button_operation->setCurrentIndex(
  239. button_operation->findData(static_cast<int>(HmiButtonOperation::Toggle)));
  240. apply_properties->click();
  241. require(editor_service.findControl(page_id, "button-1")->buttonOperation
  242. == HmiButtonOperation::Toggle,
  243. "the property panel must update the HMI button operation");
  244. HmiControl bound_button = *editor_service.findControl(page_id, "button-1");
  245. bound_button.binding = RegisterAddress{RegisterArea::M, 0};
  246. require(editor_service.updateControl(page_id, bound_button.id, bound_button).succeeded,
  247. "binding an HMI button to M0 must succeed");
  248. hmi_editor->reloadPage();
  249. hmi_editor->selectControl(bound_button.id);
  250. const QList<QGraphicsItem *> bound_items = hmi_editor->scene()->selectedItems();
  251. require(bound_items.size() == 1 && bound_items.front()->boundingRect().top() < 0,
  252. "a bound HMI control must include its address label above the control body");
  253. delete_control_action->trigger();
  254. require(editor_service.findPage(page_id)->controls.empty(),
  255. "deleting a selected control must update the HMI page model");
  256. add_indicator_action->trigger();
  257. HmiControl runtime_indicator = *editor_service.findControl(page_id, "indicator-1");
  258. runtime_indicator.binding = RegisterAddress{RegisterArea::M, 3};
  259. require(editor_service.updateControl(
  260. page_id, runtime_indicator.id, runtime_indicator).succeeded,
  261. "a configured HMI control must be available for the runtime projection");
  262. hmi_editor->reloadPage();
  263. add_normally_open_action->trigger();
  264. parallel_insert_action->trigger();
  265. add_normal_coil_action->trigger();
  266. const ControlLogic *logic = logic_editor_service.findLogic(
  267. logic_editor_service.firstLogicId());
  268. require(logic != nullptr && logic->rungs.size() == 1,
  269. "logic actions must edit the default ladder rung");
  270. require(logic->rungs.front().condition.has_value()
  271. && logic->rungs.front().condition->kind
  272. == ConditionExpressionKind::Parallel
  273. && logic->rungs.front().condition->children.size() == 2U,
  274. "parallel branch action must create a parallel expression");
  275. require(logic->rungs.front().output.has_value(),
  276. "coil action must set the fixed ladder output");
  277. offline_action->trigger();
  278. require(mode_service.mode() == ApplicationMode::Editing,
  279. "unconfigured ladder nodes must block offline running");
  280. const LadderRung &rung = logic->rungs.front();
  281. std::vector<const LogicNode *> condition_nodes;
  282. collectConditionNodes(*rung.condition, &condition_nodes);
  283. for (const LogicNode *node : condition_nodes)
  284. {
  285. require(logic_editor_service.updateNodeConfig(
  286. logic_editor_service.firstLogicId(),
  287. node->id,
  288. ContactNodeConfig{
  289. RegisterAddress{RegisterArea::M, 0},
  290. ContactMode::NormallyOpen})
  291. .succeeded,
  292. "applying a contact configuration must complete the ladder node");
  293. }
  294. require(logic_editor_service.updateNodeConfig(
  295. logic_editor_service.firstLogicId(),
  296. rung.output->id,
  297. CoilNodeConfig{
  298. RegisterAddress{RegisterArea::M, 1},
  299. CoilMode::Normal})
  300. .succeeded,
  301. "applying a coil configuration must complete the ladder node");
  302. offline_action->trigger();
  303. require(mode_service.mode() == ApplicationMode::OfflineRunning,
  304. "offline action must enter offline running");
  305. require(simulation_service.state() == SimulationState::Running,
  306. "offline action must start the actual simulation service");
  307. require(executor_status->text().contains(QStringLiteral("运行")),
  308. "executor status label must report the actual running state");
  309. require(!project_dock->isEnabled(),
  310. "project dock must be disabled while running");
  311. require(!properties_dock->isEnabled(),
  312. "properties dock must be disabled while running");
  313. require(!add_button_action->isEnabled(),
  314. "HMI add controls must be disabled while running");
  315. require(!add_normally_open_action->isEnabled(),
  316. "logic add nodes must be disabled while running");
  317. require(runtime_tab->isVisible() && runtime_hmi->isVisible()
  318. && runtime_logic->isVisible() && free_monitor->isVisible(),
  319. "offline running must show HMI, ladder trace and free monitor together");
  320. auto *runtime_hmi_view = qobject_cast<HmiEditorWidget *>(runtime_hmi);
  321. require(runtime_hmi_view != nullptr
  322. && runtime_hmi_view->scene()->items().size() == 2,
  323. "offline runtime HMI must project the configured page control");
  324. QLineEdit *monitor_address = requiredChild<QLineEdit>(window, "addressEdit");
  325. QPushButton *monitor_add = requiredChild<QPushButton>(window, "addButton");
  326. QTableWidget *monitor_table = requiredChild<QTableWidget>(window, "monitorTable");
  327. monitor_address->setText(QStringLiteral("M0"));
  328. monitor_add->click();
  329. repository.writeBit({RegisterArea::M, 0}, true);
  330. auto *monitor_widget = requiredChild<FreeMonitorWidget>(window, "freeMonitorWidget");
  331. monitor_widget->refreshValues(
  332. ApplicationMode::OfflineRunning, PlcConnectionState::Disconnected);
  333. require(monitor_table->rowCount() == 1
  334. && monitor_table->item(0, 2)->text() == QStringLiteral("ON"),
  335. "offline free monitor must read the same virtual M/D repository as HMI");
  336. online_action->trigger();
  337. require(mode_service.mode() == ApplicationMode::OfflineRunning,
  338. "running modes must not switch directly through the UI");
  339. require(offline_action->isChecked(),
  340. "failed mode changes must restore the active action");
  341. editing_action->trigger();
  342. require(mode_service.mode() == ApplicationMode::Editing,
  343. "editing action must return to editing");
  344. require(simulation_service.state() == SimulationState::Stopped,
  345. "editing action must stop the simulation service");
  346. require(executor_status->text() == QStringLiteral("逻辑执行器:停止"),
  347. "executor status label must report the actual stopped state");
  348. require(project_dock->isEnabled(),
  349. "project dock must be restored after returning to editing");
  350. require(properties_dock->isEnabled(),
  351. "properties dock must be restored after returning to editing");
  352. require(add_button_action->isEnabled(),
  353. "HMI add controls must be restored after returning to editing");
  354. require(add_normally_open_action->isEnabled(),
  355. "logic add nodes must be restored after returning to editing");
  356. require(!runtime_tab->isVisible(),
  357. "returning to editing must hide the runtime monitor workspace");
  358. online_action->trigger();
  359. require(mode_service.mode() == ApplicationMode::Editing,
  360. "online action must require an initial PLC read");
  361. require(editing_action->isChecked(),
  362. "rejected online running must restore the editing action");
  363. }
  364. void testPlcConfigurationUsesDialog()
  365. {
  366. PlcSerialConfiguration initial;
  367. initial.portName = "COM17";
  368. initial.serverAddress = 12;
  369. initial.baudRate = 38400;
  370. initial.dataBits = 7;
  371. initial.parity = 3;
  372. initial.stopBits = 2;
  373. initial.responseTimeoutMs = 2500;
  374. initial.retries = 4;
  375. initial.pollIntervalMs = 350;
  376. PlcConnectionDialog configuration_dialog(initial);
  377. const PlcSerialConfiguration actual = configuration_dialog.configuration();
  378. require(actual.portName == initial.portName,
  379. "PLC dialog must preserve a manually entered serial port");
  380. require(actual.serverAddress == initial.serverAddress
  381. && actual.baudRate == initial.baudRate
  382. && actual.dataBits == initial.dataBits
  383. && actual.parity == initial.parity
  384. && actual.stopBits == initial.stopBits,
  385. "PLC dialog must preserve Modbus RTU serial parameters");
  386. require(actual.responseTimeoutMs == initial.responseTimeoutMs
  387. && actual.retries == initial.retries
  388. && actual.pollIntervalMs == initial.pollIntervalMs,
  389. "PLC dialog must preserve communication timing parameters");
  390. TestProjectStorage storage;
  391. ProjectService project_service(storage);
  392. HmiEditorService editor_service(project_service);
  393. LogicEditorService logic_editor_service(project_service);
  394. VirtualRegisterRepository repository;
  395. HmiRuntimeService runtime_service(repository);
  396. OfflineSimulationService simulation_service(repository);
  397. RuntimeModeService mode_service(project_service, simulation_service);
  398. RegisterMonitorService monitor_service(repository);
  399. MainWindow window(
  400. mode_service,
  401. project_service,
  402. editor_service,
  403. logic_editor_service,
  404. runtime_service,
  405. monitor_service);
  406. require(window.findChild<QComboBox *>(QStringLiteral("serialPortComboBox")) == nullptr,
  407. "serial configuration controls must not be embedded in the main window");
  408. QAction *configure_action = requiredChild<QAction>(window, "configurePlcAction");
  409. bool dialog_opened = false;
  410. QTimer::singleShot(
  411. 0,
  412. [&dialog_opened]
  413. {
  414. auto *dialog = qobject_cast<PlcConnectionDialog *>(
  415. QApplication::activeModalWidget());
  416. dialog_opened = dialog != nullptr;
  417. if (dialog != nullptr)
  418. {
  419. dialog->reject();
  420. }
  421. });
  422. configure_action->trigger();
  423. require(dialog_opened,
  424. "PLC configuration action must open the dedicated configuration dialog");
  425. }
  426. void testRepeatedPlcStatusNotificationsAreCoalesced()
  427. {
  428. TestProjectStorage storage;
  429. ProjectService project_service(storage);
  430. HmiEditorService editor_service(project_service);
  431. LogicEditorService logic_editor_service(project_service);
  432. VirtualRegisterRepository virtual_repository;
  433. VirtualRegisterRepository plc_repository;
  434. ActiveRegisterRepository active_repository(virtual_repository);
  435. HmiRuntimeService runtime_service(active_repository);
  436. OfflineSimulationService simulation_service(virtual_repository);
  437. RepeatedStatusGateway gateway;
  438. RuntimeModeService mode_service(project_service, simulation_service);
  439. RegisterMonitorService monitor_service(active_repository);
  440. mode_service.configurePlc(
  441. gateway, active_repository, virtual_repository, plc_repository);
  442. MainWindow window(
  443. mode_service,
  444. project_service,
  445. editor_service,
  446. logic_editor_service,
  447. runtime_service,
  448. monitor_service);
  449. QListWidget *output = requiredChild<QListWidget>(window, "outputList");
  450. const int initial_count = output->count();
  451. const PlcCommunicationResult result = mode_service.connectPlc(
  452. {"COM3", 1, 9600, 8, 2, 1, 1000, 2, 200});
  453. require(result.succeeded, "PLC connection setup must succeed");
  454. QApplication::processEvents();
  455. const QString expected = QStringLiteral("PLC 已连接,正在读取工程使用的 M/D 地址");
  456. int matching_count = 0;
  457. for (int index = initial_count; index < output->count(); ++index)
  458. {
  459. if (output->item(index)->text() == expected)
  460. {
  461. ++matching_count;
  462. }
  463. }
  464. require(matching_count == 1,
  465. "repeated PLC status callbacks must append one coalesced output message");
  466. }
  467. void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly()
  468. {
  469. TestProjectStorage storage;
  470. ProjectService project_service(storage);
  471. HmiEditorService editor_service(project_service);
  472. LogicEditorService logic_editor_service(project_service);
  473. VirtualRegisterRepository virtual_repository;
  474. VirtualRegisterRepository plc_repository;
  475. ActiveRegisterRepository active_repository(virtual_repository);
  476. HmiRuntimeService runtime_service(active_repository);
  477. OfflineSimulationService simulation_service(virtual_repository);
  478. OnlineReadyGateway gateway;
  479. RuntimeModeService mode_service(project_service, simulation_service);
  480. RegisterMonitorService monitor_service(active_repository);
  481. mode_service.configurePlc(
  482. gateway, active_repository, virtual_repository, plc_repository);
  483. MainWindow window(
  484. mode_service,
  485. project_service,
  486. editor_service,
  487. logic_editor_service,
  488. runtime_service,
  489. monitor_service);
  490. window.resize(1200, 760);
  491. window.show();
  492. QApplication::processEvents();
  493. require(mode_service.connectPlc(
  494. {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded,
  495. "online workspace test must connect the fake PLC gateway");
  496. gateway.completeInitialRead();
  497. QAction *online_action = requiredChild<QAction>(window, "onlineModeAction");
  498. online_action->trigger();
  499. QApplication::processEvents();
  500. QWidget *runtime_tab = requiredChild<QWidget>(window, "runtimeMonitorTab");
  501. QWidget *runtime_hmi = requiredChild<QWidget>(window, "runtimeHmiView");
  502. QWidget *runtime_logic = requiredChild<QWidget>(window, "runtimeLogicView");
  503. QWidget *free_monitor = requiredChild<QWidget>(window, "freeMonitorWidget");
  504. require(mode_service.mode() == ApplicationMode::OnlineRunning,
  505. "completed PLC initial read must allow online running");
  506. require(runtime_tab->isVisible() && runtime_hmi->isVisible()
  507. && free_monitor->isVisible(),
  508. "online running must show HMI and free monitor together");
  509. require(!runtime_logic->isVisible(),
  510. "online running must not show a misleading local ladder runtime trace");
  511. QLineEdit *monitor_address = requiredChild<QLineEdit>(window, "addressEdit");
  512. QPushButton *monitor_add = requiredChild<QPushButton>(window, "addButton");
  513. monitor_address->setText(QStringLiteral("D9"));
  514. monitor_add->click();
  515. require(std::find(
  516. gateway.poll_addresses.begin(),
  517. gateway.poll_addresses.end(),
  518. RegisterAddress{RegisterArea::D, 9}) != gateway.poll_addresses.end(),
  519. "online free monitor addresses must join the active PLC poll set immediately");
  520. }
  521. } // namespace
  522. int main(int argc, char *argv[])
  523. {
  524. // 无窗口平台使 Qt Widgets 测试可在自动化环境稳定运行
  525. qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("offscreen"));
  526. QApplication application(argc, argv);
  527. try
  528. {
  529. testModeActionsControlEditingAvailability();
  530. testPlcConfigurationUsesDialog();
  531. testRepeatedPlcStatusNotificationsAreCoalesced();
  532. testOnlineWorkspaceShowsHmiAndFreeMonitorOnly();
  533. }
  534. catch (const std::exception &error)
  535. {
  536. std::cerr << "main window tests failed: " << error.what() << '\n';
  537. return 1;
  538. }
  539. std::cout << "main window tests passed\n";
  540. return 0;
  541. }