diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..f68e8f7 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,18 @@ +name: main +on: + pull_request: + branches: [ main ] +jobs: + pre_commit_run: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - uses: pre-commit/action@v3.0.0 + unit_tests: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - run: sudo apt-get install clang-19 -y + - run: cmake -S . -B build && cmake --build build + - run: ./build/willow_test diff --git a/.gitignore b/.gitignore index 6622204..ec9da65 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ iris.log -build/ +build/ +.gdb_history +.cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3d928b2..1fee693 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,5 @@ --- +exclude: ^include|^build repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 @@ -16,19 +17,10 @@ repos: hooks: - id: clang-format exclude: ".json" - - repo: local - hooks: - - id: clang-tidy - name: clang-tidy - language: system - entry: clang-tidy -p build/compile_commands.json -extra-arg=-std=c++23 - files: cpp - exclude: "^build/" - repo: https://github.com/cmake-lint/cmake-lint rev: 1.4.3 hooks: - id: cmakelint - exclude: "^include/" - repo: https://github.com/asottile/reorder-python-imports rev: v3.16.0 hooks: diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..bf6718d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.30) +set(CMAKE_CXX_COMPILER "/usr/bin/clang++") +project(willow-test LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_compile_options(-g) +add_compile_options(-Wall) +add_compile_options(-Wextra) +add_compile_options(-Wconversion) +add_compile_options(-Wimplicit-fallthrough) + +add_subdirectory(src/willow) +add_executable(willow_test src/main.cpp) +target_link_libraries(willow_test PRIVATE willow) diff --git a/README.md b/README.md index 46be4a5..cc91cfb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # Willow -A unit testing library for modern c++23 +A unit testing library for modern c++23 diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..0e83379 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,21 @@ +#include "tests/test_reporters.h" +#include "tests/test_test.h" +#include "tests/test_willow.h" +#include "willow/reporters.h" +#include "willow/willow.h" + +auto main() -> int { + Willow::PreCommitReporter reporter = {}; + + return Willow::runTests( + { + {"test_runTests", test_runTests}, + {"test_alert", test_alert}, + {"test_toString", test_toString}, + {"test_Test_Operator()", test_Test_Operator}, + {"PreCommitReporter::print", TestPreCommitReporter::test_print}, + {"PreCommitReporter::cleanup", TestPreCommitReporter::test_cleanup}, + {"PreCommitReporter::highlight", TestPreCommitReporter::test_highlight}, + }, + reporter); +} diff --git a/src/tests/testReporter.h b/src/tests/testReporter.h new file mode 100644 index 0000000..b5abc86 --- /dev/null +++ b/src/tests/testReporter.h @@ -0,0 +1,13 @@ +#ifndef WILLOW_TEST_SILENT_REPORTER_H +#define WILLOW_TEST_SILENT_REPORTER_H + +#include "willow/reporters.h" + +class SilentReporter : public Willow::Reporter { + public: + constexpr SilentReporter() {} + constexpr inline auto print([[maybe_unused]] const Willow::Test& test) -> void {} + constexpr inline auto cleanup() -> void {} +}; + +#endif // WILLOW_TEST_SILENT_REPORTER_H diff --git a/src/tests/test_reporters.h b/src/tests/test_reporters.h new file mode 100644 index 0000000..13937a5 --- /dev/null +++ b/src/tests/test_reporters.h @@ -0,0 +1,14 @@ +#include "willow/reporters.h" + +// PreCommitReporter +class TestPreCommitReporter : public Willow::PreCommitReporter { + public: + static constexpr auto test_print([[maybe_unused]] Willow::Test* test) -> int { + // NOTE: WE can test the logic and what's held in `results` + return 0; + } + + static constexpr auto test_cleanup([[maybe_unused]] Willow::Test* test) -> int { return 0; } + + static constexpr auto test_highlight([[maybe_unused]] Willow::Test* test) -> int { return 0; } +}; diff --git a/src/tests/test_test.h b/src/tests/test_test.h new file mode 100644 index 0000000..62e2642 --- /dev/null +++ b/src/tests/test_test.h @@ -0,0 +1,30 @@ +#include "willow/test.h" + +constexpr auto test_toString([[maybe_unused]] Willow::Test* test) -> int { + if (Willow::toString(Willow::Status::None) != "None") { + return 1; + } + if (Willow::toString(Willow::Status::Pass) != "Passed") { + return 2; + } + if (Willow::toString(Willow::Status::Fail) != "Failed") { + return 3; + } + if (Willow::toString(Willow::Status::Skip) != "Skipped") { + return 4; + } + return 0; +} + +namespace FixtureFuncs { + constexpr auto op_bracket([[maybe_unused]] Willow::Test* t) -> int { + return 42; + } +} // namespace FixtureFuncs + +constexpr auto test_Test_Operator([[maybe_unused]] Willow::Test* test) -> int { + Willow::Test t = {"t", FixtureFuncs::op_bracket}; + t(); + + return !(t.retcode == 42); +} diff --git a/src/tests/test_willow.h b/src/tests/test_willow.h new file mode 100644 index 0000000..1ff04a4 --- /dev/null +++ b/src/tests/test_willow.h @@ -0,0 +1,32 @@ +#include "testReporter.h" +#include "willow/willow.h" + +namespace FixtureFuncs { + constexpr auto pass([[maybe_unused]] Willow::Test* t) -> int { + return 0; + } + constexpr auto fail([[maybe_unused]] Willow::Test* t) -> int { + return 1; + } +}; // namespace FixtureFuncs + +constexpr auto test_runTests([[maybe_unused]] Willow::Test* test) -> int { + SilentReporter r = {}; + int ret = Willow::runTests( + {{"pass", FixtureFuncs::pass}, + {"fail", FixtureFuncs::fail}, + {"skip", FixtureFuncs::fail, Willow::Status::Skip}}, + r); + + return !(ret == 1); +} + +constexpr auto test_alert([[maybe_unused]] Willow::Test* test) -> int { + Willow::Test t {}; + Willow::alert(&t, "fail"); + + if (t.msg.has_value() && t.msg.value() == "fail") { + return 0; + } + return 1; +} diff --git a/src/willow/CMakeLists.txt b/src/willow/CMakeLists.txt new file mode 100644 index 0000000..868b0c7 --- /dev/null +++ b/src/willow/CMakeLists.txt @@ -0,0 +1,7 @@ +add_library(willow STATIC + willow.cpp +) + +target_include_directories(willow PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) diff --git a/src/willow/reporters.h b/src/willow/reporters.h new file mode 100644 index 0000000..63f8e6b --- /dev/null +++ b/src/willow/reporters.h @@ -0,0 +1,129 @@ +#ifndef WILLOW_REPORTER_H +#define WILLOW_REPORTER_H + +#include + +#include "test.h" + +namespace Willow { + class Reporter { + public: + virtual constexpr auto print(const Test& test) -> void = 0; + virtual constexpr auto cleanup() -> void = 0; + virtual constexpr ~Reporter() = default; + }; + + class DefaultReporter : public Reporter { + private: + int test_count = 0; + + public: + inline constexpr auto print(const Test& test) -> void { + std::println("[{}] {}\t{}", ++test_count, test.name, toString(test.status)); + } + + inline constexpr auto cleanup() -> void {} + }; + + // A reporter that displays based on the output from pre-commit + class PreCommitReporter : public Reporter { + protected: + struct Results { + int pass = 0; + int fail = 0; + int skip = 0; + }; + + const std::size_t screen_len = 80; + Results results = {}; + + public: + inline constexpr auto print(const Test& test) -> void { + const std::size_t name_len = test.name.size(); + const std::size_t status_len = toString(test.status).size(); + const std::string ansi = highlight(test.status); + + std::println( + "{}{}{}{}\x1b[0m", test.name, + std::string(screen_len - (name_len + status_len), '.'), ansi, + toString(test.status)); + + if (test.status == Status::Fail) { + results.fail++; + std::println("\x1b[31m\tReturn code: {}\x1b[0m", test.retcode); + if (test.msg.has_value()) { + std::println("\x1b[31m\t{}\x1b[0m", test.msg.value()); + } + + } else if (test.status == Status::Skip) { + results.skip++; + if (test.msg.has_value()) { + std::println("\x1b[33m\t{}\x1b[0m", test.msg.value()); + } + } else { + results.pass++; + } + } + + inline constexpr auto cleanup() -> void { + Status final = + (results.fail ? Status::Fail : (results.skip ? Status::Skip : Status::Pass)); + + auto make_bar = [&](std::size_t len) { return std::string((len - 2) / 2, '='); }; + + switch (final) { + case Status::Pass: { + std::string msg = "All tests passed!"; + std::println( + "\x1b[32m{} {} {}\x1b[0m", make_bar((screen_len - msg.size()) + 1), msg, + make_bar(screen_len - msg.size())); + break; + } + + case Status::Skip: { + std::string msg = std::to_string(results.pass) + " tests passed (" + + std::to_string(results.skip) + " skipped)"; + std::println( + "\x1b[33m{} {} {}\x1b[0m", make_bar(screen_len - msg.size()), msg, + make_bar(screen_len - msg.size())); + break; + } + + case Status::Fail: { + std::string msg = std::to_string(results.fail) + " tests failed, (" + + std::to_string(results.pass) + " passed)"; + std::println( + "\x1b[31m{} {} {}\x1b[0m", make_bar(screen_len - msg.size()), msg, + make_bar(screen_len - msg.size())); + break; + } + + case Status::None: + default: + break; + } + } + + inline constexpr auto highlight(const Status& st) -> std::string { + switch (st) { + case Status::None: + return "\x1b[0m"; + break; + case Status::Pass: + return "\x1b[42m"; + break; + case Status::Fail: + return "\x1b[41m"; + break; + case Status::Skip: + return "\x1b[43m"; + break; + } + + return ""; + } + }; + +} // namespace Willow + +#endif // WILLOW_REPORTER_H diff --git a/src/willow/test.h b/src/willow/test.h new file mode 100644 index 0000000..bb8fa15 --- /dev/null +++ b/src/willow/test.h @@ -0,0 +1,48 @@ +#ifndef WILLOW_TEST_H +#define WILLOW_TEST_H + +#include + +namespace Willow { + // forward declaration for type alias + struct Test; + using TestFn = int (*)(Test*); + + enum class Status { None, Pass, Fail, Skip }; + + constexpr auto toString(const Status& st) -> std::string { + switch (st) { + case Status::None: + return "None"; + break; + case Status::Pass: + return "Passed"; + break; + case Status::Fail: + return "Failed"; + break; + case Status::Skip: + return "Skipped"; + break; + }; + + return ""; + } + + struct Test { + std::string name; + TestFn fn; + int retcode = 0; + std::optional msg = std::nullopt; + Status status = Status::None; + + constexpr Test() : name(""), fn(NULL) {} + constexpr Test(std::string given_name, TestFn f) : name {given_name}, fn {f} {} + constexpr Test(std::string given_name, TestFn f, Status st) + : name {given_name}, fn {f}, status {st} {} + + auto operator()() -> void { retcode = fn(this); } + }; +}; // namespace Willow + +#endif // WILLOW_TEST_H diff --git a/src/willow/willow.cpp b/src/willow/willow.cpp new file mode 100644 index 0000000..858d882 --- /dev/null +++ b/src/willow/willow.cpp @@ -0,0 +1,8 @@ +#include "willow.h" + +#include + +#include "reporters.h" +#include "test.h" + +namespace Willow {} // namespace Willow diff --git a/src/willow/willow.h b/src/willow/willow.h new file mode 100644 index 0000000..452894c --- /dev/null +++ b/src/willow/willow.h @@ -0,0 +1,39 @@ +#ifndef WILLOW_H +#define WILLOW_H + +#include + +#include "reporters.h" +#include "test.h" + +namespace Willow { + constexpr auto runTests(std::vector tests, Reporter& reporter) -> int { + for (auto&& test : tests) { + if (test.status == Status::Skip) { + continue; + } + + test(); + if (test.retcode != 0) { + test.status = Status::Fail; + continue; + } + + test.status = Status::Pass; + } + + std::for_each(tests.begin(), tests.end(), [&reporter](Test& t) { reporter.print(t); }); + reporter.cleanup(); + + return int32_t(std::count_if(tests.begin(), tests.end(), [](Test& t) { + return t.status == Status::Fail; + })); + } + + constexpr auto alert(Test* test, std::string message) -> void { + test->msg = message; + } + +} // namespace Willow + +#endif // WILLOW_H