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 
30 import dextool.from;
31 import dextool.fsm;
32 
33 import dextool.plugin.mutate.backend.database : Database, MutantTimeoutCtx, MutationStatusId;
34 import dextool.plugin.mutate.backend.type : Mutation;
35 
36 @safe:
37 
38 /// Reset the state of the timeout algorithm to its inital state.
39 void resetTimeoutContext(ref Database db) @trusted {
40     db.putMutantTimeoutCtx(MutantTimeoutCtx.init);
41 }
42 
43 /// Calculate the timeout to use based on the context.
44 std_.datetime.Duration calculateTimeout(const long iter, std_.datetime.Duration base) pure nothrow @nogc {
45     import core.time : dur;
46     import std.math : sqrt;
47 
48     static immutable double constant_factor = 1.5;
49     static immutable double scale_factor = 2.0;
50     const double n = iter;
51 
52     const double scale = constant_factor + sqrt(n) * scale_factor;
53     return (1L + (cast(long)(base.total!"msecs" * scale))).dur!"msecs";
54 }
55 
56 /** Update the status of a mutant.
57  *
58  * If the mutant is `timeout` then it will be added to the worklist if the
59  * mutation testing is in the initial phase.
60  *
61  * If it has progressed beyond the init phase then it depends on if the local
62  * iteration variable of *this* instance of dextool matches the one in the
63  * database. This ensures that all instances that work on the same database is
64  * in-sync with each other.
65  *
66  * Params:
67  *  db = database to use
68  *  id = ?
69  *  st = ?
70  *  usedIter = the `iter` value that was used to test the mutant
71  */
72 void updateMutantStatus(ref Database db, const MutationStatusId id,
73         const Mutation.Status st, const long usedIter) @trusted {
74     import std.typecons : Yes;
75 
76     const ctx = db.getMutantTimeoutCtx;
77 
78     final switch (ctx.state) with (MutantTimeoutCtx.State) {
79     case init_:
80         if (st == Mutation.Status.timeout)
81             db.putMutantInTimeoutWorklist(id);
82         db.updateMutationStatus(id, st, Yes.updateTs);
83         break;
84     case running:
85         if (usedIter == ctx.iter) {
86             db.updateMutationStatus(id, st, Yes.updateTs);
87         }
88         break;
89     case done:
90         break;
91     }
92 }
93 
94 /** FSM for handling mutants during the test phase.
95  */
96 struct TimeoutFsm {
97 @safe:
98 
99     static struct Init {
100     }
101 
102     static struct ResetWorkList {
103     }
104 
105     static struct UpdateCtx {
106     }
107 
108     static struct Running {
109     }
110 
111     static struct Purge {
112         // worklist items and if they have changed or not
113         enum Event {
114             changed,
115             same
116         }
117 
118         Event ev;
119     }
120 
121     static struct Done {
122     }
123 
124     static struct ClearWorkList {
125     }
126 
127     static struct Stop {
128     }
129 
130     /// Data used by all states.
131     static struct Global {
132         MutantTimeoutCtx ctx;
133         Mutation.Kind[] kinds;
134         Database* db;
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 
163         auto t = db.transaction;
164         self.global.ctx = db.getMutantTimeoutCtx;
165 
166         // force the local state to match the starting point in the ctx
167         // (database).
168         final switch (self.global.ctx.state) with (MutantTimeoutCtx) {
169         case State.init_:
170             self.fsm.state = fsm(Init.init);
171             break;
172         case State.running:
173             self.fsm.state = fsm(Running.init);
174             break;
175         case State.done:
176             self.fsm.state = fsm(Done.init);
177             break;
178         }
179 
180         // act on the inital state
181         try {
182             self.fsm.act!self;
183         } catch (Exception e) {
184             logger.warning(e.msg).collectException;
185         }
186 
187         while (!self.fsm.isState!Stop) {
188             try {
189                 step(self, *db);
190             } catch (Exception e) {
191                 logger.warning(e.msg).collectException;
192             }
193         }
194 
195         db.putMutantTimeoutCtx(self.global.ctx);
196         t.commit;
197 
198         self.output.iter = self.global.ctx.iter;
199     }
200 
201     private static void step(ref TimeoutFsm self, ref Database db) @safe {
202         bool noUnknown() {
203             return db.unknownSrcMutants(self.global.kinds, null).count == 0;
204         }
205 
206         self.fsm.next!((Init a) {
207             if (noUnknown)
208                 return fsm(ResetWorkList.init);
209             return fsm(Stop.init);
210         }, (ResetWorkList a) => fsm(UpdateCtx.init), (UpdateCtx a) => fsm(Running.init), (Running a) {
211             if (noUnknown)
212                 return fsm(Purge.init);
213             return fsm(Stop.init);
214         }, (Purge a) {
215             final switch (a.ev) with (Purge.Event) {
216             case changed:
217                 if (self.global.ctx.iter == MaxTimeoutIterations) {
218                     return fsm(ClearWorkList.init);
219                 }
220                 return fsm(ResetWorkList.init);
221             case same:
222                 return fsm(ClearWorkList.init);
223             }
224         }, (ClearWorkList a) => fsm(Done.init), (Done a) {
225             if (noUnknown)
226                 return fsm(Stop.init);
227             // happens if an operation is performed that changes the status of
228             // already tested mutants to unknown.
229             return fsm(Running.init);
230         }, (Stop a) => fsm(a),);
231 
232         self.fsm.act!self;
233     }
234 
235     void opCall(Init) {
236         global.ctx = MutantTimeoutCtx.init;
237         output.done = false;
238     }
239 
240     void opCall(ResetWorkList) {
241         global.db.resetMutantTimeoutWorklist;
242     }
243 
244     void opCall(UpdateCtx) {
245         global.ctx.iter += 1;
246         global.ctx.worklistCount = global.db.countMutantTimeoutWorklist;
247     }
248 
249     void opCall(Running) {
250         global.ctx.state = MutantTimeoutCtx.State.running;
251         output.done = false;
252     }
253 
254     void opCall(ref Purge data) {
255         global.db.reduceMutantTimeoutWorklist;
256 
257         if (global.db.countMutantTimeoutWorklist == global.ctx.worklistCount)
258             data.ev = Purge.Event.same;
259         else
260             data.ev = Purge.Event.changed;
261     }
262 
263     void opCall(Done) {
264         global.ctx.state = MutantTimeoutCtx.State.done;
265         output.done = true;
266     }
267 
268     void opCall(ClearWorkList) {
269         global.db.clearMutantTimeoutWorklist;
270     }
271 
272     void opCall(Stop) {
273     }
274 }
275 
276 private:
277 
278 // If the mutants has been tested 3 times it should be good enough. Sometimes
279 // there are so many timeout that it would feel like the tool just end up in an
280 // infinite loop. Maybe this should be moved so it is user configurable in the
281 // future.
282 immutable MaxTimeoutIterations = 3;