diff options
Diffstat (limited to 'ept/utils/tests.cc')
-rw-r--r-- | ept/utils/tests.cc | 578 |
1 files changed, 578 insertions, 0 deletions
diff --git a/ept/utils/tests.cc b/ept/utils/tests.cc new file mode 100644 index 0000000..28ea280 --- /dev/null +++ b/ept/utils/tests.cc @@ -0,0 +1,578 @@ +/* + * @author Enrico Zini <enrico@enricozini.org>, Peter Rockai (mornfall) <me@mornfall.net> + * @brief Utility functions for the unit tests + * + * Copyright (C) 2006--2007 Peter Rockai (mornfall) <me@mornfall.net> + * Copyright (C) 2003--2015 Enrico Zini <enrico@debian.org> + */ + +#include "tests.h" +#include "string.h" +#include <fnmatch.h> +#include <cmath> +#include <iomanip> +#include <sys/types.h> +#include <regex.h> + +using namespace std; +using namespace ept; + +const ept::tests::LocationInfo ept_test_location_info; + +namespace ept { +namespace tests { + +/* + * TestStackFrame + */ + +std::string TestStackFrame::format() const +{ + std::stringstream ss; + format(ss); + return ss.str(); +} + +void TestStackFrame::format(std::ostream& out) const +{ + out << file << ":" << line << ":" << call; + if (!local_info.empty()) + out << " [" << local_info << "]"; + out << endl; +} + + +/* + * TestStack + */ + +void TestStack::backtrace(std::ostream& out) const +{ + for (const auto& frame: *this) + frame.format(out); +} + +std::string TestStack::backtrace() const +{ + std::stringstream ss; + backtrace(ss); + return ss.str(); +} + + +/* + * TestFailed + */ + +TestFailed::TestFailed(const std::exception& e) + : message(typeid(e).name()) +{ + message += ": "; + message += e.what(); +} + + +#if 0 +std::string Location::fail_msg(const std::string& error) const +{ + std::stringstream ss; + ss << "test failed at:" << endl; + backtrace(ss); + ss << file << ":" << line << ":error: " << error << endl; + return ss.str(); +} + +std::string Location::fail_msg(std::function<void(std::ostream&)> write_error) const +{ + std::stringstream ss; + ss << "test failed at:" << endl; + backtrace(ss); + ss << file << ":" << line << ":error: "; + write_error(ss); + ss << endl; + return ss.str(); +} +#endif + +std::ostream& LocationInfo::operator()() +{ + str(std::string()); + clear(); + return *this; +} + +/* + * Assertions + */ + +void assert_startswith(const std::string& actual, const std::string& expected) +{ + if (str::startswith(actual, expected)) return; + std::stringstream ss; + ss << "'" << actual << "' does not start with '" << expected << "'"; + throw TestFailed(ss.str()); +} + +void assert_endswith(const std::string& actual, const std::string& expected) +{ + if (str::endswith(actual, expected)) return; + std::stringstream ss; + ss << "'" << actual << "' does not end with '" << expected << "'"; + throw TestFailed(ss.str()); +} + +void assert_contains(const std::string& actual, const std::string& expected) +{ + if (actual.find(expected) != std::string::npos) return; + std::stringstream ss; + ss << "'" << actual << "' does not contain '" << expected << "'"; + throw TestFailed(ss.str()); +} + +void assert_not_contains(const std::string& actual, const std::string& expected) +{ + if (actual.find(expected) == std::string::npos) return; + std::stringstream ss; + ss << "'" << actual << "' contains '" << expected << "'"; + throw TestFailed(ss.str()); +} + +namespace { + +struct Regexp +{ + regex_t compiled; + + Regexp(const char* regex) + { + if (int err = regcomp(&compiled, regex, REG_EXTENDED | REG_NOSUB)) + raise_error(err); + } + ~Regexp() + { + regfree(&compiled); + } + + bool search(const char* s) + { + return regexec(&compiled, s, 0, nullptr, 0) != REG_NOMATCH; + } + + void raise_error(int code) + { + // Get the size of the error message string + size_t size = regerror(code, &compiled, nullptr, 0); + + char* buf = new char[size]; + regerror(code, &compiled, buf, size); + string msg(buf); + delete[] buf; + throw std::runtime_error(msg); + } +}; + +} + +void assert_re_matches(const std::string& actual, const std::string& expected) +{ + Regexp re(expected.c_str()); + if (re.search(actual.c_str())) return; + std::stringstream ss; + ss << "'" << actual << "' does not match '" << expected << "'"; + throw TestFailed(ss.str()); +} + +void assert_not_re_matches(const std::string& actual, const std::string& expected) +{ + Regexp re(expected.c_str()); + if (!re.search(actual.c_str())) return; + std::stringstream ss; + ss << "'" << actual << "' should not match '" << expected << "'"; + throw TestFailed(ss.str()); +} + +void assert_true(std::nullptr_t actual) +{ + throw TestFailed("actual value nullptr is not true"); +}; + +void assert_false(std::nullptr_t actual) +{ +}; + + +static void _actual_must_be_set(const char* actual) +{ + if (!actual) + throw TestFailed("actual value is the null pointer instead of a valid string"); +} + +void ActualCString::operator==(const char* expected) const +{ + if (expected && _actual) + assert_equal<std::string, std::string>(_actual, expected); + else if (!expected && !_actual) + ; + else if (expected) + { + std::stringstream ss; + ss << "actual value is nullptr instead of the expected string \"" << str::encode_cstring(expected) << "\""; + throw TestFailed(ss.str()); + } + else + { + std::stringstream ss; + ss << "actual value is the string \"" << str::encode_cstring(_actual) << "\" instead of nullptr"; + throw TestFailed(ss.str()); + } +} + +void ActualCString::operator==(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_equal<std::string, std::string>(_actual, expected); +} + +void ActualCString::operator!=(const char* expected) const +{ + if (expected && _actual) + assert_not_equal<std::string, std::string>(_actual, expected); + else if (!expected && !_actual) + throw TestFailed("actual and expected values are both nullptr but they should be different"); +} + +void ActualCString::operator!=(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_not_equal<std::string, std::string>(_actual, expected); +} + +void ActualCString::operator<(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_less<std::string, std::string>(_actual, expected); +} + +void ActualCString::operator<=(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_less_equal<std::string, std::string>(_actual, expected); +} + +void ActualCString::operator>(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_greater<std::string, std::string>(_actual, expected); +} + +void ActualCString::operator>=(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_greater_equal<std::string, std::string>(_actual, expected); +} + +void ActualCString::matches(const std::string& re) const +{ + _actual_must_be_set(_actual); + assert_re_matches(_actual, re); +} + +void ActualCString::not_matches(const std::string& re) const +{ + _actual_must_be_set(_actual); + assert_not_re_matches(_actual, re); +} + +void ActualCString::startswith(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_startswith(_actual, expected); +} + +void ActualCString::endswith(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_endswith(_actual, expected); +} + +void ActualCString::contains(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_contains(_actual, expected); +} + +void ActualCString::not_contains(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_not_contains(_actual, expected); +} + +void ActualStdString::startswith(const std::string& expected) const +{ + assert_startswith(_actual, expected); +} + +void ActualStdString::endswith(const std::string& expected) const +{ + assert_endswith(_actual, expected); +} + +void ActualStdString::contains(const std::string& expected) const +{ + assert_contains(_actual, expected); +} + +void ActualStdString::not_contains(const std::string& expected) const +{ + assert_not_contains(_actual, expected); +} + +void ActualStdString::matches(const std::string& re) const +{ + assert_re_matches(_actual, re); +} + +void ActualStdString::not_matches(const std::string& re) const +{ + assert_not_re_matches(_actual, re); +} + +void ActualDouble::almost_equal(double expected, unsigned places) const +{ + if (round((_actual - expected) * exp10(places)) == 0.0) + return; + std::stringstream ss; + ss << std::setprecision(places) << fixed << _actual << " is different than the expected " << expected; + throw TestFailed(ss.str()); +} + +void ActualDouble::not_almost_equal(double expected, unsigned places) const +{ + if (round(_actual - expected * exp10(places)) != 0.0) + return; + std::stringstream ss; + ss << std::setprecision(places) << fixed << _actual << " is the same as the expected " << expected; + throw TestFailed(ss.str()); +} + +void ActualFunction::throws(const std::string& what_match) const +{ + bool thrown = false; + try { + _actual(); + } catch (std::exception& e) { + thrown = true; + wassert(actual(e.what()).matches(what_match)); + } + if (!thrown) + throw TestFailed("code did not throw any exception"); +} + +#if 0 +void test_assert_file_exists(WIBBLE_TEST_LOCPRM, const std::string& fname) +{ + if (not sys::fs::exists(fname)) + { + std::stringstream ss; + ss << "file '" << fname << "' does not exists"; + ept_test_location.fail_test(ss.str()); + } +} + +void test_assert_not_file_exists(WIBBLE_TEST_LOCPRM, const std::string& fname) +{ + if (sys::fs::exists(fname)) + { + std::stringstream ss; + ss << "file '" << fname << "' does exists"; + ept_test_location.fail_test(ss.str()); + } +} + +#if 0 +struct TestFileExists +{ + std::string pathname; + bool inverted; + TestFileExists(const std::string& pathname, bool inverted=false) : pathname(pathname), inverted(inverted) {} + TestFileExists operator!() { return TestFileExists(pathname, !inverted); } + void check(EPT_TEST_LOCPRM) const; +}; +#endif + +void TestFileExists::check(WIBBLE_TEST_LOCPRM) const +{ + if (!inverted) + { + if (sys::fs::exists(pathname)) return; + std::stringstream ss; + ss << "file '" << pathname << "' does not exists"; + ept_test_location.fail_test(ss.str()); + } else { + if (not sys::fs::exists(pathname)) return; + std::stringstream ss; + ss << "file '" << pathname << "' exists"; + ept_test_location.fail_test(ss.str()); + } +} +#endif + +TestRegistry& TestRegistry::get() +{ + static TestRegistry* instance = 0; + if (!instance) + instance = new TestRegistry(); + return *instance; +} + +void TestRegistry::register_test_case(TestCase& test_case) +{ + entries.emplace_back(&test_case); +} + +std::vector<TestCaseResult> TestRegistry::run_tests(TestController& controller) +{ + std::vector<TestCaseResult> res; + for (auto& e: entries) + { + e->register_tests(); + // TODO: filter on e.name + res.emplace_back(std::move(e->run_tests(controller))); + } + return res; +} + +TestCaseResult TestCase::run_tests(TestController& controller) +{ + TestCaseResult res(name); + + if (!controller.test_case_begin(*this, res)) + { + res.skipped = true; + controller.test_case_end(*this, res); + return res; + } + + try { + setup(); + } catch (std::exception& e) { + res.set_setup_failed(e); + controller.test_case_end(*this, res); + return res; + } + + for (auto& m: methods) + { + // TODO: filter on m.name + res.add_test_method(run_test(controller, m)); + } + + try { + teardown(); + } catch (std::exception& e) { + res.set_teardown_failed(e); + } + + controller.test_case_end(*this, res); + return res; +} + +TestMethodResult TestCase::run_test(TestController& controller, TestMethod& method) +{ + TestMethodResult res(name, method.name); + + if (!controller.test_method_begin(method, res)) + { + res.skipped = true; + controller.test_method_end(method, res); + return res; + } + + bool run = true; + try { + method_setup(res); + } catch (std::exception& e) { + res.set_setup_exception(e); + run = false; + } + + if (run) + { + try { + method.test_function(); + } catch (TestFailed& e) { + // Location::fail_test() was called + res.set_failed(e); + } catch (std::exception& e) { + // std::exception was thrown + res.set_exception(e); + } catch (...) { + // An unknown exception was thrown + res.set_unknown_exception(); + } + } + + try { + method_teardown(res); + } catch (std::exception& e) { + res.set_teardown_exception(e); + } + + controller.test_method_end(method, res); + return res; +} + +bool SimpleTestController::test_method_should_run(const std::string& fullname) const +{ + if (!whitelist.empty() && fnmatch(whitelist.c_str(), fullname.c_str(), 0) == FNM_NOMATCH) + return false; + + if (!blacklist.empty() && fnmatch(blacklist.c_str(), fullname.c_str(), 0) != FNM_NOMATCH) + return false; + + return true; +} + +bool SimpleTestController::test_case_begin(const TestCase& test_case, const TestCaseResult& test_case_result) +{ + // Skip test case if all its methods should not run + bool should_run = false; + for (const auto& m : test_case.methods) + should_run |= test_method_should_run(test_case.name + "." + m.name); + if (!should_run) return false; + + fprintf(stdout, "%s: ", test_case.name.c_str()); + fflush(stdout); + return true; +} + +void SimpleTestController::test_case_end(const TestCase& test_case, const TestCaseResult& test_case_result) +{ + if (test_case_result.skipped) + ; + else if (test_case_result.is_success()) + fprintf(stdout, "\n"); + else + fprintf(stdout, "\n"); + fflush(stdout); +} + +bool SimpleTestController::test_method_begin(const TestMethod& test_method, const TestMethodResult& test_method_result) +{ + string name = test_method_result.test_case + "." + test_method.name; + return test_method_should_run(name); +} + +void SimpleTestController::test_method_end(const TestMethod& test_method, const TestMethodResult& test_method_result) +{ + if (test_method_result.skipped) + putc('s', stdout); + else if (test_method_result.is_success()) + putc('.', stdout); + else + putc('x', stdout); + fflush(stdout); +} + +} +} |