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

238 lines
8.8 KiB

  1. #include "domain/project_storage.h"
  2. #include "infrastructure/json_project_storage.h"
  3. #include "services/project_service.h"
  4. #include <QFile>
  5. #include <QTemporaryDir>
  6. #include <exception>
  7. #include <iostream>
  8. #include <stdexcept>
  9. #include <string>
  10. namespace domain = integrated_platform::domain;
  11. namespace infrastructure = integrated_platform::infrastructure;
  12. namespace services = integrated_platform::services;
  13. namespace {
  14. void require(bool condition, const std::string &message)
  15. {
  16. if (!condition)
  17. {
  18. throw std::runtime_error(message);
  19. }
  20. }
  21. domain::Project makeExampleProject()
  22. {
  23. domain::HmiControl start_button;
  24. start_button.id = "start-button";
  25. start_button.type = domain::HmiControlType::Button;
  26. start_button.bounds = {10, 20, 120, 48};
  27. start_button.text = "Start";
  28. start_button.binding = domain::RegisterAddress{domain::RegisterArea::M, 0};
  29. start_button.properties.emplace("color", "green");
  30. domain::HmiPage page;
  31. page.id = "main-page";
  32. page.name = "Main";
  33. page.controls.push_back(start_button);
  34. domain::LogicNode contact;
  35. contact.id = "start-contact";
  36. contact.config = domain::ContactNodeConfig{
  37. domain::RegisterAddress{domain::RegisterArea::M, 0},
  38. domain::ContactMode::NormallyOpen};
  39. domain::LogicNode compare;
  40. compare.id = "temperature-check";
  41. compare.config = domain::CompareNodeConfig{
  42. domain::RegisterAddress{domain::RegisterArea::D, 2},
  43. domain::ComparisonOperator::GreaterThanOrEqual,
  44. static_cast<std::int16_t>(100)};
  45. domain::LogicNode coil;
  46. coil.id = "run-coil";
  47. coil.config = domain::CoilNodeConfig{
  48. domain::RegisterAddress{domain::RegisterArea::M, 1},
  49. domain::CoilMode::Set};
  50. domain::ControlLogic logic;
  51. logic.id = "start-logic";
  52. logic.name = "Start logic";
  53. logic.enabled = false;
  54. logic.nodes = {contact, compare, coil};
  55. logic.connections.push_back({contact.id, compare.id});
  56. logic.connections.push_back({compare.id, coil.id});
  57. domain::Project project;
  58. project.metadata = {"example-project", "Example project", "1.0"};
  59. project.hmiPages.push_back(page);
  60. project.controlLogics.push_back(logic);
  61. return project;
  62. }
  63. void writeText(const QString &path, const QByteArray &content)
  64. {
  65. QFile file(path);
  66. require(file.open(QIODevice::WriteOnly), "test file must be writable");
  67. require(file.write(content) == content.size(), "test file must be written completely");
  68. }
  69. QByteArray readBytes(const QString &path)
  70. {
  71. QFile file(path);
  72. require(file.open(QIODevice::ReadOnly), "saved project must be readable");
  73. return file.readAll();
  74. }
  75. void testEmptyProjectRoundTrip()
  76. {
  77. QTemporaryDir directory;
  78. require(directory.isValid(), "temporary directory must be valid");
  79. infrastructure::JsonProjectStorage storage;
  80. services::ProjectService service(storage);
  81. require(service.createNewProject("Empty project").succeeded,
  82. "empty project creation must succeed");
  83. const QString path = directory.filePath("empty.json");
  84. require(service.saveAs(path.toStdString()).succeeded,
  85. "empty project save must succeed");
  86. require(!service.isModified(), "saved project must not be marked modified");
  87. require(service.load(path.toStdString()).succeeded,
  88. "empty project load must succeed");
  89. require(service.project().metadata.name == "Empty project",
  90. "empty project name must survive round trip");
  91. require(service.project().hmiPages.empty(), "empty project must have no HMI pages");
  92. require(service.project().controlLogics.empty(),
  93. "empty project must have no control logics");
  94. }
  95. void testExampleProjectRoundTrip()
  96. {
  97. QTemporaryDir directory;
  98. require(directory.isValid(), "temporary directory must be valid");
  99. infrastructure::JsonProjectStorage storage;
  100. services::ProjectService service(storage);
  101. service.editProject() = makeExampleProject();
  102. const QString first_path = directory.filePath("example.json");
  103. const QString second_path = directory.filePath("example-copy.json");
  104. require(service.saveAs(first_path.toStdString()).succeeded,
  105. "example project save must succeed");
  106. require(service.load(first_path.toStdString()).succeeded,
  107. "example project load must succeed");
  108. const domain::Project &project = service.project();
  109. require(project.metadata.id == "example-project", "project id must survive round trip");
  110. require(project.hmiPages.size() == 1, "HMI page count must survive round trip");
  111. require(project.hmiPages.front().controls.front().binding->area()
  112. == domain::RegisterArea::M,
  113. "HMI M binding must survive round trip");
  114. require(project.hmiPages.front().controls.front().properties.at("color") == "green",
  115. "HMI properties must survive round trip");
  116. require(project.controlLogics.size() == 1,
  117. "control logic count must survive round trip");
  118. require(!project.controlLogics.front().enabled,
  119. "control logic enabled state must survive round trip");
  120. require(project.controlLogics.front().nodes.size() == 3,
  121. "logic node count must survive round trip");
  122. const auto &compare = std::get<domain::CompareNodeConfig>(
  123. project.controlLogics.front().nodes.at(1).config);
  124. require(compare.address.index() == 2 && compare.value == 100,
  125. "comparison configuration must survive round trip");
  126. require(service.saveAs(second_path.toStdString()).succeeded,
  127. "save as must succeed after load");
  128. require(readBytes(first_path) == readBytes(second_path),
  129. "save and save as must produce stable JSON");
  130. }
  131. void testInvalidFiles()
  132. {
  133. QTemporaryDir directory;
  134. require(directory.isValid(), "temporary directory must be valid");
  135. infrastructure::JsonProjectStorage storage;
  136. services::ProjectService service(storage);
  137. service.editProject().metadata.name = "Current project";
  138. const QString invalid_json = directory.filePath("invalid-json.json");
  139. const QString missing_field = directory.filePath("missing-field.json");
  140. const QString unsupported_version = directory.filePath("unsupported-version.json");
  141. writeText(invalid_json, "{");
  142. auto result = service.load(invalid_json.toStdString());
  143. require(!result.succeeded
  144. && result.storageError == domain::ProjectStorageError::InvalidJson,
  145. "invalid JSON must be rejected");
  146. require(service.project().metadata.name == "Current project",
  147. "invalid load must keep current project");
  148. writeText(missing_field, R"({"formatVersion":"1.0"})");
  149. result = service.load(missing_field.toStdString());
  150. require(!result.succeeded
  151. && result.storageError == domain::ProjectStorageError::MissingField,
  152. "missing fields must be rejected");
  153. writeText(unsupported_version, R"({"formatVersion":"2.0"})");
  154. result = service.load(unsupported_version.toStdString());
  155. require(!result.succeeded
  156. && result.storageError == domain::ProjectStorageError::UnsupportedVersion,
  157. "unsupported versions must be rejected");
  158. }
  159. void testServiceStateAndSaveErrors()
  160. {
  161. QTemporaryDir directory;
  162. require(directory.isValid(), "temporary directory must be valid");
  163. infrastructure::JsonProjectStorage storage;
  164. services::ProjectService service(storage);
  165. require(service.save().error == services::ProjectServiceError::FilePathRequired,
  166. "save without a current path must be rejected");
  167. require(service.createNewProject(" ").error
  168. == services::ProjectServiceError::InvalidProjectName,
  169. "blank project names must be rejected");
  170. const QString path = directory.filePath("state.json");
  171. service.editProject().metadata.name = "State project";
  172. require(service.saveAs(path.toStdString()).succeeded,
  173. "state project save must succeed");
  174. service.editProject().metadata.name = "Changed project";
  175. require(service.isModified(), "editing the project must mark it modified");
  176. require(service.save().succeeded, "save must use the current file path");
  177. require(!service.isModified(), "successful save must clear modified state");
  178. const QString failed_path = directory.filePath("missing/subdir/state.json");
  179. require(!service.saveAs(failed_path.toStdString()).succeeded,
  180. "save to an unavailable path must fail");
  181. require(service.currentFilePath() == path.toStdString(),
  182. "failed save as must keep the previous current path");
  183. }
  184. } // namespace
  185. int main()
  186. {
  187. try
  188. {
  189. testEmptyProjectRoundTrip();
  190. testExampleProjectRoundTrip();
  191. testInvalidFiles();
  192. testServiceStateAndSaveErrors();
  193. }
  194. catch (const std::exception &error)
  195. {
  196. std::cerr << "project management tests failed: " << error.what() << '\n';
  197. return 1;
  198. }
  199. std::cout << "project management tests passed\n";
  200. return 0;
  201. }