1 /* 2 * Silly is a test runner for the D programming language 3 * 4 * Report bugs and propose new features in project's repository: https://gitlab.com/AntonMeep/silly 5 */ 6 7 /* SPDX-License-Identifier: ISC */ 8 /* Copyright (c) 2018-2019, Anton Fediushin */ 9 10 module silly; 11 12 version(unittest): 13 14 static if(!__traits(compiles, () {static import dub_test_root;})) { 15 static assert(false, "Couldn't find 'dub_test_root'. Make sure you are running tests with `dub test`"); 16 } else { 17 static import dub_test_root; 18 } 19 20 import core.time : Duration, MonoTime; 21 import std.ascii : newline; 22 import std.stdio : stdout; 23 24 shared static this() { 25 import core.runtime : Runtime, UnitTestResult; 26 import std.getopt : getopt; 27 import std.parallelism : TaskPool, totalCPUs; 28 29 Runtime.extendedModuleUnitTester = function () { 30 bool verbose; 31 bool failFast; 32 shared ulong passed, failed; 33 uint threads; 34 string include, exclude; 35 36 auto args = Runtime.args; 37 auto getoptResult = args.getopt( 38 "no-colours", 39 "Disable colours", 40 &noColours, 41 "t|threads", 42 "Number of worker threads. 0 to auto-detect (default)", 43 &threads, 44 "i|include", 45 "Run tests if their name matches specified regular expression", 46 &include, 47 "e|exclude", 48 "Skip tests if their name matches specified regular expression", 49 &exclude, 50 "fail-fast", 51 "Stop executing all tests when a test fails", 52 &failFast, 53 "v|verbose", 54 "Show verbose output (full stack traces, location and durations)", 55 &verbose, 56 ); 57 58 if(getoptResult.helpWanted) { 59 import std.string : leftJustifier; 60 61 stdout.writefln("Usage:%1$s\tdub test -- <options>%1$s%1$sOptions:", newline); 62 63 foreach(option; getoptResult.options) 64 stdout.writefln(" %s\t%s\t%s", option.optShort, option.optLong.leftJustifier(20), option.help); 65 66 return UnitTestResult(0, 0, false, false); 67 } 68 69 if(!threads) 70 threads = totalCPUs; 71 72 Console.init; 73 74 75 auto started = MonoTime.currTime; 76 77 with(new TaskPool(threads-1)) { 78 import core.atomic : atomicOp; 79 import std.regex : matchFirst; 80 81 try { 82 foreach(test; parallel(getTests)) { 83 if((!include && !exclude) || 84 (include && !(test.fullName ~ " " ~ test.testName).matchFirst(include).empty) || 85 (exclude && (test.fullName ~ " " ~ test.testName).matchFirst(exclude).empty)) { 86 87 TestResult result; 88 scope(exit) { 89 result.writeResult(verbose); 90 atomicOp!"+="(result.succeed ? passed : failed, 1UL); 91 } 92 test.executeTest(result, failFast); 93 } 94 } 95 finish(true); 96 } catch(Throwable t) { 97 stop(); 98 } 99 100 } 101 102 stdout.writeln; 103 stdout.writefln("%s: %s passed, %s failed in %d ms", 104 Console.emphasis("Summary"), 105 Console.colour(passed, Colour.ok), 106 Console.colour(failed, failed ? Colour.achtung : Colour.none), 107 (MonoTime.currTime - started).total!"msecs", 108 ); 109 110 return UnitTestResult(passed + failed, passed, false, false); 111 }; 112 } 113 114 void writeResult(TestResult result, in bool verbose) { 115 import std.format : formattedWrite; 116 import std.algorithm : canFind; 117 import std.range : drop; 118 import std.string : lastIndexOf, lineSplitter; 119 120 auto writer = stdout.lockingTextWriter; 121 122 writer.formattedWrite(" %s %s %s", 123 result.succeed 124 ? Console.colour("✓", Colour.ok) 125 : Console.colour("✗", Colour.achtung), 126 Console.emphasis(result.test.fullName[0..result.test.fullName.lastIndexOf('.')].truncateName(verbose)), 127 result.test.testName, 128 ); 129 130 if(verbose) { 131 writer.formattedWrite(" (%.3f ms)", (cast(real) result.duration.total!"usecs") / 10.0f ^^ 3); 132 133 if(result.test.location != TestLocation.init) { 134 writer.formattedWrite(" [%s:%d:%d]", 135 result.test.location.file, 136 result.test.location.line, 137 result.test.location.column); 138 } 139 } 140 141 writer.put(newline); 142 143 foreach(th; result.thrown) { 144 writer.formattedWrite(" %s thrown from %s on line %d: %s%s", 145 th.type, 146 th.file, 147 th.line, 148 th.message.lineSplitter.front, 149 newline, 150 ); 151 foreach(line; th.message.lineSplitter.drop(1)) 152 writer.formattedWrite(" %s%s", line, newline); 153 154 writer.formattedWrite(" --- Stack trace ---%s", newline); 155 if(verbose) { 156 foreach(line; th.info) 157 writer.formattedWrite(" %s%s", line, newline); 158 } else { 159 for(size_t i = 0; i < th.info.length && !th.info[i].canFind(__FILE__); ++i) 160 writer.formattedWrite(" %s%s", th.info[i], newline); 161 } 162 } 163 } 164 165 void executeTest(Test test, out TestResult result, bool failFast) { 166 import core.exception : AssertError, OutOfMemoryError; 167 result.test = test; 168 const started = MonoTime.currTime; 169 170 try { 171 scope(exit) result.duration = MonoTime.currTime - started; 172 test.ptr(); 173 result.succeed = true; 174 175 } catch(Throwable t) { 176 foreach(th; t) { 177 immutable(string)[] trace; 178 try { 179 foreach(i; th.info) 180 trace ~= i.idup; 181 } catch(OutOfMemoryError) { // TODO: Actually fix a bug instead of this workaround 182 trace ~= "<silly error> Failed to get stack trace, see https://gitlab.com/AntonMeep/silly/issues/31"; 183 } 184 185 result.thrown ~= Thrown(typeid(th).name, th.message.idup, th.file, th.line, trace); 186 } 187 if (failFast && (!(cast(Exception) t || cast(AssertError) t))) { 188 throw t; 189 } 190 } 191 } 192 193 struct TestLocation { 194 string file; 195 size_t line, column; 196 } 197 198 struct Test { 199 string fullName, 200 testName; 201 202 TestLocation location; 203 204 void function() ptr; 205 } 206 207 struct TestResult { 208 Test test; 209 bool succeed; 210 Duration duration; 211 212 immutable(Thrown)[] thrown; 213 } 214 215 struct Thrown { 216 string type, 217 message, 218 file; 219 size_t line; 220 immutable(string)[] info; 221 } 222 223 __gshared bool noColours; 224 225 enum Colour { 226 none, 227 ok = 32, 228 achtung = 31, 229 } 230 231 static struct Console { 232 static void init() { 233 if(noColours) { 234 return; 235 } else { 236 version(Posix) { 237 import core.sys.posix.unistd; 238 noColours = isatty(STDOUT_FILENO) == 0; 239 } else version(Windows) { 240 import core.sys.windows.winbase : GetStdHandle, STD_OUTPUT_HANDLE, INVALID_HANDLE_VALUE; 241 import core.sys.windows.wincon : SetConsoleOutputCP, GetConsoleMode, SetConsoleMode; 242 import core.sys.windows.windef : DWORD; 243 import core.sys.windows.winnls : CP_UTF8; 244 245 SetConsoleOutputCP(CP_UTF8); 246 247 auto hOut = GetStdHandle(STD_OUTPUT_HANDLE); 248 DWORD originalMode; 249 250 // TODO: 4 stands for ENABLE_VIRTUAL_TERMINAL_PROCESSING which should be 251 // in druntime v2.082.0 252 noColours = hOut == INVALID_HANDLE_VALUE || 253 !GetConsoleMode(hOut, &originalMode) || 254 !SetConsoleMode(hOut, originalMode | 4); 255 } 256 } 257 } 258 259 static string colour(T)(T t, Colour c = Colour.none) { 260 import std.conv : text; 261 262 return noColours ? text(t) : text("\033[", cast(int) c, "m", t, "\033[m"); 263 } 264 265 static string emphasis(string s) { 266 return noColours ? s : "\033[1m" ~ s ~ "\033[m"; 267 } 268 } 269 270 string getTestName(alias test)() { 271 string name = __traits(identifier, test); 272 273 foreach(attribute; __traits(getAttributes, test)) { 274 static if(is(typeof(attribute) : string)) { 275 name = attribute; 276 break; 277 } 278 } 279 280 return name; 281 } 282 283 string truncateName(string s, bool verbose = false) { 284 import std.algorithm : max; 285 import std.string : indexOf; 286 return s.length > 30 && !verbose 287 ? s[max(s.indexOf('.', s.length - 30), s.length - 30) .. $] 288 : s; 289 } 290 291 TestLocation getTestLocation(alias test)() { 292 // test if compiler is new enough for getLocation (since 2.088.0) 293 static if(is(typeof(__traits(getLocation, test)))) 294 return TestLocation(__traits(getLocation, test)); 295 else 296 return TestLocation.init; 297 } 298 299 Test[] getTests(){ 300 Test[] tests; 301 302 foreach(m; dub_test_root.allModules) { 303 import std.meta : Alias; 304 import std.traits : fullyQualifiedName; 305 static if(__traits(isModule, m)) { 306 alias module_ = m; 307 } else { 308 // For cases when module contains member of the same name 309 alias module_ = Alias!(__traits(parent, m)); 310 } 311 312 // Unittests in the module 313 foreach(test; __traits(getUnitTests, module_)) { 314 tests ~= Test(fullyQualifiedName!test, getTestName!test, getTestLocation!test, &test); 315 } 316 317 // Unittests in structs and classes 318 foreach(member; __traits(derivedMembers, module_)) { 319 static if(__traits(compiles, __traits(getMember, module_, member)) && 320 __traits(compiles, __traits(isTemplate, __traits(getMember, module_, member))) && 321 !__traits(isTemplate, __traits(getMember, module_, member)) && 322 __traits(compiles, __traits(parent, __traits(getMember, module_, member))) && 323 __traits(isSame, __traits(parent, __traits(getMember, module_, member)), module_) ){ 324 325 alias member_ = Alias!(__traits(getMember, module_, member)); 326 // unittest in root structs and classes 327 static if(__traits(compiles, __traits(getUnitTests, member_))) { 328 foreach(test; __traits(getUnitTests, member_)) { 329 tests ~= Test(fullyQualifiedName!test, getTestName!test, getTestLocation!test, &test); 330 } 331 } 332 333 // unittests in nested structs and classes 334 static if ( __traits(compiles, __traits(derivedMembers, member_)) ) { 335 foreach(nestedMember; __traits(derivedMembers, member_)) { 336 static if (__traits(compiles, __traits(getMember, member_ , nestedMember)) && 337 __traits(compiles, __traits(getUnitTests, __traits(getMember, member_ , nestedMember)))) { 338 foreach(test; __traits(getUnitTests, __traits(getMember, member_ , nestedMember) )) { 339 tests ~= Test(fullyQualifiedName!test, getTestName!test, getTestLocation!test, &test); 340 } 341 } 342 } 343 344 } 345 } 346 } 347 } 348 return tests; 349 }