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