综合平台编程器项目的远程存储
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 

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