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 }