]> git.stg.codes - stg.git/blobdiff - tests/tut/tut_restartable.hpp
Додано заголовочні файлі фреймворка для unit-тестування TUT
[stg.git] / tests / tut / tut_restartable.hpp
diff --git a/tests/tut/tut_restartable.hpp b/tests/tut/tut_restartable.hpp
new file mode 100644 (file)
index 0000000..eb6eb00
--- /dev/null
@@ -0,0 +1,409 @@
+#ifndef TUT_RESTARTABLE_H_GUARD
+#define TUT_RESTARTABLE_H_GUARD
+
+#include <tut/tut.hpp>
+#include <fstream>
+#include <iostream>
+#include <stdexcept>
+#include <cassert>
+
+/**
+ * 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
+