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