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 }