diff --git a/tools/test/CMakeLists.txt b/tools/test/CMakeLists.txt new file mode 100644 index 00000000..94d0a861 --- /dev/null +++ b/tools/test/CMakeLists.txt @@ -0,0 +1,28 @@ +option(PSEMEK_BUILD_TESTS "Build tests" OFF) + +if(PSEMEK_BUILD_TESTS) + + add_executable(psemek-run-tests source/main.cpp include/psemek/test/test.hpp) + set(PSEMEK_TESTS_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include") + target_include_directories(psemek-run-tests PUBLIC "${PSEMEK_TESTS_INCLUDE_DIR}") + + function(psemek_glob_tests lib dir) + file(GLOB_RECURSE _PSEMEK_ADD_TESTS_SOURCES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "${dir}/*.cpp") + list(LENGTH _PSEMEK_ADD_TESTS_SOURCES _PSEMEK_ADD_TESTS_SOURCES_LENGTH) + if(_PSEMEK_ADD_TESTS_SOURCES_LENGTH EQUAL 0) + message(WARNING "No tests found in ${dir}") + else() + target_sources(psemek-run-tests PUBLIC ${_PSEMEK_ADD_TESTS_SOURCES}) + target_link_libraries(psemek-run-tests PUBLIC ${lib}) + endif() + endfunction() + +else() + + function(psemek_add_test source) + endfunction() + + function(psemek_glob_tests dir) + endfunction() + +endif() diff --git a/tools/test/include/psemek/test/test.hpp b/tools/test/include/psemek/test/test.hpp new file mode 100644 index 00000000..f47297d9 --- /dev/null +++ b/tools/test/include/psemek/test/test.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include + +namespace psemek::test +{ + + void add_test_case(char const * name, void(*f)()); + + struct failure + : std::exception + { + failure(std::string message, std::string location) + : message_(std::move(message)) + , location_(std::move(location)) + {} + + char const * what() const noexcept { return message_.data(); } + + std::string_view message() const { return message_; } + std::string_view location() const { return location_; } + + private: + std::string message_; + std::string location_; + }; + +} + +#define test_case(name) \ +void name ## _test_case (); \ +static const auto name ## _test_case_registrator = []{ ::psemek::test::add_test_case(#name, &(name ## _test_case)); return 0; }(); \ +void name ## _test_case () + +#define fail(...) throw ::psemek::test::failure(::psemek::util::to_string(__VA_ARGS__), ::psemek::util::to_string(__FILE__, ":", __LINE__)) + +#define expect(cond) if (!static_cast(cond)) fail("!(" #cond ")") + +#define expect_equal(expr1, expr2) if ((expr1) != (expr2)) fail(#expr1, " (", (expr1), ") != ", #expr2, " (", (expr2), ")") + +#define expect_equal_ptr(expr1, expr2) if ((expr1) != (expr2)) fail(#expr1, " (", (void*)(expr1), ") != ", #expr2, " (", (void*)(expr2), ")") + +#define expect_different(expr1, expr2) if ((expr1) == (expr2)) fail(#expr1, " (", (expr1), ") == ", #expr2, " (", (expr2), ")") + +#define expect_different_ptr(expr1, expr2) if ((expr1) == (expr2)) fail(#expr1, " (", (void*)(expr1), ") == ", #expr2, " (", (void*)(expr2), ")") + +#define expect_throw(expr, type) do { bool thrown = false; try { (void)(expr); } catch (type const &) { thrown = true; } if (!thrown) fail(#expr, " didn't throw ", #type); } while (false) diff --git a/tools/test/source/main.cpp b/tools/test/source/main.cpp new file mode 100644 index 00000000..ed7d7ead --- /dev/null +++ b/tools/test/source/main.cpp @@ -0,0 +1,118 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace psemek::test +{ + + static std::map tests; + + void add_test_case(char const * name, void(*f)()) + { + std::string pname(name); + std::replace(pname.begin(), pname.end(), '_', '/'); + + if (tests.count(pname)) + { + std::cerr << "Test " << pname << " already registered" << std::endl; + std::exit(EXIT_FAILURE); + } + + tests[pname] = f; + } + +} + +int main(int argc, char ** argv) +{ + std::set tests; + if (argc == 1) + { + for (auto const & p : psemek::test::tests) + tests.insert(p.first); + } + else + { + for (int a = 1; a < argc; ++a) + { + std::string pattern = std::string(argv[a]); + if (pattern.empty()) + { + std::cerr << "Empty test case pattern in argument list" << std::endl; + std::exit(EXIT_FAILURE); + } + if (pattern.back() != '/') + pattern += "/"; + for (auto const & p : psemek::test::tests) + { + if (p.first == argv[a] || p.first.starts_with(pattern)) + tests.insert(p.first); + } + } + } + + std::size_t test_count = tests.size(); + + std::size_t const test_index_len = std::ceil(std::log10(test_count + 1)); + + std::string const indent = std::string(2 * test_index_len + 6, ' '); + + std::size_t max_name_length = 0; + for (auto const & name : tests) + max_name_length = std::max(max_name_length, name.size()); + + std::size_t success = 0; + + using clock = std::chrono::high_resolution_clock; + + auto all_start = clock::now(); + + std::size_t i = 0; + for (auto const & name : tests) + { + ++i; + + std::cout + << '[' << std::setfill(' ') << std::setw(test_index_len) << std::right << i << '/' << test_count << "] " + << std::left << std::setfill('.') << std::setw(max_name_length + 5) << name; + + try + { + auto start = clock::now(); + psemek::test::tests[name](); + auto end = clock::now(); + std::cout << "ok (" << psemek::util::pretty(end - start, std::chrono::milliseconds{1}) << ")" << std::endl; + ++success; + } + catch (psemek::test::failure const & e) + { + std::cout << "failure" << std::endl; + std::cout << indent << "Reason: " << e.message() << std::endl; + std::cout << indent << "Location: " << e.location() << std::endl; + } + catch (std::exception const & e) + { + std::cout << "failure: " << e.what() << std::endl; + } + catch (...) + { + std::cout << "failure: (unknown exception)" << std::endl; + } + } + + auto all_end = clock::now(); + + std::cout << std::endl; + std::cout << success << '/' << test_count << " passed (" << psemek::util::pretty(all_end - all_start, std::chrono::milliseconds{1}) << ")" << std::endl; + + if (success <= test_count) + return EXIT_FAILURE; +}