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;