1 /// Mecca UT support 2 module mecca.runtime.ut; 3 4 // Licensed under the Boost license. Full copyright information in the AUTHORS file 5 6 version(unittest): 7 8 import std.file: read; 9 import std.stdio; 10 import std..string; 11 import std.datetime; 12 import std.path: absolutePath, buildNormalizedPath; 13 import core.sys.posix.unistd: isatty; 14 import core.runtime: Runtime; 15 16 import mecca.lib.console; 17 import mecca.log; 18 19 shared static this() { 20 // Disable pre-main unittests run 21 Runtime.moduleUnitTester = (){return true;}; 22 } 23 24 /** 25 * Automatic main for UT compilations. 26 * 27 * Special main for UT compilations. This main accepts arguments that limit (by module) the UTs to run. 28 */ 29 @notrace int utMain(string[] argv) { 30 int res = parseArgs(argv); 31 if( res>0 ) 32 return res-1; 33 34 ModuleInfo*[] modules; 35 foreach(m; ModuleInfo) { 36 if (m && m.unitTest) { 37 if( !shouldRun(m.name) ) 38 continue; 39 40 if( listModules ) 41 writeln(m.name); 42 else 43 modules ~= m; 44 } 45 } 46 47 if( !runTests ) 48 return 0; 49 50 size_t counter; 51 bool failed = false; 52 auto startTime = MonoTime.currTime(); 53 54 META!"Started UT of %s (a total of %s found)"(buildNormalizedPath(argv[0].absolutePath()), modules.length); 55 logLine(FG.icyan("Started UT of %s (a total of %s found)".format(buildNormalizedPath(argv[0].absolutePath()), modules.length))); 56 57 foreach(m; modules) { 58 counter++; 59 version (linux) 60 DEBUG!"#LOADAVG %s"(cast(immutable char[])read("/proc/loadavg")); 61 META!"Running UT of %s"(m.name); 62 logLine(FG.yellow("Running UT of ") ~ FG.iwhite(m.name)); 63 try { 64 auto ut = m.unitTest; 65 ut(); 66 } 67 catch (Throwable ex) { 68 ERROR!"UT failed!"(); 69 logLine(FG.red("UT failed!")); 70 auto seenSep = false; 71 foreach(line; ex.toString().lineSplitter()) { 72 auto idx = line.indexOf(" "); 73 if (seenSep && idx >= 0) { 74 auto loc = line[0 .. idx]; 75 auto func = line[idx .. $]; 76 writefln(" %-30s %s", (loc == "??:?") ? "" : loc, func); 77 if (func.startsWith(" int mecca.ut_harness.main")) { 78 break; 79 } 80 } 81 else { 82 if (!seenSep && line.indexOf("------------") >= 0) { 83 seenSep = true; 84 writeln(" ----------------------------------------------"); 85 } 86 else { 87 writeln(" ", line); 88 } 89 } 90 } 91 failed = true; 92 break; 93 } 94 } 95 auto endTime = MonoTime.currTime(); 96 auto secs = (endTime - startTime).total!"msecs" / 1000.0; 97 98 int retVal; 99 if (failed) { 100 META!"Failed. Ran %s unittests in %.2f seconds"(counter, secs); 101 logLine(FG.ired("Failed. Ran %s unittests in %.2f seconds".format(counter, secs))); 102 retVal = 1; 103 } 104 else if (counter == 0) { 105 META!"Did not find any unittest to run"(); 106 logLine(FG.ired("Did not find any unittests to run")); 107 retVal = 2; 108 } 109 else { 110 META!"Success. Ran %s unittests in %.2f seconds"(counter, secs); 111 logLine(FG.igreen("Success. Ran %s unittests in %.2f seconds".format(counter, secs))); 112 retVal = 0; 113 } 114 version (linux) 115 DEBUG!"#LOADAVG %s"(cast(immutable char[])read("/proc/loadavg")); 116 117 return retVal; 118 } 119 120 struct mecca_ut {} 121 122 void runFixtureTestCases(FIXTURE, string mod = __MODULE__)() { 123 import std.stdio; 124 import std.traits; 125 scope(exit) { 126 flushLog(); 127 static if( !LogToConsole ) 128 stderr.flush(); 129 } 130 static if( !LogToConsole ) 131 writeln(); 132 foreach(testCaseName; __traits(derivedMembers, FIXTURE)) { 133 static if ( __traits(compiles, __traits(getMember, FIXTURE, testCaseName) ) ) { 134 static if (hasUDA!(__traits(getMember, FIXTURE, testCaseName), mecca_ut)) { 135 import std..string:format; 136 string fullCaseName = format("%s.%s", __traits(identifier, FIXTURE), testCaseName); 137 META!"Test Fixture: %s"(__traits(identifier, FIXTURE)); 138 META!"Test Case: %s"(fullCaseName); 139 static if( !LogToConsole ) 140 stderr.writefln("\t%s...", fullCaseName); 141 import std.typecons:scoped; 142 auto fixture = new FIXTURE(); 143 scope(exit) destroy(fixture); 144 try { 145 __traits(getMember, fixture, testCaseName)(); 146 } catch (Throwable t) { 147 ERROR!"Test %s failed with exception"(fullCaseName); 148 LOG_EXCEPTION(t); 149 static if( !LogToConsole ) 150 stderr.writeln("\tERROR"); 151 throw t; 152 } 153 } 154 } 155 } 156 } 157 158 /** 159 * Automatic UT expansion 160 * 161 * Applying the mixin on a class causes all class members labeled with the @mecca_ut attribute to run. 162 */ 163 mixin template TEST_FIXTURE(FIXTURE) { 164 unittest { 165 import mecca.runtime.ut: runFixtureTestCases; 166 runFixtureTestCases!(FIXTURE)(); 167 } 168 } 169 170 private: 171 struct FilterLine { 172 string filter; 173 enum Type { PRECISE, NEGATIVE, PARTIAL } 174 Type type; 175 bool matched; 176 } 177 178 FilterLine[] filters; 179 bool listModules; 180 bool runTests = true; 181 182 @notrace void logLine(string text) { 183 auto t = Clock.currTime(); 184 writefln(FG.grey("%02d:%02d:%02d.%03d") ~ " %s", t.hour, t.minute, t.second, 185 t.fracSecs.total!"msecs", text); 186 } 187 188 bool shouldRun(string name) { 189 if( filters.length==0 ) 190 return true; 191 192 bool should = false; 193 foreach( ref filter; filters ) { 194 with(FilterLine.Type) final switch( filter.type ) { 195 case PRECISE: 196 if( filter.filter == name ) { 197 should = true; 198 filter.matched = true; 199 } 200 break; 201 case NEGATIVE: 202 if( filter.filter == name ) { 203 should = false; 204 } 205 break; 206 case PARTIAL: 207 if( indexOf(name, filter.filter) != -1 ) { 208 should = true; 209 filter.matched = true; 210 } 211 } 212 } 213 214 return should; 215 } 216 217 int parseArgs(string[] args) { 218 foreach( i, arg; args[1..$] ) { 219 if( arg.length==0 ) { 220 stderr.writefln("Error: Argument %s has length 0", i); 221 return 2; 222 } 223 switch( arg[0] ) { 224 case '=': 225 if( arg.length==1 ) { 226 stderr.writefln("Error: Argument %s specifies precise match, but does not specify actual match", i); 227 return 2; 228 } 229 filters ~= FilterLine( arg[1..$], FilterLine.Type.PRECISE ); 230 break; 231 case '-': 232 if( arg.length==1 ) { 233 stderr.writefln("Error: Argument %s specifies negative match, but does not specify actual match", i); 234 return 2; 235 } 236 if( arg[1]=='-' ) { 237 if( !parseOption(arg[2..$]) ) 238 return 2; 239 } else { 240 filters ~= FilterLine( arg[1..$], FilterLine.Type.NEGATIVE ); 241 } 242 break; 243 default: 244 filters ~= FilterLine( arg, FilterLine.Type.PARTIAL ); 245 break; 246 } 247 } 248 249 return 0; 250 } 251 252 bool parseOption(string opt) { 253 if( opt == "list" ) { 254 listModules = true; 255 runTests = false; 256 return true; 257 } 258 259 if( opt == "help" ) { 260 writeln( 261 `UT help: program args 262 args are a list of filters: 263 Naked filters are partially matched. 264 Filters starting with minus ("-") subtract. 265 Filters starting with equal ("=") match precisely. 266 267 Example: 268 program foo -foo.bar 269 will run all tests from modules containing "foo" except the module foo.bar 270 271 program =foo 272 will run just the tests in module foo 273 274 The filters are processed in order: 275 program foo -foo.bar hello 276 277 will run a module called "foo.bar.hello". 278 279 If no filters are given, all modules with unittests are run. 280 281 Options: 282 --list list all modules passing the supplied filter 283 --help print this message 284 ` 285 ); 286 287 runTests = false; 288 289 return true; 290 } 291 292 stderr.writefln("Unknown option \"--%s\"", opt); 293 return false; 294 }