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 }