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

605 rivejä
23 KiB

  1. #include "domain/control_logic_model.h"
  2. #include "domain/hmi_model.h"
  3. #include "domain/project_model.h"
  4. #include "domain/register_address.h"
  5. #include "domain/register_repository.h"
  6. #include "domain/runtime_state.h"
  7. #include <cstdint>
  8. #include <exception>
  9. #include <iostream>
  10. #include <stdexcept>
  11. #include <string>
  12. namespace {
  13. void require(bool condition, const std::string &message)
  14. {
  15. if (!condition)
  16. {
  17. throw std::runtime_error(message);
  18. }
  19. }
  20. void testRegisterAddressBoundaries()
  21. {
  22. // 覆盖 M/D 地址允许范围及未知枚举值的拒绝路径
  23. require(RegisterAddress{RegisterArea::M, 0}.isValid(),
  24. "M0 must be valid");
  25. require(RegisterAddress{RegisterArea::D, 4000}.isValid(),
  26. "D4000 must be valid");
  27. require(!(RegisterAddress{RegisterArea::M, -1}.isValid()),
  28. "negative register index must be rejected");
  29. require(!(RegisterAddress{RegisterArea::D, 4001}.isValid()),
  30. "register index above 4000 must be rejected");
  31. require(!(RegisterAddress{static_cast<RegisterArea>(99), 0}.isValid()),
  32. "unknown register area must be rejected");
  33. }
  34. void testRegisterAddressParsing()
  35. {
  36. const RegisterAddressParseResult m0 = parseRegisterAddress(" m0 ");
  37. require(m0.succeeded && m0.address == RegisterAddress{RegisterArea::M, 0},
  38. "register parser must trim and normalize lowercase M addresses");
  39. const RegisterAddressParseResult d4000 = parseRegisterAddress("D4000");
  40. require(d4000.succeeded
  41. && d4000.address == RegisterAddress{RegisterArea::D, 4000},
  42. "register parser must accept the maximum D address");
  43. require(parseRegisterAddress("").error == RegisterAddressParseError::Empty,
  44. "register parser must distinguish empty input");
  45. require(parseRegisterAddress("X0").error
  46. == RegisterAddressParseError::UnsupportedArea,
  47. "register parser must reject unsupported areas");
  48. require(parseRegisterAddress("M1.0").error
  49. == RegisterAddressParseError::InvalidFormat,
  50. "register parser must reject non-decimal indices");
  51. require(parseRegisterAddress("D4001").error
  52. == RegisterAddressParseError::OutOfRange,
  53. "register parser must reject addresses above the project range");
  54. }
  55. void testRegisterRepositorySeparatesAreas()
  56. {
  57. // 验证离线仓库不会把 M 位和 D 字交叉解释
  58. VirtualRegisterRepository repository;
  59. const RegisterAddress m0{RegisterArea::M, 0};
  60. const RegisterAddress d0{RegisterArea::D, 0};
  61. require(repository.writeBit(m0, true).succeeded, "M bit write must succeed");
  62. require(repository.readBit(m0).value, "M bit read must return written value");
  63. require(repository.writeWord(d0, static_cast<std::int16_t>(-123)).succeeded,
  64. "D word write must succeed");
  65. require(repository.readWord(d0).value == -123, "D word read must return written value");
  66. require(repository.readBit(d0).error == RegisterError::AreaMismatch,
  67. "D address must not be read as a bit");
  68. require(repository.readWord(m0).error == RegisterError::AreaMismatch,
  69. "M address must not be read as a word");
  70. }
  71. Project makeValidProject()
  72. {
  73. // 构造包含 HMI 绑定和完整梯形图网络的最小合法工程作为测试基线
  74. HmiControl start_button;
  75. start_button.id = "start-button";
  76. start_button.type = HmiControlType::Button;
  77. start_button.text = "Start";
  78. start_button.binding = {RegisterArea::M, 0};
  79. HmiPage page;
  80. page.id = "main-page";
  81. page.name = "Main";
  82. page.controls.push_back(start_button);
  83. LogicNode contact;
  84. contact.id = "start-contact";
  85. contact.config = ContactNodeConfig{
  86. RegisterAddress{RegisterArea::M, 0},
  87. ContactMode::NormallyOpen};
  88. LogicNode coil;
  89. coil.id = "run-coil";
  90. coil.config = CoilNodeConfig{
  91. RegisterAddress{RegisterArea::M, 1},
  92. CoilMode::Normal};
  93. ControlLogic logic;
  94. logic.id = "start-logic";
  95. logic.name = "Start logic";
  96. LadderRung rung;
  97. rung.id = "rung-1";
  98. rung.name = "Network 1";
  99. rung.condition = ConditionExpression::fromNode(contact);
  100. rung.output = coil;
  101. logic.rungs.push_back(rung);
  102. Project project;
  103. project.metadata = {"sample-project", "Sample project", "1.0"};
  104. project.hmiPages.push_back(page);
  105. project.initialHmiPageId = page.id;
  106. project.controlLogics.push_back(logic);
  107. return project;
  108. }
  109. void testMultiPageAndLogicDomainRules()
  110. {
  111. Project project = makeValidProject();
  112. HmiPage settings;
  113. settings.id = "settings-page";
  114. settings.name = "Settings";
  115. project.hmiPages.push_back(settings);
  116. HmiControl label;
  117. label.id = "title";
  118. label.type = HmiControlType::Label;
  119. label.text = "Machine";
  120. project.hmiPages.front().controls.push_back(label);
  121. require(project.validate(), "an unbound label must be a valid static control");
  122. project.hmiPages.front().controls.back().binding =
  123. RegisterAddress{RegisterArea::M, 10};
  124. require(!project.validate(), "labels must reject register bindings");
  125. project = makeValidProject();
  126. project.hmiPages.push_back(settings);
  127. HmiControl jump;
  128. jump.id = "settings-jump";
  129. jump.type = HmiControlType::PageJump;
  130. jump.text = "Settings";
  131. jump.pageJump = HmiPageJumpConfig{settings.id};
  132. project.hmiPages.front().controls.push_back(jump);
  133. require(project.validate() && project.validateForRunning(),
  134. "a page jump must resolve its target by stable page id");
  135. project.hmiPages.front().controls.back().pageJump->targetPageId = "missing";
  136. require(!project.validate(), "a page jump must reject a missing target page");
  137. project = makeValidProject();
  138. project.initialHmiPageId = "missing";
  139. require(!project.validate(), "the initial HMI page id must resolve to a page");
  140. project = makeValidProject();
  141. HmiPage duplicate_name = settings;
  142. duplicate_name.name = project.hmiPages.front().name;
  143. project.hmiPages.push_back(duplicate_name);
  144. require(!project.validate(), "HMI page names must be unique");
  145. project = makeValidProject();
  146. ControlLogic disabled_draft;
  147. disabled_draft.id = "draft-logic";
  148. disabled_draft.name = "Draft logic";
  149. disabled_draft.enabled = false;
  150. disabled_draft.rungs.push_back(
  151. {"rung-1", "Draft network", {}, std::nullopt, std::nullopt});
  152. project.controlLogics.push_back(disabled_draft);
  153. require(project.validateForRunning(),
  154. "a disabled draft logic must not block offline running");
  155. project.controlLogics.back().name = project.controlLogics.front().name;
  156. require(!project.validate(), "control logic names must be unique");
  157. project = makeValidProject();
  158. project.hmiPages.front().controls.front().type =
  159. static_cast<HmiControlType>(99);
  160. project.hmiPages.front().controls.front().binding.reset();
  161. require(!project.validate(), "unknown HMI control types must be rejected");
  162. }
  163. void testLogicNodeConfigurationBoundaries()
  164. {
  165. // 触点只能绑定 M 区,数值比较只能绑定 D 区
  166. LogicNode contact;
  167. contact.id = "contact";
  168. contact.config = ContactNodeConfig{
  169. RegisterAddress{RegisterArea::M, 0},
  170. ContactMode::NormallyOpen};
  171. require(contact.validate(), "contact node bound to M address must be valid");
  172. contact.config = ContactNodeConfig{
  173. RegisterAddress{RegisterArea::D, 0},
  174. ContactMode::NormallyOpen};
  175. require(!contact.validate(), "contact node bound to D address must be rejected");
  176. LogicNode comparison;
  177. comparison.id = "comparison";
  178. comparison.config = CompareNodeConfig{
  179. RegisterAddress{RegisterArea::D, 0},
  180. ComparisonOperator::GreaterThan,
  181. static_cast<std::int16_t>(100)};
  182. require(comparison.validate(), "comparison node bound to D address must be valid");
  183. }
  184. void testTimerAndCommentBoundaries()
  185. {
  186. LogicNode edge;
  187. edge.id = "edge";
  188. edge.config = EdgeContactNodeConfig{
  189. RegisterAddress{RegisterArea::M, 0}, EdgeMode::Rising};
  190. require(edge.validate(), "a valid rising edge contact must pass validation");
  191. LogicNode timer_contact;
  192. timer_contact.id = "timer-contact";
  193. timer_contact.config = TimerContactNodeConfig{
  194. TimerAddress{4000}, ContactMode::NormallyOpen};
  195. require(timer_contact.validate(), "T4000 must be a valid timer contact");
  196. timer_contact.config = TimerContactNodeConfig{
  197. TimerAddress{-1}, ContactMode::NormallyOpen};
  198. require(!timer_contact.validate(), "a negative T address must be rejected");
  199. LogicNode ton;
  200. ton.id = "ton";
  201. ton.config = TonNodeConfig{TimerAddress{0}, TonNodeConfig::kMinimumPresetMs};
  202. require(ton.validate(), "the minimum TON preset must be valid");
  203. ton.config = TonNodeConfig{TimerAddress{0}, 0};
  204. require(!ton.validate(), "a zero TON preset must be rejected");
  205. ton.config = TonNodeConfig{
  206. TimerAddress{0}, TonNodeConfig::kMaximumPresetMs + 1};
  207. require(!ton.validate(), "an oversized TON preset must be rejected");
  208. RegisterComment comment{RegisterAddress{RegisterArea::M, 0}, "启动按钮"};
  209. require(comment.validate(), "a nonblank register comment must be valid");
  210. comment.text = " \t";
  211. require(!comment.validate(), "a blank register comment must be rejected");
  212. Project project = makeValidProject();
  213. project.registerComments = {
  214. {RegisterAddress{RegisterArea::M, 0}, "启动按钮"},
  215. {RegisterAddress{RegisterArea::M, 0}, "重复地址"}};
  216. require(!project.validate(), "duplicate register comments must be rejected");
  217. }
  218. LadderRung makeTimerRung(
  219. const std::string &rung_id,
  220. const std::string &condition_id,
  221. const std::string &output_id,
  222. int timer_index,
  223. int preset_ms)
  224. {
  225. LogicNode condition;
  226. condition.id = condition_id;
  227. condition.config = ContactNodeConfig{
  228. RegisterAddress{RegisterArea::M, timer_index}, ContactMode::NormallyOpen};
  229. LogicNode output;
  230. output.id = output_id;
  231. output.config = TonNodeConfig{TimerAddress{timer_index}, preset_ms};
  232. LadderRung rung;
  233. rung.id = rung_id;
  234. rung.name = rung_id;
  235. rung.condition = ConditionExpression::fromNode(condition);
  236. rung.output = output;
  237. return rung;
  238. }
  239. void testTimerReferencesForRunning()
  240. {
  241. ControlLogic valid;
  242. valid.id = "timer-valid";
  243. valid.name = "Timer valid";
  244. valid.rungs.push_back(makeTimerRung("rung-0", "input-0", "ton-0", 0, 100));
  245. require(validateTimerReferencesForRunning({valid}),
  246. "a timer contact-free TON network must pass timer reference validation");
  247. ControlLogic duplicate = valid;
  248. duplicate.id = "timer-duplicate";
  249. duplicate.name = "Timer duplicate";
  250. duplicate.rungs.front().output->id = "ton-duplicate";
  251. require(!validateTimerReferencesForRunning({valid, duplicate}),
  252. "the same T must not have two enabled TON drivers");
  253. ControlLogic missing;
  254. missing.id = "timer-missing";
  255. missing.name = "Timer missing";
  256. LogicNode contact;
  257. contact.id = "missing-contact";
  258. contact.config = TimerContactNodeConfig{
  259. TimerAddress{7}, ContactMode::NormallyOpen};
  260. LogicNode output;
  261. output.id = "missing-coil";
  262. output.config = CoilNodeConfig{
  263. RegisterAddress{RegisterArea::M, 7}, CoilMode::Normal};
  264. LadderRung missing_rung;
  265. missing_rung.id = "missing-rung";
  266. missing_rung.name = "Missing rung";
  267. missing_rung.condition = ConditionExpression::fromNode(contact);
  268. missing_rung.output = output;
  269. missing.rungs.push_back(missing_rung);
  270. require(!validateTimerReferencesForRunning({missing}),
  271. "a T contact without an enabled TON driver must be rejected");
  272. missing.enabled = false;
  273. require(validateTimerReferencesForRunning({missing}),
  274. "disabled timer drafts must not block runtime timer validation");
  275. }
  276. LadderRung makeCounterRung(
  277. const std::string &rung_id,
  278. const std::string &output_id,
  279. int counter_index)
  280. {
  281. LogicNode condition;
  282. condition.id = rung_id + "-input";
  283. condition.config = ContactNodeConfig{
  284. RegisterAddress{RegisterArea::M, counter_index},
  285. ContactMode::NormallyOpen};
  286. LogicNode output;
  287. output.id = output_id;
  288. output.config = CounterNodeConfig{
  289. CounterAddress{counter_index},
  290. CounterMode::Up,
  291. RegisterAddress{RegisterArea::D, counter_index},
  292. WordOperand{
  293. WordOperandKind::Constant,
  294. RegisterAddress{RegisterArea::D, 0},
  295. 10},
  296. RegisterAddress{RegisterArea::M, counter_index + 1}};
  297. LadderRung rung;
  298. rung.id = rung_id;
  299. rung.name = rung_id;
  300. rung.condition = ConditionExpression::fromNode(condition);
  301. rung.output = output;
  302. return rung;
  303. }
  304. void testCounterAndDataInstructionBoundaries()
  305. {
  306. require(CounterAddress{0}.isValid() && CounterAddress{4000}.isValid(),
  307. "C0 and C4000 must be valid counter resources");
  308. require(!CounterAddress{-1}.isValid() && !CounterAddress{4001}.isValid(),
  309. "counter resources outside 0 through 4000 must be rejected");
  310. LogicNode counter;
  311. counter.id = "counter";
  312. counter.config = CounterNodeConfig{
  313. CounterAddress{0},
  314. CounterMode::Up,
  315. RegisterAddress{RegisterArea::D, 10},
  316. WordOperand{
  317. WordOperandKind::Register,
  318. RegisterAddress{RegisterArea::D, 11},
  319. 0},
  320. RegisterAddress{RegisterArea::M, 12}};
  321. require(counter.validate(),
  322. "a counter with C identity and external M/D addresses must be valid");
  323. CounterNodeConfig invalid_counter = std::get<CounterNodeConfig>(counter.config);
  324. invalid_counter.currentValueAddress = RegisterAddress{RegisterArea::M, 10};
  325. counter.config = invalid_counter;
  326. require(!counter.validate(), "counter CV must reject M addresses");
  327. LogicNode move;
  328. move.id = "move";
  329. move.config = MoveNodeConfig{
  330. WordOperand{
  331. WordOperandKind::Constant,
  332. RegisterAddress{RegisterArea::D, 0},
  333. -100},
  334. RegisterAddress{RegisterArea::D, 20}};
  335. require(move.validate() && move.isOutput(),
  336. "MOVE with a constant source and D destination must be a valid output");
  337. LogicNode add;
  338. add.id = "add";
  339. add.config = ArithmeticNodeConfig{
  340. ArithmeticOperation::Add,
  341. WordOperand{
  342. WordOperandKind::Register,
  343. RegisterAddress{RegisterArea::D, 20},
  344. 0},
  345. WordOperand{
  346. WordOperandKind::Constant,
  347. RegisterAddress{RegisterArea::D, 0},
  348. 1},
  349. RegisterAddress{RegisterArea::D, 20}};
  350. require(add.validate() && add.isOutput(),
  351. "ADD must allow the same D register as source and destination");
  352. ControlLogic valid;
  353. valid.id = "counter-valid";
  354. valid.name = "Counter valid";
  355. valid.rungs.push_back(makeCounterRung("counter-rung", "ctu-0", 0));
  356. require(validateCounterReferencesForRunning({valid}),
  357. "a counter output without contacts must pass reference validation");
  358. ControlLogic duplicate = valid;
  359. duplicate.id = "counter-duplicate";
  360. duplicate.name = "Counter duplicate";
  361. duplicate.rungs.front().output->id = "ctu-duplicate";
  362. require(!validateCounterReferencesForRunning({valid, duplicate}),
  363. "the same C resource must not have multiple enabled drivers");
  364. ControlLogic missing;
  365. missing.id = "counter-missing";
  366. missing.name = "Counter missing";
  367. LogicNode missing_contact;
  368. missing_contact.id = "missing-counter-contact";
  369. missing_contact.config = CounterContactNodeConfig{
  370. CounterAddress{7}, ContactMode::NormallyOpen};
  371. LogicNode output;
  372. output.id = "missing-counter-coil";
  373. output.config = CoilNodeConfig{
  374. RegisterAddress{RegisterArea::M, 7}, CoilMode::Normal};
  375. LadderRung missing_rung;
  376. missing_rung.id = "missing-counter-rung";
  377. missing_rung.name = "Missing counter rung";
  378. missing_rung.condition = ConditionExpression::fromNode(missing_contact);
  379. missing_rung.output = output;
  380. missing.rungs.push_back(missing_rung);
  381. require(!validateCounterReferencesForRunning({missing}),
  382. "a C contact without an enabled counter driver must be rejected");
  383. }
  384. void testLadderLogicBoundaries()
  385. {
  386. LogicNode stop;
  387. stop.id = "stop";
  388. stop.config = ContactNodeConfig{
  389. RegisterAddress{RegisterArea::M, 1},
  390. ContactMode::NormallyClosed};
  391. LogicNode start;
  392. start.id = "start";
  393. start.config = ContactNodeConfig{
  394. RegisterAddress{RegisterArea::M, 0},
  395. ContactMode::NormallyOpen};
  396. LogicNode run_contact;
  397. run_contact.id = "run-contact";
  398. run_contact.config = ContactNodeConfig{
  399. RegisterAddress{RegisterArea::M, 1},
  400. ContactMode::NormallyOpen};
  401. LogicNode coil;
  402. coil.id = "run-coil";
  403. coil.config = CoilNodeConfig{
  404. RegisterAddress{RegisterArea::M, 1},
  405. CoilMode::Normal};
  406. ControlLogic logic;
  407. logic.id = "hold-logic";
  408. logic.name = "Hold logic";
  409. ConditionExpression start_parallel;
  410. start_parallel.id = "parallel-start";
  411. start_parallel.kind = ConditionExpressionKind::Parallel;
  412. start_parallel.children = {
  413. ConditionExpression::fromNode(start),
  414. ConditionExpression::fromNode(run_contact)};
  415. ConditionExpression root;
  416. root.id = "series-root";
  417. root.kind = ConditionExpressionKind::Series;
  418. root.children = {
  419. ConditionExpression::fromNode(stop),
  420. start_parallel};
  421. LadderRung rung;
  422. rung.id = "rung-1";
  423. rung.name = "Self hold";
  424. rung.condition = root;
  425. rung.output = coil;
  426. logic.rungs.push_back(rung);
  427. require(logic.validate(), "stop AND (start OR run) self-hold ladder must be valid");
  428. logic.rungs.front().condition->children.front() =
  429. ConditionExpression::fromNode(coil);
  430. require(!logic.validate(), "a ladder condition expression must reject coils");
  431. logic.rungs.front().condition = root;
  432. logic.rungs.front().output = start;
  433. require(!logic.validate(), "a ladder output must be a coil");
  434. logic.rungs.front().output.reset();
  435. require(logic.validate(), "incomplete ladder may remain in an editable draft");
  436. require(!logic.validateForRunning(),
  437. "conditions without an output must block runtime validation");
  438. LadderRung empty_rung{
  439. "rung-empty", "Empty network", {}, std::nullopt, std::nullopt};
  440. require(empty_rung.validate(), "an empty editing network must be valid");
  441. empty_rung.output = coil;
  442. require(empty_rung.validate(), "output-only network may remain in an editable draft");
  443. require(!empty_rung.validateForRunning(),
  444. "an output without conditions must block runtime validation");
  445. logic.rungs.front().output = coil;
  446. logic.rungs.front().condition = root;
  447. logic.rungs.front().condition->children.at(1).children.at(1).node->id = start.id;
  448. require(!logic.validate(), "logic node ids must be unique");
  449. ConditionExpression nested_parallel;
  450. nested_parallel.id = "parallel-nested";
  451. nested_parallel.kind = ConditionExpressionKind::Parallel;
  452. nested_parallel.children = {
  453. ConditionExpression::fromNode(start),
  454. root};
  455. require(nested_parallel.validate(),
  456. "nested series and parallel expressions must be valid");
  457. }
  458. void testModelsValidateBindingsAndIdentifiers()
  459. {
  460. // 聚合验证必须拒绝错误绑定、重复标识和越界控件
  461. Project project = makeValidProject();
  462. require(project.validate(), "valid project model must pass validation");
  463. project.hmiPages.front().controls.front().binding =
  464. RegisterAddress{RegisterArea::D, 0};
  465. require(!project.validate(), "button bound to D area must be rejected");
  466. project = makeValidProject();
  467. project.hmiPages.push_back(project.hmiPages.front());
  468. require(!project.validate(), "duplicate HMI page id must be rejected");
  469. project = makeValidProject();
  470. project.hmiPages.front().controls.front().bounds.x = -1;
  471. require(!project.validate(), "controls outside the page must be rejected");
  472. project = makeValidProject();
  473. project.hmiPages.front().controls.front().bounds.width = 801;
  474. require(!project.validate(), "controls wider than the page must be rejected");
  475. project = makeValidProject();
  476. project.hmiPages.front().controls.front().properties.emplace("", "value");
  477. require(!project.validate(), "empty HMI property names must be rejected");
  478. project = makeValidProject();
  479. project.hmiPages.front().controls.front().binding.reset();
  480. require(project.validate(), "unbound HMI control must be accepted in a draft");
  481. require(!project.validateForRunning(),
  482. "unbound HMI control must block runtime validation");
  483. project = makeValidProject();
  484. project.controlLogics.front().rungs.front().output->configured = false;
  485. require(project.validate(), "unconfigured ladder node must be accepted in a draft");
  486. require(!project.validateForRunning(),
  487. "unconfigured ladder node must block runtime validation");
  488. }
  489. void testRuntimeStateBoundaries()
  490. {
  491. // 运行模式测试覆盖离线和真机的互斥及 PLC 首读前置条件
  492. RuntimeState state;
  493. require(state.policy().allowsProjectEditing, "editing mode must allow project editing");
  494. require(state.enterOfflineRunning().succeeded, "editing may enter offline running");
  495. require(state.policy().usesVirtualRegisters, "offline mode must use virtual registers");
  496. require(state.policy().runsLogicExecutor, "offline mode must run logic executor");
  497. require(state.enterOnlineRunning(true).error
  498. == ModeTransitionError::MustReturnToEditing,
  499. "offline mode must not directly enter online mode");
  500. require(state.enterEditing().succeeded, "offline mode may return to editing");
  501. require(state.enterOnlineRunning(false).error
  502. == ModeTransitionError::InitialPlcReadRequired,
  503. "online mode must require an initial PLC read");
  504. require(state.enterOnlineRunning(true).succeeded,
  505. "editing may enter online mode after initial PLC read");
  506. require(!state.policy().runsLogicExecutor,
  507. "online mode must keep the software logic executor stopped");
  508. require(state.policy().usesPlcRegisters, "online mode must use PLC registers");
  509. }
  510. } // namespace
  511. int main()
  512. {
  513. try
  514. {
  515. // 每个测试函数独立覆盖一个领域边界,首个异常即终止测试进程
  516. testRegisterAddressBoundaries();
  517. testRegisterAddressParsing();
  518. testRegisterRepositorySeparatesAreas();
  519. testLogicNodeConfigurationBoundaries();
  520. testTimerAndCommentBoundaries();
  521. testTimerReferencesForRunning();
  522. testCounterAndDataInstructionBoundaries();
  523. testLadderLogicBoundaries();
  524. testModelsValidateBindingsAndIdentifiers();
  525. testMultiPageAndLogicDomainRules();
  526. testRuntimeStateBoundaries();
  527. }
  528. catch (const std::exception &error)
  529. {
  530. std::cerr << "domain tests failed: " << error.what() << '\n';
  531. return 1;
  532. }
  533. std::cout << "domain tests passed\n";
  534. return 0;
  535. }