综合平台编程器项目的远程存储
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.
 
 
 
 

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