#ifndef TUT_RESTARTABLE_H_GUARD #define TUT_RESTARTABLE_H_GUARD #include "tut.hpp" #include #include #include /** * Template Unit Tests Framework for C++. * http://tut.dozen.ru * * 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; default: throw std::logic_error("operator << : bad result_type"); } os << ' ' << escape(tr.message) << std::endl; } /** * deserialization for test_result */ void deserialize(std::istream& is, tut::test_result& tr) { std::getline(is,tr.group); if (is.eof()) { throw tut::no_more_tests(); } 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; 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"); } } }; /** * Restartable test runner wrapper. */ class restartable_wrapper { test_runner& runner_; callback* callback_; 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()), callback_(0), 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) { callback_ = cb; } /** * Returns callback object. */ callback& get_callback() const { return runner_.get_callback(); } /** * 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); try { tut::test_result tr = runner_.run_test(*gni,test); register_test_(tr); } catch (const tut::beyond_last_test&) { break; } catch(const tut::no_such_test&) { // it's ok } ++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_callback(callback_); runner_.get_callback().run_started(); std::string current_group; std::ifstream ijournal(jrn_.c_str()); while (ijournal.good()) { // read next test result try { tut::test_result tr; util::deserialize(ijournal,tr); runner_.get_callback().test_completed(tr); } catch (const no_more_tests&) { break; } } runner_.get_callback().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