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