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