Giter Club home page Giter Club logo

ut's Introduction

Version Linux MacOs Windows Coveralls Try it online AUR Badge

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

UT / μt

| Motivation | Quick Start | Overview | Tutorial | Examples | User Guide | FAQ | Benchmarks |

C++ single header/single module, macro-free μ(micro)/Unit Testing Framework

#include <boost/ut.hpp> // import boost.ut;

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

int main() {
  using namespace boost::ut;

  "sum"_test = [] {
    expect(sum(0) == 0_i);
    expect(sum(1, 2) == 3_i);
    expect(sum(1, 2) > 0_i and 41_i == sum(40, 2));
  };
}
Running "sum"...
  sum.cpp:11:FAILED [(3 > 0 and 41 == 42)]
FAILED

===============================================================================
tests:   1 | 1 failed
asserts: 3 | 2 passed | 1 failed

https://godbolt.org/z/f4jEcv9vo

Motivation

Testing is a very important part of the Software Development, however, C++ doesn't provide any good testing facilities out of the box, which often leads into a poor testing experience for develops and/or lack of tests/coverage in general.

One should treat testing code as production code!

Additionally, well established testing practises such as Test Driven Development (TDD)/Behaviour Driven Development (BDD) are often not followed due to the same reasons.

The following snippet is a common example of testing with projects in C++.

int main() {
  // should sum numbers
  {
    assert(3 == sum(1, 2));
  }
}

There are quite a few problems with the approach above

  • No names for tests (Hard to follow intentions by further readers)
  • No automatic registration of tests (No way to run specific tests)
  • Hard to debug (Assertions don't provide any information why it failed)
  • Hard to scale (No easy path forward for parameterized tests, multiple suites, parallel execution, etc...)
  • Hard to integrate (No easy way to have a custom output such as XML for CI integration)
  • Easy to make mistakes (With implicit casting, floating point comparison, pointer comparison for strings, etc...)
  • Hard to follow good practises such as TDD/BDD (Lack of support for sections and declarative expressions)
  • ...

UT is trying to address these issues by simplifying testing experience with a few simple steps:

And you good to go!

Okay, great, but why I would use UT over other/similar testing frameworks already available in C++?

Great question! There are a few unique features which makes UT worth trying

  • Firstly, it supports all the basic Unit Testing Framework features (automatic registration of tests, assertions, suites, etc...)
  • It's easy to integrate (it's just one header/module)
  • It's macro free which makes testing experience that much nicer (it uses modern C++ features instead, macros are opt-in rather than being compulsory - Can I still use macros?)
  • It's flexible (all parts of the framework such as: runner, reporter, printer can be customized, basically most other Unit Testing Frameworks can be implemented on top of UT primitives)
  • It has smaller learning curve (just a few simple concepts (expect, test, suite))
  • It leverages C++ features to support more complex testing (parameterized)
  • It's faster to compile and execute than similar frameworks which makes it suitable for bigger projects without additional hassle (Benchmarks)
  • It supports TDD/BDD workflows
  • It supports Gherkin specification
  • It supports Spec
  • ...

Sounds intriguing/interesting? Learn more at

Quick Start

https://bit.ly/ut-quick-start (slides)

Overview

Tutorial

    Step 0: Get it...

Get the latest latest header/module from here!

Include/Import

// #include <boost/ut.hpp> // single header
// import boost.ut;        // single module (C++20)

int main() { }

Compile & Run

$CXX main.cpp && ./a.out
All tests passed (0 assert in 0 test)

[Optional] Install it

cmake -Bbuild -H.
cd build && make         # run tests
cd build && make install # install

[Optional] CMake integration

This project provides a CMake config and target. Just load ut with find_package to import the Boost::ut target. Linking against this target will add the necessary include directory for the single header file. This is demonstrated in the following example.

find_package(ut REQUIRED)
add_library(my_test my_test.cpp)
target_link_libraries(my_test PRIVATE Boost::ut)

[Optional] Conan integration

The boost-ext-ut package is available from Conan Center. Just include it in your project's Conanfile with boost-ext-ut/2.0.1.

    Step 1: Expect it...

Let's write our first assertion, shall we?

int main() {
  boost::ut::expect(true);
}
All tests passed (1 asserts in 0 test)

https://godbolt.org/z/vfx-eB

Okay, let's make it fail now?

int main() {
  boost::ut::expect(1 == 2);
}
main.cpp:4:FAILED [false]
===============================================================================
tests:   0 | 0 failed
asserts: 1 | 0 passed | 1 failed

https://godbolt.org/z/7qTePx

Notice that expression 1 == 2 hasn't been printed. Instead we got false?

Let's print it then?

int main() {
  using namespace boost::ut;
  expect(1_i == 2);
}
main.cpp:4:FAILED [1 == 2]
===============================================================================
tests:   0 | 0 failed
asserts: 1 | 0 passed | 1 failed

https://godbolt.org/z/7MXVzu

Okay, now we have it! 1 == 2 has been printed as expected. Notice the User Defined Literal (UDL) 1_i was used. _i is a compile-time constant integer value

  • It allows to override comparison operators 👍
  • It disallow comparison of different types 👍

See the User-guide for more details.

Alternatively, a terse notation (no expect required) can be used.

int main() {
  using namespace boost::ut::literals;
  using namespace boost::ut::operators::terse;

  1_i == 2; // terse notation
}
main.cpp:7:FAILED [1 == 2]
===============================================================================
tests:   0 | 0 failed
asserts: 1 | 0 passed | 1 failed

https://godbolt.org/z/s77GSm

Other expression syntaxes are also available.

expect(1_i == 2);       // UDL syntax
expect(1 == 2_i);       // UDL syntax
expect(that % 1 == 2);  // Matcher syntax
expect(eq(1, 2));       // eq/neq/gt/ge/lt/le
main.cpp:6:FAILED [1 == 2]
main.cpp:7:FAILED [1 == 2]
main.cpp:8:FAILED [1 == 2]
main.cpp:9:FAILED [1 == 2]
===============================================================================
tests:   0 | 0 failed
asserts: 4 | 0 passed | 4 failed

https://godbolt.org/z/QbgGtc

Okay, but what about the case if my assertion is fatal. Meaning that the program will crash unless the processing will be terminated. Nothing easier, let's just add fatal call to make the test fail immediately.

expect(fatal(1 == 2_i)); // fatal assertion
expect(1_i == 2);        // not executed
main.cpp:6:FAILED [1 == 2]
===============================================================================
tests:   1 | 1 failed
asserts: 2 | 0 passed | 2 failed

https://godbolt.org/z/WMe8Y1

But my expression is more complex than just simple comparisons. Not a problem, logic operators are also supported in the expect 👍.

expect(42l == 42_l and 1 == 2_i); // compound expression
main.cpp:5:FAILED [(42 == 42 and 1 == 2)]
===============================================================================
tests:   0 | 0 failed
asserts: 1 | 0 passed | 1 failed

https://godbolt.org/z/aEhX4t

Can I add a custom message though? Sure, expect calls are streamable!

int main() {
  expect(42l == 42_l and 1 == 2_i) << "additional info";
}
main.cpp:5:FAILED [(42 == 42 and 1 == 2)] additional info
===============================================================================
tests:   0 | 0 failed
asserts: 1 | 0 passed | 1 failed

That's nice, can I use custom messages and fatal assertions? Yes, stream the fatal!

expect(fatal(1 == 2_i) << "fatal assertion");
expect(1_i == 2);
FAILED
in: main.cpp:6 - test condition:  [1 == 2]

 fatal assertion
===============================================================================
tests:   0 | 2 failed
asserts: 0 | 0 passed | 2 failed

I use std::expected, can I stream its error() upon failure? Yes, since std::expected's error() can only be called when there is no value it requires lazy evaluation.

"lazy log"_test = [] {
  std::expected<bool, std::string> e = std::unexpected("lazy evaluated");
  expect(e.has_value()) << [&] { return e.error(); } << fatal;
  expect(e.value() == true);
};
Running test "lazy log"... FAILED
in: main.cpp:12 - test condition:  [false]

 lazy evaluated
===============================================================================
tests:   1 | 2 failed
asserts: 0 | 0 passed | 2 failed

> https://godbolt.org/z/v2PDuU

</p>
</details>

<details open><summary>&nbsp;&nbsp;&nbsp;&nbsp;Step 2: Group it...</summary>
<p>

> Assertions are great, but how to combine them into more cohesive units?
> `Test cases` are the way to go! They allow to group expectations for the same functionality into coherent units.

