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;
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 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, Yes.updateTs);
82         break;
83     case running:
84         if (usedIter == ctx.iter) {
85             db.updateMutationStatus(id, st, 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     }
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         // must always run the loop at least once.
163         self.global.stop = false;
164 
165         auto t = db.transaction;
166         self.global.ctx = db.getMutantTimeoutCtx;
167 
168         // force the local state to match the starting point in the ctx
169         // (database).
170         final switch (self.global.ctx.state) with (MutantTimeoutCtx) {
171         case State.init_:
172             self.fsm.state = fsm(Init.init);
173             break;
174         case State.running:
175             self.fsm.state = fsm(Running.init);
176             break;
177         case State.done:
178             self.fsm.state = fsm(Done.init);
179             break;
180         }
181 
182         // act on the inital state
183         try {
184             self.fsm.act!self;
185         } catch (Exception e) {
186             logger.warning(e.msg).collectException;
187         }
188 
189         while (!self.global.stop) {
190             try {
191                 step(self, *db);
192             } catch (Exception e) {
193                 logger.warning(e.msg).collectException;
194             }
195         }
196 
197         db.putMutantTimeoutCtx(self.global.ctx);
198         t.commit;
199 
200         self.output.iter = self.global.ctx.iter;
201     }
202 
203     private static void step(ref TimeoutFsm self, ref Database db) @safe {
204         bool noUnknown() {
205             return db.unknownSrcMutants(self.global.kinds, null).count == 0;
206         }
207 
208         self.fsm.next!((Init a) {
209             if (noUnknown)
210                 return fsm(ResetWorkList.init);
211             return fsm(Stop.init);
212         }, (ResetWorkList a) => fsm(UpdateCtx.init), (UpdateCtx a) => fsm(Running.init), (Running a) {
213             if (noUnknown)
214                 return fsm(Purge.init);
215             return fsm(Stop.init);
216         }, (Purge a) {
217             final switch (a.ev) with (Purge.Event) {
218             case changed:
219                 if (self.global.ctx.iter == MaxTimeoutIterations) {
220                     return fsm(ClearWorkList.init);
221                 }
222                 return fsm(ResetWorkList.init);
223             case same:
224                 return fsm(ClearWorkList.init);
225             }
226         }, (ClearWorkList a) => fsm(Done.init), (Done a) {
227             if (noUnknown)
228                 return fsm(Stop.init);
229             // happens if an operation is performed that changes the status of
230             // already tested mutants to unknown.
231             return fsm(Running.init);
232         }, (Stop a) => fsm(a),);
233 
234         self.fsm.act!self;
235     }
236 
237     void opCall(Init) {
238         global.ctx = MutantTimeoutCtx.init;
239         output.done = false;
240     }
241 
242     void opCall(ResetWorkList) {
243         global.db.resetMutantTimeoutWorklist(Mutation.Status.unknown);
244     }
245 
246     void opCall(UpdateCtx) {
247         global.ctx.iter += 1;
248         global.ctx.worklistCount = global.db.countMutantTimeoutWorklist;
249     }
250 
251     void opCall(Running) {
252         global.ctx.state = MutantTimeoutCtx.State.running;
253         output.done = false;
254     }
255 
256     void opCall(ref Purge data) {
257         global.db.reduceMutantTimeoutWorklist;
258 
259         if (global.db.countMutantTimeoutWorklist == global.ctx.worklistCount)
260             data.ev = Purge.Event.same;
261         else
262             data.ev = Purge.Event.changed;
263     }
264 
265     void opCall(Done) {
266         global.ctx.state = MutantTimeoutCtx.State.done;
267         // must reset in case the mutation testing reach the end and is
268         // restarted with another mutation operator type than previously
269         global.ctx.iter = 0;
270         global.ctx.worklistCount = 0;
271 
272         output.done = true;
273     }
274 
275     void opCall(ClearWorkList) {
276         global.db.clearMutantTimeoutWorklist;
277     }
278 
279     void opCall(Stop) {
280         global.stop = true;
281     }
282 }
283 
284 // If the mutants has been tested 2 times it should be good enough. Sometimes
285 // there are so many timeout that it would feel like the tool just end up in an
286 // infinite loop. Maybe this should be moved so it is user configurable in the
287 // future.
288 // The user also have the admin operation stopTimeoutTest to use.
289 immutable MaxTimeoutIterations = 2;