|
- #include "domain/active_register_repository.h"
- #include "domain/project_limits.h"
- #include "domain/project_storage.h"
- #include "infrastructure/plc_communication_error_classifier.h"
- #include "infrastructure/plc_communication_service.h"
- #include "support/test_support.h"
- #include "infrastructure/plc_register_repository.h"
- #include "domain/virtual_register_repository.h"
- #include "services/offline_simulation_service.h"
- #include "services/plc_communication_gateway.h"
- #include "services/plc_discovery_gateway.h"
- #include "services/project_service.h"
- #include "services/runtime_mode_service.h"
-
- #include <functional>
- #include <iostream>
- #include <set>
- #include <stdexcept>
- #include <tuple>
- #include <utility>
-
- namespace {
-
- using TestProjectStorage = TestSupport::InMemoryProjectStorage;
-
- class FakePlcGateway final : public PlcCommunicationGateway
- {
- public:
- PlcCommunicationResult connectDevice(
- const PlcSerialConfiguration &configuration) override
- {
- last_configuration = configuration;
- last_error_type = PlcCommunicationError::None;
- last_error.clear();
- initial_read = false;
- connection_state = PlcConnectionState::Connected;
- if (state_changed)
- {
- state_changed();
- }
- return {true, {}};
- }
-
- void disconnectDevice() override
- {
- ++disconnect_count;
- connection_state = PlcConnectionState::Disconnected;
- initial_read = false;
- if (initial_read_changed)
- {
- initial_read_changed(false);
- }
- if (state_changed)
- {
- state_changed();
- }
- }
-
- PlcCommunicationResult setPollAddresses(
- const std::vector<RegisterAddress> &addresses) override
- {
- poll_addresses = addresses;
- return {true, {}};
- }
-
- PlcConnectionState state() const override { return connection_state; }
- bool initialReadCompleted() const override { return initial_read; }
- PlcCommunicationError lastErrorType() const override { return last_error_type; }
- const std::string &lastError() const override { return last_error; }
-
- void setCallbacks(
- std::function<void()> state_callback,
- std::function<void(bool)> initial_callback,
- std::function<void()> cache_callback,
- std::function<void()> poll_cycle_callback,
- std::function<void(const std::string &)> error_callback) override
- {
- state_changed = std::move(state_callback);
- initial_read_changed = std::move(initial_callback);
- cache_updated = std::move(cache_callback);
- poll_cycle_completed = std::move(poll_cycle_callback);
- error_reported = std::move(error_callback);
- }
-
- void completeInitialRead()
- {
- initial_read = true;
- if (initial_read_changed)
- {
- initial_read_changed(true);
- }
- }
-
- void failCommunication(
- PlcCommunicationError error_type, const std::string &message)
- {
- initial_read = false;
- last_error_type = error_type;
- last_error = message;
- connection_state = PlcConnectionState::Faulted;
- if (initial_read_changed)
- {
- initial_read_changed(false);
- }
- if (state_changed)
- {
- state_changed();
- }
- if (error_reported)
- {
- error_reported(last_error);
- }
- }
-
- PlcConnectionState connection_state = PlcConnectionState::Disconnected;
- bool initial_read = false;
- int disconnect_count = 0;
- PlcCommunicationError last_error_type = PlcCommunicationError::None;
- std::string last_error;
- PlcSerialConfiguration last_configuration;
- std::vector<RegisterAddress> poll_addresses;
- std::function<void()> state_changed;
- std::function<void(bool)> initial_read_changed;
- std::function<void()> cache_updated;
- std::function<void()> poll_cycle_completed;
- std::function<void(const std::string &)> error_reported;
- };
-
- using TestSupport::require;
-
- void testPlcCacheAndWriteForwarding()
- {
- PlcRegisterRepository repository;
- const RegisterAddress m0{RegisterArea::M, 0};
- const RegisterAddress d0{RegisterArea::D, 0};
- require(repository.readBit(m0).error == RegisterError::Unavailable,
- "uninitialized PLC bit cache must be unavailable");
- require(repository.readWord(d0).error == RegisterError::Unavailable,
- "uninitialized PLC word cache must be unavailable");
-
- repository.updateBit(0, true);
- repository.updateWord(0, -123);
- require(repository.readBit(m0).succeeded && repository.readBit(m0).value,
- "valid PLC bit cache must be readable");
- require(repository.readWord(d0).succeeded
- && repository.readWord(d0).value == -123,
- "valid PLC word cache must preserve signed values");
-
- RegisterAddress written_address{RegisterArea::M, 1};
- bool written_bit = false;
- std::int16_t written_word = 0;
- repository.setWriteHandlers(
- [&](const RegisterAddress &address, bool value)
- {
- written_address = address;
- written_bit = value;
- return RegisterWriteResult{true, RegisterError::None};
- },
- [&](const RegisterAddress &address, std::int16_t value)
- {
- written_address = address;
- written_word = value;
- return RegisterWriteResult{true, RegisterError::None};
- });
- require(repository.writeBit(m0, false).succeeded
- && written_address == m0 && !written_bit,
- "PLC bit writes must be forwarded without changing the cache");
- require(repository.writeWord(d0, 456).succeeded
- && written_address == d0 && written_word == 456,
- "PLC word writes must be forwarded without changing the cache");
- repository.invalidate();
- require(!repository.hasAnyValidValue(),
- "disconnecting must invalidate all PLC cache validity flags");
- }
-
- void testRuntimeRepositorySwitchingAndDisconnect()
- {
- TestProjectStorage storage;
- ProjectService project_service(storage);
- HmiPage page;
- page.id = "main-page";
- page.name = "Main";
- HmiControl indicator;
- indicator.id = "run-state";
- indicator.type = HmiControlType::Indicator;
- indicator.bounds = {0, 0, 80, 40};
- indicator.text = "Run";
- indicator.binding = RegisterAddress{RegisterArea::M, 5};
- page.controls.push_back(indicator);
- project_service.editProject().hmiPages.push_back(page);
- VirtualRegisterRepository virtual_repository;
- PlcRegisterRepository plc_repository;
- ActiveRegisterRepository active_repository(virtual_repository);
- OfflineSimulationService simulation_service(virtual_repository);
- OnlineLogicMonitorService online_monitor_service(plc_repository);
- FakePlcGateway gateway;
- RuntimeModeService service(
- project_service, simulation_service, online_monitor_service);
- service.configurePlc(
- gateway, active_repository, virtual_repository, plc_repository);
-
- const PlcCommunicationResult connected = service.connectPlc(
- {"COM9", 2, 19200, 8, 2, 1, 1000, 2, 200});
- require(connected.succeeded && gateway.poll_addresses.size() == 1U,
- "PLC connection must receive the project address poll set");
- service.setMonitorAddresses({
- RegisterAddress{RegisterArea::M, 5},
- RegisterAddress{RegisterArea::D, 8}});
- require(gateway.poll_addresses.size() == 2U
- && gateway.poll_addresses.front()
- == RegisterAddress{RegisterArea::M, 5}
- && gateway.poll_addresses.back()
- == RegisterAddress{RegisterArea::D, 8},
- "monitor addresses must merge with and deduplicate project poll addresses");
- require(service.enterOnlineRunning().error
- == ModeTransitionError::InitialPlcReadRequired,
- "online mode must wait for the first valid PLC read");
-
- plc_repository.updateBit(5, true);
- gateway.completeInitialRead();
- require(service.enterOnlineRunning().succeeded,
- "online mode must start after the first valid PLC read");
- require(active_repository.readBit({RegisterArea::M, 5}).succeeded
- && active_repository.readBit({RegisterArea::M, 5}).value,
- "online mode must expose the PLC cache through the active repository");
-
- gateway.disconnectDevice();
- require(service.mode() == ApplicationMode::Editing,
- "an online disconnect must return the application to editing mode");
- require(service.onlineLogicMonitorService().state()
- == OnlineLogicMonitorState::Stopped,
- "an online disconnect must stop the local read-only trace executor");
- require(!service.initialPlcReadCompleted(),
- "an online disconnect must clear the initial read flag");
- require(active_repository.readBit({RegisterArea::M, 5}).succeeded
- && !active_repository.readBit({RegisterArea::M, 5}).value,
- "editing after disconnect must switch back to the virtual repository");
- }
-
- void testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect()
- {
- TestProjectStorage storage;
- ProjectService project_service(storage);
- VirtualRegisterRepository virtual_repository;
- PlcRegisterRepository plc_repository;
- ActiveRegisterRepository active_repository(virtual_repository);
- OfflineSimulationService simulation_service(virtual_repository);
- OnlineLogicMonitorService online_monitor_service(plc_repository);
- FakePlcGateway gateway;
- RuntimeModeService service(
- project_service, simulation_service, online_monitor_service);
- service.configurePlc(
- gateway, active_repository, virtual_repository, plc_repository);
-
- require(service.connectPlc(
- {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded,
- "PLC connection must start before testing a communication fault");
- gateway.completeInitialRead();
- require(service.enterOnlineRunning().succeeded,
- "completed initial read must allow online running before a fault");
-
- gateway.failCommunication(
- PlcCommunicationError::CommunicationTimeout,
- "PLC communication timed out while the serial port remained open");
- require(service.mode() == ApplicationMode::Editing,
- "a PLC communication fault must return online running to editing");
- require(service.onlineLogicMonitorService().state()
- == OnlineLogicMonitorState::Stopped,
- "a PLC communication fault must stop the local trace executor");
- require(!service.initialPlcReadCompleted(),
- "a PLC communication fault must revoke initial read readiness");
- require(service.enterOnlineRunning().error
- == ModeTransitionError::InitialPlcReadRequired,
- "faulted PLC state must not reuse readiness from the previous connection");
-
- const int disconnect_count = gateway.disconnect_count;
- require(service.connectPlc(
- {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded,
- "faulted PLC state must support direct reconnect");
- require(gateway.disconnect_count == disconnect_count + 1,
- "reconnecting from a fault must clean the previous serial session first");
- }
-
- void testPlcCommunicationErrorClassification()
- {
- PlcCommunicationErrorContext context;
- context.portName = QStringLiteral("COM3");
-
- PlcCommunicationFailure failure = classifyPlcCommunicationError(
- QModbusDevice::ConnectionError, context);
- require(failure.type == PlcCommunicationError::SerialPortOpenFailed
- && failure.message.contains(QStringLiteral("未能打开")),
- "connection failure before opening the port must be classified explicitly");
-
- context.serialSessionOpened = true;
- failure = classifyPlcCommunicationError(QModbusDevice::TimeoutError, context);
- require(failure.type == PlcCommunicationError::PlcNotResponding
- && failure.message.contains(QStringLiteral("PLC 未响应")),
- "initial timeout with an open serial port must report a non-responsive PLC");
-
- context.receivedValidResponse = true;
- failure = classifyPlcCommunicationError(QModbusDevice::TimeoutError, context);
- require(failure.type == PlcCommunicationError::CommunicationTimeout
- && failure.message.contains(QStringLiteral("仍处于打开状态")),
- "timeout after valid traffic must report an open local serial session");
-
- failure = classifyPlcCommunicationError(QModbusDevice::ConnectionError, context);
- require(failure.type == PlcCommunicationError::SerialConnectionLost
- && failure.message.contains(QStringLiteral("连接已中断")),
- "connection errors after opening the port must report a lost serial link");
-
- failure = classifyPlcCommunicationError(QModbusDevice::ProtocolError, context);
- require(failure.type == PlcCommunicationError::ProtocolError
- && failure.message.contains(QStringLiteral("Modbus 协议异常")),
- "protocol errors must remain distinct from timeouts and disconnections");
- }
-
- void testPlcConfigurationBoundaries()
- {
- PlcSerialConfiguration configuration;
- configuration.portName = "COM3";
- require(validatePlcSerialConfiguration(configuration).succeeded,
- "the standard COM3 9600 8E1 configuration must be accepted");
-
- configuration.serverAddress = ProjectLimits::kMaximumPlcServerAddress;
- require(validatePlcSerialConfiguration(configuration).succeeded,
- "PLC station 247 must be accepted");
- configuration.serverAddress = ProjectLimits::kMaximumPlcServerAddress + 1;
- require(!validatePlcSerialConfiguration(configuration).succeeded,
- "PLC station 248 must be rejected");
-
- configuration.serverAddress = 1;
- configuration.retries = ProjectLimits::kMaximumRetries;
- require(validatePlcSerialConfiguration(configuration).succeeded,
- "five retries must be accepted");
- configuration.retries = ProjectLimits::kMaximumRetries + 1;
- require(!validatePlcSerialConfiguration(configuration).succeeded,
- "six retries must be rejected");
-
- configuration.retries = 0;
- configuration.responseTimeoutMs = ProjectLimits::kMinimumResponseTimeoutMs - 1;
- require(!validatePlcSerialConfiguration(configuration).succeeded,
- "a response timeout below 100 ms must be rejected");
- configuration.responseTimeoutMs = ProjectLimits::kMinimumResponseTimeoutMs;
- require(validatePlcSerialConfiguration(configuration).succeeded,
- "a response timeout of 100 ms must be accepted");
- }
-
- void testPlcDiscoveryCandidatePriorityAndCoverage()
- {
- PlcSerialConfiguration preferred;
- preferred.portName = "COM3";
- preferred.serverAddress = 17;
- preferred.baudRate = 38400;
- preferred.dataBits = 7;
- preferred.parity = 3;
- preferred.stopBits = 2;
- preferred.responseTimeoutMs = 1500;
- preferred.retries = 4;
- preferred.pollIntervalMs = 350;
-
- const std::vector<PlcSerialConfiguration> candidates =
- buildPlcDiscoveryCandidates(
- preferred, {"COM1", "COM2", "COM3", "COM2", ""});
- require(candidates.size() == 180U,
- "discovery must cover 60 serial settings on every unique available port");
- require(candidates.front().portName == "COM3"
- && candidates.front().serverAddress == 17
- && candidates.front().baudRate == 38400
- && candidates.front().dataBits == 7
- && candidates.front().parity == 3
- && candidates.front().stopBits == 2,
- "discovery must try the complete current configuration first");
- require(candidates.at(1).portName == "COM1"
- && candidates.at(2).portName == "COM2",
- "the preferred port must be first without dropping other ports");
-
- std::set<std::tuple<std::string, int, int, int, int>> unique_candidates;
- bool contains_standard_8e1 = false;
- bool contains_full_supported_edge = false;
- for (const PlcSerialConfiguration &candidate : candidates)
- {
- require(candidate.serverAddress == preferred.serverAddress
- && candidate.responseTimeoutMs == preferred.responseTimeoutMs
- && candidate.retries == preferred.retries
- && candidate.pollIntervalMs == preferred.pollIntervalMs,
- "discovery must preserve the selected station and normal timing settings");
- unique_candidates.emplace(
- candidate.portName,
- candidate.baudRate,
- candidate.dataBits,
- candidate.parity,
- candidate.stopBits);
- contains_standard_8e1 = contains_standard_8e1
- || (candidate.portName == "COM2"
- && candidate.baudRate == 9600
- && candidate.dataBits == 8
- && candidate.parity == 2
- && candidate.stopBits == 1);
- contains_full_supported_edge = contains_full_supported_edge
- || (candidate.portName == "COM1"
- && candidate.baudRate == 115200
- && candidate.dataBits == 7
- && candidate.parity == 0
- && candidate.stopBits == 2);
- }
- require(unique_candidates.size() == candidates.size(),
- "discovery candidates must not contain duplicate serial settings");
- require(contains_standard_8e1 && contains_full_supported_edge,
- "discovery must cover common settings and every supported boundary");
- require(buildPlcDiscoveryCandidates(preferred, {}).empty(),
- "discovery must stop immediately when no serial port is available");
- }
-
- void testPlcPollQuantityBoundaries()
- {
- PlcRegisterRepository repository;
- PlcCommunicationService service(repository);
-
- std::vector<RegisterAddress> too_many_addresses;
- for (int index = 0; index <= RegisterAddress::kMaximumIndex; ++index)
- {
- too_many_addresses.push_back({RegisterArea::M, index});
- }
- too_many_addresses.push_back({RegisterArea::D, 0});
- require(!service.setPollAddresses(too_many_addresses).succeeded,
- "more than the configured poll address limit must be rejected");
-
- std::vector<RegisterAddress> too_many_blocks;
- for (std::size_t index = 0;
- index < ProjectLimits::kMaximumPollBlocks + 1U;
- ++index)
- {
- too_many_blocks.push_back({RegisterArea::M, static_cast<int>(index * 2U)});
- }
- require(!service.setPollAddresses(too_many_blocks).succeeded,
- "more than the configured Modbus read block limit must be rejected");
-
- std::vector<RegisterAddress> maximum_contiguous_block;
- for (int index = 0; index < ProjectLimits::kMaximumModbusReadCount; ++index)
- {
- maximum_contiguous_block.push_back({RegisterArea::M, index});
- }
- require(service.setPollAddresses(maximum_contiguous_block).succeeded,
- "a contiguous Modbus read block of 120 values must be accepted");
- }
-
- } // namespace
-
- int main()
- {
- try
- {
- testPlcCacheAndWriteForwarding();
- testRuntimeRepositorySwitchingAndDisconnect();
- testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect();
- testPlcCommunicationErrorClassification();
- testPlcConfigurationBoundaries();
- testPlcDiscoveryCandidatePriorityAndCoverage();
- testPlcPollQuantityBoundaries();
- }
- catch (const std::exception &error)
- {
- std::cerr << "PLC runtime tests failed: " << error.what() << '\n';
- return 1;
- }
- std::cout << "PLC runtime tests passed\n";
- return 0;
- }
|