From: Maxim Mamontov Date: Sat, 18 Dec 2010 16:41:02 +0000 (+0200) Subject: Додано заголовочні файлі фреймворка для unit-тестування TUT X-Git-Tag: 2.407-rc3~237 X-Git-Url: https://git.stg.codes/stg.git/commitdiff_plain/3156083fd0c328d46be22536720ae33e1ab48090?ds=sidebyside Додано заголовочні файлі фреймворка для unit-тестування TUT --- diff --git a/tests/tut.h b/tests/tut.h new file mode 100644 index 00000000..22859cdd --- /dev/null +++ b/tests/tut.h @@ -0,0 +1,2 @@ + +#include diff --git a/tests/tut/tut.hpp b/tests/tut/tut.hpp new file mode 100644 index 00000000..93233a6a --- /dev/null +++ b/tests/tut/tut.hpp @@ -0,0 +1,535 @@ +#ifndef TUT_H_GUARD +#define TUT_H_GUARD + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(linux) +#define TUT_USE_POSIX +#endif + +#include "tut_exception.hpp" +#include "tut_result.hpp" +#include "tut_posix.hpp" +#include "tut_assert.hpp" +#include "tut_runner.hpp" + +#if defined(TUT_USE_SEH) +#include +#include +#endif + +/** + * Template Unit Tests Framework for C++. + * http://tut.dozen.ru + * + * @author Vladimir Dyuzhev, Vladimir.Dyuzhev@gmail.com + */ +namespace tut +{ + +/** + * Test object. Contains data test run upon and default test method + * implementation. Inherited from Data to allow tests to + * access test data as members. + */ +template +class test_object : public Data, public test_object_posix +{ +public: + + /** + * Default constructor + */ + test_object() + { + } + + void set_test_name(const std::string& current_test_name) + { + current_test_name_ = current_test_name; + } + + const std::string& get_test_name() const + { + return current_test_name_; + } + + void set_test_id(int current_test_id) + { + current_test_id_ = current_test_id; + } + + int get_test_id() const + { + return current_test_id_; + } + + /** + * Default do-nothing test. + */ + template + void test() + { + called_method_was_a_dummy_test_ = true; + } + + /** + * The flag is set to true by default (dummy) test. + * Used to detect usused test numbers and avoid unnecessary + * test object creation which may be time-consuming depending + * on operations described in Data::Data() and Data::~Data(). + * TODO: replace with throwing special exception from default test. + */ + bool called_method_was_a_dummy_test_; + +private: + int current_test_id_; + std::string current_test_name_; +}; + + +/** + * Walks through test tree and stores address of each + * test method in group. Instantiation stops at 0. + */ +template +struct tests_registerer +{ + static void reg(Group& group) + { + group.reg(n, &Test::template test); + tests_registerer::reg(group); + } +}; + +template +struct tests_registerer +{ + static void reg(Group&) + { + } +}; + +/** + * Test group; used to recreate test object instance for + * each new test since we have to have reinitialized + * Data base class. + */ +template +class test_group : public group_base, public test_group_posix +{ + const char* name_; + + typedef void (test_object::*testmethod)(); + typedef std::map tests; + typedef typename tests::iterator tests_iterator; + typedef typename tests::const_iterator tests_const_iterator; + typedef typename tests::const_reverse_iterator + tests_const_reverse_iterator; + typedef typename tests::size_type size_type; + + tests tests_; + tests_iterator current_test_; + + enum seh_result + { + SEH_OK, + SEH_CTOR, + SEH_TEST, + SEH_DUMMY + }; + + /** + * Exception-in-destructor-safe smart-pointer class. + */ + template + class safe_holder + { + T* p_; + bool permit_throw_in_dtor; + + safe_holder(const safe_holder&); + safe_holder& operator=(const safe_holder&); + + public: + safe_holder() + : p_(0), + permit_throw_in_dtor(false) + { + } + + ~safe_holder() + { + release(); + } + + T* operator->() const + { + return p_; + } + + T* get() const + { + return p_; + } + + /** + * Tell ptr it can throw from destructor. Right way is to + * use std::uncaught_exception(), but some compilers lack + * correct implementation of the function. + */ + void permit_throw() + { + permit_throw_in_dtor = true; + } + + /** + * Specially treats exceptions in test object destructor; + * if test itself failed, exceptions in destructor + * are ignored; if test was successful and destructor failed, + * warning exception throwed. + */ + void release() + { + try + { + if (delete_obj() == false) + { + throw warning("destructor of test object raised" + " an SEH exception"); + } + } + catch (const std::exception& ex) + { + if (permit_throw_in_dtor) + { + std::string msg = "destructor of test object raised" + " exception: "; + msg += ex.what(); + throw warning(msg); + } + } + catch( ... ) + { + if (permit_throw_in_dtor) + { + throw warning("destructor of test object raised an" + " exception"); + } + } + } + + /** + * Re-init holder to get brand new object. + */ + void reset() + { + release(); + permit_throw_in_dtor = false; + p_ = new T(); + } + + bool delete_obj() + { +#if defined(TUT_USE_SEH) + __try + { +#endif + T* p = p_; + p_ = 0; + delete p; +#if defined(TUT_USE_SEH) + } + __except(handle_seh_(::GetExceptionCode())) + { + if (permit_throw_in_dtor) + { + return false; + } + } +#endif + return true; + } + }; + +public: + + typedef test_object object; + + /** + * Creates and registers test group with specified name. + */ + test_group(const char* name) + : name_(name) + { + // register itself + runner.get().register_group(name_,this); + + // register all tests + tests_registerer::reg(*this); + } + + /** + * This constructor is used in self-test run only. + */ + test_group(const char* name, test_runner& another_runner) + : name_(name) + { + // register itself + another_runner.register_group(name_, this); + + // register all tests + tests_registerer, test_group, + MaxTestsInGroup>::reg(*this); + }; + + /** + * Registers test method under given number. + */ + void reg(int n, testmethod tm) + { + tests_[n] = tm; + } + + /** + * Reset test position before first test. + */ + void rewind() + { + current_test_ = tests_.begin(); + } + + /** + * Runs next test. + */ + bool run_next(test_result &tr) + { + if (current_test_ == tests_.end()) + { + return false; + } + + // find next user-specialized test + safe_holder obj; + while (current_test_ != tests_.end()) + { + tests_iterator current_test = current_test_++; + + if(run_test_(current_test, obj, tr) && tr.result != test_result::dummy) + { + return true; + } + } + + return false; + } + + /** + * Runs one test by position. + */ + bool run_test(int n, test_result &tr) + { + if (tests_.rbegin() == tests_.rend() || + tests_.rbegin()->first < n) + { + return false; + } + + // withing scope; check if given test exists + tests_iterator ti = tests_.find(n); + if (ti == tests_.end()) + { + return false; + } + + safe_holder obj; + return run_test_(ti, obj, tr); + } + + /** + * VC allows only one exception handling type per function, + * so I have to split the method. + */ + bool run_test_(const tests_iterator& ti, safe_holder& obj, test_result &tr) + { + std::string current_test_name; + + tr = test_result(name_, ti->first, current_test_name, test_result::ok); + + try + { + switch (run_test_seh_(ti->second, obj, current_test_name, ti->first)) + { + case SEH_CTOR: + throw bad_ctor("seh"); + break; + + case SEH_TEST: + throw seh("seh"); + break; + + case SEH_DUMMY: + tr.result = test_result::dummy; + break; + + case SEH_OK: + // ok + break; + } + } + catch (const rethrown& ex) + { + tr = ex.tr; + tr.result = test_result::rethrown; + } + catch (const tut_error& ex) + { + tr.result = ex.result(); + tr.exception_typeid = typeid(ex).name(); + tr.message = ex.what(); + } + catch (const std::exception& ex) + { + tr.result = test_result::ex; + tr.exception_typeid = typeid(ex).name(); + tr.message = ex.what(); + } + catch (...) + { + // test failed with unknown exception + tr.result = test_result::ex; + } + + if (obj.get()) + { + tr.name = obj->get_test_name(); + + // try to report to parent, if exists + send_result_(obj.get(), tr); + } + else + { + tr.name = current_test_name; + } + + return true; + } + + /** + * Runs one under SEH if platform supports it. + */ + seh_result run_test_seh_(testmethod tm, safe_holder& obj, + std::string& current_test_name, int current_test_id) + { +#if defined(TUT_USE_SEH) + __try + { +#endif + if (obj.get() == 0) + { + reset_holder_(obj); + } + + obj->called_method_was_a_dummy_test_ = false; + +#if defined(TUT_USE_SEH) + + __try + { +#endif + obj.get()->set_test_id(current_test_id); + (obj.get()->*tm)(); +#if defined(TUT_USE_SEH) + } + __except(handle_seh_(::GetExceptionCode())) + { + current_test_name = obj->get_test_name(); + return SEH_TEST; + } +#endif + + if (obj->called_method_was_a_dummy_test_) + { + // do not call obj.release(); reuse object + return SEH_DUMMY; + } + + current_test_name = obj->get_test_name(); + obj.permit_throw(); + obj.release(); +#if defined(TUT_USE_SEH) + } + __except(handle_seh_(::GetExceptionCode())) + { + return SEH_CTOR; + } +#endif + return SEH_OK; + } + + void reset_holder_(safe_holder& obj) + { + try + { + obj.reset(); + } + catch (const std::exception& ex) + { + throw bad_ctor(ex.what()); + } + catch (...) + { + throw bad_ctor("test constructor has generated an exception;" + " group execution is terminated"); + } + } +}; + +#if defined(TUT_USE_SEH) +/** + * Decides should we execute handler or ignore SE. + */ +inline int handle_seh_(DWORD excode) +{ + switch(excode) + { + case EXCEPTION_ACCESS_VIOLATION: + case EXCEPTION_DATATYPE_MISALIGNMENT: + case EXCEPTION_BREAKPOINT: + case EXCEPTION_SINGLE_STEP: + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: + case EXCEPTION_FLT_DENORMAL_OPERAND: + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + case EXCEPTION_FLT_INEXACT_RESULT: + case EXCEPTION_FLT_INVALID_OPERATION: + case EXCEPTION_FLT_OVERFLOW: + case EXCEPTION_FLT_STACK_CHECK: + case EXCEPTION_FLT_UNDERFLOW: + case EXCEPTION_INT_DIVIDE_BY_ZERO: + case EXCEPTION_INT_OVERFLOW: + case EXCEPTION_PRIV_INSTRUCTION: + case EXCEPTION_IN_PAGE_ERROR: + case EXCEPTION_ILLEGAL_INSTRUCTION: + case EXCEPTION_NONCONTINUABLE_EXCEPTION: + case EXCEPTION_STACK_OVERFLOW: + case EXCEPTION_INVALID_DISPOSITION: + case EXCEPTION_GUARD_PAGE: + case EXCEPTION_INVALID_HANDLE: + return EXCEPTION_EXECUTE_HANDLER; + }; + + return EXCEPTION_CONTINUE_SEARCH; +} +#endif +} + +#endif + diff --git a/tests/tut/tut_assert.hpp b/tests/tut/tut_assert.hpp new file mode 100644 index 00000000..7ee44bc2 --- /dev/null +++ b/tests/tut/tut_assert.hpp @@ -0,0 +1,201 @@ +#ifndef TUT_ASSERT_H_GUARD +#define TUT_ASSERT_H_GUARD + +#include "tut_exception.hpp" +#include +#include + +#if defined(TUT_USE_POSIX) +#include +#include +#endif + +namespace tut +{ + + namespace detail + { + template + std::ostream &msg_prefix(std::ostream &str, const M &msg) + { + std::stringstream ss; + ss << msg; + + if(!ss.str().empty()) + { + str << ss.rdbuf() << ": "; + } + + return str; + } + } + + +namespace +{ + +/** + * Tests provided condition. + * Throws if false. + */ +void ensure(bool cond) +{ + if (!cond) + { + // TODO: default ctor? + throw failure(""); + } +} + +/** + * Tests provided condition. + * Throws if true. + */ +void ensure_not(bool cond) +{ + ensure(!cond); +} + +/** + * Tests provided condition. + * Throws if false. + */ +template +void ensure(const M& msg, bool cond) +{ + if (!cond) + { + throw failure(msg); + } +} + +/** + * Tests provided condition. + * Throws if true. + */ +template +void ensure_not(const M& msg, bool cond) +{ + ensure(msg, !cond); +} + +/** + * Tests two objects for being equal. + * Throws if false. + * + * NB: both T and Q must have operator << defined somewhere, or + * client code will not compile at all! + */ +template +void ensure_equals(const M& msg, const LHS& actual, const RHS& expected) +{ + if (expected != actual) + { + std::stringstream ss; + detail::msg_prefix(ss,msg) + << "expected '" + << expected + << "' actual '" + << actual + << '\''; + throw failure(ss.str()); + } +} + +template +void ensure_equals(const LHS& actual, const RHS& expected) +{ + ensure_equals("Values are not equal", actual, expected); +} + +template +void ensure_equals(const M& msg, const double& actual, const double& expected, + const double& epsilon = std::numeric_limits::epsilon()) +{ + const double diff = actual - expected; + + if ( !((diff <= epsilon) && (diff >= -epsilon )) ) + { + std::stringstream ss; + detail::msg_prefix(ss,msg) + << std::scientific + << std::showpoint + << std::setprecision(16) + << "expected " << expected + << " actual " << actual + << " with precision " << epsilon; + throw failure(ss.str()); + } +} +/** + * Tests two objects for being at most in given distance one from another. + * Borders are excluded. + * Throws if false. + * + * NB: T must have operator << defined somewhere, or + * client code will not compile at all! Also, T shall have + * operators + and -, and be comparable. + * + * TODO: domains are wrong, T - T might not yield T, but Q + */ +template +void ensure_distance(const M& msg, const T& actual, const T& expected, const T& distance) +{ + if (expected-distance >= actual || expected+distance <= actual) + { + std::stringstream ss; + detail::msg_prefix(ss,msg) + << " expected (" + << expected-distance + << " - " + << expected+distance + << ") actual '" + << actual + << '\''; + throw failure(ss.str()); + } +} + +template +void ensure_distance(const T& actual, const T& expected, const T& distance) +{ + ensure_distance<>("Distance is wrong", actual, expected, distance); +} + +template +void ensure_errno(const M& msg, bool cond) +{ + if(!cond) + { +#if defined(TUT_USE_POSIX) + char e[512]; + std::stringstream ss; + detail::msg_prefix(ss,msg) + << strerror_r(errno, e, sizeof(e)); + throw failure(ss.str()); +#else + throw failure(msg); +#endif + } +} + +/** + * Unconditionally fails with message. + */ +void fail(const char* msg = "") +{ + throw failure(msg); +} + +template +void fail(const M& msg) +{ + throw failure(msg); +} + +} // end of namespace + +} + +#endif + diff --git a/tests/tut/tut_console_reporter.hpp b/tests/tut/tut_console_reporter.hpp new file mode 100644 index 00000000..3c3739be --- /dev/null +++ b/tests/tut/tut_console_reporter.hpp @@ -0,0 +1,241 @@ +#ifndef TUT_CONSOLE_REPORTER +#define TUT_CONSOLE_REPORTER + +#include +#include + +/** + * Template Unit Tests Framework for C++. + * http://tut.dozen.ru + * + * @author Vladimir Dyuzhev, Vladimir.Dyuzhev@gmail.com + */ +namespace +{ + +std::ostream& operator<<(std::ostream& os, const tut::test_result& tr) +{ + switch(tr.result) + { + case tut::test_result::ok: + os << '.'; + break; + case tut::test_result::fail: + os << '[' << tr.test << "=F]"; + break; + case tut::test_result::ex_ctor: + os << '[' << tr.test << "=C]"; + break; + case tut::test_result::ex: + os << '[' << tr.test << "=X]"; + break; + case tut::test_result::warn: + os << '[' << tr.test << "=W]"; + break; + case tut::test_result::term: + os << '[' << tr.test << "=T]"; + break; + case tut::test_result::rethrown: + os << '[' << tr.test << "=P]"; + break; + case tut::test_result::dummy: + assert(!"Should never be called"); + } + + return os; +} + +} // end of namespace + +namespace tut +{ + +/** + * Default TUT callback handler. + */ +class console_reporter : public tut::callback +{ + std::string current_group; + typedef std::vector not_passed_list; + not_passed_list not_passed; + std::ostream& os; + +public: + + int ok_count; + int exceptions_count; + int failures_count; + int terminations_count; + int warnings_count; + + console_reporter() + : os(std::cout) + { + init(); + } + + console_reporter(std::ostream& out) + : os(out) + { + init(); + } + + void run_started() + { + init(); + } + + void test_completed(const tut::test_result& tr) + { + if (tr.group != current_group) + { + os << std::endl << tr.group << ": " << std::flush; + current_group = tr.group; + } + + os << tr << std::flush; + + // update global statistics + switch (tr.result) { + case test_result::ok: + ok_count++; + break; + case test_result::fail: + case test_result::rethrown: + failures_count++; + break; + case test_result::ex: + case test_result::ex_ctor: + exceptions_count++; + break; + case test_result::warn: + warnings_count++; + break; + case test_result::term: + terminations_count++; + break; + case tut::test_result::dummy: + assert(!"Should never be called"); + } // switch + + if (tr.result != tut::test_result::ok) + { + not_passed.push_back(tr); + } + } + + void run_completed() + { + os << std::endl; + + if (not_passed.size() > 0) + { + not_passed_list::const_iterator i = not_passed.begin(); + while (i != not_passed.end()) + { + tut::test_result tr = *i; + + os << std::endl; + + os << "---> " << "group: " << tr.group + << ", test: test<" << tr.test << ">" + << (!tr.name.empty() ? (std::string(" : ") + tr.name) : std::string()) + << std::endl; + +#if defined(TUT_USE_POSIX) + if(tr.pid != getpid()) + { + os << " child pid: " << tr.pid << std::endl; + } +#endif + os << " problem: "; + switch(tr.result) + { + case test_result::rethrown: + os << "assertion failed in child" << std::endl; + break; + case test_result::fail: + os << "assertion failed" << std::endl; + break; + case test_result::ex: + case test_result::ex_ctor: + os << "unexpected exception" << std::endl; + if( tr.exception_typeid != "" ) + { + os << " exception typeid: " + << tr.exception_typeid << std::endl; + } + break; + case test_result::term: + os << "would be terminated" << std::endl; + break; + case test_result::warn: + os << "test passed, but cleanup code (destructor) raised" + " an exception" << std::endl; + break; + default: + break; + } + + if (!tr.message.empty()) + { + if (tr.result == test_result::fail) + { + os << " failed assertion: \"" << tr.message << "\"" + << std::endl; + } + else + { + os << " message: \"" << tr.message << "\"" + << std::endl; + } + } + + ++i; + } + } + + os << std::endl; + + os << "tests summary:"; + if (terminations_count > 0) + { + os << " terminations:" << terminations_count; + } + if (exceptions_count > 0) + { + os << " exceptions:" << exceptions_count; + } + if (failures_count > 0) + { + os << " failures:" << failures_count; + } + if (warnings_count > 0) + { + os << " warnings:" << warnings_count; + } + os << " ok:" << ok_count; + os << std::endl; + } + + bool all_ok() const + { + return not_passed.empty(); + } + +private: + + void init() + { + ok_count = 0; + exceptions_count = 0; + failures_count = 0; + terminations_count = 0; + warnings_count = 0; + not_passed.clear(); + } +}; + +} + +#endif diff --git a/tests/tut/tut_cppunit_reporter.hpp b/tests/tut/tut_cppunit_reporter.hpp new file mode 100644 index 00000000..3b9f38c1 --- /dev/null +++ b/tests/tut/tut_cppunit_reporter.hpp @@ -0,0 +1,201 @@ + +#ifndef TUT_CPPUNIT_REPORTER +#define TUT_CPPUNIT_REPORTER + +#include +#include +#include +#include +#include + +namespace tut +{ + +/** + * CppUnit TUT reporter + */ +class cppunit_reporter : public tut::callback +{ + private: + std::vector failed_tests; + std::vector passed_tests; + std::string filename; + + std::string encode(const std::string & text) + { + std::string out; + + for (unsigned int i=0; i': + out += ">"; + break; + case '&': + out += "&"; + break; + case '\'': + out += "'"; + break; + case '"': + out += """; + break; + default: + out += c; + } + } + + return out; + } + +public: + + cppunit_reporter(const std::string & _filename = "") + { + setFilename(_filename); + } + + void setFilename(const std::string & _filename) + { + if (_filename == "") + { + filename = "testResult.xml"; + } + else + { + filename = _filename; + } + } + + void run_started() + { + failed_tests.clear(); + passed_tests.clear(); + } + + void test_completed(const tut::test_result& tr) + { + if (tr.result == test_result::ok) { + passed_tests.push_back(tr); + } else { + failed_tests.push_back(tr); + } + } + + void run_completed() + { + int errors = 0; + int failures = 0; + std::string failure_type; + std::string failure_msg; + std::ofstream xmlfile; + + xmlfile.open(filename.c_str(), std::ios::in | std::ios::trunc); + if (!xmlfile.is_open()) { + throw (std::runtime_error("Cannot open file for output")); + } + + /* *********************** header ***************************** */ + xmlfile << "" << std::endl + << "" << std::endl; + + /* *********************** failed tests ***************************** */ + if (failed_tests.size() > 0) { + xmlfile << " " << std::endl; + + for (unsigned int i=0; i" << std::endl + << " " << encode(failed_tests[i].group) + "::" + encode(failed_tests[i].name) << "" << std::endl + << " " << failure_type << "" << std::endl + << " " << std::endl + << " Unknown" << std::endl + << " Unknown" << std::endl + << " " << std::endl + << " " << encode(failure_msg + failed_tests[i].message) << "" << std::endl + << " " << std::endl; + } + + xmlfile << " " << std::endl; + } + + /* *********************** passed tests ***************************** */ + if (passed_tests.size() > 0) { + xmlfile << " " << std::endl; + + for (unsigned int i=0; i" << std::endl + << " " << encode(passed_tests[i].group) + "::" + encode(passed_tests[i].name) << "" << std::endl + << " " << std::endl; + } + + xmlfile << " " << std::endl; + } + + /* *********************** statistics ***************************** */ + xmlfile << " " << std::endl + << " " << (failed_tests.size() + passed_tests.size()) << "" << std::endl + << " " << failed_tests.size() << "" << std::endl + << " " << errors << "" << std::endl + << " " << failures << "" << std::endl + << " " << std::endl; + + /* *********************** footer ***************************** */ + xmlfile << "" << std::endl; + + xmlfile.close(); + } + + bool all_ok() const + { + return failed_tests.empty(); + }; + + +}; + +} + +#endif + diff --git a/tests/tut/tut_exception.hpp b/tests/tut/tut_exception.hpp new file mode 100644 index 00000000..c5c88cb2 --- /dev/null +++ b/tests/tut/tut_exception.hpp @@ -0,0 +1,159 @@ +#ifndef TUT_EXCEPTION_H_GUARD +#define TUT_EXCEPTION_H_GUARD + +#include +#include "tut_result.hpp" + +namespace tut +{ + +/** + * The base for all TUT exceptions. + */ +struct tut_error : public std::exception +{ + tut_error(const std::string& msg) + : err_msg(msg) + { + } + + virtual test_result::result_type result() const + { + return test_result::ex; + } + + const char* what() const throw() + { + return err_msg.c_str(); + } + + ~tut_error() throw() + { + } + +private: + + std::string err_msg; +}; + +/** + * Group not found exception. + */ +struct no_such_group : public tut_error +{ + no_such_group(const std::string& grp) + : tut_error(grp) + { + } + + ~no_such_group() throw() + { + } +}; + +/** + * Internal exception to be throwed when + * test constructor has failed. + */ +struct bad_ctor : public tut_error +{ + bad_ctor(const std::string& msg) + : tut_error(msg) + { + } + + test_result::result_type result() const + { + return test_result::ex_ctor; + } + + ~bad_ctor() throw() + { + } +}; + +/** + * Exception to be throwed when ensure() fails or fail() called. + */ +struct failure : public tut_error +{ + failure(const std::string& msg) + : tut_error(msg) + { + } + + test_result::result_type result() const + { + return test_result::fail; + } + + ~failure() throw() + { + } +}; + +/** + * Exception to be throwed when test desctructor throwed an exception. + */ +struct warning : public tut_error +{ + warning(const std::string& msg) + : tut_error(msg) + { + } + + test_result::result_type result() const + { + return test_result::warn; + } + + ~warning() throw() + { + } +}; + +/** + * Exception to be throwed when test issued SEH (Win32) + */ +struct seh : public tut_error +{ + seh(const std::string& msg) + : tut_error(msg) + { + } + + virtual test_result::result_type result() const + { + return test_result::term; + } + + ~seh() throw() + { + } +}; + +/** + * Exception to be throwed when child processes fail. + */ +struct rethrown : public failure +{ + explicit rethrown(const test_result &result) + : failure(result.message), tr(result) + { + } + + virtual test_result::result_type result() const + { + return test_result::rethrown; + } + + ~rethrown() throw() + { + } + + const test_result tr; +}; + +} + +#endif diff --git a/tests/tut/tut_posix.hpp b/tests/tut/tut_posix.hpp new file mode 100644 index 00000000..89a71305 --- /dev/null +++ b/tests/tut/tut_posix.hpp @@ -0,0 +1,472 @@ +#ifndef TUT_FORK_H_GUARD +#define TUT_FORK_H_GUARD + +#if defined(TUT_USE_POSIX) +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "tut_result.hpp" +#include "tut_assert.hpp" +#include "tut_runner.hpp" + +namespace tut +{ + +template +class test_group; + +template +class test_object; + +class test_group_posix +{ +private: + template + friend class test_group; + + template + void send_result_(const T *obj, const test_result &tr) + { + if(obj->get_pipe_() == -1) + { + return; + } + + if(tr.result != test_result::ok) + { + std::stringstream ss; + ss << int(tr.result) << "\n" + << tr.group << "\n" + << tr.test << "\n" + << tr.name << "\n" + << tr.exception_typeid << "\n"; + std::copy( tr.message.begin(), tr.message.end(), std::ostreambuf_iterator(ss.rdbuf()) ); + + int size = ss.str().length(); + int w = write(obj->get_pipe_(), ss.str().c_str(), size); + ensure_errno("write() failed", w == size); + } + } +}; + +template +struct tut_posix +{ + pid_t fork() + { + test_object *self = dynamic_cast< tut::test_object* >(this); + ensure("trying to call 'tut_fork' in ctor of test object", self != NULL); + + return self->fork_(); + } + + pid_t waitpid(pid_t pid, int *status, int flags = 0) + { + test_object *self = dynamic_cast< tut::test_object* >(this); + ensure("trying to call 'tut_waitpid' in ctor of test object", self != NULL); + + return self->waitpid_(pid, status, flags); + } + + void ensure_child_exit(pid_t pid, int exit_status = 0) + { + test_object *self = dynamic_cast< tut::test_object* >(this); + ensure("trying to call 'ensure_child_exit' in ctor of test object", self != NULL); + + int status; + self->waitpid_(pid, &status); + + self->ensure_child_exit_(status, exit_status); + } + + + void ensure_child_signal(pid_t pid, int signal = SIGTERM) + { + test_object *self = dynamic_cast< tut::test_object* >(this); + ensure("trying to call 'ensure_child_signal' in ctor of test object", self != NULL); + + int status; + self->waitpid_(pid, &status); + + self->ensure_child_signal_(status, signal); + } + + std::set get_pids() const + { + using namespace std; + + const test_object *self = dynamic_cast< const tut::test_object* >(this); + ensure("trying to call 'get_pids' in ctor of test object", self != NULL); + + return self->get_pids_(); + } + + virtual ~tut_posix() + { + } + +}; + +class test_object_posix +{ +public: + typedef std::map pid_map; + + /** + * Default constructor + */ + test_object_posix() + : pipe_(-1) + { + } + + + virtual ~test_object_posix() + { + // we have forked + if(pipe_ != -1) + { + // in child, force exit + std::exit(0); + } + + if(!pids_.empty()) + { + std::stringstream ss; + + // in parent, reap children + for(std::map::iterator i = pids_.begin(); i != pids_.end(); ++i) + { + try { + kill_child_(i->first); + } catch(const rethrown &ex) { + ss << std::endl << "child " << ex.tr.pid << " has thrown an exception: " << ex.what(); + } catch(const failure &ex) { + ss << std::endl << ex.what(); + } + } + + if(!ss.str().empty()) + { + fail(ss.str().c_str()); + } + } + } + +private: + template + friend class tut_posix; + + friend class test_group_posix; + + int get_pipe_() const + { + return pipe_; + } + + + pid_t fork_() + { + // create pipe + int fds[2]; + ensure_errno("pipe() failed", ::pipe(fds) == 0); + + pid_t pid = ::fork(); + + ensure_errno("fork() failed", pid >= 0); + + if(pid != 0) + { + // in parent, register pid + ensure("duplicated child", pids_.insert( std::make_pair(pid, fds[0]) ).second); + + // close writing side + close(fds[1]); + } + else + { + // in child, shutdown reporter + tut::runner.get().clear_callbacks(); + + // close reading side + close(fds[0]); + pipe_ = fds[1]; + } + + return pid; + } + + void kill_child_(pid_t pid) + { + int status; + + if(waitpid_(pid, &status, WNOHANG) == pid) + { + ensure_child_exit_(status, 0); + return; + } + + if(::kill(pid, SIGTERM) != 0) + { + if(errno == ESRCH) + { + // no such process + return; + } + else + { + // cannot kill, we are in trouble + std::stringstream ss; + char e[1024]; + ss << "child " << pid << " could not be killed with SIGTERM, " << strerror_r(errno, e, sizeof(e)) << std::endl; + fail(ss.str()); + } + } + + if(waitpid_(pid, &status, WNOHANG) == pid) + { + // child killed, check signal + ensure_child_signal_(status, SIGTERM); + + ensure_equals("child process exists after SIGTERM", ::kill(pid, 0), -1); + return; + } + + // child seems to be still exiting, give it some time + sleep(2); + + if(waitpid_(pid, &status, WNOHANG) != pid) + { + // child is still running, kill it + if(::kill(pid, SIGKILL) != 0) + { + if(errno == ESRCH) + { + // no such process + return; + } + else + { + std::stringstream ss; + char e[1024]; + ss << "child " << pid << " could not be killed with SIGKILL, " << strerror_r(errno, e, sizeof(e)) << std::endl; + fail(ss.str()); + } + } + + ensure_equals("wait after SIGKILL", waitpid_(pid, &status), pid); + ensure_child_signal_(status, SIGKILL); + + ensure_equals("child process exists after SIGKILL", ::kill(pid, 0), -1); + + std::stringstream ss; + ss << "child " << pid << " had to be killed with SIGKILL"; + fail(ss.str()); + } + } + + test_result receive_result_(std::istream &ss, pid_t pid) + { + test_result tr; + + int type; + ss >> type; + tr.result = test_result::result_type(type); + ss.ignore(1024, '\n'); + + std::getline(ss, tr.group); + ss >> tr.test; + ss.ignore(1024, '\n'); + std::getline(ss, tr.name); + std::getline(ss, tr.exception_typeid); + std::copy( std::istreambuf_iterator(ss.rdbuf()), + std::istreambuf_iterator(), + std::back_inserter(tr.message) ); + + tr.pid = pid; + + return tr; + } + + struct fdclose + { + fdclose(int fd): fd_(fd) { } + ~fdclose() + { + close(fd_); + } + private: + int fd_; + }; + + pid_t waitpid_(pid_t pid, int *status, int flags = 0) + { + + ensure("trying to wait for unknown pid", pids_.count(pid) > 0); + + pid_t p = ::waitpid(pid, status, flags); + if( (flags & WNOHANG) && (p != pid) ) + { + return p; + } + + // read child result from pipe + fd_set fdset; + timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 0; + + FD_ZERO(&fdset); + + int pipe = pids_[pid]; + fdclose guard(pipe); + + FD_SET(pipe, &fdset); + + int result = select(pipe+1, &fdset, NULL, NULL, &tv); + ensure_errno("sanity check on select() failed", result >= 0); + + if(result > 0) + { + ensure("sanity check on FD_ISSET() failed", FD_ISSET(pipe, &fdset) ); + + std::stringstream ss; + + //TODO: max failure length + char buffer[1024]; + int r = read(pipe, buffer, sizeof(buffer)); + ensure_errno("sanity check on read() failed", r >= 0); + + if(r > 0) + { + ss.write(buffer, r); + throw rethrown( receive_result_(ss, pid) ); + } + } + + return pid; + } + + void ensure_child_exit_(int status, int exit_status) + { + if(WIFSIGNALED(status)) + { + std::stringstream ss; + ss << "child killed by signal " << WTERMSIG(status) + << ": expected exit with code " << exit_status; + + throw failure(ss.str().c_str()); + } + + if(WIFEXITED(status)) + { + if(WEXITSTATUS(status) != exit_status) + { + std::stringstream ss; + ss << "child exited, expected '" + << exit_status + << "' actual '" + << WEXITSTATUS(status) + << '\''; + + throw failure(ss.str().c_str()); + } + } + + if(WIFSTOPPED(status)) + { + std::stringstream ss; + ss << "child stopped by signal " << WTERMSIG(status) + << ": expected exit with code " << exit_status; + throw failure(ss.str().c_str()); + } + } + + void ensure_child_signal_(int status, int signal) + { + if(WIFSIGNALED(status)) + { + if(WTERMSIG(status) != signal) + { + std::stringstream ss; + ss << "child killed by signal, expected '" + << signal + << "' actual '" + << WTERMSIG(status) + << '\''; + throw failure(ss.str().c_str()); + } + } + + if(WIFEXITED(status)) + { + std::stringstream ss; + ss << "child exited with code " << WEXITSTATUS(status) + << ": expected signal " << signal; + + throw failure(ss.str().c_str()); + } + + if(WIFSTOPPED(status)) + { + std::stringstream ss; + ss << "child stopped by signal " << WTERMSIG(status) + << ": expected kill by signal " << signal; + + throw failure(ss.str().c_str()); + } + } + + std::set get_pids_() const + { + using namespace std; + + set pids; + for(pid_map::const_iterator i = pids_.begin(); i != pids_.end(); ++i) + { + pids.insert( i->first ); + } + + return pids; + } + + pid_map pids_; + int pipe_; +}; + +} // namespace tut + +#else + +namespace tut +{ + +struct test_object_posix +{ +}; + +struct test_group_posix +{ + template + void send_result_(const T*, const test_result &) + { + } +}; + +} // namespace tut + +#endif + + +#endif + diff --git a/tests/tut/tut_reporter.hpp b/tests/tut/tut_reporter.hpp new file mode 100644 index 00000000..56731d8b --- /dev/null +++ b/tests/tut/tut_reporter.hpp @@ -0,0 +1,11 @@ +#ifndef TUT_REPORTER +#define TUT_REPORTER + +#include + +namespace tut +{ + typedef console_reporter reporter; +} + +#endif 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 + diff --git a/tests/tut/tut_result.hpp b/tests/tut/tut_result.hpp new file mode 100644 index 00000000..5731802c --- /dev/null +++ b/tests/tut/tut_result.hpp @@ -0,0 +1,129 @@ +#ifndef TUT_RESULT_H_GUARD +#define TUT_RESULT_H_GUARD + +#include + +namespace tut +{ + +#if defined(TUT_USE_POSIX) +struct test_result_posix +{ + test_result_posix() + : pid(getpid()) + { + } + + pid_t pid; +}; +#else +struct test_result_posix +{ +}; +#endif + +/** + * Return type of runned test/test group. + * + * For test: contains result of test and, possible, message + * for failure or exception. + */ +struct test_result : public test_result_posix +{ + /** + * Test group name. + */ + std::string group; + + /** + * Test number in group. + */ + int test; + + /** + * Test name (optional) + */ + std::string name; + + /** + * ok - test finished successfully + * fail - test failed with ensure() or fail() methods + * ex - test throwed an exceptions + * warn - test finished successfully, but test destructor throwed + * term - test forced test application to terminate abnormally + */ + enum result_type + { + ok, + fail, + ex, + warn, + term, + ex_ctor, + rethrown, + dummy + }; + + result_type result; + + /** + * Exception message for failed test. + */ + std::string message; + std::string exception_typeid; + + /** + * Default constructor. + */ + test_result() + : test(0), + result(ok) + { + } + + /** + * Constructor. + */ + test_result(const std::string& grp, int pos, + const std::string& test_name, result_type res) + : group(grp), + test(pos), + name(test_name), + result(res) + { + } + + /** + * Constructor with exception. + */ + test_result(const std::string& grp,int pos, + const std::string& test_name, result_type res, + const std::exception& ex) + : group(grp), + test(pos), + name(test_name), + result(res), + message(ex.what()), + exception_typeid(typeid(ex).name()) + { + } + + /** Constructor with typeid. + */ + test_result(const std::string& grp,int pos, + const std::string& test_name, result_type res, + const std::string& ex_typeid, + const std::string& msg) + : group(grp), + test(pos), + name(test_name), + result(res), + message(msg), + exception_typeid(ex_typeid) + { + } +}; + +} + +#endif diff --git a/tests/tut/tut_runner.hpp b/tests/tut/tut_runner.hpp new file mode 100644 index 00000000..33ffe423 --- /dev/null +++ b/tests/tut/tut_runner.hpp @@ -0,0 +1,359 @@ +#ifndef TUT_RUNNER_H_GUARD +#define TUT_RUNNER_H_GUARD + +#include +#include +#include +#include "tut_exception.hpp" + +namespace tut +{ + +/** + * Interface. + * Test group operations. + */ +struct group_base +{ + virtual ~group_base() + { + } + + // execute tests iteratively + virtual void rewind() = 0; + virtual bool run_next(test_result &) = 0; + + // execute one test + virtual bool run_test(int n, test_result &tr) = 0; +}; + + +/** + * Test runner callback interface. + * Can be implemented by caller to update + * tests results in real-time. User can implement + * any of callback methods, and leave unused + * in default implementation. + */ +struct callback +{ + /** + * Default constructor. + */ + callback() + { + } + + /** + * Virtual destructor is a must for subclassed types. + */ + virtual ~callback() + { + } + + /** + * Called when new test run started. + */ + virtual void run_started() + { + } + + /** + * Called when a group started + * @param name Name of the group + */ + virtual void group_started(const std::string& /*name*/) + { + } + + /** + * Called when a test finished. + * @param tr Test results. + */ + virtual void test_completed(const test_result& /*tr*/) + { + } + + /** + * Called when a group is completed + * @param name Name of the group + */ + virtual void group_completed(const std::string& /*name*/) + { + } + + /** + * Called when all tests in run completed. + */ + virtual void run_completed() + { + } +private: + callback(const callback &); + void operator=(const callback&); +}; + +/** + * Typedef for runner::list_groups() + */ +typedef std::vector groupnames; +typedef std::set callbacks; + +/** + * Test runner. + */ +class test_runner +{ + +public: + + /** + * Constructor + */ + test_runner() + { + } + + /** + * Stores another group for getting by name. + */ + void register_group(const std::string& name, group_base* gr) + { + if (gr == 0) + { + throw tut_error("group shall be non-null"); + } + + if (groups_.find(name) != groups_.end()) + { + std::string msg("attempt to add already existent group " + name); + // this exception terminates application so we use cerr also + // TODO: should this message appear in stream? + std::cerr << msg << std::endl; + throw tut_error(msg); + } + + groups_.insert( std::make_pair(name, gr) ); + } + + void set_callback(callback *cb) + { + clear_callbacks(); + insert_callback(cb); + } + + /** + * Stores callback object. + */ + void insert_callback(callback* cb) + { + if(cb != NULL) + { + callbacks_.insert(cb); + } + } + + void erase_callback(callback* cb) + { + callbacks_.erase(cb); + } + + void clear_callbacks() + { + callbacks_.clear(); + } + + /** + * Returns callback list. + */ + const callbacks &get_callbacks() const + { + return callbacks_; + } + + void set_callbacks(const callbacks &cb) + { + callbacks_ = cb; + } + + /** + * Returns list of known test groups. + */ + const groupnames list_groups() const + { + groupnames ret; + const_iterator i = groups_.begin(); + const_iterator e = groups_.end(); + while (i != e) + { + ret.push_back(i->first); + ++i; + } + return ret; + } + + /** + * Runs all tests in all groups. + * @param callback Callback object if exists; null otherwise + */ + void run_tests() const + { + cb_run_started_(); + + const_iterator i = groups_.begin(); + const_iterator e = groups_.end(); + while (i != e) + { + cb_group_started_(i->first); + run_all_tests_in_group_(i); + cb_group_completed_(i->first); + + ++i; + } + + cb_run_completed_(); + } + + /** + * Runs all tests in specified group. + */ + void run_tests(const std::string& group_name) const + { + cb_run_started_(); + + const_iterator i = groups_.find(group_name); + if (i == groups_.end()) + { + cb_run_completed_(); + throw no_such_group(group_name); + } + + cb_group_started_(group_name); + run_all_tests_in_group_(i); + cb_group_completed_(group_name); + cb_run_completed_(); + } + + /** + * Runs one test in specified group. + */ + bool run_test(const std::string& group_name, int n, test_result &tr) const + { + cb_run_started_(); + + const_iterator i = groups_.find(group_name); + if (i == groups_.end()) + { + cb_run_completed_(); + throw no_such_group(group_name); + } + + cb_group_started_(group_name); + + bool t = i->second->run_test(n, tr); + + if(t && tr.result != test_result::dummy) + { + cb_test_completed_(tr); + } + + cb_group_completed_(group_name); + cb_run_completed_(); + + return t; + } + +protected: + + typedef std::map groups; + typedef groups::iterator iterator; + typedef groups::const_iterator const_iterator; + groups groups_; + + callbacks callbacks_; + +private: + friend class restartable_wrapper; + + void cb_run_started_() const + { + for(callbacks::const_iterator i = callbacks_.begin(); i != callbacks_.end(); ++i) + { + (*i)->run_started(); + } + } + + void cb_run_completed_() const + { + for(callbacks::const_iterator i = callbacks_.begin(); i != callbacks_.end(); ++i) + { + (*i)->run_completed(); + } + } + + void cb_group_started_(const std::string &group_name) const + { + for(callbacks::const_iterator i = callbacks_.begin(); i != callbacks_.end(); ++i) + { + (*i)->group_started(group_name); + } + } + + void cb_group_completed_(const std::string &group_name) const + { + for(callbacks::const_iterator i = callbacks_.begin(); i != callbacks_.end(); ++i) + { + (*i)->group_completed(group_name); + } + } + + void cb_test_completed_(const test_result &tr) const + { + for(callbacks::const_iterator i = callbacks_.begin(); i != callbacks_.end(); ++i) + { + (*i)->test_completed(tr); + } + } + + void run_all_tests_in_group_(const_iterator i) const + { + i->second->rewind(); + + test_result tr; + while(i->second->run_next(tr)) + { + if(tr.result != test_result::dummy) + { + cb_test_completed_(tr); + } + + if (tr.result == test_result::ex_ctor) + { + // test object ctor failed, skip whole group + break; + } + } + } +}; + +/** + * Singleton for test_runner implementation. + * Instance with name runner_singleton shall be implemented + * by user. + */ +class test_runner_singleton +{ +public: + + static test_runner& get() + { + static test_runner tr; + return tr; + } +}; + +extern test_runner_singleton runner; + +} + +#endif diff --git a/tests/tut/tut_xml_reporter.hpp b/tests/tut/tut_xml_reporter.hpp new file mode 100644 index 00000000..8a9d5dec --- /dev/null +++ b/tests/tut/tut_xml_reporter.hpp @@ -0,0 +1,361 @@ +/* + * tut_xml_reporter.hpp + * + * ECOS Library. IPT CS R&D CET ECOS Copyright 2008 Nokia + * Siemens Networks. All right + * + * + */ + +#ifndef TUT_XML_REPORTER +#define TUT_XML_REPORTER + +#include +#include +#include +#include +#include + +namespace tut +{ + +/** + * \brief JUnit XML TUT reporter + * @author Lukasz Maszczynski, NSN + * @date 11/07/2008 + */ +class xml_reporter : public tut::callback +{ +protected: + typedef std::vector TestResults; + typedef std::map TestGroups; + + TestGroups all_tests; /// holds all test results + std::string filename; /// filename base + + /** + * \brief Initializes object + * Resets counters and clears all stored test results. + */ + virtual void init() + { + ok_count = 0; + exceptions_count = 0; + failures_count = 0; + terminations_count = 0; + warnings_count = 0; + all_tests.clear(); + } + + /** + * \brief Encodes text to XML + * XML-reserved characters (e.g. "<") are encoded according to specification + * @param text text to be encoded + * @return encoded string + */ + virtual std::string encode(const std::string & text) + { + std::string out; + + for (unsigned int i=0; i': + out += ">"; + break; + case '&': + out += "&"; + break; + case '\'': + out += "'"; + break; + case '"': + out += """; + break; + default: + out += c; + } + } + + return out; + } + + /** + * \brief Builds "testcase" XML entity with given parameters + * Builds \ entity according to given parameters. \-s are part of \. + * @param tr test result to be used as source data + * @param failure_type type of failure to be reported ("Assertion" or "Error", empty if test passed) + * @param failure_msg failure message to be reported (empty, if test passed) + * @return string with \ entity + */ + virtual std::string xml_build_testcase(const tut::test_result & tr, const std::string & failure_type, + const std::string & failure_msg, int pid = 0) + { + using std::endl; + using std::string; + + std::ostringstream out; + + if (tr.result == test_result::ok) + { + out << " "; + } + else + { + string err_msg = encode(failure_msg + tr.message); + + string tag; // determines tag name: "failure" or "error" + if ( tr.result == test_result::fail || tr.result == test_result::warn || + tr.result == test_result::ex || tr.result == test_result::ex_ctor ) + { + tag = "failure"; + } + else + { + tag = "error"; + } + + out << " " << endl; + out << " <" << tag << " message=\"" << err_msg << "\"" << " type=\"" << failure_type << "\""; +#if defined(TUT_USE_POSIX) + if(pid != getpid()) + { + out << " child=\"" << pid << "\""; + } +#endif + out << ">" << err_msg << "" << endl; + out << " "; + } + + return out.str(); + } + + /** + * \brief Builds "testsuite" XML entity + * Builds \ XML entity according to given parameters. + * @param errors number of errors to be reported + * @param failures number of failures to be reported + * @param total total number of tests to be reported + * @param name test suite name + * @param testcases encoded XML string containing testcases + * @return string with \ entity + */ + virtual std::string xml_build_testsuite(int errors, int failures, int total, + const std::string & name, const std::string & testcases) + { + std::ostringstream out; + + out << "" << std::endl; + out << testcases; + out << ""; + + return out.str(); + } + +public: + int ok_count; /// number of passed tests + int exceptions_count; /// number of tests that threw exceptions + int failures_count; /// number of tests that failed + int terminations_count; /// number of tests that would terminate + int warnings_count; /// number of tests where destructors threw an exception + + /** + * \brief Default constructor + * @param filename base filename + * @see setFilenameBase + */ + xml_reporter(const std::string & _filename = "") + { + init(); + setFilenameBase(_filename); + } + + /** + * \brief Sets filename base for output + * @param _filename filename base + * Example usage: + * @code + * xml_reporter reporter; + * reporter.setFilenameBase("my_xml"); + * @endcode + * The above code will instruct reporter to create my_xml_1.xml file for the first test group, + * my_xml_2.xml file for the second, and so on. + */ + virtual void setFilenameBase(const std::string & _filename) + { + if (_filename == "") + { + filename = "testResult"; + } + else + { + if (_filename.length() > 200) + { + throw(std::runtime_error("Filename too long!")); + } + + filename = _filename; + } + } + + /** + * \brief Callback function + * This function is called before the first test is executed. It initializes counters. + */ + virtual void run_started() + { + init(); + } + + /** + * \brief Callback function + * This function is called when test completes. Counters are updated here, and test results stored. + */ + virtual void test_completed(const tut::test_result& tr) + { + // update global statistics + switch (tr.result) { + case test_result::ok: + ok_count++; + break; + case test_result::fail: + case test_result::rethrown: + failures_count++; + break; + case test_result::ex: + case test_result::ex_ctor: + exceptions_count++; + break; + case test_result::warn: + warnings_count++; + break; + case test_result::term: + terminations_count++; + break; + } // switch + + // add test result to results table + (all_tests[tr.group]).push_back(tr); + } + + /** + * \brief Callback function + * This function is called when all tests are completed. It generates XML output + * to file(s). File name base can be set with \ref setFilenameBase. + */ + virtual void run_completed() + { + using std::endl; + using std::string; + + static int number = 1; // results file sequence number (testResult_.xml) + + // iterate over all test groups + TestGroups::const_iterator tgi; + for (tgi = all_tests.begin(); tgi != all_tests.end(); ++tgi) { + /* per-group statistics */ + int passed = 0; // passed in single group + int exceptions = 0; // exceptions in single group + int failures = 0; // failures in single group + int terminations = 0; // terminations in single group + int warnings = 0; // warnings in single group + int errors = 0; // errors in single group + + /* generate output filename */ + char fn[256]; + sprintf(fn, "%s_%d.xml", filename.c_str(), number++); + + std::ofstream xmlfile; + xmlfile.open(fn, std::ios::in | std::ios::trunc); + if (!xmlfile.is_open()) { + throw (std::runtime_error("Cannot open file for output")); + } + + /* *********************** header ***************************** */ + xmlfile << "" << endl; + + // output is written to string stream buffer, because JUnit format tag + // contains statistics, which aren't known yet + std::ostringstream out; + + // iterate over all test cases in the current test group + TestResults::const_iterator tri; + for (tri = (*tgi).second.begin(); tri != (*tgi).second.end(); ++tri) { + string failure_type; // string describing the failure type + string failure_msg; // a string with failure message + + switch ((*tri).result) { + case test_result::ok: + passed++; + break; + case test_result::fail: + failure_type = "Assertion"; + failure_msg = ""; + failures++; + break; + case test_result::ex: + failure_type = "Assertion"; + failure_msg = "Thrown exception: " + (*tri).exception_typeid + '\n'; + exceptions++; + break; + case test_result::warn: + failure_type = "Assertion"; + failure_msg = "Destructor failed.\n"; + warnings++; + break; + case test_result::term: + failure_type = "Error"; + failure_msg = "Test application terminated abnormally.\n"; + terminations++; + break; + case test_result::ex_ctor: + failure_type = "Assertion"; + failure_msg = "Constructor has thrown an exception: " + (*tri).exception_typeid + '\n'; + exceptions++; + break; + case test_result::rethrown: + failure_type = "Assertion"; + failure_msg = "Child failed"; + failures++; + break; + default: + failure_type = "Error"; + failure_msg = "Unknown test status, this should have never happened. " + "You may just have found a BUG in TUT XML reporter, please report it immediately.\n"; + errors++; + break; + } // switch + +#if defined(TUT_USE_POSIX) + out << xml_build_testcase(*tri, failure_type, failure_msg, (*tri).pid) << endl; +#else + out << xml_build_testcase(*tri, failure_type, failure_msg) << endl; +#endif + + } // iterate over all test cases + + // calculate per-group statistics + int stat_errors = terminations + errors; + int stat_failures = failures + warnings + exceptions; + int stat_all = stat_errors + stat_failures + passed; + + xmlfile << xml_build_testsuite(stat_errors, stat_failures, stat_all, (*tgi).first/* name */, out.str()/* testcases */) << endl; + xmlfile.close(); + } // iterate over all test groups + } + + /** + * \brief Returns true, if all tests passed + */ + virtual bool all_ok() const + { + return ( (terminations_count + failures_count + warnings_count + exceptions_count) == 0); + }; +}; + +} + +#endif diff --git a/tests/tut_reporter.h b/tests/tut_reporter.h new file mode 100644 index 00000000..e568635a --- /dev/null +++ b/tests/tut_reporter.h @@ -0,0 +1,5 @@ + +#include +#include +#include +#include diff --git a/tests/tut_restartable.h b/tests/tut_restartable.h new file mode 100644 index 00000000..c7897797 --- /dev/null +++ b/tests/tut_restartable.h @@ -0,0 +1,2 @@ + +#include