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