From cac4a8dd2ac6670c5db8d2370cdd12451f9f50cf Mon Sep 17 00:00:00 2001 From: Claudius Holeksa Date: Tue, 9 May 2023 17:24:41 +0200 Subject: Added readme and moved src files --- README.md | 60 ++++++++++++++++++++++ src/keltest/macro.h | 20 ++++++++ src/keltest/test.cpp | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/keltest/test.h | 22 ++++++++ src/macro.h | 20 -------- src/test.cpp | 139 --------------------------------------------------- src/test.h | 22 -------- 7 files changed, 241 insertions(+), 181 deletions(-) create mode 100644 README.md create mode 100644 src/keltest/macro.h create mode 100644 src/keltest/test.cpp create mode 100644 src/keltest/test.h delete mode 100644 src/macro.h delete mode 100644 src/test.cpp delete mode 100644 src/test.h diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f9c1b9 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# keldu's minimal testing framework + +I jokingly suggested using my mini framework which is baked in a private repo for a project. +Though I didn't want to seriously suggest it, a miniscule percentage suggested to at least make it +available. +So here we are. + +This works similarly to other test frameworks. + +# Building +## If keltest is available in env + +This really is the recommended case. Put things in your env. That's why it exists. Drop in your build container if you must, but just put it in your env. + +Currently just compile each test file with a compiler of your choice with + +`c++ -std=c++17 test_file_a.cpp -c -o test_file_a.o` +`c++ -std=c++17 test_file_b.cpp -c -o test_file_b.o` + +and generate a test binary with + +`c++ -std=c++17 -DKEL_COMPILE_TEST_BINARY keltest.a test_file_a.o test_file_b.o -o tests` + +## Modified commands if not available in env + +`c++ -std=c++17 test_file_a.cpp -I${keltest_src} -c -o test_file_a.o` +`c++ -std=c++17 test_file_b.cpp -I${keltest_src} -c -o test_file_b.o` +`c++ -std=c++17 -DKEL_COMPILE_TEST_BINARY ${keltest_src}/keltest/test.cpp test_file_a.o test_file_b.o -o tests` + +# Writing tests + +It is necessary to wrap the tests in an anonymous namespace due to how the test case names are generated. An example test file would be + +``` +// test_file_hello.cpp +#include + +#include + +std::string greet(const std::string& val){ + return val; +} + +namespace { +KEL_TEST("Greeting"){ + std::string hello = "hello"; + + std::string answer = greet(hello); + + KEL_EXPECT(answer == "hello", "Person did not reply with a hello"); +} +KEL_TEST("Weird Greeting"){ + std::string hello = "How are you?"; + + std::string answer = greet(hello); + + KEL_EXPECT(answer == "Fine. How are you?", "Person did not reply with an expected answer"); +} +} +``` diff --git a/src/keltest/macro.h b/src/keltest/macro.h new file mode 100644 index 0000000..0421388 --- /dev/null +++ b/src/keltest/macro.h @@ -0,0 +1,20 @@ +#pragma once + +#define KEL_CONCAT_(x, y) x##y +#define KEL_CONCAT(x, y) KEL_CONCAT_(x, y) + +#define KEL_UNIQUE_NAME(prefix) KEL_CONCAT(prefix, __LINE__) + +#define KEL_TEST(description) \ + class KEL_UNIQUE_NAME(test_case) : public ::keltest::test_case { \ + public: \ + KEL_UNIQUE_NAME(test_case)(): ::keltest::test_case(__FILE__,__LINE__,description) {} \ + void run() override; \ + } KEL_UNIQUE_NAME(test_case_); \ + void KEL_UNIQUE_NAME(test_case)::run() + +#define KEL_EXPECT(expr, msg_split) \ + if( ! (expr) ){ \ + auto msg = msg_split; \ + throw std::runtime_error{std::string{msg}}; \ + } diff --git a/src/keltest/test.cpp b/src/keltest/test.cpp new file mode 100644 index 0000000..1178b20 --- /dev/null +++ b/src/keltest/test.cpp @@ -0,0 +1,139 @@ +#include "test.h" + +#include +#include +#include + +namespace keltest { + +template +constexpr bool always_false = false; + +test_case* test_case_head = nullptr; +test_case** test_case_tail = &test_case_head; + +test_case::test_case(std::string file_, uint32_t line_, std::string& description_): + file{std::move(file_)}, + line{line_}, + description{std::move(description_)}, + next{nullptr}, + prev{test_case_tail} +{ + /** + * Since I always forget how this works. + */ + /** + * If the list is empty, then this command sets the head to this + * If the list is not empty it sets the previous test_case "next" to this location + */ + *prev = this; + /** + * The tail is set this->next location which is used in a later test_case's constructor + */ + test_case_tail = &next; +} + +test_case::~test_case(){ + *prev = next; + if( next == nullptr ){ + test_case_tail = prev; + }else { + next->prev = prev; + } +} + +namespace colour { +struct red {}; +struct green {}; +struct blue {}; +struct white {}; + +using variant = std::variant; +} + + +class test_runner { +private: + void write(const colour::variant& col, const std::string& front, const std::string& message){ + std::string_view start_col, end_col; + start_col = std::visit([](auto& col) -> std::string_view { + using T = std::decay_t; + + if constexpr ( std::is_same_v ){ + return "\033[0;1;31m"; + } + else if constexpr ( std::is_same_v ){ + return "\033[0;1;32m"; + } + else if constexpr ( std::is_same_v ){ + return "\033[0;1;34m"; + } + else if constexpr ( std::is_same_v ){ + return "\033[0m"; + } + else { + static_assert(always_false, "Case exhausted"); + } + return "\033[0m"; + }, col); + end_col = "\033[0m"; + + std::cout<next){ + std::string name = test->fil + std::string{":"} + std::to_string(test->line) + std::string{":"} + test->description; + write(colour::blue, "[ TEST ] ", name); + bool failed = true; + + std::string fail_message; + auto start_clock = std::chrono::steady_clock::now(); + + try { + test->run(); + failed = false; + }catch(std::exception& e){ + fail_message = e.what(); + failed = true; + } + auto stop_clock = std::chrono::steady_clock::now(); + + auto runtime_duration_intern = stop_clock - start_clock; + + auto runtime_duration = std::chrono::duration_cast(runtime_duration_intern); + + std::string message = name + std::string{" ("} + std::to_string(runtime_duration.count()) + std::string{" µs) "}; + + if( failed ){ + write(colour::red, "[ FAIL ] ", message + std::string{" "} + fail_message); + ++failed_count; + }else{ + write(colour::green, "[ PASS ] ", message); + ++passed_count; + } + } + + if( passed_count > 0 ) write(colour::green, std::to_string(passed_count) + std::string{" test(s) passed"}, ""); + + if( failed_count > 0 ) write(colour::red, std::to_string(failed_count) + std::string{" test(s) failed"}, ""); + + return failed_count > 0 ? -1 : 0; + } +}; +} + +#if KELTEST_COMPILE_TEST_BINARY + +int main() { + ::keltest::test_runner runner; + int rv = runner.run(); + + return rv != 0 ? -1 : 0; +} + +#endif diff --git a/src/keltest/test.h b/src/keltest/test.h new file mode 100644 index 0000000..24f008f --- /dev/null +++ b/src/keltest/test.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +namespace keltest { +class test_runner; +class test_case { +private: + std::string file; + uint32_t line; + std::string description; + test_case* next; + test_case** prev; +public: + test_case(std::string file_, uint32_t line_, std::string description_); + ~test_case(); + + virtual void run() = 0; +}; +} +#include "macro.h" diff --git a/src/macro.h b/src/macro.h deleted file mode 100644 index 0421388..0000000 --- a/src/macro.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#define KEL_CONCAT_(x, y) x##y -#define KEL_CONCAT(x, y) KEL_CONCAT_(x, y) - -#define KEL_UNIQUE_NAME(prefix) KEL_CONCAT(prefix, __LINE__) - -#define KEL_TEST(description) \ - class KEL_UNIQUE_NAME(test_case) : public ::keltest::test_case { \ - public: \ - KEL_UNIQUE_NAME(test_case)(): ::keltest::test_case(__FILE__,__LINE__,description) {} \ - void run() override; \ - } KEL_UNIQUE_NAME(test_case_); \ - void KEL_UNIQUE_NAME(test_case)::run() - -#define KEL_EXPECT(expr, msg_split) \ - if( ! (expr) ){ \ - auto msg = msg_split; \ - throw std::runtime_error{std::string{msg}}; \ - } diff --git a/src/test.cpp b/src/test.cpp deleted file mode 100644 index 1178b20..0000000 --- a/src/test.cpp +++ /dev/null @@ -1,139 +0,0 @@ -#include "test.h" - -#include -#include -#include - -namespace keltest { - -template -constexpr bool always_false = false; - -test_case* test_case_head = nullptr; -test_case** test_case_tail = &test_case_head; - -test_case::test_case(std::string file_, uint32_t line_, std::string& description_): - file{std::move(file_)}, - line{line_}, - description{std::move(description_)}, - next{nullptr}, - prev{test_case_tail} -{ - /** - * Since I always forget how this works. - */ - /** - * If the list is empty, then this command sets the head to this - * If the list is not empty it sets the previous test_case "next" to this location - */ - *prev = this; - /** - * The tail is set this->next location which is used in a later test_case's constructor - */ - test_case_tail = &next; -} - -test_case::~test_case(){ - *prev = next; - if( next == nullptr ){ - test_case_tail = prev; - }else { - next->prev = prev; - } -} - -namespace colour { -struct red {}; -struct green {}; -struct blue {}; -struct white {}; - -using variant = std::variant; -} - - -class test_runner { -private: - void write(const colour::variant& col, const std::string& front, const std::string& message){ - std::string_view start_col, end_col; - start_col = std::visit([](auto& col) -> std::string_view { - using T = std::decay_t; - - if constexpr ( std::is_same_v ){ - return "\033[0;1;31m"; - } - else if constexpr ( std::is_same_v ){ - return "\033[0;1;32m"; - } - else if constexpr ( std::is_same_v ){ - return "\033[0;1;34m"; - } - else if constexpr ( std::is_same_v ){ - return "\033[0m"; - } - else { - static_assert(always_false, "Case exhausted"); - } - return "\033[0m"; - }, col); - end_col = "\033[0m"; - - std::cout<next){ - std::string name = test->fil + std::string{":"} + std::to_string(test->line) + std::string{":"} + test->description; - write(colour::blue, "[ TEST ] ", name); - bool failed = true; - - std::string fail_message; - auto start_clock = std::chrono::steady_clock::now(); - - try { - test->run(); - failed = false; - }catch(std::exception& e){ - fail_message = e.what(); - failed = true; - } - auto stop_clock = std::chrono::steady_clock::now(); - - auto runtime_duration_intern = stop_clock - start_clock; - - auto runtime_duration = std::chrono::duration_cast(runtime_duration_intern); - - std::string message = name + std::string{" ("} + std::to_string(runtime_duration.count()) + std::string{" µs) "}; - - if( failed ){ - write(colour::red, "[ FAIL ] ", message + std::string{" "} + fail_message); - ++failed_count; - }else{ - write(colour::green, "[ PASS ] ", message); - ++passed_count; - } - } - - if( passed_count > 0 ) write(colour::green, std::to_string(passed_count) + std::string{" test(s) passed"}, ""); - - if( failed_count > 0 ) write(colour::red, std::to_string(failed_count) + std::string{" test(s) failed"}, ""); - - return failed_count > 0 ? -1 : 0; - } -}; -} - -#if KELTEST_COMPILE_TEST_BINARY - -int main() { - ::keltest::test_runner runner; - int rv = runner.run(); - - return rv != 0 ? -1 : 0; -} - -#endif diff --git a/src/test.h b/src/test.h deleted file mode 100644 index 24f008f..0000000 --- a/src/test.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include -#include - -namespace keltest { -class test_runner; -class test_case { -private: - std::string file; - uint32_t line; - std::string description; - test_case* next; - test_case** prev; -public: - test_case(std::string file_, uint32_t line_, std::string description_); - ~test_case(); - - virtual void run() = 0; -}; -} -#include "macro.h" -- cgit v1.2.3