1 /** 2 Copyright: Copyright (c) 2019, Joakim Brännström. All rights reserved. 3 License: MPL-2 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This Source Code Form is subject to the terms of the Mozilla Public License, 7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain 8 one at http://mozilla.org/MPL/2.0/. 9 10 # Analyze 11 12 The worklist should not be cleared during an analyze phase. 13 Any mutant that has been removed in the source code will be automatically 14 removed from the worklist because the tables is setup with ON DELETE CASCADE. 15 16 Thus by not removing it old timeout mutants that need more work will be 17 "resumed". 18 19 # Test 20 21 TODO: describe the test phase and FSM 22 */ 23 module dextool.plugin.mutate.backend.test_mutant.timeout; 24 25 import logger = std.experimental.logger; 26 import std.exception : collectException; 27 28 import miniorm : spinSql; 29 import my.from_; 30 import my.fsm; 31 32 import dextool.plugin.mutate.backend.database : Database, MutantTimeoutCtx, MutationStatusId; 33 import dextool.plugin.mutate.backend.type : Mutation, ExitStatus; 34 35 @safe: 36 37 /// How the timeout is configured and what it is. 38 struct TimeoutConfig { 39 import std.datetime : Duration; 40 41 double timeoutScaleFactor = 2.0; 42 43 private { 44 bool userConfigured_; 45 Duration baseTimeout; 46 long iteration; 47 } 48 49 bool isUserConfig() @safe pure nothrow const @nogc { 50 return userConfigured_; 51 } 52 53 /// Force the value to be this 54 void userConfigured(Duration t) @safe pure nothrow @nogc { 55 userConfigured_ = true; 56 baseTimeout = t; 57 } 58 59 /// Only set the timeout if it isnt set by the user. 60 void set(Duration t) @safe pure nothrow @nogc { 61 if (!userConfigured_) 62 baseTimeout = t; 63 } 64 65 void updateIteration(long x) @safe pure nothrow @nogc { 66 iteration = x; 67 } 68 69 long iter() @safe pure nothrow const @nogc { 70 return iteration; 71 } 72 73 Duration value() @safe pure nothrow const @nogc { 74 import std.algorithm : max; 75 import std.datetime : dur; 76 77 // Assuming that a timeout <1s is too strict because of OS jitter and load. 78 // It would lead to "false" timeout status of mutants. 79 return max(1.dur!"seconds", calculateTimeout(iteration, baseTimeout, timeoutScaleFactor)); 80 } 81 82 Duration base() @safe pure nothrow const @nogc { 83 return baseTimeout; 84 } 85 } 86 87 /// Reset the state of the timeout algorithm to its inital state. 88 void resetTimeoutContext(ref Database db) @trusted { 89 db.timeoutApi.put(MutantTimeoutCtx.init); 90 } 91 92 /// Calculate the timeout to use based on the context. 93 std_.datetime.Duration calculateTimeout(const long iter, 94 std_.datetime.Duration base, const double timeoutScaleFactor) pure nothrow @nogc { 95 import core.time : dur; 96 import std.math : sqrt; 97 98 static immutable double constant_factor = 1.5; 99 const double n = iter; 100 101 const double scale = constant_factor + sqrt(n) * timeoutScaleFactor; 102 return (1L + (cast(long)(base.total!"msecs" * scale))).dur!"msecs"; 103 } 104 105 /** Update the status of a mutant. 106 * 107 * If the mutant is `timeout` then it will be added to the worklist if the 108 * mutation testing is in the initial phase. 109 * 110 * If it has progressed beyond the init phase then it depends on if the local 111 * iteration variable of *this* instance of dextool matches the one in the 112 * database. This ensures that all instances that work on the same database is 113 * in-sync with each other. 114 * 115 * Params: 116 * db = database to use 117 * id = ? 118 * st = ? 119 * usedIter = the `iter` value that was used to test the mutant 120 */ 121 void updateMutantStatus(ref Database db, const MutationStatusId id, 122 const Mutation.Status st, const ExitStatus ecode, const long usedIter) @trusted { 123 import std.typecons : Yes; 124 125 // TODO: ugly hack to integrate memory overload with timeout. Refactor in 126 // the future. It is just, the most convenient place to do it at the 127 // moment. 128 129 assert(MaxTimeoutIterations - 1 > 0, 130 "MaxTimeoutIterations configured too low for memOverload to use it"); 131 132 if (st == Mutation.Status.timeout && usedIter == 0) 133 db.timeoutApi.put(id, usedIter); 134 else 135 db.timeoutApi.update(id, usedIter); 136 137 if (st == Mutation.Status.memOverload && usedIter < MaxTimeoutIterations - 1) { 138 // the overloaded need to be re-tested a couple of times. 139 db.memOverloadApi.put(id); 140 } 141 142 db.mutantApi.update(id, st, ecode, Yes.updateTs); 143 } 144 145 /** FSM for handling mutants during the test phase. 146 */ 147 struct TimeoutFsm { 148 @safe: 149 150 static struct Init { 151 } 152 153 static struct ResetWorkList { 154 } 155 156 static struct UpdateCtx { 157 } 158 159 static struct Running { 160 } 161 162 static struct Purge { 163 // worklist items and if they have changed or not 164 enum Event { 165 changed, 166 same 167 } 168 169 Event ev; 170 } 171 172 static struct Done { 173 } 174 175 static struct ClearWorkList { 176 } 177 178 static struct Stop { 179 } 180 181 /// Data used by all states. 182 static struct Global { 183 MutantTimeoutCtx ctx; 184 Database* db; 185 bool stop; 186 } 187 188 static struct Output { 189 /// The current iteration through the timeout algorithm. 190 long iter; 191 /// When the testing of all timeouts are done, e.g. the state is "done". 192 bool done; 193 } 194 195 /// Output that may be used. 196 Output output; 197 198 private { 199 Fsm!(Init, ResetWorkList, UpdateCtx, Running, Purge, Done, ClearWorkList, Stop) fsm; 200 Global global; 201 } 202 203 void setLogLevel() @trusted nothrow { 204 try { 205 if (logger.globalLogLevel == logger.LogLevel.trace) 206 fsm.logger = (string s) { logger.trace(s); }; 207 } catch (Exception e) { 208 logger.trace(e.msg).collectException; 209 } 210 } 211 212 void execute(ref Database db) @trusted { 213 execute_(this, &db); 214 } 215 216 static void execute_(ref TimeoutFsm self, Database* db) @trusted { 217 self.global.db = db; 218 // must always run the loop at least once. 219 self.global.stop = false; 220 221 auto t = db.transaction; 222 self.global.ctx = db.timeoutApi.getMutantTimeoutCtx; 223 224 // force the local state to match the starting point in the ctx 225 // (database). 226 final switch (self.global.ctx.state) with (MutantTimeoutCtx) { 227 case State.init_: 228 self.fsm.state = fsm(Init.init); 229 break; 230 case State.running: 231 self.fsm.state = fsm(Running.init); 232 break; 233 case State.done: 234 self.fsm.state = fsm(Done.init); 235 break; 236 } 237 238 // act on the inital state 239 try { 240 self.fsm.act!self; 241 } catch (Exception e) { 242 logger.warning(e.msg).collectException; 243 } 244 245 while (!self.global.stop) { 246 try { 247 step(self, *db); 248 } catch (Exception e) { 249 logger.warning(e.msg).collectException; 250 } 251 } 252 253 db.timeoutApi.put(self.global.ctx); 254 t.commit; 255 256 self.output.iter = self.global.ctx.iter; 257 } 258 259 private static void step(ref TimeoutFsm self, ref Database db) @safe { 260 bool noUnknown() { 261 return db.mutantApi.unknownSrcMutants().count == 0 && db.worklistApi.getCount == 0; 262 } 263 264 self.fsm.next!((Init a) { 265 if (noUnknown) 266 return fsm(ResetWorkList.init); 267 return fsm(Stop.init); 268 }, (ResetWorkList a) => fsm(UpdateCtx.init), (UpdateCtx a) => fsm(Running.init), (Running a) { 269 if (noUnknown) 270 return fsm(Purge.init); 271 return fsm(Stop.init); 272 }, (Purge a) { 273 final switch (a.ev) with (Purge.Event) { 274 case changed: 275 if (self.global.ctx.iter == MaxTimeoutIterations) { 276 return fsm(ClearWorkList.init); 277 } 278 return fsm(ResetWorkList.init); 279 case same: 280 return fsm(ClearWorkList.init); 281 } 282 }, (ClearWorkList a) => fsm(Done.init), (Done a) { 283 if (noUnknown) 284 return fsm(Stop.init); 285 // happens if an operation is performed that changes the status of 286 // already tested mutants to unknown. 287 return fsm(Running.init); 288 }, (Stop a) => fsm(a),); 289 290 self.fsm.act!self; 291 } 292 293 void opCall(Init) { 294 global.ctx = MutantTimeoutCtx.init; 295 output.done = false; 296 } 297 298 void opCall(ResetWorkList) { 299 global.db.timeoutApi.copyMutantTimeoutWorklist(global.ctx.iter); 300 301 global.db.memOverloadApi.toWorklist; 302 global.db.memOverloadApi.clear; 303 } 304 305 void opCall(UpdateCtx) { 306 global.ctx.iter += 1; 307 global.ctx.worklistCount = global.db.timeoutApi.countMutantTimeoutWorklist; 308 } 309 310 void opCall(Running) { 311 global.ctx.state = MutantTimeoutCtx.State.running; 312 output.done = false; 313 } 314 315 void opCall(ref Purge data) { 316 global.db.timeoutApi.reduceMutantTimeoutWorklist(global.ctx.iter); 317 318 if (global.db.timeoutApi.countMutantTimeoutWorklist == global.ctx.worklistCount) { 319 data.ev = Purge.Event.same; 320 } else { 321 data.ev = Purge.Event.changed; 322 } 323 } 324 325 void opCall(Done) { 326 global.ctx.state = MutantTimeoutCtx.State.done; 327 // must reset in case the mutation testing reach the end and is 328 // restarted with another mutation operator type than previously 329 global.ctx.iter = 0; 330 global.ctx.worklistCount = 0; 331 332 output.done = true; 333 } 334 335 void opCall(ClearWorkList) { 336 global.db.timeoutApi.clearMutantTimeoutWorklist; 337 338 global.db.memOverloadApi.toWorklist; 339 global.db.memOverloadApi.clear; 340 } 341 342 void opCall(Stop) { 343 global.stop = true; 344 } 345 } 346 347 // If the mutants has been tested 2 times it should be good enough. Sometimes 348 // there are so many timeout that it would feel like the tool just end up in an 349 // infinite loop. Maybe this should be moved so it is user configurable in the 350 // future. 351 // The user also have the admin operation stopTimeoutTest to use. 352 immutable MaxTimeoutIterations = 2;