#ifndef TUT_RESTARTABLE_H_GUARD #define TUT_RESTARTABLE_H_GUARD #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 dozen, tut@dozen.ru */ 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& ex ) { break; } catch( const tut::no_such_test& ex ) { // 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