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 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, &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, &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 writer.put(newline); 150 151 foreach(th; result.thrown) { 152 writer.formattedWrite(" %s thrown from %s on line %d: %s%s", 153 th.type, 154 th.file, 155 th.line, 156 th.message.lineSplitter.front, 157 newline, 158 ); 159 foreach(line; th.message.lineSplitter.drop(1)) 160 writer.formattedWrite(" %s%s", line, newline); 161 162 writer.formattedWrite(" --- Stack trace ---%s", newline); 163 if(verbose) { 164 foreach(line; th.info) 165 writer.formattedWrite(" %s%s", line, newline); 166 } else { 167 for(size_t i = 0; i < th.info.length && !th.info[i].canFind(__FILE__); ++i) 168 writer.formattedWrite(" %s%s", th.info[i], newline); 169 } 170 } 171 } 172 173 TestResult executeTest(Test test) { 174 import core.exception : AssertError, OutOfMemoryError; 175 auto ret = TestResult(test); 176 auto started = MonoTime.currTime; 177 178 try { 179 scope(exit) ret.duration = MonoTime.currTime - started; 180 test.ptr(); 181 ret.succeed = true; 182 } catch(Throwable t) { 183 if(!(cast(Exception) t || cast(AssertError) t)) 184 throw t; 185 186 foreach(th; t) { 187 immutable(string)[] trace; 188 try { 189 foreach(i; th.info) 190 trace ~= i.idup; 191 } catch(OutOfMemoryError) { // TODO: Actually fix a bug instead of this workaround 192 trace ~= "<silly error> Failed to get stack trace, see https://gitlab.com/AntonMeep/silly/issues/31"; 193 } 194 195 ret.thrown ~= Thrown(typeid(th).name, th.message.idup, th.file, th.line, trace); 196 } 197 } 198 199 return ret; 200 } 201 202 struct Test { 203 string fullName, 204 testName; 205 206 void function() ptr; 207 } 208 209 struct TestResult { 210 Test test; 211 bool succeed; 212 Duration duration; 213 214 immutable(Thrown)[] thrown; 215 } 216 217 struct Thrown { 218 string type, 219 message, 220 file; 221 size_t line; 222 immutable(string)[] info; 223 } 224 225 __gshared bool noColours; 226 227 enum Colour { 228 none, 229 ok = 32, 230 achtung = 31, 231 } 232 233 static struct Console { 234 static void init() { 235 if(noColours) { 236 return; 237 } else { 238 version(Posix) { 239 import core.sys.posix.unistd; 240 noColours = isatty(STDOUT_FILENO) == 0; 241 } else version(Windows) { 242 import core.sys.windows.winbase : GetStdHandle, STD_OUTPUT_HANDLE, INVALID_HANDLE_VALUE; 243 import core.sys.windows.wincon : SetConsoleOutputCP, GetConsoleMode, SetConsoleMode; 244 import core.sys.windows.windef : DWORD; 245 import core.sys.windows.winnls : CP_UTF8; 246 247 SetConsoleOutputCP(CP_UTF8); 248 249 auto hOut = GetStdHandle(STD_OUTPUT_HANDLE); 250 DWORD originalMode; 251 252 // TODO: 4 stands for ENABLE_VIRTUAL_TERMINAL_PROCESSING which should be 253 // in druntime v2.082.0 254 noColours = hOut == INVALID_HANDLE_VALUE || 255 !GetConsoleMode(hOut, &originalMode) || 256 !SetConsoleMode(hOut, originalMode | 4); 257 } 258 } 259 } 260 261 static string colour(T)(T t, Colour c = Colour.none) { 262 import std.conv : text; 263 264 return noColours ? text(t) : text("\033[", cast(int) c, "m", t, "\033[m"); 265 } 266 267 static string emphasis(string s) { 268 return noColours ? s : "\033[1m" ~ s ~ "\033[m"; 269 } 270 } 271 272 string getTestName(alias test)() { 273 string name = __traits(identifier, test); 274 275 foreach(attribute; __traits(getAttributes, test)) { 276 static if(is(typeof(attribute) : string)) { 277 name = attribute; 278 break; 279 } 280 } 281 282 return name; 283 } 284 285 string truncateName(string s, bool verbose = false) { 286 import std.algorithm : max; 287 import std.string : indexOf; 288 return s.length > 30 && !verbose 289 ? s[max(s.indexOf('.', s.length - 30), s.length - 30) .. $] 290 : s; 291 }