#include "domain/project_storage.h" #include "infrastructure/json_project_storage.h" #include "services/project_service.h" #include #include #include #include #include #include namespace domain = integrated_platform::domain; namespace infrastructure = integrated_platform::infrastructure; namespace services = integrated_platform::services; namespace { void require(bool condition, const std::string &message) { if (!condition) { throw std::runtime_error(message); } } domain::Project makeExampleProject() { domain::HmiControl start_button; start_button.id = "start-button"; start_button.type = domain::HmiControlType::Button; start_button.bounds = {10, 20, 120, 48}; start_button.text = "Start"; start_button.binding = domain::RegisterAddress{domain::RegisterArea::M, 0}; start_button.properties.emplace("color", "green"); domain::HmiPage page; page.id = "main-page"; page.name = "Main"; page.controls.push_back(start_button); domain::LogicNode contact; contact.id = "start-contact"; contact.config = domain::ContactNodeConfig{ domain::RegisterAddress{domain::RegisterArea::M, 0}, domain::ContactMode::NormallyOpen}; domain::LogicNode compare; compare.id = "temperature-check"; compare.config = domain::CompareNodeConfig{ domain::RegisterAddress{domain::RegisterArea::D, 2}, domain::ComparisonOperator::GreaterThanOrEqual, static_cast(100)}; domain::LogicNode coil; coil.id = "run-coil"; coil.config = domain::CoilNodeConfig{ domain::RegisterAddress{domain::RegisterArea::M, 1}, domain::CoilMode::Set}; domain::ControlLogic logic; logic.id = "start-logic"; logic.name = "Start logic"; logic.enabled = false; logic.nodes = {contact, compare, coil}; logic.connections.push_back({contact.id, compare.id}); logic.connections.push_back({compare.id, coil.id}); domain::Project project; project.metadata = {"example-project", "Example project", "1.0"}; project.hmiPages.push_back(page); project.controlLogics.push_back(logic); return project; } void writeText(const QString &path, const QByteArray &content) { QFile file(path); require(file.open(QIODevice::WriteOnly), "test file must be writable"); require(file.write(content) == content.size(), "test file must be written completely"); } QByteArray readBytes(const QString &path) { QFile file(path); require(file.open(QIODevice::ReadOnly), "saved project must be readable"); return file.readAll(); } void testEmptyProjectRoundTrip() { QTemporaryDir directory; require(directory.isValid(), "temporary directory must be valid"); infrastructure::JsonProjectStorage storage; services::ProjectService service(storage); require(service.createNewProject("Empty project").succeeded, "empty project creation must succeed"); const QString path = directory.filePath("empty.json"); require(service.saveAs(path.toStdString()).succeeded, "empty project save must succeed"); require(!service.isModified(), "saved project must not be marked modified"); require(service.load(path.toStdString()).succeeded, "empty project load must succeed"); require(service.project().metadata.name == "Empty project", "empty project name must survive round trip"); require(service.project().hmiPages.empty(), "empty project must have no HMI pages"); require(service.project().controlLogics.empty(), "empty project must have no control logics"); } void testExampleProjectRoundTrip() { QTemporaryDir directory; require(directory.isValid(), "temporary directory must be valid"); infrastructure::JsonProjectStorage storage; services::ProjectService service(storage); service.editProject() = makeExampleProject(); const QString first_path = directory.filePath("example.json"); const QString second_path = directory.filePath("example-copy.json"); require(service.saveAs(first_path.toStdString()).succeeded, "example project save must succeed"); require(service.load(first_path.toStdString()).succeeded, "example project load must succeed"); const domain::Project &project = service.project(); require(project.metadata.id == "example-project", "project id must survive round trip"); require(project.hmiPages.size() == 1, "HMI page count must survive round trip"); require(project.hmiPages.front().controls.front().binding->area() == domain::RegisterArea::M, "HMI M binding must survive round trip"); require(project.hmiPages.front().controls.front().properties.at("color") == "green", "HMI properties must survive round trip"); require(project.controlLogics.size() == 1, "control logic count must survive round trip"); require(!project.controlLogics.front().enabled, "control logic enabled state must survive round trip"); require(project.controlLogics.front().nodes.size() == 3, "logic node count must survive round trip"); const auto &compare = std::get( project.controlLogics.front().nodes.at(1).config); require(compare.address.index() == 2 && compare.value == 100, "comparison configuration must survive round trip"); require(service.saveAs(second_path.toStdString()).succeeded, "save as must succeed after load"); require(readBytes(first_path) == readBytes(second_path), "save and save as must produce stable JSON"); } void testInvalidFiles() { QTemporaryDir directory; require(directory.isValid(), "temporary directory must be valid"); infrastructure::JsonProjectStorage storage; services::ProjectService service(storage); service.editProject().metadata.name = "Current project"; const QString invalid_json = directory.filePath("invalid-json.json"); const QString missing_field = directory.filePath("missing-field.json"); const QString unsupported_version = directory.filePath("unsupported-version.json"); writeText(invalid_json, "{"); auto result = service.load(invalid_json.toStdString()); require(!result.succeeded && result.storageError == domain::ProjectStorageError::InvalidJson, "invalid JSON must be rejected"); require(service.project().metadata.name == "Current project", "invalid load must keep current project"); writeText(missing_field, R"({"formatVersion":"1.0"})"); result = service.load(missing_field.toStdString()); require(!result.succeeded && result.storageError == domain::ProjectStorageError::MissingField, "missing fields must be rejected"); writeText(unsupported_version, R"({"formatVersion":"2.0"})"); result = service.load(unsupported_version.toStdString()); require(!result.succeeded && result.storageError == domain::ProjectStorageError::UnsupportedVersion, "unsupported versions must be rejected"); } void testServiceStateAndSaveErrors() { QTemporaryDir directory; require(directory.isValid(), "temporary directory must be valid"); infrastructure::JsonProjectStorage storage; services::ProjectService service(storage); require(service.save().error == services::ProjectServiceError::FilePathRequired, "save without a current path must be rejected"); require(service.createNewProject(" ").error == services::ProjectServiceError::InvalidProjectName, "blank project names must be rejected"); const QString path = directory.filePath("state.json"); service.editProject().metadata.name = "State project"; require(service.saveAs(path.toStdString()).succeeded, "state project save must succeed"); service.editProject().metadata.name = "Changed project"; require(service.isModified(), "editing the project must mark it modified"); require(service.save().succeeded, "save must use the current file path"); require(!service.isModified(), "successful save must clear modified state"); const QString failed_path = directory.filePath("missing/subdir/state.json"); require(!service.saveAs(failed_path.toStdString()).succeeded, "save to an unavailable path must fail"); require(service.currentFilePath() == path.toStdString(), "failed save as must keep the previous current path"); } } // namespace int main() { try { testEmptyProjectRoundTrip(); testExampleProjectRoundTrip(); testInvalidFiles(); testServiceStateAndSaveErrors(); } catch (const std::exception &error) { std::cerr << "project management tests failed: " << error.what() << '\n'; return 1; } std::cout << "project management tests passed\n"; return 0; }