Add a simple unit testing framework
This commit is contained in:
parent
26d06b74d3
commit
c6a8590b2c
3 changed files with 195 additions and 0 deletions
28
tools/test/CMakeLists.txt
Normal file
28
tools/test/CMakeLists.txt
Normal file
|
|
@ -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()
|
||||||
49
tools/test/include/psemek/test/test.hpp
Normal file
49
tools/test/include/psemek/test/test.hpp
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
|
||||||
|
#include <psemek/util/to_string.hpp>
|
||||||
|
|
||||||
|
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<bool>(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)
|
||||||
118
tools/test/source/main.cpp
Normal file
118
tools/test/source/main.cpp
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
#include <psemek/test/test.hpp>
|
||||||
|
#include <psemek/util/pretty_print.hpp>
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <iostream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
namespace psemek::test
|
||||||
|
{
|
||||||
|
|
||||||
|
static std::map<std::string, void(*)()> 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<std::string> 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;
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue