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

618 regels
26 KiB

  1. #include "domain/project_limits.h"
  2. #include "infrastructure/json_project_storage.h"
  3. #include "services/project_service.h"
  4. #include "services/register_comment_service.h"
  5. #include "support/test_support.h"
  6. #include <QFile>
  7. #include <QJsonArray>
  8. #include <QJsonDocument>
  9. #include <QJsonObject>
  10. #include <QTemporaryDir>
  11. #include <exception>
  12. #include <iostream>
  13. #include <string>
  14. #include <utility>
  15. namespace {
  16. using TestSupport::require;
  17. LogicNode contact(const std::string &id, int address)
  18. {
  19. return {
  20. id,
  21. ContactNodeConfig{
  22. RegisterAddress{RegisterArea::M, address},
  23. ContactMode::NormallyOpen},
  24. true};
  25. }
  26. LogicNode edge(const std::string &id, int address)
  27. {
  28. return {
  29. id,
  30. EdgeContactNodeConfig{
  31. RegisterAddress{RegisterArea::M, address},
  32. EdgeMode::Rising},
  33. true};
  34. }
  35. LogicNode coil(const std::string &id, int address)
  36. {
  37. return {
  38. id,
  39. CoilNodeConfig{
  40. RegisterAddress{RegisterArea::M, address},
  41. CoilMode::Normal},
  42. true};
  43. }
  44. LadderRung makeRung(
  45. const std::string &id,
  46. LogicNode condition,
  47. LogicNode output)
  48. {
  49. LadderRung rung;
  50. rung.id = id;
  51. rung.name = id;
  52. rung.comment = id + " comment";
  53. for (int column = 0;
  54. column < ProjectLimits::kMaximumConditionColumns;
  55. ++column)
  56. {
  57. rung.cells.push_back({
  58. id + "-cell-" + std::to_string(column),
  59. column == 0 ? LadderCellKind::Node : LadderCellKind::Wire,
  60. column == 0 ? std::optional<LogicNode>{condition}
  61. : std::nullopt});
  62. }
  63. rung.output = std::move(output);
  64. return rung;
  65. }
  66. Project makeExampleProject()
  67. {
  68. Project project;
  69. project.metadata = {"example-project", "Example project", "4.0"};
  70. HmiPage page;
  71. page.id = "main-page";
  72. page.name = "Main";
  73. HmiControl button;
  74. button.id = "start-button";
  75. button.type = HmiControlType::Button;
  76. button.bounds = {10, 10, 100, 40};
  77. button.text = "Start";
  78. button.binding = RegisterAddress{RegisterArea::M, 0};
  79. button.buttonOperation = HmiButtonOperation::SetOn;
  80. button.buttonEnableCondition = HmiButtonWordEnableCondition{
  81. RegisterAddress{RegisterArea::D, 100},
  82. RegisterDataType::Int16,
  83. HmiButtonConditionOperator::GreaterThanOrEqual,
  84. 10.0};
  85. page.controls.push_back(button);
  86. project.hmiPages.push_back(page);
  87. project.initialHmiPageId = page.id;
  88. ControlLogic logic;
  89. logic.id = "logic-1";
  90. logic.name = "Main logic";
  91. logic.rungs.push_back(makeRung(
  92. "rung-1", contact("start-contact", 0), coil("run-coil", 10)));
  93. logic.rungs.push_back(makeRung(
  94. "rung-2", edge("edge-contact", 1), coil("edge-coil", 11)));
  95. logic.rungs.push_back(makeRung(
  96. "rung-3", contact("move-contact", 2),
  97. LogicNode{
  98. "move-output",
  99. MoveNodeConfig{
  100. WordOperand{
  101. WordOperandKind::Constant,
  102. RegisterAddress{RegisterArea::D, 0},
  103. 25},
  104. RegisterAddress{RegisterArea::D, 20}},
  105. true}));
  106. logic.rungs.push_back(makeRung(
  107. "rung-4", contact("add-contact", 3),
  108. LogicNode{
  109. "add-output",
  110. ArithmeticNodeConfig{
  111. ArithmeticOperation::Add,
  112. WordOperand{
  113. WordOperandKind::Register,
  114. RegisterAddress{RegisterArea::D, 20},
  115. 0},
  116. WordOperand{
  117. WordOperandKind::Constant,
  118. RegisterAddress{RegisterArea::D, 0},
  119. 1},
  120. RegisterAddress{RegisterArea::D, 21}},
  121. true}));
  122. logic.verticalConnections = {
  123. {"vertical-left", "rung-1", "rung-2", 0},
  124. {"vertical-right", "rung-1", "rung-2", 1}};
  125. logic.rungs[1].comment.clear();
  126. project.controlLogics.push_back(std::move(logic));
  127. project.registerComments.push_back({
  128. RegisterAddress{RegisterArea::M, 0}, "Start signal"});
  129. return project;
  130. }
  131. void writeBytes(const QString &path, const QByteArray &bytes)
  132. {
  133. QFile file(path);
  134. require(file.open(QIODevice::WriteOnly), "test file must be writable");
  135. require(file.write(bytes) == bytes.size(),
  136. "test file must be written completely");
  137. }
  138. QByteArray readBytes(const QString &path)
  139. {
  140. QFile file(path);
  141. require(file.open(QIODevice::ReadOnly), "test file must be readable");
  142. return file.readAll();
  143. }
  144. QJsonObject savedFixture(
  145. JsonProjectStorage &storage,
  146. const Project &project,
  147. const QString &path)
  148. {
  149. require(storage.save(project, path.toStdString()).succeeded,
  150. "fixture project must save");
  151. return QJsonDocument::fromJson(readBytes(path)).object();
  152. }
  153. void testEmptyProjectRoundTrip()
  154. {
  155. QTemporaryDir directory;
  156. require(directory.isValid(), "temporary directory must be valid");
  157. JsonProjectStorage storage(defaultProjectLimitSettings());
  158. ProjectService service(storage, defaultProjectLimitSettings());
  159. require(service.createNewProject("Empty project").succeeded,
  160. "empty project creation must succeed");
  161. require(service.project().metadata.formatVersion == "4.0",
  162. "new projects must use the strict grid format");
  163. const QString path = directory.filePath("empty.json");
  164. require(service.saveAs(path.toStdString()).succeeded,
  165. "empty project save must succeed");
  166. require(service.load(path.toStdString()).succeeded,
  167. "empty project load must succeed");
  168. require(service.project().controlLogics.empty()
  169. && service.project().hmiPages.empty(),
  170. "round trip must not invent project content");
  171. }
  172. void testGridProjectRoundTrip()
  173. {
  174. QTemporaryDir directory;
  175. require(directory.isValid(), "temporary directory must be valid");
  176. JsonProjectStorage storage(defaultProjectLimitSettings());
  177. const QString path = directory.filePath("grid.json");
  178. const Project original = makeExampleProject();
  179. require(original.validate(defaultProjectLimitSettings()), "the grid fixture must be structurally valid");
  180. require(storage.save(original, path.toStdString()).succeeded,
  181. "the grid project must save");
  182. const QByteArray json = readBytes(path);
  183. require(json.contains("\"formatVersion\": \"4.0\"")
  184. && json.contains("\"cells\"")
  185. && json.contains("\"verticalConnections\"")
  186. && json.contains("\"kind\": \"node\"")
  187. && json.contains("\"kind\": \"wire\""),
  188. "4.0 JSON must persist cells and independent vertical edges");
  189. require(!json.contains("\"condition\"")
  190. && !json.contains("\"children\""),
  191. "4.0 JSON must not serialize the removed expression tree");
  192. const ProjectLoadResult loaded = storage.load(path.toStdString());
  193. require(loaded.succeeded, "the grid project must load");
  194. const ControlLogic &logic = loaded.project.controlLogics.front();
  195. require(logic.rungs.size() == 4U
  196. && logic.rungs.front().cells.size() == 10U,
  197. "all rows and ten fixed cells must survive round trip");
  198. require(logic.verticalConnections.size() == 2U
  199. && logic.verticalConnections.front().columnBoundary == 0
  200. && logic.verticalConnections.back().columnBoundary == 1,
  201. "independent vertical edge positions must survive round trip");
  202. require(std::holds_alternative<EdgeContactNodeConfig>(
  203. logic.rungs[1].cells[0].node->config)
  204. && std::holds_alternative<MoveNodeConfig>(
  205. logic.rungs[2].output->config)
  206. && std::holds_alternative<ArithmeticNodeConfig>(
  207. logic.rungs[3].output->config),
  208. "typed ladder instructions must survive grid round trip");
  209. }
  210. void testMOffAlarmRoundTrip()
  211. {
  212. QTemporaryDir directory;
  213. require(directory.isValid(), "temporary directory must be valid");
  214. JsonProjectStorage storage(defaultProjectLimitSettings());
  215. const QString path = directory.filePath("m-off-alarm.json");
  216. Project project = makeExampleProject();
  217. project.alarmDefinitions.push_back({
  218. "alarm-m-off",
  219. RegisterAddress{RegisterArea::M, 5},
  220. AlarmCondition::MOff,
  221. 0,
  222. "Safety circuit open"});
  223. require(storage.save(project, path.toStdString()).succeeded,
  224. "an M OFF alarm project must save");
  225. require(readBytes(path).contains("\"condition\": \"mOff\""),
  226. "M OFF alarms must serialize with the explicit mOff condition");
  227. const ProjectLoadResult loaded = storage.load(path.toStdString());
  228. require(loaded.succeeded
  229. && loaded.project.alarmDefinitions.size() == 1U
  230. && loaded.project.alarmDefinitions.front().condition
  231. == AlarmCondition::MOff
  232. && loaded.project.alarmDefinitions.front().address
  233. == RegisterAddress{RegisterArea::M, 5},
  234. "the M OFF condition and address must survive JSON round trip");
  235. }
  236. void testMultiWordHmiDataTypeRoundTrip()
  237. {
  238. QTemporaryDir directory;
  239. require(directory.isValid(), "temporary directory must be valid");
  240. JsonProjectStorage storage(defaultProjectLimitSettings());
  241. const QString path = directory.filePath("multi-word-hmi.json");
  242. Project project = makeExampleProject();
  243. HmiControl int32_display;
  244. int32_display.id = "int32-display";
  245. int32_display.type = HmiControlType::NumericDisplay;
  246. int32_display.bounds = {20, 80, 120, 40};
  247. int32_display.text = "Int32";
  248. int32_display.binding = RegisterAddress{RegisterArea::D, 100};
  249. int32_display.dataType = RegisterDataType::Int32;
  250. project.hmiPages.front().controls.push_back(int32_display);
  251. HmiControl double_input = int32_display;
  252. double_input.id = "double-input";
  253. double_input.type = HmiControlType::NumericInput;
  254. double_input.bounds = {160, 80, 120, 40};
  255. double_input.text = "Double";
  256. double_input.binding = RegisterAddress{RegisterArea::D, 200};
  257. double_input.dataType = RegisterDataType::Float64;
  258. project.hmiPages.front().controls.push_back(double_input);
  259. require(storage.save(project, path.toStdString()).succeeded,
  260. "Int32 and Double HMI controls must save in a 4.0 project");
  261. const QByteArray json = readBytes(path);
  262. require(json.contains("\"dataType\": \"int32\"")
  263. && json.contains("\"dataType\": \"float64\"")
  264. && json.contains("\"formatVersion\": \"4.0\""),
  265. "multi-word HMI types must use stable JSON names in 4.0");
  266. const ProjectLoadResult loaded = storage.load(path.toStdString());
  267. require(loaded.succeeded
  268. && loaded.project.hmiPages.front().controls[1].dataType
  269. == RegisterDataType::Int32
  270. && loaded.project.hmiPages.front().controls[1].binding
  271. == RegisterAddress{RegisterArea::D, 100}
  272. && loaded.project.hmiPages.front().controls[2].dataType
  273. == RegisterDataType::Float64
  274. && loaded.project.hmiPages.front().controls[2].binding
  275. == RegisterAddress{RegisterArea::D, 200},
  276. "Int32 and Double HMI types and bindings must survive JSON round trip");
  277. QJsonObject root = QJsonDocument::fromJson(json).object();
  278. QJsonArray pages = root.value(QStringLiteral("hmiPages")).toArray();
  279. QJsonObject page = pages.at(0).toObject();
  280. QJsonArray controls = page.value(QStringLiteral("controls")).toArray();
  281. QJsonObject invalid_control = controls.at(1).toObject();
  282. invalid_control.insert(QStringLiteral("dataType"), QStringLiteral("int64"));
  283. controls.replace(1, invalid_control);
  284. page.insert(QStringLiteral("controls"), controls);
  285. pages.replace(0, page);
  286. root.insert(QStringLiteral("hmiPages"), pages);
  287. const QString invalid_path = directory.filePath("invalid-data-type.json");
  288. writeBytes(invalid_path, QJsonDocument(root).toJson());
  289. require(storage.load(invalid_path.toStdString()).error
  290. == ProjectStorageError::InvalidField,
  291. "unknown HMI data types must be rejected by strict JSON loading");
  292. }
  293. void testButtonEnableConditionRoundTrip()
  294. {
  295. QTemporaryDir directory;
  296. require(directory.isValid(), "temporary directory must be valid");
  297. JsonProjectStorage storage(defaultProjectLimitSettings());
  298. const QString path = directory.filePath("button-condition.json");
  299. Project project = makeExampleProject();
  300. HmiControl &button = project.hmiPages.front().controls.front();
  301. button.buttonEnableCondition = HmiButtonWordEnableCondition{
  302. RegisterAddress{RegisterArea::D, 120},
  303. RegisterDataType::Float32,
  304. HmiButtonConditionOperator::LessThanOrEqual,
  305. 12.5};
  306. require(storage.save(project, path.toStdString()).succeeded,
  307. "button enable condition project must save");
  308. const QByteArray json = readBytes(path);
  309. require(json.contains("\"buttonEnableCondition\"")
  310. && json.contains("\"type\": \"dValue\"")
  311. && json.contains("\"operation\": \"lessThanOrEqual\"")
  312. && json.contains("\"dataType\": \"float32\""),
  313. "button enable conditions must use explicit 4.0 JSON fields");
  314. const ProjectLoadResult loaded = storage.load(path.toStdString());
  315. require(loaded.succeeded,
  316. "button enable condition project must load after a JSON round trip");
  317. const HmiControl &loaded_button = loaded.project.hmiPages.front().controls.front();
  318. require(loaded_button.buttonEnableCondition.has_value()
  319. && std::get<HmiButtonWordEnableCondition>(
  320. *loaded_button.buttonEnableCondition).address
  321. == RegisterAddress{RegisterArea::D, 120}
  322. && std::get<HmiButtonWordEnableCondition>(
  323. *loaded_button.buttonEnableCondition).value == 12.5,
  324. "button enable condition type, address and value must survive round trip");
  325. }
  326. void testStatusTextRoundTrip()
  327. {
  328. QTemporaryDir directory;
  329. require(directory.isValid(), "temporary directory must be valid");
  330. JsonProjectStorage storage(defaultProjectLimitSettings());
  331. const QString path = directory.filePath("status-text.json");
  332. Project project = makeExampleProject();
  333. HmiControl bit_status;
  334. bit_status.id = "bit-status";
  335. bit_status.type = HmiControlType::StatusText;
  336. bit_status.bounds = {20, 80, 140, 40};
  337. bit_status.text = "Status";
  338. bit_status.binding = RegisterAddress{RegisterArea::M, 10};
  339. bit_status.statusText = HmiStatusBitTextConfig{"Stopped", "Running"};
  340. project.hmiPages.front().controls.push_back(bit_status);
  341. HmiControl word_status = bit_status;
  342. word_status.id = "word-status";
  343. word_status.bounds.x = 180;
  344. word_status.binding = RegisterAddress{RegisterArea::D, 100};
  345. word_status.dataType = RegisterDataType::Float64;
  346. word_status.statusText = HmiStatusWordTextConfig{{
  347. {std::nullopt, 30.0, "Low"},
  348. {30.0, 80.0, "Normal"},
  349. {80.0, std::nullopt, "High"}}};
  350. project.hmiPages.front().controls.push_back(word_status);
  351. require(storage.save(project, path.toStdString()).succeeded,
  352. "M and D status text controls must save in 4.0");
  353. const QByteArray json = readBytes(path);
  354. require(json.contains("\"type\": \"statusText\"")
  355. && json.contains("\"source\": \"m\"")
  356. && json.contains("\"source\": \"d\"")
  357. && json.contains("\"lower\": null")
  358. && json.contains("\"upper\": null")
  359. && json.contains("\"dataType\": \"float64\""),
  360. "4.0 JSON must store explicit status text modes and range bounds");
  361. const ProjectLoadResult loaded = storage.load(path.toStdString());
  362. require(loaded.succeeded
  363. && loaded.project.hmiPages.front().controls.size() == 3U,
  364. "status text controls must load after a JSON round trip");
  365. const HmiControl &loaded_bit = loaded.project.hmiPages.front().controls[1];
  366. const HmiControl &loaded_word = loaded.project.hmiPages.front().controls[2];
  367. require(std::get<HmiStatusBitTextConfig>(*loaded_bit.statusText).onText
  368. == "Running"
  369. && std::get<HmiStatusWordTextConfig>(*loaded_word.statusText)
  370. .ranges[1].lowerBound == 30.0
  371. && loaded_word.dataType == RegisterDataType::Float64,
  372. "status text labels, ranges and D type must survive round trip");
  373. }
  374. void testStrictVersionAndRequiredFields()
  375. {
  376. QTemporaryDir directory;
  377. require(directory.isValid(), "temporary directory must be valid");
  378. JsonProjectStorage storage(defaultProjectLimitSettings());
  379. const QString fixture_path = directory.filePath("fixture.json");
  380. QJsonObject root = savedFixture(
  381. storage, makeExampleProject(), fixture_path);
  382. root.insert(QStringLiteral("formatVersion"), QStringLiteral("2.0"));
  383. const QString old_path = directory.filePath("old.json");
  384. writeBytes(old_path, QJsonDocument(root).toJson());
  385. require(storage.load(old_path.toStdString()).error
  386. == ProjectStorageError::UnsupportedVersion,
  387. "old 2.0 files must be rejected without migration code");
  388. root.insert(QStringLiteral("formatVersion"), QStringLiteral("3.0"));
  389. const QString old_grid_path = directory.filePath("old-grid.json");
  390. writeBytes(old_grid_path, QJsonDocument(root).toJson());
  391. require(storage.load(old_grid_path.toStdString()).error
  392. == ProjectStorageError::UnsupportedVersion,
  393. "old 3.0 files must be rejected without migration code");
  394. root = QJsonDocument::fromJson(readBytes(fixture_path)).object();
  395. QJsonArray logics = root.value(QStringLiteral("controlLogics")).toArray();
  396. QJsonObject logic = logics.at(0).toObject();
  397. logic.remove(QStringLiteral("verticalConnections"));
  398. logics.replace(0, logic);
  399. root.insert(QStringLiteral("controlLogics"), logics);
  400. const QString missing_path = directory.filePath("missing-connections.json");
  401. writeBytes(missing_path, QJsonDocument(root).toJson());
  402. require(storage.load(missing_path.toStdString()).error
  403. == ProjectStorageError::MissingField,
  404. "4.0 must require the vertical connection array explicitly");
  405. }
  406. void testInvalidGridAndConnectionsAreRejected()
  407. {
  408. QTemporaryDir directory;
  409. require(directory.isValid(), "temporary directory must be valid");
  410. JsonProjectStorage storage(defaultProjectLimitSettings());
  411. const QString fixture_path = directory.filePath("fixture.json");
  412. const QJsonObject original = savedFixture(
  413. storage, makeExampleProject(), fixture_path);
  414. const auto firstLogic = [](QJsonObject *root) -> QJsonObject
  415. {
  416. return root->value(QStringLiteral("controlLogics"))
  417. .toArray().at(0).toObject();
  418. };
  419. const auto replaceFirstLogic = [](
  420. QJsonObject *root, const QJsonObject &logic)
  421. {
  422. QJsonArray logics = root->value(
  423. QStringLiteral("controlLogics")).toArray();
  424. logics.replace(0, logic);
  425. root->insert(QStringLiteral("controlLogics"), logics);
  426. };
  427. QJsonObject short_grid = original;
  428. QJsonObject logic = firstLogic(&short_grid);
  429. QJsonArray rungs = logic.value(QStringLiteral("rungs")).toArray();
  430. QJsonObject rung = rungs.at(0).toObject();
  431. QJsonArray cells = rung.value(QStringLiteral("cells")).toArray();
  432. cells.removeLast();
  433. rung.insert(QStringLiteral("cells"), cells);
  434. rungs.replace(0, rung);
  435. logic.insert(QStringLiteral("rungs"), rungs);
  436. replaceFirstLogic(&short_grid, logic);
  437. const QString short_path = directory.filePath("short-grid.json");
  438. writeBytes(short_path, QJsonDocument(short_grid).toJson());
  439. require(storage.load(short_path.toStdString()).error
  440. == ProjectStorageError::InvalidField,
  441. "a row with fewer than ten cells must be rejected while parsing");
  442. QJsonObject duplicate_edge = original;
  443. logic = firstLogic(&duplicate_edge);
  444. QJsonArray edges = logic.value(
  445. QStringLiteral("verticalConnections")).toArray();
  446. QJsonObject duplicate = edges.at(0).toObject();
  447. duplicate.insert(QStringLiteral("id"), QStringLiteral("vertical-copy"));
  448. edges.append(duplicate);
  449. logic.insert(QStringLiteral("verticalConnections"), edges);
  450. replaceFirstLogic(&duplicate_edge, logic);
  451. const QString duplicate_path = directory.filePath("duplicate-edge.json");
  452. writeBytes(duplicate_path, QJsonDocument(duplicate_edge).toJson());
  453. require(storage.load(duplicate_path.toStdString()).error
  454. == ProjectStorageError::InvalidProject,
  455. "duplicate edges at one row boundary must be rejected");
  456. QJsonObject hidden_comment = original;
  457. logic = firstLogic(&hidden_comment);
  458. rungs = logic.value(QStringLiteral("rungs")).toArray();
  459. rung = rungs.at(1).toObject();
  460. rung.insert(QStringLiteral("comment"), QStringLiteral("hidden branch comment"));
  461. rungs.replace(1, rung);
  462. logic.insert(QStringLiteral("rungs"), rungs);
  463. replaceFirstLogic(&hidden_comment, logic);
  464. const QString hidden_comment_path = directory.filePath(
  465. "hidden-branch-comment.json");
  466. writeBytes(hidden_comment_path, QJsonDocument(hidden_comment).toJson());
  467. require(storage.load(hidden_comment_path.toStdString()).error
  468. == ProjectStorageError::InvalidProject,
  469. "a 4.0 project must reject comments stored on connected branch rows");
  470. QJsonObject non_adjacent = original;
  471. logic = firstLogic(&non_adjacent);
  472. edges = logic.value(QStringLiteral("verticalConnections")).toArray();
  473. QJsonObject invalid_edge = edges.at(0).toObject();
  474. invalid_edge.insert(QStringLiteral("lowerRungId"),
  475. QStringLiteral("rung-3"));
  476. edges.replace(0, invalid_edge);
  477. logic.insert(QStringLiteral("verticalConnections"), edges);
  478. replaceFirstLogic(&non_adjacent, logic);
  479. const QString non_adjacent_path = directory.filePath("non-adjacent.json");
  480. writeBytes(non_adjacent_path, QJsonDocument(non_adjacent).toJson());
  481. require(storage.load(non_adjacent_path.toStdString()).error
  482. == ProjectStorageError::InvalidProject,
  483. "vertical edges between non-adjacent rows must be rejected");
  484. }
  485. void testProjectServiceStateAndConfiguredLimits()
  486. {
  487. QTemporaryDir directory;
  488. require(directory.isValid(), "temporary directory must be valid");
  489. const QString path = directory.filePath("project.json");
  490. JsonProjectStorage storage(defaultProjectLimitSettings());
  491. require(storage.save(makeExampleProject(), path.toStdString()).succeeded,
  492. "service fixture must save");
  493. ProjectLimitSettings limits = defaultProjectLimitSettings();
  494. limits.maximumRungsPerLogic = 2U;
  495. JsonProjectStorage limited_storage(limits);
  496. ProjectService limited_service(limited_storage, limits);
  497. const std::string original_id = limited_service.project().metadata.id;
  498. const ProjectOperationResult load = limited_service.load(
  499. path.toStdString());
  500. require(!load.succeeded
  501. && load.storageError == ProjectStorageError::InvalidField,
  502. "configured row limits must be enforced during JSON parsing");
  503. require(limited_service.project().metadata.id == original_id
  504. && !limited_service.hasCurrentFile(),
  505. "a failed load must leave the current project untouched");
  506. ProjectService service(storage, defaultProjectLimitSettings());
  507. require(service.save().error == ProjectServiceError::FilePathRequired,
  508. "save without a current path must fail explicitly");
  509. require(service.createNewProject("State project").succeeded,
  510. "a valid project name must create a project");
  511. require(service.saveAs(path.toStdString()).succeeded,
  512. "save-as must establish the current path");
  513. service.editProject().metadata.name = "Changed";
  514. require(service.isModified() && service.save().succeeded
  515. && !service.isModified(),
  516. "successful save must clear the modified state");
  517. }
  518. void testRegisterCommentService()
  519. {
  520. JsonProjectStorage storage(defaultProjectLimitSettings());
  521. ProjectService projects(storage, defaultProjectLimitSettings());
  522. RegisterCommentService comments(projects);
  523. require(comments.setComment(
  524. RegisterAddress{RegisterArea::D, 3}, " Target value ")
  525. .succeeded,
  526. "the comment service must trim and create comments");
  527. require(comments.setComment(
  528. RegisterAddress{RegisterArea::M, 2}, "Start signal")
  529. .succeeded,
  530. "the comment service must support M comments");
  531. require(comments.comments().size() == 2U
  532. && comments.comments().front().address.area()
  533. == RegisterArea::M
  534. && comments.comments().back().text == "Target value",
  535. "comments must remain sorted and trimmed");
  536. require(!comments.setComment(
  537. RegisterAddress{RegisterArea::M, 4}, " \t").succeeded,
  538. "blank comments must be rejected");
  539. require(comments.removeComment(
  540. RegisterAddress{RegisterArea::M, 2}).succeeded,
  541. "comments must be removable");
  542. }
  543. } // namespace
  544. int main()
  545. {
  546. try
  547. {
  548. testEmptyProjectRoundTrip();
  549. testGridProjectRoundTrip();
  550. testMOffAlarmRoundTrip();
  551. testMultiWordHmiDataTypeRoundTrip();
  552. testStatusTextRoundTrip();
  553. testButtonEnableConditionRoundTrip();
  554. testStrictVersionAndRequiredFields();
  555. testInvalidGridAndConnectionsAreRejected();
  556. testProjectServiceStateAndConfiguredLimits();
  557. testRegisterCommentService();
  558. }
  559. catch (const std::exception &error)
  560. {
  561. std::cerr << "project management tests failed: "
  562. << error.what() << '\n';
  563. return 1;
  564. }
  565. std::cout << "project management tests passed\n";
  566. return 0;
  567. }