diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index dc00011..fc2e0fc 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -21,3 +21,4 @@ add_subdirectory(./app) # apps add_subdirectory(./mirror) +add_subdirectory(test) diff --git a/modules/test/CMakeLists.txt b/modules/test/CMakeLists.txt new file mode 100644 index 0000000..8fa3251 --- /dev/null +++ b/modules/test/CMakeLists.txt @@ -0,0 +1,4 @@ +add_library_module(test test.cpp entrypoint.cpp) + +add_executable(test_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/test.cpp) +target_link_libraries(test_tests PRIVATE test) diff --git a/modules/test/include/test/expects.hpp b/modules/test/include/test/expects.hpp new file mode 100644 index 0000000..40f6086 --- /dev/null +++ b/modules/test/include/test/expects.hpp @@ -0,0 +1,128 @@ +#pragma once + +#include +#include +#include + +namespace lt::test { + +template +concept Printable = requires(std::ostream &os, T t) { + { os << t } -> std::same_as; +}; + +template +concept Testable = Printable && std::equality_comparable; + +constexpr void expect_eq( + Testable auto lhs, + Testable auto rhs, + std::source_location source_location = std::source_location::current() +) +{ + if (lhs != rhs) + { + throw std::runtime_error { + std::format( + "Failed equality expectation:\n" + "\tactual: {}\n" + "\texpected: {}\n" + "\tlocation: {}:{}", + lhs, + rhs, + source_location.file_name(), + source_location.line() + ), + }; + } +} + +constexpr void expect_ne( + Testable auto lhs, + Testable auto rhs, + std::source_location source_location = std::source_location::current() +) +{ + if (lhs == rhs) + { + throw std::runtime_error { + std::format( + "Failed un-equality expectation:\n" + "\tactual: {}\n" + "\texpected: {}\n" + "\tlocation: {}:{}", + lhs, + rhs, + source_location.file_name(), + source_location.line() + ), + }; + } +} + +constexpr void expect_true( + bool expression, + std::source_location source_location = std::source_location::current() +) +{ + if (!expression) + { + throw std::runtime_error { + std::format( + "Failed true expectation:\n" + "\tactual: {}\n" + "\texpected: true\n" + "\tlocation: {}:{}", + expression, + source_location.file_name(), + source_location.line() + ), + }; + } +} + +constexpr void expect_false( + bool expression, + std::source_location source_location = std::source_location::current() +) +{ + if (expression) + { + throw std::runtime_error { + std::format( + "Failed false expectation:\n" + "\tactual: {}\n" + "\texpected: true\n" + "\tlocation: {}:{}", + expression, + source_location.file_name(), + source_location.line() + ), + }; + } +} + +constexpr void expect_le( + Testable auto lhs, + Testable auto rhs, + std::source_location source_location = std::source_location::current() +) +{ + if (lhs > rhs) + { + throw std::runtime_error { + std::format( + "Failed false expectation:\n" + "\tactual: {}\n" + "\texpected: >= {}\n" + "\tlocation: {}:{}", + lhs, + rhs, + source_location.file_name(), + source_location.line() + ), + }; + } +} + +} // namespace lt::test diff --git a/modules/test/include/test/test.hpp b/modules/test/include/test/test.hpp new file mode 100644 index 0000000..57d3988 --- /dev/null +++ b/modules/test/include/test/test.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include +#include + +namespace lt::test { + +namespace concepts { + +template +concept printable = requires(std::ostream &os, T t) { + { os << t } -> std::same_as; +}; + +template< + class T, + auto expr = + [] { + }> +concept test = requires(T test) { + { test.name } -> printable; + + { test = expr } -> std::same_as; +}; + +} // namespace concepts + + +struct Case +{ + auto operator=(std::invocable auto test) -> void // NOLINT + { + std::cout << "Running... " << name; + + try + { + test(); + } + catch (const std::exception &exp) + { + std::cout << " --> FAIL !" << '\n'; + std::cout << exp.what() << "\n\n"; + return; // TODO(Light): Should we run the remaining tests after a failure? + } + + std::cout << " --> SUCCESS :D" << "\n"; + } + + std::string_view name; +}; + +namespace details { + + +class Registry +{ +public: + using Suite = void (*)(); + + static void register_suite(Suite suite) + { + instance().m_suites.emplace_back(suite); + } + + static void run_all() + { + for (auto &test : instance().m_suites) + { + test(); + } + } + +private: + Registry() = default; + + [[nodiscard]] static auto instance() -> Registry & + { + static auto registry = Registry {}; + return registry; + } + + std::vector m_suites; +}; + + +} // namespace details + +struct TestSuite +{ + template + constexpr TestSuite(TSuite suite) + { +#ifndef LIGHT_SKIP_TESTS + details::Registry::register_suite(+suite); +#endif + } +}; + +using Suite = const TestSuite; + +} // namespace lt::test diff --git a/modules/test/src/entrypoint.cpp b/modules/test/src/entrypoint.cpp new file mode 100644 index 0000000..fc2a861 --- /dev/null +++ b/modules/test/src/entrypoint.cpp @@ -0,0 +1,15 @@ +#include + +auto main() -> int32_t +try +{ + using namespace ::lt::test; + using namespace ::lt::test::details; + + Registry::run_all(); +} +catch (const std::exception &exp) +{ + std::cout << "Terminated after uncaught exception:\n"; // NOLINT + std::cout << "exception.what: " << exp.what(); +} diff --git a/modules/test/src/test.cpp b/modules/test/src/test.cpp new file mode 100644 index 0000000..e69de29 diff --git a/modules/test/tests/test.cpp b/modules/test/tests/test.cpp new file mode 100644 index 0000000..95fd8c6 --- /dev/null +++ b/modules/test/tests/test.cpp @@ -0,0 +1,25 @@ +#include + +lt::test::Suite meta = []() { + using lt::test::expect_eq; + + lt::test::Case { "test_1" } = [] { + expect_eq(5, 5); + }; + + lt::test::Case { "test_2" } = [] { + expect_eq(20.0, 20.0); + }; + + lt::test::Case { "test_3" } = [] { + expect_eq(true, false); + }; + + lt::test::Case { "test_4" } = [] { + expect_eq(true, 1); + }; + + lt::test::Case { "test_5" } = [] { + throw std::runtime_error("Uncaught std exception!"); + }; +};