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