1 /*
2  * Hunt - A cross-platform abstraction library with asynchronous I/O.
3  *
4  * Copyright (C) 2021-2022 Kerisy.com
5  *
6  * Website: https://www.kerisy.com
7  *
8  * Licensed under the Apache-2.0 License.
9  *
10  */
11 
12 module geario.util.UnitTest;
13 
14 import core.time;
15 
16 private enum string[] FixedObjectMembers = ["toString", "opCmp", "opEquals", "Monitor", "factory"];
17 
18 void testUnits(T)() {
19     enum v = generateUnitTests!T;
20     // pragma(msg, v);
21     mixin(v);
22 }
23 
24 string generateUnitTests(T)() {
25     import std.string;
26     import std.algorithm;
27     import std.traits;
28 
29     enum fullTypeName = fullyQualifiedName!(T);
30     enum memberModuleName = moduleName!(T);
31 
32     string[] methodsBefore;
33     string[] methodsAfter;
34 
35     string str;
36     str ~= `import std.stdio; import geario.logging;
37 writeln("=================================");
38 writeln("Module: ` ~ fullTypeName ~ `     ");
39 writeln("=================================");
40 
41 `;
42     str ~= "import " ~ memberModuleName ~ ";\n";
43     str ~= T.stringof ~ " t;\n";
44 
45     // 
46     foreach (memberName; __traits(allMembers, T)) {
47         // pragma(msg, "member: " ~ memberName);
48         static if (is(T == class) && FixedObjectMembers.canFind(memberName)) {
49             // pragma(msg, "skipping fixed Object member: " ~ memberName);
50         } else {
51             enum memberProtection = __traits(getProtection, __traits(getMember, T, memberName));
52             static if (memberProtection == "private"
53                     || memberProtection == "protected" 
54                     || memberProtection == "export") {
55                 // version (GEAR_DEBUG) pragma(msg, "skip private member: " ~ memberName);
56             } else {
57                 import std.meta : Alias;
58                 alias currentMember = Alias!(__traits(getMember, T, memberName));
59                 static if (hasUDA!(currentMember, Before)) {
60                     alias memberType = typeof(currentMember);
61                     static if (is(memberType == function)) {
62                         methodsBefore ~= memberName;
63                     }
64                 }
65 
66                 static if (hasUDA!(currentMember, After)) {
67                     alias memberType = typeof(currentMember);
68                     static if (is(memberType == function)) {
69                         methodsAfter ~= memberName;
70                     }
71                 }
72             }
73         }
74     }
75 
76     // 
77     foreach (memberName; __traits(allMembers, T)) {
78     //     pragma(msg, "member: " ~ memberName);
79         static if (is(T == class) && FixedObjectMembers.canFind(memberName)) {
80             // pragma(msg, "skipping fixed Object member: " ~ memberName);
81         } else {
82             enum memberProtection = __traits(getProtection, __traits(getMember, T, memberName));
83             static if (memberProtection == "private"
84                     || memberProtection == "protected" 
85                     || memberProtection == "export") {
86                 // version (GEAR_DEBUG) pragma(msg, "skip private member: " ~ memberName);
87             } else {
88                 import std.meta : Alias;
89                 alias currentMember = Alias!(__traits(getMember, T, memberName));
90 
91                 static if (isFunction!(currentMember)) {
92                     alias testWithUDAs = getUDAs!(currentMember, TestWith);// hasUDA!(currentMember, Test);
93                     static if(testWithUDAs.length >0) {
94                         str ~= `writeln("\n========> testing: ` ~ memberName ~ "\");\n";
95 
96                         // Every @Test method will be test alone. 
97                         str ~= "t = new " ~ T.stringof ~ "();\n";
98 
99                         // running methods annotated with BEFORE
100                         foreach(string s; methodsBefore) {
101                             str ~= "t." ~ s ~ "();\n";
102                         }
103                         
104                         // execute a test 
105                         alias expectedType = typeof(testWithUDAs[0].expected);
106                         static if(is(expectedType : Throwable)) {
107                             str ~= "try { t." ~ memberName ~ "(); } catch(" ~ fullyQualifiedName!expectedType ~ 
108                                 " ex) { version(GEAR_DEBUG) { warning(ex.msg); } }\n";
109                         } else {
110                             str ~= "t." ~ memberName ~ "();\n"; 
111                         }
112 
113                         // running methods annotated with BEFORE
114                         foreach(string s; methodsAfter) {
115                             str ~= "t." ~ s ~ "();\n";
116                         }
117                     } else {
118                         static if (memberName.startsWith("test") || memberName.endsWith("Test")
119                             || hasUDA!(currentMember, Test)) {
120                             str ~= `writeln("\n========> testing: ` ~ memberName ~ "\");\n";
121 
122                             // Every @Test method will be test alone. 
123                             str ~= "t = new " ~ T.stringof ~ "();\n";
124 
125                             // running methods annotated with BEFORE
126                             foreach(string s; methodsBefore) {
127                                 str ~= "t." ~ s ~ "();\n";
128                             }
129                             
130                             // execute a test 
131                             str ~= "t." ~ memberName ~ "();\n"; 
132 
133                             // running methods annotated with BEFORE
134                             foreach(string s; methodsAfter) {
135                                 str ~= "t." ~ s ~ "();\n";
136                             }
137                         }
138                     }
139                 }
140             }
141         }
142     }
143     return str;
144 }
145 
146 /**
147 */
148 struct TestWith(T = Object) {
149     T expected; 
150 }
151 
152 
153 /**
154  * The <code>Test</code> annotation tells JUnit that the <code>public void</code> method
155  * to which it is attached can be run as a test case. To run the method,
156  * JUnit first constructs a fresh instance of the class then invokes the
157  * annotated method. Any exceptions thrown by the test will be reported
158  * by JUnit as a failure. If no exceptions are thrown, the test is assumed
159  * to have succeeded.
160  * <p>
161  * A simple test looks like this:
162  * <pre>
163  * public class Example {
164  *    <b>&#064;Test</b>
165  *    public void method() {
166  *       Assert.assertTrue( new ArrayList().isEmpty() );
167  *    }
168  * }
169  * </pre>
170  * <p>
171  * The <code>Test</code> annotation supports two optional parameters.
172  * The first, <code>expected</code>, declares that a test method should throw
173  * an exception. If it doesn't throw an exception or if it throws a different exception
174  * than the one declared, the test fails. For example, the following test succeeds:
175  * <pre>
176  *    &#064;Test(<b>expected=IndexOutOfBoundsException.class</b>) public void outOfBounds() {
177  *       new ArrayList&lt;Object&gt;().get(1);
178  *    }
179  * </pre>
180  * If the exception's message or one of its properties should be verified, the
181  * {@link rules.ExpectedException ExpectedException} rule can be used. Further
182  * information about exception testing can be found at the
183  * <a href="https://github.com/junit-team/junit/wiki/Exception-testing">JUnit Wiki</a>.
184  * <p>
185  * The second optional parameter, <code>timeout</code>, causes a test to fail if it takes
186  * longer than a specified amount of clock time (measured in milliseconds). The following test fails:
187  * <pre>
188  *    &#064;Test(<b>timeout=100</b>) public void infinity() {
189  *       while(true);
190  *    }
191  * </pre>
192  * <b>Warning</b>: while <code>timeout</code> is useful to catch and terminate
193  * infinite loops, it should <em>not</em> be considered deterministic. The
194  * following test may or may not fail depending on how the operating system
195  * schedules threads:
196  * <pre>
197  *    &#064;Test(<b>timeout=100</b>) public void sleep100() {
198  *       Thread.sleep(100);
199  *    }
200  * </pre>
201  * <b>THREAD SAFETY WARNING:</b> Test methods with a timeout parameter are run in a thread other than the
202  * thread which runs the fixture's @Before and @After methods. This may yield different behavior for
203  * code that is not thread safe when compared to the same test method without a timeout parameter.
204  * <b>Consider using the {@link rules.Timeout} rule instead</b>, which ensures a test method is run on the
205  * same thread as the fixture's @Before and @After methods.
206  *
207  */
208 struct Test {
209     Duration timeout;
210 }
211 
212 
213 /**
214  * When writing tests, it is common to find that several tests need similar
215  * objects created before they can run. Annotating a <code>public void</code> method
216  * with <code>&#064;Before</code> causes that method to be run before the {@link Test} method.
217  * The <code>&#064;Before</code> methods of superclasses will be run before those of the current class,
218  * unless they are overridden in the current class. No other ordering is defined.
219  * <p>
220  * Here is a simple example:
221  * <pre>
222  * public class Example {
223  *    List empty;
224  *    &#064;Before public void initialize() {
225  *       empty= new ArrayList();
226  *    }
227  *    &#064;Test public void size() {
228  *       ...
229  *    }
230  *    &#064;Test public void remove() {
231  *       ...
232  *    }
233  * }
234  * </pre>
235  *
236  */
237 interface Before {
238 }
239 
240 
241 /**
242  * If you allocate external resources in a {@link Before} method you need to release them
243  * after the test runs. Annotating a <code>public void</code> method
244  * with <code>&#064;After</code> causes that method to be run after the {@link Test} method. All <code>&#064;After</code>
245  * methods are guaranteed to run even if a {@link Before} or {@link Test} method throws an
246  * exception. The <code>&#064;After</code> methods declared in superclasses will be run after those of the current
247  * class, unless they are overridden in the current class.
248  * <p>
249  * Here is a simple example:
250  * <pre>
251  * public class Example {
252  *    File output;
253  *    &#064;Before public void createOutputFile() {
254  *          output= new File(...);
255  *    }
256  *    &#064;Test public void something() {
257  *          ...
258  *    }
259  *    &#064;After public void deleteOutputFile() {
260  *          output.delete();
261  *    }
262  * }
263  * </pre>
264  *
265  */
266 interface After {
267 }
268 
269 
270 /**
271  * Sometimes several tests need to share computationally expensive setup
272  * (like logging into a database). While this can compromise the independence of
273  * tests, sometimes it is a necessary optimization. Annotating a <code>public static void</code> no-arg method
274  * with <code>@BeforeClass</code> causes it to be run once before any of
275  * the test methods in the class. The <code>@BeforeClass</code> methods of superclasses
276  * will be run before those of the current class, unless they are shadowed in the current class.
277  * <p>
278  * For example:
279  * <pre>
280  * public class Example {
281  *    &#064;BeforeClass public static void onlyOnce() {
282  *       ...
283  *    }
284  *    &#064;Test public void one() {
285  *       ...
286  *    }
287  *    &#064;Test public void two() {
288  *       ...
289  *    }
290  * }
291  * </pre>
292  *
293  */
294 interface BeforeClass {
295 
296 }
297 
298 
299 /**
300  * If you allocate expensive external resources in a {@link BeforeClass} method you need to release them
301  * after all the tests in the class have run. Annotating a <code>public static void</code> method
302  * with <code>&#064;AfterClass</code> causes that method to be run after all the tests in the class have been run. All <code>&#064;AfterClass</code>
303  * methods are guaranteed to run even if a {@link BeforeClass} method throws an
304  * exception. The <code>&#064;AfterClass</code> methods declared in superclasses will be run after those of the current
305  * class, unless they are shadowed in the current class.
306  * <p>
307  * Here is a simple example:
308  * <pre>
309  * public class Example {
310  *    private static DatabaseConnection database;
311  *    &#064;BeforeClass public static void login() {
312  *          database= ...;
313  *    }
314  *    &#064;Test public void something() {
315  *          ...
316  *    }
317  *    &#064;Test public void somethingElse() {
318  *          ...
319  *    }
320  *    &#064;AfterClass public static void logout() {
321  *          database.logout();
322  *    }
323  * }
324  * </pre>
325  *
326  */
327 interface AfterClass {
328 
329 }
330 
331 
332 /**
333  * Sometimes you want to temporarily disable a test or a group of tests. Methods annotated with
334  * {@link Test} that are also annotated with <code>&#064;Ignore</code> will not be executed as tests.
335  * Also, you can annotate a class containing test methods with <code>&#064;Ignore</code> and none of the containing
336  * tests will be executed. Native JUnit 4 test runners should report the number of ignored tests along with the
337  * number of tests that ran and the number of tests that failed.
338  *
339  * <p>For example:
340  * <pre>
341  *    &#064;Ignore &#064;Test public void something() { ...
342  * </pre>
343  * &#064;Ignore takes an optional default parameter if you want to record why a test is being ignored:
344  * <pre>
345  *    &#064;Ignore("not ready yet") &#064;Test public void something() { ...
346  * </pre>
347  * &#064;Ignore can also be applied to the test class:
348  * <pre>
349  *      &#064;Ignore public class IgnoreMe {
350  *          &#064;Test public void test1() { ... }
351  *          &#064;Test public void test2() { ... }
352  *         }
353  * </pre>
354  *
355  */
356 struct Ignore {
357     /**
358      * The optional reason why the test is ignored.
359      */    
360     string value;
361 }