X-Git-Url: https://git.stg.codes/stg.git/blobdiff_plain/b084087ede50ac90d2493b6192fec9b7342e30bf..3156083fd0c328d46be22536720ae33e1ab48090:/tests/tut/tut_restartable.hpp diff --git a/tests/tut/tut_restartable.hpp b/tests/tut/tut_restartable.hpp new file mode 100644 index 00000000..eb6eb00e --- /dev/null +++ b/tests/tut/tut_restartable.hpp @@ -0,0 +1,409 @@ +#ifndef TUT_RESTARTABLE_H_GUARD +#define TUT_RESTARTABLE_H_GUARD + +#include +#include +#include +#include +#include + +/** + * Optional restartable wrapper for test_runner. + * + * Allows to restart test runs finished due to abnormal + * test application termination (such as segmentation + * fault or math error). + * + * @author Vladimir Dyuzhev, Vladimir.Dyuzhev@gmail.com + */ + +namespace tut +{ + +namespace util +{ + +/** + * Escapes non-alphabetical characters in string. + */ +std::string escape(const std::string& orig) +{ + std::string rc; + std::string::const_iterator i,e; + i = orig.begin(); + e = orig.end(); + + while (i != e) + { + if ((*i >= 'a' && *i <= 'z') || + (*i >= 'A' && *i <= 'Z') || + (*i >= '0' && *i <= '9') ) + { + rc += *i; + } + else + { + rc += '\\'; + rc += ('a'+(((unsigned int)*i) >> 4)); + rc += ('a'+(((unsigned int)*i) & 0xF)); + } + + ++i; + } + return rc; +} + +/** + * Un-escapes string. + */ +std::string unescape(const std::string& orig) +{ + std::string rc; + std::string::const_iterator i,e; + i = orig.begin(); + e = orig.end(); + + while (i != e) + { + if (*i != '\\') + { + rc += *i; + } + else + { + ++i; + if (i == e) + { + throw std::invalid_argument("unexpected end of string"); + } + unsigned int c1 = *i; + ++i; + if (i == e) + { + throw std::invalid_argument("unexpected end of string"); + } + unsigned int c2 = *i; + rc += (((c1 - 'a') << 4) + (c2 - 'a')); + } + + ++i; + } + return rc; +} + +/** + * Serialize test_result avoiding interfering with operator <<. + */ +void serialize(std::ostream& os, const tut::test_result& tr) +{ + os << escape(tr.group) << std::endl; + os << tr.test << ' '; + switch(tr.result) + { + case test_result::ok: + os << 0; + break; + case test_result::fail: + os << 1; + break; + case test_result::ex: + os << 2; + break; + case test_result::warn: + os << 3; + break; + case test_result::term: + os << 4; + break; + case test_result::rethrown: + os << 5; + break; + case test_result::ex_ctor: + os << 6; + break; + case test_result::dummy: + assert(!"Should never be called"); + default: + throw std::logic_error("operator << : bad result_type"); + } + os << ' ' << escape(tr.message) << std::endl; +} + +/** + * deserialization for test_result + */ +bool deserialize(std::istream& is, tut::test_result& tr) +{ + std::getline(is,tr.group); + if (is.eof()) + { + return false; + } + tr.group = unescape(tr.group); + + tr.test = -1; + is >> tr.test; + if (tr.test < 0) + { + throw std::logic_error("operator >> : bad test number"); + } + + int n = -1; + is >> n; + switch(n) + { + case 0: + tr.result = test_result::ok; + break; + case 1: + tr.result = test_result::fail; + break; + case 2: + tr.result = test_result::ex; + break; + case 3: + tr.result = test_result::warn; + break; + case 4: + tr.result = test_result::term; + break; + case 5: + tr.result = test_result::rethrown; + break; + case 6: + tr.result = test_result::ex_ctor; + break; + default: + throw std::logic_error("operator >> : bad result_type"); + } + + is.ignore(1); // space + std::getline(is,tr.message); + tr.message = unescape(tr.message); + if (!is.good()) + { + throw std::logic_error("malformed test result"); + } + return true; +} +}; + +/** + * Restartable test runner wrapper. + */ +class restartable_wrapper +{ + test_runner& runner_; + callbacks callbacks_; + + std::string dir_; + std::string log_; // log file: last test being executed + std::string jrn_; // journal file: results of all executed tests + +public: + /** + * Default constructor. + * @param dir Directory where to search/put log and journal files + */ + restartable_wrapper(const std::string& dir = ".") + : runner_(runner.get()), + dir_(dir) + { + // dozen: it works, but it would be better to use system path separator + jrn_ = dir_ + '/' + "journal.tut"; + log_ = dir_ + '/' + "log.tut"; + } + + /** + * Stores another group for getting by name. + */ + void register_group(const std::string& name, group_base* gr) + { + runner_.register_group(name,gr); + } + + /** + * Stores callback object. + */ + void set_callback(callback* cb) + { + callbacks_.clear(); + callbacks_.insert(cb); + } + + void insert_callback(callback* cb) + { + callbacks_.insert(cb); + } + + void erase_callback(callback* cb) + { + callbacks_.erase(cb); + } + + void set_callbacks(const callbacks& cb) + { + callbacks_ = cb; + } + + const callbacks& get_callbacks() const + { + return runner_.get_callbacks(); + } + + /** + * Returns list of known test groups. + */ + groupnames list_groups() const + { + return runner_.list_groups(); + } + + /** + * Runs all tests in all groups. + */ + void run_tests() const + { + // where last run was failed + std::string fail_group; + int fail_test; + read_log_(fail_group,fail_test); + bool fail_group_reached = (fail_group == ""); + + // iterate over groups + tut::groupnames gn = list_groups(); + tut::groupnames::const_iterator gni,gne; + gni = gn.begin(); + gne = gn.end(); + while (gni != gne) + { + // skip all groups before one that failed + if (!fail_group_reached) + { + if (*gni != fail_group) + { + ++gni; + continue; + } + fail_group_reached = true; + } + + // first or restarted run + int test = (*gni == fail_group && fail_test >= 0) ? fail_test + 1 : 1; + while(true) + { + // last executed test pos + register_execution_(*gni,test); + + tut::test_result tr; + if( !runner_.run_test(*gni,test, tr) || tr.result == test_result::dummy ) + { + break; + } + register_test_(tr); + + ++test; + } + + ++gni; + } + + // show final results to user + invoke_callback_(); + + // truncate files as mark of successful finish + truncate_(); + } + +private: + /** + * Shows results from journal file. + */ + void invoke_callback_() const + { + runner_.set_callbacks(callbacks_); + runner_.cb_run_started_(); + + std::string current_group; + std::ifstream ijournal(jrn_.c_str()); + while (ijournal.good()) + { + tut::test_result tr; + if( !util::deserialize(ijournal,tr) ) + { + break; + } + runner_.cb_test_completed_(tr); + } + + runner_.cb_run_completed_(); + } + + /** + * Register test into journal. + */ + void register_test_(const test_result& tr) const + { + std::ofstream ojournal(jrn_.c_str(), std::ios::app); + util::serialize(ojournal, tr); + ojournal << std::flush; + if (!ojournal.good()) + { + throw std::runtime_error("unable to register test result in file " + + jrn_); + } + } + + /** + * Mark the fact test going to be executed + */ + void register_execution_(const std::string& grp, int test) const + { + // last executed test pos + std::ofstream olog(log_.c_str()); + olog << util::escape(grp) << std::endl << test << std::endl << std::flush; + if (!olog.good()) + { + throw std::runtime_error("unable to register execution in file " + + log_); + } + } + + /** + * Truncate tests. + */ + void truncate_() const + { + std::ofstream olog(log_.c_str()); + std::ofstream ojournal(jrn_.c_str()); + } + + /** + * Read log file + */ + void read_log_(std::string& fail_group, int& fail_test) const + { + // read failure point, if any + std::ifstream ilog(log_.c_str()); + std::getline(ilog,fail_group); + fail_group = util::unescape(fail_group); + ilog >> fail_test; + if (!ilog.good()) + { + fail_group = ""; + fail_test = -1; + truncate_(); + } + else + { + // test was terminated... + tut::test_result tr(fail_group, fail_test, "", tut::test_result::term); + register_test_(tr); + } + } +}; + +} + +#endif +