综合平台编程器项目的远程存储
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 

298 rindas
12 KiB

  1. #include "domain/project_storage.h"
  2. #include "domain/register_repository.h"
  3. #include "services/hmi_editor_service.h"
  4. #include "services/hmi_runtime_service.h"
  5. #include "services/logic_editor_service.h"
  6. #include "services/offline_simulation_service.h"
  7. #include "services/project_service.h"
  8. #include "services/runtime_mode_service.h"
  9. #include "ui/hmi_editor_widget.h"
  10. #include "ui/logic_editor_widget.h"
  11. #include "ui/main_window.h"
  12. #include "ui/plc_connection_dialog.h"
  13. #include <QAction>
  14. #include <QApplication>
  15. #include <QComboBox>
  16. #include <QDialog>
  17. #include <QDockWidget>
  18. #include <QLabel>
  19. #include <QLineEdit>
  20. #include <QSpinBox>
  21. #include <QTimer>
  22. #include <iostream>
  23. #include <stdexcept>
  24. #include <string>
  25. namespace {
  26. class TestProjectStorage final : public ProjectStorage
  27. {
  28. public:
  29. // 主窗口测试使用无副作用存储实现,隔离文件对 UI 行为的影响
  30. ProjectSaveResult save(const Project &, const std::string &) override
  31. {
  32. return {true, ProjectStorageError::None, {}};
  33. }
  34. ProjectLoadResult load(const std::string &) override
  35. {
  36. return {false, {}, ProjectStorageError::FileReadFailed, {}};
  37. }
  38. };
  39. void require(bool condition, const std::string &message)
  40. {
  41. if (!condition)
  42. {
  43. throw std::runtime_error(message);
  44. }
  45. }
  46. template<typename ObjectType>
  47. ObjectType *requiredChild(MainWindow &window, const char *name)
  48. {
  49. // 通过 objectName 取得 Designer 组件,缺失时给出明确测试失败信息
  50. ObjectType *child = window.findChild<ObjectType *>(QString::fromLatin1(name));
  51. require(child != nullptr, std::string("missing UI object ") + name);
  52. return child;
  53. }
  54. void testModeActionsControlEditingAvailability()
  55. {
  56. // 验证模式动作会同步禁用编辑入口,并在失败时恢复当前选择
  57. TestProjectStorage storage;
  58. ProjectService project_service(storage);
  59. HmiEditorService editor_service(project_service);
  60. LogicEditorService logic_editor_service(project_service);
  61. VirtualRegisterRepository repository;
  62. HmiRuntimeService runtime_service(repository);
  63. OfflineSimulationService simulation_service(repository);
  64. RuntimeModeService mode_service(project_service, simulation_service);
  65. MainWindow window(
  66. mode_service,
  67. project_service,
  68. editor_service,
  69. logic_editor_service,
  70. runtime_service);
  71. window.resize(1000, 640);
  72. window.show();
  73. QApplication::processEvents();
  74. require(window.findChild<QWidget *>(QStringLiteral("dataPointDock")) == nullptr,
  75. "the removed data point dock must not remain in the main window");
  76. require(window.findChild<QWidget *>(QStringLiteral("hmiDataPointComboBox")) == nullptr
  77. && window.findChild<QWidget *>(QStringLiteral("logicDataPointComboBox")) == nullptr,
  78. "HMI and ladder properties must use direct M/D address inputs");
  79. QAction *editing_action = requiredChild<QAction>(window, "editingModeAction");
  80. QAction *offline_action = requiredChild<QAction>(window, "offlineModeAction");
  81. QAction *online_action = requiredChild<QAction>(window, "onlineModeAction");
  82. QAction *add_button_action = requiredChild<QAction>(window, "addButtonAction");
  83. QAction *delete_control_action = requiredChild<QAction>(window, "deleteControlAction");
  84. QAction *add_normally_open_action = requiredChild<QAction>(
  85. window, "addNormallyOpenAction");
  86. QAction *add_normal_coil_action = requiredChild<QAction>(
  87. window, "addNormalCoilAction");
  88. QAction *parallel_insert_action = requiredChild<QAction>(
  89. window, "parallelInsertAction");
  90. QDockWidget *project_dock = requiredChild<QDockWidget>(window, "projectDock");
  91. QDockWidget *properties_dock = requiredChild<QDockWidget>(window, "propertiesDock");
  92. QLabel *selection = requiredChild<QLabel>(window, "selectionValueLabel");
  93. QLineEdit *text_edit = requiredChild<QLineEdit>(window, "controlTextEdit");
  94. HmiEditorWidget *hmi_editor = requiredChild<HmiEditorWidget>(
  95. window, "hmiEditorWidget");
  96. QLabel *executor_status = requiredChild<QLabel>(window, "executorStatusLabel");
  97. const qreal compact_scale = hmi_editor->transform().m11();
  98. window.resize(1600, 900);
  99. QApplication::processEvents();
  100. require(hmi_editor->transform().m11() > compact_scale,
  101. "HMI page must refit when the window becomes larger");
  102. require(editing_action->isChecked(), "editing action must be selected initially");
  103. require(project_dock->isEnabled(), "project dock must be enabled while editing");
  104. require(properties_dock->isEnabled(), "properties dock must be enabled while editing");
  105. add_button_action->trigger();
  106. const std::string page_id = editor_service.firstPageId();
  107. require(editor_service.findPage(page_id)->controls.size() == 1,
  108. "adding a control must update the HMI page model");
  109. require(selection->text() == QStringLiteral("button-1(未绑定)"),
  110. "an unbound added control must be marked in the property panel");
  111. require(text_edit->text() == QStringLiteral("按钮"),
  112. "property panel must show the control text");
  113. delete_control_action->trigger();
  114. require(editor_service.findPage(page_id)->controls.empty(),
  115. "deleting a selected control must update the HMI page model");
  116. add_normally_open_action->trigger();
  117. parallel_insert_action->trigger();
  118. add_normal_coil_action->trigger();
  119. const ControlLogic *logic = logic_editor_service.findLogic(
  120. logic_editor_service.firstLogicId());
  121. require(logic != nullptr && logic->rungs.size() == 1,
  122. "logic actions must edit the default ladder rung");
  123. require(logic->rungs.front().condition.has_value()
  124. && logic->rungs.front().condition->kind
  125. == ConditionExpressionKind::Parallel
  126. && logic->rungs.front().condition->children.size() == 2U,
  127. "parallel branch action must create a parallel expression");
  128. require(logic->rungs.front().output.has_value(),
  129. "coil action must set the fixed ladder output");
  130. offline_action->trigger();
  131. require(mode_service.mode() == ApplicationMode::Editing,
  132. "unconfigured ladder nodes must block offline running");
  133. const LadderRung &rung = logic->rungs.front();
  134. std::vector<const LogicNode *> condition_nodes;
  135. collectConditionNodes(*rung.condition, &condition_nodes);
  136. for (const LogicNode *node : condition_nodes)
  137. {
  138. require(logic_editor_service.updateNodeConfig(
  139. logic_editor_service.firstLogicId(),
  140. node->id,
  141. ContactNodeConfig{
  142. RegisterAddress{RegisterArea::M, 0},
  143. ContactMode::NormallyOpen})
  144. .succeeded,
  145. "applying a contact configuration must complete the ladder node");
  146. }
  147. require(logic_editor_service.updateNodeConfig(
  148. logic_editor_service.firstLogicId(),
  149. rung.output->id,
  150. CoilNodeConfig{
  151. RegisterAddress{RegisterArea::M, 1},
  152. CoilMode::Normal})
  153. .succeeded,
  154. "applying a coil configuration must complete the ladder node");
  155. offline_action->trigger();
  156. require(mode_service.mode() == ApplicationMode::OfflineRunning,
  157. "offline action must enter offline running");
  158. require(simulation_service.state() == SimulationState::Running,
  159. "offline action must start the actual simulation service");
  160. require(executor_status->text().contains(QStringLiteral("运行")),
  161. "executor status label must report the actual running state");
  162. require(!project_dock->isEnabled(),
  163. "project dock must be disabled while running");
  164. require(!properties_dock->isEnabled(),
  165. "properties dock must be disabled while running");
  166. require(!add_button_action->isEnabled(),
  167. "HMI add controls must be disabled while running");
  168. require(!add_normally_open_action->isEnabled(),
  169. "logic add nodes must be disabled while running");
  170. online_action->trigger();
  171. require(mode_service.mode() == ApplicationMode::OfflineRunning,
  172. "running modes must not switch directly through the UI");
  173. require(offline_action->isChecked(),
  174. "failed mode changes must restore the active action");
  175. editing_action->trigger();
  176. require(mode_service.mode() == ApplicationMode::Editing,
  177. "editing action must return to editing");
  178. require(simulation_service.state() == SimulationState::Stopped,
  179. "editing action must stop the simulation service");
  180. require(executor_status->text() == QStringLiteral("逻辑执行器:停止"),
  181. "executor status label must report the actual stopped state");
  182. require(project_dock->isEnabled(),
  183. "project dock must be restored after returning to editing");
  184. require(properties_dock->isEnabled(),
  185. "properties dock must be restored after returning to editing");
  186. require(add_button_action->isEnabled(),
  187. "HMI add controls must be restored after returning to editing");
  188. require(add_normally_open_action->isEnabled(),
  189. "logic add nodes must be restored after returning to editing");
  190. online_action->trigger();
  191. require(mode_service.mode() == ApplicationMode::Editing,
  192. "online action must require an initial PLC read");
  193. require(editing_action->isChecked(),
  194. "rejected online running must restore the editing action");
  195. }
  196. void testPlcConfigurationUsesDialog()
  197. {
  198. PlcSerialConfiguration initial;
  199. initial.portName = "COM17";
  200. initial.serverAddress = 12;
  201. initial.baudRate = 38400;
  202. initial.dataBits = 7;
  203. initial.parity = 3;
  204. initial.stopBits = 2;
  205. initial.responseTimeoutMs = 2500;
  206. initial.retries = 4;
  207. initial.pollIntervalMs = 350;
  208. PlcConnectionDialog configuration_dialog(initial);
  209. const PlcSerialConfiguration actual = configuration_dialog.configuration();
  210. require(actual.portName == initial.portName,
  211. "PLC dialog must preserve a manually entered serial port");
  212. require(actual.serverAddress == initial.serverAddress
  213. && actual.baudRate == initial.baudRate
  214. && actual.dataBits == initial.dataBits
  215. && actual.parity == initial.parity
  216. && actual.stopBits == initial.stopBits,
  217. "PLC dialog must preserve Modbus RTU serial parameters");
  218. require(actual.responseTimeoutMs == initial.responseTimeoutMs
  219. && actual.retries == initial.retries
  220. && actual.pollIntervalMs == initial.pollIntervalMs,
  221. "PLC dialog must preserve communication timing parameters");
  222. TestProjectStorage storage;
  223. ProjectService project_service(storage);
  224. HmiEditorService editor_service(project_service);
  225. LogicEditorService logic_editor_service(project_service);
  226. VirtualRegisterRepository repository;
  227. HmiRuntimeService runtime_service(repository);
  228. OfflineSimulationService simulation_service(repository);
  229. RuntimeModeService mode_service(project_service, simulation_service);
  230. MainWindow window(
  231. mode_service,
  232. project_service,
  233. editor_service,
  234. logic_editor_service,
  235. runtime_service);
  236. require(window.findChild<QComboBox *>(QStringLiteral("serialPortComboBox")) == nullptr,
  237. "serial configuration controls must not be embedded in the main window");
  238. QAction *configure_action = requiredChild<QAction>(window, "configurePlcAction");
  239. bool dialog_opened = false;
  240. QTimer::singleShot(
  241. 0,
  242. [&dialog_opened]
  243. {
  244. auto *dialog = qobject_cast<PlcConnectionDialog *>(
  245. QApplication::activeModalWidget());
  246. dialog_opened = dialog != nullptr;
  247. if (dialog != nullptr)
  248. {
  249. dialog->reject();
  250. }
  251. });
  252. configure_action->trigger();
  253. require(dialog_opened,
  254. "PLC configuration action must open the dedicated configuration dialog");
  255. }
  256. } // namespace
  257. int main(int argc, char *argv[])
  258. {
  259. // 无窗口平台使 Qt Widgets 测试可在自动化环境稳定运行
  260. qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("offscreen"));
  261. QApplication application(argc, argv);
  262. try
  263. {
  264. testModeActionsControlEditingAvailability();
  265. testPlcConfigurationUsesDialog();
  266. }
  267. catch (const std::exception &error)
  268. {
  269. std::cerr << "main window tests failed: " << error.what() << '\n';
  270. return 1;
  271. }
  272. std::cout << "main window tests passed\n";
  273. return 0;
  274. }