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 private { 42 bool userConfigured_; 43 Duration baseTimeout; 44 long iteration; 45 } 46 47 bool isUserConfig() @safe pure nothrow const @nogc { 48 return userConfigured_; 49 } 50 51 /// Force the value to be this 52 void userConfigured(Duration t) @safe pure nothrow @nogc { 53 userConfigured_ = true; 54 baseTimeout = t; 55 } 56 57 /// Only set the timeout if it isnt set by the user. 58 void set(Duration t) @safe pure nothrow @nogc { 59 if (!userConfigured_) 60 baseTimeout = t; 61 } 62 63 void updateIteration(long x) @safe pure nothrow @nogc { 64 iteration = x; 65 } 66 67 long iter() @safe pure nothrow const @nogc { 68 return iteration; 69 } 70 71 Duration value() @safe pure nothrow const @nogc { 72 import std.algorithm : max; 73 import std.datetime : dur; 74 75 // Assuming that a timeout <1s is too strict because of OS jitter and load. 76 // It would lead to "false" timeout status of mutants. 77 return max(1.dur!"seconds", calculateTimeout(iteration, baseTimeout)); 78 } 79 80 Duration base() @safe pure nothrow const @nogc { 81 return baseTimeout; 82 } 83 } 84 85 /// Reset the state of the timeout algorithm to its inital state. 86 void resetTimeoutContext(ref Database db) @trusted { 87 db.timeoutApi.put(MutantTimeoutCtx.init); 88 } 89 90 /// Calculate the timeout to use based on the context. 91 std_.datetime.Duration calculateTimeout(const long iter, std_.datetime.Duration base) pure nothrow @nogc { 92 import core.time : dur; 93 import std.math : sqrt; 94 95 static immutable double constant_factor = 1.5; 96 static immutable double scale_factor = 2.0; 97 const double n = iter; 98 99 const double scale = constant_factor + sqrt(n) * scale_factor; 100 return (1L + (cast(long)(base.total!"msecs" * scale))).dur!"msecs"; 101 } 102 103 /** Update the status of a mutant. 104 * 105 * If the mutant is `timeout` then it will be added to the worklist if the 106 * mutation testing is in the initial phase. 107 * 108 * If it has progressed beyond the init phase then it depends on if the local 109 * iteration variable of *this* instance of dextool matches the one in the 110 * database. This ensures that all instances that work on the same database is 111 * in-sync with each other. 112 * 113 * Params: 114 * db = database to use 115 * id = ? 116 * st = ? 117 * usedIter = the `iter` value that was used to test the mutant 118 */ 119 void updateMutantStatus(ref Database db, const MutationStatusId id, 120 const Mutation.Status st, const ExitStatus ecode, const long usedIter) @trusted { 121 import std.typecons : Yes; 122 123 // TODO: ugly hack to integrate memory overload with timeout. Refactor in 124 // the future. It is just, the most convenient place to do it at the 125 // moment. 126 127 assert(MaxTimeoutIterations - 1 > 0, 128 "MaxTimeoutIterations configured too low for memOverload to use it"); 129 130 if (st == Mutation.Status.timeout && usedIter == 0) 131 db.timeoutApi.put(id, usedIter); 132 else 133 db.timeoutApi.update(id, usedIter); 134 135 if (st == Mutation.Status.memOverload && usedIter < MaxTimeoutIterations - 1) { 136 // the overloaded need to be re-tested a couple of times. 137 db.memOverloadApi.put(id); 138 } 139 140 db.mutantApi.updateMutationStatus(id, st, ecode, Yes.updateTs); 141 } 142 143 /** FSM for handling mutants during the test phase. 144 */ 145 struct TimeoutFsm { 146 @safe: 147 148 static struct Init { 149 } 150 151 static struct ResetWorkList { 152 } 153 154 static struct UpdateCtx { 155 } 156 157 static struct Running { 158 } 159 160 static struct Purge { 161 // worklist items and if they have changed or not 162 enum Event { 163 changed, 164 same 165 } 166 167 Event ev; 168 } 169 170 static struct Done { 171 } 172 173 static struct ClearWorkList { 174 } 175 176 static struct Stop { 177 } 178 179 /// Data used by all states. 180 static struct Global { 181 MutantTimeoutCtx ctx; 182 Mutation.Kind[] kinds; 183 Database* db; 184 bool stop; 185 } 186 187 static struct Output { 188 /// The current iteration through the timeout algorithm. 189 long iter; 190 /// When the testing of all timeouts are done, e.g. the state is "done". 191 bool done; 192 } 193 194 /// Output that may be used. 195 Output output; 196 197 private { 198 Fsm!(Init, ResetWorkList, UpdateCtx, Running, Purge, Done, ClearWorkList, Stop) fsm; 199 Global global; 200 } 201 202 this(const Mutation.Kind[] kinds) nothrow { 203 global.kinds = kinds.dup; 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(self.global.kinds, null) 262 .count == 0 && db.worklistApi.getCount == 0; 263 } 264 265 self.fsm.next!((Init a) { 266 if (noUnknown) 267 return fsm(ResetWorkList.init); 268 return fsm(Stop.init); 269 }, (ResetWorkList a) => fsm(UpdateCtx.init), (UpdateCtx a) => fsm(Running.init), (Running a) { 270 if (noUnknown) 271 return fsm(Purge.init); 272 return fsm(Stop.init); 273 }, (Purge a) { 274 final switch (a.ev) with (Purge.Event) { 275 case changed: 276 if (self.global.ctx.iter == MaxTimeoutIterations) { 277 return fsm(ClearWorkList.init); 278 } 279 return fsm(ResetWorkList.init); 280 case same: 281 return fsm(ClearWorkList.init); 282 } 283 }, (ClearWorkList a) => fsm(Done.init), (Done a) { 284 if (noUnknown) 285 return fsm(Stop.init); 286 // happens if an operation is performed that changes the status of 287 // already tested mutants to unknown. 288 return fsm(Running.init); 289 }, (Stop a) => fsm(a),); 290 291 self.fsm.act!self; 292 } 293 294 void opCall(Init) { 295 global.ctx = MutantTimeoutCtx.init; 296 output.done = false; 297 } 298 299 void opCall(ResetWorkList) { 300 global.db.timeoutApi.copyMutantTimeoutWorklist(global.ctx.iter); 301 302 global.db.memOverloadApi.toWorklist; 303 global.db.memOverloadApi.clear; 304 } 305 306 void opCall(UpdateCtx) { 307 global.ctx.iter += 1; 308 global.ctx.worklistCount = global.db.timeoutApi.countMutantTimeoutWorklist; 309 } 310 311 void opCall(Running) { 312 global.ctx.state = MutantTimeoutCtx.State.running; 313 output.done = false; 314 } 315 316 void opCall(ref Purge data) { 317 global.db.timeoutApi.reduceMutantTimeoutWorklist(global.ctx.iter); 318 319 if (global.db.timeoutApi.countMutantTimeoutWorklist == global.ctx.worklistCount) { 320 data.ev = Purge.Event.same; 321 } else { 322 data.ev = Purge.Event.changed; 323 } 324 } 325 326 void opCall(Done) { 327 global.ctx.state = MutantTimeoutCtx.State.done; 328 // must reset in case the mutation testing reach the end and is 329 // restarted with another mutation operator type than previously 330 global.ctx.iter = 0; 331 global.ctx.worklistCount = 0; 332 333 output.done = true; 334 } 335 336 void opCall(ClearWorkList) { 337 global.db.timeoutApi.clearMutantTimeoutWorklist; 338 339 global.db.memOverloadApi.toWorklist; 340 global.db.memOverloadApi.clear; 341 } 342 343 void opCall(Stop) { 344 global.stop = true; 345 } 346 } 347 348 // If the mutants has been tested 2 times it should be good enough. Sometimes 349 // there are so many timeout that it would feel like the tool just end up in an 350 // infinite loop. Maybe this should be moved so it is user configurable in the 351 // future. 352 // The user also have the admin operation stopTimeoutTest to use. 353 immutable MaxTimeoutIterations = 2;