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 testClass.create = () 821 { 822 mixin("return new " ~ typeof(this).stringof ~ "();"); 823 }; 824 testClass.beforeAll = () 825 { 826 mixin(_staticSequence(_members!(typeof(this), BeforeAll))); 827 }; 828 testClass.beforeEach = (Object o) 829 { 830 mixin(_sequence(_members!(typeof(this), BeforeEach))); 831 }; 832 testClass.test = (Object o, string name) 833 { 834 mixin(_choice(_members!(typeof(this), Test))); 835 }; 836 testClass.afterEach = (Object o) 837 { 838 mixin(_sequence(_members!(typeof(this), AfterEach))); 839 }; 840 testClass.afterAll = () 841 { 842 mixin(_staticSequence(_members!(typeof(this), AfterAll))); 843 }; 844 845 testClasses ~= testClass; 846 } 847 848 private static string _choice(in string[] memberFunctions) 849 { 850 string block = "auto testObject = cast(" ~ typeof(this).stringof ~ ") o;\n"; 851 852 block ~= "switch (name)\n{\n"; 853 foreach (memberFunction; memberFunctions) 854 block ~= `case "` ~ memberFunction ~ `": testObject.` ~ memberFunction ~ "(); break;\n"; 855 block ~= "default: break;\n}\n"; 856 return block; 857 } 858 859 private static string _staticSequence(in string[] memberFunctions) 860 { 861 string block = null; 862 863 foreach (memberFunction; memberFunctions) 864 block ~= memberFunction ~ "();\n"; 865 return block; 866 } 867 868 private static string _sequence(in string[] memberFunctions) 869 { 870 string block = "auto testObject = cast(" ~ typeof(this).stringof ~ ") o;\n"; 871 872 foreach (memberFunction; memberFunctions) 873 block ~= "testObject." ~ memberFunction ~ "();\n"; 874 return block; 875 } 876 877 template _members(T, alias attribute) 878 { 879 static string[] helper() 880 { 881 import std.meta : AliasSeq; 882 import std.traits : hasUDA; 883 884 string[] members; 885 886 foreach (name; __traits(allMembers, T)) 887 { 888 static if (__traits(compiles, __traits(getMember, T, name))) 889 { 890 alias member = AliasSeq!(__traits(getMember, T, name)); 891 892 static if (__traits(compiles, hasUDA!(member, attribute))) 893 { 894 static if (hasUDA!(member, attribute)) 895 members ~= name; 896 } 897 } 898 } 899 return members; 900 } 901 902 enum _members = helper; 903 } 904 905 template _attributeByMember(T, Attribute) 906 { 907 static Attribute[string] helper() 908 { 909 import std.format : format; 910 import std.meta : AliasSeq; 911 912 Attribute[string] attributeByMember; 913 914 foreach (name; __traits(allMembers, T)) 915 { 916 static if (__traits(compiles, __traits(getMember, T, name))) 917 { 918 alias member = AliasSeq!(__traits(getMember, T, name)); 919 920 static if (__traits(compiles, _getUDAs!(member, Attribute))) 921 { 922 alias attributes = _getUDAs!(member, Attribute); 923 924 static if (attributes.length > 0) 925 { 926 static assert(attributes.length == 1, 927 format("%s.%s should not have more than one attribute @%s", 928 T.stringof, name, Attribute.stringof)); 929 930 attributeByMember[name] = attributes[0]; 931 } 932 } 933 } 934 } 935 return attributeByMember; 936 } 937 938 enum _attributeByMember = helper; 939 } 940 941 template _attributesByMember(T, Attribute) 942 { 943 static Attribute[][string] helper() 944 { 945 import std.meta : AliasSeq; 946 947 Attribute[][string] attributesByMember; 948 949 foreach (name; __traits(allMembers, T)) 950 { 951 static if (__traits(compiles, __traits(getMember, T, name))) 952 { 953 alias member = AliasSeq!(__traits(getMember, T, name)); 954 955 static if (__traits(compiles, _getUDAs!(member, Attribute))) 956 { 957 alias attributes = _getUDAs!(member, Attribute); 958 959 static if (attributes.length > 0) 960 attributesByMember[name] = attributes; 961 } 962 } 963 } 964 return attributesByMember; 965 } 966 967 enum _attributesByMember = helper; 968 } 969 970 // Gets user-defined attributes, but also gets Attribute.init for @Attribute. 971 template _getUDAs(alias member, Attribute) 972 { 973 static auto helper() 974 { 975 Attribute[] attributes; 976 977 static if (__traits(compiles, __traits(getAttributes, member))) 978 { 979 foreach (attribute; __traits(getAttributes, member)) 980 { 981 static if (is(attribute == Attribute)) 982 attributes ~= Attribute.init; 983 static if (is(typeof(attribute) == Attribute)) 984 attributes ~= attribute; 985 } 986 } 987 return attributes; 988 } 989 990 enum _getUDAs = helper; 991 } 992 }