1 module unit_threaded.reflection; 2 3 import unit_threaded.attrs; 4 import unit_threaded.uda; 5 import unit_threaded.meta; 6 import std.traits; 7 import std.typetuple; 8 9 /** 10 * Common data for test functions and test classes 11 */ 12 alias void delegate() TestFunction; 13 struct TestData { 14 string name; 15 TestFunction testFunction; ///only used for functions, null for classes 16 bool hidden; 17 bool shouldFail; 18 bool singleThreaded; 19 bool builtin; 20 string suffix; // append to end of getPath 21 string[] tags; 22 23 string getPath() const pure nothrow { 24 string path = name.dup; 25 import std.array: empty; 26 if(!suffix.empty) path ~= "." ~ suffix; 27 return path; 28 } 29 30 bool isTestClass() @safe const pure nothrow { 31 return testFunction is null; 32 } 33 } 34 35 36 /** 37 * Finds all test cases (functions, classes, built-in unittest blocks) 38 * Template parameters are module strings 39 */ 40 const(TestData)[] allTestData(MOD_STRINGS...)() if(allSatisfy!(isSomeString, typeof(MOD_STRINGS))) { 41 42 string getModulesString() { 43 import std.array: join; 44 string[] modules; 45 foreach(module_; MOD_STRINGS) modules ~= module_; 46 return modules.join(", "); 47 } 48 49 enum modulesString = getModulesString; 50 mixin("import " ~ modulesString ~ ";"); 51 mixin("return allTestData!(" ~ modulesString ~ ");"); 52 } 53 54 55 /** 56 * Finds all test cases (functions, classes, built-in unittest blocks) 57 * Template parameters are module symbols 58 */ 59 const(TestData)[] allTestData(MOD_SYMBOLS...)() if(!anySatisfy!(isSomeString, typeof(MOD_SYMBOLS))) { 60 auto allTestsWithFunc(string expr, MOD_SYMBOLS...)() pure { 61 //tests is whatever type expr returns 62 ReturnType!(mixin(expr ~ q{!(MOD_SYMBOLS[0])})) tests; 63 foreach(module_; TypeTuple!MOD_SYMBOLS) { 64 tests ~= mixin(expr ~ q{!module_()}); //e.g. tests ~= moduleTestClasses!module_ 65 } 66 return tests; 67 } 68 69 return allTestsWithFunc!(q{moduleTestClasses}, MOD_SYMBOLS) ~ 70 allTestsWithFunc!(q{moduleTestFunctions}, MOD_SYMBOLS) ~ 71 allTestsWithFunc!(q{moduleUnitTests}, MOD_SYMBOLS); 72 } 73 74 75 /** 76 * Finds all built-in unittest blocks in the given module. 77 * @return An array of TestData structs 78 */ 79 TestData[] moduleUnitTests(alias module_)() pure nothrow { 80 81 // Return a name for a unittest block. If no @Name UDA is found a name is 82 // created automatically, else the UDA is used. 83 string unittestName(alias test, int index)() @safe nothrow { 84 import std.conv; 85 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 86 87 enum nameAttrs = getUDAs!(test, Name); 88 static assert(nameAttrs.length == 0 || nameAttrs.length == 1, "Found multiple Name UDAs on unittest"); 89 90 enum strAttrs = Filter!(isStringUDA, __traits(getAttributes, test)); 91 enum hasName = nameAttrs.length || strAttrs.length == 1; 92 enum prefix = fullyQualifiedName!module_ ~ "."; 93 94 static if(hasName) { 95 static if(nameAttrs.length == 1) 96 return prefix ~ nameAttrs[0].value; 97 else 98 return prefix ~ strAttrs[0]; 99 } else { 100 string name; 101 try { 102 return prefix ~ "unittest" ~ (index).to!string; 103 } catch(Exception) { 104 assert(false, text("Error converting ", index, " to string")); 105 } 106 } 107 } 108 109 TestData[] testData; 110 foreach(index, test; __traits(getUnitTests, module_)) { 111 enum name = unittestName!(test, index); 112 enum hidden = hasUDA!(test, HiddenTest); 113 enum shouldFail = hasUDA!(test, ShouldFail); 114 enum singleThreaded = hasUDA!(test, Serial); 115 enum builtin = true; 116 enum suffix = ""; 117 118 // let's check for @Values UDAs, which are actually of type ValuesImpl 119 enum isValues(alias T) = is(typeof(T)) && is(typeof(T):ValuesImpl!U, U); 120 enum valuesUDAs = Filter!(isValues, __traits(getAttributes, test)); 121 122 enum isTags(alias T) = is(typeof(T)) && is(typeof(T) == Tags); 123 enum tags = tagsFromAttrs!(Filter!(isTags, __traits(getAttributes, test))); 124 125 static if(valuesUDAs.length == 0) { 126 testData ~= TestData(name, (){ test(); }, hidden, shouldFail, singleThreaded, builtin, suffix, tags); 127 } else { 128 static assert(valuesUDAs.length == 1, "Can only use @Values once"); 129 130 foreach(value; aliasSeqOf!(valuesUDAs[0].values)) { 131 // force single threaded so a composite test case is created 132 // we set a global static to the value the test expects then call the test function, 133 // which can retrieve the value with getValue!T 134 135 enum valueAsString = getValueAsString(value); 136 137 static if(hasUDA!(test, AutoTags)) 138 enum realTags = tags ~ valueAsString; 139 else 140 enum realTags = tags; 141 142 testData ~= TestData(name ~ "." ~ valueAsString, 143 () { 144 ValueHolder!(typeof(value)).value = value; 145 test(); 146 }, 147 hidden, shouldFail, true /*serial*/, builtin, suffix, realTags); 148 } 149 } 150 } 151 return testData; 152 } 153 154 private string getValueAsString(T)(T value) nothrow pure @safe { 155 import std.conv; 156 try 157 return value.to!string; 158 catch(Exception ex) 159 assert(0, "Could not convert value to string"); 160 } 161 162 163 private template isStringUDA(alias T) { 164 static if(__traits(compiles, isSomeString!(typeof(T)))) 165 enum isStringUDA = isSomeString!(typeof(T)); 166 else 167 enum isStringUDA = false; 168 } 169 170 unittest { 171 static assert(isStringUDA!"foo"); 172 static assert(!isStringUDA!5); 173 } 174 175 private template isPrivate(alias module_, string moduleMember) { 176 mixin(`import ` ~ fullyQualifiedName!module_ ~ `: ` ~ moduleMember ~ `;`); 177 static if(__traits(compiles, isSomeFunction!(mixin(moduleMember)))) { 178 enum isPrivate = false; 179 } else { 180 enum isPrivate = true; 181 } 182 } 183 184 185 // if this member is a test function or class, given the predicate 186 private template PassesTestPred(alias module_, alias pred, string moduleMember) { 187 //should be the line below instead but a compiler bug prevents it 188 //mixin(importMember!module_(moduleMember)); 189 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); 190 enum notPrivate = __traits(compiles, mixin(moduleMember)); //only way I know to check if private 191 //enum notPrivate = !isPrivate!(module_, moduleMember); 192 static if(notPrivate) 193 enum PassesTestPred = notPrivate && pred!(module_, moduleMember) && 194 !HasAttribute!(module_, moduleMember, DontTest); 195 else 196 enum PassesTestPred = false; 197 } 198 199 200 /** 201 * Finds all test classes (classes implementing a test() function) 202 * in the given module 203 */ 204 TestData[] moduleTestClasses(alias module_)() pure nothrow { 205 206 template isTestClass(alias module_, string moduleMember) { 207 mixin(importMember!module_(moduleMember)); 208 static if(isPrivate!(module_, moduleMember)) { 209 enum isTestClass = false; 210 } else static if(!__traits(compiles, isAggregateType!(mixin(moduleMember)))) { 211 enum isTestClass = false; 212 } else static if(!isAggregateType!(mixin(moduleMember))) { 213 enum isTestClass = false; 214 } else static if(!__traits(compiles, mixin("new " ~ moduleMember))) { 215 enum isTestClass = false; //can't new it, can't use it 216 } else { 217 enum hasUnitTest = HasAttribute!(module_, moduleMember, UnitTest); 218 enum hasTestMethod = __traits(hasMember, mixin(moduleMember), "test"); 219 enum isTestClass = hasTestMethod || hasUnitTest; 220 } 221 } 222 223 224 return moduleTestData!(module_, isTestClass, memberTestData); 225 } 226 227 228 /** 229 * Finds all test functions in the given module. 230 * Returns an array of TestData structs 231 */ 232 TestData[] moduleTestFunctions(alias module_)() pure { 233 234 enum isTypesAttr(alias T) = is(T) && is(T:Types!U, U...); 235 236 template isTestFunction(alias module_, string moduleMember) { 237 mixin(importMember!module_(moduleMember)); 238 239 static if(isPrivate!(module_, moduleMember)) { 240 enum isTestFunction = false; 241 } else static if(AliasSeq!(mixin(moduleMember)).length != 1) { 242 enum isTestFunction = false; 243 } else static if(isSomeFunction!(mixin(moduleMember))) { 244 enum isTestFunction = hasTestPrefix!(module_, moduleMember) || 245 HasAttribute!(module_, moduleMember, UnitTest); 246 } else { 247 // in this case we handle the possibility of a template function with 248 // the @Types UDA attached to it 249 alias types = GetTypes!(mixin(moduleMember)); 250 enum isTestFunction = hasTestPrefix!(module_, moduleMember) && 251 types.length > 0 && 252 is(typeof(() { 253 mixin(moduleMember ~ `!` ~ types[0].stringof ~ `;`); 254 })); 255 } 256 } 257 258 template hasTestPrefix(alias module_, string member) { 259 import std.uni: isUpper; 260 mixin(importMember!module_(member)); 261 262 enum prefix = "test"; 263 enum minSize = prefix.length + 1; 264 265 static if(member.length >= minSize && member[0 .. prefix.length] == prefix && 266 isUpper(member[prefix.length])) { 267 enum hasTestPrefix = true; 268 } else { 269 enum hasTestPrefix = false; 270 } 271 } 272 273 274 return moduleTestData!(module_, isTestFunction, createFuncTestData); 275 } 276 277 private TestData[] createFuncTestData(alias module_, string moduleMember)() { 278 mixin(importMember!module_(moduleMember)); 279 /* 280 Get all the test functions for this module member. There might be more than one 281 when using parametrized unit tests. 282 283 Examples: 284 ------ 285 void testFoo() {} // -> the array contains one element, testFoo 286 @(1, 2, 3) void testBar(int) {} // The array contains 3 elements, one for each UDA value 287 @Types!(int, float) void testBaz(T)() {} //The array contains 2 elements, one for each type 288 ------ 289 */ 290 // if the predicate returned true (which is always the case here), then it's either 291 // a regular function or a templated one. If regular is has a pointer to it 292 enum isRegularFunction = __traits(compiles, &__traits(getMember, module_, moduleMember)); 293 294 static if(isRegularFunction) { 295 296 enum func = &__traits(getMember, module_, moduleMember); 297 enum arity = arity!func; 298 299 static assert(arity == 0 || arity == 1, "Test functions may take at most one parameter"); 300 301 static if(arity == 0) 302 // the reason we're creating a lambda to call the function is that test functions 303 // are ordinary functions, but we're storing delegates 304 return [ memberTestData!(module_, moduleMember)(() { func(); }) ]; //simple case, just call the function 305 else { 306 307 // the function takes a parameter, check if it has UDAs for value parameters to be passed to it 308 alias params = Parameters!func; 309 static assert(params.length == 1, "Test functions may take at most one parameter"); 310 311 alias values = GetAttributes!(module_, moduleMember, params[0]); 312 313 import std.conv; 314 static assert(values.length > 0, 315 text("Test functions with a parameter of type <", params[0].stringof, 316 "> must have value UDAs of the same type")); 317 318 TestData[] testData; 319 foreach(v; values) { 320 static if(HasAttribute!(module_, moduleMember, AutoTags)) 321 enum extraTags = [getValueAsString(v)]; 322 else 323 enum string[] extraTags = []; 324 testData ~= memberTestData!(module_, moduleMember, extraTags)( 325 () { func(v); }, 326 v.to!string 327 ); 328 } 329 330 return testData; 331 } 332 } else static if(HasTypes!(mixin(moduleMember))) { //template function with @Types 333 alias types = GetTypes!(mixin(moduleMember)); 334 TestData[] testData; 335 foreach(type; types) { 336 static if(HasAttribute!(module_, moduleMember, AutoTags)) 337 enum extraTags = [type.stringof]; 338 else 339 enum string[] extraTags = []; 340 341 testData ~= memberTestData!(module_, moduleMember, extraTags)( 342 () { 343 mixin(moduleMember ~ `!(` ~ type.stringof ~ `)();`); 344 }, 345 type.stringof); 346 } 347 return testData; 348 } else { 349 return []; 350 } 351 } 352 353 354 355 // this funtion returns TestData for either classes or test functions 356 // built-in unittest modules are handled by moduleUnitTests 357 // pred determines what qualifies as a test 358 // createTestData must return TestData[] 359 private TestData[] moduleTestData(alias module_, alias pred, alias createTestData)() pure { 360 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 361 TestData[] testData; 362 foreach(moduleMember; __traits(allMembers, module_)) { 363 364 static if(PassesTestPred!(module_, pred, moduleMember)) 365 testData ~= createTestData!(module_, moduleMember); 366 } 367 368 return testData; 369 370 } 371 372 // TestData for a member of a module (either a test function or test class) 373 private TestData memberTestData(alias module_, string moduleMember, string[] extraTags = []) 374 (TestFunction testFunction = null, string suffix = "") { 375 //if there is a suffix, all tests sharing that suffix are single threaded with multiple values per "real" test 376 //this is slightly hackish but works and actually makes sense - it causes unit_threaded.factory to make 377 //a CompositeTestCase out of them 378 immutable singleThreaded = HasAttribute!(module_, moduleMember, Serial) || suffix != ""; 379 enum builtin = false; 380 enum tags = tagsFromAttrs!(GetAttributes!(module_, moduleMember, Tags)); 381 382 return TestData(fullyQualifiedName!module_~ "." ~ moduleMember, 383 testFunction, 384 HasAttribute!(module_, moduleMember, HiddenTest), 385 HasAttribute!(module_, moduleMember, ShouldFail), 386 singleThreaded, 387 builtin, 388 suffix, 389 tags ~ extraTags); 390 } 391 392 string[] tagsFromAttrs(T...)() { 393 static assert(T.length <= 1, "@Tags can only be applied once"); 394 static if(T.length) 395 return T[0].values; 396 else 397 return []; 398 } 399 400 version(unittest) { 401 402 import unit_threaded.tests.module_with_tests; //defines tests and non-tests 403 import unit_threaded.asserts; 404 import std.algorithm; 405 import std.array; 406 407 //helper function for the unittest blocks below 408 private auto addModPrefix(string[] elements, 409 string module_ = "unit_threaded.tests.module_with_tests") nothrow { 410 return elements.map!(a => module_ ~ "." ~ a).array; 411 } 412 } 413 414 unittest { 415 const expected = addModPrefix([ "FooTest", "BarTest", "Blergh"]); 416 const actual = moduleTestClasses!(unit_threaded.tests.module_with_tests). 417 map!(a => a.name).array; 418 assertEqual(actual, expected); 419 } 420 421 unittest { 422 const expected = addModPrefix([ "testFoo", "testBar", "funcThatShouldShowUpCosOfAttr"]); 423 const actual = moduleTestFunctions!(unit_threaded.tests.module_with_tests). 424 map!(a => a.getPath).array; 425 assertEqual(actual, expected); 426 } 427 428 429 unittest { 430 const expected = addModPrefix(["unittest0", "unittest1", "myUnitTest"]); 431 const actual = moduleUnitTests!(unit_threaded.tests.module_with_tests). 432 map!(a => a.name).array; 433 assertEqual(actual, expected); 434 } 435 436 version(unittest) { 437 import unit_threaded.testcase: TestCase; 438 private void assertFail(TestCase test, string file = __FILE__, ulong line = __LINE__) { 439 import core.exception; 440 import std.conv; 441 442 try { 443 test.silence; 444 assert(test() != [], file ~ ":" ~ line.to!string ~ " Test was expected to fail but didn't"); 445 assert(false, file ~ ":" ~ line.to!string ~ " Expected test case " ~ test.getPath ~ 446 " to fail with AssertError but it didn't"); 447 } catch(AssertError) {} 448 } 449 450 private void assertPass(TestCase test, string file = __FILE__, ulong line = __LINE__) { 451 assertEqual(test(), [], file, line); 452 } 453 } 454 455 @("Test that parametrized value tests work") 456 unittest { 457 import unit_threaded.factory; 458 import unit_threaded.testcase; 459 460 const testData = allTestData!(unit_threaded.tests.parametrized). 461 filter!(a => a.name.endsWith("testValues")).array; 462 463 // there should only be on test case which is a composite of the 3 values in testValues 464 auto composite = cast(CompositeTestCase)createTestCases(testData)[0]; 465 assert(composite !is null, "Wrong dynamic type for TestCase"); 466 auto tests = composite.tests; 467 assertEqual(tests.length, 3); 468 469 // the first and third test should pass, the second should fail 470 assertPass(tests[0]); 471 assertPass(tests[2]); 472 473 assertFail(tests[1]); 474 } 475 476 477 @("Test that parametrized type tests work") 478 unittest { 479 import unit_threaded.factory; 480 import unit_threaded.testcase; 481 482 const testData = allTestData!(unit_threaded.tests.parametrized). 483 filter!(a => a.name.endsWith("testTypes")).array; 484 const expected = addModPrefix(["testTypes.float", "testTypes.int"], 485 "unit_threaded.tests.parametrized"); 486 const actual = testData.map!(a => a.getPath).array; 487 assertEqual(actual, expected); 488 489 // there should only be on test case which is a composite of the 2 testTypes 490 auto composite = cast(CompositeTestCase)createTestCases(testData)[0]; 491 assert(composite !is null, "Wrong dynamic type for TestCase"); 492 auto tests = composite.tests; 493 assertEqual(tests.map!(a => a.getPath).array, expected); 494 495 assertPass(tests[1]); 496 assertFail(tests[0]); 497 } 498 499 @("Test that value parametrized built-in unittest blocks work") 500 unittest { 501 import unit_threaded.factory; 502 import unit_threaded.testcase; 503 504 const testData = allTestData!(unit_threaded.tests.parametrized). 505 filter!(a => a.name.canFind("builtinIntValues")).array; 506 507 // there should only be on test case which is a composite of the 4 values 508 auto composite = cast(CompositeTestCase)createTestCases(testData)[0]; 509 assert(composite !is null, "Wrong dynamic type for TestCase"); 510 auto tests = composite.tests; 511 assertEqual(tests.length, 4); 512 513 // these should be ok 514 assertPass(tests[1]); 515 516 //these should fail 517 assertFail(tests[0]); 518 assertFail(tests[2]); 519 assertFail(tests[3]); 520 } 521 522 523 @("Tests can be selected by tags") unittest { 524 import unit_threaded.factory; 525 import unit_threaded.testcase; 526 527 const testData = allTestData!(unit_threaded.tests.tags).array; 528 auto testsNoTags = createTestCases(testData); 529 assertEqual(testsNoTags.length, 4); 530 assertPass(testsNoTags[0]); 531 assertFail(testsNoTags[1]); 532 assertFail(testsNoTags[2]); 533 assertFail(testsNoTags[3]); 534 535 auto testsNinja = createTestCases(testData, ["@ninja"]); 536 assertEqual(testsNinja.length, 1); 537 assertPass(testsNinja[0]); 538 539 auto testsMake = createTestCases(testData, ["@make"]); 540 assertEqual(testsMake.length, 3); 541 assertPass(testsMake.find!(a => a.getPath.canFind("testMake")).front); 542 assertPass(testsMake.find!(a => a.getPath.canFind("unittest0")).front); 543 assertFail(testsMake.find!(a => a.getPath.canFind("unittest2")).front); 544 545 auto testsNotNinja = createTestCases(testData, ["~@ninja"]); 546 assertEqual(testsNotNinja.length, 3); 547 assertPass(testsNotNinja.find!(a => a.getPath.canFind("testMake")).front); 548 assertFail(testsNotNinja.find!(a => a.getPath.canFind("unittest1")).front); 549 assertFail(testsNotNinja.find!(a => a.getPath.canFind("unittest2")).front); 550 551 assertEqual(createTestCases(testData, ["unit_threaded.tests.tags.testMake", "@ninja"]).length, 0); 552 } 553 554 @("Parametrized built-in tests with @AutoTags get tagged by value") 555 unittest { 556 import unit_threaded.factory; 557 import unit_threaded.testcase; 558 559 const testData = allTestData!(unit_threaded.tests.parametrized). 560 filter!(a => a.name.canFind("builtinIntValues")).array; 561 562 auto compositeTwo = cast(CompositeTestCase)createTestCases(testData, ["@2"])[0]; 563 assert(compositeTwo !is null, "Wrong dynamic type for TestCase"); 564 auto two = compositeTwo.tests; 565 566 assertEqual(two.length, 1); 567 assertFail(two[0]); 568 569 auto compositeThree = cast(CompositeTestCase)createTestCases(testData, ["@3"])[0]; 570 assert(compositeThree !is null, "Wrong dynamic type for TestCase"); 571 auto three = compositeThree.tests; 572 assertEqual(three.length, 1); 573 assertPass(three[0]); 574 } 575 576 @("Value parametrized function tests with @AutoTags get tagged by value") 577 unittest { 578 import unit_threaded.factory; 579 import unit_threaded.testcase; 580 581 const testData = allTestData!(unit_threaded.tests.parametrized). 582 filter!(a => a.name.canFind("testValues")).array; 583 584 auto compositeTwo = cast(CompositeTestCase)createTestCases(testData, ["@2"])[0]; 585 assert(compositeTwo !is null, "Wrong dynamic type for TestCase"); 586 auto two = compositeTwo.tests; 587 assertEqual(two.length, 1); 588 assertFail(two[0]); 589 } 590 591 @("Type parameterized tests with @AutoTags get tagged by type") 592 unittest { 593 import unit_threaded.factory; 594 import unit_threaded.testcase; 595 596 const testData = allTestData!(unit_threaded.tests.parametrized). 597 filter!(a => a.name.canFind("testTypes")).array; 598 599 auto composite = cast(CompositeTestCase)createTestCases(testData, ["@int"])[0]; 600 assert(composite !is null, "Wrong dynamic type for TestCase"); 601 auto tests = composite.tests; 602 assertEqual(tests.length, 1); 603 assertPass(tests[0]); 604 }