1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
|
// Refactored from:
// Copyright (c) 2024 Kris Jusiak (kris at jusiak dot net)
// Distributed under the Boost Software License, Version 1.0.
// (See http://www.boost.org/LICENSE_1_0.txt)
//
// UT: A simple C++23 unit testing library with compile-time and run-time support.
//
// Running specific tests:
// Set the UT_RUN environment variable to run only specific tests by name.
// Single test: UT_RUN="my test" ./my_tests
// Multiple tests: UT_RUN="[test1,test2,test3]" ./my_tests
// If UT_RUN is not set, all tests run (default behavior).
export module ut;
import std;
namespace ut
{
namespace detail
{
constexpr bool fatal = true;
template <class>
constexpr auto is_mutable_lambda_v = false;
template <class R, class B, class... Ts>
constexpr auto is_mutable_lambda_v<R (B::*)(Ts...)> = true;
template <class R, class B, class... Ts>
constexpr auto is_mutable_lambda_v<R (B::*)(Ts...) const> = false;
template <class Fn>
constexpr auto has_capture_lambda_v = sizeof(Fn) > 1ul;
template <class T, class...>
struct identity
{
using type = T;
};
}
export template <std::size_t Size>
struct fixed_string
{
constexpr fixed_string(const char (&str)[Size])
{
for (std::size_t i = 0; i < Size; ++i) {
storage[i] = str[i];
}
}
[[nodiscard]] constexpr auto operator[](const auto i) const { return storage[i]; }
[[nodiscard]] constexpr auto data() const { return storage; }
[[nodiscard]] static constexpr auto size() { return Size - 1; }
[[nodiscard]] constexpr operator std::string_view() const { return {storage, Size - 1}; }
constexpr friend auto operator<<(auto& os, const fixed_string& fs) -> decltype(auto)
{
return os << std::string_view{fs.storage, fs.size()};
}
char storage[Size]{};
};
namespace events
{
enum class mode { run_time, compile_time };
template <mode Mode>
struct test_begin
{
std::string_view file_name{};
std::uint_least32_t line{};
std::string_view name{};
};
template <mode Mode>
struct test_end
{
std::string_view file_name{};
std::uint_least32_t line{};
std::string_view name{};
enum { FAILED, PASSED, COMPILE_TIME } result{};
};
struct assertion
{
bool passed{};
std::string_view file_name{};
std::uint_least32_t line{};
};
struct fatal
{};
template <class Msg>
struct log
{
const Msg& msg;
bool result{};
};
struct summary
{
enum { FAILED, PASSED, COMPILE_TIME };
std::size_t asserts[2]{}; /* FAILED, PASSED */
std::size_t tests[3]{}; /* FAILED, PASSED, COMPILE_TIME */
};
} // namespace events
template <class OStream>
struct outputter
{
template <events::mode Mode>
constexpr auto on(const events::test_begin<Mode>&)
{}
constexpr auto on(const events::test_begin<events::mode::run_time>& event) { current_test = event; }
template <events::mode Mode>
constexpr auto on(const events::test_end<Mode>&)
{}
constexpr auto on(const events::assertion& event)
{
if (not event.passed && not std::is_constant_evaluated()) {
if (initial_new_line == '\n') {
os << initial_new_line;
}
else {
initial_new_line = '\n';
}
os << "FAILED \"" << current_test.name << "\" ";
const auto n = event.file_name.size();
const auto start = n <= 32 ? 0 : n - 32;
if (start > 0) {
os << "...";
}
os << event.file_name.substr(start, n) << ":" << event.line << '\n';
}
}
constexpr auto on(const events::fatal&) {}
template <class Msg>
constexpr auto on(const events::log<Msg>& event)
{
if (!std::is_constant_evaluated() && !event.result) {
os << ' ' << event.msg;
}
}
constexpr auto on(const events::summary& event)
{
using namespace events;
if (!std::is_constant_evaluated()) {
if (event.asserts[summary::FAILED] || event.tests[summary::FAILED]) {
os << "\nFAILED\n";
}
else {
os << "\nPASSED\n";
}
os << "tests: " << (event.tests[summary::PASSED] + event.tests[summary::FAILED]) << " ("
<< event.tests[summary::PASSED] << " passed, " << event.tests[summary::FAILED] << " failed, "
<< event.tests[summary::COMPILE_TIME] << " compile-time)\n"
<< "asserts: " << (event.asserts[summary::PASSED] + event.asserts[summary::FAILED]) << " ("
<< event.asserts[summary::PASSED] << " passed, " << event.asserts[summary::FAILED] << " failed)\n";
}
}
OStream& os;
events::test_begin<events::mode::run_time> current_test{};
char initial_new_line{};
};
template <class Outputter, std::uint32_t MaxDepth = 16>
struct reporter
{
constexpr auto on(const events::test_begin<events::mode::run_time>& event)
{
asserts_failed[current++] = summary.asserts[events::summary::FAILED];
outputter.on(event);
}
constexpr auto on(const events::test_end<events::mode::run_time>& event)
{
const auto result = summary.asserts[events::summary::FAILED] == asserts_failed[--current];
++summary.tests[result];
events::test_end<events::mode::run_time> te{event};
te.result = static_cast<decltype(te.result)>(result);
outputter.on(te);
}
constexpr auto on(const events::test_begin<events::mode::compile_time>&)
{
++summary.tests[events::summary::COMPILE_TIME];
}
constexpr auto on(const events::test_end<events::mode::compile_time>&) {}
constexpr auto on(const events::assertion& event)
{
if (event.passed) {
++summary.asserts[events::summary::PASSED];
}
else {
++summary.asserts[events::summary::FAILED];
}
outputter.on(event);
}
constexpr auto on(const events::fatal& event)
{
++summary.tests[events::summary::FAILED];
outputter.on(event);
outputter.on(summary);
std::exit(1);
}
~reporter()
{ // non constexpr
outputter.on(summary);
if (summary.asserts[events::summary::FAILED]) {
std::exit(1);
}
}
Outputter& outputter;
events::summary summary{};
std::size_t asserts_failed[MaxDepth]{};
std::size_t current{};
};
template <class Reporter>
struct runner
{
template <class Test>
constexpr auto on(Test test, const std::string_view file_name, std::uint_least32_t line, const std::string_view name)
-> bool
{
if (std::is_constant_evaluated()) {
if constexpr (requires { requires detail::is_mutable_lambda_v<decltype(&Test::operator())>; }) {
return false;
}
else {
test();
return true;
}
}
else {
static const std::string_view filter = []() -> std::string_view {
if (const char* env = std::getenv("UT_RUN")) return env;
return {};
}();
auto matches_filter = [](std::string_view test_name, std::string_view f) {
if (f.empty()) return true;
// Array format: [test1,test2,test3]
if (f.starts_with('[') && f.ends_with(']')) {
auto content = f.substr(1, f.size() - 2);
std::size_t pos = 0;
while (pos < content.size()) {
auto comma = content.find(',', pos);
auto token =
(comma == std::string_view::npos) ? content.substr(pos) : content.substr(pos, comma - pos);
if (token == test_name) return true;
if (comma == std::string_view::npos) break;
pos = comma + 1;
}
return false;
}
// Single test name
return test_name == f;
};
if (!matches_filter(name, filter)) {
return false;
}
#if defined(UT_COMPILE_TIME)
if constexpr (!requires { requires detail::is_mutable_lambda_v<decltype(&Test::operator())>; } &&
!detail::has_capture_lambda_v<Test>) {
reporter.on(events::test_begin<events::mode::compile_time>{file_name, line, name});
static_assert((test(), "[FAILED]"));
reporter.on(events::test_end<events::mode::compile_time>{file_name, line, name});
}
#endif
reporter.on(events::test_begin<events::mode::run_time>{file_name, line, name});
test();
reporter.on(events::test_end<events::mode::run_time>{file_name, line, name});
}
return true;
}
Reporter& reporter;
};
}
namespace ut
{
struct cfg_t
{
struct stream_t
{
friend constexpr decltype(auto) operator<<([[maybe_unused]] auto& os, [[maybe_unused]] const auto& t)
{
static_assert(requires { std::clog << t; });
return (std::clog << t);
}
} stream;
ut::outputter<stream_t> outputter{stream};
ut::reporter<decltype(outputter)> reporter{outputter};
ut::runner<decltype(reporter)> runner{reporter};
};
export extern cfg_t cfg;
cfg_t cfg{};
struct expect_fn final
{
template <bool Fatal>
struct eval final
{
template <class T>
requires std::convertible_to<T, bool>
constexpr eval(T&& test_passed, auto&& loc) : passed(static_cast<bool>(test_passed))
{
if (std::is_constant_evaluated()) {
if (not passed) {
std::abort();
}
}
else {
cfg.reporter.on(events::assertion{passed, loc.file_name(), loc.line()});
if (not passed) {
if constexpr (Fatal) {
cfg.reporter.on(events::fatal{});
}
}
}
}
bool passed{};
};
template <class T>
requires std::convertible_to<T, bool>
constexpr auto operator()(T&& test_passed,
const std::source_location& loc = std::source_location::current()) const
{
return log{eval<not detail::fatal>{test_passed, loc}.passed};
}
template <class T>
requires std::convertible_to<T, bool>
constexpr auto operator[](T&& test_passed,
const std::source_location& loc = std::source_location::current()) const
{
return log{eval<detail::fatal>{test_passed, loc}.passed};
}
private:
struct log final
{
bool passed{};
template <class Msg>
constexpr const auto& operator<<(const Msg& msg) const
{
cfg.outputter.on(events::log<Msg>{msg, passed});
return *this;
}
};
};
export inline constexpr expect_fn expect{};
export struct suite final
{
suite(auto&& tests) { tests(); }
};
namespace detail
{
export template <fixed_string Name>
struct test final
{
constexpr auto operator=(auto test) const
{
const auto& loc = std::source_location::current();
return cfg.runner.on(test, loc.file_name(), loc.line(), Name);
}
};
export struct runtime_test final
{
std::string_view name{};
constexpr auto operator=(auto test) const
{
const auto& loc = std::source_location::current();
return cfg.runner.on(test, loc.file_name(), loc.line(), name);
}
};
}
export constexpr auto test(const std::string_view name) { return detail::runtime_test{name}; }
export template <fixed_string Str>
[[nodiscard]] constexpr auto operator""_test()
{
return detail::test<Str>{};
}
#if __cpp_exceptions
export template <class Callable, class... Args>
constexpr auto throws(Callable&& c, Args&&... args)
{
try {
std::forward<Callable>(c)(std::forward<Args>(args)...);
}
catch (...) {
return true;
}
return false;
}
#endif
}
export using ut::operator""_test;
|