综合平台编程器项目的远程存储
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 
 

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