1 /** 2 Compile-time reflection to find unit tests and set their properties. 3 */ 4 module unit_threaded.runner.reflection; 5 6 7 import unit_threaded.from; 8 9 /* 10 These standard library imports contain something important for the code below. 11 Unfortunately I don't know what they are so they're to prevent breakage. 12 */ 13 import std.traits; 14 import std.algorithm; 15 import std.array; 16 17 18 /** 19 An alternative to writing test functions by hand to avoid compile-time 20 performance penalties by using -unittest. 21 */ 22 mixin template Test(string testName, alias Body, size_t line = __LINE__) { 23 import std.format: format; 24 import unit_threaded.runner.attrs: Name, UnitTest; 25 import unit_threaded.runner.reflection: unittestFunctionName; 26 27 enum unitTestCode = q{ 28 @UnitTest 29 @Name("%s") 30 void %s() { 31 32 } 33 }.format(testName, unittestFunctionName(line)); 34 35 //pragma(msg, unitTestCode); 36 mixin(unitTestCode); 37 } 38 39 40 string unittestFunctionName(size_t line = __LINE__) { 41 import std.conv: text; 42 return "unittest_L" ~ line.text; 43 } 44 45 /// 46 alias TestFunction = void delegate(); 47 48 /** 49 * Common data for test functions and test classes 50 */ 51 struct TestData { 52 string name; 53 TestFunction testFunction; ///only used for functions, null for classes 54 bool hidden; 55 bool shouldFail; 56 bool singleThreaded; 57 bool builtin; 58 string suffix; // append to end of getPath 59 string[] tags; 60 TypeInfo exceptionTypeInfo; // for ShouldFailWith 61 int flakyRetries = 0; 62 63 /// The test's name 64 string getPath() const pure nothrow { 65 string path = name.dup; 66 import std.array: empty; 67 if(!suffix.empty) path ~= "." ~ suffix; 68 return path; 69 } 70 71 /// If the test is a class 72 bool isTestClass() @safe const pure nothrow { 73 return testFunction is null; 74 } 75 } 76 77 78 /** 79 * Finds all test cases (functions, classes, built-in unittest blocks) 80 * Template parameters are module strings 81 */ 82 const(TestData)[] allTestData(MOD_STRINGS...)() 83 if(from!"std.meta".allSatisfy!(from!"std.traits".isSomeString, typeof(MOD_STRINGS))) 84 { 85 import std.array: join; 86 import std.range : iota; 87 import std.format : format; 88 import std.algorithm : map; 89 90 string getModulesString() { 91 string[] modules; 92 foreach(i, module_; MOD_STRINGS) modules ~= "module%d = %s".format(i, module_); 93 return modules.join(", "); 94 } 95 96 enum modulesString = getModulesString; 97 mixin("import " ~ modulesString ~ ";"); 98 mixin("return allTestData!(" ~ 99 MOD_STRINGS.length.iota.map!(i => "module%d".format(i)).join(", ") ~ 100 ");"); 101 } 102 103 104 /** 105 * Finds all test cases (functions, classes, built-in unittest blocks) 106 * Template parameters are module symbols 107 */ 108 const(TestData)[] allTestData(MOD_SYMBOLS...)() 109 if(!from!"std.meta".anySatisfy!(from!"std.traits".isSomeString, typeof(MOD_SYMBOLS))) 110 { 111 return 112 moduleTestClasses!MOD_SYMBOLS ~ 113 moduleTestFunctions!MOD_SYMBOLS ~ 114 moduleUnitTests!MOD_SYMBOLS; 115 } 116 117 118 private template Identity(T...) if(T.length > 0) { 119 static if(__traits(compiles, { alias x = T[0]; })) 120 alias Identity = T[0]; 121 else 122 enum Identity = T[0]; 123 } 124 125 126 /** 127 Names a test function / built-in unittest based on @Name or string UDAs 128 on it. If none are found, "returns" an empty string 129 */ 130 template TestNameFromAttr(alias testFunction) { 131 import unit_threaded.runner.attrs: Name; 132 import std.traits: getUDAs; 133 import std.meta: Filter; 134 135 // i.e. if @("this is my name") appears 136 enum strAttrs = Filter!(isStringUDA, __traits(getAttributes, testFunction)); 137 138 enum nameAttrs = getUDAs!(testFunction, Name); 139 static assert(nameAttrs.length < 2, "Only one @Name UDA allowed"); 140 141 // strAttrs might be values to pass so only if the length is 1 is it a name 142 enum hasName = nameAttrs.length || strAttrs.length == 1; 143 144 static if(hasName) { 145 static if(nameAttrs.length == 1) 146 enum TestNameFromAttr = nameAttrs[0].value; 147 else 148 enum TestNameFromAttr = strAttrs[0]; 149 } else 150 enum TestNameFromAttr = ""; 151 } 152 153 /** 154 * Finds all built-in unittest blocks in the given modules. 155 * Recurses into structs, classes, and unions of the modules. 156 * 157 * @return An array of TestData structs 158 */ 159 TestData[] moduleUnitTests(modules...)() { 160 TestData[] ret; 161 static foreach(module_; modules) { 162 ret ~= moduleUnitTests_!module_; 163 } 164 return ret; 165 } 166 167 /** 168 * Finds all built-in unittest blocks in the given module. 169 * Recurses into structs, classes, and unions of the module. 170 * 171 * @return An array of TestData structs 172 */ 173 private TestData[] moduleUnitTests_(alias module_)() { 174 175 // Return a name for a unittest block. If no @Name UDA is found a name is 176 // created automatically, else the UDA is used. 177 // the weird name for the first template parameter is so that it doesn't clash 178 // with a package name 179 string unittestName(alias _theUnitTest, int index)() @safe nothrow { 180 import std.conv: text; 181 import std.algorithm: startsWith, endsWith; 182 import std.traits: fullyQualifiedName; 183 184 enum prefix = fullyQualifiedName!(__traits(parent, _theUnitTest)) ~ "."; 185 enum nameFromAttr = TestNameFromAttr!_theUnitTest; 186 187 // Establish a unique name for a unittest with no name 188 static if(nameFromAttr == "") { 189 // use the unittest name if available to allow for running unittests based 190 // on location 191 if(__traits(identifier, _theUnitTest).startsWith("__unittest_L")) { 192 const ret = prefix ~ __traits(identifier, _theUnitTest)[2 .. $]; 193 const suffix = "_C1"; 194 // simplify names for the common case where there's only one 195 // unittest per line 196 197 return ret.endsWith(suffix) ? ret[0 .. $ - suffix.length] : ret; 198 } 199 200 try 201 return prefix ~ "unittest" ~ index.text; 202 catch(Exception) 203 assert(false, text("Error converting ", index, " to string")); 204 205 } else 206 return prefix ~ nameFromAttr; 207 } 208 209 void function() getUDAFunction(alias composite, alias uda)() pure nothrow { 210 import std.traits: isSomeFunction, hasUDA; 211 212 void function()[] ret; 213 foreach(memberStr; __traits(allMembers, composite)) { 214 static if(__traits(compiles, Identity!(__traits(getMember, composite, memberStr)))) { 215 alias member = Identity!(__traits(getMember, composite, memberStr)); 216 static if(__traits(compiles, &member)) { 217 static if(isSomeFunction!member && hasUDA!(member, uda)) { 218 ret ~= &member; 219 } 220 } 221 } 222 } 223 224 return ret.length ? ret[0] : null; 225 } 226 227 TestData[] testData; 228 229 void addMemberUnittests(alias composite)() pure nothrow { 230 231 import unit_threaded.runner.attrs; 232 import std.traits: hasUDA; 233 import std.meta: Filter, aliasSeqOf; 234 import std.algorithm: map, cartesianProduct; 235 236 // weird name for hygiene reasons 237 foreach(index, eLtEstO; __traits(getUnitTests, composite)) { 238 239 enum dontTest = hasUDA!(eLtEstO, DontTest); 240 241 static if(!dontTest) { 242 243 enum name = unittestName!(eLtEstO, index); 244 enum hidden = hasUDA!(eLtEstO, HiddenTest); 245 enum shouldFail = hasUDA!(eLtEstO, ShouldFail) || hasUDA!(eLtEstO, ShouldFailWith); 246 enum singleThreaded = hasUDA!(eLtEstO, Serial); 247 enum builtin = true; 248 enum suffix = ""; 249 250 // let's check for @Values UDAs, which are actually of type ValuesImpl 251 enum isValues(alias T) = is(typeof(T)) && is(typeof(T):ValuesImpl!U, U); 252 alias valuesUDAs = Filter!(isValues, __traits(getAttributes, eLtEstO)); 253 254 enum isTags(alias T) = is(typeof(T)) && is(typeof(T) == Tags); 255 enum tags = tagsFromAttrs!(Filter!(isTags, __traits(getAttributes, eLtEstO))); 256 enum exceptionTypeInfo = getExceptionTypeInfo!eLtEstO; 257 enum flakyRetries = getFlakyRetries!(eLtEstO); 258 259 static if(valuesUDAs.length == 0) { 260 testData ~= TestData(name, 261 () { 262 auto setup = getUDAFunction!(composite, Setup); 263 auto shutdown = getUDAFunction!(composite, Shutdown); 264 265 if(setup) setup(); 266 scope(exit) if(shutdown) shutdown(); 267 268 eLtEstO(); 269 }, 270 hidden, 271 shouldFail, 272 singleThreaded, 273 builtin, 274 suffix, 275 tags, 276 exceptionTypeInfo, 277 flakyRetries); 278 } else { 279 import std.range; 280 281 // cartesianProduct doesn't work with only one range, so in the usual case 282 // of only one @Values UDA, we bind to prod with a range of tuples, just 283 // as returned by cartesianProduct. 284 285 static if(valuesUDAs.length == 1) { 286 import std.typecons; 287 enum prod = valuesUDAs[0].values.map!(a => tuple(a)); 288 } else { 289 mixin(`enum prod = cartesianProduct(` ~ valuesUDAs.length.iota.map! 290 (a => `valuesUDAs[` ~ guaranteedToString(a) ~ `].values`).join(", ") ~ `);`); 291 } 292 293 foreach(comb; aliasSeqOf!prod) { 294 enum valuesName = valuesName(comb); 295 296 static if(hasUDA!(eLtEstO, AutoTags)) 297 enum realTags = tags ~ valuesName.split(".").array; 298 else 299 enum realTags = tags; 300 301 testData ~= TestData(name ~ "." ~ valuesName, 302 () { 303 foreach(i; aliasSeqOf!(comb.length.iota)) 304 ValueHolder!(typeof(comb[i])).values[i] = comb[i]; 305 eLtEstO(); 306 }, 307 hidden, 308 shouldFail, 309 singleThreaded, 310 builtin, 311 suffix, 312 realTags, 313 exceptionTypeInfo, 314 flakyRetries); 315 } 316 } 317 } 318 } 319 } 320 321 322 // Keeps track of mangled names of everything visited. 323 bool[string] visitedMembers; 324 325 void addUnitTestsRecursively(alias composite)() pure nothrow { 326 327 if (composite.mangleof in visitedMembers) 328 return; 329 330 visitedMembers[composite.mangleof] = true; 331 addMemberUnittests!composite(); 332 333 foreach(member; __traits(allMembers, composite)) { 334 335 // isPrivate can't be used here. I don't know why. 336 static if(__traits(compiles, __traits(getProtection, __traits(getMember, module_, member)))) 337 enum notPrivate = __traits(getProtection, __traits(getMember, module_, member)) != "private"; 338 else 339 enum notPrivate = false; 340 341 static if ( 342 notPrivate && 343 // If visibility of the member is deprecated, the next line still returns true 344 // and yet spills deprecation warning. If deprecation is turned into error, 345 // all works as intended. 346 __traits(compiles, __traits(getMember, composite, member)) && 347 __traits(compiles, __traits(allMembers, __traits(getMember, composite, member))) && 348 __traits(compiles, recurse!(__traits(getMember, composite, member))) 349 ) { 350 recurse!(__traits(getMember, composite, member)); 351 } 352 } 353 } 354 355 void recurse(child)() pure nothrow { 356 static if (is(child == class) || is(child == struct) || is(child == union)) { 357 addUnitTestsRecursively!child; 358 } 359 } 360 361 addUnitTestsRecursively!module_(); 362 return testData; 363 } 364 365 private TypeInfo getExceptionTypeInfo(alias Test)() { 366 import unit_threaded.runner.attrs: ShouldFailWith; 367 import std.traits: hasUDA, getUDAs; 368 369 static if(hasUDA!(Test, ShouldFailWith)) { 370 alias uda = getUDAs!(Test, ShouldFailWith)[0]; 371 return typeid(uda.Type); 372 } else 373 return null; 374 } 375 376 377 private string valuesName(T)(T tuple) { 378 import std.range: iota; 379 import std.meta: aliasSeqOf; 380 import std.array: join; 381 382 string[] parts; 383 foreach(a; aliasSeqOf!(tuple.length.iota)) 384 parts ~= guaranteedToString(tuple[a]); 385 return parts.join("."); 386 } 387 388 private string guaranteedToString(T)(T value) nothrow pure @safe { 389 import std.conv; 390 try 391 return value.to!string; 392 catch(Exception ex) 393 assert(0, "Could not convert value to string"); 394 } 395 396 private string getValueAsString(T)(T value) nothrow pure @safe { 397 import std.conv; 398 try 399 return value.to!string; 400 catch(Exception ex) 401 assert(0, "Could not convert value to string"); 402 } 403 404 405 private template isStringUDA(alias T) { 406 import std.traits: isSomeString; 407 static if(__traits(compiles, isSomeString!(typeof(T)))) 408 enum isStringUDA = isSomeString!(typeof(T)); 409 else 410 enum isStringUDA = false; 411 } 412 413 @safe pure unittest { 414 static assert(isStringUDA!"foo"); 415 static assert(!isStringUDA!5); 416 } 417 418 private template isPrivate(alias module_, string moduleMember) { 419 alias ut_mmbr__ = Identity!(__traits(getMember, module_, moduleMember)); 420 421 static if(__traits(compiles, __traits(getProtection, ut_mmbr__))) 422 enum isPrivate = __traits(getProtection, ut_mmbr__) == "private"; 423 else 424 enum isPrivate = true; 425 } 426 427 428 // if this member is a test function or class, given the predicate 429 private template PassesTestPred(alias module_, alias pred, string moduleMember) { 430 431 static if(__traits(compiles, Identity!(__traits(getMember, module_, moduleMember)))) { 432 433 import unit_threaded.runner.attrs: DontTest; 434 import std.traits: hasUDA; 435 436 alias member = Identity!(__traits(getMember, module_, moduleMember)); 437 438 static if(__traits(compiles, hasUDA!(member, DontTest))) 439 enum hasDontTest = hasUDA!(member, DontTest); 440 else 441 enum hasDontTest = false; 442 443 enum PassesTestPred = 444 !isPrivate!(module_, moduleMember) && 445 pred!(module_, moduleMember) && 446 !hasDontTest; 447 448 } else 449 enum PassesTestPred = false; 450 } 451 452 453 /** 454 * Finds all test classes (classes implementing a test() function) 455 * in the given module 456 */ 457 TestData[] moduleTestClasses(modules...)() pure nothrow { 458 459 template isTestClass(alias module_, string moduleMember) { 460 import unit_threaded.runner.attrs: UnitTest; 461 import std.traits: isAggregateType, hasUDA; 462 463 alias member = Identity!(__traits(getMember, module_, moduleMember)); 464 465 static if(.isPrivate!(module_, moduleMember)) { 466 enum isTestClass = false; 467 } else static if(!__traits(compiles, isAggregateType!(member))) { 468 enum isTestClass = false; 469 } else static if(!isAggregateType!(member)) { 470 enum isTestClass = false; 471 } else static if(!__traits(compiles, { return new member; })) { 472 enum isTestClass = false; //can't new it, can't use it 473 } else { 474 enum hasUnitTest = hasUDA!(member, UnitTest); 475 enum hasTestMethod = __traits(hasMember, member, "test"); 476 477 enum isTestClass = is(member == class) && (hasTestMethod || hasUnitTest); 478 } 479 } 480 481 TestData[] ret; 482 483 static foreach(module_; modules) { 484 ret ~= moduleTestData!(module_, isTestClass, memberTestData); 485 } 486 487 return ret; 488 } 489 490 491 /** 492 * Finds all test functions in the given module. 493 * Returns an array of TestData structs 494 */ 495 TestData[] moduleTestFunctions(modules...)() { 496 497 template isTestFunction(alias module_, string moduleMember) { 498 import unit_threaded.runner.attrs: UnitTest, Types; 499 import std.meta: AliasSeq; 500 import std.traits: isSomeFunction, hasUDA; 501 502 alias member = Identity!(__traits(getMember, module_, moduleMember)); 503 504 static if(.isPrivate!(module_, moduleMember)) { 505 enum isTestFunction = false; 506 } else static if(AliasSeq!(member).length != 1) { 507 enum isTestFunction = false; 508 } else static if(isSomeFunction!member) { 509 enum isTestFunction = 510 hasTestPrefix!(module_, moduleMember) || 511 hasUDA!(member, UnitTest); 512 } else static if(__traits(compiles, __traits(getAttributes, member))) { 513 // in this case we handle the possibility of a template function with 514 // the @Types UDA attached to it 515 enum hasTestName = 516 hasTestPrefix!(module_, moduleMember) || 517 hasUDA!(member, UnitTest); 518 enum isTestFunction = hasTestName && hasUDA!(member, Types); 519 } else { 520 enum isTestFunction = false; 521 } 522 } 523 524 template hasTestPrefix(alias module_, string memberName) { 525 import std.uni: isUpper; 526 527 alias member = Identity!(__traits(getMember, module_, memberName)); 528 529 enum prefix = "test"; 530 enum minSize = prefix.length + 1; 531 532 static if(memberName.length >= minSize && 533 memberName[0 .. prefix.length] == prefix && 534 isUpper(memberName[prefix.length])) { 535 enum hasTestPrefix = true; 536 } else { 537 enum hasTestPrefix = false; 538 } 539 } 540 541 TestData[] ret; 542 543 static foreach(module_; modules) { 544 ret ~= moduleTestData!(module_, isTestFunction, createFuncTestData); 545 } 546 547 return ret; 548 } 549 550 551 /** 552 Get all the test functions for this module member. There might be more than one 553 when using parametrized unit tests. 554 555 Examples: 556 ------ 557 void testFoo() {} // -> the array contains one element, testFoo 558 @(1, 2, 3) void testBar(int) {} // The array contains 3 elements, one for each UDA value 559 @Types!(int, float) void testBaz(T)() {} //The array contains 2 elements, one for each type 560 ------ 561 */ 562 private TestData[] createFuncTestData(alias module_, string moduleMember)() { 563 import unit_threaded.runner.attrs; 564 import std.meta: aliasSeqOf, Alias; 565 import std.traits: hasUDA; 566 567 alias testFunction = Alias!(__traits(getMember, module_, moduleMember)); 568 569 enum isRegularFunction = __traits(compiles, &__traits(getMember, module_, moduleMember)); 570 571 static if(isRegularFunction) { 572 573 static if(arity!testFunction == 0) 574 return createRegularFuncTestData!(module_, moduleMember); 575 else 576 return createValueParamFuncTestData!(module_, moduleMember, testFunction); 577 578 } else static if(hasUDA!(testFunction, Types)) { // template function with @Types 579 return createTypeParamFuncTestData!(module_, moduleMember, testFunction); 580 } else { 581 return []; 582 } 583 } 584 585 private TestData[] createRegularFuncTestData(alias module_, string moduleMember)() { 586 import std.meta: Alias; 587 588 alias member = Alias!(__traits(getMember, module_, moduleMember)); 589 enum func = &member; 590 591 // the reason we're creating a lambda to call the function is that test functions 592 // are ordinary functions, but we're storing delegates 593 return [ memberTestData!member(() { func(); }) ]; //simple case, just call the function 594 } 595 596 // for value parameterised tests 597 private TestData[] createValueParamFuncTestData(alias module_, string moduleMember, alias testFunction)() { 598 599 import unit_threaded.runner.traits: GetAttributes; 600 import unit_threaded.runner.attrs: AutoTags; 601 import std.traits: Parameters; 602 import std.range: iota; 603 import std.algorithm: map; 604 import std.typecons: tuple; 605 import std.traits: arity, hasUDA; 606 import std.meta: aliasSeqOf, Alias; 607 608 alias params = Parameters!testFunction; 609 alias member = Alias!(__traits(getMember, module_, moduleMember)); 610 611 bool hasAttributesForAllParams() { 612 auto ret = true; 613 static foreach(P; params) { 614 static if(GetAttributes!(member, P).length == 0) ret = false; 615 } 616 return ret; 617 } 618 619 static if(!hasAttributesForAllParams) { 620 import std.conv: text; 621 pragma(msg, text("Warning: ", __traits(identifier, testFunction), 622 " passes the criteria for a value-parameterized test function", 623 " but doesn't have the appropriate value UDAs.\n", 624 " Consider changing its name or annotating it with @DontTest")); 625 return []; 626 } else { 627 628 static if(arity!testFunction == 1) { 629 // bind a range of tuples to prod just as cartesianProduct returns 630 enum prod = [GetAttributes!(member, params[0])].map!(a => tuple(a)); 631 } else { 632 import std.conv: text; 633 634 mixin(`enum prod = cartesianProduct(` ~ params.length.iota.map! 635 (a => `[GetAttributes!(member, params[` ~ guaranteedToString(a) ~ `])]`).join(", ") ~ `);`); 636 } 637 638 TestData[] testData; 639 foreach(comb; aliasSeqOf!prod) { 640 enum valuesName = valuesName(comb); 641 642 static if(hasUDA!(member, AutoTags)) 643 enum extraTags = valuesName.split(".").array; 644 else 645 enum string[] extraTags = []; 646 647 648 testData ~= memberTestData!member( 649 // testFunction(value0, value1, ...) 650 () { testFunction(comb.expand); }, 651 valuesName, 652 extraTags, 653 ); 654 } 655 656 return testData; 657 } 658 } 659 660 661 // template function with @Types 662 private TestData[] createTypeParamFuncTestData(alias module_, string moduleMember, alias testFunction) 663 () 664 { 665 import unit_threaded.attrs: Types, AutoTags; 666 import std.traits: getUDAs, hasUDA; 667 668 alias typesAttrs = getUDAs!(testFunction, Types); 669 static assert(typesAttrs.length > 0); 670 671 TestData[] testData; 672 673 // To get a cartesian product of all @Types on the function, we use a mixin 674 string nestedForEachMixin() { 675 import std.array: join, array; 676 import std.range: iota, retro; 677 import std.algorithm: map; 678 import std.conv: text; 679 import std.format: format; 680 681 string[] lines; 682 683 string indentation(size_t n) { 684 string ret; 685 foreach(i; 0 .. n) ret ~= " "; 686 return ret; 687 } 688 689 // e.g. 3 -> [type0, type1, type2] 690 string typeVars() { 691 return typesAttrs.length.iota.map!(i => text(`type`, i)).join(`, `); 692 } 693 694 // e.g. 3 -> [int, float, Foo] 695 string typeIds() { 696 return typesAttrs.length.iota.map!(i => text(`type`, i, `.stringof`)).join(` ~ "." ~ `); 697 } 698 699 // nested static foreachs, one per attribute 700 lines ~= typesAttrs 701 .length 702 .iota 703 .map!(i => indentation(i) ~ `static foreach(type%s; typesAttrs[%s].types) {`.format(i, i)) 704 .array 705 ; 706 707 lines ~= q{ 708 { 709 static if(hasUDA!(testFunction, AutoTags)) 710 enum extraTags = [type0.stringof]; // FIXME 711 else 712 enum string[] extraTags = []; 713 714 testData ~= memberTestData!testFunction( 715 () { testFunction!(%s)(); }, 716 %s, 717 extraTags 718 ); 719 } 720 }.format(typeVars, typeIds); 721 722 // close all static foreach braces 723 lines ~= typesAttrs 724 .length 725 .iota 726 .retro 727 .map!(i => indentation(i) ~ `}`) 728 .array 729 ; 730 731 return lines.join("\n"); 732 } 733 734 735 736 enum mixinStr = nestedForEachMixin; 737 //pragma(msg, "\n", mixinStr, "\n"); 738 mixin(mixinStr); 739 740 return testData; 741 } 742 743 744 // this funtion returns TestData for either classes or test functions 745 // built-in unittest modules are handled by moduleUnitTests 746 // pred determines what qualifies as a test 747 // createTestData must return TestData[] 748 private TestData[] moduleTestData(alias module_, alias pred, alias createTestData)() pure { 749 TestData[] testData; 750 751 foreach(moduleMember; __traits(allMembers, module_)) { 752 753 static if(PassesTestPred!(module_, pred, moduleMember)) 754 testData ~= createTestData!(module_, moduleMember); 755 } 756 757 return testData; 758 759 } 760 761 // Deprecated: here for backwards compatibility 762 // TestData for a member of a module (either a test function or a test class) 763 private TestData memberTestData 764 (alias module_, string moduleMember, string[] extraTags = []) 765 (TestFunction testFunction = null, string suffix = "") 766 { 767 import std.meta: Alias; 768 alias member = Alias!(__traits(getMember, module_, moduleMember)); 769 return memberTestData!member(testFunction, suffix, extraTags); 770 } 771 772 773 // TestData for a member of a module (either a test function or a test class) 774 private TestData memberTestData(alias member) 775 (TestFunction testFunction, string suffix = "", string[] extraTags = []) 776 { 777 import unit_threaded.runner.attrs; 778 import std.traits: hasUDA, getUDAs; 779 import std.meta: Alias; 780 781 enum singleThreaded = hasUDA!(member, Serial); 782 enum builtin = false; 783 enum tags = tagsFromAttrs!(getUDAs!(member, Tags)); 784 enum exceptionTypeInfo = getExceptionTypeInfo!member; 785 enum shouldFail = hasUDA!(member, ShouldFail) || hasUDA!(member, ShouldFailWith); 786 enum flakyRetries = getFlakyRetries!member; 787 // change names if explicitly asked to with a @Name UDA 788 enum nameFromAttr = TestNameFromAttr!member; 789 790 static if(nameFromAttr == "") 791 enum name = __traits(identifier, member); 792 else 793 enum name = nameFromAttr; 794 795 alias module_ = Alias!(__traits(parent, member)); 796 797 return TestData(fullyQualifiedName!module_~ "." ~ name, 798 testFunction, 799 hasUDA!(member, HiddenTest), 800 shouldFail, 801 singleThreaded, 802 builtin, 803 suffix, 804 tags ~ extraTags, 805 exceptionTypeInfo, 806 flakyRetries); 807 } 808 809 private int getFlakyRetries(alias test)() { 810 import unit_threaded.runner.attrs: Flaky; 811 import std.traits: getUDAs; 812 import std.conv: text; 813 814 alias flakies = getUDAs!(test, Flaky); 815 816 static assert(flakies.length == 0 || flakies.length == 1, 817 text("Only 1 @Flaky allowed, found ", flakies.length, " on ", 818 __traits(identifier, test))); 819 820 static if(flakies.length == 1) { 821 static if(is(flakies[0])) 822 return Flaky.defaultRetries; 823 else 824 return flakies[0].retries; 825 } else 826 return 0; 827 } 828 829 string[] tagsFromAttrs(T...)() { 830 static assert(T.length <= 1, "@Tags can only be applied once"); 831 static if(T.length) 832 return T[0].values; 833 else 834 return []; 835 }