Newer
Older
dub_jkp / source / configy / Test.d
@Geod24 Geod24 on 28 Jul 2022 16 KB Adjust configy for DUB usage
  1. /*******************************************************************************
  2. Contains all the tests for this library.
  3.  
  4. Copyright:
  5. Copyright (c) 2019-2022 BOSAGORA Foundation
  6. All rights reserved.
  7.  
  8. License:
  9. MIT License. See LICENSE for details.
  10.  
  11. *******************************************************************************/
  12.  
  13. module configy.Test;
  14.  
  15. import configy.Attributes;
  16. import configy.Exceptions;
  17. import configy.Read;
  18. import configy.Utils;
  19.  
  20. import dyaml.node;
  21.  
  22. import std.format;
  23.  
  24. import core.time;
  25.  
  26. /// Basic usage tests
  27. unittest
  28. {
  29. static struct Address
  30. {
  31. string address;
  32. string city;
  33. bool accessible;
  34. }
  35.  
  36. static struct Nested
  37. {
  38. Address address;
  39. }
  40.  
  41. static struct Config
  42. {
  43. bool enabled = true;
  44.  
  45. string name = "Jessie";
  46. int age = 42;
  47. double ratio = 24.42;
  48.  
  49. Address address = { address: "Yeoksam-dong", city: "Seoul", accessible: true };
  50.  
  51. Nested nested = { address: { address: "Gangnam-gu", city: "Also Seoul", accessible: false } };
  52. }
  53.  
  54. auto c1 = parseConfigString!Config("enabled: false", "/dev/null");
  55. assert(!c1.enabled);
  56. assert(c1.name == "Jessie");
  57. assert(c1.age == 42);
  58. assert(c1.ratio == 24.42);
  59.  
  60. assert(c1.address.address == "Yeoksam-dong");
  61. assert(c1.address.city == "Seoul");
  62. assert(c1.address.accessible);
  63.  
  64. assert(c1.nested.address.address == "Gangnam-gu");
  65. assert(c1.nested.address.city == "Also Seoul");
  66. assert(!c1.nested.address.accessible);
  67. }
  68.  
  69. // Tests for SetInfo
  70. unittest
  71. {
  72. static struct Address
  73. {
  74. string address;
  75. string city;
  76. bool accessible;
  77. }
  78.  
  79. static struct Config
  80. {
  81. SetInfo!int value;
  82. SetInfo!int answer = 42;
  83. SetInfo!string name = SetInfo!string("Lorene", false);
  84.  
  85. SetInfo!Address address;
  86. }
  87.  
  88. auto c1 = parseConfigString!Config("value: 24", "/dev/null");
  89. assert(c1.value == 24);
  90. assert(c1.value.set);
  91.  
  92. assert(c1.answer.set);
  93. assert(c1.answer == 42);
  94.  
  95. assert(!c1.name.set);
  96. assert(c1.name == "Lorene");
  97.  
  98. assert(!c1.address.set);
  99.  
  100. auto c2 = parseConfigString!Config(`
  101. name: Lorene
  102. address:
  103. address: Somewhere
  104. city: Over the rainbow
  105. `, "/dev/null");
  106.  
  107. assert(!c2.value.set);
  108. assert(c2.name == "Lorene");
  109. assert(c2.name.set);
  110. assert(c2.address.set);
  111. assert(c2.address.address == "Somewhere");
  112. assert(c2.address.city == "Over the rainbow");
  113. }
  114.  
  115. unittest
  116. {
  117. static struct Nested { core.time.Duration timeout; }
  118. static struct Config { Nested node; }
  119. try
  120. {
  121. auto result = parseConfigString!Config("node:\n timeout:", "/dev/null");
  122. assert(0);
  123. }
  124. catch (Exception exc)
  125. {
  126. assert(exc.toString() == "/dev/null(1:10): node.timeout: Field is of type scalar, " ~
  127. "but expected a mapping with at least one of: weeks, days, hours, minutes, " ~
  128. "seconds, msecs, usecs, hnsecs, nsecs");
  129. }
  130. }
  131.  
  132. unittest
  133. {
  134. static struct Config { string required; }
  135. try
  136. auto result = parseConfigString!Config("value: 24", "/dev/null");
  137. catch (ConfigException e)
  138. {
  139. assert(format("%s", e) ==
  140. "/dev/null(0:0): value: Key is not a valid member of this section. There are 1 valid keys: required");
  141. assert(format("%S", e) ==
  142. format("%s/dev/null%s(%s0%s:%s0%s): %svalue%s: Key is not a valid member of this section. " ~
  143. "There are %s1%s valid keys: %srequired%s", Yellow, Reset, Cyan, Reset, Cyan, Reset,
  144. Yellow, Reset, Yellow, Reset, Green, Reset));
  145. }
  146. }
  147.  
  148. // Test for various type errors
  149. unittest
  150. {
  151. static struct Mapping
  152. {
  153. string value;
  154. }
  155.  
  156. static struct Config
  157. {
  158. @Optional Mapping map;
  159. @Optional Mapping[] array;
  160. int scalar;
  161. }
  162.  
  163. try
  164. {
  165. auto result = parseConfigString!Config("map: Hello World", "/dev/null");
  166. assert(0);
  167. }
  168. catch (ConfigException exc)
  169. {
  170. assert(exc.toString() == "/dev/null(0:5): map: Expected to be of type mapping (object), but is a scalar");
  171. }
  172.  
  173. try
  174. {
  175. auto result = parseConfigString!Config("map:\n - Hello\n - World", "/dev/null");
  176. assert(0);
  177. }
  178. catch (ConfigException exc)
  179. {
  180. assert(exc.toString() == "/dev/null(1:2): map: Expected to be of type mapping (object), but is a sequence");
  181. }
  182.  
  183. try
  184. {
  185. auto result = parseConfigString!Config("scalar:\n - Hello\n - World", "/dev/null");
  186. assert(0);
  187. }
  188. catch (ConfigException exc)
  189. {
  190. assert(exc.toString() == "/dev/null(1:2): scalar: Expected to be of type scalar (value), but is a sequence");
  191. }
  192.  
  193. try
  194. {
  195. auto result = parseConfigString!Config("scalar:\n hello:\n World", "/dev/null");
  196. assert(0);
  197. }
  198. catch (ConfigException exc)
  199. {
  200. assert(exc.toString() == "/dev/null(1:2): scalar: Expected to be of type scalar (value), but is a mapping");
  201. }
  202. }
  203.  
  204. // Test for strict mode
  205. unittest
  206. {
  207. static struct Config
  208. {
  209. string value;
  210. }
  211.  
  212. try
  213. {
  214. auto result = parseConfigString!Config("valeu: This is a typo", "/dev/null");
  215. assert(0);
  216. }
  217. catch (ConfigException exc)
  218. {
  219. assert(exc.toString() == "/dev/null(0:0): valeu: Key is not a valid member of this section. There are 1 valid keys: value");
  220. }
  221. }
  222.  
  223. // Test for required key
  224. unittest
  225. {
  226. static struct Nested
  227. {
  228. string required;
  229. string optional = "Default";
  230. }
  231.  
  232. static struct Config
  233. {
  234. Nested inner;
  235. }
  236.  
  237. try
  238. {
  239. auto result = parseConfigString!Config("inner:\n optional: Not the default value", "/dev/null");
  240. assert(0);
  241. }
  242. catch (ConfigException exc)
  243. {
  244. assert(exc.toString() == "/dev/null(1:2): inner.required: Required key was not found in configuration or command line arguments");
  245. }
  246. }
  247.  
  248. // Testing 'validate()' on nested structures
  249. unittest
  250. {
  251. __gshared int validateCalls0 = 0;
  252. __gshared int validateCalls1 = 1;
  253. __gshared int validateCalls2 = 2;
  254.  
  255. static struct SecondLayer
  256. {
  257. string value = "default";
  258.  
  259. public void validate () const
  260. {
  261. validateCalls2++;
  262. }
  263. }
  264.  
  265. static struct FirstLayer
  266. {
  267. bool enabled = true;
  268. SecondLayer ltwo;
  269.  
  270. public void validate () const
  271. {
  272. validateCalls1++;
  273. }
  274. }
  275.  
  276. static struct Config
  277. {
  278. FirstLayer lone;
  279.  
  280. public void validate () const
  281. {
  282. validateCalls0++;
  283. }
  284. }
  285.  
  286. auto r1 = parseConfigString!Config("lone:\n ltwo:\n value: Something\n", "/dev/null");
  287.  
  288. assert(r1.lone.ltwo.value == "Something");
  289. // `validateCalls` are given different value to avoid false-positive
  290. // if they are set to 0 / mixed up
  291. assert(validateCalls0 == 1);
  292. assert(validateCalls1 == 2);
  293. assert(validateCalls2 == 3);
  294.  
  295. auto r2 = parseConfigString!Config("lone:\n enabled: false\n", "/dev/null");
  296. assert(validateCalls0 == 2); // + 1
  297. assert(validateCalls1 == 2); // Other are disabled
  298. assert(validateCalls2 == 3);
  299. }
  300.  
  301. // Test the throwing ctor / fromString
  302. unittest
  303. {
  304. static struct ThrowingFromString
  305. {
  306. public static ThrowingFromString fromString (scope const(char)[] value)
  307. @safe pure
  308. {
  309. throw new Exception("Some meaningful error message");
  310. }
  311.  
  312. public int value;
  313. }
  314.  
  315. static struct ThrowingCtor
  316. {
  317. public this (scope const(char)[] value)
  318. @safe pure
  319. {
  320. throw new Exception("Something went wrong... Obviously");
  321. }
  322.  
  323. public int value;
  324. }
  325.  
  326. static struct InnerConfig
  327. {
  328. public int value;
  329. @Optional ThrowingCtor ctor;
  330. @Optional ThrowingFromString fromString;
  331.  
  332. @Converter!int(
  333. (scope ConfigParser!int parser) {
  334. // We have to trick DMD a bit so that it infers an `int` return
  335. // type but doesn't emit a "Statement is not reachable" warning
  336. if (parser.node is Node.init || parser.node !is Node.init )
  337. throw new Exception("You shall not pass");
  338. return 42;
  339. })
  340. @Optional int converter;
  341. }
  342.  
  343. static struct Config
  344. {
  345. public InnerConfig config;
  346. }
  347.  
  348. try
  349. {
  350. auto result = parseConfigString!Config("config:\n value: 42\n ctor: 42", "/dev/null");
  351. assert(0);
  352. }
  353. catch (ConfigException exc)
  354. {
  355. assert(exc.toString() == "/dev/null(2:8): config.ctor: Something went wrong... Obviously");
  356. }
  357.  
  358. try
  359. {
  360. auto result = parseConfigString!Config("config:\n value: 42\n fromString: 42", "/dev/null");
  361. assert(0);
  362. }
  363. catch (ConfigException exc)
  364. {
  365. assert(exc.toString() == "/dev/null(2:14): config.fromString: Some meaningful error message");
  366. }
  367.  
  368. try
  369. {
  370. auto result = parseConfigString!Config("config:\n value: 42\n converter: 42", "/dev/null");
  371. assert(0);
  372. }
  373. catch (ConfigException exc)
  374. {
  375. assert(exc.toString() == "/dev/null(2:13): config.converter: You shall not pass");
  376. }
  377.  
  378. // We also need to test with arrays, to ensure they are correctly called
  379. static struct InnerArrayConfig
  380. {
  381. @Optional int value;
  382. @Optional ThrowingCtor ctor;
  383. @Optional ThrowingFromString fromString;
  384. }
  385.  
  386. static struct ArrayConfig
  387. {
  388. public InnerArrayConfig[] configs;
  389. }
  390.  
  391. try
  392. {
  393. auto result = parseConfigString!ArrayConfig("configs:\n - ctor: something", "/dev/null");
  394. assert(0);
  395. }
  396. catch (ConfigException exc)
  397. {
  398. assert(exc.toString() == "/dev/null(1:10): configs[0].ctor: Something went wrong... Obviously");
  399. }
  400.  
  401. try
  402. {
  403. auto result = parseConfigString!ArrayConfig(
  404. "configs:\n - value: 42\n - fromString: something", "/dev/null");
  405. assert(0);
  406. }
  407. catch (ConfigException exc)
  408. {
  409. assert(exc.toString() == "/dev/null(2:16): configs[1].fromString: Some meaningful error message");
  410. }
  411. }
  412.  
  413. // Test duplicate fields detection
  414. unittest
  415. {
  416. static struct Config
  417. {
  418. @Name("shadow") int value;
  419. @Name("value") int shadow;
  420. }
  421.  
  422. auto result = parseConfigString!Config("shadow: 42\nvalue: 84\n", "/dev/null");
  423. assert(result.value == 42);
  424. assert(result.shadow == 84);
  425.  
  426. static struct BadConfig
  427. {
  428. int value;
  429. @Name("value") int something;
  430. }
  431.  
  432. // Cannot test the error message, so this is as good as it gets
  433. static assert(!is(typeof(() {
  434. auto r = parseConfigString!BadConfig("shadow: 42\nvalue: 84\n", "/dev/null");
  435. })));
  436. }
  437.  
  438. // Test a renamed `enabled` / `disabled`
  439. unittest
  440. {
  441. static struct ConfigA
  442. {
  443. @Name("enabled") bool shouldIStay;
  444. int value;
  445. }
  446.  
  447. static struct ConfigB
  448. {
  449. @Name("disabled") bool orShouldIGo;
  450. int value;
  451. }
  452.  
  453. {
  454. auto c = parseConfigString!ConfigA("enabled: true\nvalue: 42", "/dev/null");
  455. assert(c.shouldIStay == true);
  456. assert(c.value == 42);
  457. }
  458.  
  459. {
  460. auto c = parseConfigString!ConfigB("disabled: false\nvalue: 42", "/dev/null");
  461. assert(c.orShouldIGo == false);
  462. assert(c.value == 42);
  463. }
  464. }
  465.  
  466. // Test for 'mightBeOptional' & missing key
  467. unittest
  468. {
  469. static struct RequestLimit { size_t reqs = 100; }
  470. static struct Nested { @Name("jay") int value; }
  471. static struct Config { @Name("chris") Nested value; RequestLimit limits; }
  472.  
  473. auto r = parseConfigString!Config("chris:\n jay: 42", "/dev/null");
  474. assert(r.limits.reqs == 100);
  475.  
  476. try
  477. {
  478. auto _ = parseConfigString!Config("limits:\n reqs: 42", "/dev/null");
  479. }
  480. catch (ConfigException exc)
  481. {
  482. assert(exc.toString() == "(0:0): chris.jay: Required key was not found in configuration or command line arguments");
  483. }
  484. }
  485.  
  486. // Support for associative arrays
  487. unittest
  488. {
  489. static struct Nested
  490. {
  491. int[string] answers;
  492. }
  493.  
  494. static struct Parent
  495. {
  496. Nested[string] questions;
  497. string[int] names;
  498. }
  499.  
  500. auto c = parseConfigString!Parent(
  501. `names:
  502. 42: "Forty two"
  503. 97: "Quatre vingt dix sept"
  504. questions:
  505. first:
  506. answers:
  507. # Need to use quotes here otherwise it gets interpreted as
  508. # true / false, perhaps a dyaml issue ?
  509. 'yes': 42
  510. 'no': 24
  511. second:
  512. answers:
  513. maybe: 69
  514. whynot: 20
  515. `, "/dev/null");
  516.  
  517. assert(c.names == [42: "Forty two", 97: "Quatre vingt dix sept"]);
  518. assert(c.questions.length == 2);
  519. assert(c.questions["first"] == Nested(["yes": 42, "no": 24]));
  520. assert(c.questions["second"] == Nested(["maybe": 69, "whynot": 20]));
  521. }
  522.  
  523. unittest
  524. {
  525. static struct FlattenMe
  526. {
  527. int value;
  528. string name;
  529. }
  530.  
  531. static struct Config
  532. {
  533. FlattenMe flat = FlattenMe(24, "Four twenty");
  534. alias flat this;
  535.  
  536. FlattenMe not_flat;
  537. }
  538.  
  539. auto c = parseConfigString!Config(
  540. "value: 42\nname: John\nnot_flat:\n value: 69\n name: Henry",
  541. "/dev/null");
  542. assert(c.flat.value == 42);
  543. assert(c.flat.name == "John");
  544. assert(c.not_flat.value == 69);
  545. assert(c.not_flat.name == "Henry");
  546.  
  547. auto c2 = parseConfigString!Config(
  548. "not_flat:\n value: 69\n name: Henry", "/dev/null");
  549. assert(c2.flat.value == 24);
  550. assert(c2.flat.name == "Four twenty");
  551.  
  552. static struct OptConfig
  553. {
  554. @Optional FlattenMe flat;
  555. alias flat this;
  556.  
  557. int value;
  558. }
  559. auto c3 = parseConfigString!OptConfig("value: 69\n", "/dev/null");
  560. assert(c3.value == 69);
  561. }
  562.  
  563. unittest
  564. {
  565. static struct Config
  566. {
  567. @Name("names")
  568. string[] names_;
  569.  
  570. size_t names () const scope @safe pure nothrow @nogc
  571. {
  572. return this.names_.length;
  573. }
  574. }
  575.  
  576. auto c = parseConfigString!Config("names:\n - John\n - Luca\n", "/dev/null");
  577. assert(c.names_ == [ "John", "Luca" ]);
  578. assert(c.names == 2);
  579. }
  580.  
  581. // Make sure unions don't compile
  582. unittest
  583. {
  584. static union MyUnion
  585. {
  586. string value;
  587. int number;
  588. }
  589.  
  590. static struct Config
  591. {
  592. MyUnion hello;
  593. }
  594.  
  595. static assert(!is(typeof(parseConfigString!Config("hello: world\n", "/dev/null"))));
  596. static assert(!is(typeof(parseConfigString!MyUnion("hello: world\n", "/dev/null"))));
  597. }
  598.  
  599. // Test the `@Key` attribute
  600. unittest
  601. {
  602. static struct Interface
  603. {
  604. string name;
  605. string static_ip;
  606. }
  607.  
  608. static struct Config
  609. {
  610. string profile;
  611.  
  612. @Key("name")
  613. immutable(Interface)[] ifaces = [
  614. Interface("lo", "127.0.0.1"),
  615. ];
  616. }
  617.  
  618. auto c = parseConfigString!Config(`profile: default
  619. ifaces:
  620. eth0:
  621. static_ip: "192.168.1.42"
  622. lo:
  623. static_ip: "127.0.0.42"
  624. `, "/dev/null");
  625. assert(c.ifaces.length == 2);
  626. assert(c.ifaces == [ Interface("eth0", "192.168.1.42"), Interface("lo", "127.0.0.42")]);
  627. }
  628.  
  629. unittest
  630. {
  631. static struct Config
  632. {
  633. @Name("names", true)
  634. string[][string] names_;
  635. }
  636.  
  637. auto c = parseConfigString!Config("names-x86:\n - John\n - Luca\nnames:\n - Marie", "/dev/null");
  638. assert(c.names_[null] == [ "Marie" ]);
  639. assert(c.names_["x86"] == [ "John", "Luca" ]);
  640. }
  641.  
  642. unittest
  643. {
  644. static struct PackageDef
  645. {
  646. string name;
  647. @Optional string target;
  648. int build = 42;
  649. }
  650.  
  651. static struct Package
  652. {
  653. string path;
  654. PackageDef def;
  655.  
  656. public static Package fromYAML (scope ConfigParser!Package parser)
  657. {
  658. if (parser.node.nodeID == NodeID.mapping)
  659. return Package(null, parser.parseField!"def");
  660. else
  661. return Package(parser.parseField!"path");
  662. }
  663. }
  664.  
  665. static struct Config
  666. {
  667. string name;
  668. Package[] deps;
  669. }
  670.  
  671. auto c = parseConfigString!Config(
  672. `
  673. name: myPkg
  674. deps:
  675. - /foo/bar
  676. - name: foo
  677. target: bar
  678. build: 24
  679. - name: fur
  680. - /one/last/path
  681. `, "/dev/null");
  682. assert(c.name == "myPkg");
  683. assert(c.deps.length == 4);
  684. assert(c.deps[0] == Package("/foo/bar"));
  685. assert(c.deps[1] == Package(null, PackageDef("foo", "bar", 24)));
  686. assert(c.deps[2] == Package(null, PackageDef("fur", null, 42)));
  687. assert(c.deps[3] == Package("/one/last/path"));
  688. }