综合平台编程器项目的远程存储
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

387 lines
15 KiB

  1. #include "domain/active_register_repository.h"
  2. #include "domain/project_limits.h"
  3. #include "domain/project_storage.h"
  4. #include "infrastructure/plc_communication_error_classifier.h"
  5. #include "infrastructure/plc_communication_service.h"
  6. #include "support/test_support.h"
  7. #include "infrastructure/plc_register_repository.h"
  8. #include "domain/virtual_register_repository.h"
  9. #include "services/offline_simulation_service.h"
  10. #include "services/plc_communication_gateway.h"
  11. #include "services/project_service.h"
  12. #include "services/runtime_mode_service.h"
  13. #include <functional>
  14. #include <iostream>
  15. #include <stdexcept>
  16. #include <utility>
  17. namespace {
  18. using TestProjectStorage = TestSupport::InMemoryProjectStorage;
  19. class FakePlcGateway final : public PlcCommunicationGateway
  20. {
  21. public:
  22. PlcCommunicationResult connectDevice(
  23. const PlcSerialConfiguration &configuration) override
  24. {
  25. last_configuration = configuration;
  26. last_error_type = PlcCommunicationError::None;
  27. last_error.clear();
  28. initial_read = false;
  29. connection_state = PlcConnectionState::Connected;
  30. if (state_changed)
  31. {
  32. state_changed();
  33. }
  34. return {true, {}};
  35. }
  36. void disconnectDevice() override
  37. {
  38. ++disconnect_count;
  39. connection_state = PlcConnectionState::Disconnected;
  40. initial_read = false;
  41. if (initial_read_changed)
  42. {
  43. initial_read_changed(false);
  44. }
  45. if (state_changed)
  46. {
  47. state_changed();
  48. }
  49. }
  50. PlcCommunicationResult setPollAddresses(
  51. const std::vector<RegisterAddress> &addresses) override
  52. {
  53. poll_addresses = addresses;
  54. return {true, {}};
  55. }
  56. PlcConnectionState state() const override { return connection_state; }
  57. bool initialReadCompleted() const override { return initial_read; }
  58. PlcCommunicationError lastErrorType() const override { return last_error_type; }
  59. const std::string &lastError() const override { return last_error; }
  60. void setCallbacks(
  61. std::function<void()> state_callback,
  62. std::function<void(bool)> initial_callback,
  63. std::function<void()> cache_callback,
  64. std::function<void(const std::string &)> error_callback) override
  65. {
  66. state_changed = std::move(state_callback);
  67. initial_read_changed = std::move(initial_callback);
  68. cache_updated = std::move(cache_callback);
  69. error_reported = std::move(error_callback);
  70. }
  71. void completeInitialRead()
  72. {
  73. initial_read = true;
  74. if (initial_read_changed)
  75. {
  76. initial_read_changed(true);
  77. }
  78. }
  79. void failCommunication(
  80. PlcCommunicationError error_type, const std::string &message)
  81. {
  82. initial_read = false;
  83. last_error_type = error_type;
  84. last_error = message;
  85. connection_state = PlcConnectionState::Faulted;
  86. if (initial_read_changed)
  87. {
  88. initial_read_changed(false);
  89. }
  90. if (state_changed)
  91. {
  92. state_changed();
  93. }
  94. if (error_reported)
  95. {
  96. error_reported(last_error);
  97. }
  98. }
  99. PlcConnectionState connection_state = PlcConnectionState::Disconnected;
  100. bool initial_read = false;
  101. int disconnect_count = 0;
  102. PlcCommunicationError last_error_type = PlcCommunicationError::None;
  103. std::string last_error;
  104. PlcSerialConfiguration last_configuration;
  105. std::vector<RegisterAddress> poll_addresses;
  106. std::function<void()> state_changed;
  107. std::function<void(bool)> initial_read_changed;
  108. std::function<void()> cache_updated;
  109. std::function<void(const std::string &)> error_reported;
  110. };
  111. using TestSupport::require;
  112. void testPlcCacheAndWriteForwarding()
  113. {
  114. PlcRegisterRepository repository;
  115. const RegisterAddress m0{RegisterArea::M, 0};
  116. const RegisterAddress d0{RegisterArea::D, 0};
  117. require(repository.readBit(m0).error == RegisterError::Unavailable,
  118. "uninitialized PLC bit cache must be unavailable");
  119. require(repository.readWord(d0).error == RegisterError::Unavailable,
  120. "uninitialized PLC word cache must be unavailable");
  121. repository.updateBit(0, true);
  122. repository.updateWord(0, -123);
  123. require(repository.readBit(m0).succeeded && repository.readBit(m0).value,
  124. "valid PLC bit cache must be readable");
  125. require(repository.readWord(d0).succeeded
  126. && repository.readWord(d0).value == -123,
  127. "valid PLC word cache must preserve signed values");
  128. RegisterAddress written_address{RegisterArea::M, 1};
  129. bool written_bit = false;
  130. std::int16_t written_word = 0;
  131. repository.setWriteHandlers(
  132. [&](const RegisterAddress &address, bool value)
  133. {
  134. written_address = address;
  135. written_bit = value;
  136. return RegisterWriteResult{true, RegisterError::None};
  137. },
  138. [&](const RegisterAddress &address, std::int16_t value)
  139. {
  140. written_address = address;
  141. written_word = value;
  142. return RegisterWriteResult{true, RegisterError::None};
  143. });
  144. require(repository.writeBit(m0, false).succeeded
  145. && written_address == m0 && !written_bit,
  146. "PLC bit writes must be forwarded without changing the cache");
  147. require(repository.writeWord(d0, 456).succeeded
  148. && written_address == d0 && written_word == 456,
  149. "PLC word writes must be forwarded without changing the cache");
  150. repository.invalidate();
  151. require(!repository.hasAnyValidValue(),
  152. "disconnecting must invalidate all PLC cache validity flags");
  153. }
  154. void testRuntimeRepositorySwitchingAndDisconnect()
  155. {
  156. TestProjectStorage storage;
  157. ProjectService project_service(storage);
  158. HmiPage page;
  159. page.id = "main-page";
  160. page.name = "Main";
  161. HmiControl indicator;
  162. indicator.id = "run-state";
  163. indicator.type = HmiControlType::Indicator;
  164. indicator.bounds = {0, 0, 80, 40};
  165. indicator.text = "Run";
  166. indicator.binding = RegisterAddress{RegisterArea::M, 5};
  167. page.controls.push_back(indicator);
  168. project_service.editProject().hmiPages.push_back(page);
  169. VirtualRegisterRepository virtual_repository;
  170. PlcRegisterRepository plc_repository;
  171. ActiveRegisterRepository active_repository(virtual_repository);
  172. OfflineSimulationService simulation_service(virtual_repository);
  173. FakePlcGateway gateway;
  174. RuntimeModeService service(project_service, simulation_service);
  175. service.configurePlc(
  176. gateway, active_repository, virtual_repository, plc_repository);
  177. const PlcCommunicationResult connected = service.connectPlc(
  178. {"COM9", 2, 19200, 8, 2, 1, 1000, 2, 200});
  179. require(connected.succeeded && gateway.poll_addresses.size() == 1U,
  180. "PLC connection must receive the project address poll set");
  181. service.setMonitorAddresses({
  182. RegisterAddress{RegisterArea::M, 5},
  183. RegisterAddress{RegisterArea::D, 8}});
  184. require(gateway.poll_addresses.size() == 2U
  185. && gateway.poll_addresses.front()
  186. == RegisterAddress{RegisterArea::M, 5}
  187. && gateway.poll_addresses.back()
  188. == RegisterAddress{RegisterArea::D, 8},
  189. "monitor addresses must merge with and deduplicate project poll addresses");
  190. require(service.enterOnlineRunning().error
  191. == ModeTransitionError::InitialPlcReadRequired,
  192. "online mode must wait for the first valid PLC read");
  193. plc_repository.updateBit(5, true);
  194. gateway.completeInitialRead();
  195. require(service.enterOnlineRunning().succeeded,
  196. "online mode must start after the first valid PLC read");
  197. require(active_repository.readBit({RegisterArea::M, 5}).succeeded
  198. && active_repository.readBit({RegisterArea::M, 5}).value,
  199. "online mode must expose the PLC cache through the active repository");
  200. gateway.disconnectDevice();
  201. require(service.mode() == ApplicationMode::Editing,
  202. "an online disconnect must return the application to editing mode");
  203. require(!service.initialPlcReadCompleted(),
  204. "an online disconnect must clear the initial read flag");
  205. require(active_repository.readBit({RegisterArea::M, 5}).succeeded
  206. && !active_repository.readBit({RegisterArea::M, 5}).value,
  207. "editing after disconnect must switch back to the virtual repository");
  208. }
  209. void testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect()
  210. {
  211. TestProjectStorage storage;
  212. ProjectService project_service(storage);
  213. VirtualRegisterRepository virtual_repository;
  214. PlcRegisterRepository plc_repository;
  215. ActiveRegisterRepository active_repository(virtual_repository);
  216. OfflineSimulationService simulation_service(virtual_repository);
  217. FakePlcGateway gateway;
  218. RuntimeModeService service(project_service, simulation_service);
  219. service.configurePlc(
  220. gateway, active_repository, virtual_repository, plc_repository);
  221. require(service.connectPlc(
  222. {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded,
  223. "PLC connection must start before testing a communication fault");
  224. gateway.completeInitialRead();
  225. require(service.enterOnlineRunning().succeeded,
  226. "completed initial read must allow online running before a fault");
  227. gateway.failCommunication(
  228. PlcCommunicationError::CommunicationTimeout,
  229. "PLC communication timed out while the serial port remained open");
  230. require(service.mode() == ApplicationMode::Editing,
  231. "a PLC communication fault must return online running to editing");
  232. require(!service.initialPlcReadCompleted(),
  233. "a PLC communication fault must revoke initial read readiness");
  234. require(service.enterOnlineRunning().error
  235. == ModeTransitionError::InitialPlcReadRequired,
  236. "faulted PLC state must not reuse readiness from the previous connection");
  237. const int disconnect_count = gateway.disconnect_count;
  238. require(service.connectPlc(
  239. {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded,
  240. "faulted PLC state must support direct reconnect");
  241. require(gateway.disconnect_count == disconnect_count + 1,
  242. "reconnecting from a fault must clean the previous serial session first");
  243. }
  244. void testPlcCommunicationErrorClassification()
  245. {
  246. PlcCommunicationErrorContext context;
  247. context.portName = QStringLiteral("COM3");
  248. PlcCommunicationFailure failure = classifyPlcCommunicationError(
  249. QModbusDevice::ConnectionError, context);
  250. require(failure.type == PlcCommunicationError::SerialPortOpenFailed
  251. && failure.message.contains(QStringLiteral("未能打开")),
  252. "connection failure before opening the port must be classified explicitly");
  253. context.serialSessionOpened = true;
  254. failure = classifyPlcCommunicationError(QModbusDevice::TimeoutError, context);
  255. require(failure.type == PlcCommunicationError::PlcNotResponding
  256. && failure.message.contains(QStringLiteral("PLC 未响应")),
  257. "initial timeout with an open serial port must report a non-responsive PLC");
  258. context.receivedValidResponse = true;
  259. failure = classifyPlcCommunicationError(QModbusDevice::TimeoutError, context);
  260. require(failure.type == PlcCommunicationError::CommunicationTimeout
  261. && failure.message.contains(QStringLiteral("仍处于打开状态")),
  262. "timeout after valid traffic must report an open local serial session");
  263. failure = classifyPlcCommunicationError(QModbusDevice::ConnectionError, context);
  264. require(failure.type == PlcCommunicationError::SerialConnectionLost
  265. && failure.message.contains(QStringLiteral("连接已中断")),
  266. "connection errors after opening the port must report a lost serial link");
  267. failure = classifyPlcCommunicationError(QModbusDevice::ProtocolError, context);
  268. require(failure.type == PlcCommunicationError::ProtocolError
  269. && failure.message.contains(QStringLiteral("Modbus 协议异常")),
  270. "protocol errors must remain distinct from timeouts and disconnections");
  271. }
  272. void testPlcConfigurationBoundaries()
  273. {
  274. PlcSerialConfiguration configuration;
  275. configuration.portName = "COM3";
  276. require(validatePlcSerialConfiguration(configuration).succeeded,
  277. "the standard COM3 9600 8E1 configuration must be accepted");
  278. configuration.serverAddress = ProjectLimits::kMaximumPlcServerAddress;
  279. require(validatePlcSerialConfiguration(configuration).succeeded,
  280. "PLC station 247 must be accepted");
  281. configuration.serverAddress = ProjectLimits::kMaximumPlcServerAddress + 1;
  282. require(!validatePlcSerialConfiguration(configuration).succeeded,
  283. "PLC station 248 must be rejected");
  284. configuration.serverAddress = 1;
  285. configuration.retries = ProjectLimits::kMaximumRetries;
  286. require(validatePlcSerialConfiguration(configuration).succeeded,
  287. "five retries must be accepted");
  288. configuration.retries = ProjectLimits::kMaximumRetries + 1;
  289. require(!validatePlcSerialConfiguration(configuration).succeeded,
  290. "six retries must be rejected");
  291. configuration.retries = 0;
  292. configuration.responseTimeoutMs = ProjectLimits::kMinimumResponseTimeoutMs - 1;
  293. require(!validatePlcSerialConfiguration(configuration).succeeded,
  294. "a response timeout below 100 ms must be rejected");
  295. configuration.responseTimeoutMs = ProjectLimits::kMinimumResponseTimeoutMs;
  296. require(validatePlcSerialConfiguration(configuration).succeeded,
  297. "a response timeout of 100 ms must be accepted");
  298. }
  299. void testPlcPollQuantityBoundaries()
  300. {
  301. PlcRegisterRepository repository;
  302. PlcCommunicationService service(repository);
  303. std::vector<RegisterAddress> too_many_addresses;
  304. for (int index = 0; index <= RegisterAddress::kMaximumIndex; ++index)
  305. {
  306. too_many_addresses.push_back({RegisterArea::M, index});
  307. }
  308. too_many_addresses.push_back({RegisterArea::D, 0});
  309. require(!service.setPollAddresses(too_many_addresses).succeeded,
  310. "more than the configured poll address limit must be rejected");
  311. std::vector<RegisterAddress> too_many_blocks;
  312. for (std::size_t index = 0;
  313. index < ProjectLimits::kMaximumPollBlocks + 1U;
  314. ++index)
  315. {
  316. too_many_blocks.push_back({RegisterArea::M, static_cast<int>(index * 2U)});
  317. }
  318. require(!service.setPollAddresses(too_many_blocks).succeeded,
  319. "more than the configured Modbus read block limit must be rejected");
  320. std::vector<RegisterAddress> maximum_contiguous_block;
  321. for (int index = 0; index < ProjectLimits::kMaximumModbusReadCount; ++index)
  322. {
  323. maximum_contiguous_block.push_back({RegisterArea::M, index});
  324. }
  325. require(service.setPollAddresses(maximum_contiguous_block).succeeded,
  326. "a contiguous Modbus read block of 120 values must be accepted");
  327. }
  328. } // namespace
  329. int main()
  330. {
  331. try
  332. {
  333. testPlcCacheAndWriteForwarding();
  334. testRuntimeRepositorySwitchingAndDisconnect();
  335. testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect();
  336. testPlcCommunicationErrorClassification();
  337. testPlcConfigurationBoundaries();
  338. testPlcPollQuantityBoundaries();
  339. }
  340. catch (const std::exception &error)
  341. {
  342. std::cerr << "PLC runtime tests failed: " << error.what() << '\n';
  343. return 1;
  344. }
  345. std::cout << "PLC runtime tests passed\n";
  346. return 0;
  347. }