```cpp
"hello world"_test = [] { };

Alternatively test("hello world") = [] {} can be used.

All tests passed (0 asserts in 1 tests)

https://godbolt.org/z/Bh-EmY

Notice 1 tests but 0 asserts.

Let's make our first end-2-end test case, shall we?

int main() {
  "hello world"_test = [] {
    int i = 43;
    expect(42_i == i);
  };
}
Running "hello world"...
  main.cpp:8:FAILED [42 == 43]
FAILED
===============================================================================
tests:   1 | 1 failed
asserts: 1 | 0 passed | 1 failed

https://godbolt.org/z/Y43mXz

👍 We are done here!

I'd like to nest my tests, though and share setup/tear-down. With lambdas used to represents tests/sections we can easily achieve that. Let's just take a look at the following example.

int main() {
  "[vector]"_test = [] {
    std::vector<int> v(5);

    expect(fatal(5_ul == std::size(v)));

    should("resize bigger") = [v] { // or "resize bigger"_test
      mut(v).resize(10);
      expect(10_ul == std::size(v));
    };

    expect(fatal(5_ul == std::size(v)));

    should("resize smaller") = [=]() mutable { // or "resize smaller"_test
      v.resize(0);
      expect(0_ul == std::size(v));
    };
  }
}
All tests passed (4 asserts in 1 tests)

https://godbolt.org/z/XWAdYt

Nice! That was easy, but I'm a believer into Behaviour Driven Development (BDD). Is there a support for that? Yes! Same example as above just with the BDD syntax.

int main() {
  "vector"_test = [] {
    given("I have a vector") = [] {
      std::vector<int> v(5);
      expect(fatal(5_ul == std::size(v)));

      when("I resize bigger") = [=] {
        mut(v).resize(10);

        then("The size should increase") = [=] {
          expect(10_ul == std::size(v));
        };
      };
    };
  };
}
All tests passed (2 asserts in 1 tests)

https://godbolt.org/z/dnvxsE

On top of that, feature/scenario aliases can be leveraged.

int main() {
  feature("vector") = [] {
    scenario("size") = [] {
      given("I have a vector") = [] {
        std::vector<int> v(5);
        expect(fatal(5_ul == std::size(v)));

        when("I resize bigger") = [=] {
          mut(v).resize(10);

          then("The size should increase") = [=] {
            expect(10_ul == std::size(v));
          };
        };
      };
    };
  };
}
All tests passed (2 asserts in 1 tests)

https://godbolt.org/z/T4cWss

Can I use Gherkin? Yeah, let's rewrite the example using Gherkin specification

int main() {
  bdd::gherkin::steps steps = [](auto& steps) {
    steps.feature("Vector") = [&] {
      steps.scenario("*") = [&] {
        steps.given("I have a vector") = [&] {
          std::vector<int> v(5);
          expect(fatal(5_ul == std::size(v)));

          steps.when("I resize bigger") = [&] {
            v.resize(10);
          };

          steps.then("The size should increase") = [&] {
            expect(10_ul == std::size(v));
          };
        };
      };
    };
  };

  "Vector"_test = steps |
    R"(
      Feature: Vector
        Scenario: Resize
          Given I have a vector
           When I resize bigger
           Then The size should increase
    )";
}
All tests passed (2 asserts in 1 tests)

https://godbolt.org/z/jb1d8P

Nice, is Spec notation supported as well?

int main() {
  describe("vector") = [] {
    std::vector<int> v(5);
    expect(fatal(5_ul == std::size(v)));

    it("should resize bigger") = [v] {
      mut(v).resize(10);
      expect(10_ul == std::size(v));
    };
  };
}
All tests passed (2 asserts in 1 tests)

https://godbolt.org/z/6jKKzT

That's great, but how can call the same tests with different arguments/types to be DRY (Don't Repeat Yourself)? Parameterized tests to the rescue!

int main() {
  for (auto i : std::vector{1, 2, 3}) {
    test("parameterized " + std::to_string(i)) = [i] { // 3 tests
      expect(that % i > 0); // 3 asserts
    };
  }
}
All tests passed (3 asserts in 3 tests)

https://godbolt.org/z/Utnd6X

That's it 😮! Alternatively, a convenient test syntax is also provided 👍

int main() {
  "args"_test = [](const auto& arg) {
    expect(arg > 0_i) << "all values greater than 0";
  } | std::vector{1, 2, 3};
}
All tests passed (3 asserts in 3 tests)

https://godbolt.org/z/6FHtpq

Check Examples for further reading.

    Step 3: Scale it...

Okay, but my project is more complex than that. How can I scale? Test suites will make that possible. By using suite in translation units tests defined inside will be automatically registered 👍

suite errors = [] { // or suite<"nameofsuite">
  "exception"_test = [] {
    expect(throws([] { throw 0; })) << "throws any exception";
  };

  "failure"_test = [] {
    expect(aborts([] { assert(false); }));
  };
};

int main() { }
All tests passed (2 asserts in 2 tests)

https://godbolt.org/z/_ccGwZ


What's next?

Examples

    Assertions

// operators
expect(0_i == sum());
expect(2_i != sum(1, 2));
expect(sum(1) >= 0_i);
expect(sum(1) <= 1_i);
// message
expect(3_i == sum(1, 2)) << "wrong sum";
// expressions
expect(0_i == sum() and 42_i == sum(40, 2));
expect(0_i == sum() or 1_i == sum()) << "compound";
// matchers
expect(that % 0 == sum());
expect(that % 42 == sum(40, 2) and that % (1 + 2) == sum(1, 2));
expect(that % 1 != 2 or 2_i > 3);
// eq/neq/gt/ge/lt/le
expect(eq(42, sum(40, 2)));
expect(neq(1, 2));
expect(eq(sum(1), 1) and neq(sum(1, 2), 2));
expect(eq(1, 1) and that % 1 == 1 and 1_i == 1);
// floating points
expect(42.1_d == 42.101) << "epsilon=0.1";
expect(42.10_d == 42.101) << "epsilon=0.01";
expect(42.10000001 == 42.1_d) << "epsilon=0.1";
// constant
constexpr auto compile_time_v = 42;
auto run_time_v = 99;
expect(constant<42_i == compile_time_v> and run_time_v == 99_i);
// failure
expect(1_i == 2) << "should fail";
expect(sum() == 1_i or 2_i == sum()) << "sum?";
assertions.cpp:53:FAILED [1 == 2] should fail
assertions.cpp:54:FAILED [(0 == 1 or 2 == 0)] sum?
===============================================================================
tests:   0  | 0 failed
asserts: 20 | 18 passed | 2 failed

https://godbolt.org/z/E1c7G5

    Tests

        Run/Skip/Tag

"run UDL"_test = [] {
  expect(42_i == 42);
};

skip / "don't run UDL"_test = [] {
  expect(42_i == 43) << "should not fire!";
};
All tests passed (1 asserts in 1 tests)
1 tests skipped
test("run function") = [] {
  expect(42_i == 42);
};

skip / test("don't run function") = [] {
  expect(42_i == 43) << "should not fire!";
};
All tests passed (1 asserts in 1 tests)
1 tests skipped
tag("nightly") / tag("slow") /
"performance"_test= [] {
  expect(42_i == 42);
};

tag("slow") /
"run slowly"_test= [] {
  expect(42_i == 43) << "should not fire!";
};
cfg<override> = {.tag = {"nightly"}};
All tests passed (1 asserts in 1 tests)
1 tests skipped

https://godbolt.org/z/X3_kG4

        Sections

"[vector]"_test = [] {
  std::vector<int> v(5);

  expect(fatal(5_ul == std::size(v)));

  should("resize bigger") = [=] { // or "resize bigger"_test
    mut(v).resize(10);
    expect(10_ul == std::size(v));
  };

  expect(fatal(5_ul == std::size(v)));

  should("resize smaller") = [=]() mutable { // or "resize smaller"_test
    v.resize(0);
    expect(0_ul == std::size(v));
  };
};
All tests passed (4 asserts in 1 tests)

https://godbolt.org/z/cE91bj

        Behavior Driven Development (BDD)

"Scenario"_test = [] {
  given("I have...") = [] {
    when("I run...") = [] {
      then("I expect...") = [] { expect(1_i == 1); };
      then("I expect...") = [] { expect(1 == 1_i); };
    };
  };
};
All tests passed (2 asserts in 1 tests)

https://godbolt.org/z/mNBySr

        Gherkin

int main() {
  bdd::gherkin::steps steps = [](auto& steps) {
    steps.feature("*") = [&] {
      steps.scenario("*") = [&] {
        steps.given("I have a number {value}") = [&](int value) {
          auto number = value;
          steps.when("I add {value} to it") = [&](int value) {
            number += value;
          };
          steps.then("I expect number to be {value}") = [&](int value) {
            expect(that % number == value);
          };
        };
      };
    };
  };

  "Gherkin"_test = steps |
    R"(
      Feature: Number
        Scenario: Addition
          Given I have a number 40
           When I add 2 to it
           Then I expect number to be 42
    )";
}
All tests passed (1 asserts in 1 tests)

https://godbolt.org/z/BP3hyt

        Spec

int main() {
  describe("equality") = [] {
    it("should be equal")     = [] { expect(0_i == 0); };
    it("should not be equal") = [] { expect(1_i != 0); };
  };
}
All tests passed (2 asserts in 1 tests)

https://godbolt.org/z/BXYJ3a

        Parameterized

for (auto i : std::vector{1, 2, 3}) {
  test("parameterized " + std::to_string(i)) = [i] {
    expect(that % i > 0);
  };
}

"args"_test =
   [](auto arg) {
      expect(arg >= 1_i);
    }
  | std::vector{1, 2, 3};

"types"_test =
    []<class T> {
      expect(std::is_integral_v<T>) << "all types are integrals";
    }
  | std::tuple<bool, int>{};

"args and types"_test =
    []<class TArg>(TArg arg) {
      expect(fatal(std::is_integral_v<TArg>));
      expect(42_i == arg or "is true"_b == arg);
      expect(type<TArg> == type<int> or type<TArg> == type<bool>);
    }
  | std::tuple{true, 42};
All tests passed (14 asserts in 10 tests)

https://godbolt.org/z/4xGGdo

And whenever I need to know the specific type for which the test failed, I can use reflection::type_name<T>(), like this:

"types with type name"_test =
    []<class T>() {
      expect(std::is_unsigned_v<T>) << reflection::type_name<T>() << "is unsigned";
    }
  | std::tuple<unsigned int, float>{};
Running "types with type name"...PASSED
Running "types with type name"...
  <source>:10:FAILED [false] float is unsigned
FAILED

https://godbolt.org/z/MEnGnbTY4

    Suites

namespace ut = boost::ut;

ut::suite errors = [] {
  using namespace ut;

  "throws"_test = [] {
    expect(throws([] { throw 0; }));
  };

  "doesn't throw"_test = [] {
    expect(nothrow([]{}));
  };
};

int main() { }
All tests passed (2 asserts in 2 tests)

https://godbolt.org/z/CFbTP9

    Misc

        Logging using streams

"logging"_test = [] {
  log << "pre";
  expect(42_i == 43) << "message on failure";
  log << "post";
};
Running "logging"...
pre
  logging.cpp:8:FAILED [42 == 43] message on failure
post
FAILED

===============================================================================

tests:   1 | 1 failed
asserts: 1 | 0 passed | 1 failed

https://godbolt.org/z/26fPSY

        Logging using formatting

This requires using C++20 with a standard library with std::format support.

"logging"_test = [] {
  log("\npre  {} == {}\n", 42, 43);
  expect(42_i == 43) << "message on failure";
  log("\npost {} == {} -> {}\n", 42, 43, 42 == 43);
};
Running "logging"...
pre  42 == 43
  logging.cpp:8:FAILED [42 == 43] message on failure
post 42 == 43 -> false
FAILED

===============================================================================

tests:   1 | 1 failed
asserts: 1 | 0 passed | 1 failed

https://godbolt.org/z/26fPSY

        Matchers

"matchers"_test = [] {
  constexpr auto is_between = [](auto lhs, auto rhs) {
    return [=](auto value) {
      return that % value >= lhs and that % value <= rhs;
    };
  };

  expect(is_between(1, 100)(42));
  expect(not is_between(1, 100)(0));
};
All tests passed (2 asserts in 1 tests)

https://godbolt.org/z/4qwrCi

        Exceptions/Aborts

"exceptions/aborts"_test = [] {
  expect(throws<std::runtime_error>([] { throw std::runtime_error{""}; }))
    << "throws runtime_error";
  expect(throws([] { throw 0; })) << "throws any exception";
  expect(nothrow([]{})) << "doesn't throw";
  expect(aborts([] { assert(false); }));
};
All tests passed (4 asserts in 1 tests)

https://godbolt.org/z/A2EehK

    Config

        Runner

namespace ut = boost::ut;

namespace cfg {
  class runner {
   public:
    template <class... Ts> auto on(ut::events::test<Ts...> test) { test(); }
    template <class... Ts> auto on(ut::events::skip<Ts...>) {}
    template <class TExpr>
    auto on(ut::events::assertion<TExpr>) -> bool { return true; }
    auto on(ut::events::fatal_assertion) {}
    template <class TMsg> auto on(ut::events::log<TMsg>) {}
  };
} // namespace cfg

template<> auto ut::cfg<ut::override> = cfg::runner{};

https://godbolt.org/z/jdg687

        Reporter

namespace ut = boost::ut;

namespace cfg {
  class reporter {
   public:
    auto on(ut::events::test_begin) -> void {}
    auto on(ut::events::test_run) -> void {}
    auto on(ut::events::test_skip) -> void {}
    auto on(ut::events::test_end) -> void {}
    template <class TMsg> auto on(ut::events::log<TMsg>) -> void {}
    template <class TExpr>
    auto on(ut::events::assertion_pass<TExpr>) -> void {}
    template <class TExpr>
    auto on(ut::events::assertion_fail<TExpr>) -> void {}
    auto on(ut::events::fatal_assertion) -> void {}
    auto on(ut::events::exception) -> void {}
    auto on(ut::events::summary) -> void {}
  };
}  // namespace cfg

template <>
auto ut::cfg<ut::override> = ut::runner<cfg::reporter>{};

https://godbolt.org/z/gsAPKg

        Printer

namespace ut = boost::ut;

namespace cfg {
struct printer : ut::printer {
  template <class T>
  auto& operator<<(T&& t) {
    std::cerr << std::forward<T>(t);
    return *this;
  }
};
}  // namespace cfg

template <>
auto ut::cfg<ut::override> = ut::runner<ut::reporter<cfg::printer>>{};

int main() {
  using namespace ut;
  "printer"_test = [] {};
}

https://godbolt.org/z/XCscF9

User Guide

    API

export module boost.ut; /// __cpp_modules

namespace boost::inline ext::ut::inline v2_0_1 {
  /**
   * Represents test suite object
   */
  struct suite final {
    /**
     * Creates and executes test suite
     * @example suite _ = [] {};
     * @param suite test suite function
     */
    constexpr explicit(false) suite(auto suite);
  };

  /**
   * Creates a test
   * @example "name"_test = [] {};
   * @return test object to be executed
   */
  constexpr auto operator""_test;

  /**
   * Creates a test
   * @example test("name") = [] {};
   * @return test object to be executed
   */
  constexpr auto test = [](const auto name);

  /**
   * Creates a test
   * @example should("name") = [] {};
   * @return test object to be executed
   */
  constexpr auto should = [](const auto name);

  /**
   * Behaviour Driven Development (BDD) helper functions
   * @param name step name
   * @return test object to be executed
   */
  constexpr auto given = [](const auto name);
  constexpr auto when  = [](const auto name);
  constexpr auto then  = [](const auto name);

  /**
   * Evaluates an expression
   * @example expect(42 == 42_i and 1 != 2_i);
   * @param expr expression to be evaluated
   * @param source location https://en.cppreference.com/w/cpp/utility/source_location
   * @return stream
   */
  constexpr OStream& expect(
    Expression expr,
    const std::source_location& location = std::source_location::current()
  );

  struct {
    /**
     * @example (that % 42 == 42);
     * @param expr expression to be evaluated
     */
    [[nodiscard]] constexpr auto operator%(Expression expr) const;
  } that{};

  inline namespace literals {
    /**
     * User defined literals to represent constant values
     * @example 42_i, 0_uc, 1.23_d
     */
    constexpr auto operator""_i;  /// int
    constexpr auto operator""_s;  /// short
    constexpr auto operator""_c;  /// char
    constexpr auto operator""_l;  /// long
    constexpr auto operator""_ll; /// long long
    constexpr auto operator""_u;  /// unsigned
    constexpr auto operator""_uc; /// unsigned char
    constexpr auto operator""_us; /// unsigned short
    constexpr auto operator""_ul; /// unsigned long
    constexpr auto operator""_f;  /// float
    constexpr auto operator""_d;  /// double
    constexpr auto operator""_ld; /// long double

    /**
     * Represents dynamic values
     * @example _i(42), _f(42.)
     */
    constexpr auto _b(bool);
    constexpr auto _c(char);
    constexpr auto _s(short);
    constexpr auto _i(int);
    constexpr auto _l(long);
    constexpr auto _ll(long long);
    constexpr auto _u(unsigned);
    constexpr auto _uc(unsigned char);
    constexpr auto _us(unsigned short);
    constexpr auto _ul(unsigned long);
    constexpr auto _f(float);
    constexpr auto _d(double);
    constexpr auto _ld(long double);

    /**
     * Logical representation of constant boolean (true) value
     * @example "is set"_b     : true
     *          not "is set"_b : false
     */
    constexpr auto operator ""_b;
  } // namespace literals

  inline namespace operators {
    /**
     * Comparison functions to be used in expressions
     * @example eq(42, 42), neq(1, 2)
     */
    constexpr auto eq(Operator lhs, Operator rhs);  /// ==
    constexpr auto neq(Operator lhs, Operator rhs); /// !=
    constexpr auto gt(Operator lhs, Operator rhs);  /// >
    constexpr auto ge(Operator lhs, Operator rhs);  /// >=
    constexpr auto lt(Operator lhs, Operator rhs);  /// <
    constexpr auto le(Operator lhs, Operator rhs);  /// <=

    /**
     * Overloaded comparison operators to be used in expressions
     * @example (42_i != 0)
     */
    constexpr auto operator==;
    constexpr auto operator!=;
    constexpr auto operator>;
    constexpr auto operator>=;
    constexpr auto operator<;
    constexpr auto operator<=;

    /**
     * Overloaded logic operators to be used in expressions
     * @example (42_i != 0 and 1 == 2_i)
     */
    constexpr auto operator and;
    constexpr auto operator or;
    constexpr auto operator not;

    /**
     * Executes parameterized tests
     * @example "parameterized"_test = [](auto arg) {} | std::tuple{1, 2, 3};
     */
    constexpr auto operator|;

    /**
     * Creates tags
     * @example tag("slow") / tag("nightly") / "perf"_test = []{};
     */
    constexpr auto operator/;

    /**
     * Creates a `fatal_assertion` from an expression
     * @example (42_i == 0) >> fatal
     */
    constexpr auto operator>>;
  } // namespace operators

  /**
   * Creates skippable test object
   * @example skip / "don't run"_test = [] { };
   */
  constexpr auto skip = tag("skip");

  struct {
    /**
     * @example log << "message!";
     * @param msg stringable message
     */
    auto& operator<<(Msg msg);
  } log{};

  /**
   * Makes object mutable
   * @example mut(object)
   * @param t object to be mutated
   */
  template<class T> auto mut(const T& t) -> T&;

  /**
   * Default execution flow policy
   */
  class runner {
   public:
    /**
     * @example cfg<override> = {
        .filter  = "test.section.*",
        .colors  = { .none = "" },
        .dry__run = true
       };
     * @param options.filter {default: "*"} runs all tests which names
                                            matches test.section.* filter
     * @param options.colors {default: {
                               .none = "\033[0m",
                               .pass = "\033[32m",
                               .fail  = "\033[31m"
              } if specified then overrides default color values
     * @param options.dry_run {default: false} if true then print test names to be
                                               executed without running them
     */
    auto operator=(options);

    /**
     * @example suite _ = [] {};
     * @param suite() executes suite
     */
    template<class TSuite>
    auto on(ut::events::suite<TSuite>);

    /**
     * @example "name"_test = [] {};
     * @param test.type ["test", "given", "when", "then"]
     * @param test.name "name"
     * @param test.arg parameterized argument
     * @param test() executes test
     */
    template<class... Ts>
    auto on(ut::events::test<Ts...>);

    /**
     * @example skip / "don't run"_test = []{};
     * @param skip.type ["test", "given", "when", "then"]
     * @param skip.name "don't run"
     * @param skip.arg parameterized argument
     */
    template<class... Ts>
    auto on(ut::events::skip<Ts...>);

    /**
     * @example file.cpp:42: expect(42_i == 42);
     * @param assertion.expr 42_i == 42
     * @param assertion.location { "file.cpp", 42 }
     * @return true if expr passes, false otherwise
     */
    template <class TExpr>
    auto on(ut::events::assertion<TExpr>) -> bool;

    /**
     * @example expect((2_i == 1) >> fatal)
     * @note triggered by `fatal`
     *       should std::exit
     */
    auto on(ut::events::fatal_assertion);

    /**
     * @example log << "message"
     * @param log.msg "message"
     */
    template<class TMsg>
    auto on(ut::events::log<TMsg>);

    /**
     * Explicitly runs registered test suites
     * If not called directly test suites are executed with run's destructor
     * @example return run({.report_errors = true})
     * @param run_cfg.report_errors {default: false} if true it prints the summary after runnig
     */
    auto run(run_cfg);

    /**
     * Runs registered test suites if they haven't been explicilty executed already
     */
    ~run();
  };

  /**
   * Default reporter policy
   */
  class reporter {
   public:
    /**
     * @example file.cpp:42: "name"_test = [] {};
     * @param test_begin.type ["test", "given", "when", "then"]
     * @param test_begin.name "name"
     * @param test_begin.location { "file.cpp", 42 }
     */
    auto on(ut::events::test_begin) -> void;

    /**
     * @example "name"_test = [] {};
     * @param test_run.type ["test", "given", "when", "then"]
     * @param test_run.name "name"
     */
    auto on(ut::events::test_run) -> void;

    /**
     * @example "name"_test = [] {};
     * @param test_skip.type ["test", "given", "when", "then"]
     * @param test_skip.name "name"
     */
    auto on(ut::events::test_skip) -> void;

    /**
     * @example "name"_test = [] {};
     * @param test_end.type ["test", "given", "when", "then"]
     * @param test_end.name "name"
     */
    auto on(ut::events::test_end) -> void;

    /**
     * @example log << "message"
     * @param log.msg "message"
     */
    template<class TMsg>
    auto on(ut::events::log<TMsg>) -> void;

    /**
     * @example file.cpp:42: expect(42_i == 42);
     * @param assertion_pass.expr 42_i == 42
     * @param assertion_pass.location { "file.cpp", 42 }
     */
    template <class TExpr>
    auto on(ut::events::assertion_pass<TExpr>) -> void;

    /**
     * @example file.cpp:42: expect(42_i != 42);
     * @param assertion_fail.expr 42_i != 42
     * @param assertion_fail.location { "file.cpp", 42 }
     */
    template <class TExpr>
    auto on(ut::events::assertion_fail<TExpr>) -> void;

    /**
     * @example expect((2_i == 1) >> fatal)
     * @note triggered by `fatal`
     *       should std::exit
     */
    auto on(ut::events::fatal_assertion) -> void;

    /**
     * @example "exception"_test = [] { throw std::runtime_error{""}; };
     */
    auto on(ut::events::exception) -> void;

    /**
     * @note triggered on destruction of runner
     */
    auto on(ut::events::summary) -> void;
  };

  /**
   * Used to override default running policy
   * @example template <> auto cfg<override> = runner<reporter>{};
   */
  struct override {};

  /**
   * Default UT execution policy
   * Can be overwritten with override
   */
  template <class = override> auto cfg = runner<reporter>{};
}

    Configuration

Option Description Example
BOOST_UT_VERSION Current version 2'0'1

FAQ

    How does it work?

suite

/**
 * Reperesents suite object
 * @example suite _ = []{};
 */
struct suite final {
  /**
   * Assigns and executes test suite
   */
  [[nodiscard]] constexpr explicit(false) suite(Suite suite) {
    suite();
  }
};

test

/**
 * Creates named test object
 * @example "hello world"_test
 * @return test object
 */
[[nodiscard]] constexpr Test operator ""_test(const char* name, std::size_t size) {
  return test{{name, size}};
}
/**
 * Represents test object
 */
struct test final {
  std::string_view name{}; /// test case name

  /**
   * Assigns and executes test function
   * @param test function
   */
  constexpr auto operator=(const Test& test) {
    std::cout << "Running... " << name << '\n';
    test();
  }
};

expect

/**
 * Evaluates an expression
 * @example expect(42_i == 42);
 * @param expr expression to be evaluated
 * @param source location https://en.cppreference.com/w/cpp/utility/source_location
 * @return stream
 */
constexpr OStream& expect(
  Expression expr,
  const std::source_location& location = std::source_location::current()
) {
  if (not static_cast<bool>(expr) {
    std::cerr << location.file()
              << ':'
              << location.line()
              << ":FAILED: "
              << expr
              << '\n';
  }

  return std::cerr;
}
/**
 * Creates constant object for which operators can be overloaded
 * @example 42_i
 * @return integral constant object
 */
template <char... Cs>
[[nodiscard]] constexpr Operator operator""_i() -> integral_constant<int, value<Cs...>>;
/**
 * Overloads comparison if at least one of {lhs, rhs} is an Operator
 * @example (42_i == 42)
 * @param lhs Left-hand side operator
 * @param rhs Right-hand side operator
 * @return Comparison object
 */
[[nodiscard]] constexpr auto operator==(Operator lhs, Operator rhs) {
  return eq{lhs, rhs};
}
/**
 * Comparison Operator
 */
template <Operator TLhs, Opeartor TRhs>
struct eq final {
  TLhs lhs{}; // Left-hand side operator
  TRhs rhs{}; // Right-hand side operator

  /**
   * Performs comparison operatation
   * @return true if expression is succesful
   */
  [[nodiscard]] constexpr explicit operator bool() const {
    return lhs == rhs;
  }

  /**
   * Nicely prints the operation
   */
  friend auto operator<<(OStream& os, const eq& op) -> Ostream& {
    return (os << op.lhs << " == " << op.rhs);
  }
};

Sections

/**
 * Convenient aliases for creating test named object
 * @example should("return true") = [] {};
 */
constexpr auto should = [](const auto name) { return test{name}; };

Behaviour Driven Development (BDD)

/**
 * Convenient aliases for creating BDD tests
 * @example feature("Feature") = [] {};
 * @example scenario("Scenario") = [] {};
 * @example given("I have an object") = [] {};
 * @example when("I call it") = [] {};
 * @example then("I should get") = [] {};
 */
constexpr auto feature  = [](const auto name) { return test{name}; };
constexpr auto scenario = [](const auto name) { return test{name}; };
constexpr auto given    = [](const auto name) { return test{name}; };
constexpr auto when     = [](const auto name) { return test{name}; };
constexpr auto then     = [](const auto name) { return test{name}; };

https://godbolt.org/z/6Nk5Mi

Spec

/**
 * Convenient aliases for creating Spec tests
 * @example describe("test") = [] {};
 * @example it("should...") = [] {};
 */
constexpr auto describe = [](const auto name) { return test{name}; };
constexpr auto it       = [](const auto name) { return test{name}; };

Example implementation

Try it online

    Fast compilation times (Benchmarks)?

Implementation

  • Leveraging C++20 features

  • Avoiding unique types for lambda expressions

  template <class Test>
    requires not std::convertible_to<Test, void (*)()>>
  constexpr auto operator=(Test test);

vs

  // Compiles 5x faster because it doesn't introduce a new type for each lambda
  constexpr auto operator=(void (*test)());
  • Type-name erasure (allows types/function memoization)
  eq<integral_constant<42>, int>{ {}, 42 }

vs

  // Can be memoized - faster to compile
  eq<int, int>{42, 42}
  • Limiting preprocessor work

    • Single header/module
    • Minimal number of include files
  • Simplified versions of

    • std::function
    • std::string_view

    C++20 features?

    C++2X integration?

Parameterized tests with Expansion statements (https://wg21.link/P1306r1)

template for (auto arg : std::tuple<int, double>{}) {
  test("types " + std::to_string(arg)) = [arg] {
    expect(type(arg) == type<int> or type(arg) == type<double>);
  };
}
All tests passed (2 asserts in 2 tests)

https://cppx.godbolt.org/z/dMmqmM

    Is standardization an option?

Personally, I believe that C++ standard could benefit from common testing primitives (expect, ""_test) because

  • It lowers the entry-level to the language (no need for third-party libraries)
  • It improves the education aspect (one standard way of doing it)
  • It makes the language more coherent/stable (consistent design with other features, stable API)
  • It makes the testing a first class citizen (shows that the community cares about this aspect of the language)
  • It allows to publish tests for the Standard Library (STL) in the standard way (coherency, easier to extend)
  • It allows to act as additional documentation as a way to verify whether a particular implementation is conforming (quality, self-verification)
  • It helps with establishing standard vocabulary for testing (common across STL and other projects)

    Can I still use macros?

Sure, although please notice that there are negatives of using macros such as

  • Error messages might be not clear and/or point to the wrong line
  • Global scope will be polluted
  • Type safety will be ignored
#define EXPECT(...) ::boost::ut::expect(::boost::ut::that % __VA_ARGS__)
#define SUITE       ::boost::ut::suite _ = []
#define TEST(name)  ::boost::ut::detail::test{"test", name} = [=]() mutable

SUITE {
  TEST("suite") {
    EXPECT(42 == 42);
  };
};

int main() {
  TEST("macro") {
    EXPECT(1 != 2);
  };

  TEST("vector") {
    std::vector<int> v(5);

   EXPECT(fatal(5u == std::size(v))) << "fatal";

    TEST("resize bigger") {
      v.resize(10);
      EXPECT(10u == std::size(v));
    };
  };
}
All tests passed (4 asserts in 3 tests)

https://godbolt.org/z/WcEKTr

    What about Mocks/Stubs/Fakes?

Consider using one of the following frameworks

    What about Microbenchmarking?

Example benchmark

Consider using one of the following frameworks

    Related materials/talks?

    How to contribute?

CONTRIBUTING

Benchmarks

Framework Version Standard License Linkage Test configuration
Boost.Test 1.71.0 C++03 Boost 1.0 single header/library static library
GoogleTest 1.10.0 C++11 BSD-3 library static library
Catch 2.10.2 C++11 Boost 1.0 single header CATCH_CONFIG_FAST_COMPILE
Doctest 2.3.5 C++11 MIT single header DOCTEST_CONFIG_SUPER_FAST_ASSERTS
UT 1.1.0 C++20 Boost 1.0 single header/module
Include / 0 tests, 0 asserts, 1 cpp file
Assert / 1 test, 1'000'000 asserts, 1 cpp file
Test / 1'000 tests, 0 asserts, 1 cpp file
Suite / 10'000 tests, 0 asserts, 100 cpp files
Suite+Assert / 10'000 tests, 40'000 asserts, 100 cpp files
Suite+Assert+STL / 10'000 tests, 20'000 asserts, 100 cpp files
Incremental Build - Suite+Assert+STL / 1 cpp file change (1'000 tests, 20'000 asserts, 100 cpp files)
Suite+Assert+STL / 10'000 tests, 20'000 asserts, 100 cpp files
(Headers vs Precompiled headers vs C++20 Modules)

https://github.com/cpp-testing/ut-benchmark


Disclaimer UT is not an official Boost library.

ut's People

Contributors

80ltrumpet avatar ambushed avatar andreaslokko avatar bebuch avatar bobini1 avatar clausklein avatar gaalexandre avatar hazelnusse avatar helmesjo avatar jessestricker avatar jwillikers avatar kris-jusiak avatar krzysztof-jusiak avatar luncliff avatar mordante avatar onihusube avatar r2rt avatar ramirisu avatar rianquinn avatar sam20908 avatar sandervocke avatar sebanisu avatar staffantj avatar tambry avatar terenstare avatar theta682 avatar tomboehmer avatar tusharpm avatar willwray avatar xkaraman avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ut's Issues

Add user-defined literals and type aliases for unsigned long long and signed char

Expected Behavior

ut should provide user-defined literals and type aliases for all fundamental types.

Actual Behavior

Currently there are user defined literals and type aliases for char, unsigned char, short, unsigned short, int, unsigned, long, unsigned long, long long, float, double, long double (and with slightly different behavior bool).
But there's no such thing for signed char or unsigned long long.

I would even love to have user defined literals for the integer types from <cstdint>, e.g. std::int64_t or std::intmax_t, and the two types std::size_t and std::ptrdiff_t from <cstddef>. But I don't know ut well enough to know whether adding them makes sense.

Steps to Reproduce the Problem

#include <tuple>
#include "ut.hpp"

int main()
{
    // === user-defined literals
    using namespace boost::ut::literals;

    std::ignore = 0_c;      // char

    std::ignore = 0_s;      // signed short
    std::ignore = 0_i;      // signed int
    std::ignore = 0_l;      // signed long
    std::ignore = 0_ll;     // signed long long

    std::ignore = 0_uc;     // unsigned char
    std::ignore = 0_us;     // unsigned short
    std::ignore = 0_u;      // unsigned int
    std::ignore = 0_ul;     // unsigned long

    std::ignore = 0._f;     // float
    std::ignore = 0._d;     // double
    std::ignore = 0._ld;    // long double

    std::ignore = "everything is true"_b;

    // these two don't exist:
    //std::ignore = 0_sc;     // signed char
    //std::ignore = 0_ull;    // unsigned long long


    // ==== type aliases
    using namespace boost::ut;

    std::ignore = _c(0);     // char

    std::ignore = _s(0);     // signed short
    std::ignore = _i(0);     // signed int
    std::ignore = _l(0);     // signed long
    std::ignore = _ll(0);    // signed long long

    std::ignore = _uc(0);    // unsigned char
    std::ignore = _us(0);    // unsigned short
    std::ignore = _u(0);     // unsigned int
    std::ignore = _ul(0);    // unsigned long

    std::ignore = _f(0);     // float
    std::ignore = _d(0);     // double
    std::ignore = _ld(0);    // long double

    std::ignore = _b(0);     // bool

    // these two don't exist:
    //std::ignore = _sc(0);    // signed char
    //std::ignore = _ull(0);   // unsigned long long
}

Specifications

  • Version: HEAD (1.8.1)
  • Platform: all
  • Subsystem: all

log should support non-char[] values

Expected Behavior

ut::log supporting at least std::string.

Actual Behavior

ut::log supports only compile-time known char[] string literals.
utility::string_view duplicates some std::string_view functionality, but is incomplete and not convertible.

Specifications

  • Version: latest master (b937560)
  • Platform: Windows Clang/LLVM 10 HEAD & MSVC 19.24.28307.1

Nested Tests

Nested tests

Expected Behavior

A file test.cpp contains

#include "ut.hpp"

int main() {
  using namespace boost::ut;

  test("set") = []{
    test("one") = []{ expect(1==1); };
    test("two") = []{ expect(2==2); };
  };
}

Compilation and execution is

$ g++ -std=c++20 -Wall -Wextra -pedantic   -c -o test.o test.cpp
$ g++ -std=c++20 -Wall -Wextra -pedantic -o test test.o
$ ./test
All tests passed (2 asserts in 2 tests)

Actual Behavior

All other things being equal,

$ ./test
All tests passed (2 asserts in 1 tests)

Steps to Reproduce the Problem

Project Directory Structure

./ut-test/
  |
  ├── ut.hpp
  |
  └── test.cpp

See Expected Behavior.

Specifications

OS: bento/ubuntu-20.04
Compiler: gcc version 10.0.1 (Ubuntu 10-20200411-0ubuntu1)
          g++ -> x86_64-linux-gnu-gcc-10

Further Implications

In BDD syntax, the standard output is undesirable. For example, something like

#include "ut.hpp"

int main() {
  using namespace boost::ut;

  feature("sort") = []{
    scenario("small arrays") = [] {
      given("trivial arrays") = [] {
        when("the array is empty") = [] { ... };
        when("the array is of size 1") = [] { ... };
      };
      given("array of size 2") = { ... };
      given("array of size 3") = { ... };
      ⋮  
    };
    scenario("significant arrays") = [] {
      given("array of size 20") = { ... }
      ⋮  
    };
    ⋮  
    scenario("mega arrays") = [] {
      ...
    };
  };
}

can result in the output

$ ./test
All tests passed (23192 asserts in 1 tests)

Suggestion

Add an optional parameter to the test function that will cause it to be counted as a test:

... auto test = [](const auto name, const bool isTest) { ...

Then, one could apply it as follows

#include "ut.hpp"

int main() {
  using namespace boost::ut;

  test("set") = []{
    test("one", true) = []{ expect(1==1); };
    test("two", true) = []{ expect(2==2); };
  };

  feature("sort") = []{
    scenario("small arrays") = [] {
      given("trivial arrays") = [] {
        when("the array is empty", true) = [] { ... };
        when("the array is of size 1", true) = [] { ... };
        ⋮  
      };
      ⋮  
    };
    ⋮  
  };
}

Employ a more elegant solution or update the tutorial if my understanding is off.

Thanks!

Microbenchmarking

Would it be possible to support creating microbenchmarks? It is not uncommon for test frameworks to incorporate microbenchmark support. Below are a couple of frameworks I'm aware of which closely tie microbenchmarking and unit testing.

  1. Catch2 implements microbenchmarks -> https://github.com/catchorg/Catch2/blob/master/docs/benchmarks.md#top
  2. Google Benchmark (related to Google Test) is a simple, macro-based solution -> https://github.com/google/benchmark
  3. The go programming language implements microbenchmarking as a part of its built-in testing framework -> https://golang.org/pkg/testing/

I'm not aware of any libraries for benchmarking C++ code that are macro-free, and I figure I'm not the only one that would have a similar interest in macro-free microbenchmarking.

Improve readability and glancability of docs in README.txt

Expected Behavior

That it's easy to search text, and see the whole structure, of the docs on the front page:

https://github.com/boost-experimental/ut

Actual Behavior

  • Most sections on the front page are collapsed, and some require two levels of expansion of triangles to see the content
  • Searching in the browser (Ctrl+F) doesn't find text in collapsed sections
  • I've seen someone who is more familiar with UT than me still click on 2 or 3 triangles before finding the code example they wanted to show me

Specifications

  • Testing in both Firefox and Chrome (Windows 10)

Suggestions

  • Perhaps provide a button at the top that opens all the triangles all the way down
  • Or just remove the triangles and collapsing altogether

Tests using true_b fail to compile in 1.1.8

Expected Behavior

That this code compiles:

        catch (const std::exception& e)
        {
            expect(approvalMissingExceptionReceived == true_b)
                << "Unexpected exception received: " << e.what();
        }

Actual Behavior

Compiler error:

.../ApprovalTests.cpp/tests/UT_Tests/UTApprovalTestTests.cpp:34:56: error: use of undeclared identifier 'true_b'
            expect(approvalMissingExceptionReceived == true_b)

Steps to Reproduce the Problem

With v 1.1.7:

  • write a test using the following:
expect(false_b == true_b);

With v1.1.8:

  • Compile that test, and observe it fail to compile

Specifications

  • Version: 1.1.7
  • Platform: macOS 10.15.7
  • Subsystem:
/opt/local/bin/clang-mp-9.0 --version
clang version 9.0.1 
Target: x86_64-apple-darwin19.6.0
Thread model: posix
InstalledDir: /opt/local/libexec/llvm-9.0/bin

expect_death

Any plans on adding an expect_death test? One feature that I need badly is the ability to test for std::terminate() and continue on, and I am not sure how to best approach that as throwing from a std::terminate() handler is not an option when a function is marked as noexcept.

Boost.UT tests that throw unexpected exceptions appear to pass, by returning 0 exit status

Hi @krzysztof-jusiak - I have a patch for this, ready to submit as a PR... I thought it would be helpful to log the behaviour here to provide a more detailed description.

Expected Behavior

When there is an unexpected exception caught during the running of tests, the program should exit with a non-zero exit code.

Actual Behavior

When there is an unexpected exception caught during the running of tests, and no other assertion-type failures, there is a console message written out, but the program exits with exit code 0 - so ctest and CI systems see it as a passing test.

This is particularly important when using ctest --output-on-failure . ... ctest thinks the test passed, so it does not displaying any console output at all, and so the user sees only 100% success...

(This is the underlying cause of approvals/ApprovalTests.cpp#87.)

Steps to Reproduce the Problem

  1. Edit the example program example/exception.cpp to have this content:
#include <boost/ut.hpp>
#include <stdexcept>

int result() {
  throw std::runtime_error("no!");
}

int main() {
  using namespace boost::ut;

  "exceptions"_test = [] {
    expect(result() == 1_i);
  };
}
  1. Run the exception program
  2. Note the exit code 0 in the console output:
.../ut-claremacrae/cmake-build-spaces/cmake-build-debug-gcc9-brew/example/exception
Running "exceptions"...
  Unexpected exception with message:
no!
FAILED

===============================================================================
tests:   1 | 1 failed
asserts: 0 | 0 passed | 0 failed


Process finished with exit code 0

Specifications

  • Version: 46a4f2e
  • Platform: macOS Catalina
  • Subsystem: ?

Observed on multiple different compilers and generators.

Honest advertising on supported language versions

Expected Behavior

If C++17* support (*with limitations) is advertised, things should mostly work when in C++17 mode.

Actual Behavior

Even

int main() {
  boost::ut::expect(true);
}

fails to compile with MSVC saying:

[build] C:\Users\mnagy\Source\Repos\ut\.vscode\install\include\boost/ut.hpp(1402): error C7555: use of designated initializers requires at least '/std:c++latest'
[build] C:\Users\mnagy\Source\Repos\ut\.vscode\install\include\boost/ut.hpp(1402): note: This diagnostic occurred in the compiler generated function 'auto boost::ext::ut::v1_1_8::runner<TReporter,MaxPathSize>::on(boost::ext::ut::v1_1_8::events::test<Ts...>)'

This issue is not unique to MSVC, even Appveyor build with clang-cl have output such as:

[57/112] Building CXX object example\CMakeFiles\boost_ut_parameterized.dir\parameterized.cpp.obj
..\example\parameterized.cpp(30,20): warning: explicit template parameter list for lambdas is a C++20 extension [-Wc++20-extensions]
  "types"_test = []<class T>() {
                   ^

When the entry points to testing use C++20 features, I naturally don't expect Boost UT to work in C++17 mode, but most features should have C++17 workarounds (hence the asterisk "with limitations").

Either decide whether the library supports C++17 or not. If yes, actually specify/mark which features are known to not work. Run strict C++17 builds in CI (-Werror) and disable such tests on purpose if there are no workarounds implemented.

BTW, this regression was introduced in v1.1.5, as installing tagged version of v1.1.4 compiles the simplest example as intended.

Steps to Reproduce the Problem

cmake_minimum_required(VERSION 3.0)

project(Test-Boost.UT
  LANGUAGES CXX
)

if (MSVC)
  string(REGEX REPLACE "/W[0-9]" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS})
endif (MSVC)

find_package(ut REQUIRED)

add_executable(Test1 Test1.cpp)

target_link_libraries(Test1
  PRIVATE
    boost::ut
)

target_compile_options(Test1
  PRIVATE
    $<$<CXX_COMPILER_ID:MSVC>:
      /W4          # Turn on all (sensible) warnings
      /permissive- # Turn on strict language conformance
      /EHsc        # Specify exception handling model
    >
)
  1. C:\Kellekek\Microsoft\VisualStudio\2019\BuildTools\VC\Tools\MSVC\14.26.28801\bin\Hostx64\x64\cl.exe /nologo /TP -IC:\Users\mnagy\Source\Repos\ut\.vscode\install\include /DWIN32 /D_WINDOWS /GR /EHsc /MDd /Zi /Ob0 /Od /RTC1 /W4 /permissive- /EHsc /std:c++17 /showIncludes /FoCMakeFiles\Test1.dir\Test1.cpp.obj /FdCMakeFiles\Test1.dir\ /FS -c ..\..\Test1.cpp

Specifications

  • Version: 1.1.5+
  • Platform: Windows
  • Subsystem: ?

Disable Running Tests automatically

Would it be possible to disable tests running as part of the build? It makes it difficult to incorporate this project as sub-build when failing tests block building the rest of the project. It would be nice to be able to then run all of the tests with a CTest invocation after the build step.

GCC unused-local-typedefs Warning

Expected Behavior

To receive no warnings when compiling.

Actual Behavior

I received the following warnings using GCC 9.1.1.

_deps/ut-src/include/boost/ut.hpp: In function ‘constexpr auto boost::ut::v1_1_7::operators::terse::operator==(const T&, const boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type>&)’:
_deps/ut-src/include/boost/ut.hpp:1974:11: warning: typedef ‘using type = using eq_t = class boost::ut::v1_1_7::detail::eq_<T, boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type> >’ locally defined but not used [-Wunused-local-typedefs]
 1974 |     using type = eq_t;
      |           ^~~~
_deps/ut-src/include/boost/ut.hpp: In function ‘constexpr auto boost::ut::v1_1_7::operators::terse::operator==(const boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type>&, const T&)’:
_deps/ut-src/include/boost/ut.hpp:1987:11: warning: typedef ‘using type = using eq_t = class boost::ut::v1_1_7::detail::eq_<boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type>, T>’ locally defined but not used [-Wunused-local-typedefs]
 1987 |     using type = eq_t;
      |           ^~~~
_deps/ut-src/include/boost/ut.hpp: In function ‘constexpr auto boost::ut::v1_1_7::operators::terse::operator!=(const T&, const boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type>&)’:
_deps/ut-src/include/boost/ut.hpp:2000:11: warning: typedef ‘using type = using neq_t = class boost::ut::v1_1_7::detail::neq_<T, boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type> >’ locally defined but not used [-Wunused-local-typedefs]
 2000 |     using type = neq_t;
      |           ^~~~
_deps/ut-src/include/boost/ut.hpp: In function ‘constexpr auto boost::ut::v1_1_7::operators::terse::operator!=(const boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type>&, const T&)’:
_deps/ut-src/include/boost/ut.hpp:2013:11: warning: typedef ‘using type = using neq_t = class boost::ut::v1_1_7::detail::neq_<boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type>, T>’ locally defined but not used [-Wunused-local-typedefs]
 2013 |     using type = neq_t;
      |           ^~~~
_deps/ut-src/include/boost/ut.hpp: In function ‘constexpr auto boost::ut::v1_1_7::operators::terse::operator>(const T&, const boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type>&)’:
_deps/ut-src/include/boost/ut.hpp:2026:11: warning: typedef ‘using type = using gt_t = class boost::ut::v1_1_7::detail::gt_<T, boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type> >’ locally defined but not used [-Wunused-local-typedefs]
 2026 |     using type = gt_t;
      |           ^~~~
_deps/ut-src/include/boost/ut.hpp: In function ‘constexpr auto boost::ut::v1_1_7::operators::terse::operator>(const boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type>&, const T&)’:
_deps/ut-src/include/boost/ut.hpp:2039:11: warning: typedef ‘using type = using gt_t = class boost::ut::v1_1_7::detail::gt_<boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type>, T>’ locally defined but not used [-Wunused-local-typedefs]
 2039 |     using type = gt_t;
      |           ^~~~
_deps/ut-src/include/boost/ut.hpp: In function ‘constexpr auto boost::ut::v1_1_7::operators::terse::operator>=(const T&, const boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type>&)’:
_deps/ut-src/include/boost/ut.hpp:2052:11: warning: typedef ‘using type = using ge_t = class boost::ut::v1_1_7::detail::ge_<T, boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type> >’ locally defined but not used [-Wunused-local-typedefs]
 2052 |     using type = ge_t;
      |           ^~~~
_deps/ut-src/include/boost/ut.hpp: In function ‘constexpr auto boost::ut::v1_1_7::operators::terse::operator>=(const boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type>&, const T&)’:
_deps/ut-src/include/boost/ut.hpp:2065:11: warning: typedef ‘using type = using ge_t = class boost::ut::v1_1_7::detail::ge_<boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type>, T>’ locally defined but not used [-Wunused-local-typedefs]
 2065 |     using type = ge_t;
      |           ^~~~
_deps/ut-src/include/boost/ut.hpp: In function ‘constexpr auto boost::ut::v1_1_7::operators::terse::operator<(const T&, const boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type>&)’:
_deps/ut-src/include/boost/ut.hpp:2078:11: warning: typedef ‘using type = using lt_t = class boost::ut::v1_1_7::detail::lt_<T, boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type> >’ locally defined but not used [-Wunused-local-typedefs]
 2078 |     using type = lt_t;
      |           ^~~~
_deps/ut-src/include/boost/ut.hpp: In function ‘constexpr auto boost::ut::v1_1_7::operators::terse::operator<(const boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type>&, const T&)’:
_deps/ut-src/include/boost/ut.hpp:2091:11: warning: typedef ‘using type = using lt_t = class boost::ut::v1_1_7::detail::lt_<boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type>, T>’ locally defined but not used [-Wunused-local-typedefs]
 2091 |     using type = lt_t;
      |           ^~~~
_deps/ut-src/include/boost/ut.hpp: In function ‘constexpr auto boost::ut::v1_1_7::operators::terse::operator<=(const T&, const boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type>&)’:
_deps/ut-src/include/boost/ut.hpp:2104:11: warning: typedef ‘using type = using le_t = class boost::ut::v1_1_7::detail::le_<T, boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type> >’ locally defined but not used [-Wunused-local-typedefs]
 2104 |     using type = le_t;
      |           ^~~~
_deps/ut-src/include/boost/ut.hpp: In function ‘constexpr auto boost::ut::v1_1_7::operators::terse::operator<=(const boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type>&, const T&)’:
_deps/ut-src/include/boost/ut.hpp:2117:11: warning: typedef ‘using type = using le_t = class boost::ut::v1_1_7::detail::le_<boost::ut::v1_1_7::operators::terse::detail::value_location<typename T::value_type>, T>’ locally defined but not used [-Wunused-local-typedefs]
 2117 |     using type = le_t;
      |           ^~~~
_deps/ut-src/include/boost/ut.hpp: In function ‘constexpr auto boost::ut::v1_1_7::operators::terse::operator&&(const TLhs&, const TRhs&)’:
_deps/ut-src/include/boost/ut.hpp:2130:11: warning: typedef ‘using type = using and_t = class boost::ut::v1_1_7::detail::and_<typename TLhs::type, typename TRhs::type>’ locally defined but not used [-Wunused-local-typedefs]
 2130 |     using type = and_t;
      |           ^~~~
_deps/ut-src/include/boost/ut.hpp: In function ‘constexpr auto boost::ut::v1_1_7::operators::terse::operator||(const TLhs&, const TRhs&)’:
_deps/ut-src/include/boost/ut.hpp:2143:11: warning: typedef ‘using type = using or_t = class boost::ut::v1_1_7::detail::or_<typename TLhs::type, typename TRhs::type>’ locally defined but not used [-Wunused-local-typedefs]
 2143 |     using type = or_t;
      |           ^~~~
_deps/ut-src/include/boost/ut.hpp: In function ‘constexpr auto boost::ut::v1_1_7::operators::terse::operator!(const T&)’:
_deps/ut-src/include/boost/ut.hpp:2154:11: warning: typedef ‘using type = using not_t = class boost::ut::v1_1_7::detail::not_<typename T::type>’ locally defined but not used [-Wunused-local-typedefs]
 2154 |     using type = not_t;
      |           ^~~~

Steps to Reproduce the Problem

It seems like these warnings can occur by just using a very basic test case.
For instance, expect(true);.
This does not occur for all of my test executables, only simple ones.

Specifications

  • Version: 1.1.7
  • Platform: CentOS 7-1908
  • Subsystem: devtoolset-9, GCC 9.1.1, CMake 3.17.0, Ninja 1.10.0, CCache 3.7.9

Add CMake Interface Library

I am very interested in using your library. I would like to use FetchContent and have access to your library through add_subdirectory. However, your library does not create a convenience target to use with target_link_libraries. While it is little effort for me to add the necessary include directory myself, I would much rather just "link" against your header only library like so:

target_link_libraries(MyTarget PRIVATE Boost::ut)

I have included an import prefix to mirror Boost's component linking behavior.
Defining an interface library through CMake with the appropriate include directories will make this possible.
The changes might look like the following.

add_library(ut INTERFACE)
add_library(Boost::ut ALIAS ut)
target_include_directories(ut INTERFACE $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)

This code also paves the way for the interface target to be exported and included with find_package.

Source-file-location fails on macOS, giving 'unknown'

Expected Behavior

The ApprovalTests.cpp UT integration tests pass with the latest version of this project on macOS.

This combination passes:

Actual Behavior

On macOS, with both g++9 homebrew and clang-9 MacPorts, using 515c080, the UT tests all fail with:

*****************************************************************************
*                                                                           *
* Welcome to Approval Tests.
*
* There seems to be a problem with your build configuration.
* We cannot find the test source file at:
*   unknown
*
* For details on how to fix this, please visit:
* https://github.com/approvals/ApprovalTests.cpp/blob/master/doc/TroubleshootingMisconfiguredBuild.md
*                                                                           *
*****************************************************************************

Steps to Reproduce the Problem

Specifications

It's failing for me with:

CMAKE_VERSION = 3.16.2
CMAKE_GENERATOR = Unix Makefiles
CMAKE_SOURCE_DIR = /Users/clare/Documents/develop/ApprovalTests/ApprovalTests.cpp
CMAKE_CURRENT_SOURCE_DIR = /Users/clare/Documents/develop/ApprovalTests/ApprovalTests.cpp
CMAKE_CURRENT_BINARY_DIR = /Users/clare/Documents/develop/ApprovalTests/ApprovalTests.cpp/cmake-build-debug-g-9-brew
CMAKE_CXX_COMPILER = /usr/local/Cellar/gcc/9.2.0_2/bin/g++-9
CMAKE_CXX_COMPILER_ID = GNU
CMAKE_CXX_COMPILER_VERSION = 9.2.0
CMAKE_UNITY_BUILD = 

and

CMAKE_VERSION = 3.16.2
CMAKE_GENERATOR = Unix Makefiles
CMAKE_SOURCE_DIR = /Users/clare/Documents/develop/ApprovalTests/ApprovalTests.cpp
CMAKE_CURRENT_SOURCE_DIR = /Users/clare/Documents/develop/ApprovalTests/ApprovalTests.cpp
CMAKE_CURRENT_BINARY_DIR = /Users/clare/Documents/develop/ApprovalTests/ApprovalTests.cpp/cmake-build-debug-clang-mp-90-brew
CMAKE_CXX_COMPILER = /opt/local/bin/clang++-mp-9.0
CMAKE_CXX_COMPILER_ID = Clang
CMAKE_CXX_COMPILER_VERSION = 9.0.1
CMAKE_UNITY_BUILD = 

Likely cause

There are not many differences between 8004290 and 515c080

I suspect that the cause is this, as in both compiler cases, __APPLE__ is defined:

@@ -211,10 +211,10 @@ namespace reflection {
 class source_location {
  public:
   [[nodiscard]] static constexpr auto current(
-#if (__GNUC__ >= 9 or __clang_major__ >= 9)
+#if ((__GNUC__ >= 9 or __clang_major__ >= 9) and not defined(__APPLE__))
       const char* file = __builtin_FILE(), int line = __builtin_LINE()
 #else
-      const char* file = {}, int line = {}
+      const char* file = "unknown", int line = {}
 #endif
           ) noexcept {
     source_location sl{};

Missing coverage data

Expected Behavior

The tests should allow the extraction of coverage data.

Actual Behavior

That's not the case.
Coverage data is extracted from the rest of the program (so it's not a problem of options) but not from anything that runs within the tests. It worked just fine with doctest too but nothing with UT.
The gcno / gcda files are found just fine by lcov, it captures everything that ran outside of UT, but nothing from inside (either from a suite or not).
Maybe an option I missed somewhere?

Steps to Reproduce the Problem

Not sure, it fails every time on my system and I haven't done anything particularly strange.
I'm using exclusively the BDD but tests made outside of BDD don't show up either.

Specifications

  • Version: 2020/12/11 (commit 572f180)
  • Platform: Linux
  • Subsystem: Arch
    With gcc g++ (GCC) 11.0.0 20201016 (experimental) [devel/c++-modules ea1205e3bbe??M:20201020-1848], c++20 activated.

Warning: "possible misuse of comma operator here [-Werror,-Wcomma]"

This is more of a question than a bug report, as I haven't managed to narrow down my configuration enough to give you a minimal repro.

The question is: is the comma operator in this code intentional, and if so, could a static cast be added to silence the warning?

In some of my builds, where I treat warnings as errors, I am getting this:

/Users/clare/Documents/develop/ApprovalTests/ApprovalTests.cpp.CMakeSamples/ut/include/boost/ut.hpp:210:9: error: possible misuse of comma operator here [-Werror,-Wcomma]
    ++pi, ++si;
        ^
/Users/clare/Documents/develop/ApprovalTests/ApprovalTests.cpp.CMakeSamples/ut/include/boost/ut.hpp:210:5: note: cast expression to void to silence warning
    ++pi, ++si;
    ^~~~
    static_cast<void>( )
1 error generated.

Specifications

  • Version: bba087c
  • Platform: macOS
  • Subsystem:
/opt/local/bin/clang-mp-9.0 --version
clang version 9.0.1 
Target: x86_64-apple-darwin19.6.0
Thread model: posix
InstalledDir: /opt/local/libexec/llvm-9.0/bin

Using coroutines

Expected Behavior

To execute the test and the assert inside.

I don't know how to co_await inside a test lambda, without being a coroutine.

Actual Behavior

All tests passed (0 asserts in 1 tests)

Steps to Reproduce the Problem

"hello coro"_test = []() -> ::cppcoro::task<> {
    ::boost::ut::log << "Not printed, because never reaches here.";
    auto task_lambda = []() -> ::cppcoro::task<int> { co_return 43; }();
    int coro_43 = co_await task_lambda;
    expect(42_i == coro_43);
};

Specifications

  • Version: master branch at commit 4799a77
  • Platform: Ubuntu 18.04
  • Subsystem:

Unexpected execptions are ignored

Expected Behavior

If this executes:

"unexcepted throw"_test = [] {
    throw std::runtime_error("unexpected");
};

I would expect that the test would fail.

Actual Behavior

The test doesn't fail, and silently passed. All of the rest of the expect() in that test are ignored.

Steps to Reproduce the Problem

  1. Run the above code

Specifications

  • Version: master
  • Platform: Fedora 31
  • Subsystem: ???

Print Predicate for Compile-time Assertions on Failure

Is it possible to have the predicate printed for constant expressions when there is a failure?

Take the following code as an example, which is taken from the tutorial but evaluated at compile-time.

int main() {
  using namespace boost::ut;
  expect(constant<1_i == 2>);
}

Is it possible to get output similar to the following at run-time where the predicate is shown on failure?

main.cpp:4:FAILED [1 == 2]
===============================================================================
tests:   0 | 0 failed
asserts: 1 | 0 passed | 1 failed

Conan Center

What do you think about making a [Boost-ext].UT package available in ConanCenter?
I would like to able to handle the dependency through Conan, and having it available in ConanCenter seems the easiest way to do so.

Their instructions for doing so are available here.
I found an example recipe for a header-only library on ConanCenter here.

Silence CMake Policy Warning CMP0054

When configuring the [Boost].µT project with CMake, I receive the following warning:

CMake Warning (dev) at cmake-build-debug/_deps/ut-src/CMakeLists.txt:41 (elseif):
  Policy CMP0054 is not set: Only interpret if() arguments as variables or
  keywords when unquoted.  Run "cmake --help-policy CMP0054" for policy
  details.  Use the cmake_policy command to set the policy and suppress this
  warning.

  Quoted variables like "MSVC" will no longer be dereferenced when the policy
  is set to NEW.  Since the policy is not set the OLD behavior will be used.

It looks like setting the minimum CMake version to 3.1 would silence this, according to the documentation of CMP0054.

What about support non-mangled outputs in cases

Expected Behavior

All tests passed (1 asserts in 1 tests)

Actual Behavior

[32mAll tests passed[0m (1 asserts in 1 tests)

Steps to Reproduce the Problem

1.Download a modern Mingw (GCC-10/Clang-11) compiler from http://winlibs.com/
1.No installation required
1.Run test: "run"_test = [] {expect(42_i==42);};

Specifications

  • Version: 1.1.8
  • Platform: OS Windows
  • Subsystem:

Thanks!

the installed cmake config files can't used

Expected Behavior

find_package(ut) # works

Actual Behavior

cmake -S test -B build/test -G Ninja -DCMAKE_PREFIX_PATH=/Users/clausklein/Workspace/cpp/ut/root -DTEST_INSTALLED_VERSION=1
-- The CXX compiler identification is GNU 10.2.0
-- Checking whether CXX compiler has -isysroot
-- Checking whether CXX compiler has -isysroot - yes
-- Checking whether CXX compiler supports OSX deployment target flag
-- Checking whether CXX compiler supports OSX deployment target flag - yes
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/local/bin/g++-10 - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
CMake Error at CMakeLists.txt:11 (find_package):
  Could not find a configuration file for package "ut" that is compatible
  with requested version "1.1".

  The following configuration files were considered but not accepted:

    /Users/clausklein/Workspace/cpp/ut/root/lib/cmake/ut/ut-config.cmake, version: unknown

-- Configuring incomplete, errors occurred!

Steps to Reproduce the Problem

diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index b00892a..00cb6fb 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -4,5 +4,12 @@
# Distributed under the Boost Software License, Version 1.0.
# (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
#
+cmake_minimum_required(VERSION 3.14...3.20)
+project(boost.ut LANGUAGES CXX)
+
+if(TEST_INSTALLED_VERSION)
+    find_package(ut 1.1 REQUIRED)
+endif()
+
add_subdirectory(ut)
add_subdirectory(ft)

Specifications

  • Version: 1.1.8
  • Platform: Apple OSX
  • Subsystem: Clang

CMake Variable Name Collisions & Cache Pollution

Several CMake options are likely to cause name collisions with similarly named variables, and variables are put into the CMake cache without consideration for parent projects.

Variables such as BUILD_TESTS are usually prefixed with the project's name to avoid potential name collisions.
An example for [Boost].µT is UT_BUILD_TESTS.

Projects incorporating [Boost].µT may wish to not have certain [Boost].µT-specific variables shown in the cache, like the BUILD_TESTS option.
Best practice is to use a non-cache variable as this leaves the decision up to parent projects whether to make those variables available in the cache or not.
A common idiom used for testing is to automatically include the tests if the project is the top-level project or if the appropriate variable is enabled.
The example below demonstrates this behavior.

if(TEST_UT OR CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR)
  add_subdirectory(test)
endif()

There is no call to the option() command, i.e.
option(TEST_UT "Enable building the tests" yes), as this populates the variable in the CMake cache.

Approval Test Integration Bug

Hi,

I really like the [Boost].UT framework. It's clean and easy to use and does away with macros. Thanks for developing it!

Anyways, I want to use approval tests (https://github.com/approvals/ApprovalTests.cpp) in a project I'm working on, and I'm implementing the integration between UT <> ApprovalTests.cpp. I followed the guidelines (https://github.com/approvals/ApprovalTests.cpp/blob/master/doc/SupportingNewTestFramework.md#top) and the UT reporter example (https://godbolt.org/z/gsAPKg). I made this simple example:

#include <string>

#include "ut.hpp"
#include "ApprovalTests.hpp"

namespace ut = boost::ut;

namespace ApprovalTests
{
    namespace cfg {
        class reporter : public ut::reporter<ut::printer> {
            private:
                TestName currentTest;

            public:
                auto on(ut::events::test_begin test_begin) -> void {
                    std::string name = test_begin.name;
                    currentTest.sections.emplace_back(name);
                    ut::reporter<ut::printer>::on(test_begin);
                }
                auto on(ut::events::test_run test_run) -> void { 
                    ut::reporter<ut::printer>::on(test_run);
                }
                auto on(ut::events::test_skip test_skip) -> void { ut::reporter<ut::printer>::on(test_skip); }
                auto on(ut::events::test_end test_end) -> void {
                    while (!currentTest.sections.empty()) {
                        currentTest.sections.pop_back();
                    }
                    ut::reporter<ut::printer>::on(test_end);
                }
                template <class TMsg>
                auto on(ut::events::log<TMsg>) -> void {}
                template <class TLocation, class TExpr>
                auto on(ut::events::assertion_pass<TLocation, TExpr> location) -> void {
                    currentTest.setFileName(location.location.file_name());
                    ApprovalTestNamer::currentTest(&currentTest);
                    ut::reporter<ut::printer>::on(location);
                }
                template <class TLocation, class TExpr>
                auto on(ut::events::assertion_fail<TLocation, TExpr> fail) -> void { ut::reporter<ut::printer>::on(fail); }
                auto on(ut::events::fatal_assertion fatal) -> void { ut::reporter<ut::printer>::on(fatal); }
                auto on(ut::events::exception exception) -> void { ut::reporter<ut::printer>::on(exception); }
                auto on(ut::events::summary summary) -> void { ut::reporter<ut::printer>::on(summary); }
        };
    }  // namespace cfg
}

template <>
auto ut::cfg<ut::override> = ut::runner<ApprovalTests::cfg::reporter>{};

int main()
{
    using namespace ut;
    using namespace ApprovalTests;

	"Approval"_test = []() {
        //expect(true);
        expect(nothrow([] { Approvals::verify("Approval Tests can verify text via the golder master method"); }));
	};

}

The issue is unless the line containing expect(true); is uncommented it doesn't work. It complains that currentTest was not configured. This seems more like a UT issue than an ApprovalTests issue.

Can anyone help? I'm not familiar enough with the UT code logic to track this issue. Or am I wrong and this is an ApprovalTests.cpp issue?

I appreciate any help.

Build errors on Visual Studio with clang-cl compiler when using boost.ut's CMakeLists.txt

Expected Behavior

Boost.ut builds and tests pass with VS2019 MSVC with clang-cl compiler

I know this, as we have been building that with the Approval Tests integration, which works file with this compiler.

Actual Behavior

Builds fail, with the error:

  Building Custom Rule D:/a/ApprovalTests.cpp.Builds/ApprovalTests.cpp.Builds/_deps/ut-src/example/CMakeLists.txt
clang-cl : error : unknown argument ignored in clang-cl: '-pedantic' [-Werror,-Wunknown-argument] [D:\a\ApprovalTests.cpp.Builds\ApprovalTests.cpp.Builds\_deps\ut-build\example\BDD.vcxproj]
clang-cl : error : unknown argument ignored in clang-cl: '-pedantic-errors' [-Werror,-Wunknown-argument] [D:\a\ApprovalTests.cpp.Builds\ApprovalTests.cpp.Builds\_deps\ut-build\example\BDD.vcxproj]

For an example failure, see https://github.com/claremacrae/ApprovalTests.cpp.Builds/runs/370748554

Steps to Reproduce the Problem

  1. cmake -G "Visual Studio 16 2019" -T "clangcl"
  2. build all of Boost.ut, including examples

Specifications

  • Version: 8004290
  • Platform: 'The CXX compiler identification is Clang 9.0.0 with MSVC-like command-line'
  • Subsystem: ?

Nature of fix

See https://github.com/approvals/ApprovalTests.cpp/blob/v.7.0.0/tests/UT_Tests/CMakeLists.txt#L17 for the type of check to use...

Someone previously reported this problem with ApprovalTests.cpp on this environment, and provided a fix which used windows-style compiler warnings if using clang-cl...

[Question]: What is the reason for the user defined literals?

In the following example:

"args"_test =
   [](const auto& arg) {
      expect(arg >= 1_i);
    }
  | std::vector{1, 2, 3};

You use a custom integer literal. Why? I noticed there is afair amount of code in the library to support this so I was just curious what its purpose is as it works fine without the literals.

Can't link the parallel runner with multiple translation units

Expected Behavior

I'd like to configure a parallel runner for a multi-translation unit program. Just took the example parallel_runner.cpp, fit into my main.cpp and expected the project to just build.

Actual Behavior

Tons of linker errors

Steps to Reproduce the Problem

  1. Take parallel_runner.cpp
  2. Create another test.cpp with tests
  3. Compile and try to link them together
  4. Observe the messages like the following:
[build] : && /usr/bin/c++ -g  CMakeFiles/aoc.dir/main.cpp.o CMakeFiles/aoc.dir/test2.cpp.o -o aoc  -lpthread  -lssl  -lcrypto  -lgmp  -ltbb && :
[build] /usr/bin/ld: /usr/bin/ld: DWARF error: could not find variable specification at offset befe
[build] CMakeFiles/aoc.dir/test2.cpp.o:(.bss._ZN5boost3ext2ut6v1_1_83cfgINS2_8overrideEJEEE[_ZN5boost3ext2ut6v1_1_83cfgINS2_8overrideEJEEE]+0x0): multiple definition of `boost::ext::ut::v1_1_8::cfg<boost::ext::ut::v1_1_8::override>'; /usr/bin/ld: DWARF error: could not find variable specification at offset c578
[build] CMakeFiles/aoc.dir/main.cpp.o:(.bss+0x0): first defined here
[build] collect2: error: ld returned 1 exit status

