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 /// Reset the state of the timeout algorithm to its inital state. 38 void resetTimeoutContext(ref Database db) @trusted { 39 db.putMutantTimeoutCtx(MutantTimeoutCtx.init); 40 } 41 42 /// Calculate the timeout to use based on the context. 43 std_.datetime.Duration calculateTimeout(const long iter, std_.datetime.Duration base) pure nothrow @nogc { 44 import core.time : dur; 45 import std.math : sqrt; 46 47 static immutable double constant_factor = 1.5; 48 static immutable double scale_factor = 2.0; 49 const double n = iter; 50 51 const double scale = constant_factor + sqrt(n) * scale_factor; 52 return (1L + (cast(long)(base.total!"msecs" * scale))).dur!"msecs"; 53 } 54 55 /** Update the status of a mutant. 56 * 57 * If the mutant is `timeout` then it will be added to the worklist if the 58 * mutation testing is in the initial phase. 59 * 60 * If it has progressed beyond the init phase then it depends on if the local 61 * iteration variable of *this* instance of dextool matches the one in the 62 * database. This ensures that all instances that work on the same database is 63 * in-sync with each other. 64 * 65 * Params: 66 * db = database to use 67 * id = ? 68 * st = ? 69 * usedIter = the `iter` value that was used to test the mutant 70 */ 71 void updateMutantStatus(ref Database db, const MutationStatusId id, 72 const Mutation.Status st, const ExitStatus ecode, const long usedIter) @trusted { 73 import std.typecons : Yes; 74 75 const ctx = db.getMutantTimeoutCtx; 76 77 final switch (ctx.state) with (MutantTimeoutCtx.State) { 78 case init_: 79 if (st == Mutation.Status.timeout) 80 db.putMutantInTimeoutWorklist(id); 81 db.updateMutationStatus(id, st, ecode, Yes.updateTs); 82 break; 83 case running: 84 if (usedIter == ctx.iter) { 85 db.updateMutationStatus(id, st, ecode, Yes.updateTs); 86 } 87 break; 88 case done: 89 break; 90 } 91 } 92 93 /** FSM for handling mutants during the test phase. 94 */ 95 struct TimeoutFsm { 96 @safe: 97 98 static struct Init { 99 } 100 101 static struct ResetWorkList { 102 } 103 104 static struct UpdateCtx { 105 } 106 107 static struct Running { 108 } 109 110 static struct Purge { 111 // worklist items and if they have changed or not 112 enum Event { 113 changed, 114 same 115 } 116 117 Event ev; 118 } 119 120 static struct Done { 121 } 122 123 static struct ClearWorkList { 124 } 125 126 static struct Stop { 127 } 128 129 /// Data used by all states. 130 static struct Global { 131 MutantTimeoutCtx ctx; 132 Mutation.Kind[] kinds; 133 Database* db; 134 bool stop; 135 } 136 137 static struct Output { 138 /// The current iteration through the timeout algorithm. 139 long iter; 140 /// When the testing of all timeouts are done, e.g. the state is "done". 141 bool done; 142 } 143 144 /// Output that may be used. 145 Output output; 146 147 private { 148 Fsm!(Init, ResetWorkList, UpdateCtx, Running, Purge, Done, ClearWorkList, Stop) fsm; 149 Global global; 150 } 151 152 this(const Mutation.Kind[] kinds) nothrow { 153 global.kinds = kinds.dup; 154 try { 155 if (logger.globalLogLevel == logger.LogLevel.trace) 156 fsm.logger = (string s) { logger.trace(s); }; 157 } catch (Exception e) { 158 logger.trace(e.msg).collectException; 159 } 160 } 161 162 void execute(ref Database db) @trusted { 163 execute_(this, &db); 164 } 165 166 static void execute_(ref TimeoutFsm self, Database* db) @trusted { 167 self.global.db = db; 168 // must always run the loop at least once. 169 self.global.stop = false; 170 171 auto t = db.transaction; 172 self.global.ctx = db.getMutantTimeoutCtx; 173 174 // force the local state to match the starting point in the ctx 175 // (database). 176 final switch (self.global.ctx.state) with (MutantTimeoutCtx) { 177 case State.init_: 178 self.fsm.state = fsm(Init.init); 179 break; 180 case State.running: 181 self.fsm.state = fsm(Running.init); 182 break; 183 case State.done: 184 self.fsm.state = fsm(Done.init); 185 break; 186 } 187 188 // act on the inital state 189 try { 190 self.fsm.act!self; 191 } catch (Exception e) { 192 logger.warning(e.msg).collectException; 193 } 194 195 while (!self.global.stop) { 196 try { 197 step(self, *db); 198 } catch (Exception e) { 199 logger.warning(e.msg).collectException; 200 } 201 } 202 203 db.putMutantTimeoutCtx(self.global.ctx); 204 t.commit; 205 206 self.output.iter = self.global.ctx.iter; 207 } 208 209 private static void step(ref TimeoutFsm self, ref Database db) @safe { 210 bool noUnknown() { 211 return db.unknownSrcMutants(self.global.kinds, null).count == 0 212 && db.getWorklistCount == 0; 213 } 214 215 self.fsm.next!((Init a) { 216 if (noUnknown) 217 return fsm(ResetWorkList.init); 218 return fsm(Stop.init); 219 }, (ResetWorkList a) => fsm(UpdateCtx.init), (UpdateCtx a) => fsm(Running.init), (Running a) { 220 if (noUnknown) 221 return fsm(Purge.init); 222 return fsm(Stop.init); 223 }, (Purge a) { 224 final switch (a.ev) with (Purge.Event) { 225 case changed: 226 if (self.global.ctx.iter == MaxTimeoutIterations) { 227 return fsm(ClearWorkList.init); 228 } 229 return fsm(ResetWorkList.init); 230 case same: 231 return fsm(ClearWorkList.init); 232 } 233 }, (ClearWorkList a) => fsm(Done.init), (Done a) { 234 if (noUnknown) 235 return fsm(Stop.init); 236 // happens if an operation is performed that changes the status of 237 // already tested mutants to unknown. 238 return fsm(Running.init); 239 }, (Stop a) => fsm(a),); 240 241 self.fsm.act!self; 242 } 243 244 void opCall(Init) { 245 global.ctx = MutantTimeoutCtx.init; 246 output.done = false; 247 } 248 249 void opCall(ResetWorkList) { 250 global.db.copyMutantTimeoutWorklist; 251 } 252 253 void opCall(UpdateCtx) { 254 global.ctx.iter += 1; 255 global.ctx.worklistCount = global.db.countMutantTimeoutWorklist; 256 } 257 258 void opCall(Running) { 259 global.ctx.state = MutantTimeoutCtx.State.running; 260 output.done = false; 261 } 262 263 void opCall(ref Purge data) { 264 global.db.reduceMutantTimeoutWorklist; 265 266 if (global.db.countMutantTimeoutWorklist == global.ctx.worklistCount) { 267 data.ev = Purge.Event.same; 268 } else { 269 data.ev = Purge.Event.changed; 270 } 271 } 272 273 void opCall(Done) { 274 global.ctx.state = MutantTimeoutCtx.State.done; 275 // must reset in case the mutation testing reach the end and is 276 // restarted with another mutation operator type than previously 277 global.ctx.iter = 0; 278 global.ctx.worklistCount = 0; 279 280 output.done = true; 281 } 282 283 void opCall(ClearWorkList) { 284 global.db.clearMutantTimeoutWorklist; 285 } 286 287 void opCall(Stop) { 288 global.stop = true; 289 } 290 } 291 292 // If the mutants has been tested 2 times it should be good enough. Sometimes 293 // there are so many timeout that it would feel like the tool just end up in an 294 // infinite loop. Maybe this should be moved so it is user configurable in the 295 // future. 296 // The user also have the admin operation stopTimeoutTest to use. 297 immutable MaxTimeoutIterations = 2;