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