Specifications

  • Version: v1.1.8
  • Platform: Arch Linux x64
  • Subsystem:

The UT library is messing up the std::ofstream output

Expected Behavior

When I execute:

std::string msg = "The answer is: 42\n";
if (auto strm = std::ofstream("test.txt")) {
    strm << msg;
}

I would expect the following:

The answer is: 42

Actual Behavior

I actually get:

{T, h, e,  , a, n, s, w, e, r,  , i, s, :,  , 4, 2, }

If I comment out the unit test header, the above code works as expected.

Steps to Reproduce the Problem

  1. Run the example above

Specifications

  • Version: master
  • Platform: Fedora 31
  • Subsystem: ???

Recent CMake changes in this project have broken builds of dependent projects

Expected Behavior

1. boost.ut target seems to no longer be created

That the target boost.ut is still provided, to link against, when this project is obtained via add_subdirectory(), FetchContent() or similar...

2. include file no longer found: #include <boost/ut.hpp>

Example code - this is how I obtain the boost.ut project - UtVersion is master:

https://github.com/claremacrae/ApprovalTests.cpp.CMakeSamples/blob/main/dev_approvals_fetch_content/CMakeLists.txt#L130-L133

FetchContent_Declare(boost.ut
        GIT_REPOSITORY https://github.com/boost-ext/ut.git
        GIT_TAG ${UtVersion})
