diff --git a/README.md b/README.md index 04566c29..dc39d75f 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ The goal of _snitch_ is to be a simple, cheap, non-invasive, and user-friendly t - [Exceptions](#exceptions) - [Header-only build](#header-only-build) - [`clang-format` support](#clang-format-support) - - [Why _snitch_?](#why-_snitch_) +- [Contributing](#contributing) +- [Why _snitch_?](#why-_snitch_) @@ -240,10 +241,10 @@ Results for Debug builds: | **Debug** | _snitch_ | _Catch2_ | _doctest_ | _Boost UT_ | |-----------------|----------|----------|-----------|------------| | Build framework | 2.0s | 41s | 2.0s | 0s | -| Build tests | 64s | 79s | 73s | 118s | -| Build all | 66s | 120s | 75s | 118s | -| Run tests | 31ms | 76ms | 63ms | 20ms | -| Library size | 3.4MB | 38.6MB | 2.8MB | 0MB | +| Build tests | 65s | 79s | 73s | 118s | +| Build all | 67s | 120s | 75s | 118s | +| Run tests | 34ms | 76ms | 63ms | 20ms | +| Library size | 3.3MB | 38.6MB | 2.8MB | 0MB | | Executable size | 32.5MB | 49.3MB | 38.6MB | 51.9MB | Results for Release builds: @@ -252,9 +253,9 @@ Results for Release builds: |-----------------|----------|----------|-----------|------------| | Build framework | 2.6s | 47s | 3.5s | 0s | | Build tests | 134s | 254s | 207s | 289s | -| Build all | 136s | 301s | 210s | 289s | +| Build all | 137s | 301s | 210s | 289s | | Run tests | 24ms | 46ms | 44ms | 5ms | -| Library size | 0.65MB | 2.6MB | 0.39MB | 0MB | +| Library size | 0.63MB | 2.6MB | 0.39MB | 0MB | | Executable size | 8.9MB | 17.4MB | 15.2MB | 11.3MB | Notes: @@ -939,7 +940,26 @@ IfMacros: ['SECTION', 'SNITCH_SECTION'] SpaceBeforeParens: ControlStatementsExceptControlMacros ``` +## Contributing -### Why _snitch_? +Contributions to the source code are always welcome! Simply follow the rules laid out below. If you are not familiar with contributing to an open-source project, feel free to open a [Discussion](https://github.com/cschreib/snitch/discussions/categories/ideas) and ask for guidance. Regardless, you will receive help all the way, and particularly during the code review. + +The process: + - If you are considering adding a feature from *Catch2* that *snitch* currently does not support, please check the [*Catch2* support roadmap](doc/comparison_catch2.md) first. + - Please check the [Issue Tracker](https://github.com/cschreib/snitch/issues) for any issue (open or closed) related to the feature you would like to add, or the problem you would like to solve. Read the discussion that has taken place there, if any, and check if any decision was taken that would be incompatible with your planned contribution. + - If the path is clear, fork this repository and commit your changes to your own fork. + - Use "atomic" commits (check that the code compiles before committing) and reasonably clear commit messages (no "WIP"). Linear history is preferred (i.e., avoid merge commits), but will not be enforced. + - Check your code mostly follows the [*snitch* C++ Coding Guidelines](doc/coding_guidelines.md). + - Run `clang-format` on your code before committing. The `.clang-format` file at the root of this repository contains all the formatting rules, and will be used automatically. + - Add tests to cover your new code if applicable (see the `tests` sub-folder). + - Run the *snitch* tests and fix any failure if you can (CMake can run them for you if you ask it to "build" the `snitch_runtime_tests_run` target, otherwise just run manually `build/tests/snitch_runtime_tests`). + - Open a [Pull Request](https://github.com/cschreib/snitch/pulls), with a description of what you are trying to do. + - If there are issues you were unable to solve on your own (e.g., tests failing for reasons you do not understand, or high-impact design decisions), please feel free to open the pull request as a "draft", and highlight the areas that you need help with in the description. Once the issues are addressed, you can take your Pull Request out of draft mode. + - Your code will then be reviewed, and the reviewer(s) may leave comments and suggestions. It is up to you to act on these comments and suggestions, and commit any required code changes. It's OK to push back on a suggestion if you have a good reason; don't always assume the reviewer is right. + - When all comments are addressed, the reviewer(s) should mark the Pull Request as "approved", at which point anyone involved can merge it into the main branch. + - Job done! Congratulations. + + +## Why _snitch_? Libraries and programs sometimes do shady or downright illegal stuff (i.e., bugs, crashes). _snitch_ is a library like any other; it may have its own bugs and faults. But it's a snitch! It will tell you when other libraries and programs misbehave, with the hope that you will overlook its own wrongdoings. diff --git a/doc/coding_guidelines.md b/doc/coding_guidelines.md new file mode 100644 index 00000000..3d551f08 --- /dev/null +++ b/doc/coding_guidelines.md @@ -0,0 +1,53 @@ +# Guidelines for writing C++ code for *snitch* + +Unless otherwise stated, follow the [C++ Core Guidelines](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines). Below are exceptions to these guidelines, or more opinionated choices. + + +## `noexcept` + +Follow the [Lakos Rule](https://quuxplusone.github.io/blog/2018/04/25/the-lakos-rule/) (with some tweaks): + - Functions with a wide contract (i.e., with no precondition) should be marked as unconditionally `noexcept`. + - Functions with a narrow contract (i.e., with preconditions), should not be marked as `noexcept`. + - If a template function is conditionally-wide, (e.g., its purpose is only to be a wrapper or adapter over another function), then it may be marked conditionally `noexcept`. + +In particular: + - Do not mark a function `noexcept` just because it happens not to throw. The decision should be based on the *interface* of the function (which includes its pre-condition), and not its implementation. + +Rationale: + - Easy transition to using contracts when they come to C++. + - Enable testing for pre-condition violations by conditionally throwing. + + +## Heap allocations + +*snitch* code must not directly or indirectly allocate heap (or "free store") memory while running tests. This means that a number of common C++ STL classes cannot be used (at least not with the default allocator): + - `std::string`: use `std::string_view` (for constant strings) or `snitch::small_string` (for variable strings) instead. + - `std::vector`, `std::map`, `std::set`, and their variants: use `std::array` (for fixed size arrays) or `snitch::small_vector` (for variable size arrays) instead. + - `std::function`: use `snitch::small_function` instead. + - `std::unique_ptr`, `std::shared_ptr`: use values on the stack, and raw pointers for non-owning references. + +Unfortunately, the standard does not generally specify if a function or class allocates heap memory or not. We can make reasonable guesses for simple cases; in particular the following are fine to use: + - `std::string_view`, + - `std::array`, + - `std::span`, + - `std::variant` with `std::get_if` and `std::visit`, + - `std::optional`, + - `std::tuple`, + - Functions in `` ([except `std::stable_sort`, `std::stable_partition`, and `std::inplace_merge`](https://stackoverflow.com/a/46714875/1565581)). + - Concepts and type traits. + +Any type or function not listed above *should* be assumed to use heap memory unless demonstrated otherwise. One way to monitor this on Linux is to use [valgrind/massif](https://valgrind.org/docs/manual/ms-manual.html). + +Note: If necessary, it is acceptable to allocate/de-allocate heap memory when a test is not running, i.e., either at the start or end of the program, or (if single-threaded) in between test cases. For example, as of writing this, with GCC 10 on Linux and the default reporter, *snitch* performs heap allocations on two occasions: + - several allocations at program startup, generated by [`libstdc++` initialisation](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=68606). + - one at the first console output, generated by `glibc` for its internal I/O buffer. + + +## Heavy headers and compilation time + +One of the advantages of *snitch* over competing testing framework is fast compilation of tests. To preserve this advantage, ["heavy" STL headers](https://artificial-mind.net/projects/compile-health/) should not be included in *snitch* headers unless absolutely necessary. However, they can be included in the *snitch* implementation `*.cpp` files. + +Therefore: + - Place as much code as possible in the `*.cpp` files rather than in headers. + - When not possible (templates, constexpr, etc.), consider if you can use a short and clear handwritten alternative instead. For example, `std::max(a, b)` requires ``, but can also be written as `a > b ? a : b`. Some of the simplest algorithms in ``, like `std::copy`, can also be written with an explicit loop. + - Finally, consider if you really need the full feature from the STL, or just a small subset. For example, if you need a metaprogramming type list and don't need to instanciate the types, don't use `std::tuple<...>`: use a custom `template struct type_list {}` instead. diff --git a/include/snitch/snitch.hpp b/include/snitch/snitch.hpp index 7b1235dd..52211750 100644 --- a/include/snitch/snitch.hpp +++ b/include/snitch/snitch.hpp @@ -15,7 +15,6 @@ #include // for compile-time integer to string #include // for cli #include // for all strings -#include // for std::is_nothrow_* #include // for std::forward, std::move #include // for events and small_function @@ -65,6 +64,34 @@ struct section_id { std::string_view name = {}; std::string_view description = {}; }; + +template +concept signed_integral = std::is_signed_v; + +template +concept unsigned_integral = std::is_unsigned_v; + +template +concept floating_point = std::is_floating_point_v; + +template +concept convertible_to = std::is_convertible_v; + +template +concept enumeration = std::is_enum_v; + +template +struct overload : Args... { + using Args::operator()...; +}; + +template +overload(Args...) -> overload; + +template +struct constant { + static constexpr auto value = T; +}; } // namespace snitch namespace snitch::matchers { @@ -101,6 +128,181 @@ constexpr std::string_view get_type_name() noexcept { } } // namespace snitch::impl +// Public utilities: small_function. +// --------------------------------- + +namespace snitch::impl { +template +struct function_traits { + static_assert(!std::is_same_v, "incorrect template parameter for small_function"); +}; + +template +struct function_traits_base { + static_assert(!std::is_same_v, "incorrect template parameter for small_function"); +}; + +template +struct function_traits { + using return_type = Ret; + using function_ptr = Ret (*)(Args...) noexcept; + using function_data_ptr = Ret (*)(void*, Args...) noexcept; + using function_const_data_ptr = Ret (*)(const void*, Args...) noexcept; + + static constexpr bool is_noexcept = true; + + template + static constexpr function_data_ptr to_free_function() noexcept { + return [](void* ptr, Args... args) noexcept { + if constexpr (std::is_same_v) { + (static_cast(ptr)->*constant::value)( + std::move(args)...); + } else { + return (static_cast(ptr)->*constant::value)( + std::move(args)...); + } + }; + } + + template + static constexpr function_const_data_ptr to_const_free_function() noexcept { + return [](const void* ptr, Args... args) noexcept { + if constexpr (std::is_same_v) { + (static_cast(ptr)->*constant::value)( + std::move(args)...); + } else { + return (static_cast(ptr)->*constant::value)( + std::move(args)...); + } + }; + } +}; + +template +struct function_traits { + using return_type = Ret; + using function_ptr = Ret (*)(Args...); + using function_data_ptr = Ret (*)(void*, Args...); + using function_const_data_ptr = Ret (*)(const void*, Args...); + + static constexpr bool is_noexcept = false; + + template + static constexpr function_data_ptr to_free_function() noexcept { + return [](void* ptr, Args... args) { + if constexpr (std::is_same_v) { + (static_cast(ptr)->*constant::value)( + std::move(args)...); + } else { + return (static_cast(ptr)->*constant::value)( + std::move(args)...); + } + }; + } + + template + static constexpr function_const_data_ptr to_const_free_function() noexcept { + return [](const void* ptr, Args... args) { + if constexpr (std::is_same_v) { + (static_cast(ptr)->*constant::value)( + std::move(args)...); + } else { + return (static_cast(ptr)->*constant::value)( + std::move(args)...); + } + }; + } +}; +} // namespace snitch::impl + +namespace snitch { +template +class small_function { + using traits = impl::function_traits; + +public: + using return_type = typename traits::return_type; + using function_ptr = typename traits::function_ptr; + using function_data_ptr = typename traits::function_data_ptr; + using function_const_data_ptr = typename traits::function_const_data_ptr; + +private: + struct function_and_data_ptr { + void* data = nullptr; + function_data_ptr ptr; + }; + + struct function_and_const_data_ptr { + const void* data = nullptr; + function_const_data_ptr ptr; + }; + + using data_type = + std::variant; + + data_type data; + +public: + constexpr small_function(function_ptr ptr) noexcept : data{ptr} {} + + template FunctionType> + constexpr small_function(FunctionType&& obj) noexcept : data{static_cast(obj)} {} + + template + constexpr small_function(ObjectType& obj, constant) noexcept : + data{function_and_data_ptr{ + &obj, traits::template to_free_function()}} {} + + template + constexpr small_function(const ObjectType& obj, constant) noexcept : + data{function_and_const_data_ptr{ + &obj, traits::template to_const_free_function()}} {} + + template + constexpr small_function(FunctorType& obj) noexcept : + small_function(obj, constant<&FunctorType::operator()>{}) {} + + template + constexpr small_function(const FunctorType& obj) noexcept : + small_function(obj, constant<&FunctorType::operator()>{}) {} + + // Prevent inadvertently using temporary stateful lambda; not supported at the moment. + template + constexpr small_function(FunctorType&& obj) noexcept = delete; + + // Prevent inadvertently using temporary object; not supported at the moment. + template + constexpr small_function(FunctorType&& obj, constant) noexcept = delete; + + template + constexpr return_type operator()(CArgs&&... args) const noexcept(traits::is_noexcept) { + if constexpr (std::is_same_v) { + std::visit( + overload{ + [&](function_ptr f) { (*f)(std::forward(args)...); }, + [&](const function_and_data_ptr& f) { + (*f.ptr)(f.data, std::forward(args)...); + }, + [&](const function_and_const_data_ptr& f) { + (*f.ptr)(f.data, std::forward(args)...); + }}, + data); + } else { + return std::visit( + overload{ + [&](function_ptr f) { return (*f)(std::forward(args)...); }, + [&](const function_and_data_ptr& f) { + return (*f.ptr)(f.data, std::forward(args)...); + }, + [&](const function_and_const_data_ptr& f) { + return (*f.ptr)(f.data, std::forward(args)...); + }}, + data); + } + } +}; +} // namespace snitch + // Public utilities. // ------------------------------------------------ @@ -112,6 +314,10 @@ template struct type_list {}; [[noreturn]] void terminate_with(std::string_view msg) noexcept; + +extern small_function assertion_failed_handler; + +[[noreturn]] void assertion_failed(std::string_view msg); } // namespace snitch // Public utilities: small_vector. @@ -143,24 +349,29 @@ class small_vector_span { constexpr void clear() noexcept { *data_size = 0; } - constexpr void resize(std::size_t size) noexcept { - if (!std::is_constant_evaluated() && size > buffer_size) { - terminate_with("small vector is full"); + + // Requires: new_size <= capacity(). + constexpr void resize(std::size_t new_size) { + if (new_size > buffer_size) { + assertion_failed("small vector is full"); } - *data_size = size; + *data_size = new_size; } - constexpr void grow(std::size_t elem) noexcept { - if (!std::is_constant_evaluated() && *data_size + elem > buffer_size) { - terminate_with("small vector is full"); + + // Requires: size() + elem <= capacity(). + constexpr void grow(std::size_t elem) { + if (*data_size + elem > buffer_size) { + assertion_failed("small vector is full"); } *data_size += elem; } - constexpr ElemType& - push_back(const ElemType& t) noexcept(std::is_nothrow_copy_assignable_v) { - if (!std::is_constant_evaluated() && *data_size == buffer_size) { - terminate_with("small vector is full"); + + // Requires: size() < capacity(). + constexpr ElemType& push_back(const ElemType& t) { + if (*data_size == buffer_size) { + assertion_failed("small vector is full"); } ++*data_size; @@ -170,10 +381,11 @@ class small_vector_span { return elem; } - constexpr ElemType& - push_back(ElemType&& t) noexcept(std::is_nothrow_move_assignable_v) { - if (!std::is_constant_evaluated() && *data_size == buffer_size) { - terminate_with("small vector is full"); + + // Requires: size() < capacity(). + constexpr ElemType& push_back(ElemType&& t) { + if (*data_size == buffer_size) { + assertion_failed("small vector is full"); } ++*data_size; @@ -182,27 +394,34 @@ class small_vector_span { return elem; } - constexpr void pop_back() noexcept { - if (!std::is_constant_evaluated() && *data_size == 0) { - terminate_with("pop_back() called on empty vector"); + + // Requires: !empty(). + constexpr void pop_back() { + if (*data_size == 0) { + assertion_failed("pop_back() called on empty vector"); } --*data_size; } - constexpr ElemType& back() noexcept { - if (!std::is_constant_evaluated() && *data_size == 0) { - terminate_with("back() called on empty vector"); + + // Requires: !empty(). + constexpr ElemType& back() { + if (*data_size == 0) { + assertion_failed("back() called on empty vector"); } return buffer_ptr[*data_size - 1]; } - constexpr const ElemType& back() const noexcept { - if (!std::is_constant_evaluated() && *data_size == 0) { - terminate_with("back() called on empty vector"); + + // Requires: !empty(). + constexpr const ElemType& back() const { + if (*data_size == 0) { + assertion_failed("back() called on empty vector"); } return buffer_ptr[*data_size - 1]; } + constexpr ElemType* data() noexcept { return buffer_ptr; } @@ -227,15 +446,19 @@ class small_vector_span { constexpr const ElemType* cend() const noexcept { return begin() + size(); } - constexpr ElemType& operator[](std::size_t i) noexcept { - if (!std::is_constant_evaluated() && i >= size()) { - terminate_with("operator[] called with incorrect index"); + + // Requires: i < size(). + constexpr ElemType& operator[](std::size_t i) { + if (i >= size()) { + assertion_failed("operator[] called with incorrect index"); } return buffer_ptr[i]; } - constexpr const ElemType& operator[](std::size_t i) const noexcept { - if (!std::is_constant_evaluated() && i >= size()) { - terminate_with("operator[] called with incorrect index"); + + // Requires: i < size(). + constexpr const ElemType& operator[](std::size_t i) const { + if (i >= size()) { + assertion_failed("operator[] called with incorrect index"); } return buffer_ptr[i]; } @@ -266,13 +489,16 @@ class small_vector_span { constexpr bool empty() const noexcept { return *data_size == 0; } - constexpr const ElemType& back() const noexcept { - if (!std::is_constant_evaluated() && *data_size == 0) { - terminate_with("back() called on empty vector"); + + // Requires: !empty(). + constexpr const ElemType& back() const { + if (*data_size == 0) { + assertion_failed("back() called on empty vector"); } return buffer_ptr[*data_size - 1]; } + constexpr const ElemType* data() const noexcept { return buffer_ptr; } @@ -288,9 +514,11 @@ class small_vector_span { constexpr const ElemType* cend() const noexcept { return begin() + size(); } - constexpr const ElemType& operator[](std::size_t i) const noexcept { - if (!std::is_constant_evaluated() && i >= size()) { - terminate_with("operator[] called with incorrect index"); + + // Requires: i < size(). + constexpr const ElemType& operator[](std::size_t i) const { + if (i >= size()) { + assertion_failed("operator[] called with incorrect index"); } return buffer_ptr[i]; } @@ -305,15 +533,16 @@ class small_vector { constexpr small_vector() noexcept = default; constexpr small_vector(const small_vector& other) noexcept = default; constexpr small_vector(small_vector&& other) noexcept = default; - constexpr small_vector(std::initializer_list list) noexcept( - noexcept(span().push_back(std::declval()))) { + constexpr small_vector(std::initializer_list list) { for (const auto& e : list) { span().push_back(e); } } + constexpr small_vector& operator=(const small_vector& other) noexcept = default; constexpr small_vector& operator=(small_vector&& other) noexcept = default; - constexpr std::size_t capacity() const noexcept { + + constexpr std::size_t capacity() const noexcept { return MaxLength; } constexpr std::size_t available() const noexcept { @@ -328,29 +557,42 @@ class small_vector { constexpr void clear() noexcept { span().clear(); } - constexpr void resize(std::size_t size) noexcept { + + // Requires: new_size <= capacity(). + constexpr void resize(std::size_t size) { span().resize(size); } - constexpr void grow(std::size_t elem) noexcept { + + // Requires: size() + elem <= capacity(). + constexpr void grow(std::size_t elem) { span().grow(elem); } - constexpr ElemType& - push_back(const ElemType& t) noexcept(std::is_nothrow_copy_assignable_v) { + + // Requires: size() < capacity(). + constexpr ElemType& push_back(const ElemType& t) { return this->span().push_back(t); } - constexpr ElemType& - push_back(ElemType&& t) noexcept(std::is_nothrow_move_assignable_v) { + + // Requires: size() < capacity(). + constexpr ElemType& push_back(ElemType&& t) { return this->span().push_back(t); } - constexpr void pop_back() noexcept { + + // Requires: !empty(). + constexpr void pop_back() { return span().pop_back(); } - constexpr ElemType& back() noexcept { + + // Requires: !empty(). + constexpr ElemType& back() { return span().back(); } - constexpr const ElemType& back() const noexcept { - return const_cast(this)->span().back(); + + // Requires: !empty(). + constexpr const ElemType& back() const { + return span().back(); } + constexpr ElemType* data() noexcept { return data_buffer.data(); } @@ -375,23 +617,31 @@ class small_vector { constexpr const ElemType* cend() const noexcept { return begin() + size(); } + constexpr small_vector_span span() noexcept { return small_vector_span(data_buffer.data(), MaxLength, &data_size); } + constexpr small_vector_span span() const noexcept { return small_vector_span(data_buffer.data(), MaxLength, &data_size); } + constexpr operator small_vector_span() noexcept { return span(); } + constexpr operator small_vector_span() const noexcept { return span(); } - constexpr ElemType& operator[](std::size_t i) noexcept { + + // Requires: i < size(). + constexpr ElemType& operator[](std::size_t i) { return span()[i]; } - constexpr const ElemType& operator[](std::size_t i) const noexcept { - return const_cast(this)->span()[i]; + + // Requires: i < size(). + constexpr const ElemType& operator[](std::size_t i) const { + return span()[i]; } }; } // namespace snitch @@ -412,17 +662,22 @@ class small_string { constexpr small_string() noexcept = default; constexpr small_string(const small_string& other) noexcept = default; constexpr small_string(small_string&& other) noexcept = default; - constexpr small_string(std::string_view str) noexcept { + + // Requires: str.size() <= MaxLength. + constexpr small_string(std::string_view str) { resize(str.size()); for (std::size_t i = 0; i < str.size(); ++i) { data_buffer[i] = str[i]; } } - constexpr small_string& operator=(const small_string& other) noexcept = default; - constexpr small_string& operator=(small_string&& other) noexcept = default; + + constexpr small_string& operator=(const small_string& other) noexcept = default; + constexpr small_string& operator=(small_string&& other) noexcept = default; + constexpr std::string_view str() const noexcept { return std::string_view(data(), length()); } + constexpr std::size_t capacity() const noexcept { return MaxLength; } @@ -441,24 +696,37 @@ class small_string { constexpr void clear() noexcept { span().clear(); } - constexpr void resize(std::size_t length) noexcept { + + // Requires: new_size <= capacity(). + constexpr void resize(std::size_t length) { span().resize(length); } - constexpr void grow(std::size_t chars) noexcept { + + // Requires: size() + elem <= capacity(). + constexpr void grow(std::size_t chars) { span().grow(chars); } - constexpr char& push_back(char t) noexcept { + + // Requires: size() < capacity(). + constexpr char& push_back(char t) { return span().push_back(t); } - constexpr void pop_back() noexcept { + + // Requires: !empty(). + constexpr void pop_back() { return span().pop_back(); } - constexpr char& back() noexcept { + + // Requires: !empty(). + constexpr char& back() { return span().back(); } - constexpr const char& back() const noexcept { + + // Requires: !empty(). + constexpr const char& back() const { return span().back(); } + constexpr char* data() noexcept { return data_buffer.data(); } @@ -483,27 +751,36 @@ class small_string { constexpr const char* cend() const noexcept { return begin() + length(); } + constexpr small_string_span span() noexcept { return small_string_span(data_buffer.data(), MaxLength, &data_size); } + constexpr small_string_view span() const noexcept { return small_string_view(data_buffer.data(), MaxLength, &data_size); } + constexpr operator small_string_span() noexcept { return span(); } + constexpr operator small_string_view() const noexcept { return span(); } - constexpr char& operator[](std::size_t i) noexcept { + + constexpr operator std::string_view() const noexcept { + return std::string_view(data(), length()); + } + + // Requires: i < size(). + constexpr char& operator[](std::size_t i) { return span()[i]; } - constexpr char operator[](std::size_t i) const noexcept { + + // Requires: i < size(). + constexpr char operator[](std::size_t i) const { return const_cast(this)->span()[i]; } - constexpr operator std::string_view() const noexcept { - return std::string_view(data(), length()); - } }; } // namespace snitch @@ -749,7 +1026,7 @@ struct float_bits { }; template -[[nodiscard]] constexpr float_bits to_bits(T f) { +[[nodiscard]] constexpr float_bits to_bits(T f) noexcept { using traits = float_traits; using bits_full_t = typename traits::bits_full_t; using bits_sig_t = typename traits::bits_sig_t; @@ -895,21 +1172,6 @@ template // ------------------------- namespace snitch { -template -concept signed_integral = std::is_signed_v; - -template -concept unsigned_integral = std::is_unsigned_v; - -template -concept floating_point = std::is_floating_point_v; - -template -concept convertible_to = std::is_convertible_v; - -template -concept enumeration = std::is_enum_v; - // These types are used to define the largest printable integer types. // In C++, integer literals must fit on uintmax_t/intmax_t, so these are good candidates. // They aren't perfect though. On most 64 bit platforms they are defined as 64 bit integers, @@ -1324,133 +1586,6 @@ concept matcher_for = requires(const T& m, const U& value) { }; } // namespace snitch -// Public utilities: small_function. -// --------------------------------- - -namespace snitch { -template -struct overload : Args... { - using Args::operator()...; -}; - -template -overload(Args...) -> overload; - -template -struct constant { - static constexpr auto value = T; -}; - -template -class small_function { - static_assert(!std::is_same_v, "incorrect template parameter for small_function"); -}; - -template -class small_function { - using function_ptr = Ret (*)(Args...) noexcept; - using function_data_ptr = Ret (*)(void*, Args...) noexcept; - using function_const_data_ptr = Ret (*)(const void*, Args...) noexcept; - - struct function_and_data_ptr { - void* data = nullptr; - function_data_ptr ptr; - }; - - struct function_and_const_data_ptr { - const void* data = nullptr; - function_const_data_ptr ptr; - }; - - using data_type = std:: - variant; - - data_type data; - -public: - constexpr small_function() = default; - - constexpr small_function(function_ptr ptr) noexcept : data{ptr} {} - - template T> - constexpr small_function(T&& obj) noexcept : data{static_cast(obj)} {} - - template - constexpr small_function(T& obj, constant) noexcept : - data{function_and_data_ptr{ - &obj, [](void* ptr, Args... args) noexcept { - if constexpr (std::is_same_v) { - (static_cast(ptr)->*constant::value)(std::move(args)...); - } else { - return (static_cast(ptr)->*constant::value)(std::move(args)...); - } - }}} {} - - template - constexpr small_function(const T& obj, constant) noexcept : - data{function_and_const_data_ptr{ - &obj, [](const void* ptr, Args... args) noexcept { - if constexpr (std::is_same_v) { - (static_cast(ptr)->*constant::value)(std::move(args)...); - } else { - return (static_cast(ptr)->*constant::value)(std::move(args)...); - } - }}} {} - - template - constexpr small_function(T& obj) noexcept : small_function(obj, constant<&T::operator()>{}) {} - - template - constexpr small_function(const T& obj) noexcept : - small_function(obj, constant<&T::operator()>{}) {} - - // Prevent inadvertently using temporary stateful lambda; not supported at the moment. - template - constexpr small_function(T&& obj) noexcept = delete; - - // Prevent inadvertently using temporary object; not supported at the moment. - template - constexpr small_function(T&& obj, constant) noexcept = delete; - - template - constexpr Ret operator()(CArgs&&... args) const noexcept { - if constexpr (std::is_same_v) { - std::visit( - overload{ - [](std::monostate) { - terminate_with("small_function called without an implementation"); - }, - [&](function_ptr f) { (*f)(std::forward(args)...); }, - [&](const function_and_data_ptr& f) { - (*f.ptr)(f.data, std::forward(args)...); - }, - [&](const function_and_const_data_ptr& f) { - (*f.ptr)(f.data, std::forward(args)...); - }}, - data); - } else { - return std::visit( - overload{ - [](std::monostate) -> Ret { - terminate_with("small_function called without an implementation"); - }, - [&](function_ptr f) { return (*f)(std::forward(args)...); }, - [&](const function_and_data_ptr& f) { - return (*f.ptr)(f.data, std::forward(args)...); - }, - [&](const function_and_const_data_ptr& f) { - return (*f.ptr)(f.data, std::forward(args)...); - }}, - data); - } - } - - constexpr bool empty() const noexcept { - return std::holds_alternative(data); - } -}; -} // namespace snitch - // Implementation details. // ----------------------- @@ -1501,17 +1636,20 @@ struct test_state { }; test_state& get_current_test() noexcept; + test_state* try_get_current_test() noexcept; -void set_current_test(test_state* current) noexcept; + +void set_current_test(test_state* current) noexcept; struct section_entry_checker { section_id section = {}; test_state& state; bool entered = false; - ~section_entry_checker() noexcept; + ~section_entry_checker(); - explicit operator bool() noexcept; + // Requires: number of sections < max_nested_sections. + explicit operator bool(); }; #define DEFINE_OPERATOR(OP, NAME, DISP, DISP_INV) \ @@ -1520,8 +1658,8 @@ struct section_entry_checker { static constexpr std::string_view inverse = DISP_INV; \ \ template \ - constexpr bool operator()(const T& lhs, const U& rhs) const noexcept \ - requires requires(const T& lhs, const U& rhs) { lhs OP rhs; } \ + constexpr bool operator()(const T& lhs, const U& rhs) const noexcept(noexcept(lhs OP rhs)) \ + requires(requires(const T& lhs, const U& rhs) { lhs OP rhs; }) \ { \ return lhs OP rhs; \ } \ @@ -1644,7 +1782,8 @@ struct extracted_binary_expression { #undef EXPR_OPERATOR_INVALID - constexpr expression to_expression() const noexcept + // NB: Cannot make this noexcept since user operators may throw. + constexpr expression to_expression() const noexcept(noexcept(static_cast(O{}(lhs, rhs)))) requires(requires(const T& lhs, const U& rhs) { O{}(lhs, rhs); }) { expression expr{expected}; @@ -1745,7 +1884,7 @@ struct extracted_unary_expression { #undef EXPR_OPERATOR_INVALID - constexpr expression to_expression() const noexcept + constexpr expression to_expression() const noexcept(noexcept(static_cast(lhs))) requires(requires(const T& lhs) { static_cast(lhs); }) { expression expr{expected}; @@ -1790,30 +1929,33 @@ struct scoped_capture { capture_state& captures; std::size_t count = 0; - ~scoped_capture() noexcept { + ~scoped_capture() { captures.resize(captures.size() - count); } }; std::string_view extract_next_name(std::string_view& names) noexcept; -small_string& add_capture(test_state& state) noexcept; +// Requires: number of captures < max_captures. +small_string& add_capture(test_state& state); +// Requires: number of captures < max_captures. template -void add_capture(test_state& state, std::string_view& names, const T& arg) noexcept { +void add_capture(test_state& state, std::string_view& names, const T& arg) { auto& capture = add_capture(state); append_or_truncate(capture, extract_next_name(names), " := ", arg); } +// Requires: number of captures < max_captures. template -scoped_capture -add_captures(test_state& state, std::string_view names, const Args&... args) noexcept { +scoped_capture add_captures(test_state& state, std::string_view names, const Args&... args) { (add_capture(state, names, args), ...); return {state.captures, sizeof...(args)}; } +// Requires: number of captures < max_captures. template -scoped_capture add_info(test_state& state, const Args&... args) noexcept { +scoped_capture add_info(test_state& state, const Args&... args) { auto& capture = add_capture(state); append_or_truncate(capture, args...); return {state.captures, 1}; @@ -1939,22 +2081,14 @@ void for_each_positional_argument( // Test registry. // -------------- +namespace snitch::impl { +void default_reporter(const registry& r, const event::data& event) noexcept; +} + namespace snitch { class registry { small_vector test_list; - void print_location( - const impl::test_case& current_case, - const impl::section_state& sections, - const impl::capture_state& captures, - const assertion_location& location) const noexcept; - - void print_failure() const noexcept; - void print_expected_failure() const noexcept; - void print_skip() const noexcept; - void print_details(std::string_view message) const noexcept; - void print_details_expr(const impl::expression& exp) const noexcept; - public: enum class verbosity { quiet, normal, high } verbose = verbosity::normal; bool with_color = true; @@ -1962,29 +2096,33 @@ class registry { using print_function = small_function; using report_function = small_function; - print_function print_callback = &snitch::impl::stdout_print; - report_function report_callback; + print_function print_callback = &snitch::impl::stdout_print; + report_function report_callback = &snitch::impl::default_reporter; template void print(Args&&... args) const noexcept { small_string message; - append_or_truncate(message, std::forward(args)...); + const bool could_fit = append(message, std::forward(args)...); this->print_callback(message); + if (!could_fit) { + this->print_callback("..."); + } } - const char* add(const test_id& id, impl::test_ptr func) noexcept; + // Requires: number of tests + 1 <= max_test_cases, well-formed test ID. + const char* add(const test_id& id, impl::test_ptr func); + // Requires: number of tests + added tests <= max_test_cases, well-formed test ID. template - const char* - add_with_types(std::string_view name, std::string_view tags, const F& func) noexcept { + const char* add_with_types(std::string_view name, std::string_view tags, const F& func) { return ( add({name, tags, impl::get_type_name()}, impl::to_test_case_ptr(func)), ...); } + // Requires: number of tests + added tests <= max_test_cases, well-formed test ID. template - const char* - add_with_type_list(std::string_view name, std::string_view tags, const F& func) noexcept { + const char* add_with_type_list(std::string_view name, std::string_view tags, const F& func) { return [&] typename TL, typename... Args>(type_list>) { return this->add_with_types(name, tags, func); }(type_list{}); @@ -2024,7 +2162,10 @@ class registry { void configure(const cli::input& args) noexcept; void list_all_tests() const noexcept; - void list_all_tags() const noexcept; + + // Requires: number unique tags <= max_unique_tags. + void list_all_tags() const; + void list_tests_with_tag(std::string_view tag) const noexcept; impl::test_case* begin() noexcept; @@ -2041,7 +2182,7 @@ extern constinit registry tests; namespace snitch::impl { template -[[nodiscard]] constexpr auto constexpr_match(T&& value, M&& matcher) { +[[nodiscard]] constexpr auto constexpr_match(T&& value, M&& matcher) noexcept { using result_type = decltype(matcher.describe_match(value, matchers::match_status::failed)); if (!matcher.match(value)) { return std::optional( diff --git a/src/snitch.cpp b/src/snitch.cpp index aebb3be6..310bbab1 100644 --- a/src/snitch.cpp +++ b/src/snitch.cpp @@ -2,7 +2,7 @@ #include // for std::sort #include // for format strings -#include // for std::printf, std::snprintf +#include // for std::fwrite, std::snprintf #include // for std::memcpy #include // for std::optional @@ -15,7 +15,7 @@ namespace { using namespace std::literals; -using color_t = const char*; +using color_t = std::string_view; namespace color { constexpr color_t error [[maybe_unused]] = "\x1b[1;31m"; @@ -37,7 +37,7 @@ struct colored { }; template -colored make_colored(const T& t, bool with_color, color_t start) { +colored make_colored(const T& t, bool with_color, color_t start) noexcept { return {t, with_color ? start : "", with_color ? color::reset : ""}; } @@ -279,8 +279,7 @@ bool is_match(std::string_view string, std::string_view regex) noexcept { namespace snitch::impl { void stdout_print(std::string_view message) noexcept { - // TODO: replace this with std::print? - std::printf("%.*s", static_cast(message.length()), message.data()); + std::fwrite(message.data(), sizeof(char), message.length(), stdout); } test_state& get_current_test() noexcept { @@ -312,7 +311,17 @@ using snitch::small_string; template bool append(small_string_span ss, const colored& colored_value) noexcept { - return append(ss, colored_value.color_start, colored_value.value, colored_value.color_end); + if (ss.available() <= colored_value.color_start.size() + colored_value.color_end.size()) { + return false; + } + + bool could_fit = true; + if (!append(ss, colored_value.color_start, colored_value.value)) { + ss.resize(ss.capacity() - colored_value.color_end.size()); + could_fit = false; + } + + return append(ss, colored_value.color_end) && could_fit; } template @@ -322,21 +331,24 @@ void console_print(Args&&... args) noexcept { snitch::cli::console_print(message); } -bool is_at_least(snitch::registry::verbosity verbose, snitch::registry::verbosity required) { +bool is_at_least( + snitch::registry::verbosity verbose, snitch::registry::verbosity required) noexcept { using underlying_type = std::underlying_type_t; return static_cast(verbose) >= static_cast(required); } void trim(std::string_view& str, std::string_view patterns) noexcept { std::size_t start = str.find_first_not_of(patterns); - if (start == str.npos) + if (start == str.npos) { return; + } str.remove_prefix(start); std::size_t end = str.find_last_not_of(patterns); - if (end != str.npos) + if (end != str.npos) { str.remove_suffix(str.size() - end - 1); + } } } // namespace @@ -348,19 +360,46 @@ namespace snitch { std::terminate(); } + +[[noreturn]] void assertion_failed(std::string_view msg) { + assertion_failed_handler(msg); + + // The assertion handler should either spin, throw, or terminate, but never return. + // We cannot enforce [[noreturn]] through the small_function wrapper. So just in case + // it accidentally returns, we terminate. + std::terminate(); +} + +small_function assertion_failed_handler = &terminate_with; } // namespace snitch // Sections implementation. // ------------------------ namespace snitch::impl { -section_entry_checker::~section_entry_checker() noexcept { +section_entry_checker::~section_entry_checker() { if (entered) { - if (state.sections.levels.size() == state.sections.depth) { + if (state.sections.depth == state.sections.levels.size()) { + // We just entered this section, and there was no child section in it. + // This is a leaf; flag that a leaf has been executed so that no other leaf + // is executed in this run. + // Note: don't pop this level from the section state yet, it may have siblings + // that we don't know about yet. Popping will be done when we exit from the parent, + // since then we will know if there is any sibling. state.sections.leaf_executed = true; } else { - auto& child = state.sections.levels[state.sections.depth]; - if (child.previous_section_id == child.max_section_id) { + // Check if there is any child section left to execute, at any depth below this one. + bool no_child_section_left = true; + for (std::size_t c = state.sections.depth; c < state.sections.levels.size(); ++c) { + auto& child = state.sections.levels[c]; + if (child.previous_section_id != child.max_section_id) { + no_child_section_left = false; + break; + } + } + + if (no_child_section_left) { + // No more children, we can pop this level and never go back. state.sections.levels.pop_back(); } } @@ -371,33 +410,41 @@ section_entry_checker::~section_entry_checker() noexcept { --state.sections.depth; } -section_entry_checker::operator bool() noexcept { - ++state.sections.depth; - - if (state.sections.depth > state.sections.levels.size()) { - if (state.sections.depth > max_nested_sections) { +section_entry_checker::operator bool() { + if (state.sections.depth >= state.sections.levels.size()) { + if (state.sections.depth >= max_nested_sections) { state.reg.print( make_colored("error:", state.reg.with_color, color::fail), " max number of nested sections reached; " "please increase 'SNITCH_MAX_NESTED_SECTIONS' (currently ", max_nested_sections, ")\n."); - std::terminate(); + assertion_failed("max number of nested sections reached"); } state.sections.levels.push_back({}); } + ++state.sections.depth; + auto& level = state.sections.levels[state.sections.depth - 1]; ++level.current_section_id; - if (level.max_section_id < level.current_section_id) { + if (level.current_section_id > level.max_section_id) { level.max_section_id = level.current_section_id; } - if (!state.sections.leaf_executed && - (level.previous_section_id + 1 == level.current_section_id || - (level.previous_section_id == level.current_section_id && - state.sections.levels.size() > state.sections.depth))) { + if (state.sections.leaf_executed) { + // We have already executed another leaf section; can't execute more + // on this run, so don't bother going inside this one now. + return false; + } + + // Only enter this section if: + // - The section entered in the previous run was its immediate previous sibling, or + // - This section was already entered in the previous run, and child sections exist in it. + if (level.current_section_id == level.previous_section_id + 1 || + (level.current_section_id == level.previous_section_id && + state.sections.depth < state.sections.levels.size())) { level.previous_section_id = level.current_section_id; state.sections.current_section.push_back(section); @@ -461,14 +508,14 @@ std::string_view extract_next_name(std::string_view& names) noexcept { return result; } -small_string& add_capture(test_state& state) noexcept { +small_string& add_capture(test_state& state) { if (state.captures.available() == 0) { state.reg.print( make_colored("error:", state.reg.with_color, color::fail), " max number of captures reached; " "please increase 'SNITCH_MAX_CAPTURES' (currently ", max_captures, ")\n."); - std::terminate(); + assertion_failed("max number of captures reached"); } state.captures.grow(1); @@ -508,15 +555,16 @@ namespace { using namespace snitch; using namespace snitch::impl; +// Requires: s contains a well-formed list of tags. template -void for_each_raw_tag(std::string_view s, F&& callback) noexcept { +void for_each_raw_tag(std::string_view s, F&& callback) { if (s.empty()) { return; } if (s.find_first_of("[") == std::string_view::npos || s.find_first_of("]") == std::string_view::npos) { - terminate_with("incorrectly formatted tag; please use \"[tag1][tag2][...]\""); + assertion_failed("incorrectly formatted tag; please use \"[tag1][tag2][...]\""); } std::string_view delim = "]["; @@ -543,8 +591,9 @@ struct should_fail {}; using parsed_tag = std::variant; } // namespace tags +// Requires: s contains a well-formed list of tags, each of length <= max_tag_length. template -void for_each_tag(std::string_view s, F&& callback) noexcept { +void for_each_tag(std::string_view s, F&& callback) { small_string buffer; for_each_raw_tag(s, [&](std::string_view t) { @@ -561,7 +610,7 @@ void for_each_tag(std::string_view s, F&& callback) noexcept { buffer.clear(); if (!append(buffer, "[", t.substr(2u))) { - terminate_with("tag is too long"); + assertion_failed("tag is too long"); } t = buffer; @@ -623,7 +672,8 @@ snitch::test_case_state convert_to_public_state(impl::test_case_state s) noexcep } } -small_vector make_capture_buffer(const capture_state& captures) { +small_vector +make_capture_buffer(const capture_state& captures) noexcept { small_vector captures_buffer; for (const auto& c : captures) { captures_buffer.push_back(c); @@ -672,15 +722,135 @@ filter_result is_filter_match_id(const test_id& id, std::string_view filter) noe return is_filter_match_name(id.name, filter); } } +} // namespace snitch + +namespace { +void print_location( + const registry& r, + const test_id& id, + const section_info& sections, + const capture_info& captures, + const assertion_location& location) noexcept { + + r.print("running test case \"", make_colored(id.name, r.with_color, color::highlight1), "\"\n"); + + for (auto& section : sections) { + r.print( + " in section \"", make_colored(section.name, r.with_color, color::highlight1), + "\"\n"); + } + + r.print(" at ", location.file, ":", location.line, "\n"); -const char* registry::add(const test_id& id, test_ptr func) noexcept { - if (test_list.size() == test_list.capacity()) { + if (!id.type.empty()) { + r.print( + " for type ", make_colored(id.type, r.with_color, color::highlight1), "\n"); + } + + for (auto& capture : captures) { + r.print(" with ", make_colored(capture, r.with_color, color::highlight1), "\n"); + } +} +} // namespace + +namespace snitch::impl { +void default_reporter(const registry& r, const event::data& event) noexcept { + std::visit( + snitch::overload{ + [&](const snitch::event::test_run_started& e) { + if (is_at_least(r.verbose, registry::verbosity::normal)) { + r.print( + make_colored("starting ", r.with_color, color::highlight2), + make_colored(e.name, r.with_color, color::highlight1), + make_colored(" with ", r.with_color, color::highlight2), + make_colored( + "snitch v" SNITCH_FULL_VERSION "\n", r.with_color, color::highlight1)); + r.print("==========================================\n"); + } + }, + [&](const snitch::event::test_run_ended& e) { + if (is_at_least(r.verbose, registry::verbosity::normal)) { + r.print("==========================================\n"); + + if (e.success) { + r.print( + make_colored("success:", r.with_color, color::pass), + " all tests passed (", e.run_count, " test cases, ", e.assertion_count, + " assertions"); + } else { + r.print( + make_colored("error:", r.with_color, color::fail), + " some tests failed (", e.fail_count, " out of ", e.run_count, + " test cases, ", e.assertion_count, " assertions"); + } + + if (e.skip_count > 0) { + r.print(", ", e.skip_count, " test cases skipped"); + } + +#if SNITCH_WITH_TIMINGS + r.print(", ", e.duration, " seconds"); +#endif + + r.print(")\n"); + } + }, + [&](const snitch::event::test_case_started& e) { + if (is_at_least(r.verbose, registry::verbosity::high)) { + small_string full_name; + make_full_name(full_name, e.id); + + r.print( + make_colored("starting:", r.with_color, color::status), " ", + make_colored(full_name, r.with_color, color::highlight1), "\n"); + } + }, + [&](const snitch::event::test_case_ended& e) { + if (is_at_least(r.verbose, registry::verbosity::high)) { + small_string full_name; + make_full_name(full_name, e.id); + +#if SNITCH_WITH_TIMINGS + r.print( + make_colored("finished:", r.with_color, color::status), " ", + make_colored(full_name, r.with_color, color::highlight1), " (", e.duration, + "s)\n"); +#else + r.print( + make_colored("finished:", r.with_color, color::status), " ", + make_colored(full_name, r.with_color, color::highlight1), "\n"); +#endif + } + }, + [&](const snitch::event::test_case_skipped& e) { + r.print(make_colored("skipped: ", r.with_color, color::skipped)); + print_location(r, e.id, e.sections, e.captures, e.location); + r.print(" ", make_colored(e.message, r.with_color, color::highlight2)); + r.print("\n"); + }, + [&](const snitch::event::assertion_failed& e) { + if (e.expected) { + r.print(make_colored("expected failure: ", r.with_color, color::pass)); + } else { + r.print(make_colored("failed: ", r.with_color, color::fail)); + } + print_location(r, e.id, e.sections, e.captures, e.location); + r.print(" ", make_colored(e.message, r.with_color, color::highlight2)); + r.print("\n"); + }}, + event); +} +} // namespace snitch::impl + +namespace snitch { +const char* registry::add(const test_id& id, test_ptr func) { + if (test_list.available() == 0u) { print( make_colored("error:", with_color, color::fail), " max number of test cases reached; " "please increase 'SNITCH_MAX_TEST_CASES' (currently ", max_test_cases, ")\n."); - std::terminate(); + assertion_failed("max number of test cases reached"); } test_list.push_back(test_case{id, func}); @@ -692,67 +862,12 @@ const char* registry::add(const test_id& id, test_ptr func) noexcept { " max length of test name reached; " "please increase 'SNITCH_MAX_TEST_NAME_LENGTH' (currently ", max_test_name_length, ")\n."); - std::terminate(); + assertion_failed("test case name exceeds max length"); } return id.name.data(); } -void registry::print_location( - const impl::test_case& current_case, - const impl::section_state& sections, - const impl::capture_state& captures, - const assertion_location& location) const noexcept { - - print( - "running test case \"", make_colored(current_case.id.name, with_color, color::highlight1), - "\"\n"); - - for (auto& section : sections.current_section) { - print( - " in section \"", make_colored(section.name, with_color, color::highlight1), - "\"\n"); - } - - print(" at ", location.file, ":", location.line, "\n"); - - if (!current_case.id.type.empty()) { - print( - " for type ", - make_colored(current_case.id.type, with_color, color::highlight1), "\n"); - } - - for (auto& capture : captures) { - print(" with ", make_colored(capture, with_color, color::highlight1), "\n"); - } -} - -void registry::print_failure() const noexcept { - print(make_colored("failed: ", with_color, color::fail)); -} - -void registry::print_expected_failure() const noexcept { - print(make_colored("expected failure: ", with_color, color::pass)); -} - -void registry::print_skip() const noexcept { - print(make_colored("skipped: ", with_color, color::skipped)); -} - -void registry::print_details(std::string_view message) const noexcept { - print(" ", make_colored(message, with_color, color::highlight2), "\n"); -} - -void registry::print_details_expr(const expression& exp) const noexcept { - print(" ", make_colored(exp.expected, with_color, color::highlight2)); - - if (!exp.actual.empty()) { - print(", got ", make_colored(exp.actual, with_color, color::highlight2)); - } - - print("\n"); -} - void registry::report_failure( impl::test_state& state, const assertion_location& location, @@ -762,21 +877,12 @@ void registry::report_failure( set_state(state.test, impl::test_case_state::failed); } - if (!report_callback.empty()) { - const auto captures_buffer = make_capture_buffer(state.captures); - report_callback( - *this, event::assertion_failed{ - state.test.id, state.sections.current_section, captures_buffer.span(), - location, message, state.should_fail, state.may_fail}); - } else { - if (state.should_fail) { - print_expected_failure(); - } else { - print_failure(); - } - print_location(state.test, state.sections, state.captures, location); - print_details(message); - } + const auto captures_buffer = make_capture_buffer(state.captures); + + report_callback( + *this, event::assertion_failed{ + state.test.id, state.sections.current_section, captures_buffer.span(), location, + message, state.should_fail, state.may_fail}); } void registry::report_failure( @@ -792,21 +898,12 @@ void registry::report_failure( small_string message; append_or_truncate(message, message1, message2); - if (!report_callback.empty()) { - const auto captures_buffer = make_capture_buffer(state.captures); - report_callback( - *this, event::assertion_failed{ - state.test.id, state.sections.current_section, captures_buffer.span(), - location, message, state.should_fail, state.may_fail}); - } else { - if (state.should_fail) { - print_expected_failure(); - } else { - print_failure(); - } - print_location(state.test, state.sections, state.captures, location); - print_details(message); - } + const auto captures_buffer = make_capture_buffer(state.captures); + + report_callback( + *this, event::assertion_failed{ + state.test.id, state.sections.current_section, captures_buffer.span(), location, + message, state.should_fail, state.may_fail}); } void registry::report_failure( @@ -818,29 +915,20 @@ void registry::report_failure( set_state(state.test, impl::test_case_state::failed); } - if (!report_callback.empty()) { - const auto captures_buffer = make_capture_buffer(state.captures); - if (!exp.actual.empty()) { - small_string message; - append_or_truncate(message, exp.expected, ", got ", exp.actual); - report_callback( - *this, event::assertion_failed{ - state.test.id, state.sections.current_section, captures_buffer.span(), - location, message, state.should_fail, state.may_fail}); - } else { - report_callback( - *this, event::assertion_failed{ - state.test.id, state.sections.current_section, captures_buffer.span(), - location, exp.expected, state.should_fail, state.may_fail}); - } + const auto captures_buffer = make_capture_buffer(state.captures); + + if (!exp.actual.empty()) { + small_string message; + append_or_truncate(message, exp.expected, ", got ", exp.actual); + report_callback( + *this, event::assertion_failed{ + state.test.id, state.sections.current_section, captures_buffer.span(), + location, message, state.should_fail, state.may_fail}); } else { - if (state.should_fail) { - print_expected_failure(); - } else { - print_failure(); - } - print_location(state.test, state.sections, state.captures, location); - print_details_expr(exp); + report_callback( + *this, event::assertion_failed{ + state.test.id, state.sections.current_section, captures_buffer.span(), + location, exp.expected, state.should_fail, state.may_fail}); } } @@ -851,33 +939,20 @@ void registry::report_skipped( set_state(state.test, impl::test_case_state::skipped); - if (!report_callback.empty()) { - const auto captures_buffer = make_capture_buffer(state.captures); - report_callback( - *this, event::test_case_skipped{ - state.test.id, state.sections.current_section, captures_buffer.span(), - location, message}); - } else { - print_skip(); - print_location(state.test, state.sections, state.captures, location); - print_details(message); - } + const auto captures_buffer = make_capture_buffer(state.captures); + + report_callback( + *this, event::test_case_skipped{ + state.test.id, state.sections.current_section, captures_buffer.span(), location, + message}); } test_state registry::run(test_case& test) noexcept { - small_string full_name; - - if (!report_callback.empty()) { - report_callback(*this, event::test_case_started{test.id}); - } else if (is_at_least(verbose, verbosity::high)) { - make_full_name(full_name, test.id); - print( - make_colored("starting:", with_color, color::status), " ", - make_colored(full_name, with_color, color::highlight1), "\n"); - } + report_callback(*this, event::test_case_started{test.id}); test.state = impl::test_case_state::success; + // Fetch special tags for this test case. bool may_fail = false; bool should_fail = false; for_each_tag(test.id.tags, [&](const tags::parsed_tag& v) { @@ -901,12 +976,13 @@ test_state registry::run(test_case& test) noexcept { #endif do { + // Reset section state. + state.sections.leaf_executed = false; for (std::size_t i = 0; i < state.sections.levels.size(); ++i) { state.sections.levels[i].current_section_id = 0; } - state.sections.leaf_executed = false; - + // Run the test case. #if SNITCH_WITH_EXCEPTIONS try { test.func(); @@ -924,8 +1000,10 @@ test_state registry::run(test_case& test) noexcept { #endif if (state.sections.levels.size() == 1) { + // This test case contained sections; check if there are any more left to evaluate. auto& child = state.sections.levels[0]; if (child.previous_section_id == child.max_section_id) { + // No more; clear the section state. state.sections.levels.clear(); state.sections.current_section.clear(); } @@ -947,32 +1025,20 @@ test_state registry::run(test_case& test) noexcept { state.duration = std::chrono::duration(time_end - time_start).count(); #endif - if (!report_callback.empty()) { -#if SNITCH_WITH_TIMINGS - report_callback( - *this, event::test_case_ended{ - .id = test.id, - .state = convert_to_public_state(state.test.state), - .assertion_count = state.asserts, - .duration = state.duration}); -#else - report_callback( - *this, event::test_case_ended{ - .id = test.id, - .state = convert_to_public_state(state.test.state), - .assertion_count = state.asserts}); -#endif - } else if (is_at_least(verbose, verbosity::high)) { #if SNITCH_WITH_TIMINGS - print( - make_colored("finished:", with_color, color::status), " ", - make_colored(full_name, with_color, color::highlight1), " (", state.duration, "s)\n"); + report_callback( + *this, event::test_case_ended{ + .id = test.id, + .state = convert_to_public_state(state.test.state), + .assertion_count = state.asserts, + .duration = state.duration}); #else - print( - make_colored("finished:", with_color, color::status), " ", - make_colored(full_name, with_color, color::highlight1), "\n"); + report_callback( + *this, event::test_case_ended{ + .id = test.id, + .state = convert_to_public_state(state.test.state), + .assertion_count = state.asserts}); #endif - } thread_current_test = previous_run; @@ -983,14 +1049,7 @@ bool registry::run_selected_tests( std::string_view run_name, const small_function& predicate) noexcept { - if (!report_callback.empty()) { - report_callback(*this, event::test_run_started{run_name}); - } else if (is_at_least(verbose, registry::verbosity::normal)) { - print( - make_colored("starting tests with ", with_color, color::highlight2), - make_colored("snitch v" SNITCH_FULL_VERSION "\n", with_color, color::highlight1)); - print("==========================================\n"); - } + report_callback(*this, event::test_run_started{run_name}); bool success = true; std::size_t run_count = 0; @@ -1039,50 +1098,26 @@ bool registry::run_selected_tests( float duration = std::chrono::duration(time_end - time_start).count(); #endif - if (!report_callback.empty()) { #if SNITCH_WITH_TIMINGS - report_callback( - *this, event::test_run_ended{ - .name = run_name, - .success = success, - .run_count = run_count, - .fail_count = fail_count, - .skip_count = skip_count, - .assertion_count = assertion_count, - .duration = duration}); + report_callback( + *this, event::test_run_ended{ + .name = run_name, + .success = success, + .run_count = run_count, + .fail_count = fail_count, + .skip_count = skip_count, + .assertion_count = assertion_count, + .duration = duration}); #else - report_callback( - *this, event::test_run_ended{ - .name = run_name, - .success = success, - .run_count = run_count, - .fail_count = fail_count, - .skip_count = skip_count, - .assertion_count = assertion_count}); + report_callback( + *this, event::test_run_ended{ + .name = run_name, + .success = success, + .run_count = run_count, + .fail_count = fail_count, + .skip_count = skip_count, + .assertion_count = assertion_count}); #endif - } else if (is_at_least(verbose, registry::verbosity::normal)) { - print("==========================================\n"); - - if (success) { - print( - make_colored("success:", with_color, color::pass), " all tests passed (", run_count, - " test cases, ", assertion_count, " assertions"); - } else { - print( - make_colored("error:", with_color, color::fail), " some tests failed (", fail_count, - " out of ", run_count, " test cases, ", assertion_count, " assertions"); - } - - if (skip_count > 0) { - print(", ", skip_count, " test cases skipped"); - } - -#if SNITCH_WITH_TIMINGS - print(", ", duration, " seconds"); -#endif - - print(")\n"); - } return success; } @@ -1102,7 +1137,7 @@ bool registry::run_tests(std::string_view run_name) noexcept { return run_selected_tests(run_name, filter); } -void registry::list_all_tags() const noexcept { +void registry::list_all_tags() const { small_vector tags; for (const auto& t : test_list) { for_each_tag(t.id.tags, [&](const tags::parsed_tag& v) { @@ -1114,7 +1149,7 @@ void registry::list_all_tags() const noexcept { " max number of tags reached; " "please increase 'SNITCH_MAX_UNIQUE_TAGS' (currently ", max_unique_tags, ")\n."); - std::terminate(); + assertion_failed("max number of unique tags reached"); } tags.push_back(*vs); @@ -1189,7 +1224,7 @@ struct parser_settings { bool with_color = true; }; -std::string_view extract_executable(std::string_view path) { +std::string_view extract_executable(std::string_view path) noexcept { if (auto folder_end = path.find_last_of("\\/"); folder_end != path.npos) { path.remove_prefix(folder_end + 1); } @@ -1200,23 +1235,23 @@ std::string_view extract_executable(std::string_view path) { return path; } -bool is_option(const expected_argument& e) { +bool is_option(const expected_argument& e) noexcept { return !e.names.empty(); } -bool is_option(const cli::argument& a) { +bool is_option(const cli::argument& a) noexcept { return !a.name.empty(); } -bool has_value(const expected_argument& e) { +bool has_value(const expected_argument& e) noexcept { return e.value_name.has_value(); } -bool is_mandatory(const expected_argument& e) { +bool is_mandatory(const expected_argument& e) noexcept { return (e.type & argument_type::mandatory) != 0; } -bool is_repeatable(const expected_argument& e) { +bool is_repeatable(const expected_argument& e) noexcept { return (e.type & argument_type::repeatable) != 0; } @@ -1372,7 +1407,7 @@ void print_help( std::string_view program_name, std::string_view program_description, const expected_arguments& expected, - const print_help_settings& settings = print_help_settings{}) { + const print_help_settings& settings = print_help_settings{}) noexcept { // Print program description console_print(make_colored(program_description, settings.with_color, color::highlight2), "\n"); @@ -1427,7 +1462,7 @@ void print_help( } if (!success) { - terminate_with("argument name is too long"); + truncate_end(heading); } console_print( diff --git a/tests/runtime_tests/section.cpp b/tests/runtime_tests/section.cpp index f5158594..5ad63c9b 100644 --- a/tests/runtime_tests/section.cpp +++ b/tests/runtime_tests/section.cpp @@ -170,6 +170,48 @@ TEST_CASE("section", "[test macros]") { CHECK_CASE(snitch::test_case_state::failed, 11u); } + SECTION("nested sections multiple leaves") { + framework.test_case.func = []() { + SNITCH_SECTION("section 1") { + SNITCH_SECTION("section 1.1") { + SNITCH_SECTION("section 1.1.1") { + SNITCH_FAIL_CHECK("trigger"); + } + SNITCH_SECTION("section 1.1.2") { + SNITCH_FAIL_CHECK("trigger"); + } + SNITCH_SECTION("section 1.1.3") { + SNITCH_FAIL_CHECK("trigger"); + } + } + } + SNITCH_SECTION("section 2") { + SNITCH_SECTION("section 2.1") { + SNITCH_SECTION("section 2.1.1") { + SNITCH_FAIL_CHECK("trigger"); + } + SNITCH_SECTION("section 2.1.2") { + SNITCH_FAIL_CHECK("trigger"); + } + SNITCH_SECTION("section 2.1.3") { + SNITCH_FAIL_CHECK("trigger"); + } + } + } + }; + + framework.run_test(); + + REQUIRE(framework.get_num_failures() == 6u); + CHECK_SECTIONS_FOR_FAILURE(0u, "section 1", "section 1.1", "section 1.1.1"); + CHECK_SECTIONS_FOR_FAILURE(1u, "section 1", "section 1.1", "section 1.1.2"); + CHECK_SECTIONS_FOR_FAILURE(2u, "section 1", "section 1.1", "section 1.1.3"); + CHECK_SECTIONS_FOR_FAILURE(3u, "section 2", "section 2.1", "section 2.1.1"); + CHECK_SECTIONS_FOR_FAILURE(4u, "section 2", "section 2.1", "section 2.1.2"); + CHECK_SECTIONS_FOR_FAILURE(5u, "section 2", "section 2.1", "section 2.1.3"); + CHECK_CASE(snitch::test_case_state::failed, 6u); + } + SECTION("one section in a loop") { framework.test_case.func = []() { for (std::size_t i = 0u; i < 5u; ++i) { @@ -235,7 +277,7 @@ TEST_CASE("section readme example", "[test macros]") { }; framework.registry.print_callback = print; - framework.registry.report_callback = {}; + framework.registry.report_callback = &snitch::impl::default_reporter; framework.test_case.func = []() { auto& reg = snitch::impl::get_current_test().reg; diff --git a/tests/runtime_tests/small_function.cpp b/tests/runtime_tests/small_function.cpp index 2ac2e0c3..6a1458ff 100644 --- a/tests/runtime_tests/small_function.cpp +++ b/tests/runtime_tests/small_function.cpp @@ -81,18 +81,13 @@ TEMPLATE_TEST_CASE( function_2_int) { [&](type_holder) { - snitch::small_function f; - test_object_instances = 0u; return_value = 0u; function_called = false; constexpr std::size_t expected_instances = sizeof...(Args) > 0 ? 3u : 0u; - CHECK(f.empty()); - SECTION("from free function") { - f = &test_class::method_static; - CHECK(!f.empty()); + snitch::small_function f = &test_class::method_static; call_function(f); @@ -104,9 +99,9 @@ TEMPLATE_TEST_CASE( } SECTION("from non-const member function") { - test_class obj; - f = {obj, snitch::constant<&test_class::method>{}}; - CHECK(!f.empty()); + test_class obj; + snitch::small_function f = { + obj, snitch::constant<&test_class::method>{}}; call_function(f); @@ -118,9 +113,9 @@ TEMPLATE_TEST_CASE( } SECTION("from const member function") { - const test_class obj; - f = {obj, snitch::constant<&test_class::method_const>{}}; - CHECK(!f.empty()); + const test_class obj; + snitch::small_function f = { + obj, snitch::constant<&test_class::method_const>{}}; call_function(f); @@ -132,13 +127,13 @@ TEMPLATE_TEST_CASE( } SECTION("from stateless lambda") { - f = snitch::small_function{[](Args...) noexcept -> R { - function_called = true; - if constexpr (!std::is_same_v) { - return 45; - } - }}; - CHECK(!f.empty()); + snitch::small_function f = + snitch::small_function{[](Args...) noexcept -> R { + function_called = true; + if constexpr (!std::is_same_v) { + return 45; + } + }}; call_function(f); @@ -158,8 +153,7 @@ TEMPLATE_TEST_CASE( } }; - f = snitch::small_function{lambda}; - CHECK(!f.empty()); + snitch::small_function f = snitch::small_function{lambda}; call_function(f); diff --git a/tests/runtime_tests/small_vector.cpp b/tests/runtime_tests/small_vector.cpp index 9f7732b4..1b8646a7 100644 --- a/tests/runtime_tests/small_vector.cpp +++ b/tests/runtime_tests/small_vector.cpp @@ -1,4 +1,5 @@ #include "testing.hpp" +#include "testing_assertions.hpp" namespace { constexpr std::size_t max_test_elements = 5u; @@ -391,6 +392,114 @@ TEMPLATE_TEST_CASE("small vector", "[utility]", vector_type, span_type, const_sp } } +#if SNITCH_WITH_EXCEPTIONS +TEST_CASE("small vector error cases", "[utility]") { + using TestType = vector_type; + assertion_exception_enabler enabler; + + SECTION("resize") { + TestType v; + SECTION("from empty") { + CHECK_THROWS_WHAT(v.resize(100u), assertion_exception, "small vector is full"); + } + SECTION("from full") { + v.resize(v.capacity()); + CHECK_THROWS_WHAT(v.resize(100u), assertion_exception, "small vector is full"); + } + } + + SECTION("grow") { + TestType v; + SECTION("from empty") { + CHECK_THROWS_WHAT(v.grow(100u), assertion_exception, "small vector is full"); + } + SECTION("from full") { + v.resize(v.capacity()); + CHECK_THROWS_WHAT(v.grow(1u), assertion_exception, "small vector is full"); + } + } + + SECTION("push_back") { + TestType v; + v.resize(v.capacity()); + SECTION("const T&") { + test_struct s; + CHECK_THROWS_WHAT(v.push_back(s), assertion_exception, "small vector is full"); + } + SECTION("T&&") { + test_struct s; + CHECK_THROWS_WHAT( + v.push_back(std::move(s)), assertion_exception, "small vector is full"); + } + } + + SECTION("pop_back") { + TestType v; + CHECK_THROWS_WHAT(v.pop_back(), assertion_exception, "pop_back() called on empty vector"); + } + + SECTION("back") { + SECTION("const T&") { + const TestType v; + CHECK_THROWS_WHAT(v.back(), assertion_exception, "back() called on empty vector"); + } + SECTION("T&") { + TestType v; + CHECK_THROWS_WHAT(v.back(), assertion_exception, "back() called on empty vector"); + } + SECTION("T& const") { + TestType v; + const auto& s = v.span(); + CHECK_THROWS_WHAT(s.back(), assertion_exception, "back() called on empty vector"); + } + } + + SECTION("operator[]") { + SECTION("from empty") { + SECTION("const T&") { + const TestType v; + CHECK_THROWS_WHAT( + v[0], assertion_exception, "operator[] called with incorrect index"); + } + SECTION("T&") { + TestType v; + CHECK_THROWS_WHAT( + v[0], assertion_exception, "operator[] called with incorrect index"); + } + SECTION("T& const") { + TestType v; + const auto& s = v.span(); + CHECK_THROWS_WHAT( + s[0], assertion_exception, "operator[] called with incorrect index"); + } + } + + SECTION("from non-empty") { + SECTION("const T&") { + TestType v0; + v0.resize(2); + const TestType& v = v0; + CHECK_THROWS_WHAT( + v[3], assertion_exception, "operator[] called with incorrect index"); + } + SECTION("T&") { + TestType v; + v.resize(2); + CHECK_THROWS_WHAT( + v[3], assertion_exception, "operator[] called with incorrect index"); + } + SECTION("T& const") { + TestType v; + v.resize(2); + const auto& s = v.span(); + CHECK_THROWS_WHAT( + s[3], assertion_exception, "operator[] called with incorrect index"); + } + } + } +} +#endif + TEST_CASE("constexpr small vector test_struct", "[utility]") { using TestType = vector_type; diff --git a/tests/testing.hpp b/tests/testing.hpp index 55fb8e63..55754a02 100644 --- a/tests/testing.hpp +++ b/tests/testing.hpp @@ -5,6 +5,8 @@ # else # include "snitch/snitch.hpp" # endif +# define CHECK_THROWS_WHAT(EXPR, EXCEPT, MESSAGE) \ + CHECK_THROWS_MATCHES(EXPR, EXCEPT, snitch::matchers::with_what_contains{(MESSAGE)}) #else @@ -36,6 +38,7 @@ } else { \ REQUIRE(__VA_ARGS__); \ } +# define CHECK_THROWS_WHAT(EXPR, EXCEPT, MESSAGE) CHECK_THROWS_WITH_AS(EXPR, MESSAGE, EXCEPT) # include diff --git a/tests/testing_assertions.hpp b/tests/testing_assertions.hpp new file mode 100644 index 00000000..7a4d15db --- /dev/null +++ b/tests/testing_assertions.hpp @@ -0,0 +1,32 @@ +#if SNITCH_WITH_EXCEPTIONS +struct assertion_exception : public std::exception { + snitch::small_string message = {}; + + explicit assertion_exception(std::string_view msg) { + snitch::append_or_truncate(message, msg); + if (message.available() > 0) { + message.push_back('\0'); + } else { + message.back() = '\0'; + } + } + + const char* what() const noexcept override { + return message.data(); + } +}; + +struct assertion_exception_enabler { + snitch::small_function prev_handler; + + assertion_exception_enabler() : prev_handler(&snitch::impl::stdout_print) { + snitch::assertion_failed_handler = [](std::string_view msg) { + throw assertion_exception(msg); + }; + } + + ~assertion_exception_enabler() { + snitch::assertion_failed_handler = prev_handler; + } +}; +#endif diff --git a/tests/testing_event.cpp b/tests/testing_event.cpp index 3541df04..5fc79e86 100644 --- a/tests/testing_event.cpp +++ b/tests/testing_event.cpp @@ -129,14 +129,14 @@ void mock_framework::print(std::string_view msg) noexcept { void mock_framework::setup_reporter() { registry.report_callback = {*this, snitch::constant<&mock_framework::report>{}}; - registry.print_callback = {}; + registry.print_callback = &snitch::impl::stdout_print; } void mock_framework::setup_print() { registry.with_color = false; registry.verbose = snitch::registry::verbosity::high; - registry.report_callback = {}; + registry.report_callback = &snitch::impl::default_reporter; registry.print_callback = {*this, snitch::constant<&mock_framework::print>{}}; } diff --git a/tests/testing_event.hpp b/tests/testing_event.hpp index 1156afc5..eebaeff6 100644 --- a/tests/testing_event.hpp +++ b/tests/testing_event.hpp @@ -1,6 +1,3 @@ -#include -#include - struct event_deep_copy { enum class type { unknown, @@ -72,11 +69,10 @@ struct mock_framework { }; struct console_output_catcher { - snitch::small_string<4086> messages = {}; - snitch::small_function prev_print = {}; + snitch::small_string<4086> messages = {}; + snitch::small_function prev_print; - console_output_catcher() { - prev_print = snitch::cli::console_print; + console_output_catcher() : prev_print(&snitch::impl::stdout_print) { snitch::cli::console_print = {*this, snitch::constant<&console_output_catcher::print>{}}; }