#include "domain/active_register_repository.h" #include "domain/project_storage.h" #include "infrastructure/plc_register_repository.h" #include "services/offline_simulation_service.h" #include "services/plc_communication_gateway.h" #include "services/project_service.h" #include "services/runtime_mode_service.h" #include #include #include #include namespace { class TestProjectStorage final : public ProjectStorage { public: ProjectSaveResult save(const Project &, const std::string &) override { return {true, ProjectStorageError::None, {}}; } ProjectLoadResult load(const std::string &) override { return {false, {}, ProjectStorageError::FileReadFailed, {}}; } }; class FakePlcGateway final : public PlcCommunicationGateway { public: PlcCommunicationResult connectDevice( const PlcSerialConfiguration &configuration) override { last_configuration = configuration; connection_state = PlcConnectionState::Connected; if (state_changed) { state_changed(); } return {true, {}}; } void disconnectDevice() override { connection_state = PlcConnectionState::Disconnected; initial_read = false; if (initial_read_changed) { initial_read_changed(false); } if (state_changed) { state_changed(); } } void setPollAddresses(const std::vector &addresses) override { poll_addresses = addresses; } PlcConnectionState state() const override { return connection_state; } bool initialReadCompleted() const override { return initial_read; } const std::string &lastError() const override { return last_error; } void setCallbacks( std::function state_callback, std::function initial_callback, std::function cache_callback, std::function error_callback) override { state_changed = std::move(state_callback); initial_read_changed = std::move(initial_callback); cache_updated = std::move(cache_callback); error_reported = std::move(error_callback); } void completeInitialRead() { initial_read = true; if (initial_read_changed) { initial_read_changed(true); } } PlcConnectionState connection_state = PlcConnectionState::Disconnected; bool initial_read = false; std::string last_error; PlcSerialConfiguration last_configuration; std::vector poll_addresses; std::function state_changed; std::function initial_read_changed; std::function cache_updated; std::function error_reported; }; void require(bool condition, const std::string &message) { if (!condition) { throw std::runtime_error(message); } } 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); FakePlcGateway gateway; RuntimeModeService service(project_service, simulation_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"); 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.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"); } } // namespace int main() { try { testPlcCacheAndWriteForwarding(); testRuntimeRepositorySwitchingAndDisconnect(); } 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; }