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