1 #ifndef TUT_RESTARTABLE_H_GUARD
2 #define TUT_RESTARTABLE_H_GUARD
3 
4 #include <tut/tut.hpp>
5 #include <fstream>
6 #include <iostream>
7 #include <stdexcept>
8 
9 /**
10  * Template Unit Tests Framework for C++.
11  * http://tut.dozen.ru
12  *
13  * Optional restartable wrapper for test_runner. Allows to restart test runs
14  * finished due to abnormal test application termination (such as segmentation
15  * fault or math error).
16  *
17  * @author Vladimir Dyuzhev, Vladimir.Dyuzhev@gmail.com
18  */
19 
20 namespace tut
21 {
22 
23 namespace util
24 {
25 
26 /**
27  * Escapes non-alphabetical characters in string.
28  */
escape(const std::string & orig)29 std::string escape(const std::string& orig)
30 {
31     std::string rc;
32     std::string::const_iterator i,e;
33     i = orig.begin();
34     e = orig.end();
35 
36     while (i != e)
37     {
38         if ((*i >= 'a' && *i <= 'z') ||
39                 (*i >= 'A' && *i <= 'Z') ||
40                 (*i >= '0' && *i <= '9') )
41         {
42             rc += *i;
43         }
44         else
45         {
46             rc += '\\';
47             rc += ('a'+(((unsigned int)*i) >> 4));
48             rc += ('a'+(((unsigned int)*i) & 0xF));
49         }
50 
51         ++i;
52     }
53     return rc;
54 }
55 
56 /**
57  * Un-escapes string.
58  */
unescape(const std::string & orig)59 std::string unescape(const std::string& orig)
60 {
61     std::string rc;
62     std::string::const_iterator i,e;
63     i = orig.begin();
64     e = orig.end();
65 
66     while (i != e)
67     {
68         if (*i != '\\')
69         {
70             rc += *i;
71         }
72         else
73         {
74             ++i;
75             if (i == e)
76             {
77                 throw std::invalid_argument("unexpected end of string");
78             }
79             unsigned int c1 = *i;
80             ++i;
81             if (i == e)
82             {
83                 throw std::invalid_argument("unexpected end of string");
84             }
85             unsigned int c2 = *i;
86             rc += (((c1 - 'a') << 4) + (c2 - 'a'));
87         }
88 
89         ++i;
90     }
91     return rc;
92 }
93 
94 /**
95  * Serialize test_result avoiding interfering with operator <<.
96  */
serialize(std::ostream & os,const tut::test_result & tr)97 void serialize(std::ostream& os, const tut::test_result& tr)
98 {
99     os << escape(tr.group) << std::endl;
100     os << tr.test << ' ';
101     switch(tr.result)
102     {
103     case test_result::ok:
104         os << 0;
105         break;
106     case test_result::fail:
107         os << 1;
108         break;
109     case test_result::ex:
110         os << 2;
111         break;
112     case test_result::warn:
113         os << 3;
114         break;
115     case test_result::term:
116         os << 4;
117         break;
118     default:
119         throw std::logic_error("operator << : bad result_type");
120     }
121     os << ' ' << escape(tr.message) << std::endl;
122 }
123 
124 /**
125  * deserialization for test_result
126  */
deserialize(std::istream & is,tut::test_result & tr)127 void deserialize(std::istream& is, tut::test_result& tr)
128 {
129     std::getline(is,tr.group);
130     if (is.eof())
131     {
132         throw tut::no_more_tests();
133     }
134     tr.group = unescape(tr.group);
135 
136     tr.test = -1;
137     is >> tr.test;
138     if (tr.test < 0)
139     {
140         throw std::logic_error("operator >> : bad test number");
141     }
142 
143     int n = -1;
144     is >> n;
145     switch(n)
146     {
147     case 0:
148         tr.result = test_result::ok;
149         break;
150     case 1:
151         tr.result = test_result::fail;
152         break;
153     case 2:
154         tr.result = test_result::ex;
155         break;
156     case 3:
157         tr.result = test_result::warn;
158         break;
159     case 4:
160         tr.result = test_result::term;
161         break;
162     default:
163         throw std::logic_error("operator >> : bad result_type");
164     }
165 
166     is.ignore(1); // space
167     std::getline(is,tr.message);
168     tr.message = unescape(tr.message);
169     if (!is.good())
170     {
171         throw std::logic_error("malformed test result");
172     }
173 }
174 };
175 
176 /**
177  * Restartable test runner wrapper.
178  */
179 class restartable_wrapper
180 {
181     test_runner& runner_;
182     callback* callback_;
183 
184     std::string dir_;
185     std::string log_; // log file: last test being executed
186     std::string jrn_; // journal file: results of all executed tests
187 
188 public:
189     /**
190      * Default constructor.
191      * @param dir Directory where to search/put log and journal files
192      */
restartable_wrapper(const std::string & dir=".")193     restartable_wrapper(const std::string& dir = ".")
194         : runner_(runner.get()),
195           callback_(0),
196           dir_(dir)
197     {
198         // dozen: it works, but it would be better to use system path separator
199         jrn_ = dir_ + '/' + "journal.tut";
200         log_ = dir_ + '/' + "log.tut";
201     }
202 
203     /**
204      * Stores another group for getting by name.
205      */
register_group(const std::string & name,group_base * gr)206     void register_group(const std::string& name, group_base* gr)
207     {
208         runner_.register_group(name,gr);
209     }
210 
211     /**
212      * Stores callback object.
213      */
set_callback(callback * cb)214     void set_callback(callback* cb)
215     {
216         callback_ = cb;
217     }
218 
219     /**
220      * Returns callback object.
221      */
get_callback() const222     callback& get_callback() const
223     {
224         return runner_.get_callback();
225     }
226 
227     /**
228      * Returns list of known test groups.
229      */
list_groups() const230     groupnames list_groups() const
231     {
232         return runner_.list_groups();
233     }
234 
235     /**
236      * Runs all tests in all groups.
237      */
run_tests() const238     void run_tests() const
239     {
240         // where last run was failed
241         std::string fail_group;
242         int fail_test;
243         read_log_(fail_group,fail_test);
244         bool fail_group_reached = (fail_group == "");
245 
246         // iterate over groups
247         tut::groupnames gn = list_groups();
248         tut::groupnames::const_iterator gni,gne;
249         gni = gn.begin();
250         gne = gn.end();
251         while (gni != gne)
252         {
253             // skip all groups before one that failed
254             if (!fail_group_reached)
255             {
256                 if (*gni != fail_group)
257                 {
258                     ++gni;
259                     continue;
260                 }
261                 fail_group_reached = true;
262             }
263 
264             // first or restarted run
265             int test = (*gni == fail_group && fail_test >= 0) ? fail_test + 1 : 1;
266             while(true)
267             {
268                 // last executed test pos
269                 register_execution_(*gni,test);
270 
271                 try
272                 {
273                     tut::test_result tr = runner_.run_test(*gni,test);
274                     register_test_(tr);
275                 }
276                 catch (const tut::beyond_last_test&)
277                 {
278                     break;
279                 }
280                 catch(const tut::no_such_test&)
281                 {
282                     // it's ok
283                 }
284 
285                 ++test;
286             }
287 
288             ++gni;
289         }
290 
291         // show final results to user
292         invoke_callback_();
293 
294         // truncate files as mark of successful finish
295         truncate_();
296     }
297 
298 private:
299     /**
300      * Shows results from journal file.
301      */
invoke_callback_() const302     void invoke_callback_() const
303     {
304         runner_.set_callback(callback_);
305         runner_.get_callback().run_started();
306 
307         std::string current_group;
308         std::ifstream ijournal(jrn_.c_str());
309         while (ijournal.good())
310         {
311             // read next test result
312             try
313             {
314                 tut::test_result tr;
315                 util::deserialize(ijournal,tr);
316                 runner_.get_callback().test_completed(tr);
317             }
318             catch (const no_more_tests&)
319             {
320                 break;
321             }
322         }
323 
324         runner_.get_callback().run_completed();
325     }
326 
327     /**
328      * Register test into journal.
329      */
register_test_(const test_result & tr) const330     void register_test_(const test_result& tr) const
331     {
332         std::ofstream ojournal(jrn_.c_str(), std::ios::app);
333         util::serialize(ojournal, tr);
334         ojournal << std::flush;
335         if (!ojournal.good())
336         {
337             throw std::runtime_error("unable to register test result in file "
338                 + jrn_);
339         }
340     }
341 
342     /**
343      * Mark the fact test going to be executed
344      */
register_execution_(const std::string & grp,int test) const345     void register_execution_(const std::string& grp, int test) const
346     {
347         // last executed test pos
348         std::ofstream olog(log_.c_str());
349         olog << util::escape(grp) << std::endl << test << std::endl << std::flush;
350         if (!olog.good())
351         {
352             throw std::runtime_error("unable to register execution in file "
353                 + log_);
354         }
355     }
356 
357     /**
358      * Truncate tests.
359      */
truncate_() const360     void truncate_() const
361     {
362         std::ofstream olog(log_.c_str());
363         std::ofstream ojournal(jrn_.c_str());
364     }
365 
366     /**
367      * Read log file
368      */
read_log_(std::string & fail_group,int & fail_test) const369     void read_log_(std::string& fail_group, int& fail_test) const
370     {
371         // read failure point, if any
372         std::ifstream ilog(log_.c_str());
373         std::getline(ilog,fail_group);
374         fail_group = util::unescape(fail_group);
375         ilog >> fail_test;
376         if (!ilog.good())
377         {
378             fail_group = "";
379             fail_test = -1;
380             truncate_();
381         }
382         else
383         {
384             // test was terminated...
385             tut::test_result tr(fail_group, fail_test, "", tut::test_result::term);
386             register_test_(tr);
387         }
388     }
389 };
390 
391 }
392 
393 #endif
394 
395