1 // Copyright Juan Manuel Cabo 2012. 2 // Copyright Mario Kröplin 2018. 3 // Distributed under the Boost Software License, Version 1.0. 4 // (See accompanying file LICENSE_1_0.txt or copy at 5 // http://www.boost.org/LICENSE_1_0.txt) 6 7 module dunit.framework; 8 9 import dunit.assertion; 10 import dunit.attributes; 11 import dunit.color; 12 13 import core.runtime; 14 import core.time; 15 import std.algorithm; 16 import std.array; 17 import std.conv; 18 import std.stdio; 19 import std..string; 20 public import std.typetuple; 21 22 struct TestClass 23 { 24 string name; 25 string[] tests; 26 Disabled[string] disabled; 27 EnabledIf[string] enabledIf; 28 DisabledIf[string] disabledIf; 29 EnabledIfEnvironmentVariable[string] enabledIfEnvVar; 30 DisabledIfEnvironmentVariable[string] disabledIfEnvVar; 31 EnabledOnOs[string] enabledOnOs; 32 DisabledOnOs[string] disabledOnOs; 33 Tag[][string] tags; 34 35 Object function() create; 36 void function() beforeAll; 37 void function(Object o) beforeEach; 38 void delegate(Object o, string test) test; 39 void function(Object o) afterEach; 40 void function() afterAll; 41 } 42 43 TestClass[] testClasses; 44 45 struct TestSelection 46 { 47 TestClass testClass; 48 string[] tests; 49 } 50 51 mixin template Main() 52 { 53 int main (string[] args) 54 { 55 return dunit_main(args); 56 } 57 } 58 59 /** 60 * Runs the tests according to the command-line arguments. 61 */ 62 public int dunit_main(string[] args) 63 { 64 import std.getopt : config, defaultGetoptPrinter, getopt, GetoptResult; 65 import std.path : baseName; 66 import std.regex : matchFirst; 67 68 GetoptResult result; 69 string[] filters = null; 70 string[] includeTags = null; 71 string[] excludeTags = null; 72 bool list = false; 73 string report = null; 74 bool verbose = false; 75 bool xml = false; 76 string testSuiteName = "dunit"; 77 78 try 79 { 80 result = getopt(args, 81 config.caseSensitive, 82 "list|l", "Display the test functions, then exit", &list, 83 "filter|f", "Select test functions matching the regular expression", &filters, 84 "include|t", "Provide a tag to be included in the test run", &includeTags, 85 "exclude|T", "Provide a tag to be excluded from the test run", &excludeTags, 86 "verbose|v", "Display more information as the tests are run", &verbose, 87 "xml", "Display progressive XML output", &xml, 88 "report", "Write JUnit-style XML test report", &report, 89 "testsuite", "Provide a test-suite name for the JUnit-style XML test report", &testSuiteName, 90 ); 91 } 92 catch (Exception exception) 93 { 94 stderr.writeln("error: ", exception.msg); 95 return 1; 96 } 97 98 if (result.helpWanted) 99 { 100 writefln("Usage: %s [options]", args.empty ? "testrunner" : baseName(args[0])); 101 writeln("Run the functions with @Test attribute of all classes that mix in UnitTest."); 102 defaultGetoptPrinter("Options:", result.options); 103 return 0; 104 } 105 106 testClasses = unitTestFunctions ~ testClasses; 107 108 TestSelection[] testSelections = null; 109 110 if (filters is null) 111 { 112 foreach (testClass; testClasses) 113 testSelections ~= TestSelection(testClass, testClass.tests); 114 } 115 else 116 { 117 foreach (filter; filters) 118 { 119 foreach (testClass; testClasses) 120 { 121 foreach (test; testClass.tests) 122 { 123 string fullyQualifiedName = testClass.name ~ '.' ~ test; 124 125 if (matchFirst(fullyQualifiedName, filter)) 126 { 127 auto foundTestSelections = testSelections.find!"a.testClass.name == b"(testClass.name); 128 129 if (foundTestSelections.empty) 130 testSelections ~= TestSelection(testClass, [test]); 131 else 132 foundTestSelections.front.tests ~= test; 133 } 134 } 135 } 136 } 137 } 138 if (!includeTags.empty) 139 { 140 testSelections = testSelections 141 .select!"!a.findAmong(b).empty"(includeTags) 142 .array; 143 } 144 if (!excludeTags.empty) 145 { 146 testSelections = testSelections 147 .select!"a.findAmong(b).empty"(excludeTags) 148 .array; 149 } 150 151 if (list) 152 { 153 foreach (testSelection; testSelections) with (testSelection) 154 { 155 foreach (test; tests) 156 { 157 string fullyQualifiedName = testClass.name ~ '.' ~ test; 158 159 writeln(fullyQualifiedName); 160 } 161 } 162 return 0; 163 } 164 165 if (xml) 166 { 167 testListeners ~= new XmlReporter(); 168 } 169 else 170 { 171 if (verbose) 172 testListeners ~= new DetailReporter(); 173 else 174 testListeners ~= new IssueReporter(); 175 } 176 177 if (!report.empty) 178 testListeners ~= new ReportReporter(report, testSuiteName); 179 180 auto reporter = new ResultReporter(); 181 182 testListeners ~= reporter; 183 runTests(testSelections, testListeners); 184 if (!xml) 185 reporter.write(); 186 return (reporter.errors > 0) ? 1 : (reporter.failures > 0) ? 2 : 0; 187 } 188 189 private auto select(alias pred)(TestSelection[] testSelections, string[] tags) 190 { 191 import std.functional : binaryFun; 192 193 bool matches(TestClass testClass, string test) 194 { 195 auto testTags = testClass.tags.get(test, null) 196 .map!(tag => tag.name); 197 198 return binaryFun!pred(testTags, tags); 199 } 200 201 TestSelection select(TestSelection testSelection) 202 { 203 string[] tests = testSelection.tests 204 .filter!(test => matches(testSelection.testClass, test)) 205 .array; 206 207 return TestSelection(testSelection.testClass, tests); 208 } 209 210 return testSelections 211 .map!(testSelection => select(testSelection)) 212 .filter!(testSelection => !testSelection.tests.empty); 213 } 214 215 private TestSelection[] restrict(alias pred)(TestSelection[] testSelections, string[] tags) 216 { 217 TestSelection restrict(TestSelection testSelection) 218 { 219 string[] tests = testSelection.tests 220 .filter!(test => pred(testSelection.testClass.tags.get(test, null), tags)) 221 .array; 222 223 return TestSelection(testSelection.testClass, tests); 224 } 225 226 return testSelections 227 .map!(testSelection => restrict(testSelection)) 228 .filter!(testSelection => !testSelection.tests.empty) 229 .array; 230 } 231 232 public bool matches(Tag[] tags, string[] choices) 233 { 234 return tags.any!(tag => choices.canFind(tag.name)); 235 } 236 237 public void runTests(TestSelection[] testSelections, TestListener[] testListeners) 238 in 239 { 240 assert(all!"a !is null"(testListeners)); 241 } 242 body 243 { 244 bool tryRun(string phase, void delegate() action) 245 { 246 try 247 { 248 version(Have_dshould) 249 { 250 import dshould.ShouldType : FluentException; 251 252 try 253 { 254 action(); 255 } 256 catch (FluentException exception) 257 { 258 // convert exception to "fix" the message format 259 throw new AssertException('\n' ~ exception.msg, 260 exception.file, exception.line, exception); 261 } 262 } 263 else 264 { 265 action(); 266 } 267 return true; 268 } 269 catch (AssertException exception) 270 { 271 foreach (testListener; testListeners) 272 testListener.addFailure(phase, exception); 273 return false; 274 } 275 catch (Throwable throwable) 276 { 277 foreach (testListener; testListeners) 278 testListener.addError(phase, throwable); 279 return false; 280 } 281 } 282 283 foreach (testSelection; testSelections) with (testSelection) 284 { 285 foreach (testListener; testListeners) 286 testListener.enterClass(testClass.name); 287 288 bool initialized = false; 289 bool setUp = false; 290 291 // run each @Test of the class 292 foreach (test; tests) 293 { 294 bool success = false; 295 296 foreach (testListener; testListeners) 297 testListener.enterTest(test); 298 scope (exit) 299 foreach (testListener; testListeners) 300 testListener.exitTest(success); 301 302 const disablingCauses = findDisablingCauses(testClass, test); 303 304 if ((initialized && !setUp) || !disablingCauses.empty) 305 { 306 const reason = (initialized && !setUp) 307 ? "running @BeforeAll failed for preceding test case" 308 : disablingCauses.front.reason; 309 310 foreach (testListener; testListeners) 311 testListener.skip(reason); 312 continue; 313 } 314 315 // use lazy initialization to run @BeforeAll 316 // (failure or error can only be reported for a given test) 317 if (!initialized) 318 { 319 setUp = tryRun("@BeforeAll", 320 { testClass.beforeAll(); }); 321 initialized = true; 322 } 323 324 Object testObject = null; 325 326 if (setUp) 327 { 328 success = tryRun("this", 329 { testObject = testClass.create(); }); 330 } 331 if (success) 332 { 333 success = tryRun("@BeforeEach", 334 { testClass.beforeEach(testObject); }); 335 } 336 if (success) 337 { 338 success = tryRun("@Test", 339 { testClass.test(testObject, test); }); 340 // run @AfterEach even if @Test failed 341 success = tryRun("@AfterEach", 342 { testClass.afterEach(testObject); }) 343 && success; 344 } 345 } 346 if (setUp) 347 { 348 tryRun("@AfterAll", 349 { testClass.afterAll(); }); 350 } 351 } 352 353 foreach (testListener; testListeners) 354 testListener.exit(); 355 } 356 357 private Disabled[] findDisablingCauses(TestClass testClass, string test) 358 { 359 import std.process : environment; 360 import std.regex : regex, matchFirst; 361 import std.system : os, OS; 362 363 Disabled[] disablingCauses; 364 365 if (test in testClass.disabled) 366 { 367 disablingCauses ~= testClass.disabled[test]; 368 } 369 if (test in testClass.disabledOnOs) with (testClass.disabledOnOs[test]) 370 { 371 if (value.canFind(os)) 372 { 373 const reason = format("operating system %s included in %s", os, value); 374 375 disablingCauses ~= Disabled(reason); 376 } 377 } 378 if (test in testClass.enabledOnOs) with (testClass.enabledOnOs[test]) 379 { 380 if (!value.canFind(os)) 381 { 382 const reason = format("operating system %s not included in %s", os, value); 383 384 disablingCauses ~= Disabled(reason); 385 } 386 } 387 if (test in testClass.disabledIfEnvVar) with (testClass.disabledIfEnvVar[test]) 388 { 389 if (environment.get(named) !is null && matchFirst(environment.get(named), regex(matches))) 390 { 391 const reason = format(`value "%s" of environment variable %s matches %s`, 392 environment.get(named), named, matches); 393 394 disablingCauses ~= Disabled(reason); 395 } 396 } 397 if (test in testClass.enabledIfEnvVar) with (testClass.enabledIfEnvVar[test]) 398 { 399 if (environment.get(named) is null) 400 { 401 const reason = format("environment variable %s not set", named); 402 403 disablingCauses ~= Disabled(reason); 404 } 405 else if (!matchFirst(environment.get(named), regex(matches))) 406 { 407 const reason = format(`value "%s" of environment variable %s does not match %s`, 408 environment.get(named), named, matches); 409 410 disablingCauses ~= Disabled(reason); 411 } 412 } 413 if (test in testClass.disabledIf) with (testClass.disabledIf[test]) 414 { 415 if (condition()) 416 disablingCauses ~= Disabled(reason); 417 } 418 if (test in testClass.enabledIf) with (testClass.enabledIf[test]) 419 { 420 if (!condition()) 421 disablingCauses ~= Disabled(reason); 422 } 423 return disablingCauses; 424 } 425 426 private __gshared TestListener[] testListeners = null; 427 428 /** 429 * Registered implementations of this interface will be notified 430 * about events that occur during the test run. 431 */ 432 interface TestListener 433 { 434 public void enterClass(string className); 435 public void enterTest(string test); 436 public void skip(string reason); 437 public void addFailure(string phase, AssertException exception); 438 public void addError(string phase, Throwable throwable); 439 public void exitTest(bool success); 440 public void exit(); 441 442 public static string prettyOrigin(string className, string test, string phase) 443 { 444 const origin = prettyOrigin(test, phase); 445 446 if (origin.startsWith('@')) 447 return className ~ origin; 448 else 449 return className ~ '.' ~ origin; 450 } 451 452 public static string prettyOrigin(string test, string phase) 453 { 454 switch (phase) 455 { 456 case "@Test": 457 return test; 458 case "this": 459 case "@BeforeAll": 460 case "@AfterAll": 461 return phase; 462 default: 463 return test ~ phase; 464 } 465 } 466 } 467 468 /** 469 * Writes a "progress bar", followed by the errors and the failures. 470 */ 471 class IssueReporter : TestListener 472 { 473 private struct Issue 474 { 475 string testClass; 476 string test; 477 string phase; 478 Throwable throwable; 479 } 480 481 private Issue[] failures = null; 482 private Issue[] errors = null; 483 private string className; 484 private string test; 485 486 public override void enterClass(string className) 487 { 488 this.className = className; 489 } 490 491 public override void enterTest(string test) 492 { 493 this.test = test; 494 } 495 496 public override void skip(string reason) 497 { 498 writec(Color.onYellow, "S"); 499 } 500 501 public override void addFailure(string phase, AssertException exception) 502 { 503 this.failures ~= Issue(this.className, this.test, phase, exception); 504 writec(Color.onRed, "F"); 505 } 506 507 public override void addError(string phase, Throwable throwable) 508 { 509 this.errors ~= Issue(this.className, this.test, phase, throwable); 510 writec(Color.onRed, "E"); 511 } 512 513 public override void exitTest(bool success) 514 { 515 if (success) 516 writec(Color.onGreen, "."); 517 } 518 519 public override void exit() 520 { 521 writeln(); 522 523 // report errors 524 if (!this.errors.empty) 525 { 526 writeln(); 527 if (this.errors.length == 1) 528 writeln("There was 1 error:"); 529 else 530 writefln("There were %d errors:", this.errors.length); 531 532 foreach (i, issue; this.errors) 533 { 534 writefln("%d) %s", i + 1, 535 prettyOrigin(issue.testClass, issue.test, issue.phase)); 536 writeln(issue.throwable.description); 537 stdout.lockingTextWriter.description(issue.throwable.info); 538 } 539 } 540 541 // report failures 542 if (!this.failures.empty) 543 { 544 writeln(); 545 if (this.failures.length == 1) 546 writeln("There was 1 failure:"); 547 else 548 writefln("There were %d failures:", this.failures.length); 549 550 foreach (i, issue; this.failures) 551 { 552 writefln("%d) %s", i + 1, 553 prettyOrigin(issue.testClass, issue.test, issue.phase)); 554 writeln(issue.throwable.description); 555 } 556 } 557 } 558 } 559 560 /** 561 * Writes a detailed test report. 562 */ 563 class DetailReporter : TestListener 564 { 565 private string test; 566 private TickDuration startTime; 567 568 public override void enterClass(string className) 569 { 570 writeln(className); 571 } 572 573 public override void enterTest(string test) 574 { 575 this.test = test; 576 this.startTime = TickDuration.currSystemTick(); 577 } 578 579 public override void skip(string reason) 580 { 581 writec(Color.yellow, " SKIP: "); 582 writeln(this.test); 583 if (!reason.empty) 584 writeln(indent(format(`"%s"`, reason))); 585 } 586 587 public override void addFailure(string phase, AssertException exception) 588 { 589 writec(Color.red, " FAILURE: "); 590 writeln(prettyOrigin(this.test, phase)); 591 writeln(indent(exception.description)); 592 } 593 594 public override void addError(string phase, Throwable throwable) 595 { 596 writec(Color.red, " ERROR: "); 597 writeln(prettyOrigin(this.test, phase)); 598 writeln(indent(throwable.description)); 599 stdout.lockingTextWriter.description(throwable.info); 600 } 601 602 public override void exitTest(bool success) 603 { 604 if (success) 605 { 606 const elapsed = (TickDuration.currSystemTick() - this.startTime).usecs() / 1_000.0; 607 608 writec(Color.green, " OK: "); 609 writefln("%6.2f ms %s", elapsed, this.test); 610 } 611 } 612 613 public override void exit() 614 { 615 // do nothing 616 } 617 618 private static string indent(string s, string indent = " ") 619 { 620 return s.splitLines(KeepTerminator.yes).map!(line => indent ~ line).join; 621 } 622 } 623 624 /** 625 * Writes a summary about the tests run. 626 */ 627 class ResultReporter : TestListener 628 { 629 private uint tests = 0; 630 private uint failures = 0; 631 private uint errors = 0; 632 private uint skips = 0; 633 634 public override void enterClass(string className) 635 { 636 // do nothing 637 } 638 639 public override void enterTest(string test) 640 { 641 ++this.tests; 642 } 643 644 public override void skip(string reason) 645 { 646 ++this.skips; 647 } 648 649 public override void addFailure(string phase, AssertException exception) 650 { 651 ++this.failures; 652 } 653 654 public override void addError(string phase, Throwable throwable) 655 { 656 ++this.errors; 657 } 658 659 public override void exitTest(bool success) 660 { 661 // do nothing 662 } 663 664 public override void exit() 665 { 666 // do nothing 667 } 668 669 public void write() const 670 { 671 writeln(); 672 writefln("Tests run: %d, Failures: %d, Errors: %d, Skips: %d", 673 this.tests, this.failures, this.errors, this.skips); 674 675 if (this.failures + this.errors == 0) 676 { 677 writec(Color.onGreen, "OK"); 678 writeln(); 679 } 680 else 681 { 682 writec(Color.onRed, "NOT OK"); 683 writeln(); 684 } 685 } 686 } 687 688 /** 689 * Writes progressive XML output. 690 */ 691 class XmlReporter : TestListener 692 { 693 import undead.xml : Document, Element, Tag; 694 695 private Document testCase; 696 private string className; 697 private TickDuration startTime; 698 699 public override void enterClass(string className) 700 { 701 this.className = className; 702 } 703 704 public override void enterTest(string test) 705 { 706 this.testCase = new Document(new Tag("testcase")); 707 this.testCase.tag.attr["classname"] = this.className; 708 this.testCase.tag.attr["name"] = test; 709 this.startTime = TickDuration.currSystemTick(); 710 } 711 712 public override void skip(string reason) 713 { 714 auto element = new Element("skipped"); 715 716 element.tag.attr["message"] = reason; 717 this.testCase ~= element; 718 } 719 720 public override void addFailure(string phase, AssertException exception) 721 { 722 auto element = new Element("failure"); 723 const message = format("%s %s", phase, exception.description); 724 725 element.tag.attr["message"] = message; 726 this.testCase ~= element; 727 } 728 729 public override void addError(string phase, Throwable throwable) 730 { 731 auto element = new Element("error", throwable.info.toString); 732 const message = format("%s %s", phase, throwable.description); 733 734 element.tag.attr["message"] = message; 735 this.testCase ~= element; 736 } 737 738 public override void exitTest(bool success) 739 { 740 const elapsed = (TickDuration.currSystemTick() - this.startTime).msecs() / 1_000.0; 741 742 this.testCase.tag.attr["time"] = format("%.3f", elapsed); 743 744 const report = join(this.testCase.pretty(4), "\n"); 745 746 writeln(report); 747 } 748 749 public override void exit() 750 { 751 // do nothing 752 } 753 } 754 755 /** 756 * Writes a JUnit-style XML test report. 757 */ 758 class ReportReporter : TestListener 759 { 760 import undead.xml : Document, Element, Tag; 761 762 private const string fileName; 763 private Document document; 764 private Element testSuite; 765 private Element testCase; 766 private string className; 767 private TickDuration startTime; 768 769 public this(string fileName, string testSuiteName) 770 { 771 this.fileName = fileName; 772 this.document = new Document(new Tag("testsuites")); 773 this.testSuite = new Element("testsuite"); 774 this.testSuite.tag.attr["name"] = testSuiteName; 775 this.document ~= this.testSuite; 776 } 777 778 public override void enterClass(string className) 779 { 780 this.className = className; 781 } 782 783 public override void enterTest(string test) 784 { 785 this.testCase = new Element("testcase"); 786 this.testCase.tag.attr["classname"] = this.className; 787 this.testCase.tag.attr["name"] = test; 788 this.testSuite ~= this.testCase; 789 this.startTime = TickDuration.currSystemTick(); 790 } 791 792 public override void skip(string reason) 793 { 794 // avoid wrong interpretation of more than one child 795 if (this.testCase.elements.empty) 796 { 797 auto element = new Element("skipped"); 798 799 element.tag.attr["message"] = reason; 800 this.testCase ~= element; 801 } 802 } 803 804 public override void addFailure(string phase, AssertException exception) 805 { 806 // avoid wrong interpretation of more than one child 807 if (this.testCase.elements.empty) 808 { 809 auto element = new Element("failure"); 810 const message = format("%s %s", phase, exception.description); 811 812 element.tag.attr["message"] = message; 813 this.testCase ~= element; 814 } 815 } 816 817 public override void addError(string phase, Throwable throwable) 818 { 819 // avoid wrong interpretation of more than one child 820 if (this.testCase.elements.empty) 821 { 822 auto element = new Element("error", throwable.info.toString); 823 const message = format("%s %s", phase, throwable.description); 824 825 element.tag.attr["message"] = message; 826 this.testCase ~= element; 827 } 828 } 829 830 public override void exitTest(bool success) 831 { 832 const elapsed = (TickDuration.currSystemTick() - this.startTime).msecs() / 1_000.0; 833 834 this.testCase.tag.attr["time"] = format("%.3f", elapsed); 835 } 836 837 public override void exit() 838 { 839 import std.file : mkdirRecurse, write; 840 import std.path: dirName; 841 842 const report = join(this.document.pretty(4), "\n") ~ "\n"; 843 844 mkdirRecurse(this.fileName.dirName); 845 write(this.fileName, report); 846 } 847 } 848 849 shared static this() 850 { 851 Runtime.moduleUnitTester = () => true; 852 } 853 854 private TestClass[] unitTestFunctions() 855 { 856 TestClass[] testClasses = null; 857 TestClass testClass; 858 859 testClass.tests = ["unittest"]; 860 testClass.create = () => null; 861 testClass.beforeAll = () {}; 862 testClass.beforeEach = (o) {}; 863 testClass.afterEach = (o) {}; 864 testClass.afterAll = () {}; 865 866 foreach (moduleInfo; ModuleInfo) 867 { 868 if (moduleInfo) 869 { 870 auto unitTest = moduleInfo.unitTest; 871 872 if (unitTest) 873 { 874 testClass.name = moduleInfo.name; 875 testClass.test = (o, test) { unitTest(); }; 876 testClasses ~= testClass; 877 } 878 } 879 } 880 return testClasses; 881 } 882 883 /** 884 * Registers a class as a unit test. 885 */ 886 mixin template UnitTest() 887 { 888 private static this() 889 { 890 TestClass testClass; 891 892 testClass.name = this.classinfo.name; 893 testClass.tests = _members!(typeof(this), Test); 894 testClass.disabled = _attributeByMember!(typeof(this), Disabled); 895 testClass.enabledIf = _attributeByMember!(typeof(this), EnabledIf); 896 testClass.disabledIf = _attributeByMember!(typeof(this), DisabledIf); 897 testClass.enabledIfEnvVar = _attributeByMember!(typeof(this), EnabledIfEnvironmentVariable); 898 testClass.disabledIfEnvVar = _attributeByMember!(typeof(this), DisabledIfEnvironmentVariable); 899 testClass.enabledOnOs = _attributeByMember!(typeof(this), EnabledOnOs); 900 testClass.disabledOnOs = _attributeByMember!(typeof(this), DisabledOnOs); 901 testClass.tags = _attributesByMember!(typeof(this), Tag); 902 903 testClass.create = () 904 { 905 mixin("return new " ~ typeof(this).stringof ~ "();"); 906 }; 907 testClass.beforeAll = () 908 { 909 mixin(_staticSequence(_members!(typeof(this), BeforeAll))); 910 }; 911 testClass.beforeEach = (Object o) 912 { 913 mixin(_sequence(_members!(typeof(this), BeforeEach))); 914 }; 915 testClass.test = (Object o, string name) 916 { 917 mixin(_choice(_members!(typeof(this), Test))); 918 }; 919 testClass.afterEach = (Object o) 920 { 921 mixin(_sequence(_members!(typeof(this), AfterEach))); 922 }; 923 testClass.afterAll = () 924 { 925 mixin(_staticSequence(_members!(typeof(this), AfterAll))); 926 }; 927 928 testClasses ~= testClass; 929 } 930 931 private static string _choice(in string[] memberFunctions) 932 { 933 string block = "auto testObject = cast(" ~ typeof(this).stringof ~ ") o;\n"; 934 935 block ~= "switch (name)\n{\n"; 936 foreach (memberFunction; memberFunctions) 937 block ~= `case "` ~ memberFunction ~ `": testObject.` ~ memberFunction ~ "(); break;\n"; 938 block ~= "default: break;\n}\n"; 939 return block; 940 } 941 942 private static string _staticSequence(in string[] memberFunctions) 943 { 944 string block = null; 945 946 foreach (memberFunction; memberFunctions) 947 block ~= memberFunction ~ "();\n"; 948 return block; 949 } 950 951 private static string _sequence(in string[] memberFunctions) 952 { 953 string block = "auto testObject = cast(" ~ typeof(this).stringof ~ ") o;\n"; 954 955 foreach (memberFunction; memberFunctions) 956 block ~= "testObject." ~ memberFunction ~ "();\n"; 957 return block; 958 } 959 960 template _members(T, alias attribute) 961 { 962 static string[] helper() 963 { 964 import std.meta : AliasSeq; 965 import std.traits : hasUDA; 966 967 string[] members; 968 969 foreach (name; __traits(allMembers, T)) 970 { 971 static if (__traits(compiles, __traits(getMember, T, name))) 972 { 973 alias member = AliasSeq!(__traits(getMember, T, name)); 974 975 static if (__traits(compiles, hasUDA!(member, attribute))) 976 { 977 static if (hasUDA!(member, attribute)) 978 members ~= name; 979 } 980 } 981 } 982 return members; 983 } 984 985 enum _members = helper; 986 } 987 988 template _attributeByMember(T, Attribute) 989 { 990 static Attribute[string] helper() 991 { 992 import std.format : format; 993 import std.meta : AliasSeq; 994 995 Attribute[string] attributeByMember; 996 997 foreach (name; __traits(allMembers, T)) 998 { 999 static if (__traits(compiles, __traits(getMember, T, name))) 1000 { 1001 alias member = AliasSeq!(__traits(getMember, T, name)); 1002 1003 static if (__traits(compiles, _getUDAs!(member, Attribute))) 1004 { 1005 alias attributes = _getUDAs!(member, Attribute); 1006 1007 static if (attributes.length > 0) 1008 { 1009 static assert(attributes.length == 1, 1010 format("%s.%s should not have more than one attribute @%s", 1011 T.stringof, name, Attribute.stringof)); 1012 1013 attributeByMember[name] = attributes[0]; 1014 } 1015 } 1016 } 1017 } 1018 return attributeByMember; 1019 } 1020 1021 enum _attributeByMember = helper; 1022 } 1023 1024 template _attributesByMember(T, Attribute) 1025 { 1026 static Attribute[][string] helper() 1027 { 1028 import std.meta : AliasSeq; 1029 1030 Attribute[][string] attributesByMember; 1031 1032 foreach (name; __traits(allMembers, T)) 1033 { 1034 static if (__traits(compiles, __traits(getMember, T, name))) 1035 { 1036 alias member = AliasSeq!(__traits(getMember, T, name)); 1037 1038 static if (__traits(compiles, _getUDAs!(member, Attribute))) 1039 { 1040 alias attributes = _getUDAs!(member, Attribute); 1041 1042 static if (attributes.length > 0) 1043 attributesByMember[name] = attributes; 1044 } 1045 } 1046 } 1047 return attributesByMember; 1048 } 1049 1050 enum _attributesByMember = helper; 1051 } 1052 1053 // Gets user-defined attributes, but also gets Attribute.init for @Attribute. 1054 template _getUDAs(alias member, Attribute) 1055 { 1056 static auto helper() 1057 { 1058 Attribute[] attributes; 1059 1060 static if (__traits(compiles, __traits(getAttributes, member))) 1061 { 1062 foreach (attribute; __traits(getAttributes, member)) 1063 { 1064 static if (is(attribute == Attribute)) 1065 attributes ~= Attribute.init; 1066 static if (is(typeof(attribute) == Attribute)) 1067 attributes ~= attribute; 1068 } 1069 } 1070 return attributes; 1071 } 1072 1073 enum _getUDAs = helper; 1074 } 1075 }