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; 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 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, Yes.updateTs); 82 break; 83 case running: 84 if (usedIter == ctx.iter) { 85 db.updateMutationStatus(id, st, 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 } 155 156 void execute(ref Database db) @trusted { 157 execute_(this, &db); 158 } 159 160 static void execute_(ref TimeoutFsm self, Database* db) @trusted { 161 self.global.db = db; 162 // must always run the loop at least once. 163 self.global.stop = false; 164 165 auto t = db.transaction; 166 self.global.ctx = db.getMutantTimeoutCtx; 167 168 // force the local state to match the starting point in the ctx 169 // (database). 170 final switch (self.global.ctx.state) with (MutantTimeoutCtx) { 171 case State.init_: 172 self.fsm.state = fsm(Init.init); 173 break; 174 case State.running: 175 self.fsm.state = fsm(Running.init); 176 break; 177 case State.done: 178 self.fsm.state = fsm(Done.init); 179 break; 180 } 181 182 // act on the inital state 183 try { 184 self.fsm.act!self; 185 } catch (Exception e) { 186 logger.warning(e.msg).collectException; 187 } 188 189 while (!self.global.stop) { 190 try { 191 step(self, *db); 192 } catch (Exception e) { 193 logger.warning(e.msg).collectException; 194 } 195 } 196 197 db.putMutantTimeoutCtx(self.global.ctx); 198 t.commit; 199 200 self.output.iter = self.global.ctx.iter; 201 } 202 203 private static void step(ref TimeoutFsm self, ref Database db) @safe { 204 bool noUnknown() { 205 return db.unknownSrcMutants(self.global.kinds, null).count == 0; 206 } 207 208 self.fsm.next!((Init a) { 209 if (noUnknown) 210 return fsm(ResetWorkList.init); 211 return fsm(Stop.init); 212 }, (ResetWorkList a) => fsm(UpdateCtx.init), (UpdateCtx a) => fsm(Running.init), (Running a) { 213 if (noUnknown) 214 return fsm(Purge.init); 215 return fsm(Stop.init); 216 }, (Purge a) { 217 final switch (a.ev) with (Purge.Event) { 218 case changed: 219 if (self.global.ctx.iter == MaxTimeoutIterations) { 220 return fsm(ClearWorkList.init); 221 } 222 return fsm(ResetWorkList.init); 223 case same: 224 return fsm(ClearWorkList.init); 225 } 226 }, (ClearWorkList a) => fsm(Done.init), (Done a) { 227 if (noUnknown) 228 return fsm(Stop.init); 229 // happens if an operation is performed that changes the status of 230 // already tested mutants to unknown. 231 return fsm(Running.init); 232 }, (Stop a) => fsm(a),); 233 234 self.fsm.act!self; 235 } 236 237 void opCall(Init) { 238 global.ctx = MutantTimeoutCtx.init; 239 output.done = false; 240 } 241 242 void opCall(ResetWorkList) { 243 global.db.resetMutantTimeoutWorklist(Mutation.Status.unknown); 244 } 245 246 void opCall(UpdateCtx) { 247 global.ctx.iter += 1; 248 global.ctx.worklistCount = global.db.countMutantTimeoutWorklist; 249 } 250 251 void opCall(Running) { 252 global.ctx.state = MutantTimeoutCtx.State.running; 253 output.done = false; 254 } 255 256 void opCall(ref Purge data) { 257 global.db.reduceMutantTimeoutWorklist; 258 259 if (global.db.countMutantTimeoutWorklist == global.ctx.worklistCount) 260 data.ev = Purge.Event.same; 261 else 262 data.ev = Purge.Event.changed; 263 } 264 265 void opCall(Done) { 266 global.ctx.state = MutantTimeoutCtx.State.done; 267 // must reset in case the mutation testing reach the end and is 268 // restarted with another mutation operator type than previously 269 global.ctx.iter = 0; 270 global.ctx.worklistCount = 0; 271 272 output.done = true; 273 } 274 275 void opCall(ClearWorkList) { 276 global.db.clearMutantTimeoutWorklist; 277 } 278 279 void opCall(Stop) { 280 global.stop = true; 281 } 282 } 283 284 // If the mutants has been tested 2 times it should be good enough. Sometimes 285 // there are so many timeout that it would feel like the tool just end up in an 286 // infinite loop. Maybe this should be moved so it is user configurable in the 287 // future. 288 // The user also have the admin operation stopTimeoutTest to use. 289 immutable MaxTimeoutIterations = 2;