FetchContent_MakeAvailable(boost.ut)

The repo goes on to use the target boost.ut.

This directory and GitHub action exists to help the ApprovalTests.cpp project have early detection of any changes in test frameworks that might affect our users, when we do our next release.

This has been working for some months, and was still working on the 24th of Feb.

Actual Behavior

The next build, on 27th Feb at 01:10 GMT gave these errors

1. in clang builds:

Fetching boost.ut...
CMake Error at CMakeLists.txt:137 (target_compile_options):
  Cannot specify compile options for target "boost.ut" which is not built by
  this project.

2. in gcc builds:

Building CXX object approvaltests.cpp_build/tests/UT_Tests/CMakeFiles/UT_Tests.dir/UTApprovalTestTests.cpp.o
In file included from /home/runner/work/ApprovalTests.cpp.CMakeSamples/ApprovalTests.cpp.CMakeSamples/ApprovalTests.cpp/ApprovalTests/ApprovalTests.hpp:68,
                 from /home/runner/work/ApprovalTests.cpp.CMakeSamples/ApprovalTests.cpp.CMakeSamples/ApprovalTests.cpp/tests/UT_Tests/UTApprovalTestTests.cpp:3:
/home/runner/work/ApprovalTests.cpp.CMakeSamples/ApprovalTests.cpp.CMakeSamples/ApprovalTests.cpp/ApprovalTests/../ApprovalTests/integrations/ut/UTApprovals.h:14:10: fatal error: boost/ut.hpp: No such file or directory
   14 | #include <boost/ut.hpp>
      |          ^~~~~~~~~~~~~~
