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