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 }