compilation terminated.
approvaltests.cpp_build/tests/UT_Tests/CMakeFiles/UT_Tests.dir/build.make:81: recipe for target 'approvaltests.cpp_build/tests/UT_Tests/CMakeFiles/UT_Tests.dir/UTApprovalTestTests.cpp.o' failed

Steps to Reproduce the Problem

Run this on a unix box with a recent version of either linux or gcc installed:

git clone https://github.com/claremacrae/ApprovalTests.cpp.CMakeSamples
cd ApprovalTests.cpp.CMakeSamples
git clone https://github.com/approvals/ApprovalTests.cpp.git
cd dev_approvals_fetch_content
./build.sh

Wait until all the dependencies have been cloned, and then one of the errors will occur - depending on whether you are using gcc or clang.

Specifications

The failure was detected in a GitHub Actions build. The failing builds can be seen here:

https://github.com/claremacrae/ApprovalTests.cpp.CMakeSamples/actions/runs/604457152

  • Linux Clang 6, 8, 9, and 10
  • Linux gcc 9 and 10
  • macOS Xcode 10.3, 11.7 and 12.

It's likely that the ones that passed are skipping the building of boost.ut

Printing values with BOOST_UT_FORWARD does not work

Printing values when a test fails doesn't seem to work with BOOST_UT_FORWARD.
Given the two files:
a.cpp:

