Skip to content

qlibs/ut

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

62 Commits
 
 
 
 

Repository files navigation

Boost Licence Version build Try it online


"If you liked it then you "should have put a"_test on it", Beyonce rule

UT2: C++20 minimal/compile-time first unit-testing library

https://en.wikipedia.org/wiki/Unit_testing

Features

Based on the constexpr ability of given compiler/standard

Requirements


Examples

Hello world (https://godbolt.org/z/9d6PTEfbf)

#include <ut2>
#include <iostream> // output at run-time

constexpr auto sum(auto... args) { return (args + ...); }

int main() {
  using namespace ut;

  "sum"_test = [] {
    expect(sum(1) == 1_i);
    expect(sum(1, 2) == 3_i);
    expect(sum(1, 2, 3) == 6_i);
  };
}
$CXX example.cpp -std=c++20 -o example && ./example
PASSED: tests: 1 (1 passed, 0 failed, 1 compile-time), asserts: 3 (3 passed, 0 failed)

Execution model (https://godbolt.org/z/48PGjz3x9)

static_assert(("sum"_test = [] { // compile-time only
  expect(sum(1, 2, 3) == 6_i);
}));

int main() {
  "sum"_test = [] {              // compile time and run-time
    expect(sum(1, 2, 3) == 5_i);
  };

  "sum"_test = [] constexpr {    // compile-time and run-time
    expect(sum(1, 2, 3) == 6_i);
  };

  "sum"_test = [] mutable {      // run-time only
    expect(sum(1, 2, 3) == 6_i);
  };

  "sum"_test = [] consteval {    // compile-time only
    expect(sum(1, 2, 3) == 6_i);
  };
}
$CXX example.cpp -std=c++20 # -DUT_COMPILE_TIME_ONLY
ut2:156:25: error: static_assert((test(), "[FAILED]"));
example.cpp:13:44: note:"sum"_test
example.cpp:14:5:  note: in call to 'expect.operator()<ut::eq<int, int>>({6, 5})'
$CXX example.cpp -std=c++20 -o example -DUT_RUNTIME_ONLY && ./example
example.cpp:14:FAILED:"sum": 6 == 5
FAILED: tests: 3 (2 passed, 1 failed, 0 compile-time), asserts: 2 (1 passed, 1 failed)

Constant evaluation (https://godbolt.org/z/v1snWP5K3)

constexpr auto test() {
  if consteval { return 42; } else { return 87; }
}

int main() {
  "compile-time"_test = [] consteval {
    expect(42_i == test());
  };

  "run-time"_test = [] mutable {
    expect(87_i == test());
  };
}
$CXX example.cpp -std=c++20 -o example && ./example
PASSED: tests: 2 (2 passed, 0 failed, 1 compile-time), asserts: 1 (1 passed, 0 failed)

Suites/Sub-tests (https://godbolt.org/z/Mbzc9s3ad)

ut::suite test_suite = [] {
  "vector [sub-tests]"_test = [] {
    std::vector<int> v(5);
    expect(v.size() == 5_ul);
    expect(v.capacity() >= 5_ul);

    "resizing bigger changes size and capacity"_test = [=] {
      mut(v).resize(10);
      expect(v.size() == 10_ul);
      expect(v.capacity() >= 10_ul);
    };
  };
};

int main() { }
$CXX example.cpp -std=c++20 -o example && ./example
PASSED: tests: 2 (2 passed, 0 failed, 1 compile-time), asserts: 4 (4 passed, 0 failed)

Assertions (https://godbolt.org/z/W8qnMsrGo)

int main() {
  "expect"_test = [] {
    "different ways"_test = [] {
      expect(42_i == 42);
      expect(eq(42, 42))   << "same as expect(42_i == 42)";
      expect(_i(42) == 42) << "same as expect(42_i == 42)";
    };

    "floating point"_test = [] {
      expect((4.2 == 4.2_d)(.01)) << "floating point comparison with .01 epsilon precision";
    };

    "fatal"_test = [] mutable { // at run-time
      std::vector<int> v{1};
      expect[v.size() > 1_ul] << "fatal, aborts further execution";
      expect(v[1] == 42_i); // not executed
    };

    "compile-time expression"_test = [] {
      expect(constant<42 == 42_i>) << "requires compile-time expression";
    };
  };
}
$CXX example.cpp -std=c++20 -o example && ./example
example.cpp:21:FAILED:"fatal": 1 > 1
FAILED: tests: 3 (2 passed, 1 failed, 3 compile-time), asserts: 5 (4 passed, 1 failed)

Errors/Checks (https://godbolt.org/z/Tvnce9j4d)

int main() {
  "leak"_test = [] {
    new int; // compile-time error
  };

  "ub"_test = [] {
    int* i{};
    *i = 42; // compile-time error
  };

  "errors"_test = [] {
    expect(42_i == short(42)); // [ERROR] Comparision of different types is not allowed
    expect(42 == 42);          // [ERROR] Expression required: expect(42_i == 42)
    expect(4.2 == 4.2_d);      // [ERROR] Epsilon is required: expect((4.2 == 4.2_d)(.01))
  };
}

Reflection integration (https://godbolt.org/z/5T51odcfP)

int main() {
  struct foo { int a; int b; };
  struct bar { int a; int b; };

  "reflection"_test = [] {
    auto f = foo{.a=1, .b=2};
    expect(eq(foo{1, 2}, f));
    expect(members(foo{1, 2}) == members(f));
    expect(names(foo{}) == names(bar{}));
  };
};
$CXX example.cpp -std=c++20 -o example && ./example
PASSED: tests: 1 (1 passed, 0 failed, 1 compile-time), asserts: 3 (3 passed, 0 failed)

Custom configuration (https://godbolt.org/z/qMhdYnhMP)

struct outputter {
  template<ut::events::mode Mode>
  constexpr auto on(const ut::events::test_begin<Mode>&) { }
  template<ut::events::mode Mode>
  constexpr auto on(const ut::events::test_end<Mode>&) { }
  template<class TExpr>
  constexpr auto on(const ut::events::assert_pass<TExpr>&) { }
  template<class TExpr>
  constexpr auto on(const ut::events::assert_fail<TExpr>&) { }
  constexpr auto on(const ut::events::fatal&) { }
  constexpr auto on(const ut::events::summary&) { }
  template<class TMsg>
  constexpr auto on(const ut::events::log<TMsg>&) { }
};

struct custom_config {
  ::outputter outputter{};
  ut::reporter<decltype(outputter)> reporter{outputter};
  ut::runner<decltype(reporter)> runner{reporter};
};

template<class... Ts> auto ut::cfg<ut::override, Ts...> = custom_config{};

int main() {
  "config"_test = [] mutable {
    expect(42 == 43_i); // no output
  };
};
$CXX example.cpp -std=c++20 -o example && ./example
echo $? # 139 # no output

Compilation times

Include - no iostream (https://raw.githubusercontent.com/boost-ext/ut2/main/ut2)

time $CXX -x c++ -std=c++20 ut2 -c -DDISABLE_STATIC_ASSERT_TESTS # 0.028s
time $CXX -x c++ -std=c++20 ut2 -c                               # 0.049s

Benchmark - 100 tests, 1000 asserts (https://godbolt.org/z/xhfc518xx)

[ut]:  time $CXX benchmark.cpp -std=c++20                               # 0m13.498s
[ut2]: time $CXX benchmark.cpp -std=c++20                               # 0m0.813s
[ut2]: time $CXX benchmark.cpp -std=c++20 -DDISABLE_STATIC_ASSERT_TESTS # 0m0.758s
-------------------------------------------------------------------------
[ut]  https://github.com/boost-ext/ut/releases/tag/v2.0.1
[ut2] https://github.com/boost-ext/ut2/releases/tag/v2.1.1

Performance

Benchmark - 100 tests, 1000 asserts (https://godbolt.org/z/6WzxvjEex)

time ./benchmark # 0m0.002s (-O3)
time ./benchmark # 0m0.013s (-g)

X86-64 assembly -O3 (https://godbolt.org/z/x6GYbs4hT)

int main() {
  "sum"_test = [] {
    expect(42_i == 42);
  };
}
main:
  mov  rax, qword ptr [rip + cfg<ut::override>+136]
  inc  dword ptr [rax + 24]
  mov  ecx, dword ptr [rax + 8]
  mov  edx, dword ptr [rax + 92]
  lea  esi, [rdx + 1]
  mov  dword ptr [rax + 92], esi
  mov  dword ptr [rax + 4*rdx + 28], ecx
  mov  rax, qword ptr [rax]
  lea  rcx, [rip + .L.str]
  mov  qword ptr [rax + 8], rcx
  mov  dword ptr [rax + 16], 6
  lea  rcx, [rip + template parameter object for fixed_string
  mov  qword ptr [rax + 24], rcx
  inc  dword ptr [rip + ut::v2_1_1::cfg<ut::v2_1_1::override>+52]
  mov  rax, qword ptr [rip + ut::cfg<ut::override>+136]
  mov  ecx, dword ptr [rax + 8]
  mov  edx, dword ptr [rax + 92]
  dec  edx
  mov  dword ptr [rax + 92], edx
  xor  esi, esi
  cmp  ecx, dword ptr [rax + 4*rdx + 28]
  sete sil
  inc  dword ptr [rax + 4*rsi + 16]
  xor  eax, eax
  ret

API

/**
 * Assert definition
 * @code
 * expect(42 == 42_i);
 * expect(42 == 42_i) << "log";
 * expect[42 == 42_i]; // fatal assertion, aborts further execution
 * @endcode
 */
inline constexpr struct {
  constexpr auto operator()(auto expr);
  constexpr auto operator[](auto expr);
} expect{};
/**
 * Test suite definition
 * @code
 * suite test_suite = [] { ... };
 * @encode
 */
struct suite;
/**
 * Test definition
 * @code
 * "foo"_test = []          { ... }; // compile-time and run-time
 * "foo"_test = [] mutable  { ... }; // run-time only
 * "foo"_test = [] constval { ... }; // compile-time only
 * @endcode
 */
template<fixed_string Str>
[[nodiscard]] constexpr auto operator""_test();
/**
 * Compile time expression
 * @code
 * expect(constant<42_i == 42>); // forces compile-time evaluation and run-time check
 * auto i = 0;
 * expect(constant<i == 42_i>);  // compile-time error
 * @encode
 */
template<auto Expr> inline constexpr auto constant;
/**
 * Allows mutating object (by default lambdas are immutable)
 * @code
 * "foo"_test = [] {
 *   int i = 0;
 *   "sub"_test = [i] {
 *     mut(i) = 42;
 *   };
 *   expect(i == 42_i);
 * };
 * @endcode
 */
template<class T> [[nodiscard]] constexpr auto& mut(const T&);
template<class TLhs, class TRhs> struct eq;  // equal
template<class TLhs, class TRhs> struct neq; // not equal
template<class TLhs, class TRhs> struct gt;  // greater
template<class TLhs, class TRhs> struct ge;  // greater equal
template<class TLhs, class TRhs> struct lt;  // less
template<class TLhs, class TRhs> struct le;  // less equal
template<class TLhs, class TRhs> struct nt;  // not
constexpr auto operator==(const auto& lhs, const auto& rhs) -> decltype(eq{lhs, rhs});
constexpr auto operator!=(const auto& lhs, const auto& rhs) -> decltype(neq{lhs, rhs});
constexpr auto operator> (const auto& lhs, const auto& rhs) -> decltype(gt{lhs, rhs});
constexpr auto operator>=(const auto& lhs, const auto& rhs) -> decltype(ge{lhs, rhs});
constexpr auto operator< (const auto& lhs, const auto& rhs) -> decltype(lt{lhs, rhs});
constexpr auto operator<=(const auto& lhs, const auto& rhs) -> decltype(le{lhs, rhs});
constexpr auto operator! (const auto& t)                    -> decltype(nt{t});
struct _b;      // bool (true_b = _b{true}, false_b = _b{false})
struct _c;      // char
struct _sc;     // signed char
struct _s;      // short
struct _i;      // int
struct _l;      // long
struct _ll;     // long long
struct _u;      // unsigned
struct _uc;     // unsigned char
struct _us;     // unsigned short
struct _ul;     // unsigned long
struct _ull;    // unsigned long long
struct _f;      // float
struct _d;      // double
struct _ld;     // long double
struct _i8;     // int8_t
struct _i16;    // int16_t
struct _i32;    // int32_t
struct _i64;    // int64_t
struct _u8;     // uint8_t
struct _u16;    // uint16_t
struct _u32;    // uint32_t
struct _u64;    // uint64_t
struct _string; // const char*
constexpr auto operator""_i(auto value)   -> decltype(_i(value));
constexpr auto operator""_s(auto value)   -> decltype(_s(value));
constexpr auto operator""_c(auto value)   -> decltype(_c(value));
constexpr auto operator""_sc(auto value)  -> decltype(_sc(value));
constexpr auto operator""_l(auto value)   -> decltype(_l(value));
constexpr auto operator""_ll(auto value)  -> decltype(_ll(value));
constexpr auto operator""_u(auto value)   -> decltype(_u(value));
constexpr auto operator""_uc(auto value)  -> decltype(_uc(value));
constexpr auto operator""_us(auto value)  -> decltype(_us(value));
constexpr auto operator""_ul(auto value)  -> decltype(_ul(value));
constexpr auto operator""_ull(auto value) -> decltype(_ull(value));
constexpr auto operator""_f(auto value)   -> decltype(_f(value));
constexpr auto operator""_d(auto value)   -> decltype(_d(value));
constexpr auto operator""_ld(auto value)  -> decltype(_ld(value));
constexpr auto operator""_i8(auto value)  -> decltype(_i8(value));
constexpr auto operator""_i16(auto value) -> decltype(_i16(value));
constexpr auto operator""_i32(auto value) -> decltype(_i32(value));
constexpr auto operator""_i64(auto value) -> decltype(_i64(value));
constexpr auto operator""_u8(auto value)  -> decltype(_u8(value));
constexpr auto operator""_u16(auto value) -> decltype(_u16(value));
constexpr auto operator""_u32(auto value) -> decltype(_u32(value));
constexpr auto operator""_u64(auto value) -> decltype(_u64(value));
template<fixed_string Str>
[[nodiscard]] constexpr auto operator""_s() -> decltype(_string(Str));

Configuration

namespace events {
enum class mode {
  run_time,
  compile_time
};

template<mode Mode>
struct test_begin {
  const char* file_name{};
  int line{}; const char* name{};
};

template<mode Mode>
struct test_end {
  const char* file_name{};
  int line{};
  const char* name{};
  enum { FAILED, PASSED, COMPILE_TIME } result{};
};

template<class TExpr>
struct assert_pass {
  const char* file_name{};
  int line{};
  TExpr expr{};
};

template<class TExpr>
struct assert_fail {
  const char* file_name{};
  int line{};
  TExpr expr{};
};

struct fatal { };

template<class TMsg>
struct log {
  const TMsg& msg;
  bool result{};
};

struct summary {
  enum { FAILED, PASSED, COMPILE_TIME };
  unsigned asserts[2]{}; /* FAILED, PASSED */
  unsigned tests[3]{}; /* FAILED, PASSED, COMPILE_TIME */
};
} // namespace events
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);
  template<events::mode Mode> constexpr auto on(const events::test_end<Mode>&);
  template<class TExpr> constexpr auto on(const events::assert_pass<TExpr>&);
  template<class TExpr> constexpr auto on(const events::assert_fail<TExpr>&);
  constexpr auto on(const events::fatal&);
  template<class TMsg> constexpr auto on(const events::log<TMsg>&);
  constexpr auto on(const events::summary& event);
};
struct reporter {
  constexpr auto on(const events::test_begin<events::mode::run_time>&);
  constexpr auto on(const events::test_end<events::mode::run_time>&);
  constexpr auto on(const events::test_begin<events::mode::compile_time>&);
  constexpr auto on(const events::test_end<events::mode::compile_time>&);
  template<class TExpr> constexpr auto on(const events::assert_pass<TExpr>&);
  template<class TExpr> constexpr auto on(const events::assert_fail<TExpr>&);
  constexpr auto on(const events::fatal& event);
};
struct runner {
  template<class Test> constexpr auto on(Test test) -> bool;
};
/**
 * Customization point to override the default configuration
 * @code
 * template<class... Ts> auto ut::cfg<ut::override, Ts...> = my_config{};
 * @endcode
 */
struct override { }; /// to override configuration by users
struct default_cfg;  /// default configuration
template <class...> inline auto cfg = default_cfg{};
#define UT2 2'1'1                   // Current library version (SemVer)
#define UT_RUN_TIME_ONLY            // If defined tests will be executed
                                    // at run-time + static_assert tests
#define UT_COMPILE_TIME_ONLY        // If defined only compile-time tests
                                    // will be executed
#define DISABLE_STATIC_ASSERT_TESTS // If defined it disables running
                                    // static_asserts tests for the UT library
                                    // (user tests are not affected)

FAQ

  • How does UT2 compare to https://github.com/boost-ext/ut?

    UT2 ideas are based on UT. UT2 aim is not to replace UT. UT2 is minimal (no STL required). UT2 has different execution model (can run tests at compile-time and/or run-time).

  • Can I disable running tests at compile-time for faster compilation times?

    When DISABLE_STATIC_ASSERT_TESTS is defined static_asserts tests won't be executed upon inclusion. Note: Use with caution as disabling tests means that there are no guarantees upon inclusion that the given compiler/env combination works as expected.

  • How to integrate with CMake/CPM?

    CPMAddPackage(
      Name ut2
      GITHUB_REPOSITORY boost-ext/ut2
      GIT_TAG v2.1.1
    )
    add_library(ut2 INTERFACE)
    target_include_directories(ut2 SYSTEM INTERFACE ${ut2_SOURCE_DIR})
    add_library(ut2::ut2 ALIAS ut2)
    
    target_link_libraries(${PROJECT_NAME} ut2::ut2);
    
  • Similar projects?

    ut, catch2, googletest, gunit, boost.test