1 module isodi.tests;
2 
3 import core.runtime;
4 
5 import std.format;
6 import std.typecons;
7 
8 import isodi.bind;
9 import isodi.pack;
10 import isodi.display;
11 
12 // Disable cylic dependency checks while unittesting
13 //
14 // This thing  is madness and should  be disabled by default.  It causes more problems  than it fixes due  to how many
15 // false-positives it  catches. It makes  it impossible to  create custom inline tests,  because "oh module  1 imports
16 // module 2  and they  both have  tests! oh no!".  It's annoying.  And I feel  stupid making  a separate  directory or
17 // something just  for testing.  Tests are  better inline.  Also, it's  a runtime  feature. It  should be  done by the
18 // compiler. I don't mean just speed, but... why runtime?
19 //
20 // Well, I probably should use `unittest` blocks with  decorators `@DisplayTest` and `@Benchmark`, but that comes with
21 // a problem:  there's no  way to  list all  modules compile-time  (wtf?) which  would be  necessary to  load unittest
22 // attributes. Same if I used some magic property or function to define tests.
23 version (unittest)
24 private extern(C) __gshared string[] rt_options = ["oncycle=ignore"];
25 
26 /// Type of the test callback.
27 alias TestCallback = immutable void delegate(Display);
28 
29 /// Test type.
30 enum TestType {
31 
32     /// Unit test.
33     unit,
34 
35     /// This test is meant to check if everything is displayed correctly and so requires human approval.
36     display,
37 
38     /// This test is meant to measure performance of a certain feature.
39     ///
40     /// Unimplemented.
41     benchmark,
42 
43 }
44 
45 
46 @safe:
47 
48 
49 /// Register a display test
50 mixin template DisplayTest(TestCallback callback) {
51     version (unittest)
52     private TestRunner.Register!(TestType.display, callback) register;
53 }
54 
55 /// Register a benchmark
56 mixin template Benchmark(TestCallback callback) {
57     version (unittest)
58     private TestRunner.Register!(TestType.benchmark, callback) register;
59 }
60 
61 /// Struct for running the tests.
62 version (unittest)
63 struct TestRunner {
64 
65     /// Current status of the test runner.
66     enum Status {
67 
68         /// The tester is done with the current test.
69         idle,
70 
71         /// The tester is working, wait before starting another test.
72         working,
73 
74         /// Awaits <Enter> key press.
75         paused,
76 
77         /// The tester has finished and there are no tests left.
78         finished,
79 
80     }
81 
82     /// Helper template for registering tests
83     package struct Register(TestType type, TestCallback callback) {
84 
85         static this() {
86 
87             // Add this test
88             TestRunner.tests.require(type, []) ~= callback;
89 
90         }
91 
92     }
93 
94     /// List of registered tests.
95     static TestCallback[][TestType] tests;
96 
97     public {
98 
99         /// Current status of the test runner.
100         Status status;
101 
102         /// Message about the current status for the user.
103         string statusMessage;
104 
105         /// Temporary display this test is running in. The object this points to will change every test, so make
106         /// sure to not save the pointer, but use this reference instead.
107         Display display;
108 
109     }
110 
111     // Private data used by the runner
112     private {
113 
114         /// Number of total tests executed.
115         size_t totalExecuted, totalPassed;
116 
117         /// Number of tests of this type executed.
118         size_t typeExecuted, typePassed;
119 
120         /// Last test type ran.
121         TestType lastType;
122 
123         /// Queue of tests.
124         Tuple!(TestType, TestCallback)[] testQueue;
125 
126     }
127 
128     static Display makeDisplay() {
129 
130         auto result = Display.make;
131         result.packs = PackList.make(
132             getPack("res/samerion-retro/pack.json")
133         );
134         result.camera.angle.x = result.camera.angle.x + 180;
135         return result;
136 
137     }
138 
139     /// Start the tests
140     void runTests() {
141 
142         import std.array : array;
143         import std.algorithm : map;
144         import std.traits : EnumMembers;
145 
146         statusMessage = "";
147 
148         // Create the queue
149         static foreach (type; EnumMembers!TestType) {
150 
151             // For each type, fetch all the tests
152             testQueue ~= tests.get(type, [])
153 
154                 // Create a testing tuple
155                 .map!(cb => tuple(type, cb))
156                 .array;
157 
158         }
159 
160     }
161 
162     /// Start next test.
163     ///
164     /// This function may return before the test completes. Check the `status` property for updates.
165     ///
166     /// Since this function is thread-local, it is guarateed to be thread safe.
167     void nextTest() {
168 
169         import std.range : front, popFront;
170 
171         assert(status != Status.working, "Cannot start tests, the runner is busy.");
172         assert(status != Status.finished, "Cannot start tests, all tests have finished already.");
173 
174         status = Status.working;
175 
176         // Check if the tester should proceed
177         if (!proceed) return;
178 
179         statusMessage = lastType.format!"Running %s tests... %s/%s completed"(typePassed, typeExecuted);
180 
181         // Count the test
182         typeExecuted++;
183 
184         () @trusted {
185 
186             // Create a display
187             display = makeDisplay;
188 
189             // Run the test
190             try {
191 
192                 testQueue.front[1](display);
193                 typePassed++;
194 
195             }
196 
197             // Show all errors
198             catch (Throwable e) Renderer.log(e.toString, LogType.error);
199 
200 
201             // React based on test type
202             with (TestType)
203             final switch (testQueue.front[0]) {
204 
205                 // Unittest? Can't happen!
206                 case unit:
207                     assert(0, "Unit tests should be created through the unittest statement, not isodi's test runner");
208 
209                 // Display test, need to wait for user input
210                 case display:
211                     status = Status.paused;
212                     statusMessage = format!"Display test %s/%s. Press <Enter> to continue."(
213                         typeExecuted, tests[display].length
214                     );
215                     break;
216 
217                 // Benchmark, should probably wait for some callback
218                 case benchmark:
219                     break;
220 
221             }
222 
223         }();
224 
225         // Pop the queue
226         testQueue.popFront();
227 
228     }
229 
230     /// Update the test data.
231     /// Returns: true if the runner should proceed to the next test.
232     private bool proceed() {
233 
234         import std.conv : text;
235         import std.range : front;
236 
237         // Waiting for unit tests
238         if (lastType == TestType.unit) {
239 
240             // Start them
241             import core.runtime : runModuleUnitTests;
242 
243             statusMessage = "Running unit tests...";
244 
245             // Run the tests
246             const result = (() @trusted => runModuleUnitTests())();
247 
248             typePassed   += result.passed;
249             typeExecuted += result.executed;
250 
251         }
252 
253         // Check if there are any tests left
254         if (!testQueue.length) {
255 
256             endSection();
257             endTests();
258 
259             return false;
260 
261         }
262 
263         // Next test section
264         if (lastType != testQueue.front[0]) {
265 
266             endSection();
267 
268         }
269 
270         lastType = testQueue.front[0];
271 
272         return true;
273 
274     }
275 
276     /// Force abort tests before completion.
277     void abortTests() {
278 
279         // Update test counts
280         totalPassed   += typePassed;
281         totalExecuted += typeExecuted;
282 
283         // Prepare output
284         const output = format!"tests aborted. %s/%s passed, %s left unfinished."(
285             totalPassed - 1, totalExecuted - 1, testQueue.length + 1
286         );
287 
288         // Output as failure
289         Renderer.log(output, LogType.error);
290 
291         status = Status.finished;
292 
293     }
294 
295     /// End test section.
296     private void endSection() {
297 
298         const output = lastType.format!"%s tests finished. %s/%s passed."(typePassed, typeExecuted);
299 
300         // Output the log
301         Renderer.log(output, typePassed == typeExecuted ? LogType.success : LogType.error);
302 
303         // Update test counts
304         totalPassed   += typePassed;
305         totalExecuted += typeExecuted;
306         typePassed   = 0;
307         typeExecuted = 0;
308 
309     }
310 
311     /// End all tests
312     private void endTests() {
313 
314         const output = format!"all tests finished. %s/%s passed."(totalPassed, totalExecuted);
315 
316         // Output
317         Renderer.log(output, totalPassed == totalExecuted ? LogType.success : LogType.error);
318 
319         statusMessage = "All done! Finishing...";
320         status = Status.finished;
321 
322     }
323 
324 }