#define BOOST_UT_IMPLEMENTATION
#include <boost/ut.hpp>
int main () {}

and b.cpp

#define BOOST_UT_FORWARD
#include <boost/ut.hpp>
using namespace boost::ut;
suite b = []
{
    expect (that % 1 == 2);
};

and the command line

g++ -std=c++2a  -o a a.cpp b.cpp && ./a

I get

  b.cpp:8:FAILED [false]
===============================================================================
tests:   0 | 0 failed
asserts: 1 | 0 passed | 1 failed

The values of the test are not printed. Move the test into a.cpp (or put some tests into this file) and the values are printed. E.g. replace a.cpp above with

#define BOOST_UT_IMPLEMENTATION
#include <boost/ut.hpp>
    using namespace boost::ut;
suite a = []
{
    expect (that % 1 == 2);
};
int main () {}

and the result is

  a.cpp:6:FAILED [1 == 2]
  b.cpp:6:FAILED [1 == 2]
===============================================================================
tests:   0 | 0 failed
asserts: 2 | 0 passed | 2 failed

i.e. both tests print their values.

Non-Type Template Parameters causes compilation error with g++-10

Specifications

  • Version: gcc 10.0.1 (20200327)
  • Platform: Linux - Centos 7

Error Message

../example/tmp.cpp:19:12: error: 'boost::ut::v1_1_7::detail::eq_<boost::ut::v1_1_7::detail::integral_constant<42>, int>' is not a valid type for a template non-type parameter because it is not structural
19 | expect(constant<42_i == i> and type == type and
| ^~~~~~~~~~~~~~~~~~~
In file included from ../example/tmp.cpp:8:
../include/boost/ut.hpp:738:14: note: 'boost::ut::v1_1_7::detail::eq_<boost::ut::v1_1_7::detail::integral_constant<42>, int>::lhs_' is not public
738 | const TLhs lhs_{};
| ^~~~

en.cppreference.com states, for non-type template parameters
a literal class type with the following properties:

  • all base classes and non-static data members are public and non-mutable and
  • the types of all bases classes and non-static data members are structural types or (possibly multi-dimensional) array thereof.

As a workaround, I've commented the ifdefs for Constant in line 2190

explicit operator bool does not work with expect()

Expected Behavior

The following should compile and work:

    "test bool"_test = [] {
        std::unique_ptr<int> ptr;
        expect(ptr);
    };

Actual Behavior

This does not compile:

/home/user/working/bsl/tests/ifarray.cpp: In lambda function:
/home/user/working/bsl/tests/ifarray.cpp:66:19: error: no matching function for call to ‘expect(std::unique_ptr<int>&)’
   66 |         expect(ptr);
      |                   ^
In file included from /home/user/working/bsl/tests/ifarray.cpp:26:
/home/user/working/bsl/tests/ut.h:1606:9: note: candidate: ‘template<class TExpr, typename std::enable_if<(is_op_v<TExpr> || is_same_v<bool, TExpr>), int>::type <anonymous> > constexpr auto boost::ut::v1_0_0::expect(const TExpr&, const std::experimental::fundamentals_v2::source_location&)’
 1606 |         expect(
      |         ^~~~~~
/home/user/working/bsl/tests/ut.h:1606:9: note:   template argument deduction/substitution failed:
/home/user/working/bsl/tests/ut.h:1604:79: error: no type named ‘type’ in ‘struct std::enable_if<false, int>’
 1604 |                 type_traits::is_op_v<TExpr> or std::is_same_v<bool, TExpr>> = 0>
      |                                                                               ^
make[2]: *** [tests/CMakeFiles/test_ifarray.dir/build.make:63: tests/CMakeFiles/test_ifarray.dir/ifarray.cpp.o] Error 1
make[1]: *** [CMakeFiles/Makefile2:246: tests/CMakeFiles/test_ifarray.dir/all] Error 2
make: *** [Makefile:130: all] Error 2

Steps to Reproduce the Problem

  1. Just compile the above test

Specifications

  • Version: master
  • Platform: Fedora 31
  • Subsystem: ???

Runner Example Segfaults

Expected Behavior

The runner example should build and run successfully.

Actual Behavior

The runner example program segfaults.

Steps to Reproduce the Problem

  1. mkdir build
  2. cmake ..
  3. cmake --build .

Specifications

  • Version: Current master (commit hash 35ac252)
  • Platform: macOS 10.15 Catalina
  • Toolchain: CMake 3.16.2 / Ninja / XCode / MacPorts LLVM Clang 9.0.0

Parallel Runner Example Fails to Build on GCC 9

Expected Behavior

Parallel Runner example should build without error.

Actual Behavior

The parallel_runner fails to build with GCC 9.1.1 (devtoolset-9) on RHEL 7 and with Mingw64 (GCC 9.2) on Windows 10.
The <execution> header pulls in the exact same missing include on both platforms:

/opt/rh/devtoolset-9/root/usr/include/c++/9/pstl/parallel_backend_tbb.h:19:10: fatal error: tbb/blocked_range.h: No such file or directory
  19 | #include <tbb/blocked_range.h>
     |        ^~~~~~~~~~~~~~~~~~~~~

Steps to Reproduce the Problem

Build the parallel_runner test on RHEL 7 using devtoolset-9 with C++20 enabled.
Or
Build the parallel_runner test on Mingw64 (GCC 9.2) with C++20 enabled.

Specifications

  • Version: 1.1.6
  • Platform: RHEL 7 / Windows 10 (MSYS2)
  • Toolchain: devtoolset-9 / Mingw64

g++-10 Infinite Recursion in operator==(string_view,string_view)

Expected Behavior

Expected example/reporters to be built and run without a problem

Actual Behavior

It segfaults with the following backtrace

#112224 0x0000000000407b2d in boost::ut::v1_1_7::detail::neq_<std::basic_string_view<char, std::char_traits >, std::basic_string_view<char, std::char_traits > >::neq_ (this=0x7fffffffccb0, lhs=..., rhs=...) at ../include/boost/ut.hpp:766
#112225 0x0000000000406fa8 in boost::ut::v1_1_7::operators::operator!= (lhs=..., rhs=...) at ../include/boost/ut.hpp:1860
#112226 0x0000000000407abe in ZZN5boost2ut6v1_1_76detail4neq_ISt17basic_string_viewIcSt11char_traitsIcEES7_EC4ERKS7_SA_ENKUlvE_clEv (this=0x7fffffffcd90) at ../include/boost/ut.hpp:764
#112227 0x0000000000407b2d in boost::ut::v1_1_7::detail::neq
<std::basic_string_view<char, std::char_traits >, std::basic_string_view<char, std::char_traits > >::neq_ (this=0x7fffffffcd90, lhs=..., rhs=...) at ../include/boost/ut.hpp:766
#112228 0x0000000000406fa8 in boost::ut::v1_1_7::operators::operator!= (lhs=..., rhs=...) at ../include/boost/ut.hpp:1860
#112229 0x00000000004066dd in operator() (__closure=0x0) at ../example/cfg/reporter.cpp:41
#112230 0x000000000040671d in _FUN () at ../example/cfg/reporter.cpp:42
#112231 0x00000000004072d7 in boost::ut::v1_1_7::events::test<void ()(), boost::ut::v1_1_7::none>::run_impl (test=0x40670f <_FUN()>) at ../include/boost/ut.hpp:504
#112232 0x0000000000407301 in boost::ut::v1_1_7::events::test<void (
)(), boost::ut::v1_1_7::none>::operator() (this=0x7fffffffcf10) at ../include/boost/ut.hpp:500
#112233 0x0000000000407504 in boost::ut::v1_1_7::runner<cfg::reporter, 16>::on<void ()(), boost::ut::v1_1_7::none> (this=0x60c280 <boost::ut::v1_1_7::cfgboost::ut::v1_1_7::override>, test=...) at ../include/boost/ut.hpp:1364
#112234 0x0000000000407678 in boost::ut::v1_1_7::detail::on<, boost::ut::v1_1_7::events::test<void (
)(), boost::ut::v1_1_7::none> > (event=<unknown type in /home/furkan/.local/cpp/ut/build/example/reporter, CU 0x0, DIE 0xaa46>) at ../include/boost/ut.hpp:1563
#112235 0x00000000004076d7 in boost::ut::v1_1_7::detail::test::operator=<>(boost::ut::v1_1_7::detail::test_location<void (*)()>) (this=0x7fffffffd030, test=...) at ../include/boost/ut.hpp:1586
#112236 0x0000000000406794 in main () at ../example/cfg/reporter.cpp:39

Specifications

  • Version: g++ 10.0.1 (20200327)
  • Platform: Linux
  • Subsystem: Centos 7

Edit: When I comment those operators, this time I get the same error in example/expect but this time string instead of string_view

install target

For this to work with ExternalProject_Add, it really should have an install target.

No error reported when not finding gherkin steps.

Expected Behavior

Did not find implementation for pattern "Given I have a victor"
Did not find implementation for pattern "When I resize begger"
Did not find implementation for pattern "Then The size suould increase"
All tests passed (3 fatal in 1 tests)

Actual Behavior

All tests passed (0 asserts in 1 tests)

Steps to Reproduce the Problem

https://godbolt.org/z/xxEEGo

int main() {
using namespace boost::ut;

bdd::gherkin::steps steps = [](auto& steps) {
steps.feature("Vector") = [&] {
steps.scenario("*") = [&] {
steps.given("I have a vector") = [&] {
std::vector v(5);
expect((5_ul == std::size(v)) >> fatal);
steps.when("I resize bigger") = [&] { v.resize(10); };
steps.then("The size should increase") = [&] { expect(10_ul == std::size(v)); };
};
};
};
};

"Vector"_test = steps |
R"(
Feature: Vector
Scenario: Resize
Given I have a victor
When I resize begger
Then The size suould increase
)";
}

Specifications

  • Version: master
  • Platform:
  • Subsystem:

Exception objects inspection while handling.

Expected Behavior

Something similar to
BOOST_REQUIRE_EXCEPTION(expression, exception_type, predicate);

In Boost.Test (at least) there is a useful opportunity to inspect exception object itself.
BOOST_REQUIRE_EXCEPTION

Actual Behavior

It seems now ut can not simulate this behaviour by overriding runner/reporter/... In the case of

expect(throws<std::runtime_error>([] { throw std::runtime_error{""}; }))

by the time runner::on(ut::events::assertion<TExpr>) fires, a decision on the status of test validation has already been made, without the ability to somehow intervene either before or after for completion.

As a workaround I just embed a 'predicate function' to ut::throws and ut::detail::throws_ as overriding variants.

template<class T>
void func_1(T const & e)
{
  boost::ut::expect(e.code() == ex_code_1);
}
...
"test1"_test = [] {

      /* some 'when' precondition */
      expect(throws<some_exception>(
          [] { raising_func(); },
          func_1) ); // fails at func_1 body line if raising_func throws with ex_code_2
      /* some other 'when' precondition */
      expect(throws<some_exception>(
          [] { raising_func(); },
          func_2) ); // passes if raising_func throws with ex_code_2
      expect((throws<some_exception>([] { throw some_exception(); })) >> fatal); // passes
      expect((throws<some_exception>([] { raising_func(); })) >> fatal); // passes if raising any "some_exception" (or fail at this particular line)
      expect((throws([] { throw 1; })) >> fatal); // as always!
}

And my question is: Will similar(somewhat) functionality be introduced to ut sometime?

Fails to compile with GCC trunk

In case you're interested in upcoming versions of GCC, ut fails to compile with g++ (GCC) 10.0.0 20200108 (experimental)

$ g++ -std=c++2a -Wall -Wextra -I../../external/boost_experimental/ut/include   -c -o tests.o tests.cpp
In file included from tests.cpp:5:
../../external/boost_experimental/ut/include/boost/ut.hpp: In instantiation of ‘constexpr boost::ut::v1_1_6::detail::neq_<TLhs, TRhs>::neq_(const TLhs&, const TRhs&) [with TLhs = std::basic_string_view<char>; TRhs = std::basic_string_view<char>]’:
../../external/boost_experimental/ut/include/boost/ut.hpp:1747:31:   required from here
../../external/boost_experimental/ut/include/boost/ut.hpp:614:30: error: return type of ‘constexpr auto boost::ut::v1_1_6::operators::operator==(std::string_view, std::string_view)’ is not ‘bool’
  614 |             return get(lhs_) != get(rhs_);
      |                    ~~~~~~~~~~^~~~~~~~~~~~
../../external/boost_experimental/ut/include/boost/ut.hpp:614:30: note: used as rewritten candidate for comparison of ‘std::basic_string_view<char>’ and ‘std::basic_string_view<char>’
../../external/boost_experimental/ut/include/boost/ut.hpp:616:12: error: cannot convert ‘<brace-enclosed initializer list>’ to ‘const bool’ in initialization
  616 |         }()} {}
      |            ^
make: *** [<builtin>: tests.o] Error 1

At first glance I'd say that the call to operator!=(std::string_view, std::string_view) in neq_ uses the overload of operator==(std::string_view, std::string_view defined just above, which of course doesn't return a bool. Adding a using std::operator== solves the issue.

Now as to why GCC 9 doesn't complain, I have no idea.

Disable tests

Could you add an option to disable the compilation of the examples/tests so that a make install just installs the header into a desired prefix with no compilation steps?

Logging does not work with BOOST_UT_FORWARD

This

#define BOOST_UT_FORWARD
#include <boost/ut.hpp>
using namespace boost::ut;
suite some_tests = []{
    "x"_test = []{
        log << "Hello";
    };
};

does not compile. It does without #define BOOST_UT_FORWARD.

Visual C++ Compiler Error C2326 using Constexpr Array from Lambda in a Test Case in a Throws Assertion

Expected Behavior

A function with a throws / nothrow assertion should compile with the Visual C++ Compiler when given a lambda which uses a constexpr std::array variable... right? This is a very specific issue 😩

#include <array>
#include <boost/ut.hpp>

int main() {
  using namespace boost::ut;
  "example_test"_test = [] {
    constexpr std::array<int, 1> i{{10}};
    expect(nothrow([] { return i.front(); }));
  };
}

It is also interesting to note this only occurs in a test case and not when used directly from main, hence the following compiles:

#include <array>
#include <boost/ut.hpp>

int main() {
  using namespace boost::ut;
  expect(nothrow([] {
    constexpr std::array<int, 1> i{{10}};
    return i.front();
  }));
}

Using a constexpr int instead of a std::array also compiles:

#include <boost/ut.hpp>

int main() {
  using namespace boost::ut;
  "example_test"_test = [] {
    constexpr int i{10};
    expect(nothrow([] { return i; }));
  };
}

Perhaps this is a bug in the MSVC compiler?

Actual Behavior

Error C2326 is given stating that the lambda function cannot access the constexpr variable.

I can workaround this issue by putting the constexpr variable within the lambda itself, as follows:

#include <array>
#include <boost/ut.hpp>

int main() {
  using namespace boost::ut;
  "example_test"_test = [] {
    expect(nothrow([] {
      constexpr std::array<int, 1> i{{10}};
      return i.front();
    }));
  };
}

Steps to Reproduce the Problem

Compiling the preceding snippets above using C++20 / MSVC 16.4.4 should showcase the error.
By the way, Clang compiles the problematic code just fine.

Specifications

  • Version: 1.1.6
  • Platform: Windows
  • Subsystem: MSVC 16.4.4

Logging output when using fatal assertions

Example

A fatal assertion with logged output for failure.

expect((true == false) >> fatal) << "Incorrect!";

Expected Behavior

When the assertion fails, "Incorrect!" should be printed.

FAILED [false] Incorrect!

Actual Behavior

When the assertion fails, "Incorrect!" is not printed.

FAILED [false]

Steps to Reproduce the Problem

  1. A failing fatal assertion with streamed output will fail to print the output.

Specifications

  • Version: v1.1.8
  • Platform: macOS 10.15 Catalina
  • Subsystem: MacPorts, LLVM Clang 10.0.1

Document if mut is UB

I am not a language lawyer, but I think it would be nice if mut was documented in a sense that if it is technically UB(as used in examples in tutorial), but it works in practice then that is is mentioned (in docs or code comments).

cppref says this:

const_cast makes it possible to form a reference or pointer to non-const type that is actually referring to a const object or a reference or pointer to non-volatile type that is actually referring to a volatile object. Modifying a const object through a non-const access path and referring to a volatile object through a non-volatile glvalue results in undefined behavior.

Apologies in advance if I misunderstood the cppref quote.

Compilation Warnings and Tutorial Issues

This is a report about some issues that occurred as I worked through the Tutorial.

Environment

OS: bento/ubuntu-20.04

Compiler: gcc version 9.3.0 (Ubuntu 9.3.0-10ubuntu2)
          c++ -> x86_64-linux-gnu-g++-9

Project Directory Structure

./ut-tutorial/
  |
  ├── ut.hpp
  |
  └── main.cpp

Tutorial Executions: Step 0 "Get it..."

import boost.ut;

int main() { }
$ c++ -std=c++2a main.cpp && ./a.out
main.cpp:1:1: error: ‘import’ does not name a type
    1 | import boost.ut;
      | ^~~~~~

Compilation with flag -std=c++2a  and statement import boost.ut; failed.


#include "ut.hpp"

int main() { }
$  c++ -std=c++17 main.cpp
In file included from main.cpp:1:
ut.hpp: In member function ‘void boost::ut::v1_1_7::bdd::gherkin::steps::next(const TPattern&)’:
ut.hpp:2457:22: warning: range-based ‘for’ loops with initializer only available with ‘-std=c++2a’ or ‘-std=gnu++2a’
 2457 |     for (auto i = 0; const auto& step : gherkin_) {
      |                      ^~~~~

Compilation with flag "-std=c++17" results in a warning flag.


$ c++ -std=c++2a main.cpp && ./a.out
-bash: ./a.out: Permission denied
$ ./a.out
$

The tutorial statement $CXX main.cpp && ./a.out assumes something about permissions I needed to separate the commands. I found nothing on Google on how to address this. How did the combined command work for you?

Tutorial Executions: Step 2 "Group it..."

#include "ut.hpp"
#include <vector>

int main() {
  using namespace boost::ut;
  "[vector]"_test = [] {
    std::vector<int> v(5);

    !expect(5_ul == size(v));

    should("resize bigger") = [v] {
      mut(v).resize(10);
      expect(10_ul == size(v));
    };

    !expect(5_ul == size(v));

    should("resize smaller") = [=]() mutable {
      v.resize(0);
      expect(0_ul == size(v));
    };
  };
}
$  c++ -std=c++2a main.cpp
$ ./a.out
All tests passed (4 asserts in 1 tests)
$
$ c++ -std=c++2a -Wall main.cpp
In file included from main.cpp:1:
ut.hpp: In instantiation of ‘void boost::ut::v1_1_7::bdd::gherkin::steps::next(const TPattern&) [with TPattern = std::__cxx11::basic_string<char>]’:
ut.hpp:2374:35:   required from here
ut.hpp:2458:15: warning: comparison of integer expressions of different signedness: ‘int’ and ‘long unsigned int’ [-Wsign-compare]
 2458 |       if (i++ == step_) {
      |           ~~~~^~~~~~~~

$ ./a.out
All tests passed (4 asserts in 1 tests)
$

Compilation with warning flag "-Wall" resulted in a warning about comparing signed and unsigned integers.

Location and Exception information

Hi,

I'm working on the ApprovalTests.cpp integration and I've ran into a couple issues:

  • I need the filename from where the test is implemented (caller function). assertion_pass and assertion_fail have that information but not test_begin. As this information needs to be set before the test is actually run, test_begin needs to have that information. I've been looking at the UT code, but haven't quite figure out the best way to do it.

  • In ApprovalTests.cpp, when a test fails it throws an exception which import information inside the exception (e.what()). So when a test throws an exception I would like to check it's contents (and possibility it's type), not simply see if the test threw an exception or not, as it's implemented now. What do you think it would be the best approach here. Simply to add a std::string to the exception struct and store the exception data there? Again not sure the best approach to change the code here.

    Any help would be appreciated. If you want to take a look at the integration between ApprovalTests.cpp and UT take a look at this pull request at the UTApprovals.h file.

    Thanks!

Allow default runner to run and reporting on unit tests before exit()

There are a number of cases when you want unit tests to run earlier than the program exits. E.g. an application may want to run and report failing unit tests on startup. While this is doable with a custom runner, it should be fairly straightforward to add a method to replicate the default runner's destructor behavior. Something that effectively allows:

    int main(int argc, char **argv)
    {
        boost::ut::cfg<>.run();                            // (1)
        boost::ut::cfg<>.on(boost::ut::events::summary{}); // (2) currently not accessible
        ...

Possible APIs could be a bool run_and_report() method to replicate the current destructor functionality or more flexible a void report_summary() to just do (2).

prevent clang-tidy warnings

Expected Behavior

no warnings

Actual Behavior

WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='154', severity='warning', message='multiple declarations in a single statement reduces readability [readability-isolate-declaration]', column='3')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='261', severity='warning', message='do not declare C-style arrays, use std::array<> instead [modernize-avoid-c-arrays]', column='19')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='263', severity='warning', message='multiple declarations in a single statement reduces readability [readability-isolate-declaration]', column='3')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='285', severity='warning', message='do not declare C-style arrays, use std::array<> instead [modernize-avoid-c-arrays]', column='19')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='288', severity='warning', message='statement should be inside braces [readability-braces-around-statements]', column='25')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='298', severity='warning', message='do not declare C-style arrays, use std::array<> instead [modernize-avoid-c-arrays]', column='19')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='300', severity='warning', message='statement should be inside braces [readability-braces-around-statements]', column='25')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='517', severity='warning', message="function 'what' should be marked [[nodiscard]] [modernize-use-nodiscard]", column='3')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='540', severity='warning', message='do not implicitly decay an array into a pointer; consider using gsl::array_view or an explicit cast instead [hicpp-no-array-decay]', column='10')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='546', severity='warning', message="return type 'const type_<TOther>' is 'const'-qualified at the top level, which may reduce code readability without improving const correctness [readability-const-return-type]", column='17')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='929', severity='warning', message='do not declare C-style arrays, use std::array<> instead [modernize-avoid-c-arrays]', column='11')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='1314', severity='warning', message="do not use 'else' after 'return' [readability-else-after-return]", column='7')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='1329', severity='warning', message="throwing an exception whose type 'events::fatal_assertion' is not derived from 'std::exception' [hicpp-exception-baseclass]", column='11')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='1428', severity='warning', message="method 'operator=' can be made const [readability-make-member-function-const]", column='18')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='1454', severity='warning', message='do not implicitly decay an array into a pointer; consider using gsl::array_view or an explicit cast instead [hicpp-no-array-decay]', column='26')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ft/test_suite_2.cpp', lineno='14', severity='warning', message="throwing an exception whose type 'int' is not derived from 'std::exception' [hicpp-exception-baseclass]", column='62')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/benchmark/test.cpp', lineno='10', severity='warning', message="function 'main' exceeds recommended size/complexity thresholds [readability-function-size]", column='5')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='546', severity='warning', message="return type 'const type_<int>' is 'const'-qualified at the top level, which may reduce code readability without improving const correctness [readability-const-return-type]", column='17')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='1446', severity='warning', message='do not implicitly decay an array into a pointer; consider using gsl::array_view or an explicit cast instead [hicpp-no-array-decay]', column='28')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='1563', severity='warning', message='do not implicitly decay an array into a pointer; consider using gsl::array_view or an explicit cast instead [hicpp-no-array-decay]', column='25')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/include/boost/ut.hpp', lineno='2204', severity='warning', message='do not implicitly decay an array into a pointer; consider using gsl::array_view or an explicit cast instead [hicpp-no-array-decay]', column='44')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ut/ut.cpp', lineno='71', severity='warning', message='use emplace_back instead of push_back [modernize-use-emplace]', column='25')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ut/ut.cpp', lineno='73', severity='warning', message='use emplace_back instead of push_back [modernize-use-emplace]', column='25')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ut/ut.cpp', lineno='75', severity='warning', message='use emplace_back instead of push_back [modernize-use-emplace]', column='25')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ut/ut.cpp', lineno='96', severity='warning', message="throwing an exception whose type 'ut::events::fatal_assertion' is not derived from 'std::exception' [hicpp-exception-baseclass]", column='11')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ut/ut.cpp', lineno='130', severity='warning', message="throwing an exception whose type 'int' is not derived from 'std::exception' [hicpp-exception-baseclass]", column='37')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ut/ut.cpp', lineno='238', severity='warning', message="function 'main' exceeds recommended size/complexity thresholds [readability-function-size]", column='5')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ut/ut.cpp', lineno='314', severity='warning', message="the 'empty' method should be used to check for emptiness instead of comparing to an empty object [readability-container-size-empty]", column='19')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ut/ut.cpp', lineno='479', severity='warning', message="'auto old_cout' can be declared as 'auto *old_cout' [readability-qualified-auto]", column='7')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ut/ut.cpp', lineno='480', severity='warning', message="'auto old_cerr' can be declared as 'auto *old_cerr' [readability-qualified-auto]", column='7')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ut/ut.cpp', lineno='1006', severity='warning', message="the 'empty' method should be used to check for emptiness instead of comparing to an empty object [readability-container-size-empty]", column='14')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ut/ut.cpp', lineno='1108', severity='warning', message="throwing an exception whose type 'int' is not derived from 'std::exception' [hicpp-exception-baseclass]", column='34')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ut/ut.cpp', lineno='1109', severity='warning', message="throwing an exception whose type 'int' is not derived from 'std::exception' [hicpp-exception-baseclass]", column='54')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ut/ut.cpp', lineno='1113', severity='warning', message="throwing an exception whose type 'int' is not derived from 'std::exception' [hicpp-exception-baseclass]", column='35')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ut/ut.cpp', lineno='1150', severity='warning', message='statement should be inside braces [readability-braces-around-statements]', column='28')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ut/ut.cpp', lineno='1150', severity='warning', message="throwing an exception whose type 'int' is not derived from 'std::exception' [hicpp-exception-baseclass]", column='35')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ut/ut.cpp', lineno='1161', severity='warning', message="throwing an exception whose type 'events::exception' is not derived from 'std::exception' [hicpp-exception-baseclass]", column='15')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ut/ut.cpp', lineno='1164', severity='warning', message="throwing an exception whose type 'int' is not derived from 'std::exception' [hicpp-exception-baseclass]", column='41')
WarningErrorEntry(path='/Users/clausklein/Workspace/cpp/ut/test/ut/ut.cpp', lineno='1605', severity='warning', message="function 'get' should be marked [[nodiscard]] [modernize-use-nodiscard]", column='7')

Steps to Reproduce the Problem

  1. merge #421
  2. run-clang-tidy.py -p build -header-filter='.*' -checks='-cert-*' test
  3. see https://github.com/ClausKlein/ut/runs/1973383718?check_suite_focus=true
  4. and https://github.com/boost-ext/ut/blob/d15ad0e4945d187d4500d4c191cda69262f4f884/.github/workflows/macos.yml

Specifications

  • Version: 1.1.8
  • Platform: OSX
  • Subsystem: Clang

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.