From 2e8ba3c80f0ae1c0c47491a74c40fc440bd5195f Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Wed, 2 Apr 2025 08:03:20 -0400 Subject: [PATCH 01/33] Migrate from std::string parameter to const char* due to ambiguity --- CMakeLists.txt | 7 +++- examples/sample/example_spec.cpp | 6 +-- include/class_description.hpp | 65 ++++++++++++++------------------ include/description.hpp | 27 ++++++------- include/formatters/tap.hpp | 4 +- include/it.hpp | 6 +-- include/it_base.hpp | 5 +-- spec/CMakeLists.txt | 5 ++- spec/describe_a_spec.cpp | 52 +++++++++++-------------- spec/describe_spec.cpp | 4 +- 10 files changed, 88 insertions(+), 93 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0941231..359f861 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,10 @@ cmake_minimum_required(VERSION 3.27 FATAL_ERROR) -project(c++spec) +project(c++spec + VERSION 1.2.0 + DESCRIPTION "BDD testing for C++" + LANGUAGES CXX +) set(CMAKE_COLOR_DIAGNOSTICS ON) @@ -77,6 +81,7 @@ option(CPPSPEC_BUILD_DOCS "Build C++Spec documentation") if(CPPSPEC_BUILD_TESTS) enable_testing() + include(CTest) # Tests add_subdirectory(spec) diff --git a/examples/sample/example_spec.cpp b/examples/sample/example_spec.cpp index 2dcc7a9..0de8cc1 100644 --- a/examples/sample/example_spec.cpp +++ b/examples/sample/example_spec.cpp @@ -68,11 +68,11 @@ describe bool_spec("Some Tests", $ { }); - explain > ({1,2,3}, _ { + explain(std::list{1,2,3}, _ { it(_ { is_expected().to_contain(1); }); it("includes [1,2,3]", _ { - expect>({1,2,3}).to_contain({1,2,3}); + expect(std::list{1,2,3}).to_contain({1,2,3}); }); it( _ { is_expected().not_().to_contain(4); }); @@ -185,7 +185,7 @@ describe let_spec("let", $ { // }); describe list_spec("A list spec", $ { - explain > ({1,2,3,4}, _ { + explain(std::list{1,2,3,4}, _ { it( _ { is_expected().to_contain(8); }); it( _ { is_expected().to_start_with({1,2,3}); }); }); diff --git a/include/class_description.hpp b/include/class_description.hpp index f6273c5..0b15ce7 100755 --- a/include/class_description.hpp +++ b/include/class_description.hpp @@ -36,7 +36,7 @@ class ClassDescription : public Description { this->description = Pretty::to_word(subject); } - ClassDescription(std::string description, Block block) : Description(description), block(block), subject(T()) {} + ClassDescription(const char* description, Block block) : Description(description), block(block), subject(T()) {} ClassDescription(T subject, Block block) : Description(Pretty::to_word(subject)), @@ -44,7 +44,7 @@ class ClassDescription : public Description { type(" : " + Util::demangle(typeid(T).name())), subject(subject) {} - ClassDescription(std::string description, T subject, Block block) + ClassDescription(const char* description, T subject, Block block) : Description(description), block(block), subject(subject) {} ClassDescription(T &subject, Block block) @@ -60,27 +60,25 @@ class ClassDescription : public Description { } template - ClassDescription(std::string description, std::initializer_list init_list, Block block) + ClassDescription(const char* description, std::initializer_list init_list, Block block) : Description(description), block(block), subject(T(init_list)) {} - Result it(std::string description, std::function &)> block); + Result it(const char* description, std::function &)> block); Result it(std::function &)> block); /** @brief an alias for it */ - Result specify(std::string description, std::function &)> block) { return it(description, block); } + Result specify(const char* description, std::function &)> block) { return it(description, block); } /** @brief an alias for it */ Result specify(std::function &)> block) { return it(block); } - template - Result context(std::string description, std::function &)> block); + template + Result context(const char* description, B block); - template - Result context(std::string description, U subject, std::function &)> block); - template - Result context(std::string description, U &subject, std::function &)> block); - template - Result context(U subject, std::function &)> block); - template - Result context(U &subject, std::function &)> block); + template + Result context(const char* description, U subject, B block); + template + Result context(const char* description, U &subject, B block); + template + Result context(U subject, B block); Result run(Formatters::BaseFormatter &printer) override; @@ -91,9 +89,9 @@ template using ClassContext = ClassDescription; template -template -Result ClassDescription::context(std::string description, U subject, - std::function &)> block) { +template +Result ClassDescription::context(const char* description, U subject, + B block) { ClassContext context(description, subject, block); context.set_parent(this); context.ClassContext::before_eaches = this->before_eaches; @@ -102,15 +100,15 @@ Result ClassDescription::context(std::string description, U subject, } template -template -Result ClassDescription::context(U subject, std::function &)> block) { +template +Result ClassDescription::context(U subject, B block) { return this->context("", std::forward(subject), block); } template -template -Result ClassDescription::context(std::string description, U &subject, - std::function &)> block) { +template +Result ClassDescription::context(const char* description, U &subject, + B block) { ClassContext context(description, subject, block); context.set_parent(this); context.ClassContext::before_eaches = this->before_eaches; @@ -119,14 +117,8 @@ Result ClassDescription::context(std::string description, U &subject, } template -template -Result ClassDescription::context(U &subject, std::function &)> block) { - return this->context("", std::forward(subject), block); -} - -template -template -Result ClassDescription::context(std::string description, std::function &)> block) { +template +Result ClassDescription::context(const char* description, B block) { ClassContext context(description, this->subject, block); context.set_parent(this); context.before_eaches = this->before_eaches; @@ -134,13 +126,14 @@ Result ClassDescription::context(std::string description, std::functionget_formatter()); } -template -Result Description::context(T subject, std::function &)> block) { +template +requires (!std::is_same_v) +Result Description::context(T subject, B block) { return this->context("", subject, block); } -template -Result Description::context(std::string description, T subject, std::function &)> block) { +template +Result Description::context(const char* description, T subject, B block) { ClassContext context(description, subject, block); context.set_parent(this); context.before_eaches = this->before_eaches; @@ -179,7 +172,7 @@ Result Description::context(std::initializer_list init_list, std::function -Result ClassDescription::it(std::string name, std::function &)> block) { +Result ClassDescription::it(const char* name, std::function &)> block) { ItCD it(*this, this->subject, name, block); Result result = it.run(this->get_formatter()); exec_after_eaches(); diff --git a/include/description.hpp b/include/description.hpp index 33b3ee1..5b70f5a 100755 --- a/include/description.hpp +++ b/include/description.hpp @@ -6,7 +6,6 @@ #include #include -#include #include #include @@ -40,10 +39,11 @@ class Description : public Runnable { // field initialized. They should only be used by subclasses // of Description. Description() = default; - explicit Description(std::string description) noexcept : description(std::move(description)) {} + explicit Description(std::string&& description) noexcept : description(std::move(description)) {} + explicit Description(const char* description) noexcept : description(description) {} - Description(const Child &parent, std::string description, Block block) noexcept - : Runnable(parent), block(std::move(block)), description(std::move(description)) {} + Description(const Child &parent, const char* description, Block block) noexcept + : Runnable(parent), block(std::move(block)), description(description) {} void exec_before_eaches(); void exec_after_eaches(); @@ -54,24 +54,25 @@ class Description : public Runnable { Description(Description &©) = default; // Primary constructor. Entry of all specs. - Description(std::string description, Block block) noexcept + Description(const char* description, Block block) noexcept : block(std::move(block)), description(std::move(description)) {} /********* Specify/It *********/ - Result it(std::string description, ItD::Block body); + Result it(const char* description, ItD::Block body); Result it(ItD::Block body); /********* Context ***********/ template - Result context(std::string name, Block body); + Result context(const char* name, Block body); - template - Result context(T subject, std::function &)> block); + template + requires (!std::is_same_v) + Result context(T subject, B block); - template - Result context(std::string description, T subject, std::function &)> block); + template + Result context(const char* description, T subject, B block); template Result context(std::initializer_list init_list, std::function &)> block); @@ -108,7 +109,7 @@ using Context = Description; /*========= Description::it =========*/ -inline Result Description::it(std::string description, ItD::Block block) { +inline Result Description::it(const char* description, ItD::Block block) { ItD it(*this, description, block); Result result = it.run(this->get_formatter()); exec_after_eaches(); @@ -127,7 +128,7 @@ inline Result Description::it(ItD::Block block) { /*========= Description::context =========*/ template -inline Result Description::context(std::string description, Block body) { +inline Result Description::context(const char* description, Block body) { Context context(*this, description, body); context.before_eaches = this->before_eaches; context.after_eaches = this->after_eaches; diff --git a/include/formatters/tap.hpp b/include/formatters/tap.hpp index 6136804..1582c59 100644 --- a/include/formatters/tap.hpp +++ b/include/formatters/tap.hpp @@ -34,14 +34,14 @@ inline void TAP::format(const Description &description) { } inline void TAP::format(const ItBase &it) { - std::string description = it.get_description(); + std::string description{it.get_description()}; // Build up the description for the test by ascending the // execution tree and chaining the individual descriptions together for (const auto *parent = it.get_parent_as(); parent != nullptr; parent = parent->get_parent_as()) { - description = parent->get_description() + " " + description; + description = std::string(parent->get_description()) + " " + description; } if (color_output) { diff --git a/include/it.hpp b/include/it.hpp index 7f64360..059ce91 100755 --- a/include/it.hpp +++ b/include/it.hpp @@ -48,8 +48,8 @@ class ItD : public ItBase { * * @return the constructed ItD object */ - ItD(const Child &parent, std::string description, Block block) - : ItBase(parent, std::move(description)), block(std::move(block)) {} + ItD(const Child &parent, const char* description, Block block) + : ItBase(parent, description), block(std::move(block)) {} /** * @brief The anonymous ItD constructor @@ -104,7 +104,7 @@ class ItCD : public ItBase { T &subject; // This is only ever instantiated by ClassDescription - ItCD(const Child &parent, T &subject, std::string description, Block block) + ItCD(const Child &parent, T &subject, const char* description, Block block) : ItBase(parent, description), block(block), subject(subject) {} ItCD(const Child &parent, T &subject, Block block) : ItBase(parent), block(block), subject(subject) {} diff --git a/include/it_base.hpp b/include/it_base.hpp index e15348f..0ad0ede 100755 --- a/include/it_base.hpp +++ b/include/it_base.hpp @@ -48,7 +48,7 @@ class ItBase : public Runnable { * @param description the documentation string of the `it` statement * @return the constructed BaseIt */ - explicit ItBase(const Child &parent, std::string description) noexcept + explicit ItBase(const Child &parent, const char* description) noexcept : Runnable(parent), description(std::move(description)) {} /** @@ -61,14 +61,13 @@ class ItBase : public Runnable { * @brief Get the description string for the `it` statement * @return the description string */ - std::string get_description() noexcept { return description; } [[nodiscard]] std::string get_description() const noexcept { return description; } /** * @brief Set the description string * @return a reference to the modified ItBase */ - ItBase &set_description(const std::string &description) noexcept { + ItBase &set_description(std::string_view description) noexcept { this->description = description; return *this; } diff --git a/spec/CMakeLists.txt b/spec/CMakeLists.txt index 21cc06a..fe6191d 100644 --- a/spec/CMakeLists.txt +++ b/spec/CMakeLists.txt @@ -1,2 +1,5 @@ -add_compile_options(-Wno-unused-parameter -Wno-missing-template-arg-list-after-template-kw) +if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + add_compile_options(-Wno-unused-parameter -Wno-missing-template-arg-list-after-template-kw -Wno-missing-template-keyword -Wno-unknown-warning-option) +endif() + discover_specs(${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/spec/describe_a_spec.cpp b/spec/describe_a_spec.cpp index fe13063..fd8699c 100644 --- a/spec/describe_a_spec.cpp +++ b/spec/describe_a_spec.cpp @@ -13,40 +13,34 @@ struct TestClass { : field1(field1), field2(field2){}; }; -describe_a describe_a_implicit_spec( - "Implicit subjects", $ { - it( - "Default constructor should be called", _ { - expect(subject.field1).to_equal("foo"); - expect(subject.field2).to_equal("bar"); - }); +// clang-format off +describe_a describe_a_implicit_spec("Implicit subjects", $ { + it("Default constructor should be called", _ { + expect(subject.field1).to_equal("foo"); + expect(subject.field2).to_equal("bar"); }); +}); -describe_a describe_a_explicit_spec( - "Explicit subjects", TestClass("bar", "baz"), $ { - it( - "Specified constructor should be called", _ { - expect(subject.field1).to_equal("bar"); - expect(subject.field2).to_equal("baz"); - }); +describe_a describe_a_explicit_spec("Explicit subjects", TestClass("bar", "baz"), $ { + it("Specified constructor should be called", _ { + expect(subject.field1).to_equal("bar"); + expect(subject.field2).to_equal("baz"); }); +}); -describe_a describe_a_syntax_spec( - "describe_a syntax", $ { - context( - "A nested context with no given subject", _ { - it( - "should inherit the subject", - _ { expect(subject).to_equal(true); }); - }); - - context( - "A nested context with a given subject", 1, _ { - it( - "should not have the same subject as the parent", - _ { is_expected().to_equal(1); }); - }); +describe_a describe_a_syntax_spec("describe_a syntax", $ { + context("A nested context with no given subject", _ { + it("should inherit the subject", _ { + expect(subject).to_equal(true); }); + }); + + context("A nested context with a given subject", 1, _ { + it("should not have the same subject as the parent", _ { + is_expected().to_equal(1); + }); + }); +}); int main(int argc, char **argv) { return CppSpec ::parse(argc, argv) diff --git a/spec/describe_spec.cpp b/spec/describe_spec.cpp index 2cebb6c..713fc30 100644 --- a/spec/describe_spec.cpp +++ b/spec/describe_spec.cpp @@ -4,8 +4,8 @@ using namespace CppSpec; describe describe_spec("Description", $ { it("responds to it", _ { }); - context("can create ClassContexts", 1, _{}); + context("can create ClassContexts", 1, _{}); }); -CPPSPEC_MAIN(describe_spec); \ No newline at end of file +CPPSPEC_MAIN(describe_spec); From 5959b6a55770f32cc1723a1abb71c8b5b10bd523 Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Wed, 2 Apr 2025 08:52:43 -0400 Subject: [PATCH 02/33] Add source_location support --- include/class_description.hpp | 2 +- include/expectations/expectation.hpp | 13 ++++++++----- include/it.hpp | 25 +++++++++++++------------ include/it_base.hpp | 11 ++++++----- spec/expectations/expectation_spec.cpp | 2 +- spec/matchers/be_within_spec.cpp | 9 +++++---- 6 files changed, 34 insertions(+), 28 deletions(-) diff --git a/include/class_description.hpp b/include/class_description.hpp index 0b15ce7..f059aaf 100755 --- a/include/class_description.hpp +++ b/include/class_description.hpp @@ -229,7 +229,7 @@ Result ClassDescription::run(Formatters::BaseFormatter &printer) { template ExpectationValue ItCD::is_expected() { auto cd = static_cast *>(this->get_parent()); - ExpectationValue expectation(*this, cd->subject); + ExpectationValue expectation(*this, cd->subject, std::source_location::current()); return expectation; } diff --git a/include/expectations/expectation.hpp b/include/expectations/expectation.hpp index 093d70d..5160cf0 100755 --- a/include/expectations/expectation.hpp +++ b/include/expectations/expectation.hpp @@ -395,6 +395,7 @@ Result Expectation::to_have_error(std::string msg) { template class ExpectationValue : public Expectation { A value; + std::source_location location; public: /** @@ -404,7 +405,8 @@ class ExpectationValue : public Expectation { * * @return The constructed ExpectationValue. */ - ExpectationValue(ItBase &it, A value) : Expectation(it), value(value) {} + ExpectationValue(ItBase &it, A value, std::source_location location) + : Expectation(it), value(value), location{location} {} /** * @brief Create an Expectation using an initializer list. @@ -414,8 +416,8 @@ class ExpectationValue : public Expectation { * @return The constructed Expectation. */ template - ExpectationValue(ItBase &it, std::initializer_list init_list) - : Expectation(it), value(std::vector(init_list)) {} + ExpectationValue(ItBase &it, std::initializer_list init_list, std::source_location location) + : Expectation(it), value(std::vector(init_list)), location{location} {} /** @brief Get the target of the expectation. */ A &get_target() & override { return value; } @@ -436,9 +438,10 @@ class ExpectationFunc : public Expectation()())> { using block_ret_t = decltype(std::declval()()); F block; std::shared_ptr computed = nullptr; + std::source_location location; public: - ExpectationFunc(ExpectationFunc const ©) : Expectation(copy), block(copy.block) {} + ExpectationFunc(ExpectationFunc const ©, std::source_location location) : Expectation(copy), block(copy.block), location{location} {} /** * @brief Create an ExpectationValue using a value. @@ -447,7 +450,7 @@ class ExpectationFunc : public Expectation()())> { * * @return The constructed ExpectationValue. */ - ExpectationFunc(ItBase &it, F block) : Expectation(it), block(block) {} + ExpectationFunc(ItBase &it, F block, std::source_location location) : Expectation(it), block(block), location{location} {} /** * @brief Create an Expectation using a function. diff --git a/include/it.hpp b/include/it.hpp index 059ce91..bd55b4e 100755 --- a/include/it.hpp +++ b/include/it.hpp @@ -48,7 +48,7 @@ class ItD : public ItBase { * * @return the constructed ItD object */ - ItD(const Child &parent, const char* description, Block block) + ItD(const Child &parent, const char *description, Block block) : ItBase(parent, description), block(std::move(block)) {} /** @@ -104,7 +104,7 @@ class ItCD : public ItBase { T &subject; // This is only ever instantiated by ClassDescription - ItCD(const Child &parent, T &subject, const char* description, Block block) + ItCD(const Child &parent, T &subject, const char *description, Block block) : ItBase(parent, description), block(block), subject(subject) {} ItCD(const Child &parent, T &subject, Block block) : ItBase(parent), block(block), subject(subject) {} @@ -123,18 +123,18 @@ class ItCD : public ItBase { * @endcode */ template -ExpectationValue ItBase::expect(T value) { - return ExpectationValue(*this, value); +ExpectationValue ItBase::expect(T value, std::source_location location) { + return ExpectationValue(*this, value, location); } template -ExpectationFunc ItBase::expect(T block) { - return ExpectationFunc(*this, block); +ExpectationFunc ItBase::expect(T block, std::source_location location) { + return ExpectationFunc(*this, block, location); } template -ExpectationValue ItBase::expect(Let &let) { - return ExpectationValue(*this, let.value()); +ExpectationValue ItBase::expect(Let &let, std::source_location location) { + return ExpectationValue(*this, let.value(), location); } /** @@ -145,12 +145,13 @@ ExpectationValue ItBase::expect(Let &let) { * @endcode */ template -ExpectationValue> ItBase::expect(std::initializer_list init_list) { - return ExpectationValue>(*this, init_list); +ExpectationValue> ItBase::expect(std::initializer_list init_list, + std::source_location location) { + return ExpectationValue>(*this, init_list, location); } -inline ExpectationValue ItBase::expect(const char *str) { - return ExpectationValue(*this, std::string(str)); +inline ExpectationValue ItBase::expect(const char *str, std::source_location location) { + return ExpectationValue(*this, std::string(str), location); } } // namespace CppSpec diff --git a/include/it_base.hpp b/include/it_base.hpp index 0ad0ede..56c9705 100755 --- a/include/it_base.hpp +++ b/include/it_base.hpp @@ -1,6 +1,7 @@ /** @file */ #pragma once +#include #include #include #include @@ -84,7 +85,7 @@ class ItBase : public Runnable { * @return a ExpectationValue object containing the given value. */ template - ExpectationValue expect(T value); + ExpectationValue expect(T value, std::source_location location = std::source_location::current()); /** * @brief The `expect` object generator for lambdas @@ -96,7 +97,7 @@ class ItBase : public Runnable { * @return a ExpectationFunc object containing the given value. */ template - ExpectationFunc expect(T block); + ExpectationFunc expect(T block, std::source_location location = std::source_location::current()); /** * @brief The `expect` object generator for initializer lists @@ -108,7 +109,7 @@ class ItBase : public Runnable { * @return a ExpectationValue object containing the given init_list. */ template - ExpectationValue> expect(std::initializer_list init_list); + ExpectationValue> expect(std::initializer_list init_list, std::source_location location = std::source_location::current()); /** * @brief The `expect` object generator for Let @@ -120,7 +121,7 @@ class ItBase : public Runnable { * @return a ExpectationValue object containing the given value. */ template - ExpectationValue expect(Let &let); + ExpectationValue expect(Let &let, std::source_location location = std::source_location::current()); /** * @brief The `expect` object generator for const char* @@ -128,7 +129,7 @@ class ItBase : public Runnable { * @param string the string to wrap * @return a ExpectationValue object containing a C++ string */ - ExpectationValue expect(const char *string); + ExpectationValue expect(const char *string, std::source_location location = std::source_location::current()); }; } // namespace CppSpec diff --git a/spec/expectations/expectation_spec.cpp b/spec/expectations/expectation_spec.cpp index 67958c0..d269350 100644 --- a/spec/expectations/expectation_spec.cpp +++ b/spec/expectations/expectation_spec.cpp @@ -106,7 +106,7 @@ describe expectation_spec("Expectation", $ { // we explicitly want a function. Any other time // that would be perfectly okay. std::function foo = [] { return 1 + 2; }; - ExpectationFunc expectation(self, foo); + ExpectationFunc expectation(self, foo, std::source_location::current()); expect(expectation.get_target()).to_equal(3); }); }); diff --git a/spec/matchers/be_within_spec.cpp b/spec/matchers/be_within_spec.cpp index 5b97129..49cc4c2 100644 --- a/spec/matchers/be_within_spec.cpp +++ b/spec/matchers/be_within_spec.cpp @@ -1,3 +1,4 @@ +#include #include "cppspec.hpp" using namespace CppSpec; @@ -38,7 +39,7 @@ describe be_within_spec("expect(actual).to_be_within(delta).of(expected)", $ { it("passes with negative arguments", _ { expect(-1.0001).to_be_within(5).percent_of(-1); }); - + it("works with std::time", _ { expect(std::time(nullptr)).to_be_within(0.1).of(std::time(nullptr)); }); @@ -57,7 +58,7 @@ describe be_within_spec("expect(actual).to_be_within(delta).of(expected)", $ { it("provides a description", _ { auto d = 5.1; - ExpectationValue ex(self, d); + ExpectationValue ex(self, d, std::source_location::current()); Matchers::BeWithin matcher(ex, 0.5); matcher.of(5.0); expect(matcher.description()).to_equal("be within 0.5 of 5"); @@ -83,7 +84,7 @@ describe be_within_spec("expect(actual).to_be_within(delta).of(expected)", $ { it("provides a description", _ { auto d = 5.1; - ExpectationValue ex(self, d); + ExpectationValue ex(self, d, std::source_location::current()); Matchers::BeWithin matcher(ex, 0.5); matcher.percent_of(5.0); expect(matcher.description()).to_equal("be within 0.5% of 5"); @@ -136,4 +137,4 @@ describe be_within_spec("expect(actual).to_be_within(delta).of(expected)", $ { }); -CPPSPEC_MAIN(be_within_spec); \ No newline at end of file +CPPSPEC_MAIN(be_within_spec); From ddb9c571a489a23a18794f9256db8c7e6998f7d1 Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Wed, 2 Apr 2025 13:05:26 -0400 Subject: [PATCH 03/33] Refactor around verb --- include/expectations/expectation.hpp | 6 +- include/matchers/be_nullptr.hpp | 1 + include/matchers/contain.hpp | 5 +- include/matchers/equal.hpp | 9 +- include/matchers/errors/fail.hpp | 3 - include/matchers/errors/have_error.hpp | 6 +- include/matchers/errors/have_value.hpp | 8 +- include/matchers/errors/throw.hpp | 23 ++--- include/matchers/matcher_base.hpp | 28 +++--- include/matchers/numeric/be_between.hpp | 7 +- include/matchers/numeric/be_greater_than.hpp | 2 +- include/matchers/numeric/be_less_than.hpp | 2 +- include/matchers/numeric/be_within.hpp | 60 ++++++------- include/matchers/pretty_matchers.hpp | 89 ++++++++++---------- include/matchers/satisfy.hpp | 17 +--- include/matchers/strings/end_with.hpp | 2 +- include/matchers/strings/match.hpp | 2 +- include/matchers/strings/start_with.hpp | 2 +- include/runnable.hpp | 6 +- spec/matchers/be_within_spec.cpp | 29 +++---- 20 files changed, 132 insertions(+), 175 deletions(-) diff --git a/include/expectations/expectation.hpp b/include/expectations/expectation.hpp index 5160cf0..c241581 100755 --- a/include/expectations/expectation.hpp +++ b/include/expectations/expectation.hpp @@ -100,7 +100,7 @@ class Expectation : public Child { Result to_be_less_than(E rhs, std::string msg = ""); template - Matchers::BeWithin to_be_within(E expected, std::string msg = ""); + Matchers::BeWithinHelper to_be_within(E expected, std::string msg = ""); /*-------- to... ----------*/ @@ -302,8 +302,8 @@ Result Expectation::to_equal(E expected, std::string msg) { */ template template -Matchers::BeWithin Expectation::to_be_within(E expected, std::string msg) { - Matchers::BeWithin matcher(*this, expected); +Matchers::BeWithinHelper Expectation::to_be_within(E expected, std::string msg) { + Matchers::BeWithinHelper matcher(*this, expected); matcher.set_message(msg); return matcher; } diff --git a/include/matchers/be_nullptr.hpp b/include/matchers/be_nullptr.hpp index d0ca3f1..19a0fb0 100644 --- a/include/matchers/be_nullptr.hpp +++ b/include/matchers/be_nullptr.hpp @@ -12,6 +12,7 @@ class BeNullptr : MatcherBase { public: explicit BeNullptr(Expectation &expectation) : MatcherBase(expectation) {} + std::string verb() override { return "be"; } std::string description() override { return "be nullptr"; } bool match() override { return this->actual() == nullptr; } }; diff --git a/include/matchers/contain.hpp b/include/matchers/contain.hpp index c67db22..180ff37 100644 --- a/include/matchers/contain.hpp +++ b/include/matchers/contain.hpp @@ -13,6 +13,7 @@ class ContainBase : public MatcherBase { A actual_; public: + std::string verb() override { return "contain"; } std::string description() override; std::string failure_message() override; std::string failure_message_when_negated() override; @@ -30,7 +31,7 @@ class ContainBase : public MatcherBase { template std::string ContainBase::description() { // std::vector described_items; - return Pretty::improve_hash_formatting("contain" + Pretty::to_sentence(this->expected())); + return Pretty::improve_hash_formatting(verb() + Pretty::to_sentence(this->expected())); } template @@ -49,7 +50,7 @@ bool ContainBase::actual_collection_includes(U expected_item) { auto last = *(actual.begin()); static_assert(Util::verbose_assert>::value, "Expected item is not the same type as what is inside container."); - return std::find(actual.begin(), actual.end(), expected_item) != actual.end(); + return std::ranges::find(actual, expected_item) != actual.end(); } /** diff --git a/include/matchers/equal.hpp b/include/matchers/equal.hpp index ee07153..69d36ac 100644 --- a/include/matchers/equal.hpp +++ b/include/matchers/equal.hpp @@ -18,7 +18,7 @@ class Equal : public MatcherBase { public: Equal(Expectation &expectation, E expected) : MatcherBase(expectation, expected) {} - std::string description() override; + std::string verb() override { return "equal"; } std::string failure_message() override; std::string failure_message_when_negated() override; bool diffable(); @@ -31,13 +31,6 @@ class Equal : public MatcherBase { // std::string detailed_failure_message(); }; -template -std::string Equal::description() { - std::stringstream ss; - ss << "equal" << Pretty::to_sentence(this->expected()); - return ss.str(); -} - template std::string Equal::failure_message() { // if (expected_is_a_literal()) { diff --git a/include/matchers/errors/fail.hpp b/include/matchers/errors/fail.hpp index 43dc315..9efb8c4 100644 --- a/include/matchers/errors/fail.hpp +++ b/include/matchers/errors/fail.hpp @@ -1,8 +1,5 @@ -/** @file */ #pragma once -#include - #include "matchers/matcher_base.hpp" namespace CppSpec::Matchers { diff --git a/include/matchers/errors/have_error.hpp b/include/matchers/errors/have_error.hpp index 53cba6c..c946d8b 100644 --- a/include/matchers/errors/have_error.hpp +++ b/include/matchers/errors/have_error.hpp @@ -3,12 +3,11 @@ #include "matchers/matcher_base.hpp" - namespace CppSpec::Matchers { template concept expected = requires(T t) { - { t.error() } -> std::same_as; + { t.error() } -> std::same_as; }; template @@ -16,7 +15,8 @@ class HaveError : public MatcherBase { public: HaveError(Expectation &expectation) : MatcherBase(expectation) {} - std::string description() override {return "have an error"; } + std::string verb() override { return "have an error"; } + std::string description() override { return verb(); } bool match() override { return (!this->actual().has_value()); } }; diff --git a/include/matchers/errors/have_value.hpp b/include/matchers/errors/have_value.hpp index 145fd68..b19ea46 100644 --- a/include/matchers/errors/have_value.hpp +++ b/include/matchers/errors/have_value.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include "matchers/equal.hpp" #include "matchers/matcher_base.hpp" namespace CppSpec::Matchers { @@ -15,7 +15,8 @@ class HaveValue : public MatcherBase { public: HaveValue(Expectation &expectation) : MatcherBase(expectation) {} - std::string description() override {return "have a value"; } + std::string verb() override { return "have"; } + std::string description() override { return "have a value"; } bool match() override { return (this->actual().has_value()); } }; @@ -23,8 +24,7 @@ class HaveValue : public MatcherBase { template class HaveValueEqualTo : public Equal { public: - static_assert(std::is_same_v, - "the contained value_type must match the expected type"); + static_assert(std::is_same_v, "the contained value_type must match the expected type"); HaveValueEqualTo(Expectation &expectation, E expected) : Equal(expectation, expected) {} bool match() { return (this->actual().value() == this->expected()); } diff --git a/include/matchers/errors/throw.hpp b/include/matchers/errors/throw.hpp index 162e259..ce0835b 100644 --- a/include/matchers/errors/throw.hpp +++ b/include/matchers/errors/throw.hpp @@ -1,9 +1,5 @@ -/** @file */ #pragma once -#include -#include - #include "matchers/matcher_base.hpp" #include "util.hpp" @@ -14,6 +10,7 @@ class Throw : public MatcherBase { public: explicit Throw(Expectation &expectation) : MatcherBase(expectation, nullptr) {} bool match() override; + std::string verb() override { return "throw"; } std::string description() override; std::string failure_message() override; std::string failure_message_when_negated() override; @@ -32,26 +29,18 @@ bool Throw::match() { template std::string Throw::description() { - std::stringstream ss; - ss << "throw " << Util::demangle(typeid(Ex).name()); - return ss.str(); + return std::format("throw {}", Util::demangle(typeid(Ex).name())); } template std::string Throw::failure_message() { - std::stringstream ss; - ss << "expected the given function ([] -> " << Util::demangle(typeid(A).name()) << " {...}) to " - << this->description(); - return ss.str(); + return std::format("expected the given function ([] -> {} {{...}}) to {}", Util::demangle(typeid(A).name()), + description()); } template std::string Throw::failure_message_when_negated() { - std::stringstream ss; - ss << "expected the given function ([] -> " << Util::demangle(typeid(A).name()) << " {...}) not to " - << this->description(); - return ss.str(); + return std::format("expected the given function ([] -> {} {{...}}) not to {}", Util::demangle(typeid(A).name()), + description()); } - } // namespace CppSpec::Matchers - diff --git a/include/matchers/matcher_base.hpp b/include/matchers/matcher_base.hpp index 001e2d9..bf81699 100644 --- a/include/matchers/matcher_base.hpp +++ b/include/matchers/matcher_base.hpp @@ -65,6 +65,7 @@ class MatcherBase : public Runnable, public Pretty { virtual std::string failure_message(); virtual std::string failure_message_when_negated(); virtual std::string description(); + virtual std::string verb() { return "match"; } // Get the 'actual' object from the Expectation constexpr Actual &actual() { return expectation_.get_target(); } @@ -113,9 +114,7 @@ std::string MatcherBase::failure_message() { if (not custom_failure_message.empty()) { return this->custom_failure_message; } - std::stringstream ss; - ss << "expected " << Pretty::to_word(actual()) << " to " << description(); - return ss.str(); + return std::format("expected {} to {}", Pretty::to_word(actual()), description()); } /** @@ -128,9 +127,7 @@ std::string MatcherBase::failure_message_when_negated() { if (not custom_failure_message.empty()) { return this->custom_failure_message; } - std::stringstream ss; - ss << "expected " << Pretty::to_word(actual()) << " to not " << description(); - return ss.str(); + return std::format("expected {} to not {}", Pretty::to_word(actual()), description()); } /** @@ -140,13 +137,11 @@ std::string MatcherBase::failure_message_when_negated() { */ template std::string MatcherBase::description() { - std::stringstream ss; - //std::string pretty_expected = this->to_sentence(expected_); - // ss << "match " << - // this->name_to_sentence(Util::demangle(typeid(*this).name())) - // << "(" << pretty_expected.substr(1, pretty_expected.length()) << ")"; - ss << "match" << Pretty::to_sentence(expected_); - return ss.str(); + // std::string pretty_expected = this->to_sentence(expected_); + // ss << "match " << + // this->name_to_sentence(Util::demangle(typeid(*this).name())) + // << "(" << pretty_expected.substr(1, pretty_expected.length()) << ")"; + return std::format("{} {}", verb(), Pretty::to_sentence(expected_)); } /** @@ -158,15 +153,14 @@ std::string MatcherBase::description() { */ template Result MatcherBase::run(Formatters::BaseFormatter &printer) { - auto *par = static_cast(this->get_parent()); + auto *parent = static_cast(this->get_parent()); // If we need a description for our test, generate it // unless we're ignoring the output. - if (par->needs_description() && !expectation_.ignore_failure()) { + if (parent->needs_description() && !expectation_.ignore_failure()) { std::stringstream ss; ss << (expectation_.sign() ? PositiveExpectationHandler::verb() : NegativeExpectationHandler::verb()) << " " << this->description(); - std::string ss_str = ss.str(); - par->set_description(ss_str); + parent->set_description(ss.str()); } Result matched = expectation_.sign() ? PositiveExpectationHandler::handle_matcher(*this) diff --git a/include/matchers/numeric/be_between.hpp b/include/matchers/numeric/be_between.hpp index b102fae..e211438 100644 --- a/include/matchers/numeric/be_between.hpp +++ b/include/matchers/numeric/be_between.hpp @@ -34,6 +34,7 @@ class BeBetween : public MatcherBase { } bool match() override; + std::string verb() override { return "be between"; } std::string description() override; }; @@ -63,11 +64,7 @@ bool BeBetween::match() { template std::string BeBetween::description() { - std::stringstream ss; - ss << "be between " << min << " and " << max << " (" << (mode == RangeMode::exclusive ? "exclusive" : "inclusive") - << ")"; - return ss.str(); + return std::format("be between {} and {} ({})", min, max, (mode == RangeMode::exclusive ? "exclusive" : "inclusive")); } } // namespace CppSpec::Matchers - diff --git a/include/matchers/numeric/be_greater_than.hpp b/include/matchers/numeric/be_greater_than.hpp index 8448764..133b257 100644 --- a/include/matchers/numeric/be_greater_than.hpp +++ b/include/matchers/numeric/be_greater_than.hpp @@ -12,7 +12,7 @@ class BeGreaterThan : public MatcherBase { public: BeGreaterThan(Expectation &expectation, E expected) : MatcherBase(expectation, expected) {} bool match() override { return this->actual() > this->expected(); } - std::string description() override { return "be greater than" + Pretty::to_word(this->expected()); } + std::string verb() override { return "be greater than"; } }; } // namespace CppSpec::Matchers diff --git a/include/matchers/numeric/be_less_than.hpp b/include/matchers/numeric/be_less_than.hpp index 9c1359a..ddbc144 100644 --- a/include/matchers/numeric/be_less_than.hpp +++ b/include/matchers/numeric/be_less_than.hpp @@ -12,7 +12,7 @@ class BeLessThan : public MatcherBase { public: BeLessThan(Expectation &expectation, E expected) : MatcherBase(expectation, expected) {} bool match() override { return this->actual() < this->expected(); } - std::string description() override { return "be less than" + Pretty::to_word(this->expected()); } + std::string verb() override { return "be less than"; } }; } // namespace CppSpec::Matchers diff --git a/include/matchers/numeric/be_within.hpp b/include/matchers/numeric/be_within.hpp index ec5c154..52a4f5f 100644 --- a/include/matchers/numeric/be_within.hpp +++ b/include/matchers/numeric/be_within.hpp @@ -1,51 +1,60 @@ -/** @file */ #pragma once #include "matchers/matcher_base.hpp" namespace CppSpec::Matchers { +template +class BeWithin; + +template +class BeWithinHelper { + E tolerance; + Expectation& expectation; + std::string msg; + + public: + BeWithinHelper(Expectation& expectation, E tolerance) : expectation(expectation), tolerance(tolerance) {} + + BeWithin of(E expected); + BeWithin percent_of(E expected); + void set_message(const std::string& msg) { this->msg = msg; } + std::string get_message() { return this->msg; } +}; template class BeWithin : public MatcherBase { - E delta; std::string unit; E tolerance; public: - BeWithin(Expectation &expectation, E delta) : MatcherBase(expectation, 0), delta(delta) {} - - bool of(E expected); - bool percent_of(E expected); + BeWithin(Expectation& expectation, E tolerance, E value, std::string_view unit) + : MatcherBase(expectation, value), tolerance{tolerance}, unit{unit} {} bool match() override; std::string failure_message() override; std::string failure_message_when_negated() override; std::string description() override; + std::string verb() override { return "be within"; } }; template -bool BeWithin::of(E expected) { - this->expected_ = expected; - this->tolerance = this->delta; - this->unit = ""; - return this->run(this->get_formatter()); +BeWithin BeWithinHelper::of(E expected) { + BeWithin matcher(expectation, tolerance, expected, ""); // No unit specified + matcher.set_message(msg); + return matcher; } template -bool BeWithin::percent_of(E expected) { - this->expected_ = expected; - this->tolerance = this->delta; - this->unit = "%"; - return this->run(this->get_formatter()); +BeWithin BeWithinHelper::percent_of(E expected) { + BeWithin matcher(expectation, tolerance, expected, "%"); // Percent unit specified + matcher.set_message(msg); + return matcher; } template bool BeWithin::match() { if (!this->expected()) { - std::stringstream ss; - ss << "You must set an expected value using #of: be_within(" << this->delta << ").of(expected_value)"; - return false; } return std::abs(this->actual() - this->expected()) <= this->tolerance; @@ -53,24 +62,17 @@ bool BeWithin::match() { template std::string BeWithin::failure_message() { - std::stringstream ss; - ss << "expected " << this->actual() << " to " << description(); - return ss.str(); + return std::format("expected {} to {}", this->actual(), description()); } template std::string BeWithin::failure_message_when_negated() { - std::stringstream ss; - ss << "expected " << this->actual() << " not to " << description(); - return ss.str(); + return std::format("expected {} not to {}", this->actual(), description()); } template std::string BeWithin::description() { - std::stringstream ss; - ss << "be within " << this->delta << this->unit << " of " << this->expected(); - return ss.str(); + return std::format("be within {}{} of {}", this->tolerance, this->unit, this->expected()); } } // namespace CppSpec::Matchers - diff --git a/include/matchers/pretty_matchers.hpp b/include/matchers/pretty_matchers.hpp index 42b80eb..17bf5a9 100755 --- a/include/matchers/pretty_matchers.hpp +++ b/include/matchers/pretty_matchers.hpp @@ -34,25 +34,25 @@ struct Pretty { static std::string improve_hash_formatting(const std::string &inspect_string); template - static std::string to_word(T &item); + static std::string to_word(const T &item); template - static std::string to_word(T &item); + static std::string to_word(const T &item); template - static std::string to_word_type(T &item); + static std::string to_word_type(const T &item); template - static std::string to_word(Matchers::MatcherBase &matcher); + static std::string to_word(const Matchers::MatcherBase &matcher); template - static std::string to_sentence(T &item); + static std::string to_sentence(const T &item); template - static std::string to_sentence(std::vector &words); + static std::string to_sentence(const std::vector &words); template - static std::string inspect_object(T &object); + static std::string inspect_object(const T &object); }; /** @@ -64,28 +64,31 @@ struct Pretty { * @return a human-readable comma-delimited list */ template -inline std::string Pretty::to_sentence(std::vector &words) { - std::vector my_words; - for (T word : words) { - my_words.push_back(to_word(word)); +inline std::string Pretty::to_sentence(const std::vector &objects) { + std::vector words; + for (const auto &object : objects) { + words.push_back(to_word(object)); } + std::stringstream ss; - switch (my_words.size()) { + switch (words.size()) { case 0: return ""; case 1: - ss << " " << my_words[0]; + ss << " " << words[0]; break; case 2: - ss << " " << my_words[0] << " and " << my_words[1]; + ss << " " << words[0] << " and " << words[1]; break; default: ss << " "; - for (size_t i = 0; i < my_words.size() - 1; ++i) { - if (i != 0) ss << ", "; - ss << my_words[i]; + for (size_t i = 0; i < words.size() - 1; ++i) { + if (i != 0) { + ss << ", "; + } + ss << words[i]; } - ss << ", and " << my_words.back(); + ss << ", and " << words.back(); break; } @@ -100,9 +103,8 @@ inline std::string Pretty::to_sentence(std::vector &words) { * @return a human-readable representation of the object (as a list) */ template -inline std::string Pretty::to_sentence(T &item) { - std::vector v = {item}; - return to_sentence(v); +inline std::string Pretty::to_sentence(const T &item) { + return to_sentence(std::vector{item}); } /** @@ -117,24 +119,24 @@ inline std::string Pretty::to_sentence(T &item) { * @return the string representation */ template -inline std::string Pretty::to_word(T &item) { - std::stringstream ss; - ss << item; - return ss.str(); +inline std::string Pretty::to_word(const T &item) { + std::ostringstream oss; + oss << item; // use operator<< to convert and append + return oss.str(); } template <> -inline std::string Pretty::to_word(bool &item) { +inline std::string Pretty::to_word(const bool &item) { return item ? "true" : "false"; } template <> -inline std::string Pretty::to_word(std::true_type & /* item */) { +inline std::string Pretty::to_word(const std::true_type & /* item */) { return "true"; } template <> -inline std::string Pretty::to_word(std::false_type & /* item */) { +inline std::string Pretty::to_word(const std::false_type & /* item */) { return "false"; } @@ -146,7 +148,7 @@ inline std::string Pretty::to_word(std::false_type & /* item */ * @return the string representation */ template -inline std::string Pretty::to_word(T &item) { +inline std::string Pretty::to_word(const T &item) { std::stringstream ss; // Ruby-style inspect for objects without an overloaded operator<< ss << "#<" << Util::demangle(typeid(item).name()) << ":" << &item << ">"; @@ -165,7 +167,7 @@ inline std::string Pretty::to_word(T &item) { * @return a string representation of the Matcher */ template -inline std::string Pretty::to_word(Matchers::MatcherBase &matcher) { +inline std::string Pretty::to_word(const Matchers::MatcherBase &matcher) { std::string description = matcher.description(); if (description.empty()) { return "[No description]"; @@ -174,7 +176,7 @@ inline std::string Pretty::to_word(Matchers::MatcherBase &matcher) { } template -inline std::string Pretty::to_word_type(T &item) { +inline std::string Pretty::to_word_type(const T &item) { std::string word = to_word(item); if constexpr (Util::is_streamable) { word += " : " + Util::demangle(typeid(T).name()); @@ -197,7 +199,7 @@ inline std::string Pretty::underscore(const std::string &word) { std::string str = std::regex_replace(word, std::regex("([A-Z]+)([A-Z][a-z])"), "$1_$2"); str = std::regex_replace(str, std::regex("([a-z\\d])([A-Z])"), "$1_$2"); str = std::regex_replace(str, std::regex("-"), "_"); - std::transform(str.begin(), str.end(), str.begin(), ::tolower); + std::ranges::transform(str, str.begin(), ::tolower); return str; } @@ -225,10 +227,8 @@ inline std::string Pretty::improve_hash_formatting(const std::string &inspect_st * @return the generated string */ template -inline std::string Pretty::inspect_object(O &o) { - std::stringstream ss; - ss << "(" << Util::demangle(typeid(o).name()) << ") =>" << to_sentence(o); - return ss.str(); +inline std::string Pretty::inspect_object(const O &o) { + return std::format("({}) => {}", Util::demangle(typeid(o).name()), to_word(o)); } /** @@ -239,10 +239,8 @@ inline std::string Pretty::inspect_object(O &o) { * @return the generated string */ template <> -inline std::string Pretty::inspect_object(const char *&o) { - std::stringstream ss; - ss << "(const char *) => " << '"' << o << '"'; - return ss.str(); +inline std::string Pretty::inspect_object(const char *const &o) { + return std::format("(const char *) => \"{}\"", o); } /** @@ -253,10 +251,13 @@ inline std::string Pretty::inspect_object(const char *&o) { * @return the generated string */ template <> -inline std::string Pretty::inspect_object(std::string &o) { - std::stringstream ss; - ss << "(std::string) => " << '"' << o << '"'; - return ss.str(); +inline std::string Pretty::inspect_object(const std::string &o) { + return std::format("(std::string) => \"{}\"", o); +} + +template <> +inline std::string Pretty::inspect_object(const std::string_view &o) { + return std::format("(std::string_view) => \"{}\"", o); } } // namespace CppSpec diff --git a/include/matchers/satisfy.hpp b/include/matchers/satisfy.hpp index 88100fa..5fa8a4e 100644 --- a/include/matchers/satisfy.hpp +++ b/include/matchers/satisfy.hpp @@ -24,32 +24,21 @@ class Satisfy : public MatcherBase //, BeHelpers> public: Satisfy(Expectation &expectation, std::function test) : MatcherBase(expectation), test(test) {} - std::string description() override; std::string failure_message() override; std::string failure_message_when_negated() override; + std::string verb() override { return "be"; } bool match() override; }; -template -std::string Satisfy::description() { - std::stringstream ss; - ss << "be" << Pretty::to_sentence(this->expected()); - return ss.str(); -} - template std::string Satisfy::failure_message() { - std::stringstream ss; - ss << "expected " << MatcherBase::actual() << " to evaluate to true"; - return ss.str(); + return std::format("expected {} to evaluate to true", MatcherBase::actual()); } template std::string Satisfy::failure_message_when_negated() { - std::stringstream ss; - ss << "expected " << MatcherBase::actual() << " to evaluate to false"; - return ss.str(); + return std::format("expected {} to evaluate to false", MatcherBase::actual()); } template diff --git a/include/matchers/strings/end_with.hpp b/include/matchers/strings/end_with.hpp index ae1c076..b1ba3c5 100644 --- a/include/matchers/strings/end_with.hpp +++ b/include/matchers/strings/end_with.hpp @@ -13,7 +13,7 @@ class EndWith : public MatcherBase { public: EndWith(Expectation &expectation, E start) : MatcherBase(expectation, start) {} - std::string description() override { return "end with " + Pretty::to_word(this->expected()); } + std::string verb() override { return "end with"; } bool match() override { A &actual = this->actual(); diff --git a/include/matchers/strings/match.hpp b/include/matchers/strings/match.hpp index 158b04f..cf140d1 100644 --- a/include/matchers/strings/match.hpp +++ b/include/matchers/strings/match.hpp @@ -17,7 +17,7 @@ class Match : MatcherBase { explicit Match(Expectation &expectation, std::regex expected) : MatcherBase(expectation, expected) {} - std::string description() override { return "match " + Pretty::to_word(this->expected()); } + std::string verb() override { return "match"; } bool match() override { std::smatch temp_match; diff --git a/include/matchers/strings/start_with.hpp b/include/matchers/strings/start_with.hpp index f8cc2d7..7c92c40 100644 --- a/include/matchers/strings/start_with.hpp +++ b/include/matchers/strings/start_with.hpp @@ -13,7 +13,7 @@ class StartWith : public MatcherBase { public: StartWith(Expectation &expectation, E start) : MatcherBase(expectation, start) {} - std::string description() override { return "start with " + Pretty::to_word(this->expected()); } + std::string verb() override { return "start with"; } bool match() override { A &actual = this->actual(); diff --git a/include/runnable.hpp b/include/runnable.hpp index e4fddd7..e8e1a88 100755 --- a/include/runnable.hpp +++ b/include/runnable.hpp @@ -1,12 +1,8 @@ -/** - * @file - */ #pragma once - #include "child.hpp" -#include "result.hpp" namespace CppSpec { +class Result; /** * @brief Abstract base class for executable objects diff --git a/spec/matchers/be_within_spec.cpp b/spec/matchers/be_within_spec.cpp index 49cc4c2..d9b4187 100644 --- a/spec/matchers/be_within_spec.cpp +++ b/spec/matchers/be_within_spec.cpp @@ -1,5 +1,7 @@ #include + #include "cppspec.hpp" +#include "formatters/formatters_base.hpp" using namespace CppSpec; @@ -11,6 +13,7 @@ using namespace CppSpec; * to the RSpec team from 2005-2016 */ +// clang-format off describe be_within_spec("expect(actual).to_be_within(delta).of(expected)", $ { it("passes when actual == expected", _ { expect(5.0).to_be_within(0.5).of(5.0); @@ -57,10 +60,9 @@ describe be_within_spec("expect(actual).to_be_within(delta).of(expected)", $ { // }); it("provides a description", _ { - auto d = 5.1; - ExpectationValue ex(self, d, std::source_location::current()); - Matchers::BeWithin matcher(ex, 0.5); - matcher.of(5.0); + double d = 5.1; + ExpectationValue ex(self, d, std::source_location::current()); + Matchers::BeWithin matcher = Matchers::BeWithinHelper(ex, 0.5).of(5.0); expect(matcher.description()).to_equal("be within 0.5 of 5"); }); @@ -71,22 +73,17 @@ describe be_within_spec("expect(actual).to_be_within(delta).of(expected)", $ { expect(11).to_be_within(10).percent_of(10); }); - // TODO: PEDNING - // it("fails when actual is outside the given percent variance", _ { - // expect([]{ - // expect(8.9).to_be_within(10).percent_of(10.0)(); - // }).to_fail_with("expected 8.9 to be within 10% of 10.0")(); - - // expect([]{ - // expect(11.1).to_be_within(10).percent_of(10.0)(); - // }).to_fail_with("expected 11.1 to be within 10% of 10.0")(); - // }); + it("fails when actual is outside the given percent variance", _ { + auto base_formatter = Formatters::BaseFormatter(); + auto ex = ExpectationValue(self, 20.1, std::source_location::current()); + auto result = Matchers::BeWithinHelper(ex, 10).percent_of(10.0).run(base_formatter); + expect(result).to_fail_with("expected 20.1 to be within 10% of 10"); + }); it("provides a description", _ { auto d = 5.1; ExpectationValue ex(self, d, std::source_location::current()); - Matchers::BeWithin matcher(ex, 0.5); - matcher.percent_of(5.0); + Matchers::BeWithin matcher = Matchers::BeWithinHelper(ex, 0.5).percent_of(5.0); expect(matcher.description()).to_equal("be within 0.5% of 5"); }); }); From 1dc36f64a2a94732123dfdd7592dbdd3fd1cb56b Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Thu, 3 Apr 2025 18:25:25 -0400 Subject: [PATCH 04/33] WIP JUnitXML output support --- .clang-format | 2 +- .pre-commit-config.yaml | 11 + examples/sample/example_spec.cpp | 3 +- examples/sample/jasmine_intro.cpp | 3 +- include/argparse.hpp | 5 +- include/child.hpp | 159 -------------- include/class_description.hpp | 204 ++++++++++-------- include/cppspec.hpp | 18 +- include/description.hpp | 126 ++++++----- include/expectations/expectation.hpp | 213 +++++++++---------- include/expectations/handler.hpp | 15 +- include/formatters/formatters_base.hpp | 45 ++-- include/formatters/junit_xml.hpp | 207 ++++++++++++++++++ include/formatters/progress.hpp | 50 ++--- include/formatters/tap.hpp | 120 +++++------ include/formatters/term_colors.hpp | 1 - include/formatters/verbose.hpp | 51 ++--- include/it.hpp | 28 +-- include/it_base.hpp | 37 ++-- include/let.hpp | 10 +- include/matchers/be_nullptr.hpp | 4 +- include/matchers/contain.hpp | 11 +- include/matchers/equal.hpp | 3 +- include/matchers/errors/fail.hpp | 13 +- include/matchers/errors/have_error.hpp | 8 +- include/matchers/errors/have_value.hpp | 6 +- include/matchers/errors/throw.hpp | 6 +- include/matchers/matcher_base.hpp | 77 +++---- include/matchers/numeric/be_between.hpp | 2 +- include/matchers/numeric/be_greater_than.hpp | 4 +- include/matchers/numeric/be_less_than.hpp | 4 +- include/matchers/numeric/be_within.hpp | 20 +- include/matchers/pretty_matchers.hpp | 70 +++--- include/matchers/satisfy.hpp | 3 +- include/matchers/strings/end_with.hpp | 7 +- include/matchers/strings/match.hpp | 10 +- include/matchers/strings/start_with.hpp | 7 +- include/result.hpp | 85 +++++--- include/runnable.hpp | 170 +++++++++++++-- include/runner.hpp | 20 +- include/util.hpp | 23 +- spec/describe_a_spec.cpp | 14 +- spec/expectations/expectation_spec.cpp | 37 ++-- spec/matchers/be_within_spec.cpp | 4 +- 44 files changed, 1072 insertions(+), 844 deletions(-) create mode 100644 .pre-commit-config.yaml delete mode 100644 include/child.hpp create mode 100644 include/formatters/junit_xml.hpp diff --git a/.clang-format b/.clang-format index bfd118e..b0522ba 100644 --- a/.clang-format +++ b/.clang-format @@ -1,2 +1,2 @@ -BasedOnStyle: Google +BasedOnStyle: Chromium ColumnLimit: 120 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..941be93 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +repos: + - repo: https://github.com/rhysd/actionlint + rev: v1.7.7 + hooks: + - id: actionlint + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v20.1.0 + hooks: + - id: clang-format + types_or: [c, c++] + files: 'include/.*(? #include #include "cppspec.hpp" -#include "formatters/verbose.hpp" describe bool_spec("Some Tests", $ { context("true is", _ { @@ -219,5 +218,5 @@ int main(int argc, char **argv){ .add_spec(let_spec) .add_spec(list_spec) .add_spec(expectation_spec) - .exec() ? EXIT_SUCCESS : EXIT_FAILURE; + .exec().is_success() ? EXIT_SUCCESS : EXIT_FAILURE; } diff --git a/examples/sample/jasmine_intro.cpp b/examples/sample/jasmine_intro.cpp index 035db21..3589d50 100644 --- a/examples/sample/jasmine_intro.cpp +++ b/examples/sample/jasmine_intro.cpp @@ -1,7 +1,6 @@ // Copyright 2016 Katherine Whitlock #include "cppspec.hpp" -#include "formatters/verbose.hpp" describe a_suite("A suite", $ { it("contains a spec with an expectation", _ { @@ -266,5 +265,5 @@ int main(int argc, char** argv) { .add_spec(a_spec_before_each) .add_spec(a_spec_nesting) .add_spec(to_include_matcher) - .exec() ? EXIT_SUCCESS : EXIT_FAILURE; + .exec().is_success() ? EXIT_SUCCESS : EXIT_FAILURE; } diff --git a/include/argparse.hpp b/include/argparse.hpp index d42a97f..648c64e 100644 --- a/include/argparse.hpp +++ b/include/argparse.hpp @@ -3,6 +3,7 @@ #include #include +#include "formatters/junit_xml.hpp" #include "formatters/progress.hpp" #include "formatters/tap.hpp" #include "formatters/verbose.hpp" @@ -30,7 +31,7 @@ inline Runner parse(int argc, char** const argv) { program.add_argument("-f", "--format") .default_value(std::string{"p"}) - .choices("progress", "p", "tap", "t", "detail", "d") + .choices("progress", "p", "tap", "t", "detail", "d", "junit", "j") .required() .help("set the output format"); @@ -53,6 +54,8 @@ inline Runner parse(int argc, char** const argv) { opts.formatter = std::make_unique(); } else if (format_string == "t" || format_string == "tap") { opts.formatter = std::make_unique(); + } else if (format_string == "j" || format_string == "junit") { + opts.formatter = std::make_unique(); } else { std::cerr << "Unrecognized format type" << std::endl; std::exit(-1); diff --git a/include/child.hpp b/include/child.hpp deleted file mode 100644 index d1120e7..0000000 --- a/include/child.hpp +++ /dev/null @@ -1,159 +0,0 @@ -/** - * Copyright 2016 Katherine Whitlock - * - * @file child.hpp - * @brief Contains the base Child class. - * - * @author Katherine Whitlock (toroidalcode) - */ - -#pragma once - -#include -#include -#include -#include - -namespace CppSpec { - -namespace Formatters { -class BaseFormatter; // Forward declaration to allow reference -} - -/** - * @brief Base class for all objects in the execution tree. - * - * A base class for all objects that comprise some abstract structure - * with a nesting concept. Used to propogate ('pass') failures from leaf - * to root without exceptions (and/or code-jumping), thus allowing - * execution to continue virtually uninterrupted. - */ -class Child { - // The parent of this child - // We use a raw pointer here instead of the safer std::shared_ptr - // due to the way that tests are inherently constructed - // (`describe some_spec("a test", $ { ... });`). In order to use - // a shared pointer, each object that is set as the parent must be - // contained in a shared pointer. As tests are created by `describe ...`, - // the root object is not wrapped by a shared pointer. Attempting to create - // this shared pointer at some later time doesn't work, as it results in - // trying to delete the current object `this` once the pointer goes out of - // scope. Since children are always destroyed before their parents, this - // isn't a problem anyways. In addition, any structures that are children - // are allocated on the stack for speed reasons. - Child *parent = nullptr; - - // Represents whether the Child is healthy (has not failed). - // A Child is healthy if and only if all of its children are healthy. - // All instances of Child start out healthy. - bool status = true; - - // The global formatter when running the tests. Would typically point to one - // of the globally instantiated formatters in formatters.hpp - Formatters::BaseFormatter *formatter = nullptr; - - public: - // Default constructor/destructor - Child() = default; - virtual ~Child() = default; - - // Move constructor/operator - Child(Child &&) = default; - Child &operator=(Child &&) = default; - - // Copy constructor/operator - Child(const Child &) = default; - Child &operator=(const Child &) = default; - - // Custom constructors - static Child of(const Child &parent) noexcept { return Child().set_parent(&parent); } - - /*--------- Parent helper functions -------------*/ - - /** @brief Check to see if the Child has a parent. */ - bool has_parent() noexcept { return parent != nullptr; } - [[nodiscard]] bool has_parent() const noexcept { return parent != nullptr; } - - // TODO: Look in to making these references instead of pointer returns - /** @brief Get the Child's parent. */ - [[nodiscard]] Child *get_parent() const noexcept { return parent; } - - template - C get_parent_as() const noexcept { - return static_cast(get_parent()); - } - - /** @brief Set the Child's parent */ - Child set_parent(Child *parent) noexcept { - this->parent = parent; - return *this; - } - Child &set_parent(const Child *parent) noexcept { - this->parent = const_cast(parent); - return *this; - } - - /*--------- Formatter helper functions -----------*/ - // Check to see if the tree has a printer - [[nodiscard]] bool has_formatter() const noexcept; - - // Get the printer from the tree - [[nodiscard]] Formatters::BaseFormatter &get_formatter() const noexcept; - - void set_formatter(Formatters::BaseFormatter &formatter) { this->formatter = &formatter; } - - /*--------- Primary member functions -------------*/ - - /** @brief Get the status of the object (success/failure) */ - [[nodiscard]] bool get_status() const noexcept { return this->status; } - void failed() noexcept; // Report failure to the object. - - // Calculate the padding for printing this object - [[nodiscard]] std::string padding() const noexcept; -}; - -/*>>>>>>>>>>>>>>>>>>>> Child <<<<<<<<<<<<<<<<<<<<<<<<<*/ - -/** - * @brief Report failure to the object. - * - * This is propogated up the parent/child tree, so that when a child object - * fails, the parent object is immediately updated to reflect that as well. - */ -inline void Child::failed() noexcept { - this->status = false; - // propogates the failure up the tree - if (this->has_parent()) { - this->get_parent()->failed(); - } -} - -/** - * @brief Generate padding (indentation) fore the current object. - * @return A string of spaces for use in pretty-printing. - */ -inline std::string Child::padding() const noexcept { - return this->has_parent() ? this->get_parent()->padding() + " " : ""; -} - -inline bool Child::has_formatter() const noexcept { - if (this->formatter != nullptr) { - return true; - } - if (!this->has_parent()) { - return false; // base case; - } - return parent->has_formatter(); -} - -inline Formatters::BaseFormatter &Child::get_formatter() const noexcept { - if (this->formatter != nullptr) { - return *formatter; - } - if (!this->has_parent()) { - std::terminate(); - } - return parent->get_formatter(); -} - -} // namespace CppSpec diff --git a/include/class_description.hpp b/include/class_description.hpp index f059aaf..aff8e0a 100755 --- a/include/class_description.hpp +++ b/include/class_description.hpp @@ -1,5 +1,6 @@ /** @file */ #pragma once +#include #include #include "description.hpp" @@ -19,7 +20,7 @@ namespace CppSpec { */ template class ClassDescription : public Description { - using Block = std::function &)>; + using Block = std::function&)>; Block block; std::string type; @@ -31,56 +32,75 @@ class ClassDescription : public Description { // Constructor // if there's no explicit subject given, then use // the default constructor of the given type as the implicit subject. - ClassDescription(Block block) - : Description(), block(block), type(" : " + Util::demangle(typeid(T).name())), subject(T()) { + ClassDescription(Block block, std::source_location location = std::source_location::current()) + : block(block), type(" : " + Util::demangle(typeid(T).name())), subject(T()) { this->description = Pretty::to_word(subject); + this->set_location(location); } - ClassDescription(const char* description, Block block) : Description(description), block(block), subject(T()) {} + ClassDescription(const char* description, + Block block, + std::source_location location = std::source_location::current()) + : Description(location, description), block(block), subject(T()) {} - ClassDescription(T subject, Block block) - : Description(Pretty::to_word(subject)), + ClassDescription(T subject, Block block, std::source_location location = std::source_location::current()) + : Description(location, Pretty::to_word(subject)), block(block), type(" : " + Util::demangle(typeid(T).name())), subject(subject) {} - ClassDescription(const char* description, T subject, Block block) - : Description(description), block(block), subject(subject) {} + ClassDescription(const char* description, + T subject, + Block block, + std::source_location location = std::source_location::current()) + : Description(location, description), block(block), subject(subject) {} - ClassDescription(T &subject, Block block) - : Description(Pretty::to_word(subject)), + ClassDescription(T&& subject, Block block, std::source_location location = std::source_location::current()) + : Description(location, Pretty::to_word(subject)), block(block), type(" : " + Util::demangle(typeid(T).name())), - subject(subject) {} + subject(std::move(subject)) {} template - ClassDescription(std::initializer_list init_list, Block block) - : block(block), type(" : " + Util::demangle(typeid(T).name())), subject(T(init_list)) { - this->description = Pretty::to_word(subject); - } + ClassDescription(std::initializer_list init_list, + Block block, + std::source_location location = std::source_location::current()) + : Description(location, Pretty::to_word(subject)), + block(block), + type(" : " + Util::demangle(typeid(T).name())), + subject(T(init_list)) {} template - ClassDescription(const char* description, std::initializer_list init_list, Block block) - : Description(description), block(block), subject(T(init_list)) {} + ClassDescription(const char* description, + std::initializer_list init_list, + Block block, + std::source_location location = std::source_location::current()) + : Description(location, description), block(block), subject(T(init_list)) {} - Result it(const char* description, std::function &)> block); - Result it(std::function &)> block); - /** @brief an alias for it */ - Result specify(const char* description, std::function &)> block) { return it(description, block); } - /** @brief an alias for it */ - Result specify(std::function &)> block) { return it(block); } + ItCD& it(const char* description, + std::function&)> block, + std::source_location location = std::source_location::current()); + ItCD& it(std::function&)> block, std::source_location location = std::source_location::current()); template - Result context(const char* description, B block); + ClassDescription& context(const char* description, + B block, + std::source_location location = std::source_location::current()); template - Result context(const char* description, U subject, B block); + ClassDescription& context(const char* description, + U subject, + B block, + std::source_location location = std::source_location::current()); template - Result context(const char* description, U &subject, B block); + ClassDescription& context(const char* description, + U& subject, + B block, + std::source_location location = std::source_location::current()); template - Result context(U subject, B block); + ClassDescription& context(U subject, B block, std::source_location location = std::source_location::current()); - Result run(Formatters::BaseFormatter &printer) override; + void run() override; [[nodiscard]] std::string get_subject_type() const noexcept override { return type; } }; @@ -90,64 +110,82 @@ using ClassContext = ClassDescription; template template -Result ClassDescription::context(const char* description, U subject, - B block) { - ClassContext context(description, subject, block); - context.set_parent(this); - context.ClassContext::before_eaches = this->before_eaches; - context.ClassContext::after_eaches = this->after_eaches; - return context.run(this->get_formatter()); +ClassContext& ClassDescription::context(const char* description, + U subject, + B block, + std::source_location location) { + auto* context = new ClassContext(description, subject, block, location); + context->set_parent(this); + context->ClassContext::before_eaches = this->before_eaches; + context->ClassContext::after_eaches = this->after_eaches; + context->timed_run(); + return *context; } template template -Result ClassDescription::context(U subject, B block) { - return this->context("", std::forward(subject), block); +ClassContext& ClassDescription::context(U subject, B block, std::source_location location) { + return this->context("", std::forward(subject), block, location); } template template -Result ClassDescription::context(const char* description, U &subject, - B block) { - ClassContext context(description, subject, block); - context.set_parent(this); - context.ClassContext::before_eaches = this->before_eaches; - context.ClassContext::after_eaches = this->after_eaches; - return context.run(this->get_formatter()); +ClassContext& ClassDescription::context(const char* description, + U& subject, + B block, + std::source_location location) { + auto* context = new ClassContext(description, subject, block, location); + context->set_parent(this); + context->ClassContext::before_eaches = this->before_eaches; + context->ClassContext::after_eaches = this->after_eaches; + context->timed_run(); + return *context; } template template -Result ClassDescription::context(const char* description, B block) { - ClassContext context(description, this->subject, block); - context.set_parent(this); - context.before_eaches = this->before_eaches; - context.after_eaches = this->after_eaches; - return context.run(this->get_formatter()); +ClassContext& ClassDescription::context(const char* description, B block, std::source_location location) { + auto* context = new ClassContext(description, this->subject, block, location); + context->set_parent(this); + context->before_eaches = this->before_eaches; + context->after_eaches = this->after_eaches; + context->timed_run(); + return *context; } template -requires (!std::is_same_v) -Result Description::context(T subject, B block) { - return this->context("", subject, block); + requires(!std::is_same_v) +ClassContext& Description::context(T subject, B block, std::source_location location) { + auto* context = new ClassContext(subject, block, location); + context->set_parent(this); + context->set_location(location); + context->before_eaches = this->before_eaches; + context->after_eaches = this->after_eaches; + context->timed_run(); + return *context; } template -Result Description::context(const char* description, T subject, B block) { - ClassContext context(description, subject, block); - context.set_parent(this); - context.before_eaches = this->before_eaches; - context.after_eaches = this->after_eaches; - return context.run(this->get_formatter()); +ClassContext& Description::context(const char* description, T subject, B block, std::source_location location) { + auto* context = new ClassContext(description, subject, block, location); + context->set_parent(this); + context->set_location(location); + context->before_eaches = this->before_eaches; + context->after_eaches = this->after_eaches; + context->timed_run(); + return *context; } template -Result Description::context(std::initializer_list init_list, std::function &)> block) { - ClassContext context(T(init_list), block); - context.set_parent(this); - context.before_eaches = this->before_eaches; - context.after_eaches = this->after_eaches; - return context.run(this->get_formatter()); +ClassContext& Description::context(std::initializer_list init_list, + std::function&)> block, + std::source_location location) { + auto* context = new ClassContext(T(init_list), block, location); + context->set_parent(this); + context->before_eaches = this->before_eaches; + context->after_eaches = this->after_eaches; + context->timed_run(); + return *context; } /** @@ -172,12 +210,12 @@ Result Description::context(std::initializer_list init_list, std::function -Result ClassDescription::it(const char* name, std::function &)> block) { - ItCD it(*this, this->subject, name, block); - Result result = it.run(this->get_formatter()); +ItCD& ClassDescription::it(const char* name, std::function&)> block, std::source_location location) { + auto* it = new ItCD(*this, location, this->subject, name, block); + it->timed_run(); exec_after_eaches(); exec_before_eaches(); - return result; + return *it; } /** @@ -202,44 +240,34 @@ Result ClassDescription::it(const char* name, std::function &)> * @return the result of the test */ template -Result ClassDescription::it(std::function &)> block) { - ItCD it(*this, this->subject, block); - Result result = it.run(this->get_formatter()); +ItCD& ClassDescription::it(std::function&)> block, std::source_location location) { + auto* it = new ItCD(*this, location, this->subject, block); + it->timed_run(); exec_after_eaches(); exec_before_eaches(); - return result; + return *it; } template -Result ClassDescription::run(Formatters::BaseFormatter &printer) { - if (not this->has_formatter()) { - this->set_formatter(printer); - } - printer.format(*this); +void ClassDescription::run() { this->block(*this); - for (const auto &a : after_alls) { + for (const auto& a : after_alls) { a(); } - if (this->get_parent() == nullptr) { - printer.flush(); - } - return this->get_status() ? Result::success() : Result::failure(); } template ExpectationValue ItCD::is_expected() { - auto cd = static_cast *>(this->get_parent()); + auto cd = this->get_parent_as>(); ExpectationValue expectation(*this, cd->subject, std::source_location::current()); return expectation; } template -Result ItCD::run(Formatters::BaseFormatter &printer) { +void ItCD::run() { this->block(*this); - printer.format(*this); - auto cd = static_cast *>(this->get_parent()); + auto cd = this->get_parent_as>(); cd->reset_lets(); - return this->get_status() ? Result::success() : Result::failure(); } } // namespace CppSpec diff --git a/include/cppspec.hpp b/include/cppspec.hpp index 362f398..9528036 100755 --- a/include/cppspec.hpp +++ b/include/cppspec.hpp @@ -5,6 +5,7 @@ #pragma once #include "argparse.hpp" +#include "class_description.hpp" #ifndef CPPSPEC_MACROLESS /*>>>>>>>>>>>>>>>>>>>> MACROS <<<<<<<<<<<<<<<<<<<<<<*/ @@ -12,8 +13,8 @@ // For *some* reason, MSVC++ refuses to correctly deduce the types of // Description blocks unless the void return type is explicitly stated. // GCC and clang have no problem with it being omitted. Weird. -#define $ [](auto &self) -> void -#define _ [=](auto &self) mutable -> void +#define $ [](auto& self) -> void +#define _ [=](auto& self) mutable -> void #define it self.it #define specify it @@ -32,14 +33,14 @@ #define after_each self.after_each #define let(name, body) auto(name) = self.let(body); -#define CPPSPEC_MAIN(spec) \ - int main(int argc, char **const argv) { \ - return CppSpec::parse(argc, argv).add_spec(spec).exec() ? EXIT_SUCCESS : EXIT_FAILURE; \ +#define CPPSPEC_MAIN(spec) \ + int main(int argc, char** const argv) { \ + return CppSpec::parse(argc, argv).add_spec(spec).exec().is_success() ? EXIT_SUCCESS : EXIT_FAILURE; \ } -#define CPPSPEC_SPEC(spec_name) \ - int spec_name##_spec(int argc, char **const argv) { \ - return CppSpec::parse(argc, argv).add_spec(spec_name).exec() ? EXIT_SUCCESS : EXIT_FAILURE; \ +#define CPPSPEC_SPEC(spec_name) \ + int spec_name##_spec(int argc, char** const argv) { \ + return CppSpec::parse(argc, argv).add_spec(spec_name).exec().is_success() ? EXIT_SUCCESS : EXIT_FAILURE; \ } #endif @@ -52,4 +53,3 @@ using describe_a = CppSpec::ClassDescription; template using describe_an = CppSpec::ClassDescription; - diff --git a/include/description.hpp b/include/description.hpp index 5b70f5a..b2a0951 100755 --- a/include/description.hpp +++ b/include/description.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -20,10 +21,10 @@ class Description : public Runnable { using VoidBlock = std::function; public: - using Block = std::function; + using Block = std::function; const bool has_subject = false; - std::forward_list lets{}; + std::forward_list lets{}; std::deque after_alls{}; std::deque before_eaches{}; std::deque after_eaches{}; @@ -34,48 +35,48 @@ class Description : public Runnable { protected: std::string description; - // These two constructors are the most basic ones, - // used to create Descriptions with only their description - // field initialized. They should only be used by subclasses - // of Description. - Description() = default; - explicit Description(std::string&& description) noexcept : description(std::move(description)) {} - explicit Description(const char* description) noexcept : description(description) {} + Description(std::source_location location, std::string&& description) noexcept + : Runnable(location), description(std::move(description)) {} - Description(const Child &parent, const char* description, Block block) noexcept - : Runnable(parent), block(std::move(block)), description(description) {} + Description(Runnable& parent, std::source_location location, const char* description, Block block) noexcept + : Runnable(parent, location), block(block), description(description) {} void exec_before_eaches(); void exec_after_eaches(); public: - // Copy constructor - Description(const Description ©) = default; - Description(Description &©) = default; - // Primary constructor. Entry of all specs. - Description(const char* description, Block block) noexcept - : block(std::move(block)), description(std::move(description)) {} + Description(const char* description, + Block block, + std::source_location location = std::source_location::current()) noexcept + : Runnable(location), block(block), description(description) { + this->set_location(location); + } /********* Specify/It *********/ - Result it(const char* description, ItD::Block body); - Result it(ItD::Block body); + ItD& it(const char* description, ItD::Block body, std::source_location location = std::source_location::current()); + ItD& it(ItD::Block body, std::source_location location = std::source_location::current()); /********* Context ***********/ template - Result context(const char* name, Block body); + Description& context(const char* name, Block body, std::source_location location = std::source_location::current()); template - requires (!std::is_same_v) - Result context(T subject, B block); + requires(!std::is_same_v) + ClassDescription& context(T subject, B block, std::source_location location = std::source_location::current()); template - Result context(const char* description, T subject, B block); + ClassDescription& context(const char* description, + T subject, + B block, + std::source_location location = std::source_location::current()); template - Result context(std::initializer_list init_list, std::function &)> block); + ClassDescription& context(std::initializer_list init_list, + std::function&)> block, + std::source_location location = std::source_location::current()); /********* Each/All *********/ @@ -97,7 +98,7 @@ class Description : public Runnable { /********* Run *********/ - Result run(Formatters::BaseFormatter &printer) override; + void run() override; // std::function template inline auto as_main(); @@ -109,30 +110,31 @@ using Context = Description; /*========= Description::it =========*/ -inline Result Description::it(const char* description, ItD::Block block) { - ItD it(*this, description, block); - Result result = it.run(this->get_formatter()); +inline ItD& Description::it(const char* description, ItD::Block block, std::source_location location) { + auto* it = new ItD(*this, location, description, block); + it->timed_run(); exec_after_eaches(); exec_before_eaches(); - return result; + return *it; } -inline Result Description::it(ItD::Block block) { - ItD it(*this, block); - Result result = it.run(this->get_formatter()); +inline ItD& Description::it(ItD::Block block, std::source_location location) { + auto* it = new ItD(*this, location, block); + it->timed_run(); exec_after_eaches(); exec_before_eaches(); - return result; + return *it; } /*========= Description::context =========*/ template -inline Result Description::context(const char* description, Block body) { - Context context(*this, description, body); - context.before_eaches = this->before_eaches; - context.after_eaches = this->after_eaches; - return context.run(this->get_formatter()); +inline Context& Description::context(const char* description, Block body, std::source_location location) { + auto* context = new Context(*this, location, description, body); + context->before_eaches = this->before_eaches; + context->after_eaches = this->after_eaches; + context->timed_run(); + return *context; } /*========= Description:: each/alls =========*/ @@ -149,20 +151,28 @@ inline void Description::before_each(VoidBlock b) { b(); } -inline void Description::before_all(VoidBlock b) { b(); } +inline void Description::before_all(VoidBlock b) { + b(); +} -inline void Description::after_each(VoidBlock b) { after_eaches.push_back(b); } +inline void Description::after_each(VoidBlock b) { + after_eaches.push_back(b); +} -inline void Description::after_all(VoidBlock b) { after_alls.push_back(b); } +inline void Description::after_all(VoidBlock b) { + after_alls.push_back(b); +} /*----------- private -------------*/ inline void Description::exec_before_eaches() { - for (VoidBlock &b : before_eaches) b(); + for (VoidBlock& b : before_eaches) + b(); } inline void Description::exec_after_eaches() { - for (VoidBlock &b : after_eaches) b(); + for (VoidBlock& b : after_eaches) + b(); } /*========= Description::let =========*/ @@ -190,42 +200,30 @@ auto Description::let(T block) -> Let { // TODO: Should this be protected? inline void Description::reset_lets() noexcept { // For every let in our list, reset it. - for (auto &let : lets) let->reset(); + for (auto& let : lets) + let->reset(); // Recursively reset all the lets in the family tree if (this->has_parent()) { - this->get_parent_as()->reset_lets(); + this->get_parent_as()->reset_lets(); } } /*========= Description::run =========*/ -inline Result Description::run(Formatters::BaseFormatter &formatter) { - // If there isn't already a formatter in the family tree, set ours. - if (!this->has_formatter()) { - this->set_formatter(formatter); - } - - formatter.format(*this); // Format our description in some way - block(*this); // Run the block - for (VoidBlock &a : after_alls) a(); // Run all our after_alls - if (!this->has_parent()) { - formatter.flush(); // Inform the printer we're done - } - - // Return success or failure - return this->get_status() ? Result::success() : Result::failure(); +inline void Description::run() { + block(*this); // Run the block + for (VoidBlock& a : after_alls) + a(); // Run all our after_alls } /*>>>>>>>>>>>>>>>>>>>> ItD <<<<<<<<<<<<<<<<<<<<<<<<<*/ /*========= ItD::run =========*/ -inline Result ItD::run(Formatters::BaseFormatter &printer) { +inline void ItD::run() { block(*this); - printer.format(*this); - this->get_parent_as()->reset_lets(); - return this->get_status() ? Result::success() : Result::failure(); + this->get_parent_as()->reset_lets(); } } // namespace CppSpec diff --git a/include/expectations/expectation.hpp b/include/expectations/expectation.hpp index c241581..4e7b593 100755 --- a/include/expectations/expectation.hpp +++ b/include/expectations/expectation.hpp @@ -1,8 +1,3 @@ -/** - * @file - * @brief Defines the Expectation class and associated functions - */ - #pragma once #include @@ -48,13 +43,18 @@ namespace CppSpec { * to the Matcher object, or chain in a builder-like fashion. */ template -class Expectation : public Child { +class Expectation { + ItBase* it = nullptr; + std::source_location location; + protected: bool is_positive_ = true; // Have we been negated? bool ignore_failure_ = false; public: + Expectation() = default; + /** * @brief Create an Expectation using a value. * @@ -62,11 +62,14 @@ class Expectation : public Child { * * @return The constructed Expectation. */ - explicit Expectation(ItBase &it) : Child(Child::of(it)) {} + explicit Expectation(ItBase& it, std::source_location location) : it(&it), location(location) {} /** @brief Get the target of the expectation. */ // virtual const A &get_target() const & { return target; } - virtual A &get_target() & = 0; + virtual A& get_target() & = 0; + + ItBase* get_it() const { return it; } + std::source_location get_location() const { return location; } /** @brief Get whether the expectation is normal or negated. */ [[nodiscard]] constexpr bool sign() const { return is_positive_; } @@ -74,64 +77,64 @@ class Expectation : public Child { /********* Modifiers *********/ - virtual Expectation ¬_() = 0; - virtual Expectation &ignore() = 0; + virtual Expectation& not_() = 0; + virtual Expectation& ignore() = 0; /********* Matchers *********/ template - Result to(M matcher, std::string msg = ""); + void to(M matcher, std::string msg = ""); /*-------- to be... ----------*/ - Result to_be_false(std::string msg = ""); - Result to_be_falsy(std::string msg = ""); - Result to_be_null(std::string msg = ""); - Result to_be_true(std::string msg = ""); - Result to_be_truthy(std::string msg = ""); + void to_be_false(std::string msg = ""); + void to_be_falsy(std::string msg = ""); + void to_be_null(std::string msg = ""); + void to_be_true(std::string msg = ""); + void to_be_truthy(std::string msg = ""); template - Result to_be_between(E min, E max, Matchers::RangeMode mode = Matchers::RangeMode::inclusive, std::string msg = ""); + void to_be_between(E min, E max, Matchers::RangeMode mode = Matchers::RangeMode::inclusive, std::string msg = ""); template - Result to_be_greater_than(E rhs, std::string msg = ""); + void to_be_greater_than(E rhs, std::string msg = ""); template - Result to_be_less_than(E rhs, std::string msg = ""); + void to_be_less_than(E rhs, std::string msg = ""); template Matchers::BeWithinHelper to_be_within(E expected, std::string msg = ""); /*-------- to... ----------*/ - Result to_end_with(std::string ending, std::string msg = ""); - Result to_fail(std::string msg = ""); - Result to_fail_with(std::string failure_message, std::string msg = ""); - Result to_match(std::regex regex, std::string msg = ""); - Result to_match(std::string str, std::string msg = ""); - Result to_partially_match(std::regex regex, std::string msg = ""); - Result to_partially_match(std::string str, std::string msg = ""); - Result to_satisfy(std::function /*test*/, std::string msg = ""); - Result to_start_with(std::string start, std::string msg = ""); + void to_end_with(std::string ending, std::string msg = ""); + void to_fail(std::string msg = ""); + void to_fail_with(std::string failure_message, std::string msg = ""); + void to_match(std::regex regex, std::string msg = ""); + void to_match(std::string str, std::string msg = ""); + void to_partially_match(std::regex regex, std::string msg = ""); + void to_partially_match(std::string str, std::string msg = ""); + void to_satisfy(std::function /*test*/, std::string msg = ""); + void to_start_with(std::string start, std::string msg = ""); template - Result to_contain(std::initializer_list expected, std::string msg = ""); + void to_contain(std::initializer_list expected, std::string msg = ""); template - Result to_contain(E expected, std::string msg = ""); + void to_contain(E expected, std::string msg = ""); template - Result to_end_with(std::initializer_list start, std::string msg = ""); + void to_end_with(std::initializer_list start, std::string msg = ""); template - Result to_equal(E expected, std::string msg = ""); + void to_equal(E expected, std::string msg = ""); template - Result to_start_with(std::initializer_list start, std::string msg = ""); + void to_start_with(std::initializer_list start, std::string msg = ""); - Result to_have_value(std::string msg = ""); + void to_have_value(std::string msg = ""); #if __cpp_lib_expected - Result to_have_error(std::string msg = ""); + void to_have_error(std::string msg = ""); #endif }; @@ -147,12 +150,12 @@ class Expectation : public Child { */ template template -Result Expectation::to(M matcher, std::string msg) { +void Expectation::to(M matcher, std::string msg) { static_assert(std::is_base_of_v, M>, "Matcher is not a subclass of BaseMatcher."); // auto base_matcher = static_cast>(matcher); - return matcher.set_message(msg).run(this->get_formatter()); + matcher.set_message(msg).run(); } /** @@ -164,10 +167,10 @@ Result Expectation::to(M matcher, std::string msg) { * @return */ template -Result Expectation::to_be_false(std::string msg) { +void Expectation::to_be_false(std::string msg) { static_assert(std::is_same_v, ".to_be_false() can only be used on booleans or functions that return booleans"); - return to_equal(false, msg); + to_equal(false, msg); } /** @@ -178,8 +181,8 @@ Result Expectation::to_be_false(std::string msg) { * @return */ template -Result Expectation::to_be_falsy(std::string msg) { - return to_satisfy([](const A &t) { return !static_cast(t); }, msg); +void Expectation::to_be_falsy(std::string msg) { + to_satisfy([](const A& t) { return !static_cast(t); }, msg); } /** @@ -190,8 +193,8 @@ Result Expectation::to_be_falsy(std::string msg) { * @return */ template -Result Expectation::to_be_null(std::string msg) { - return Matchers::BeNullptr(*this).set_message(msg).run(this->get_formatter()); +void Expectation::to_be_null(std::string msg) { + Matchers::BeNullptr(*this).set_message(msg).run(); } /** @@ -202,11 +205,11 @@ Result Expectation::to_be_null(std::string msg) { * @return */ template -Result Expectation::to_be_true(std::string msg) { +void Expectation::to_be_true(std::string msg) { static_assert(std::is_same_v, ".to_be_true() can only be used on booleans or functions that return booleans"); // return to_be([](A t) { return static_cast(t); }, msg); - return to_equal(true, msg); + to_equal(true, msg); } /** @@ -218,8 +221,8 @@ Result Expectation::to_be_true(std::string msg) { * @return */ template -Result Expectation::to_be_truthy(std::string msg) { - return to_satisfy([](const A &t) { return static_cast(t); }, msg); +void Expectation::to_be_truthy(std::string msg) { + to_satisfy([](const A& t) { return static_cast(t); }, msg); } /** @@ -234,20 +237,20 @@ Result Expectation::to_be_truthy(std::string msg) { */ template template -Result Expectation::to_be_between(E min, E max, Matchers::RangeMode mode, std::string msg) { - return Matchers::BeBetween(*this, min, max, mode).set_message(msg).run(this->get_formatter()); +void Expectation::to_be_between(E min, E max, Matchers::RangeMode mode, std::string msg) { + Matchers::BeBetween(*this, min, max, mode).set_message(msg).run(); } template template -Result Expectation::to_be_less_than(E rhs, std::string msg) { - return Matchers::BeLessThan(*this, rhs).set_message(msg).run(this->get_formatter()); +void Expectation::to_be_less_than(E rhs, std::string msg) { + Matchers::BeLessThan(*this, rhs).set_message(msg).run(); } template template -Result Expectation::to_be_greater_than(E rhs, std::string msg) { - return Matchers::BeGreaterThan(*this, rhs).set_message(msg).run(this->get_formatter()); +void Expectation::to_be_greater_than(E rhs, std::string msg) { + Matchers::BeGreaterThan(*this, rhs).set_message(msg).run(); } /** @@ -260,8 +263,8 @@ Result Expectation::to_be_greater_than(E rhs, std::string msg) { */ template template -Result Expectation::to_contain(std::initializer_list expected, std::string msg) { - return Matchers::Contain, U>(*this, expected).set_message(msg).run(this->get_formatter()); +void Expectation::to_contain(std::initializer_list expected, std::string msg) { + Matchers::Contain, U>(*this, expected).set_message(msg).run(); } /** @@ -274,8 +277,8 @@ Result Expectation::to_contain(std::initializer_list expected, std::string */ template template -Result Expectation::to_contain(E expected, std::string msg) { - return Matchers::Contain(*this, expected).set_message(msg).run(this->get_formatter()); +void Expectation::to_contain(E expected, std::string msg) { + Matchers::Contain(*this, expected).set_message(msg).run(); } /** @@ -288,8 +291,8 @@ Result Expectation::to_contain(E expected, std::string msg) { */ template template -Result Expectation::to_equal(E expected, std::string msg) { - return Matchers::Equal(*this, expected).set_message(msg).run(this->get_formatter()); +void Expectation::to_equal(E expected, std::string msg) { + Matchers::Equal(*this, expected).set_message(msg).run(); } /** @@ -309,35 +312,35 @@ Matchers::BeWithinHelper Expectation::to_be_within(E expected, std::str } template -Result Expectation::to_fail(std::string msg) { +void Expectation::to_fail(std::string msg) { static_assert(is_result_v, ".to_fail() must be used on an expression that returns a Result."); - return Matchers::Fail(*this).set_message(msg).run(this->get_formatter()); + Matchers::Fail(*this).set_message(msg).run(); } template -Result Expectation::to_fail_with(std::string failure_message, std::string msg) { +void Expectation::to_fail_with(std::string failure_message, std::string msg) { static_assert(is_result_v, ".to_fail_with() must be used on an expression that returns a Result."); - return Matchers::FailWith(*this, failure_message).set_message(msg).run(this->get_formatter()); + Matchers::FailWith(*this, failure_message).set_message(msg).run(); } template -Result Expectation::to_match(std::string str, std::string msg) { - return Matchers::Match(*this, str).set_message(msg).run(this->get_formatter()); +void Expectation::to_match(std::string str, std::string msg) { + Matchers::Match(*this, str).set_message(msg).run(); } template -Result Expectation::to_match(std::regex regex, std::string msg) { - return Matchers::Match(*this, regex).set_message(msg).run(this->get_formatter()); +void Expectation::to_match(std::regex regex, std::string msg) { + Matchers::Match(*this, regex).set_message(msg).run(); } template -Result Expectation::to_partially_match(std::string str, std::string msg) { - return Matchers::MatchPartial(*this, str).set_message(msg).run(this->get_formatter()); +void Expectation::to_partially_match(std::string str, std::string msg) { + Matchers::MatchPartial(*this, str).set_message(msg).run(); } template -Result Expectation::to_partially_match(std::regex regex, std::string msg) { - return Matchers::MatchPartial(*this, regex).set_message(msg).run(this->get_formatter()); +void Expectation::to_partially_match(std::regex regex, std::string msg) { + Matchers::MatchPartial(*this, regex).set_message(msg).run(); } /** @@ -350,52 +353,47 @@ Result Expectation::to_partially_match(std::regex regex, std::string msg) { * @return Whether the expectation succeeds or fails. */ template -Result Expectation::to_satisfy(std::function test, std::string msg) { - return Matchers::Satisfy(*this, test).set_message(msg).run(this->get_formatter()); +void Expectation::to_satisfy(std::function test, std::string msg) { + Matchers::Satisfy(*this, test).set_message(msg).run(); } template -Result Expectation::to_start_with(std::string start, std::string msg) { - return Matchers::StartWith(*this, start).set_message(msg).run(this->get_formatter()); +void Expectation::to_start_with(std::string start, std::string msg) { + Matchers::StartWith(*this, start).set_message(msg).run(); } template template -Result Expectation::to_start_with(std::initializer_list start_sequence, std::string msg) { - return Matchers::StartWith>(*this, start_sequence) - .set_message(msg) - .run(this->get_formatter()); +void Expectation::to_start_with(std::initializer_list start_sequence, std::string msg) { + Matchers::StartWith>(*this, start_sequence).set_message(msg).run(); } template -Result Expectation::to_end_with(std::string ending, std::string msg) { - return Matchers::EndWith(*this, ending).set_message(msg).run(this->get_formatter()); +void Expectation::to_end_with(std::string ending, std::string msg) { + Matchers::EndWith(*this, ending).set_message(msg).run(); } template template -Result Expectation::to_end_with(std::initializer_list start_sequence, std::string msg) { - return Matchers::StartWith>(*this, start_sequence) - .set_message(msg) - .run(this->get_formatter()); +void Expectation::to_end_with(std::initializer_list start_sequence, std::string msg) { + Matchers::StartWith>(*this, start_sequence).set_message(msg).run(); } template -Result Expectation::to_have_value(std::string msg) { - return Matchers::HaveValue(*this).set_message(msg).run(this->get_formatter()); +void Expectation::to_have_value(std::string msg) { + Matchers::HaveValue(*this).set_message(msg).run(); } #if __cpp_lib_expected template -Result Expectation::to_have_error(std::string msg) { - return Matchers::HaveError(*this).set_message(msg).run(this->get_formatter()); +void Expectation::to_have_error(std::string msg) { + Matchers::HaveError(*this).set_message(msg).run(); } #endif template class ExpectationValue : public Expectation { A value; - std::source_location location; public: /** @@ -405,8 +403,7 @@ class ExpectationValue : public Expectation { * * @return The constructed ExpectationValue. */ - ExpectationValue(ItBase &it, A value, std::source_location location) - : Expectation(it), value(value), location{location} {} + ExpectationValue(ItBase& it, A value, std::source_location location) : Expectation(it, location), value(value) {} /** * @brief Create an Expectation using an initializer list. @@ -416,18 +413,18 @@ class ExpectationValue : public Expectation { * @return The constructed Expectation. */ template - ExpectationValue(ItBase &it, std::initializer_list init_list, std::source_location location) - : Expectation(it), value(std::vector(init_list)), location{location} {} + ExpectationValue(ItBase& it, std::initializer_list init_list, std::source_location location) + : Expectation(it, location), value(std::vector(init_list)) {} /** @brief Get the target of the expectation. */ - A &get_target() & override { return value; } + A& get_target() & override { return value; } - ExpectationValue ¬_() override { + ExpectationValue& not_() override { this->is_positive_ = not this->is_positive_; return *this; } - ExpectationValue &ignore() override { + ExpectationValue& ignore() override { this->ignore_failure_ = true; return *this; } @@ -438,10 +435,10 @@ class ExpectationFunc : public Expectation()())> { using block_ret_t = decltype(std::declval()()); F block; std::shared_ptr computed = nullptr; - std::source_location location; public: - ExpectationFunc(ExpectationFunc const ©, std::source_location location) : Expectation(copy), block(copy.block), location{location} {} + ExpectationFunc(ExpectationFunc const& copy, std::source_location location) + : Expectation(copy, location), block(copy.block) {} /** * @brief Create an ExpectationValue using a value. @@ -450,7 +447,8 @@ class ExpectationFunc : public Expectation()())> { * * @return The constructed ExpectationValue. */ - ExpectationFunc(ItBase &it, F block, std::source_location location) : Expectation(it), block(block), location{location} {} + ExpectationFunc(ItBase& it, F block, std::source_location location) + : Expectation(it, location), block(block) {} /** * @brief Create an Expectation using a function. @@ -470,7 +468,7 @@ class ExpectationFunc : public Expectation()())> { // {} /** @brief Get the target of the expectation. */ - block_ret_t &get_target() & override { + block_ret_t& get_target() & override { if (computed == nullptr) { computed = std::make_shared(block()); } @@ -480,27 +478,26 @@ class ExpectationFunc : public Expectation()())> { // auto get_target() & override -> decltype(std::declval()()) & { return // Expectation::get_target()(); } - ExpectationFunc ¬_() override { + ExpectationFunc& not_() override { this->is_positive_ = !this->is_positive_; return *this; } - ExpectationFunc &ignore() override { + ExpectationFunc& ignore() override { this->ignore_failure_ = true; return *this; } - Expectation &casted() { return static_cast(*this); } + Expectation& casted() { return static_cast(*this); } template - Result to_throw(std::string msg = ""); + void to_throw(std::string msg = ""); }; template template -Result ExpectationFunc::to_throw(std::string msg) { - Matchers::Throwblock.operator()()), Ex> matcher(*this); - return matcher.set_message(msg).run(this->get_formatter()); +void ExpectationFunc::to_throw(std::string msg) { + Matchers::Throwblock.operator()()), Ex>(*this).set_message(msg).run(); } } // namespace CppSpec diff --git a/include/expectations/handler.hpp b/include/expectations/handler.hpp index 47fd1c7..a613d62 100755 --- a/include/expectations/handler.hpp +++ b/include/expectations/handler.hpp @@ -13,20 +13,19 @@ #include "result.hpp" - namespace CppSpec { /** @brief Handles "positive" expectations (i.e. non-negated) */ struct PositiveExpectationHandler { template - static Result handle_matcher(Matcher &matcher); + static Result handle_matcher(Matcher& matcher); static std::string verb() { return "should"; } }; /** @brief Handles "negative" expectations (i.e. negated with '.not_() */ struct NegativeExpectationHandler { template - static Result handle_matcher(Matcher &matcher); + static Result handle_matcher(Matcher& matcher); static std::string verb() { return "should not"; } }; @@ -39,9 +38,10 @@ struct NegativeExpectationHandler { * @return the Result of the expectation */ template -Result PositiveExpectationHandler::handle_matcher(Matcher &matcher) { +Result PositiveExpectationHandler::handle_matcher(Matcher& matcher) { // TODO: handle expectation failure here - return !matcher.match() ? Result::failure_with(matcher.failure_message()) : Result::success(); + return !matcher.match() ? Result::failure_with(matcher.get_location(), matcher.failure_message()) + : Result::success(matcher.get_location()); } /** @@ -53,9 +53,10 @@ Result PositiveExpectationHandler::handle_matcher(Matcher &matcher) { * @return the Result of the expectation */ template -Result NegativeExpectationHandler::handle_matcher(Matcher &matcher) { +Result NegativeExpectationHandler::handle_matcher(Matcher& matcher) { // TODO: handle expectation failure here - return !matcher.negated_match() ? Result::failure_with(matcher.failure_message_when_negated()) : Result::success(); + return !matcher.negated_match() ? Result::failure_with(matcher.get_location(), matcher.failure_message_when_negated()) + : Result::success(matcher.get_location()); } } // namespace CppSpec diff --git a/include/formatters/formatters_base.hpp b/include/formatters/formatters_base.hpp index 1c8d880..1015cf3 100644 --- a/include/formatters/formatters_base.hpp +++ b/include/formatters/formatters_base.hpp @@ -3,7 +3,10 @@ #include #include -#include + +#include "description.hpp" +#include "it_base.hpp" +#include "runnable.hpp" extern "C" { #ifdef _WIN32 @@ -28,16 +31,16 @@ namespace Formatters { class BaseFormatter { protected: - std::ostream &out_stream; + std::ostream& out_stream; int test_counter = 1; bool multiple = false; bool color_output; public: - explicit BaseFormatter(std::ostream &out_stream = std::cout, bool color = is_terminal()) + explicit BaseFormatter(std::ostream& out_stream = std::cout, bool color = is_terminal()) : out_stream(out_stream), color_output(color) {} - BaseFormatter(const BaseFormatter &) = default; - BaseFormatter(const BaseFormatter ©, std::ostream &out_stream) + BaseFormatter(const BaseFormatter&) = default; + BaseFormatter(const BaseFormatter& copy, std::ostream& out_stream) : out_stream(out_stream), test_counter(copy.test_counter), multiple(copy.multiple), @@ -45,26 +48,40 @@ class BaseFormatter { virtual ~BaseFormatter() = default; - virtual void format(const Description & /* description */) {} - virtual void format(const ItBase & /* it */) {} - virtual void format(const std::string &message) { out_stream << message << std::endl; } - virtual void format_failure(const std::string & /* message */) {} - virtual void flush() {} + void format(Runnable& runnable) { + if (Description* description = dynamic_cast(&runnable)) { + format(*description); + } else if (ItBase* it = dynamic_cast(&runnable)) { + format(*it); + } + format_children(runnable); + } + + void format_children(Runnable& runnable) { + for (auto child : runnable.get_children()) { + if (Runnable* runnable = dynamic_cast(child)) { + this->format(*runnable); + } + } + } + + virtual void format(Description& description) {} + virtual void format(ItBase& it) {} virtual void cleanup() {} - BaseFormatter &set_multiple(bool value); - BaseFormatter &set_color_output(bool value); + BaseFormatter& set_multiple(bool value); + BaseFormatter& set_color_output(bool value); int get_and_increment_test_counter() { return test_counter++; } void reset_test_counter() { test_counter = 1; } }; -inline BaseFormatter &BaseFormatter::set_multiple(bool multiple) { +inline BaseFormatter& BaseFormatter::set_multiple(bool multiple) { this->multiple = multiple; return *this; } -inline BaseFormatter &BaseFormatter::set_color_output(bool value) { +inline BaseFormatter& BaseFormatter::set_color_output(bool value) { this->color_output = value; return *this; } diff --git a/include/formatters/junit_xml.hpp b/include/formatters/junit_xml.hpp new file mode 100644 index 0000000..971e41b --- /dev/null +++ b/include/formatters/junit_xml.hpp @@ -0,0 +1,207 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +#include "formatters_base.hpp" +#include "it_base.hpp" + +namespace CppSpec::Formatters { +// JUnit XML header +constexpr static std::string_view junit_xml_header = R"()"; + +struct XMLSerializable { + virtual ~XMLSerializable() = default; + [[nodiscard]] virtual std::string to_xml() const = 0; +}; + +namespace JUnitNodes { +struct Result { + enum class Status { Failure, Error, Skipped }; + Status status = Status::Failure; + std::string message; + std::string type; + std::string text; + + Result(const std::string& message, const std::string& type, const std::string& text, Status status = Status::Failure) + : status(status), message(message), type(type), text(text) {} + + [[nodiscard]] std::string status_string() const { + switch (status) { + case Status::Failure: + return "failure"; + case Status::Error: + return "error"; + case Status::Skipped: + return "skipped"; + } + } + + [[nodiscard]] std::string to_xml() const { + return std::format(R"( <{} message="{}" type="{}">{})", status_string(), message, type, text, + status_string()); + } +}; + +struct TestCase { + std::string name; + std::string classname; + std::size_t assertions = 0; + std::chrono::duration time; + std::list results; + std::string file; + std::size_t line; + + [[nodiscard]] std::string to_xml() const { + auto start = + std::format(R"( "; + } + + auto xml_results = results | std::views::transform([](const Result& r) { return r.to_xml(); }); + + std::stringstream ss; + ss << start << ">" << std::endl; + + ss << *std::ranges::fold_left_first(xml_results, + [](const std::string& acc, const std::string& r) { return acc + "\n" + r; }); + ss << std::endl; + ss << " "; + return ss.str(); + } +}; + +struct TestSuite { + size_t id; + std::string name; + std::chrono::duration time; + std::chrono::time_point timestamp; + std::size_t tests; + std::size_t failures; + std::list cases; + + TestSuite(std::string name, + std::chrono::duration time, + size_t tests, + size_t failures, + std::chrono::time_point timestamp) + : id(get_next_id()), name(name), time(time), timestamp(timestamp), tests(tests), failures(failures) {} + + [[nodiscard]] std::string to_xml() const { + auto timestamp_str = + std::format("{0:%F}T{0:%T}", std::chrono::zoned_time(std::chrono::current_zone(), timestamp).get_local_time()); + + std::stringstream ss; + ss << " " + << std::format(R"()", id, name, + time.count(), timestamp_str, tests, failures); + ss << std::endl; + for (const auto& test_case : cases) { + ss << test_case.to_xml() << std::endl; + } + ss << " "; + return ss.str(); + } + + static size_t get_next_id() { + static std::atomic_size_t id_counter = 0; + return id_counter++; + } +}; + +struct TestSuites { + std::string name; + size_t tests = 0; + size_t failures = 0; + std::chrono::duration time; + std::chrono::time_point timestamp; + + std::list suites; + + [[nodiscard]] std::string to_xml() const { + std::stringstream ss; + auto timestamp_str = std::format("{0:%F}T{0:%T}", timestamp); + ss << std::format(R"()", name, tests, + failures, time.count(), timestamp_str); + ss << std::endl; + for (const auto& suite : suites) { + ss << suite.to_xml() << std::endl; + } + ss << "" << std::endl; + return ss.str(); + } +}; +} // namespace JUnitNodes + +class JUnitXML : public BaseFormatter { + JUnitNodes::TestSuites test_suites; + + public: + explicit JUnitXML(std::ostream& out_stream = std::cout, bool color = is_terminal()) + : BaseFormatter(out_stream, color) {} + + ~JUnitXML() { + test_suites.tests = + std::accumulate(test_suites.suites.begin(), test_suites.suites.end(), 0, + [](size_t sum, const JUnitNodes::TestSuite& suite) { return sum + suite.tests; }); + test_suites.failures = + std::accumulate(test_suites.suites.begin(), test_suites.suites.end(), 0, + [](size_t sum, const JUnitNodes::TestSuite& suite) { return sum + suite.failures; }); + test_suites.time = std::ranges::fold_left(test_suites.suites, std::chrono::duration(0), + [](const auto& acc, const auto& suite) { return acc + suite.time; }); + test_suites.timestamp = test_suites.suites.front().timestamp; + + out_stream << std::fixed; // disable scientific notation + // out_stream << std::setprecision(6); // set precision to 6 decimal places + out_stream << junit_xml_header << std::endl; + out_stream << test_suites.to_xml() << std::endl; + out_stream.flush(); + } + + void format(Description& description) override { + if (description.has_parent()) { + return; + } + test_suites.suites.emplace_back(description.get_description(), description.get_runtime(), description.num_tests(), + description.num_failures(), description.get_start_time()); + } + + void format(ItBase& it) override { + using namespace std::chrono; + std::forward_list descriptions; + + descriptions.push_front(it.get_description()); + for (auto parent = it.get_parent_as(); parent->has_parent(); + parent = parent->get_parent_as()) { + descriptions.push_front(parent->get_description()); + } + + std::string description = Util::join(descriptions, " "); + + auto test_case = JUnitNodes::TestCase{ + .name = description, + .classname = "", + .assertions = it.get_results().size(), + .time = it.get_runtime(), + .results = {}, + .file = it.get_location().file_name(), + .line = it.get_location().line(), + }; + + for (auto& result : it.get_results()) { + if (result.is_success()) { + continue; + } + std::string junit_message = result.get_location_string() + ": " + result.get_message(); + test_case.results.emplace_back("Match failure.", result.get_type(), result.get_message()); + } + + test_suites.suites.back().cases.push_back(test_case); + } +}; +} // namespace CppSpec::Formatters diff --git a/include/formatters/progress.hpp b/include/formatters/progress.hpp index 369ca81..c689a28 100644 --- a/include/formatters/progress.hpp +++ b/include/formatters/progress.hpp @@ -5,8 +5,8 @@ #include #include -#include "verbose.hpp" #include "term_colors.hpp" +#include "verbose.hpp" namespace CppSpec::Formatters { @@ -15,20 +15,20 @@ class Progress : public BaseFormatter { std::list baked_failure_messages{}; std::list raw_failure_messages{}; - std::string prep_failure_helper(const ItBase &it); + std::string prep_failure_helper(ItBase& it); public: - void format(const ItBase &it) override; - void format_failure(const std::string &message) override; - void flush() override; - void cleanup() override; + ~Progress() override { + format_failure_messages(); // Print any failures that we have + } + void format(ItBase& it) override; void format_failure_messages(); - void prep_failure(const ItBase &it); + void prep_failure(ItBase& it); }; /** @brief An assistant function for prep_failure to reduce complexity */ -inline std::string Progress::prep_failure_helper(const ItBase &it) { +inline std::string Progress::prep_failure_helper(ItBase& it) { // a singly-linked list to act as a LIFO queue std::forward_list list; @@ -52,17 +52,17 @@ inline std::string Progress::prep_failure_helper(const ItBase &it) { // Ascend the tree to the root, formatting the nodes and // enqueing each formatted string as we go. - const auto *parent = it.get_parent_as(); + Description* parent = it.get_parent_as(); do { helper_formatter.format(*parent); // Format the node push_and_clear(); - } while ((parent = dynamic_cast(parent->get_parent())) != nullptr); + } while ((parent = dynamic_cast(parent->get_parent())) != nullptr); return Util::join(list); // squash the list of strings and return it. } -inline void Progress::prep_failure(const ItBase &it) { +inline void Progress::prep_failure(ItBase& it) { std::ostringstream string_builder; // oss is used as the local string builder if (color_output) { string_builder << RED; // if we're doing color, make it red @@ -83,8 +83,8 @@ inline void Progress::prep_failure(const ItBase &it) { baked_failure_messages.push_back(string_builder.str()); } -inline void Progress::format(const ItBase &it) { - if (it.get_status()) { +inline void Progress::format(ItBase& it) { + if (it.get_result().status()) { if (color_output) { out_stream << GREEN; } @@ -103,31 +103,14 @@ inline void Progress::format(const ItBase &it) { get_and_increment_test_counter(); } -inline void Progress::format_failure(const std::string &message) { raw_failure_messages.push_back(message); } - -inline void Progress::flush() { - if (not multiple) { // If we aren't executing through a runner - out_stream << std::endl; // always newline - format_failure_messages(); - test_counter = 1; // and reset the test counter - } -} - -inline void Progress::cleanup() { - if (multiple) { - out_stream << std::endl; - format_failure_messages(); - } - // TODO: Fancy test reports here -} - inline void Progress::format_failure_messages() { if (!baked_failure_messages.empty()) { // If we have any failures to format - // if (color_output) out_stream << RED; // make them red + out_stream << std::endl; + out_stream << Util::join(baked_failure_messages, "\n\n") // separated by a blank line << std::endl; // newline - // if (color_output) out_stream << RESET; + baked_failure_messages.clear(); // Finally, clear the failures list. } } @@ -135,4 +118,3 @@ inline void Progress::format_failure_messages() { static Progress progress; } // namespace CppSpec::Formatters - diff --git a/include/formatters/tap.hpp b/include/formatters/tap.hpp index 1582c59..0d591c8 100644 --- a/include/formatters/tap.hpp +++ b/include/formatters/tap.hpp @@ -1,99 +1,91 @@ /** @file */ #pragma once -#include +#include #include -#include "class_description.hpp" #include "formatters_base.hpp" #include "term_colors.hpp" namespace CppSpec::Formatters { - -// The TAP format makes things a little tricky -class TAP : public BaseFormatter { +struct TAP final : public BaseFormatter { bool first = true; std::ostringstream buffer; - std::list failure_messages{}; - - public: - void format(const Description &description) override; - void format(const ItBase &it) override; - void format(const std::string &message) override; - void format_failure(const std::string &message) override; - void flush() override; + + std::string result_to_yaml(const Result& result); + void format(Description& description) override; + void format(ItBase& it) override; }; -inline void TAP::format(const Description &description) { - if (!first && description.get_parent() == nullptr) { - out_stream << std::endl; +inline std::string TAP::result_to_yaml(const Result& result) { + if (result.is_success()) { + return std::string(); + } + + std::ostringstream oss; + oss << " " << "---" << std::endl; + oss << " " << "message: " << "'" << result.get_message() << "'" << std::endl; + oss << " " << "severity: failure" << std::endl; + oss << " " << "at:" << std::endl; + oss << " " << " " << "file: " << result.get_location().file_name() << std::endl; + oss << " " << " " << "line: " << result.get_location().line() << std::endl; + // oss << " " << "data:" << std::endl; + // oss << " " << " " << "expected: " << result.get_expected() << std::endl; + // oss << " " << " " << "got: " << result.get_actual() << std::endl; + oss << " " << "..." << std::endl; + return oss.str(); +} + +inline void TAP::format(Description& description) { + if (!first && !description.has_parent()) { + std::string str = buffer.str(); + std::ostringstream oss; + if (str[0] == '\n') { + oss << str[0]; + } + + if (color_output) { + oss << GREEN; + } + oss << "1.." << test_counter - 1; + if (color_output) { + oss << RESET; + } + oss << std::endl; + + oss << ((str[0] == '\n') ? str.substr(1) : str); + + std::cout << oss.str() << std::flush; + first = false; + test_counter = 1; + buffer = std::ostringstream(); } + if (first) { - this->first = false; + first = false; } } -inline void TAP::format(const ItBase &it) { +inline void TAP::format(ItBase& it) { std::string description{it.get_description()}; // Build up the description for the test by ascending the // execution tree and chaining the individual descriptions together - for (const auto *parent = it.get_parent_as(); parent != nullptr; - parent = parent->get_parent_as()) { + for (auto parent = it.get_parent_as(); parent != nullptr; + parent = parent->get_parent_as()) { description = std::string(parent->get_description()) + " " + description; } if (color_output) { - buffer << (it.get_status() ? GREEN : RED); + buffer << (it.get_result().status() ? GREEN : RED); } - buffer << (it.get_status() ? "ok" : "not ok"); + buffer << (it.get_result().status() ? "ok" : "not ok"); if (color_output) { buffer << RESET; } buffer << " " << get_and_increment_test_counter() << " - " << description << std::endl; - if (not failure_messages.empty()) { - for (const auto &failure : failure_messages) { - buffer << failure; - } - failure_messages.clear(); - } -} - -inline void TAP::format(const std::string &message) { buffer << message << std::endl; } - -inline void TAP::format_failure(const std::string &message) { - std::ostringstream oss; - std::istringstream iss(message); - std::string line; - while (std::getline(iss, line)) { - oss << " " << line << std::endl; - } - failure_messages.push_back(oss.str()); -} - -inline void TAP::flush() { - std::string str = buffer.str(); - std::ostringstream oss; - if (str[0] == '\n') { - oss << str[0]; - } - - if (color_output) { - oss << GREEN; - } - oss << "1.." << test_counter - 1; - if (color_output) { - oss << RESET; - } - oss << std::endl; - - oss << ((str[0] == '\n') ? str.substr(1) : str); - - std::cout << oss.str() << std::flush; - first = false; - test_counter = 1; - buffer = std::ostringstream(); + buffer << result_to_yaml(it.get_result()); } static TAP tap; diff --git a/include/formatters/term_colors.hpp b/include/formatters/term_colors.hpp index 8631ab8..0c872ea 100644 --- a/include/formatters/term_colors.hpp +++ b/include/formatters/term_colors.hpp @@ -20,4 +20,3 @@ constexpr std::string_view BOLDBLUE("\033[1m\033[34m"); /* Bold Blue */ constexpr std::string_view BOLDMAGENTA("\033[1m\033[35m"); /* Bold Magenta */ constexpr std::string_view BOLDCYAN("\033[1m\033[36m"); /* Bold Cyan */ constexpr std::string_view BOLDWHITE("\033[1m\033[37m"); /* Bold White */ - diff --git a/include/formatters/verbose.hpp b/include/formatters/verbose.hpp index 3f79095..b4ad5ea 100644 --- a/include/formatters/verbose.hpp +++ b/include/formatters/verbose.hpp @@ -4,8 +4,6 @@ #include #include -#include "argparse.hpp" -#include "class_description.hpp" #include "formatters_base.hpp" #include "it_base.hpp" #include "term_colors.hpp" @@ -18,17 +16,15 @@ class Verbose : public BaseFormatter { public: Verbose() = default; - explicit Verbose(std::ostream &out_stream) : BaseFormatter(out_stream) {} - Verbose(const BaseFormatter &base, std::ostream &out_stream) : BaseFormatter(base, out_stream) {} - explicit Verbose(const BaseFormatter &base) : BaseFormatter(base) {} + explicit Verbose(std::ostream& out_stream) : BaseFormatter(out_stream) {} + Verbose(const BaseFormatter& base, std::ostream& out_stream) : BaseFormatter(base, out_stream) {} + explicit Verbose(const BaseFormatter& base) : BaseFormatter(base) {} - void format(const Description &description) override; - void format(const ItBase &description) override; - void format_failure(const std::string &message) override; - void format_failure_messages(); + void format(Description& description) override; + void format(ItBase& description) override; }; -inline void Verbose::format(const Description &description) { +inline void Verbose::format(Description& description) { if (!first && !description.has_parent()) { out_stream << std::endl; } @@ -38,9 +34,9 @@ inline void Verbose::format(const Description &description) { } } -inline void Verbose::format(const ItBase &it) { +inline void Verbose::format(ItBase& it) { if (color_output) { - out_stream << (it.get_status() ? GREEN : RED); + out_stream << (it.get_result().status() ? GREEN : RED); } out_stream << it.padding() << it.get_description() << std::endl; if (color_output) { @@ -50,29 +46,22 @@ inline void Verbose::format(const ItBase &it) { // Print any failures if we've got them // 'it' having a bad status necessarily // implies that there are failure messages - format_failure_messages(); - get_and_increment_test_counter(); -} - -inline void Verbose::format_failure(const std::string &message) { failure_messages.push_back(message); } - -// TODO: Compare this with Progress::format_failure_messages -inline void Verbose::format_failure_messages() { - if (not failure_messages.empty()) { // If we have any failures to format - if (color_output) { - out_stream << RED; // make them red + for (const auto& result : it.get_results()) { + if (result.is_failure()) { + if (color_output) { + out_stream << RED; // make them red + } + out_stream << Util::join(failure_messages, + "\n"); // separated by a blank line + if (color_output) { + out_stream << RESET; + } } - out_stream << Util::join(failure_messages, - "\n") // separated by a blank line - << std::endl; // newline - if (color_output) { - out_stream << RESET; - } - failure_messages.clear(); // Finally, clear the failures list. } + + get_and_increment_test_counter(); } static Verbose verbose; } // namespace CppSpec::Formatters - diff --git a/include/it.hpp b/include/it.hpp index bd55b4e..b1a360e 100755 --- a/include/it.hpp +++ b/include/it.hpp @@ -1,6 +1,7 @@ /** @file */ #pragma once +#include #include #include #include @@ -25,7 +26,7 @@ namespace CppSpec { */ class ItD : public ItBase { public: - using Block = std::function; + using Block = std::function; private: /** @brief The block contained in the ItD */ @@ -48,8 +49,8 @@ class ItD : public ItBase { * * @return the constructed ItD object */ - ItD(const Child &parent, const char *description, Block block) - : ItBase(parent, description), block(std::move(block)) {} + ItD(Runnable& parent, std::source_location location, const char* description, Block block) + : ItBase(parent, location, description), block(std::move(block)) {} /** * @brief The anonymous ItD constructor @@ -68,10 +69,10 @@ class ItD : public ItBase { * * @return the constructed ItD object */ - ItD(const Child &parent, Block block) : ItBase(parent), block(block) {} + ItD(Runnable& parent, std::source_location location, Block block) : ItBase(parent, location), block(block) {} // implemented in description.hpp - Result run(Formatters::BaseFormatter &printer) override; + void run() override; }; /** @@ -87,7 +88,7 @@ class ItD : public ItBase { template class ItCD : public ItBase { public: - using Block = std::function &)>; + using Block = std::function&)>; private: /** @brief The block contained in the ItCD */ @@ -101,16 +102,17 @@ class ItCD : public ItBase { * needing to have a dedicated macro for accessing the subject via getter, or * needing to introduce parenthesis for call syntax (like `subject()`) */ - T &subject; + T& subject; // This is only ever instantiated by ClassDescription - ItCD(const Child &parent, T &subject, const char *description, Block block) - : ItBase(parent, description), block(block), subject(subject) {} + ItCD(Runnable& parent, std::source_location location, T& subject, const char* description, Block block) + : ItBase(parent, location, description), block(block), subject(subject) {} - ItCD(const Child &parent, T &subject, Block block) : ItBase(parent), block(block), subject(subject) {} + ItCD(Runnable& parent, std::source_location location, T& subject, Block block) + : ItBase(parent, location), block(block), subject(subject) {} ExpectationValue is_expected(); - Result run(Formatters::BaseFormatter &printer) override; + void run() override; }; /** @@ -133,7 +135,7 @@ ExpectationFunc ItBase::expect(T block, std::source_location location) { } template -ExpectationValue ItBase::expect(Let &let, std::source_location location) { +ExpectationValue ItBase::expect(Let& let, std::source_location location) { return ExpectationValue(*this, let.value(), location); } @@ -150,7 +152,7 @@ ExpectationValue> ItBase::expect(std::initializer_list< return ExpectationValue>(*this, init_list, location); } -inline ExpectationValue ItBase::expect(const char *str, std::source_location location) { +inline ExpectationValue ItBase::expect(const char* str, std::source_location location) { return ExpectationValue(*this, std::string(str), location); } diff --git a/include/it_base.hpp b/include/it_base.hpp index 56c9705..238906d 100755 --- a/include/it_base.hpp +++ b/include/it_base.hpp @@ -1,16 +1,16 @@ /** @file */ #pragma once +#include +#include #include #include #include -#include #include "let.hpp" #include "runnable.hpp" #include "util.hpp" - namespace CppSpec { template @@ -31,26 +31,24 @@ class ExpectationFunc; class ItBase : public Runnable { /** @brief The documentation string for this `it` */ std::string description; + std::list results{}; // The results of the `it` statement public: ItBase() = delete; // Don't allow a default constructor - /** @brief Copy constructor */ - ItBase(const ItBase ©) noexcept = default; - /** * @brief Create an BaseIt without an explicit description * @return the constructed BaseIt */ - explicit ItBase(const Child &parent) noexcept : Runnable(parent) {} + explicit ItBase(Runnable& parent, std::source_location location) noexcept : Runnable(parent, location) {} /** * @brief Create an BaseIt with an explicit description. * @param description the documentation string of the `it` statement * @return the constructed BaseIt */ - explicit ItBase(const Child &parent, const char* description) noexcept - : Runnable(parent), description(std::move(description)) {} + explicit ItBase(Runnable& parent, std::source_location location, const char* description) noexcept + : Runnable(parent, location), description(std::move(description)) {} /** * @brief Get whether the object needs a description string @@ -68,7 +66,7 @@ class ItBase : public Runnable { * @brief Set the description string * @return a reference to the modified ItBase */ - ItBase &set_description(std::string_view description) noexcept { + ItBase& set_description(std::string_view description) noexcept { this->description = description; return *this; } @@ -109,7 +107,8 @@ class ItBase : public Runnable { * @return a ExpectationValue object containing the given init_list. */ template - ExpectationValue> expect(std::initializer_list init_list, std::source_location location = std::source_location::current()); + ExpectationValue> expect(std::initializer_list init_list, + std::source_location location = std::source_location::current()); /** * @brief The `expect` object generator for Let @@ -121,7 +120,7 @@ class ItBase : public Runnable { * @return a ExpectationValue object containing the given value. */ template - ExpectationValue expect(Let &let, std::source_location location = std::source_location::current()); + ExpectationValue expect(Let& let, std::source_location location = std::source_location::current()); /** * @brief The `expect` object generator for const char* @@ -129,7 +128,21 @@ class ItBase : public Runnable { * @param string the string to wrap * @return a ExpectationValue object containing a C++ string */ - ExpectationValue expect(const char *string, std::source_location location = std::source_location::current()); + ExpectationValue expect(const char* string, + std::source_location location = std::source_location::current()); + + void add_result(const Result& result) { results.push_back(result); } + std::list& get_results() noexcept { return results; } + const std::list& get_results() const noexcept { return results; } + void clear_results() noexcept { results.clear(); } + + [[nodiscard]] Result get_result() const override { + if (results.empty()) { + return Result::success(this->get_location()); + } + + return *std::ranges::fold_left_first(results, std::logical_and<>{}); + } }; } // namespace CppSpec diff --git a/include/let.hpp b/include/let.hpp index 9133aad..f3a05bc 100755 --- a/include/let.hpp +++ b/include/let.hpp @@ -19,7 +19,7 @@ class LetBase { public: constexpr LetBase() noexcept = default; - LetBase(const LetBase ©) = default; + LetBase(const LetBase& copy) = default; void reset() noexcept { delivered = false; } [[nodiscard]] constexpr bool has_result() const noexcept { return this->delivered; } }; @@ -42,13 +42,13 @@ class Let : public LetBase { public: explicit Let(block_t body) noexcept : LetBase(), body(body) {} - T *operator->() { + T* operator->() { value(); return result.operator->(); } - T &operator*() & { return value(); } - T &value() &; + T& operator*() & { return value(); } + T& value() &; }; /** @brief Executes the block of the let statment */ @@ -65,7 +65,7 @@ void Let::exec() { * @return a reference to the returned object of the let statement */ template -T &Let::value() & { +T& Let::value() & { exec(); return result.value(); } diff --git a/include/matchers/be_nullptr.hpp b/include/matchers/be_nullptr.hpp index 19a0fb0..af0f44a 100644 --- a/include/matchers/be_nullptr.hpp +++ b/include/matchers/be_nullptr.hpp @@ -4,13 +4,12 @@ #include "matcher_base.hpp" - namespace CppSpec::Matchers { template class BeNullptr : MatcherBase { public: - explicit BeNullptr(Expectation &expectation) : MatcherBase(expectation) {} + explicit BeNullptr(Expectation& expectation) : MatcherBase(expectation) {} std::string verb() override { return "be"; } std::string description() override { return "be nullptr"; } @@ -18,4 +17,3 @@ class BeNullptr : MatcherBase { }; } // namespace CppSpec::Matchers - diff --git a/include/matchers/contain.hpp b/include/matchers/contain.hpp index 180ff37..61d5df6 100644 --- a/include/matchers/contain.hpp +++ b/include/matchers/contain.hpp @@ -19,10 +19,10 @@ class ContainBase : public MatcherBase { std::string failure_message_when_negated() override; virtual bool diffable() { return true; } - ContainBase(Expectation &expectation, std::initializer_list expected) - : MatcherBase>(expectation, std::vector(expected)), actual_(this->actual()){}; - ContainBase(Expectation &expectation, U expected) - : MatcherBase(expectation, expected), actual_(this->actual()){}; + ContainBase(Expectation& expectation, std::initializer_list expected) + : MatcherBase>(expectation, std::vector(expected)), actual_(this->actual()) {}; + ContainBase(Expectation& expectation, U expected) + : MatcherBase(expectation, expected), actual_(this->actual()) {}; protected: bool actual_collection_includes(U expected_item); @@ -119,7 +119,7 @@ bool Contain::perform_match(Predicate predicate, Predicate /*hash_subse template class Contain : public ContainBase { public: - Contain(Expectation &expectation, U expected) : ContainBase(expectation, expected){}; + Contain(Expectation& expectation, U expected) : ContainBase(expectation, expected) {}; bool match() override; }; @@ -130,4 +130,3 @@ bool Contain::match() { } } // namespace CppSpec::Matchers - diff --git a/include/matchers/equal.hpp b/include/matchers/equal.hpp index 69d36ac..8ecaa82 100644 --- a/include/matchers/equal.hpp +++ b/include/matchers/equal.hpp @@ -16,7 +16,7 @@ namespace CppSpec::Matchers { template class Equal : public MatcherBase { public: - Equal(Expectation &expectation, E expected) : MatcherBase(expectation, expected) {} + Equal(Expectation& expectation, E expected) : MatcherBase(expectation, expected) {} std::string verb() override { return "equal"; } std::string failure_message() override; @@ -104,4 +104,3 @@ std::string Equal::actual_inspected() { // } } // namespace CppSpec::Matchers - diff --git a/include/matchers/errors/fail.hpp b/include/matchers/errors/fail.hpp index 9efb8c4..c036d7a 100644 --- a/include/matchers/errors/fail.hpp +++ b/include/matchers/errors/fail.hpp @@ -5,24 +5,21 @@ namespace CppSpec::Matchers { template -class Fail : public MatcherBase { +class Fail : public MatcherBase { public: static_assert(is_result_v, ".fail must() be matched against a Result."); - explicit Fail(Expectation &expectation) : MatcherBase(expectation, nullptr) {} + explicit Fail(Expectation& expectation) : MatcherBase(expectation, nullptr) {} - bool match() { return !this->actual().get_status(); } + bool match() { return !this->actual().status(); } }; template class FailWith : public MatcherBase { public: static_assert(is_result_v, ".fail_with() must be matched against a Result."); - FailWith(Expectation &expectation, std::string expected) - : MatcherBase(expectation, expected) {} + FailWith(Expectation& expectation, std::string expected) : MatcherBase(expectation, expected) {} - bool match() { - return (!this->actual().get_status()) && this->actual().get_message() == this->expected(); - } + bool match() { return (!this->actual().status()) && this->actual().get_message() == this->expected(); } }; } // namespace CppSpec::Matchers diff --git a/include/matchers/errors/have_error.hpp b/include/matchers/errors/have_error.hpp index c946d8b..693e324 100644 --- a/include/matchers/errors/have_error.hpp +++ b/include/matchers/errors/have_error.hpp @@ -7,13 +7,13 @@ namespace CppSpec::Matchers { template concept expected = requires(T t) { - { t.error() } -> std::same_as; + { t.error() } -> std::same_as; }; template -class HaveError : public MatcherBase { +class HaveError : public MatcherBase { public: - HaveError(Expectation &expectation) : MatcherBase(expectation) {} + HaveError(Expectation& expectation) : MatcherBase(expectation) {} std::string verb() override { return "have an error"; } std::string description() override { return verb(); } @@ -25,7 +25,7 @@ template class HaveErrorEqualTo : public MatcherBase { public: static_assert(std::is_same_v, "the contained error_type must match the expected type"); - HaveErrorEqualTo(Expectation &expectation, E expected) : MatcherBase(expectation, expected) {} + HaveErrorEqualTo(Expectation& expectation, E expected) : MatcherBase(expectation, expected) {} bool match() { return (this->actual().error() == this->expected()); } }; diff --git a/include/matchers/errors/have_value.hpp b/include/matchers/errors/have_value.hpp index b19ea46..0d056c4 100644 --- a/include/matchers/errors/have_value.hpp +++ b/include/matchers/errors/have_value.hpp @@ -11,9 +11,9 @@ concept optional = requires(T t) { }; template -class HaveValue : public MatcherBase { +class HaveValue : public MatcherBase { public: - HaveValue(Expectation &expectation) : MatcherBase(expectation) {} + HaveValue(Expectation& expectation) : MatcherBase(expectation) {} std::string verb() override { return "have"; } std::string description() override { return "have a value"; } @@ -25,7 +25,7 @@ template class HaveValueEqualTo : public Equal { public: static_assert(std::is_same_v, "the contained value_type must match the expected type"); - HaveValueEqualTo(Expectation &expectation, E expected) : Equal(expectation, expected) {} + HaveValueEqualTo(Expectation& expectation, E expected) : Equal(expectation, expected) {} bool match() { return (this->actual().value() == this->expected()); } }; diff --git a/include/matchers/errors/throw.hpp b/include/matchers/errors/throw.hpp index ce0835b..e4457dd 100644 --- a/include/matchers/errors/throw.hpp +++ b/include/matchers/errors/throw.hpp @@ -6,9 +6,9 @@ namespace CppSpec::Matchers { template -class Throw : public MatcherBase { +class Throw : public MatcherBase { public: - explicit Throw(Expectation &expectation) : MatcherBase(expectation, nullptr) {} + explicit Throw(Expectation& expectation) : MatcherBase(expectation, nullptr) {} bool match() override; std::string verb() override { return "throw"; } std::string description() override; @@ -21,7 +21,7 @@ bool Throw::match() { bool caught = false; try { this->actual(); - } catch (Ex &ex) { + } catch (Ex& ex) { caught = true; } return caught; diff --git a/include/matchers/matcher_base.hpp b/include/matchers/matcher_base.hpp index bf81699..c1eed37 100644 --- a/include/matchers/matcher_base.hpp +++ b/include/matchers/matcher_base.hpp @@ -9,10 +9,10 @@ #pragma once +#include #include #include "expectations/handler.hpp" -#include "formatters/formatters_base.hpp" #include "it_base.hpp" #include "pretty_matchers.hpp" @@ -34,31 +34,27 @@ namespace Matchers { * it would be `float` */ template -class MatcherBase : public Runnable, public Pretty { +class MatcherBase : public Pretty { std::string custom_failure_message; protected: Expected expected_; // The expected object contained by the matcher // A reference to the Expectation (i.e. `expect(2)`) - Expectation &expectation_; + Expectation& expectation_; public: // Copy constructor - MatcherBase(MatcherBase const ©) = default; + MatcherBase(MatcherBase const& copy) = default; // Constructor when matcher has no 'object' to match for - explicit MatcherBase(Expectation &expectation) - : Runnable(*expectation.get_parent()), // We want the parent of the - // matcher to be the `it` block, - // not the - // Expectation. - expectation_(expectation) {} + explicit MatcherBase(Expectation& expectation) + // We want the parent of the matcher to be the `it` block, not the Expectation. + : expectation_(expectation) {} // Constructor when the matcher has an object to match for. This is the most // commonly used constructor - MatcherBase(Expectation &expectation, Expected expected) - : Runnable(*expectation.get_parent()), expected_(expected), expectation_(expectation) {} + MatcherBase(Expectation& expectation, Expected expected) : expected_(expected), expectation_(expectation) {} /*--------- Helper functions -------------*/ @@ -68,21 +64,23 @@ class MatcherBase : public Runnable, public Pretty { virtual std::string verb() { return "match"; } // Get the 'actual' object from the Expectation - constexpr Actual &actual() { return expectation_.get_target(); } + constexpr Actual& actual() { return expectation_.get_target(); } // Get the 'expected' object from the Matcher - Expected &expected() { return expected_; } + Expected& expected() { return expected_; } // Get the Expectation itself - Expectation &expectation() { return expectation_; } + Expectation& expectation() { return expectation_; } // Set the message to give on match failure - virtual MatcherBase &set_message(const std::string &message); + virtual MatcherBase& set_message(const std::string& message); + + std::source_location get_location() const { return expectation_.get_location(); } /*--------- Primary functions -------------*/ // Run the matcher - Result run(Formatters::BaseFormatter &printer) override; + Result run(); // TODO: match and negated match should return Result virtual bool match() = 0; @@ -99,7 +97,7 @@ class MatcherBase : public Runnable, public Pretty { * @return the modified Matcher */ template -MatcherBase &MatcherBase::set_message(const std::string &message) { +MatcherBase& MatcherBase::set_message(const std::string& message) { this->custom_failure_message = message; return *this; } @@ -152,35 +150,40 @@ std::string MatcherBase::description() { * @return the Result of running the Matcher */ template -Result MatcherBase::run(Formatters::BaseFormatter &printer) { - auto *parent = static_cast(this->get_parent()); - // If we need a description for our test, generate it - // unless we're ignoring the output. - if (parent->needs_description() && !expectation_.ignore_failure()) { - std::stringstream ss; - ss << (expectation_.sign() ? PositiveExpectationHandler::verb() : NegativeExpectationHandler::verb()) << " " - << this->description(); - parent->set_description(ss.str()); +Result MatcherBase::run() { + ItBase* parent = static_cast(expectation_.get_it()); + if (parent) { + // If we need a description for our test, generate it + // unless we're ignoring the output. + if (parent->needs_description() && !expectation_.ignore_failure()) { + parent->set_description( + (expectation_.sign() ? PositiveExpectationHandler::verb() : NegativeExpectationHandler::verb()) + " " + + this->description()); + } } - Result matched = expectation_.sign() ? PositiveExpectationHandler::handle_matcher(*this) - : NegativeExpectationHandler::handle_matcher(*this); + Result result = expectation_.sign() ? PositiveExpectationHandler::handle_matcher(*this) + : NegativeExpectationHandler::handle_matcher(*this); + + result.set_type(Util::demangle(typeid(*this).name())); // If our items didn't match, we obviously failed. // Only report the failure if we aren't actively ignoring it. - if (!matched && !expectation_.ignore_failure()) { - this->failed(); - std::string message = matched.get_message(); - if (message.empty()) { - printer.format_failure( + if (result.is_failure()) { + if (expectation_.ignore_failure()) { + result = Result::success(result.get_location()); + } else if (result.get_message().empty()) { + result.set_message( "Failure message is empty. Does your matcher define the " "appropriate failure_message[_when_negated] method to " "return a string?"); - } else { - printer.format_failure(matched.get_message()); } } - return matched; + + if (parent) { + parent->add_result(result); + } + return result; } } // namespace Matchers diff --git a/include/matchers/numeric/be_between.hpp b/include/matchers/numeric/be_between.hpp index e211438..0c9ee2c 100644 --- a/include/matchers/numeric/be_between.hpp +++ b/include/matchers/numeric/be_between.hpp @@ -19,7 +19,7 @@ class BeBetween : public MatcherBase { // BeBetween(Expectation &expectation, E min, E max) // : BaseMatcher(expectation), min(min), max(max) {} - BeBetween(Expectation &expectation, E min, E max, RangeMode mode = RangeMode::inclusive) + BeBetween(Expectation& expectation, E min, E max, RangeMode mode = RangeMode::inclusive) : MatcherBase(expectation), min(min), max(max), mode(mode) { switch (mode) { case RangeMode::inclusive: diff --git a/include/matchers/numeric/be_greater_than.hpp b/include/matchers/numeric/be_greater_than.hpp index 133b257..0f47638 100644 --- a/include/matchers/numeric/be_greater_than.hpp +++ b/include/matchers/numeric/be_greater_than.hpp @@ -4,16 +4,14 @@ #include "matchers/matcher_base.hpp" - namespace CppSpec::Matchers { template class BeGreaterThan : public MatcherBase { public: - BeGreaterThan(Expectation &expectation, E expected) : MatcherBase(expectation, expected) {} + BeGreaterThan(Expectation& expectation, E expected) : MatcherBase(expectation, expected) {} bool match() override { return this->actual() > this->expected(); } std::string verb() override { return "be greater than"; } }; } // namespace CppSpec::Matchers - diff --git a/include/matchers/numeric/be_less_than.hpp b/include/matchers/numeric/be_less_than.hpp index ddbc144..8cf972e 100644 --- a/include/matchers/numeric/be_less_than.hpp +++ b/include/matchers/numeric/be_less_than.hpp @@ -4,16 +4,14 @@ #include "matchers/matcher_base.hpp" - namespace CppSpec::Matchers { template class BeLessThan : public MatcherBase { public: - BeLessThan(Expectation &expectation, E expected) : MatcherBase(expectation, expected) {} + BeLessThan(Expectation& expectation, E expected) : MatcherBase(expectation, expected) {} bool match() override { return this->actual() < this->expected(); } std::string verb() override { return "be less than"; } }; } // namespace CppSpec::Matchers - diff --git a/include/matchers/numeric/be_within.hpp b/include/matchers/numeric/be_within.hpp index 52a4f5f..d32449f 100644 --- a/include/matchers/numeric/be_within.hpp +++ b/include/matchers/numeric/be_within.hpp @@ -15,8 +15,8 @@ class BeWithinHelper { public: BeWithinHelper(Expectation& expectation, E tolerance) : expectation(expectation), tolerance(tolerance) {} - BeWithin of(E expected); - BeWithin percent_of(E expected); + BeWithin& of(E expected); + BeWithin& percent_of(E expected); void set_message(const std::string& msg) { this->msg = msg; } std::string get_message() { return this->msg; } }; @@ -39,17 +39,17 @@ class BeWithin : public MatcherBase { }; template -BeWithin BeWithinHelper::of(E expected) { - BeWithin matcher(expectation, tolerance, expected, ""); // No unit specified - matcher.set_message(msg); - return matcher; +BeWithin& BeWithinHelper::of(E expected) { + auto* matcher = new BeWithin(expectation, tolerance, expected, ""); // No unit specified + matcher->set_message(msg); + return *matcher; } template -BeWithin BeWithinHelper::percent_of(E expected) { - BeWithin matcher(expectation, tolerance, expected, "%"); // Percent unit specified - matcher.set_message(msg); - return matcher; +BeWithin& BeWithinHelper::percent_of(E expected) { + auto* matcher = new BeWithin(expectation, tolerance, expected, "%"); // Percent unit specified + matcher->set_message(msg); + return *matcher; } template diff --git a/include/matchers/pretty_matchers.hpp b/include/matchers/pretty_matchers.hpp index 17bf5a9..2790525 100755 --- a/include/matchers/pretty_matchers.hpp +++ b/include/matchers/pretty_matchers.hpp @@ -26,33 +26,33 @@ class MatcherBase; */ struct Pretty { std::string _name; - [[nodiscard]] std::string name(const std::string &name) const; - [[nodiscard]] std::string name_to_sentence(const std::string &name) const; - static std::string split_words(const std::string &sym); - static std::string underscore(const std::string &camel_cased_word); - static std::string last(const std::string &s, char delim); - static std::string improve_hash_formatting(const std::string &inspect_string); + [[nodiscard]] std::string name(const std::string& name) const; + [[nodiscard]] std::string name_to_sentence(const std::string& name) const; + static std::string split_words(const std::string& sym); + static std::string underscore(const std::string& camel_cased_word); + static std::string last(const std::string& s, char delim); + static std::string improve_hash_formatting(const std::string& inspect_string); template - static std::string to_word(const T &item); + static std::string to_word(const T& item); template - static std::string to_word(const T &item); + static std::string to_word(const T& item); template - static std::string to_word_type(const T &item); + static std::string to_word_type(const T& item); template - static std::string to_word(const Matchers::MatcherBase &matcher); + static std::string to_word(const Matchers::MatcherBase& matcher); template - static std::string to_sentence(const T &item); + static std::string to_sentence(const T& item); template - static std::string to_sentence(const std::vector &words); + static std::string to_sentence(const std::vector& words); template - static std::string inspect_object(const T &object); + static std::string inspect_object(const T& object); }; /** @@ -64,9 +64,9 @@ struct Pretty { * @return a human-readable comma-delimited list */ template -inline std::string Pretty::to_sentence(const std::vector &objects) { +inline std::string Pretty::to_sentence(const std::vector& objects) { std::vector words; - for (const auto &object : objects) { + for (const auto& object : objects) { words.push_back(to_word(object)); } @@ -103,7 +103,7 @@ inline std::string Pretty::to_sentence(const std::vector &objects) { * @return a human-readable representation of the object (as a list) */ template -inline std::string Pretty::to_sentence(const T &item) { +inline std::string Pretty::to_sentence(const T& item) { return to_sentence(std::vector{item}); } @@ -119,24 +119,24 @@ inline std::string Pretty::to_sentence(const T &item) { * @return the string representation */ template -inline std::string Pretty::to_word(const T &item) { +inline std::string Pretty::to_word(const T& item) { std::ostringstream oss; oss << item; // use operator<< to convert and append return oss.str(); } template <> -inline std::string Pretty::to_word(const bool &item) { +inline std::string Pretty::to_word(const bool& item) { return item ? "true" : "false"; } template <> -inline std::string Pretty::to_word(const std::true_type & /* item */) { +inline std::string Pretty::to_word(const std::true_type& /* item */) { return "true"; } template <> -inline std::string Pretty::to_word(const std::false_type & /* item */) { +inline std::string Pretty::to_word(const std::false_type& /* item */) { return "false"; } @@ -148,7 +148,7 @@ inline std::string Pretty::to_word(const std::false_type & /* i * @return the string representation */ template -inline std::string Pretty::to_word(const T &item) { +inline std::string Pretty::to_word(const T& item) { std::stringstream ss; // Ruby-style inspect for objects without an overloaded operator<< ss << "#<" << Util::demangle(typeid(item).name()) << ":" << &item << ">"; @@ -167,7 +167,7 @@ inline std::string Pretty::to_word(const T &item) { * @return a string representation of the Matcher */ template -inline std::string Pretty::to_word(const Matchers::MatcherBase &matcher) { +inline std::string Pretty::to_word(const Matchers::MatcherBase& matcher) { std::string description = matcher.description(); if (description.empty()) { return "[No description]"; @@ -176,7 +176,7 @@ inline std::string Pretty::to_word(const Matchers::MatcherBase &matcher) { } template -inline std::string Pretty::to_word_type(const T &item) { +inline std::string Pretty::to_word_type(const T& item) { std::string word = to_word(item); if constexpr (Util::is_streamable) { word += " : " + Util::demangle(typeid(T).name()); @@ -184,18 +184,22 @@ inline std::string Pretty::to_word_type(const T &item) { return word; } -inline std::string Pretty::name_to_sentence(const std::string &n) const { return split_words(name(n)); } +inline std::string Pretty::name_to_sentence(const std::string& n) const { + return split_words(name(n)); +} -inline std::string Pretty::name(const std::string &name) const { +inline std::string Pretty::name(const std::string& name) const { if (_name.empty()) { return last(name, ':'); } return _name; } -inline std::string Pretty::split_words(const std::string &sym) { return std::regex_replace(sym, std::regex("_"), " "); } +inline std::string Pretty::split_words(const std::string& sym) { + return std::regex_replace(sym, std::regex("_"), " "); +} -inline std::string Pretty::underscore(const std::string &word) { +inline std::string Pretty::underscore(const std::string& word) { std::string str = std::regex_replace(word, std::regex("([A-Z]+)([A-Z][a-z])"), "$1_$2"); str = std::regex_replace(str, std::regex("([a-z\\d])([A-Z])"), "$1_$2"); str = std::regex_replace(str, std::regex("-"), "_"); @@ -203,7 +207,7 @@ inline std::string Pretty::underscore(const std::string &word) { return str; } -inline std::string Pretty::last(const std::string &s, const char delim) { +inline std::string Pretty::last(const std::string& s, const char delim) { std::vector elems; std::stringstream ss(s); std::string item; @@ -215,7 +219,7 @@ inline std::string Pretty::last(const std::string &s, const char delim) { return elems.back(); } -inline std::string Pretty::improve_hash_formatting(const std::string &inspect_string) { +inline std::string Pretty::improve_hash_formatting(const std::string& inspect_string) { return std::regex_replace(inspect_string, std::regex("(\\S)=>(\\S)"), "$1 => $2"); } @@ -227,7 +231,7 @@ inline std::string Pretty::improve_hash_formatting(const std::string &inspect_st * @return the generated string */ template -inline std::string Pretty::inspect_object(const O &o) { +inline std::string Pretty::inspect_object(const O& o) { return std::format("({}) => {}", Util::demangle(typeid(o).name()), to_word(o)); } @@ -239,7 +243,7 @@ inline std::string Pretty::inspect_object(const O &o) { * @return the generated string */ template <> -inline std::string Pretty::inspect_object(const char *const &o) { +inline std::string Pretty::inspect_object(const char* const& o) { return std::format("(const char *) => \"{}\"", o); } @@ -251,12 +255,12 @@ inline std::string Pretty::inspect_object(const char *const &o) { * @return the generated string */ template <> -inline std::string Pretty::inspect_object(const std::string &o) { +inline std::string Pretty::inspect_object(const std::string& o) { return std::format("(std::string) => \"{}\"", o); } template <> -inline std::string Pretty::inspect_object(const std::string_view &o) { +inline std::string Pretty::inspect_object(const std::string_view& o) { return std::format("(std::string_view) => \"{}\"", o); } diff --git a/include/matchers/satisfy.hpp b/include/matchers/satisfy.hpp index 5fa8a4e..6de1f2a 100644 --- a/include/matchers/satisfy.hpp +++ b/include/matchers/satisfy.hpp @@ -22,7 +22,7 @@ class Satisfy : public MatcherBase //, BeHelpers> std::function test; public: - Satisfy(Expectation &expectation, std::function test) : MatcherBase(expectation), test(test) {} + Satisfy(Expectation& expectation, std::function test) : MatcherBase(expectation), test(test) {} std::string failure_message() override; std::string failure_message_when_negated() override; @@ -47,4 +47,3 @@ bool Satisfy::match() { } } // namespace CppSpec::Matchers - diff --git a/include/matchers/strings/end_with.hpp b/include/matchers/strings/end_with.hpp index b1ba3c5..6331666 100644 --- a/include/matchers/strings/end_with.hpp +++ b/include/matchers/strings/end_with.hpp @@ -11,16 +11,15 @@ namespace CppSpec::Matchers { template class EndWith : public MatcherBase { public: - EndWith(Expectation &expectation, E start) : MatcherBase(expectation, start) {} + EndWith(Expectation& expectation, E start) : MatcherBase(expectation, start) {} std::string verb() override { return "end with"; } bool match() override { - A &actual = this->actual(); - E &expected = this->expected(); + A& actual = this->actual(); + E& expected = this->expected(); return std::equal(expected.rbegin(), expected.rend(), actual.rbegin()); } }; } // namespace CppSpec::Matchers - diff --git a/include/matchers/strings/match.hpp b/include/matchers/strings/match.hpp index cf140d1..6470eda 100644 --- a/include/matchers/strings/match.hpp +++ b/include/matchers/strings/match.hpp @@ -5,16 +5,15 @@ #include "matchers/matcher_base.hpp" - namespace CppSpec::Matchers { template class Match : MatcherBase { public: - explicit Match(Expectation &expectation, std::string expected) + explicit Match(Expectation& expectation, std::string expected) : MatcherBase(expectation, std::regex(expected)) {} - explicit Match(Expectation &expectation, std::regex expected) + explicit Match(Expectation& expectation, std::regex expected) : MatcherBase(expectation, expected) {} std::string verb() override { return "match"; } @@ -28,10 +27,10 @@ class Match : MatcherBase { template class MatchPartial : public MatcherBase { public: - explicit MatchPartial(Expectation &expectation, std::string expected) + explicit MatchPartial(Expectation& expectation, std::string expected) : MatcherBase(expectation, std::regex(expected)) {} - explicit MatchPartial(Expectation &expectation, std::regex expected) + explicit MatchPartial(Expectation& expectation, std::regex expected) : MatcherBase(expectation, expected) {} std::string description() override { return "partially match " + Pretty::to_word(this->expected()); } @@ -40,4 +39,3 @@ class MatchPartial : public MatcherBase { }; } // namespace CppSpec::Matchers - diff --git a/include/matchers/strings/start_with.hpp b/include/matchers/strings/start_with.hpp index 7c92c40..51d8fe2 100644 --- a/include/matchers/strings/start_with.hpp +++ b/include/matchers/strings/start_with.hpp @@ -11,16 +11,15 @@ namespace CppSpec::Matchers { template class StartWith : public MatcherBase { public: - StartWith(Expectation &expectation, E start) : MatcherBase(expectation, start) {} + StartWith(Expectation& expectation, E start) : MatcherBase(expectation, start) {} std::string verb() override { return "start with"; } bool match() override { - A &actual = this->actual(); - E &expected = this->expected(); + A& actual = this->actual(); + E& expected = this->expected(); return std::equal(expected.begin(), expected.end(), actual.begin()); } }; } // namespace CppSpec::Matchers - diff --git a/include/result.hpp b/include/result.hpp index 798f9e1..63935df 100644 --- a/include/result.hpp +++ b/include/result.hpp @@ -2,69 +2,92 @@ #pragma once #include +#include +#include #include #include -#include namespace CppSpec { class Result { - const bool value; + bool value; + std::source_location location; std::string message; - explicit Result(bool value, std::string message = "") noexcept : value(value), message(std::move(message)) {} + std::string type; + explicit Result(bool value, std::source_location location, const std::string& message = "") noexcept + : value(value), location(location), message(message) {} public: - // Default destructor - virtual ~Result() = default; - - // Copy constructor/operator - Result(const Result &) = default; - Result &operator=(const Result &) = delete; - - // Move constructor/operator - Result(Result &&) = default; - Result &operator=(Result &&) = delete; + Result() = default; /*--------- Status helper functions --------------*/ - [[nodiscard]] bool get_status() const noexcept { return value; } + [[nodiscard]] bool status() const noexcept { return value; } + [[nodiscard]] bool status() noexcept { return value; } + [[nodiscard]] bool is_success() const noexcept { return value; } + [[nodiscard]] bool is_failure() const noexcept { return value == false; } + + /*--------- Location helper functions ------------*/ + [[nodiscard]] std::source_location get_location() const noexcept { return location; } + [[nodiscard]] std::string get_location_string() const noexcept { + return std::format("{}:{}:{}", location.file_name(), location.line(), location.column()); + } - operator bool() const noexcept { return this->get_status(); } + [[nodiscard]] std::string get_type() const noexcept { return type; } + [[nodiscard]] std::string get_type() noexcept { return type; } + void set_type(const std::string& type) noexcept { this->type = type; } /*--------- Message helper functions -------------*/ std::string get_message() noexcept { return message; } [[nodiscard]] std::string get_message() const noexcept { return message; } - Result &set_message(const std::string &message) noexcept; + Result& set_message(const std::string& message) noexcept; /*--------- Explicit constructor functions -------*/ - static Result success() noexcept; - static Result failure() noexcept; - static Result success_with(const std::string &success_message) noexcept; - static Result failure_with(const std::string &failure_message) noexcept; + static Result success(std::source_location location) noexcept; + static Result failure(std::source_location location) noexcept; + static Result success_with(std::source_location location, const std::string& success_message) noexcept; + static Result failure_with(std::source_location location, const std::string& failure_message) noexcept; /*-------------- Friend functions ----------------*/ // Stream operator - friend std::ostream &operator<<(std::ostream &os, const Result &res); + friend std::ostream& operator<<(std::ostream& os, const Result& res); + + friend Result& operator&=(Result& lhs, const Result& rhs) { + lhs.value = lhs.value && rhs.value; + return lhs; + } + + friend Result operator&&(const Result& lhs, const Result& rhs) { + Result result; + result.location = lhs.location; + result.message = lhs.message + " and\n" + rhs.message; + result.value = lhs.value && rhs.value; + return result; + } }; -inline Result &Result::set_message(const std::string &message) noexcept { +inline Result& Result::set_message(const std::string& message) noexcept { this->message = message; return *this; } -inline Result Result::success() noexcept { return Result(true); } -inline Result Result::success_with(const std::string &success_message) noexcept { - return Result(true, success_message); +inline Result Result::success(std::source_location location) noexcept { + return Result(true, location); +} +inline Result Result::success_with(std::source_location location, const std::string& success_message) noexcept { + return Result(true, location, success_message); } -inline Result Result::failure() noexcept { return Result(false); } -inline Result Result::failure_with(const std::string &failure_message) noexcept { - return Result(false, failure_message); +inline Result Result::failure(std::source_location location) noexcept { + return Result(false, location); +} +inline Result Result::failure_with(std::source_location location, const std::string& failure_message) noexcept { + return Result(false, location, failure_message); } -inline std::ostream &operator<<(std::ostream &os, const Result &res) { +inline std::ostream& operator<<(std::ostream& os, const Result& res) { std::stringstream ss; - ss << (res.get_status() ? "Result::success" : "Result::failure"); + ss << (res.status() ? "Result::success" : "Result::failure"); if (not res.get_message().empty()) { ss << "(\"" + res.get_message() + "\")"; @@ -74,7 +97,7 @@ inline std::ostream &operator<<(std::ostream &os, const Result &res) { } template -constexpr bool is_result_v = std::is_same_v; +constexpr bool is_result_v = std::is_same_v; template concept is_result = is_result_v; diff --git a/include/runnable.hpp b/include/runnable.hpp index e8e1a88..a70db0e 100755 --- a/include/runnable.hpp +++ b/include/runnable.hpp @@ -1,34 +1,158 @@ #pragma once -#include "child.hpp" + +#include +#include +#include +#include +#include "result.hpp" namespace CppSpec { + class Result; +namespace Formatters { +class BaseFormatter; // Forward declaration to allow reference +} + /** - * @brief Abstract base class for executable objects + * @brief Base class for all objects in the execution tree. + * + * A base class for all objects that comprise some abstract structure + * with a nesting concept. Used to propogate ('pass') failures from leaf + * to root without exceptions (and/or code-jumping), thus allowing + * execution to continue virtually uninterrupted. */ -class Runnable : public Child { +class Runnable { + // The parent of this child + // We use a raw pointer here instead of the safer std::shared_ptr + // due to the way that tests are inherently constructed + // (`describe some_spec("a test", $ { ... });`). In order to use + // a shared pointer, each object that is set as the parent must be + // contained in a shared pointer. As tests are created by `describe ...`, + // the root object is not wrapped by a shared pointer. Attempting to create + // this shared pointer at some later time doesn't work, as it results in + // trying to delete the current object `this` once the pointer goes out of + // scope. Since children are always destroyed before their parents, this + // isn't a problem anyways. In addition, any structures that are children + // are allocated on the stack for speed reasons. + Runnable* parent = nullptr; + + // The source file location of the Runnable object + std::source_location location; + + std::list children_{}; // List of children + + std::chrono::time_point start_time_; + std::chrono::duration runtime_; + public: - // Default constructor/destructor - Runnable() = default; - ~Runnable() override = default; - - // Move constructor/operator - Runnable(Runnable &&) = default; - Runnable &operator=(Runnable &&) = default; - - // Copy constructor/operator - Runnable(const Runnable &parent) = default; - Runnable &operator=(const Runnable &) = delete; - - // WARNING! Be *very* carful about calling this constructor. - // You *need* to make sure that the argument is a Child and not a - // subclass of Runnable. Otherwise you'll end up calling the - // copy constructor on accident. - explicit Runnable(const Child &parent) noexcept : Child(Child::of(parent)) {} - - // Interface function - virtual Result run(Formatters::BaseFormatter &printer) = 0; + Runnable(std::source_location location) : location(location) {} + + Runnable(Runnable& parent, std::source_location location) : parent(&parent), location(location) { + parent.children_.push_back(this); + } + virtual ~Runnable() = default; + + /*--------- Parent helper functions -------------*/ + + /** @brief Check to see if the Runnable has a parent. */ + bool has_parent() noexcept { return parent != nullptr; } + [[nodiscard]] bool has_parent() const noexcept { return parent != nullptr; } + + // TODO: Look in to making these references instead of pointer returns + /** @brief Get the Runnable's parent. */ + [[nodiscard]] Runnable* get_parent() noexcept { return parent; } + [[nodiscard]] const Runnable* get_parent() const noexcept { return parent; } + + std::list& get_children() noexcept { return children_; } + const std::list& get_children() const noexcept { return children_; } + + template + C* get_parent_as() noexcept { + return static_cast(parent); + } + + /** @brief Set the Runnable's parent */ + Runnable& set_parent(Runnable* parent) noexcept { + if (this->parent != nullptr) { + this->parent->children_.remove(this); + } + if (parent != nullptr) { + parent->children_.push_back(this); + } + this->parent = parent; + return *this; + } + + /*--------- Primary member functions -------------*/ + + // Calculate the padding for printing this object + [[nodiscard]] std::string padding() const noexcept; + + // Get the location of the object + [[nodiscard]] std::source_location get_location() const noexcept { return this->location; } + + // Set the location of the object + void set_location(std::source_location location) noexcept { this->location = location; } + + virtual void run() = 0; + + virtual void timed_run() { + using namespace std::chrono; + start_time_ = system_clock::now(); + time_point start_time = high_resolution_clock::now(); + run(); + time_point end = high_resolution_clock::now(); + runtime_ = end - start_time; + } + + [[nodiscard]] std::chrono::duration get_runtime() const { return runtime_; } + + [[nodiscard]] std::chrono::time_point get_start_time() const { return start_time_; } + + [[nodiscard]] virtual Result get_result() const { + Result result = Result::success(location); + for (auto* child : get_children()) { + result &= child->get_result(); + } + return result; + } + + size_t num_tests() const noexcept { + if (get_children().empty()) { + return 1; // This is a leaf node + } + + // This is not a leaf node, so we need to count the children + size_t count = 0; + for (Runnable* child : get_children()) { + count += child->num_tests(); // +1 for the child itself + } + return count; + } + + size_t num_failures() const noexcept { + if (get_children().empty()) { + return this->get_result().is_failure(); // This is a leaf node + } + + // This is not a leaf node, so we need to count the children + size_t count = 0; + for (Runnable* child : get_children()) { + count += child->num_failures(); // +1 for the child itself + } + return count; + } }; +/*>>>>>>>>>>>>>>>>>>>> Runnable <<<<<<<<<<<<<<<<<<<<<<<<<*/ + +/** + * @brief Generate padding (indentation) fore the current object. + * @return A string of spaces for use in pretty-printing. + */ +inline std::string Runnable::padding() const noexcept { + return this->has_parent() ? this->get_parent()->padding() + " " : ""; +} + } // namespace CppSpec diff --git a/include/runner.hpp b/include/runner.hpp index 7cd95a0..47d95a6 100644 --- a/include/runner.hpp +++ b/include/runner.hpp @@ -16,7 +16,7 @@ namespace CppSpec { * @brief A collection of Descriptions that are run in sequence */ class Runner { - std::list specs{}; + std::list specs{}; std::shared_ptr formatter; public: @@ -30,17 +30,25 @@ class Runner { * @param spec the spec to be added * @return a reference to the modified Runner */ - Runner &add_spec(Description &spec) { + Runner& add_spec(Description& spec) { specs.push_back(&spec); return *this; } - Result run() { + template + Runner& add_specs(Specs... specs) { + (add_spec(specs), ...); // Fold expression to add all specs + return *this; + } + + Result run(std::source_location location = std::source_location::current()) { bool success = true; - for (auto *spec : specs) { - success &= static_cast(spec->run(*formatter)); + for (Description* spec : specs) { + spec->timed_run(); + formatter->format(static_cast(*spec)); + success &= spec->get_result().status(); } - return success ? Result::success() : Result::failure(); + return success ? Result::success(location) : Result::failure(location); } Result exec() { diff --git a/include/util.hpp b/include/util.hpp index 9de4db4..a84a0d2 100755 --- a/include/util.hpp +++ b/include/util.hpp @@ -9,10 +9,11 @@ #include #include - #ifdef __GNUG__ #include + #include + #endif namespace CppSpec::Util { @@ -25,14 +26,18 @@ namespace CppSpec::Util { * @return a human-readable translation of the given symbol */ #ifdef __GNUG__ -inline std::string demangle(const char *mangled) { +inline std::string demangle(const char* mangled) { int status; - std::unique_ptr result{abi::__cxa_demangle(mangled, NULL, NULL, &status), std::free}; - + std::unique_ptr result{ + abi::__cxa_demangle(mangled, nullptr, nullptr, &status), + std::free, + }; return (status == 0) ? result.get() : mangled; } #else -inline std::string demangle(const char *name) { return name; } +inline std::string demangle(const char* name) { + return name; +} #endif /** @@ -73,8 +78,8 @@ concept is_container = requires(C c) { * @tparam C a type to check */ template -concept is_streamable = requires(std::ostream &os, const C &obj) { - { os << obj } -> std::same_as; +concept is_streamable = requires(std::ostream& os, const C& obj) { + { os << obj } -> std::same_as; }; template @@ -107,10 +112,10 @@ concept is_not_functional = !is_functional; * * @return the joined string */ -inline std::string join(std::ranges::range auto &iterable, const std::string &separator = "") { +[[nodiscard]] inline std::string join(std::ranges::range auto& iterable, const std::string& separator = "") { std::ostringstream oss; bool first = true; - for (auto &thing : iterable) { + for (auto& thing : iterable) { if (!first) { oss << separator; } diff --git a/spec/describe_a_spec.cpp b/spec/describe_a_spec.cpp index fd8699c..7f13e86 100644 --- a/spec/describe_a_spec.cpp +++ b/spec/describe_a_spec.cpp @@ -2,15 +2,13 @@ #include "cppspec.hpp" - using namespace CppSpec; struct TestClass { std::string field1; std::string field2; - TestClass() : field1("foo"), field2("bar"){}; - TestClass(std::string field1, std::string field2) - : field1(field1), field2(field2){}; + TestClass() : field1("foo"), field2("bar") {}; + TestClass(std::string field1, std::string field2) : field1(field1), field2(field2) {}; }; // clang-format off @@ -42,12 +40,12 @@ describe_a describe_a_syntax_spec("describe_a syntax", $ { }); }); +// clang-format on int main(int argc, char **argv) { - return CppSpec ::parse(argc, argv) - .add_spec(describe_a_implicit_spec) - .add_spec(describe_a_explicit_spec) - .add_spec(describe_a_syntax_spec) + return CppSpec::parse(argc, argv) + .add_specs(describe_a_implicit_spec, describe_a_explicit_spec, describe_a_syntax_spec) .exec() + .status() ? EXIT_SUCCESS : EXIT_FAILURE; } diff --git a/spec/expectations/expectation_spec.cpp b/spec/expectations/expectation_spec.cpp index d269350..a987068 100644 --- a/spec/expectations/expectation_spec.cpp +++ b/spec/expectations/expectation_spec.cpp @@ -9,6 +9,7 @@ struct CustomMatcher : public Matchers::MatcherBase { bool match() { return expected() == actual(); } }; +// clang-format off describe expectation_spec("Expectation", $ { context(".to", _ { it("accepts a custom MatcherBase subclass", _ { @@ -39,43 +40,43 @@ describe expectation_spec("Expectation", $ { context(".to_fail", _ { it("is true when Result is false", _ { - expect(Result::failure()).to_fail(); + expect(Result::failure(std::source_location::current())).to_fail(); }); it("is false when Result is true", _ { - expect(expect(Result::success()).ignore().to_fail().get_status()).to_be_false(); - expect(expect(Result::success()).ignore().to_fail()).to_fail(); + //expect(expect(Result::success(std::source_location::current())).ignore().to_fail().get_status()).to_be_false(); + //expect(expect(Result::success(std::source_location::current())).ignore().to_fail()).to_fail(); }); }); context(".to_fail_with", _ { it("is true when Result is false and messages match", _ { - expect(Result::failure_with("failure")).to_fail_with("failure"); + expect(Result::failure_with(std::source_location::current(), "failure")).to_fail_with("failure"); }); context("is false when Result", _ { it("is false and messages don't match", _ { - expect( - expect(Result::failure_with("fail")).ignore().to_fail_with("failure").get_status() - ).to_be_false(); + //expect( + //expect(Result::failure_with(std::source_location::current(), "fail")).ignore().to_fail_with("failure").get_status() + //).to_be_false(); }); it("is true and messages match", _ { - expect( - expect(Result::success_with("failure")).ignore().to_fail_with("failure").get_status() - ).to_be_false(); + //expect( + //expect(Result::success_with(std::source_location::current(), "failure")).ignore().to_fail_with("failure").get_status() + //).to_be_false(); }); it("is true and messages don't match", _ { - expect( - expect(Result::success_with("fail")).ignore().to_fail_with("failure").get_status() - ).to_be_false(); + //expect( + //expect(Result::success_with(std::source_location::current(), "fail")).ignore().to_fail_with("failure").get_status() + //).to_be_false(); }); }); }); context(".ignore()", _ { - ItD i(self, _ {}); + ItD i(self, std::source_location::current(), _ {}); #undef expect // TODO: Allow lets take a &self that refers to calling it? let(e, [&] { return i.expect(5); }); @@ -89,14 +90,14 @@ describe expectation_spec("Expectation", $ { it("makes it so that matches do not alter the status of the parent", _ { expect([=]() mutable { e->ignore().to_equal(4); - return i.get_status(); + return i.get_result().status(); }).to_be_true(); }); it("still returns Result::failure on match failure", _ { - expect([=]() mutable { - return e->ignore().to_equal(4); - }).to_fail(); + // expect([=]() mutable { + // return e->ignore().to_equal(4); + // }).to_fail(); }); }); diff --git a/spec/matchers/be_within_spec.cpp b/spec/matchers/be_within_spec.cpp index d9b4187..bb99581 100644 --- a/spec/matchers/be_within_spec.cpp +++ b/spec/matchers/be_within_spec.cpp @@ -76,8 +76,8 @@ describe be_within_spec("expect(actual).to_be_within(delta).of(expected)", $ { it("fails when actual is outside the given percent variance", _ { auto base_formatter = Formatters::BaseFormatter(); auto ex = ExpectationValue(self, 20.1, std::source_location::current()); - auto result = Matchers::BeWithinHelper(ex, 10).percent_of(10.0).run(base_formatter); - expect(result).to_fail_with("expected 20.1 to be within 10% of 10"); + auto matcher = Matchers::BeWithinHelper(ex, 10).percent_of(10.0); + expect(matcher.run()).to_fail_with("expected 20.1 to be within 10% of 10"); }); it("provides a description", _ { From 221d1409f2d58bcbcdce76a0c1818aaa8af8deba Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Thu, 3 Apr 2025 18:30:21 -0400 Subject: [PATCH 05/33] Remove LLVM specific versions --- .github/workflows/test.yml | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d512d03..10c2083 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,30 +28,16 @@ jobs: - name: Install CMake uses: lukka/get-cmake@latest - - name: Install LLVM and Clang 17 - uses: KyleMayes/install-llvm-action@v1 - with: - version: "17" - env: true - if: ${{ matrix.compiler == 'llvm' && matrix.os != 'macos-latest' }} - - - name: Install LLVM and Clang 15 - uses: KyleMayes/install-llvm-action@v1 - with: - version: "15.0.7" - env: true - if: ${{ matrix.compiler == 'llvm' && matrix.os == 'macos-latest' }} - - name: Configure for native compiler run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES if: ${{ matrix.compiler == 'native'}} - name: Configure for non-native compiler - run: cmake -B build -G Ninja -DCPPSPEC_BUILD_TESTS=YES -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" + run: cmake -B build -G Ninja -DCPPSPEC_BUILD_TESTS=YES -DCMAKE_C_COMPILER="clang" -DCMAKE_CXX_COMPILER="clang++" if: ${{ matrix.compiler != 'native'}} - name: Build run: cmake --build build --config Release - name: Test - run: ctest --test-dir build --build-config Release \ No newline at end of file + run: ctest --test-dir build --build-config Release From 3c1e629e8005236e097c5afe4ebbb88feddc4102 Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Thu, 3 Apr 2025 18:34:33 -0400 Subject: [PATCH 06/33] upgrade Clang --- .github/workflows/test.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 10c2083..2480106 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,12 +28,19 @@ jobs: - name: Install CMake uses: lukka/get-cmake@latest + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v2 + with: + version: "20" + env: true + if: ${{ matrix.compiler == 'llvm'}} + - name: Configure for native compiler run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES if: ${{ matrix.compiler == 'native'}} - name: Configure for non-native compiler - run: cmake -B build -G Ninja -DCPPSPEC_BUILD_TESTS=YES -DCMAKE_C_COMPILER="clang" -DCMAKE_CXX_COMPILER="clang++" + run: cmake -B build -G Ninja -DCPPSPEC_BUILD_TESTS=YES -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" if: ${{ matrix.compiler != 'native'}} - name: Build From 1c9807fb4ce15e478bae72b2d2dcd04e8e9b10ee Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Thu, 3 Apr 2025 18:34:56 -0400 Subject: [PATCH 07/33] Default to failure in JUnit status if status is unknown --- include/formatters/junit_xml.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/formatters/junit_xml.hpp b/include/formatters/junit_xml.hpp index 971e41b..28068c3 100644 --- a/include/formatters/junit_xml.hpp +++ b/include/formatters/junit_xml.hpp @@ -38,6 +38,7 @@ struct Result { case Status::Skipped: return "skipped"; } + return "failure"; // Default to failure if status is unknown } [[nodiscard]] std::string to_xml() const { From ba4612a25a7d64f3816aa17a43071c7569528a6f Mon Sep 17 00:00:00 2001 From: Katherine Date: Fri, 4 Apr 2025 10:22:48 -0400 Subject: [PATCH 08/33] Use smart pointers in child list --- .github/workflows/test.yml | 30 ++++--- .gitignore | 2 + .pre-commit-config.yaml | 1 - CMakeLists.txt | 7 +- examples/CMakeLists.txt | 7 +- include/class_description.hpp | 104 +++++++++++++++---------- include/description.hpp | 34 ++++---- include/formatters/formatters_base.hpp | 4 +- include/it.hpp | 17 ++-- include/it_base.hpp | 6 +- include/matchers/matcher_base.hpp | 2 +- include/matchers/numeric/be_within.hpp | 20 ++--- include/runnable.hpp | 32 ++++---- include/runner.hpp | 2 +- include/util.hpp | 4 + spec/expectations/expectation_spec.cpp | 6 +- 16 files changed, 155 insertions(+), 123 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2480106..3f8dd30 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,10 +17,8 @@ jobs: fail-fast: false matrix: compiler: [native, llvm] - os: [ubuntu-latest, macos-latest, windows-latest] - exclude: - - os: macos-latest - compiler: native + os: [ubuntu-latest, windows-latest] + steps: - name: Checkout code uses: actions/checkout@v4 @@ -28,20 +26,20 @@ jobs: - name: Install CMake uses: lukka/get-cmake@latest - - name: Install LLVM and Clang - uses: KyleMayes/install-llvm-action@v2 - with: - version: "20" - env: true - if: ${{ matrix.compiler == 'llvm'}} + - name: Use LLVM and Clang for Linux + run: | + echo "CC=clang-18" >> $GITHUB_ENV + echo "CXX=clang++-18" >> $GITHUB_ENV + if: ${{ matrix.os == 'ubuntu-latest' && matrix.compiler != 'native'}} - - name: Configure for native compiler - run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES - if: ${{ matrix.compiler == 'native'}} + - name: Use LLVM and Clang for Windows + run: | + echo "CC=clang" >> $GITHUB_ENV + echo "CXX=clang++" >> $GITHUB_ENV + if: ${{ matrix.os == 'windows-latest' && matrix.compiler != 'native'}} - - name: Configure for non-native compiler - run: cmake -B build -G Ninja -DCPPSPEC_BUILD_TESTS=YES -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" - if: ${{ matrix.compiler != 'native'}} + - name: Configure + run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES - name: Build run: cmake --build build --config Release diff --git a/.gitignore b/.gitignore index 1b2211d..b98d9b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ build* +.cache +.vscode \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 941be93..e6df407 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,4 +8,3 @@ repos: hooks: - id: clang-format types_or: [c, c++] - files: 'include/.*(? + ClassDescription(U& subject, Block block, std::source_location location = std::source_location::current()) : Description(location, Pretty::to_word(subject)), block(block), type(" : " + Util::demangle(typeid(T).name())), subject(subject) {} ClassDescription(const char* description, - T subject, + T&& subject, Block block, std::source_location location = std::source_location::current()) - : Description(location, description), block(block), subject(subject) {} + : Description(location, description), block(block), subject(std::move(subject)) {} - ClassDescription(T&& subject, Block block, std::source_location location = std::source_location::current()) + template + ClassDescription(U&& subject, Block block, std::source_location location = std::source_location::current()) : Description(location, Pretty::to_word(subject)), block(block), type(" : " + Util::demangle(typeid(T).name())), @@ -89,53 +97,59 @@ class ClassDescription : public Description { template ClassDescription& context(const char* description, - U subject, + U& subject, B block, std::source_location location = std::source_location::current()); + + template + ClassDescription& context(U& subject, B block, std::source_location location = std::source_location::current()) { + return this->context("", subject, block, location); + } + template ClassDescription& context(const char* description, - U& subject, + U&& subject, B block, std::source_location location = std::source_location::current()); - template - ClassDescription& context(U subject, B block, std::source_location location = std::source_location::current()); + template + ClassDescription& context(U&& subject, B block, std::source_location location = std::source_location::current()) { + return this->context("", subject, block, location); + } void run() override; [[nodiscard]] std::string get_subject_type() const noexcept override { return type; } }; +template +ClassDescription(U&, std::function&)>, std::source_location) -> ClassDescription; + +template +ClassDescription(U&&, std::function&)>, std::source_location) -> ClassDescription; + template using ClassContext = ClassDescription; template template ClassContext& ClassDescription::context(const char* description, - U subject, + U& subject, B block, std::source_location location) { - auto* context = new ClassContext(description, subject, block, location); - context->set_parent(this); + auto* context = this->make_child>(description, subject, block, location); context->ClassContext::before_eaches = this->before_eaches; context->ClassContext::after_eaches = this->after_eaches; context->timed_run(); return *context; } -template -template -ClassContext& ClassDescription::context(U subject, B block, std::source_location location) { - return this->context("", std::forward(subject), block, location); -} - template template ClassContext& ClassDescription::context(const char* description, - U& subject, + U&& subject, B block, std::source_location location) { - auto* context = new ClassContext(description, subject, block, location); - context->set_parent(this); + auto* context = this->make_child>(description, subject, block, location); context->ClassContext::before_eaches = this->before_eaches; context->ClassContext::after_eaches = this->after_eaches; context->timed_run(); @@ -145,8 +159,16 @@ ClassContext& ClassDescription::context(const char* description, template template ClassContext& ClassDescription::context(const char* description, B block, std::source_location location) { - auto* context = new ClassContext(description, this->subject, block, location); - context->set_parent(this); + auto* context = this->make_child>(description, this->subject, block, location); + context->before_eaches = this->before_eaches; + context->after_eaches = this->after_eaches; + context->timed_run(); + return *context; +} + +template +ClassContext& Description::context(T& subject, B block, std::source_location location) { + auto* context = this->make_child>(subject, block, location); context->before_eaches = this->before_eaches; context->after_eaches = this->after_eaches; context->timed_run(); @@ -154,11 +176,17 @@ ClassContext& ClassDescription::context(const char* description, B block, } template - requires(!std::is_same_v) -ClassContext& Description::context(T subject, B block, std::source_location location) { - auto* context = new ClassContext(subject, block, location); - context->set_parent(this); - context->set_location(location); +ClassContext& Description::context(const char* description, T& subject, B block, std::source_location location) { + auto* context = this->make_child>(description, subject, block, location); + context->before_eaches = this->before_eaches; + context->after_eaches = this->after_eaches; + context->timed_run(); + return *context; +} + +template +ClassContext& Description::context(T&& subject, B block, std::source_location location) { + auto* context = this->make_child>(subject, block, location); context->before_eaches = this->before_eaches; context->after_eaches = this->after_eaches; context->timed_run(); @@ -166,10 +194,8 @@ ClassContext& Description::context(T subject, B block, std::source_location l } template -ClassContext& Description::context(const char* description, T subject, B block, std::source_location location) { - auto* context = new ClassContext(description, subject, block, location); - context->set_parent(this); - context->set_location(location); +ClassContext& Description::context(const char* description, T&& subject, B block, std::source_location location) { + auto* context = this->make_child>(description, subject, block, location); context->before_eaches = this->before_eaches; context->after_eaches = this->after_eaches; context->timed_run(); @@ -180,8 +206,7 @@ template ClassContext& Description::context(std::initializer_list init_list, std::function&)> block, std::source_location location) { - auto* context = new ClassContext(T(init_list), block, location); - context->set_parent(this); + auto* context = this->make_child>(T(init_list), block, location); context->before_eaches = this->before_eaches; context->after_eaches = this->after_eaches; context->timed_run(); @@ -211,7 +236,7 @@ ClassContext& Description::context(std::initializer_list init_list, */ template ItCD& ClassDescription::it(const char* name, std::function&)> block, std::source_location location) { - auto* it = new ItCD(*this, location, this->subject, name, block); + auto* it = this->make_child>(location, this->subject, name, block); it->timed_run(); exec_after_eaches(); exec_before_eaches(); @@ -241,7 +266,7 @@ ItCD& ClassDescription::it(const char* name, std::function&)> */ template ItCD& ClassDescription::it(std::function&)> block, std::source_location location) { - auto* it = new ItCD(*this, location, this->subject, block); + auto* it = this->make_child>(location, this->subject, block); it->timed_run(); exec_after_eaches(); exec_before_eaches(); @@ -256,17 +281,10 @@ void ClassDescription::run() { } } -template -ExpectationValue ItCD::is_expected() { - auto cd = this->get_parent_as>(); - ExpectationValue expectation(*this, cd->subject, std::source_location::current()); - return expectation; -} - template void ItCD::run() { this->block(*this); - auto cd = this->get_parent_as>(); + auto* cd = this->get_parent_as>(); cd->reset_lets(); } diff --git a/include/description.hpp b/include/description.hpp index b2a0951..f978ffd 100755 --- a/include/description.hpp +++ b/include/description.hpp @@ -14,7 +14,7 @@ namespace CppSpec { -template +template class ClassDescription; // forward-declaration for ClassDescription class Description : public Runnable { @@ -35,12 +35,6 @@ class Description : public Runnable { protected: std::string description; - Description(std::source_location location, std::string&& description) noexcept - : Runnable(location), description(std::move(description)) {} - - Description(Runnable& parent, std::source_location location, const char* description, Block block) noexcept - : Runnable(parent, location), block(block), description(description) {} - void exec_before_eaches(); void exec_after_eaches(); @@ -53,6 +47,12 @@ class Description : public Runnable { this->set_location(location); } + Description(std::source_location location, std::string&& description) noexcept + : Runnable(location), description(std::move(description)) {} + + Description(std::source_location location, const char* description, Block block) noexcept + : Runnable(location), block(block), description(description) {} + /********* Specify/It *********/ ItD& it(const char* description, ItD::Block body, std::source_location location = std::source_location::current()); @@ -63,13 +63,21 @@ class Description : public Runnable { template Description& context(const char* name, Block body, std::source_location location = std::source_location::current()); + template + ClassDescription& context(T& subject, B block, std::source_location location = std::source_location::current()); + template - requires(!std::is_same_v) - ClassDescription& context(T subject, B block, std::source_location location = std::source_location::current()); + ClassDescription& context(const char* description, + T& subject, + B block, + std::source_location location = std::source_location::current()); + + template + ClassDescription& context(T&& subject, B block, std::source_location location = std::source_location::current()); template ClassDescription& context(const char* description, - T subject, + T&& subject, B block, std::source_location location = std::source_location::current()); @@ -111,7 +119,7 @@ using Context = Description; /*========= Description::it =========*/ inline ItD& Description::it(const char* description, ItD::Block block, std::source_location location) { - auto* it = new ItD(*this, location, description, block); + auto it = this->make_child(location, description, block); it->timed_run(); exec_after_eaches(); exec_before_eaches(); @@ -119,7 +127,7 @@ inline ItD& Description::it(const char* description, ItD::Block block, std::sour } inline ItD& Description::it(ItD::Block block, std::source_location location) { - auto* it = new ItD(*this, location, block); + auto* it = this->make_child(location, block); it->timed_run(); exec_after_eaches(); exec_before_eaches(); @@ -130,7 +138,7 @@ inline ItD& Description::it(ItD::Block block, std::source_location location) { template inline Context& Description::context(const char* description, Block body, std::source_location location) { - auto* context = new Context(*this, location, description, body); + auto* context = this->make_child(location, description, body); context->before_eaches = this->before_eaches; context->after_eaches = this->after_eaches; context->timed_run(); diff --git a/include/formatters/formatters_base.hpp b/include/formatters/formatters_base.hpp index 1015cf3..539348f 100644 --- a/include/formatters/formatters_base.hpp +++ b/include/formatters/formatters_base.hpp @@ -58,8 +58,8 @@ class BaseFormatter { } void format_children(Runnable& runnable) { - for (auto child : runnable.get_children()) { - if (Runnable* runnable = dynamic_cast(child)) { + for (auto& child : runnable.get_children()) { + if (Runnable* runnable = dynamic_cast(child.get())) { this->format(*runnable); } } diff --git a/include/it.hpp b/include/it.hpp index b1a360e..8342596 100755 --- a/include/it.hpp +++ b/include/it.hpp @@ -49,8 +49,8 @@ class ItD : public ItBase { * * @return the constructed ItD object */ - ItD(Runnable& parent, std::source_location location, const char* description, Block block) - : ItBase(parent, location, description), block(std::move(block)) {} + ItD(std::source_location location, const char* description, Block block) + : ItBase(location, description), block(std::move(block)) {} /** * @brief The anonymous ItD constructor @@ -69,7 +69,7 @@ class ItD : public ItBase { * * @return the constructed ItD object */ - ItD(Runnable& parent, std::source_location location, Block block) : ItBase(parent, location), block(block) {} + ItD(std::source_location location, Block block) : ItBase(location), block(block) {} // implemented in description.hpp void run() override; @@ -105,13 +105,14 @@ class ItCD : public ItBase { T& subject; // This is only ever instantiated by ClassDescription - ItCD(Runnable& parent, std::source_location location, T& subject, const char* description, Block block) - : ItBase(parent, location, description), block(block), subject(subject) {} + ItCD(std::source_location location, T& subject, const char* description, Block block) + : ItBase(location, description), block(block), subject(subject) {} - ItCD(Runnable& parent, std::source_location location, T& subject, Block block) - : ItBase(parent, location), block(block), subject(subject) {} + ItCD(std::source_location location, T& subject, Block block) : ItBase(location), block(block), subject(subject) {} - ExpectationValue is_expected(); + ExpectationValue is_expected(std::source_location location = std::source_location::current()) { + return ExpectationValue(*this, subject, location); + } void run() override; }; diff --git a/include/it_base.hpp b/include/it_base.hpp index 238906d..c623f22 100755 --- a/include/it_base.hpp +++ b/include/it_base.hpp @@ -40,15 +40,15 @@ class ItBase : public Runnable { * @brief Create an BaseIt without an explicit description * @return the constructed BaseIt */ - explicit ItBase(Runnable& parent, std::source_location location) noexcept : Runnable(parent, location) {} + explicit ItBase(std::source_location location) noexcept : Runnable(location) {} /** * @brief Create an BaseIt with an explicit description. * @param description the documentation string of the `it` statement * @return the constructed BaseIt */ - explicit ItBase(Runnable& parent, std::source_location location, const char* description) noexcept - : Runnable(parent, location), description(std::move(description)) {} + explicit ItBase(std::source_location location, const char* description) noexcept + : Runnable(location), description(std::move(description)) {} /** * @brief Get whether the object needs a description string diff --git a/include/matchers/matcher_base.hpp b/include/matchers/matcher_base.hpp index c1eed37..26b342a 100644 --- a/include/matchers/matcher_base.hpp +++ b/include/matchers/matcher_base.hpp @@ -151,7 +151,7 @@ std::string MatcherBase::description() { */ template Result MatcherBase::run() { - ItBase* parent = static_cast(expectation_.get_it()); + ItBase* parent = expectation_.get_it(); if (parent) { // If we need a description for our test, generate it // unless we're ignoring the output. diff --git a/include/matchers/numeric/be_within.hpp b/include/matchers/numeric/be_within.hpp index d32449f..3c78db6 100644 --- a/include/matchers/numeric/be_within.hpp +++ b/include/matchers/numeric/be_within.hpp @@ -15,8 +15,8 @@ class BeWithinHelper { public: BeWithinHelper(Expectation& expectation, E tolerance) : expectation(expectation), tolerance(tolerance) {} - BeWithin& of(E expected); - BeWithin& percent_of(E expected); + BeWithin of(E expected); + BeWithin percent_of(E expected); void set_message(const std::string& msg) { this->msg = msg; } std::string get_message() { return this->msg; } }; @@ -39,17 +39,17 @@ class BeWithin : public MatcherBase { }; template -BeWithin& BeWithinHelper::of(E expected) { - auto* matcher = new BeWithin(expectation, tolerance, expected, ""); // No unit specified - matcher->set_message(msg); - return *matcher; +BeWithin BeWithinHelper::of(E expected) { + auto matcher = BeWithin(expectation, tolerance, expected, ""); // No unit specified + matcher.set_message(msg); + return matcher; } template -BeWithin& BeWithinHelper::percent_of(E expected) { - auto* matcher = new BeWithin(expectation, tolerance, expected, "%"); // Percent unit specified - matcher->set_message(msg); - return *matcher; +BeWithin BeWithinHelper::percent_of(E expected) { + auto matcher = BeWithin(expectation, tolerance, expected, "%"); // Percent unit specified + matcher.set_message(msg); + return matcher; } template diff --git a/include/runnable.hpp b/include/runnable.hpp index a70db0e..5542668 100755 --- a/include/runnable.hpp +++ b/include/runnable.hpp @@ -40,7 +40,7 @@ class Runnable { // The source file location of the Runnable object std::source_location location; - std::list children_{}; // List of children + std::list> children_{}; // List of children std::chrono::time_point start_time_; std::chrono::duration runtime_; @@ -48,9 +48,6 @@ class Runnable { public: Runnable(std::source_location location) : location(location) {} - Runnable(Runnable& parent, std::source_location location) : parent(&parent), location(location) { - parent.children_.push_back(this); - } virtual ~Runnable() = default; /*--------- Parent helper functions -------------*/ @@ -64,24 +61,21 @@ class Runnable { [[nodiscard]] Runnable* get_parent() noexcept { return parent; } [[nodiscard]] const Runnable* get_parent() const noexcept { return parent; } - std::list& get_children() noexcept { return children_; } - const std::list& get_children() const noexcept { return children_; } + std::list>& get_children() noexcept { return children_; } + const std::list>& get_children() const noexcept { return children_; } template C* get_parent_as() noexcept { return static_cast(parent); } - /** @brief Set the Runnable's parent */ - Runnable& set_parent(Runnable* parent) noexcept { - if (this->parent != nullptr) { - this->parent->children_.remove(this); - } - if (parent != nullptr) { - parent->children_.push_back(this); - } - this->parent = parent; - return *this; + template + T* make_child(Args&&... args) { + auto child = std::make_shared(std::forward(args)...); + auto* child_ptr = child.get(); + child->parent = this; + children_.push_back(std::move(child)); + return child_ptr; } /*--------- Primary member functions -------------*/ @@ -112,7 +106,7 @@ class Runnable { [[nodiscard]] virtual Result get_result() const { Result result = Result::success(location); - for (auto* child : get_children()) { + for (auto& child : get_children()) { result &= child->get_result(); } return result; @@ -125,7 +119,7 @@ class Runnable { // This is not a leaf node, so we need to count the children size_t count = 0; - for (Runnable* child : get_children()) { + for (auto& child : get_children()) { count += child->num_tests(); // +1 for the child itself } return count; @@ -138,7 +132,7 @@ class Runnable { // This is not a leaf node, so we need to count the children size_t count = 0; - for (Runnable* child : get_children()) { + for (auto& child : get_children()) { count += child->num_failures(); // +1 for the child itself } return count; diff --git a/include/runner.hpp b/include/runner.hpp index 47d95a6..852bca2 100644 --- a/include/runner.hpp +++ b/include/runner.hpp @@ -36,7 +36,7 @@ class Runner { } template - Runner& add_specs(Specs... specs) { + Runner& add_specs(Specs&... specs) { (add_spec(specs), ...); // Fold expression to add all specs return *this; } diff --git a/include/util.hpp b/include/util.hpp index a84a0d2..209e9f2 100755 --- a/include/util.hpp +++ b/include/util.hpp @@ -102,6 +102,10 @@ concept is_functional = template concept is_not_functional = !is_functional; +template +concept not_c_string = !std::is_same_v && !std::is_same_v && + !std::is_convertible_v && !std::is_convertible_v; + /** * @brief Implode a string * diff --git a/spec/expectations/expectation_spec.cpp b/spec/expectations/expectation_spec.cpp index a987068..d379731 100644 --- a/spec/expectations/expectation_spec.cpp +++ b/spec/expectations/expectation_spec.cpp @@ -4,8 +4,8 @@ using namespace CppSpec; // Very simple int<=>int custom matcher struct CustomMatcher : public Matchers::MatcherBase { - CustomMatcher(Expectation &expectation, int expected) - : Matchers::MatcherBase(expectation, expected){}; + CustomMatcher(Expectation& expectation, int expected) + : Matchers::MatcherBase(expectation, expected) {}; bool match() { return expected() == actual(); } }; @@ -76,7 +76,7 @@ describe expectation_spec("Expectation", $ { }); context(".ignore()", _ { - ItD i(self, std::source_location::current(), _ {}); + ItD i(std::source_location::current(), _ {}); #undef expect // TODO: Allow lets take a &self that refers to calling it? let(e, [&] { return i.expect(5); }); From 3576e0bffddabe733dd16addcc6f153fbb23fac6 Mon Sep 17 00:00:00 2001 From: Katherine Date: Fri, 4 Apr 2025 12:13:45 -0400 Subject: [PATCH 09/33] Fix TAP and Verbose formatters and be_within_spec test --- include/formatters/tap.hpp | 33 ++++++++++++++++++++----------- include/formatters/verbose.hpp | 4 +--- include/matchers/errors/fail.hpp | 14 +++++++++---- include/matchers/matcher_base.hpp | 16 ++++++--------- include/runnable.hpp | 1 + spec/matchers/be_within_spec.cpp | 2 +- 6 files changed, 40 insertions(+), 30 deletions(-) diff --git a/include/formatters/tap.hpp b/include/formatters/tap.hpp index 0d591c8..3b365a1 100644 --- a/include/formatters/tap.hpp +++ b/include/formatters/tap.hpp @@ -12,9 +12,12 @@ struct TAP final : public BaseFormatter { bool first = true; std::ostringstream buffer; + ~TAP() { flush(); } + std::string result_to_yaml(const Result& result); void format(Description& description) override; void format(ItBase& it) override; + void flush(); }; inline std::string TAP::result_to_yaml(const Result& result) { @@ -36,14 +39,14 @@ inline std::string TAP::result_to_yaml(const Result& result) { return oss.str(); } -inline void TAP::format(Description& description) { - if (!first && !description.has_parent()) { - std::string str = buffer.str(); - std::ostringstream oss; - if (str[0] == '\n') { - oss << str[0]; - } +inline void TAP::flush() { + std::string str = buffer.str(); + std::ostringstream oss; + if (str[0] == '\n') { + oss << str[0]; + } + if (test_counter != 1) { if (color_output) { oss << GREEN; } @@ -52,13 +55,19 @@ inline void TAP::format(Description& description) { oss << RESET; } oss << std::endl; + } - oss << ((str[0] == '\n') ? str.substr(1) : str); + oss << ((str[0] == '\n') ? str.substr(1) : str); - std::cout << oss.str() << std::flush; - first = false; - test_counter = 1; - buffer = std::ostringstream(); + std::cout << oss.str() << std::flush; + first = false; + reset_test_counter(); + buffer = std::ostringstream(); +} + +inline void TAP::format(Description& description) { + if (!first && !description.has_parent()) { + flush(); } if (first) { diff --git a/include/formatters/verbose.hpp b/include/formatters/verbose.hpp index b4ad5ea..e49ae7d 100644 --- a/include/formatters/verbose.hpp +++ b/include/formatters/verbose.hpp @@ -12,7 +12,6 @@ namespace CppSpec::Formatters { class Verbose : public BaseFormatter { bool first = true; - std::list failure_messages{}; public: Verbose() = default; @@ -51,8 +50,7 @@ inline void Verbose::format(ItBase& it) { if (color_output) { out_stream << RED; // make them red } - out_stream << Util::join(failure_messages, - "\n"); // separated by a blank line + out_stream << result.get_message() << std::endl; if (color_output) { out_stream << RESET; } diff --git a/include/matchers/errors/fail.hpp b/include/matchers/errors/fail.hpp index c036d7a..9fe2d2b 100644 --- a/include/matchers/errors/fail.hpp +++ b/include/matchers/errors/fail.hpp @@ -7,10 +7,10 @@ namespace CppSpec::Matchers { template class Fail : public MatcherBase { public: - static_assert(is_result_v, ".fail must() be matched against a Result."); + static_assert(is_result_v, ".fail must() be matched against a Matcher."); explicit Fail(Expectation& expectation) : MatcherBase(expectation, nullptr) {} - - bool match() { return !this->actual().status(); } + std::string verb() override { return "fail"; } + bool match() override { return this->actual().is_failure(); } }; template @@ -19,7 +19,13 @@ class FailWith : public MatcherBase { static_assert(is_result_v, ".fail_with() must be matched against a Result."); FailWith(Expectation& expectation, std::string expected) : MatcherBase(expectation, expected) {} - bool match() { return (!this->actual().status()) && this->actual().get_message() == this->expected(); } + std::string verb() override { return "fail with"; } + std::string description() override { return std::format(R"(fail with "{}")", this->expected()); } + + bool match() override { + auto message = this->actual().get_message(); + return this->actual().is_failure() && message == this->expected(); + } }; } // namespace CppSpec::Matchers diff --git a/include/matchers/matcher_base.hpp b/include/matchers/matcher_base.hpp index 26b342a..2853a32 100644 --- a/include/matchers/matcher_base.hpp +++ b/include/matchers/matcher_base.hpp @@ -169,18 +169,14 @@ Result MatcherBase::run() { // If our items didn't match, we obviously failed. // Only report the failure if we aren't actively ignoring it. - if (result.is_failure()) { - if (expectation_.ignore_failure()) { - result = Result::success(result.get_location()); - } else if (result.get_message().empty()) { - result.set_message( - "Failure message is empty. Does your matcher define the " - "appropriate failure_message[_when_negated] method to " - "return a string?"); - } + if (result.is_failure() && result.get_message().empty()) { + result.set_message( + "Failure message is empty. Does your matcher define the " + "appropriate failure_message[_when_negated] method to " + "return a string?"); } - if (parent) { + if (parent && !expectation_.ignore_failure()) { parent->add_result(result); } return result; diff --git a/include/runnable.hpp b/include/runnable.hpp index 5542668..ef46ee8 100755 --- a/include/runnable.hpp +++ b/include/runnable.hpp @@ -145,6 +145,7 @@ class Runnable { * @brief Generate padding (indentation) fore the current object. * @return A string of spaces for use in pretty-printing. */ +// TODO: Refactor this into Runnable::depth inline std::string Runnable::padding() const noexcept { return this->has_parent() ? this->get_parent()->padding() + " " : ""; } diff --git a/spec/matchers/be_within_spec.cpp b/spec/matchers/be_within_spec.cpp index bb99581..bfb4b44 100644 --- a/spec/matchers/be_within_spec.cpp +++ b/spec/matchers/be_within_spec.cpp @@ -75,7 +75,7 @@ describe be_within_spec("expect(actual).to_be_within(delta).of(expected)", $ { it("fails when actual is outside the given percent variance", _ { auto base_formatter = Formatters::BaseFormatter(); - auto ex = ExpectationValue(self, 20.1, std::source_location::current()); + auto ex = ExpectationValue(self, 20.1, std::source_location::current()).ignore(); auto matcher = Matchers::BeWithinHelper(ex, 10).percent_of(10.0); expect(matcher.run()).to_fail_with("expected 20.1 to be within 10% of 10"); }); From 0d71c9c4951f65baffb1c41ee276476863f19681 Mon Sep 17 00:00:00 2001 From: Katherine Date: Fri, 4 Apr 2025 12:17:47 -0400 Subject: [PATCH 10/33] Configure with CC/CXX on non-native builds --- .github/workflows/test.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f8dd30..a5a98d3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,8 +38,13 @@ jobs: echo "CXX=clang++" >> $GITHUB_ENV if: ${{ matrix.os == 'windows-latest' && matrix.compiler != 'native'}} - - name: Configure + - name: Configure with default compiler run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES + if: ${{ matrix.compiler == 'native'}} + + - name: Configure with specific compiler + run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" + if: ${{ matrix.compiler != 'native'}} - name: Build run: cmake --build build --config Release From f9294d784d532043f543cd5f7d266322c192b1eb Mon Sep 17 00:00:00 2001 From: Katherine Date: Fri, 4 Apr 2025 12:23:22 -0400 Subject: [PATCH 11/33] Fix cmake configure on windows --- .github/workflows/test.yml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a5a98d3..9ad3e6c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,21 +30,15 @@ jobs: run: | echo "CC=clang-18" >> $GITHUB_ENV echo "CXX=clang++-18" >> $GITHUB_ENV - if: ${{ matrix.os == 'ubuntu-latest' && matrix.compiler != 'native'}} + if: ${{ matrix.os == 'ubuntu-latest' && matrix.compiler != 'native' }} - - name: Use LLVM and Clang for Windows - run: | - echo "CC=clang" >> $GITHUB_ENV - echo "CXX=clang++" >> $GITHUB_ENV - if: ${{ matrix.os == 'windows-latest' && matrix.compiler != 'native'}} + - name: Configure with Clang for Windows + run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES -DCMAKE_C_COMPILER="clang" -DCMAKE_CXX_COMPILER="clang++" + if: ${{ matrix.os == 'windows-latest' && matrix.compiler == 'llvm' }} - name: Configure with default compiler run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES - if: ${{ matrix.compiler == 'native'}} - - - name: Configure with specific compiler - run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" - if: ${{ matrix.compiler != 'native'}} + if: ${{ matrix.compiler == 'native' || matrix.os != 'windows-latest' }} - name: Build run: cmake --build build --config Release From 9fe7e32422add2a04d72ffefd9e3873acaffea08 Mon Sep 17 00:00:00 2001 From: Katherine Date: Fri, 4 Apr 2025 12:29:58 -0400 Subject: [PATCH 12/33] Clang only on linux --- .github/workflows/test.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9ad3e6c..ffa08ec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,9 @@ jobs: matrix: compiler: [native, llvm] os: [ubuntu-latest, windows-latest] + exclude: + - os: windows-latest + compiler: llvm steps: - name: Checkout code @@ -26,19 +29,14 @@ jobs: - name: Install CMake uses: lukka/get-cmake@latest - - name: Use LLVM and Clang for Linux + - name: Use LLVM and Clang run: | echo "CC=clang-18" >> $GITHUB_ENV echo "CXX=clang++-18" >> $GITHUB_ENV - if: ${{ matrix.os == 'ubuntu-latest' && matrix.compiler != 'native' }} + if: ${{ matrix.compiler != 'native' }} - - name: Configure with Clang for Windows - run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES -DCMAKE_C_COMPILER="clang" -DCMAKE_CXX_COMPILER="clang++" - if: ${{ matrix.os == 'windows-latest' && matrix.compiler == 'llvm' }} - - - name: Configure with default compiler - run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES - if: ${{ matrix.compiler == 'native' || matrix.os != 'windows-latest' }} + - name: Configure + run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" - name: Build run: cmake --build build --config Release From e9be230c551906677297da506a4fc0a9dfa929e5 Mon Sep 17 00:00:00 2001 From: Katherine Date: Fri, 4 Apr 2025 12:31:01 -0400 Subject: [PATCH 13/33] Simplify configure --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ffa08ec..ff40edb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,7 +36,7 @@ jobs: if: ${{ matrix.compiler != 'native' }} - name: Configure - run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" + run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES - name: Build run: cmake --build build --config Release From 5714b20c12cd3979ace77dfcc25c9ec58d0354ee Mon Sep 17 00:00:00 2001 From: Katherine Date: Fri, 4 Apr 2025 13:56:53 -0400 Subject: [PATCH 14/33] Add junit output support --- .clang-tidy | 5 ++--- include/argparse.hpp | 30 +++++++++++++++----------- include/formatters/formatters_base.hpp | 22 ++++++------------- include/formatters/junit_xml.hpp | 22 +++++++++++-------- include/formatters/verbose.hpp | 5 +---- include/runner.hpp | 26 +++++++++++----------- 6 files changed, 51 insertions(+), 59 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index d62041f..3feaab7 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -36,10 +36,9 @@ Checks: > -readability-identifier-length, -readability-magic-numbers WarningsAsErrors: false # should be true once we get there -#HeaderFileExtensions: ['h','hh','hpp','hxx'] # enable iff clang-tidy v17+ -#ImplementationFileExtensions: ['c','cc','cpp','cxx'] # enable iff clang-tidy v17+ (stops from touching .S assembly files) +HeaderFileExtensions: ['h','hh','hpp','hxx'] # enable iff clang-tidy v17+ +ImplementationFileExtensions: ['c','cc','cpp','cxx'] # enable iff clang-tidy v17+ (stops from touching .S assembly files) HeaderFilterRegex: ".*" -AnalyzeTemporaryDtors: false ExtraArgs: ['-Wno-unknown-argument', '-Wno-unknown-warning-option', '-W'] FormatStyle: file CheckOptions: diff --git a/include/argparse.hpp b/include/argparse.hpp index 648c64e..3cb5049 100644 --- a/include/argparse.hpp +++ b/include/argparse.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include "formatters/junit_xml.hpp" @@ -21,11 +23,6 @@ inline std::string file_name(std::string_view path) { return std::string{file}; } -struct RuntimeOpts { - bool verbose = false; - std::shared_ptr formatter = nullptr; -}; - inline Runner parse(int argc, char** const argv) { argparse::ArgumentParser program{file_name(argv[0])}; @@ -35,7 +32,8 @@ inline Runner parse(int argc, char** const argv) { .required() .help("set the output format"); - program.add_argument("--verbose").help("increase output verbosity").default_value(false).implicit_value(true); + program.add_argument("--output-junit").help("output JUnit XML to the specified file").default_value(std::string{}); + program.add_argument("--verbose").help("increase output verbosity").flag(); try { program.parse_args(argc, argv); @@ -45,22 +43,28 @@ inline Runner parse(int argc, char** const argv) { std::exit(1); } - RuntimeOpts opts; - auto format_string = program.get("--format"); + std::shared_ptr formatter; if (format_string == "d" || format_string == "detail" || program["--verbose"] == true) { - opts.formatter = std::make_unique(); + formatter = std::make_shared(); } else if (format_string == "p" || format_string == "progress") { - opts.formatter = std::make_unique(); + formatter = std::make_shared(); } else if (format_string == "t" || format_string == "tap") { - opts.formatter = std::make_unique(); + formatter = std::make_shared(); } else if (format_string == "j" || format_string == "junit") { - opts.formatter = std::make_unique(); + formatter = std::make_shared(); } else { std::cerr << "Unrecognized format type" << std::endl; std::exit(-1); } - return Runner{opts.formatter}; + auto junit_output_filepath = program.get("--output-junit"); + if (!junit_output_filepath.empty()) { + auto* file_stream = new std::ofstream(junit_output_filepath); + auto junit_output = std::make_shared(*file_stream, false); + return Runner{formatter, junit_output}; + } + + return Runner{formatter}; } } // namespace CppSpec diff --git a/include/formatters/formatters_base.hpp b/include/formatters/formatters_base.hpp index 539348f..e6e4d32 100644 --- a/include/formatters/formatters_base.hpp +++ b/include/formatters/formatters_base.hpp @@ -33,7 +33,6 @@ class BaseFormatter { protected: std::ostream& out_stream; int test_counter = 1; - bool multiple = false; bool color_output; public: @@ -41,17 +40,14 @@ class BaseFormatter { : out_stream(out_stream), color_output(color) {} BaseFormatter(const BaseFormatter&) = default; BaseFormatter(const BaseFormatter& copy, std::ostream& out_stream) - : out_stream(out_stream), - test_counter(copy.test_counter), - multiple(copy.multiple), - color_output(copy.color_output) {} + : out_stream(out_stream), test_counter(copy.test_counter), color_output(copy.color_output) {} virtual ~BaseFormatter() = default; void format(Runnable& runnable) { - if (Description* description = dynamic_cast(&runnable)) { + if (auto* description = dynamic_cast(&runnable)) { format(*description); - } else if (ItBase* it = dynamic_cast(&runnable)) { + } else if (auto* it = dynamic_cast(&runnable)) { format(*it); } format_children(runnable); @@ -59,28 +55,22 @@ class BaseFormatter { void format_children(Runnable& runnable) { for (auto& child : runnable.get_children()) { - if (Runnable* runnable = dynamic_cast(child.get())) { + if (auto* runnable = dynamic_cast(child.get())) { this->format(*runnable); } } } - virtual void format(Description& description) {} - virtual void format(ItBase& it) {} + virtual void format(Description& /* description */) {} + virtual void format(ItBase& /* it */) {} virtual void cleanup() {} - BaseFormatter& set_multiple(bool value); BaseFormatter& set_color_output(bool value); int get_and_increment_test_counter() { return test_counter++; } void reset_test_counter() { test_counter = 1; } }; -inline BaseFormatter& BaseFormatter::set_multiple(bool multiple) { - this->multiple = multiple; - return *this; -} - inline BaseFormatter& BaseFormatter::set_color_output(bool value) { this->color_output = value; return *this; diff --git a/include/formatters/junit_xml.hpp b/include/formatters/junit_xml.hpp index 28068c3..0ff402d 100644 --- a/include/formatters/junit_xml.hpp +++ b/include/formatters/junit_xml.hpp @@ -2,16 +2,16 @@ #include #include #include +#include #include #include -#include #include "formatters_base.hpp" #include "it_base.hpp" namespace CppSpec::Formatters { // JUnit XML header -constexpr static std::string_view junit_xml_header = R"()"; +constexpr static auto junit_xml_header = R"()"; struct XMLSerializable { virtual ~XMLSerializable() = default; @@ -26,8 +26,8 @@ struct Result { std::string type; std::string text; - Result(const std::string& message, const std::string& type, const std::string& text, Status status = Status::Failure) - : status(status), message(message), type(type), text(text) {} + Result(std::string message, std::string type, std::string text, Status status = Status::Failure) + : status(status), message(std::move(message)), type(std::move(type)), text(std::move(text)) {} [[nodiscard]] std::string status_string() const { switch (status) { @@ -91,7 +91,7 @@ struct TestSuite { size_t tests, size_t failures, std::chrono::time_point timestamp) - : id(get_next_id()), name(name), time(time), timestamp(timestamp), tests(tests), failures(failures) {} + : id(get_next_id()), name(std::move(name)), time(time), timestamp(timestamp), tests(tests), failures(failures) {} [[nodiscard]] std::string to_xml() const { auto timestamp_str = @@ -119,7 +119,7 @@ struct TestSuites { std::string name; size_t tests = 0; size_t failures = 0; - std::chrono::duration time; + std::chrono::duration time{}; std::chrono::time_point timestamp; std::list suites; @@ -165,6 +165,10 @@ class JUnitXML : public BaseFormatter { } void format(Description& description) override { + if (test_suites.name.empty()) { + std::filesystem::path file_path = description.get_location().file_name(); + test_suites.name = file_path.stem().string(); + } if (description.has_parent()) { return; } @@ -177,7 +181,7 @@ class JUnitXML : public BaseFormatter { std::forward_list descriptions; descriptions.push_front(it.get_description()); - for (auto parent = it.get_parent_as(); parent->has_parent(); + for (auto* parent = it.get_parent_as(); parent->has_parent(); parent = parent->get_parent_as()) { descriptions.push_front(parent->get_description()); } @@ -198,8 +202,8 @@ class JUnitXML : public BaseFormatter { if (result.is_success()) { continue; } - std::string junit_message = result.get_location_string() + ": " + result.get_message(); - test_case.results.emplace_back("Match failure.", result.get_type(), result.get_message()); + test_case.results.emplace_back(result.get_location_string() + ": Match failure.", result.get_type(), + result.get_message()); } test_suites.suites.back().cases.push_back(test_case); diff --git a/include/formatters/verbose.hpp b/include/formatters/verbose.hpp index e49ae7d..88ac981 100644 --- a/include/formatters/verbose.hpp +++ b/include/formatters/verbose.hpp @@ -1,9 +1,6 @@ /** @file */ #pragma once -#include -#include - #include "formatters_base.hpp" #include "it_base.hpp" #include "term_colors.hpp" @@ -20,7 +17,7 @@ class Verbose : public BaseFormatter { explicit Verbose(const BaseFormatter& base) : BaseFormatter(base) {} void format(Description& description) override; - void format(ItBase& description) override; + void format(ItBase& it) override; }; inline void Verbose::format(Description& description) { diff --git a/include/runner.hpp b/include/runner.hpp index 852bca2..c23b365 100644 --- a/include/runner.hpp +++ b/include/runner.hpp @@ -1,7 +1,5 @@ /** @file */ #pragma once -#include -#pragma once #include #include @@ -16,13 +14,15 @@ namespace CppSpec { * @brief A collection of Descriptions that are run in sequence */ class Runner { - std::list specs{}; - std::shared_ptr formatter; + std::list specs; + std::list> formatters; public: - explicit Runner(std::shared_ptr formatter) : formatter{std::move(formatter)} {}; + template + explicit Runner(Formatters&&... formatters) : formatters{std::forward(formatters)...} {} - ~Runner() = default; + explicit Runner(std::list>&& formatters) + : formatters{std::move(formatters)} {} /** * @brief Add a Description object @@ -45,19 +45,17 @@ class Runner { bool success = true; for (Description* spec : specs) { spec->timed_run(); - formatter->format(static_cast(*spec)); success &= spec->get_result().status(); } + for (auto& formatter : formatters) { + for (Description* spec : specs) { + formatter->format(static_cast(*spec)); + } + } return success ? Result::success(location) : Result::failure(location); } - Result exec() { - if (specs.size() > 1) { - formatter->set_multiple(true); - } - Result result = run(); - return result; - } + Result exec() { return run(); } }; } // namespace CppSpec From 0a692712aa8706a02d06e32a58d7523092c267c5 Mon Sep 17 00:00:00 2001 From: Katherine Date: Fri, 4 Apr 2025 14:56:25 -0400 Subject: [PATCH 15/33] Cleanup and output JUnit --- .github/workflows/test.yml | 32 +++++++++++++++++- CMakeLists.txt | 15 +++++++-- include/argparse.hpp | 5 +++ include/class_description.hpp | 21 ++++++------ include/description.hpp | 33 ++++++++++-------- include/expectations/expectation.hpp | 46 +++++++++++++------------- include/formatters/formatters_base.hpp | 16 ++++----- include/formatters/junit_xml.hpp | 8 ++--- include/formatters/progress.hpp | 30 ++++++++--------- include/formatters/tap.hpp | 20 ++++++----- include/formatters/term_colors.hpp | 35 ++++++++++---------- include/formatters/verbose.hpp | 10 +++--- include/it_base.hpp | 4 +-- include/matchers/matcher_base.hpp | 10 +++--- include/result.hpp | 5 ++- include/runnable.hpp | 23 ++++++++----- spec/CMakeLists.txt | 2 +- 17 files changed, 183 insertions(+), 132 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ff40edb..4ae4965 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ on: - main jobs: - build: + build-and-test: runs-on: ${{ matrix.os }} strategy: @@ -43,3 +43,33 @@ jobs: - name: Test run: ctest --test-dir build --build-config Release + + - name: Upload Test Results + uses: actions/upload-artifact@v4 + if: always() + with: + name: Test Results + path: build/spec/results/*.xml + + publish-test-results: + name: "Publish Tests Results" + needs: build-and-test + runs-on: ubuntu-latest + permissions: + checks: write + + # only needed unless run with comment_mode: off + pull-requests: write + + if: always() + + steps: + - name: Download Artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + files: "artifacts/**/*.xml" \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 91d2969..c38f6f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,7 +56,7 @@ endif() # HELPERS # Add spec -function(add_spec source_file) +function(add_spec source_file args) cmake_path(GET source_file STEM spec_name) add_executable(${spec_name} ${source_file}) target_link_libraries(${spec_name} c++spec) @@ -65,7 +65,7 @@ function(add_spec source_file) CXX_STANDARD 23 CXX_STANDARD_REQUIRED YES ) - add_test(NAME ${spec_name} COMMAND ${spec_name} --verbose) + add_test(NAME ${spec_name} COMMAND ${spec_name} --verbose ${args}) endfunction(add_spec) # Discover Specs @@ -73,10 +73,19 @@ function(discover_specs spec_folder) file(GLOB_RECURSE specs ${spec_folder}/*_spec.cpp) foreach(spec IN LISTS specs) - add_spec(${spec}) + add_spec(${spec} "") endforeach() endfunction(discover_specs) +function(discover_specs_output_junit spec_folder) + file(GLOB_RECURSE specs ${spec_folder}/*_spec.cpp) + + foreach(spec IN LISTS specs) + cmake_path(GET spec STEM spec_name) + add_spec(${spec} "--output-junit;${CMAKE_CURRENT_BINARY_DIR}/results/${spec_name}.xml") + endforeach() +endfunction(discover_specs_output_junit) + # OPTIONS option(CPPSPEC_BUILD_TESTS "Build C++Spec tests") option(CPPSPEC_BUILD_EXAMPLES "Build C++Spec examples") diff --git a/include/argparse.hpp b/include/argparse.hpp index 3cb5049..3e0c104 100644 --- a/include/argparse.hpp +++ b/include/argparse.hpp @@ -60,6 +60,11 @@ inline Runner parse(int argc, char** const argv) { auto junit_output_filepath = program.get("--output-junit"); if (!junit_output_filepath.empty()) { + // create directories recursively if they don't exist + std::filesystem::path junit_output_path = junit_output_filepath; + std::filesystem::create_directories(junit_output_path.parent_path()); + + // open file stream auto* file_stream = new std::ofstream(junit_output_filepath); auto junit_output = std::make_shared(*file_stream, false); return Runner{formatter, junit_output}; diff --git a/include/class_description.hpp b/include/class_description.hpp index ed48ef3..c51c085 100755 --- a/include/class_description.hpp +++ b/include/class_description.hpp @@ -26,17 +26,16 @@ class ClassDescription : public Description { std::string type; public: - const bool has_subject = true; T subject; // subject field public for usage in `expect([self.]subject)` // Constructor // if there's no explicit subject given, then use // the default constructor of the given type as the implicit subject. ClassDescription(Block block, std::source_location location = std::source_location::current()) - : block(block), type(" : " + Util::demangle(typeid(T).name())), subject(T()) { - this->description = Pretty::to_word(subject); - this->set_location(location); - } + : Description(location, Pretty::to_word(subject)), + block(block), + type(" : " + Util::demangle(typeid(T).name())), + subject(T()) {} ClassDescription(const char* description, Block block, @@ -67,7 +66,7 @@ class ClassDescription : public Description { : Description(location, Pretty::to_word(subject)), block(block), type(" : " + Util::demangle(typeid(T).name())), - subject(std::move(subject)) {} + subject(std::forward(subject)) {} template ClassDescription(std::initializer_list init_list, @@ -85,7 +84,7 @@ class ClassDescription : public Description { std::source_location location = std::source_location::current()) : Description(location, description), block(block), subject(T(init_list)) {} - ItCD& it(const char* description, + ItCD& it(const char* name, std::function&)> block, std::source_location location = std::source_location::current()); ItCD& it(std::function&)> block, std::source_location location = std::source_location::current()); @@ -114,7 +113,7 @@ class ClassDescription : public Description { template ClassDescription& context(U&& subject, B block, std::source_location location = std::source_location::current()) { - return this->context("", subject, block, location); + return this->context("", std::forward(subject), block, location); } void run() override; @@ -149,7 +148,7 @@ ClassContext& ClassDescription::context(const char* description, U&& subject, B block, std::source_location location) { - auto* context = this->make_child>(description, subject, block, location); + auto* context = this->make_child>(description, std::forward(subject), block, location); context->ClassContext::before_eaches = this->before_eaches; context->ClassContext::after_eaches = this->after_eaches; context->timed_run(); @@ -186,7 +185,7 @@ ClassContext& Description::context(const char* description, T& subject, B blo template ClassContext& Description::context(T&& subject, B block, std::source_location location) { - auto* context = this->make_child>(subject, block, location); + auto* context = this->make_child>(std::forward(subject), block, location); context->before_eaches = this->before_eaches; context->after_eaches = this->after_eaches; context->timed_run(); @@ -195,7 +194,7 @@ ClassContext& Description::context(T&& subject, B block, std::source_location template ClassContext& Description::context(const char* description, T&& subject, B block, std::source_location location) { - auto* context = this->make_child>(description, subject, block, location); + auto* context = this->make_child>(description, std::forward(subject), block, location); context->before_eaches = this->before_eaches; context->after_eaches = this->after_eaches; context->timed_run(); diff --git a/include/description.hpp b/include/description.hpp index f978ffd..d3f03c5 100755 --- a/include/description.hpp +++ b/include/description.hpp @@ -23,11 +23,10 @@ class Description : public Runnable { public: using Block = std::function; - const bool has_subject = false; - std::forward_list lets{}; - std::deque after_alls{}; - std::deque before_eaches{}; - std::deque after_eaches{}; + std::forward_list lets; + std::deque after_alls; + std::deque before_eaches; + std::deque after_eaches; private: Block block; @@ -43,7 +42,7 @@ class Description : public Runnable { Description(const char* description, Block block, std::source_location location = std::source_location::current()) noexcept - : Runnable(location), block(block), description(description) { + : Runnable(location), block(std::move(block)), description(description) { this->set_location(location); } @@ -51,17 +50,19 @@ class Description : public Runnable { : Runnable(location), description(std::move(description)) {} Description(std::source_location location, const char* description, Block block) noexcept - : Runnable(location), block(block), description(description) {} + : Runnable(location), block(std::move(block)), description(description) {} /********* Specify/It *********/ - ItD& it(const char* description, ItD::Block body, std::source_location location = std::source_location::current()); + ItD& it(const char* name, ItD::Block body, std::source_location location = std::source_location::current()); ItD& it(ItD::Block body, std::source_location location = std::source_location::current()); /********* Context ***********/ template - Description& context(const char* name, Block body, std::source_location location = std::source_location::current()); + Description& context(const char* description, + Block body, + std::source_location location = std::source_location::current()); template ClassDescription& context(T& subject, B block, std::source_location location = std::source_location::current()); @@ -119,7 +120,7 @@ using Context = Description; /*========= Description::it =========*/ inline ItD& Description::it(const char* description, ItD::Block block, std::source_location location) { - auto it = this->make_child(location, description, block); + auto* it = this->make_child(location, description, block); it->timed_run(); exec_after_eaches(); exec_before_eaches(); @@ -174,13 +175,15 @@ inline void Description::after_all(VoidBlock b) { /*----------- private -------------*/ inline void Description::exec_before_eaches() { - for (VoidBlock& b : before_eaches) + for (VoidBlock& b : before_eaches) { b(); + } } inline void Description::exec_after_eaches() { - for (VoidBlock& b : after_eaches) + for (VoidBlock& b : after_eaches) { b(); + } } /*========= Description::let =========*/ @@ -208,8 +211,9 @@ auto Description::let(T block) -> Let { // TODO: Should this be protected? inline void Description::reset_lets() noexcept { // For every let in our list, reset it. - for (auto& let : lets) + for (auto& let : lets) { let->reset(); + } // Recursively reset all the lets in the family tree if (this->has_parent()) { @@ -221,8 +225,9 @@ inline void Description::reset_lets() noexcept { inline void Description::run() { block(*this); // Run the block - for (VoidBlock& a : after_alls) + for (VoidBlock& a : after_alls) { a(); // Run all our after_alls + } } /*>>>>>>>>>>>>>>>>>>>> ItD <<<<<<<<<<<<<<<<<<<<<<<<<*/ diff --git a/include/expectations/expectation.hpp b/include/expectations/expectation.hpp index 4e7b593..9b38754 100755 --- a/include/expectations/expectation.hpp +++ b/include/expectations/expectation.hpp @@ -155,7 +155,7 @@ void Expectation::to(M matcher, std::string msg) { "Matcher is not a subclass of BaseMatcher."); // auto base_matcher = static_cast>(matcher); - matcher.set_message(msg).run(); + matcher.set_message(std::move(msg)).run(); } /** @@ -194,7 +194,7 @@ void Expectation::to_be_falsy(std::string msg) { */ template void Expectation::to_be_null(std::string msg) { - Matchers::BeNullptr(*this).set_message(msg).run(); + Matchers::BeNullptr(*this).set_message(std::move(msg)).run(); } /** @@ -238,19 +238,19 @@ void Expectation::to_be_truthy(std::string msg) { template template void Expectation::to_be_between(E min, E max, Matchers::RangeMode mode, std::string msg) { - Matchers::BeBetween(*this, min, max, mode).set_message(msg).run(); + Matchers::BeBetween(*this, min, max, mode).set_message(std::move(msg)).run(); } template template void Expectation::to_be_less_than(E rhs, std::string msg) { - Matchers::BeLessThan(*this, rhs).set_message(msg).run(); + Matchers::BeLessThan(*this, rhs).set_message(std::move(msg)).run(); } template template void Expectation::to_be_greater_than(E rhs, std::string msg) { - Matchers::BeGreaterThan(*this, rhs).set_message(msg).run(); + Matchers::BeGreaterThan(*this, rhs).set_message(std::move(msg)).run(); } /** @@ -264,7 +264,7 @@ void Expectation::to_be_greater_than(E rhs, std::string msg) { template template void Expectation::to_contain(std::initializer_list expected, std::string msg) { - Matchers::Contain, U>(*this, expected).set_message(msg).run(); + Matchers::Contain, U>(*this, expected).set_message(std::move(msg)).run(); } /** @@ -278,7 +278,7 @@ void Expectation::to_contain(std::initializer_list expected, std::string m template template void Expectation::to_contain(E expected, std::string msg) { - Matchers::Contain(*this, expected).set_message(msg).run(); + Matchers::Contain(*this, expected).set_message(std::move(msg)).run(); } /** @@ -292,7 +292,7 @@ void Expectation::to_contain(E expected, std::string msg) { template template void Expectation::to_equal(E expected, std::string msg) { - Matchers::Equal(*this, expected).set_message(msg).run(); + Matchers::Equal(*this, expected).set_message(std::move(msg)).run(); } /** @@ -307,40 +307,40 @@ template template Matchers::BeWithinHelper Expectation::to_be_within(E expected, std::string msg) { Matchers::BeWithinHelper matcher(*this, expected); - matcher.set_message(msg); + matcher.set_message(std::move(msg)); return matcher; } template void Expectation::to_fail(std::string msg) { static_assert(is_result_v, ".to_fail() must be used on an expression that returns a Result."); - Matchers::Fail(*this).set_message(msg).run(); + Matchers::Fail(*this).set_message(std::move(msg)).run(); } template void Expectation::to_fail_with(std::string failure_message, std::string msg) { static_assert(is_result_v, ".to_fail_with() must be used on an expression that returns a Result."); - Matchers::FailWith(*this, failure_message).set_message(msg).run(); + Matchers::FailWith(*this, failure_message).set_message(std::move(msg)).run(); } template void Expectation::to_match(std::string str, std::string msg) { - Matchers::Match(*this, str).set_message(msg).run(); + Matchers::Match(*this, str).set_message(std::move(msg)).run(); } template void Expectation::to_match(std::regex regex, std::string msg) { - Matchers::Match(*this, regex).set_message(msg).run(); + Matchers::Match(*this, regex).set_message(std::move(msg)).run(); } template void Expectation::to_partially_match(std::string str, std::string msg) { - Matchers::MatchPartial(*this, str).set_message(msg).run(); + Matchers::MatchPartial(*this, str).set_message(std::move(msg)).run(); } template void Expectation::to_partially_match(std::regex regex, std::string msg) { - Matchers::MatchPartial(*this, regex).set_message(msg).run(); + Matchers::MatchPartial(*this, regex).set_message(std::move(msg)).run(); } /** @@ -354,40 +354,40 @@ void Expectation::to_partially_match(std::regex regex, std::string msg) { */ template void Expectation::to_satisfy(std::function test, std::string msg) { - Matchers::Satisfy(*this, test).set_message(msg).run(); + Matchers::Satisfy(*this, test).set_message(std::move(msg)).run(); } template void Expectation::to_start_with(std::string start, std::string msg) { - Matchers::StartWith(*this, start).set_message(msg).run(); + Matchers::StartWith(*this, start).set_message(std::move(msg)).run(); } template template void Expectation::to_start_with(std::initializer_list start_sequence, std::string msg) { - Matchers::StartWith>(*this, start_sequence).set_message(msg).run(); + Matchers::StartWith>(*this, start_sequence).set_message(std::move(msg)).run(); } template void Expectation::to_end_with(std::string ending, std::string msg) { - Matchers::EndWith(*this, ending).set_message(msg).run(); + Matchers::EndWith(*this, ending).set_message(std::move(msg)).run(); } template template void Expectation::to_end_with(std::initializer_list start_sequence, std::string msg) { - Matchers::StartWith>(*this, start_sequence).set_message(msg).run(); + Matchers::StartWith>(*this, start_sequence).set_message(std::move(msg)).run(); } template void Expectation::to_have_value(std::string msg) { - Matchers::HaveValue(*this).set_message(msg).run(); + Matchers::HaveValue(*this).set_message(std::move(msg)).run(); } #if __cpp_lib_expected template void Expectation::to_have_error(std::string msg) { - Matchers::HaveError(*this).set_message(msg).run(); + Matchers::HaveError(*this).set_message(std::move(msg)).run(); } #endif @@ -497,7 +497,7 @@ class ExpectationFunc : public Expectation()())> { template template void ExpectationFunc::to_throw(std::string msg) { - Matchers::Throwblock.operator()()), Ex>(*this).set_message(msg).run(); + Matchers::Throwblock.operator()()), Ex>(*this).set_message(std::move(msg)).run(); } } // namespace CppSpec diff --git a/include/formatters/formatters_base.hpp b/include/formatters/formatters_base.hpp index e6e4d32..7e0f102 100644 --- a/include/formatters/formatters_base.hpp +++ b/include/formatters/formatters_base.hpp @@ -44,25 +44,25 @@ class BaseFormatter { virtual ~BaseFormatter() = default; - void format(Runnable& runnable) { - if (auto* description = dynamic_cast(&runnable)) { + void format(const Runnable& runnable) { + if (const auto* description = dynamic_cast(&runnable)) { format(*description); - } else if (auto* it = dynamic_cast(&runnable)) { + } else if (const auto* it = dynamic_cast(&runnable)) { format(*it); } format_children(runnable); } - void format_children(Runnable& runnable) { - for (auto& child : runnable.get_children()) { - if (auto* runnable = dynamic_cast(child.get())) { + void format_children(const Runnable& runnable) { + for (const auto& child : runnable.get_children()) { + if (const auto* runnable = dynamic_cast(child.get())) { this->format(*runnable); } } } - virtual void format(Description& /* description */) {} - virtual void format(ItBase& /* it */) {} + virtual void format(const Description& /* description */) {} + virtual void format(const ItBase& /* it */) {} virtual void cleanup() {} BaseFormatter& set_color_output(bool value); diff --git a/include/formatters/junit_xml.hpp b/include/formatters/junit_xml.hpp index 0ff402d..5e8cbda 100644 --- a/include/formatters/junit_xml.hpp +++ b/include/formatters/junit_xml.hpp @@ -164,7 +164,7 @@ class JUnitXML : public BaseFormatter { out_stream.flush(); } - void format(Description& description) override { + void format(const Description& description) override { if (test_suites.name.empty()) { std::filesystem::path file_path = description.get_location().file_name(); test_suites.name = file_path.stem().string(); @@ -176,12 +176,12 @@ class JUnitXML : public BaseFormatter { description.num_failures(), description.get_start_time()); } - void format(ItBase& it) override { + void format(const ItBase& it) override { using namespace std::chrono; std::forward_list descriptions; descriptions.push_front(it.get_description()); - for (auto* parent = it.get_parent_as(); parent->has_parent(); + for (const auto* parent = it.get_parent_as(); parent->has_parent(); parent = parent->get_parent_as()) { descriptions.push_front(parent->get_description()); } @@ -198,7 +198,7 @@ class JUnitXML : public BaseFormatter { .line = it.get_location().line(), }; - for (auto& result : it.get_results()) { + for (const auto& result : it.get_results()) { if (result.is_success()) { continue; } diff --git a/include/formatters/progress.hpp b/include/formatters/progress.hpp index c689a28..7f86788 100644 --- a/include/formatters/progress.hpp +++ b/include/formatters/progress.hpp @@ -12,23 +12,23 @@ namespace CppSpec::Formatters { // The TAP format makes things a little tricky class Progress : public BaseFormatter { - std::list baked_failure_messages{}; - std::list raw_failure_messages{}; + std::list baked_failure_messages; + std::list raw_failure_messages; - std::string prep_failure_helper(ItBase& it); + std::string prep_failure_helper(const ItBase& it); public: ~Progress() override { format_failure_messages(); // Print any failures that we have } - void format(ItBase& it) override; + void format(const ItBase& it) override; void format_failure_messages(); - void prep_failure(ItBase& it); + void prep_failure(const ItBase& it); }; /** @brief An assistant function for prep_failure to reduce complexity */ -inline std::string Progress::prep_failure_helper(ItBase& it) { +inline std::string Progress::prep_failure_helper(const ItBase& it) { // a singly-linked list to act as a LIFO queue std::forward_list list; @@ -52,17 +52,17 @@ inline std::string Progress::prep_failure_helper(ItBase& it) { // Ascend the tree to the root, formatting the nodes and // enqueing each formatted string as we go. - Description* parent = it.get_parent_as(); + const auto* parent = it.get_parent_as(); do { helper_formatter.format(*parent); // Format the node push_and_clear(); - } while ((parent = dynamic_cast(parent->get_parent())) != nullptr); + } while ((parent = dynamic_cast(parent->get_parent())) != nullptr); return Util::join(list); // squash the list of strings and return it. } -inline void Progress::prep_failure(ItBase& it) { +inline void Progress::prep_failure(const ItBase& it) { std::ostringstream string_builder; // oss is used as the local string builder if (color_output) { string_builder << RED; // if we're doing color, make it red @@ -83,7 +83,7 @@ inline void Progress::prep_failure(ItBase& it) { baked_failure_messages.push_back(string_builder.str()); } -inline void Progress::format(ItBase& it) { +inline void Progress::format(const ItBase& it) { if (it.get_result().status()) { if (color_output) { out_stream << GREEN; @@ -105,12 +105,10 @@ inline void Progress::format(ItBase& it) { inline void Progress::format_failure_messages() { if (!baked_failure_messages.empty()) { // If we have any failures to format - out_stream << std::endl; - - out_stream << Util::join(baked_failure_messages, - "\n\n") // separated by a blank line - << std::endl; // newline - + for (const std::string& message : baked_failure_messages) { + out_stream << std::endl; + out_stream << message; // separated by a blank line + } baked_failure_messages.clear(); // Finally, clear the failures list. } } diff --git a/include/formatters/tap.hpp b/include/formatters/tap.hpp index 3b365a1..0c6daa6 100644 --- a/include/formatters/tap.hpp +++ b/include/formatters/tap.hpp @@ -15,14 +15,14 @@ struct TAP final : public BaseFormatter { ~TAP() { flush(); } std::string result_to_yaml(const Result& result); - void format(Description& description) override; - void format(ItBase& it) override; + void format(const Description& description) override; + void format(const ItBase& it) override; void flush(); }; inline std::string TAP::result_to_yaml(const Result& result) { if (result.is_success()) { - return std::string(); + return {}; } std::ostringstream oss; @@ -65,7 +65,7 @@ inline void TAP::flush() { buffer = std::ostringstream(); } -inline void TAP::format(Description& description) { +inline void TAP::format(const Description& description) { if (!first && !description.has_parent()) { flush(); } @@ -75,17 +75,19 @@ inline void TAP::format(Description& description) { } } -inline void TAP::format(ItBase& it) { - std::string description{it.get_description()}; - +inline void TAP::format(const ItBase& it) { // Build up the description for the test by ascending the // execution tree and chaining the individual descriptions together + std::forward_list descriptions; - for (auto parent = it.get_parent_as(); parent != nullptr; + descriptions.push_front(it.get_description()); + for (const auto* parent = it.get_parent_as(); parent->has_parent(); parent = parent->get_parent_as()) { - description = std::string(parent->get_description()) + " " + description; + descriptions.push_front(parent->get_description()); } + std::string description = Util::join(descriptions, " "); + if (color_output) { buffer << (it.get_result().status() ? GREEN : RED); } diff --git a/include/formatters/term_colors.hpp b/include/formatters/term_colors.hpp index 0c872ea..06ef72b 100644 --- a/include/formatters/term_colors.hpp +++ b/include/formatters/term_colors.hpp @@ -1,22 +1,21 @@ /** @file */ #pragma once -#include // the following are Unix/BASH ONLY terminal color codes. -constexpr std::string_view RESET("\033[0m"); -constexpr std::string_view BLACK("\033[30m"); /* Black */ -constexpr std::string_view RED("\033[31m"); /* Red */ -constexpr std::string_view GREEN("\033[32m"); /* Green */ -constexpr std::string_view YELLOW("\033[33m"); /* Yellow */ -constexpr std::string_view BLUE("\033[34m"); /* Blue */ -constexpr std::string_view MAGENTA("\033[35m"); /* Magenta */ -constexpr std::string_view CYAN("\033[36m"); /* Cyan */ -constexpr std::string_view WHITE("\033[37m"); /* White */ -constexpr std::string_view BOLDBLACK("\033[1m\033[30m"); /* Bold Black */ -constexpr std::string_view BOLDRED("\033[1m\033[31m"); /* Bold Red */ -constexpr std::string_view BOLDGREEN("\033[1m\033[32m"); /* Bold Green */ -constexpr std::string_view BOLDYELLOW("\033[1m\033[33m"); /* Bold Yellow */ -constexpr std::string_view BOLDBLUE("\033[1m\033[34m"); /* Bold Blue */ -constexpr std::string_view BOLDMAGENTA("\033[1m\033[35m"); /* Bold Magenta */ -constexpr std::string_view BOLDCYAN("\033[1m\033[36m"); /* Bold Cyan */ -constexpr std::string_view BOLDWHITE("\033[1m\033[37m"); /* Bold White */ +constexpr auto RESET("\033[0m"); +constexpr auto BLACK("\033[30m"); /* Black */ +constexpr auto RED("\033[31m"); /* Red */ +constexpr auto GREEN("\033[32m"); /* Green */ +constexpr auto YELLOW("\033[33m"); /* Yellow */ +constexpr auto BLUE("\033[34m"); /* Blue */ +constexpr auto MAGENTA("\033[35m"); /* Magenta */ +constexpr auto CYAN("\033[36m"); /* Cyan */ +constexpr auto WHITE("\033[37m"); /* White */ +constexpr auto BOLDBLACK("\033[1m\033[30m"); /* Bold Black */ +constexpr auto BOLDRED("\033[1m\033[31m"); /* Bold Red */ +constexpr auto BOLDGREEN("\033[1m\033[32m"); /* Bold Green */ +constexpr auto BOLDYELLOW("\033[1m\033[33m"); /* Bold Yellow */ +constexpr auto BOLDBLUE("\033[1m\033[34m"); /* Bold Blue */ +constexpr auto BOLDMAGENTA("\033[1m\033[35m"); /* Bold Magenta */ +constexpr auto BOLDCYAN("\033[1m\033[36m"); /* Bold Cyan */ +constexpr auto BOLDWHITE("\033[1m\033[37m"); /* Bold White */ diff --git a/include/formatters/verbose.hpp b/include/formatters/verbose.hpp index 88ac981..f5e768a 100644 --- a/include/formatters/verbose.hpp +++ b/include/formatters/verbose.hpp @@ -16,11 +16,11 @@ class Verbose : public BaseFormatter { Verbose(const BaseFormatter& base, std::ostream& out_stream) : BaseFormatter(base, out_stream) {} explicit Verbose(const BaseFormatter& base) : BaseFormatter(base) {} - void format(Description& description) override; - void format(ItBase& it) override; + void format(const Description& description) override; + void format(const ItBase& it) override; }; -inline void Verbose::format(Description& description) { +inline void Verbose::format(const Description& description) { if (!first && !description.has_parent()) { out_stream << std::endl; } @@ -30,7 +30,7 @@ inline void Verbose::format(Description& description) { } } -inline void Verbose::format(ItBase& it) { +inline void Verbose::format(const ItBase& it) { if (color_output) { out_stream << (it.get_result().status() ? GREEN : RED); } @@ -42,7 +42,7 @@ inline void Verbose::format(ItBase& it) { // Print any failures if we've got them // 'it' having a bad status necessarily // implies that there are failure messages - for (const auto& result : it.get_results()) { + for (const Result& result : it.get_results()) { if (result.is_failure()) { if (color_output) { out_stream << RED; // make them red diff --git a/include/it_base.hpp b/include/it_base.hpp index c623f22..82a215d 100755 --- a/include/it_base.hpp +++ b/include/it_base.hpp @@ -31,7 +31,7 @@ class ExpectationFunc; class ItBase : public Runnable { /** @brief The documentation string for this `it` */ std::string description; - std::list results{}; // The results of the `it` statement + std::list results; // The results of the `it` statement public: ItBase() = delete; // Don't allow a default constructor @@ -48,7 +48,7 @@ class ItBase : public Runnable { * @return the constructed BaseIt */ explicit ItBase(std::source_location location, const char* description) noexcept - : Runnable(location), description(std::move(description)) {} + : Runnable(location), description(description) {} /** * @brief Get whether the object needs a description string diff --git a/include/matchers/matcher_base.hpp b/include/matchers/matcher_base.hpp index 2853a32..ed9aca0 100644 --- a/include/matchers/matcher_base.hpp +++ b/include/matchers/matcher_base.hpp @@ -73,9 +73,9 @@ class MatcherBase : public Pretty { Expectation& expectation() { return expectation_; } // Set the message to give on match failure - virtual MatcherBase& set_message(const std::string& message); + virtual MatcherBase& set_message(std::string message); - std::source_location get_location() const { return expectation_.get_location(); } + [[nodiscard]] std::source_location get_location() const { return expectation_.get_location(); } /*--------- Primary functions -------------*/ @@ -97,8 +97,8 @@ class MatcherBase : public Pretty { * @return the modified Matcher */ template -MatcherBase& MatcherBase::set_message(const std::string& message) { - this->custom_failure_message = message; +MatcherBase& MatcherBase::set_message(std::string message) { + this->custom_failure_message = std::move(message); return *this; } @@ -152,7 +152,7 @@ std::string MatcherBase::description() { template Result MatcherBase::run() { ItBase* parent = expectation_.get_it(); - if (parent) { + if (parent != nullptr) { // If we need a description for our test, generate it // unless we're ignoring the output. if (parent->needs_description() && !expectation_.ignore_failure()) { diff --git a/include/result.hpp b/include/result.hpp index 63935df..d2338c0 100644 --- a/include/result.hpp +++ b/include/result.hpp @@ -10,7 +10,7 @@ namespace CppSpec { class Result { - bool value; + bool value = false; std::source_location location; std::string message; std::string type; @@ -22,9 +22,8 @@ class Result { /*--------- Status helper functions --------------*/ [[nodiscard]] bool status() const noexcept { return value; } - [[nodiscard]] bool status() noexcept { return value; } [[nodiscard]] bool is_success() const noexcept { return value; } - [[nodiscard]] bool is_failure() const noexcept { return value == false; } + [[nodiscard]] bool is_failure() const noexcept { return !value; } /*--------- Location helper functions ------------*/ [[nodiscard]] std::source_location get_location() const noexcept { return location; } diff --git a/include/runnable.hpp b/include/runnable.hpp index ef46ee8..a9a9ca3 100755 --- a/include/runnable.hpp +++ b/include/runnable.hpp @@ -40,10 +40,10 @@ class Runnable { // The source file location of the Runnable object std::source_location location; - std::list> children_{}; // List of children + std::list> children_; // List of children std::chrono::time_point start_time_; - std::chrono::duration runtime_; + std::chrono::duration runtime_{}; public: Runnable(std::source_location location) : location(location) {} @@ -62,13 +62,18 @@ class Runnable { [[nodiscard]] const Runnable* get_parent() const noexcept { return parent; } std::list>& get_children() noexcept { return children_; } - const std::list>& get_children() const noexcept { return children_; } + [[nodiscard]] const std::list>& get_children() const noexcept { return children_; } template C* get_parent_as() noexcept { return static_cast(parent); } + template + [[nodiscard]] const C* get_parent_as() const noexcept { + return static_cast(parent); + } + template T* make_child(Args&&... args) { auto child = std::make_shared(std::forward(args)...); @@ -106,33 +111,33 @@ class Runnable { [[nodiscard]] virtual Result get_result() const { Result result = Result::success(location); - for (auto& child : get_children()) { + for (const auto& child : get_children()) { result &= child->get_result(); } return result; } - size_t num_tests() const noexcept { + [[nodiscard]] size_t num_tests() const noexcept { if (get_children().empty()) { return 1; // This is a leaf node } // This is not a leaf node, so we need to count the children size_t count = 0; - for (auto& child : get_children()) { + for (const auto& child : get_children()) { count += child->num_tests(); // +1 for the child itself } return count; } - size_t num_failures() const noexcept { + [[nodiscard]] size_t num_failures() const noexcept { if (get_children().empty()) { - return this->get_result().is_failure(); // This is a leaf node + return this->get_result().is_failure() ? 1 : 0; // This is a leaf node } // This is not a leaf node, so we need to count the children size_t count = 0; - for (auto& child : get_children()) { + for (const auto& child : get_children()) { count += child->num_failures(); // +1 for the child itself } return count; diff --git a/spec/CMakeLists.txt b/spec/CMakeLists.txt index fe6191d..040aa5d 100644 --- a/spec/CMakeLists.txt +++ b/spec/CMakeLists.txt @@ -2,4 +2,4 @@ if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") add_compile_options(-Wno-unused-parameter -Wno-missing-template-arg-list-after-template-kw -Wno-missing-template-keyword -Wno-unknown-warning-option) endif() -discover_specs(${CMAKE_CURRENT_SOURCE_DIR}) +discover_specs_output_junit(${CMAKE_CURRENT_SOURCE_DIR}) From b825374c6b609d7bb7355f5253efb1a7f26d3233 Mon Sep 17 00:00:00 2001 From: Katherine Date: Fri, 4 Apr 2025 14:58:40 -0400 Subject: [PATCH 16/33] Separate artefacts per-strategy --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ae4965..f20e3eb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,7 +48,7 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: Test Results + name: Test Results (${{ matrix.os }} - ${{ matrix.compiler }}) path: build/spec/results/*.xml publish-test-results: From 2d5b43d5b14ffae92443c258838323da9c7743bc Mon Sep 17 00:00:00 2001 From: Katherine Date: Fri, 4 Apr 2025 15:04:43 -0400 Subject: [PATCH 17/33] Fix warnings in junit_xml --- include/formatters/junit_xml.hpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/include/formatters/junit_xml.hpp b/include/formatters/junit_xml.hpp index 5e8cbda..5b3e60a 100644 --- a/include/formatters/junit_xml.hpp +++ b/include/formatters/junit_xml.hpp @@ -130,7 +130,7 @@ struct TestSuites { ss << std::format(R"()", name, tests, failures, time.count(), timestamp_str); ss << std::endl; - for (const auto& suite : suites) { + for (const TestSuite& suite : suites) { ss << suite.to_xml() << std::endl; } ss << "" << std::endl; @@ -148,10 +148,10 @@ class JUnitXML : public BaseFormatter { ~JUnitXML() { test_suites.tests = - std::accumulate(test_suites.suites.begin(), test_suites.suites.end(), 0, + std::accumulate(test_suites.suites.begin(), test_suites.suites.end(), size_t{0}, [](size_t sum, const JUnitNodes::TestSuite& suite) { return sum + suite.tests; }); test_suites.failures = - std::accumulate(test_suites.suites.begin(), test_suites.suites.end(), 0, + std::accumulate(test_suites.suites.begin(), test_suites.suites.end(), size_t{0}, [](size_t sum, const JUnitNodes::TestSuite& suite) { return sum + suite.failures; }); test_suites.time = std::ranges::fold_left(test_suites.suites, std::chrono::duration(0), [](const auto& acc, const auto& suite) { return acc + suite.time; }); @@ -181,7 +181,7 @@ class JUnitXML : public BaseFormatter { std::forward_list descriptions; descriptions.push_front(it.get_description()); - for (const auto* parent = it.get_parent_as(); parent->has_parent(); + for (const Description* parent = it.get_parent_as(); parent->has_parent(); parent = parent->get_parent_as()) { descriptions.push_front(parent->get_description()); } @@ -198,7 +198,7 @@ class JUnitXML : public BaseFormatter { .line = it.get_location().line(), }; - for (const auto& result : it.get_results()) { + for (const Result& result : it.get_results()) { if (result.is_success()) { continue; } From 584dceba569bc585ab8e57acaf786401626fd32f Mon Sep 17 00:00:00 2001 From: Katherine Date: Fri, 4 Apr 2025 15:06:57 -0400 Subject: [PATCH 18/33] remove comma from testcase xml output --- include/formatters/junit_xml.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/formatters/junit_xml.hpp b/include/formatters/junit_xml.hpp index 5b3e60a..95ce589 100644 --- a/include/formatters/junit_xml.hpp +++ b/include/formatters/junit_xml.hpp @@ -58,7 +58,7 @@ struct TestCase { [[nodiscard]] std::string to_xml() const { auto start = - std::format(R"( "; From f1a4a508fc5d52f14dd3a371904c6039fef821c0 Mon Sep 17 00:00:00 2001 From: Katherine Date: Fri, 4 Apr 2025 15:18:35 -0400 Subject: [PATCH 19/33] encode strings for xml use --- include/formatters/junit_xml.hpp | 47 +++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/include/formatters/junit_xml.hpp b/include/formatters/junit_xml.hpp index 95ce589..72f0f37 100644 --- a/include/formatters/junit_xml.hpp +++ b/include/formatters/junit_xml.hpp @@ -13,10 +13,31 @@ namespace CppSpec::Formatters { // JUnit XML header constexpr static auto junit_xml_header = R"()"; -struct XMLSerializable { - virtual ~XMLSerializable() = default; - [[nodiscard]] virtual std::string to_xml() const = 0; -}; +inline std::string encode_xml(const std::string& data) { + std::string buffer; + for (char c : data) { + switch (c) { + case '<': + buffer += "<"; + break; + case '>': + buffer += ">"; + break; + case '&': + buffer += "&"; + break; + case '"': + buffer += """; + break; + case '\'': + buffer += "'"; + break; + default: + buffer += c; + } + } + return buffer; +} namespace JUnitNodes { struct Result { @@ -42,8 +63,8 @@ struct Result { } [[nodiscard]] std::string to_xml() const { - return std::format(R"( <{} message="{}" type="{}">{})", status_string(), message, type, text, - status_string()); + return std::format(R"( <{} message="{}" type="{}">{})", status_string(), encode_xml(message), + encode_xml(type), encode_xml(text), status_string()); } }; @@ -58,8 +79,8 @@ struct TestCase { [[nodiscard]] std::string to_xml() const { auto start = - std::format(R"( "; } @@ -99,10 +120,10 @@ struct TestSuite { std::stringstream ss; ss << " " - << std::format(R"()", id, name, - time.count(), timestamp_str, tests, failures); + << std::format(R"()", id, + encode_xml(name), time.count(), timestamp_str, tests, failures); ss << std::endl; - for (const auto& test_case : cases) { + for (const TestCase& test_case : cases) { ss << test_case.to_xml() << std::endl; } ss << " "; @@ -127,8 +148,8 @@ struct TestSuites { [[nodiscard]] std::string to_xml() const { std::stringstream ss; auto timestamp_str = std::format("{0:%F}T{0:%T}", timestamp); - ss << std::format(R"()", name, tests, - failures, time.count(), timestamp_str); + ss << std::format(R"()", encode_xml(name), + tests, failures, time.count(), timestamp_str); ss << std::endl; for (const TestSuite& suite : suites) { ss << suite.to_xml() << std::endl; From a3f78d5ff7ca8a0af457ec3b97c65fba9839e3b2 Mon Sep 17 00:00:00 2001 From: Katherine Date: Fri, 4 Apr 2025 15:25:11 -0400 Subject: [PATCH 20/33] output on failure --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f20e3eb..83cdfd4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,7 +42,7 @@ jobs: run: cmake --build build --config Release - name: Test - run: ctest --test-dir build --build-config Release + run: ctest --test-dir build --build-config Release --output-on-failure - name: Upload Test Results uses: actions/upload-artifact@v4 From 9ba46551bd87b1defe717e8438f1d566a626d4cb Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Fri, 4 Apr 2025 16:38:38 -0400 Subject: [PATCH 21/33] Upgrade argparse --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c38f6f2..800be72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,7 @@ include(FetchContent) FetchContent_Declare(argparse GIT_REPOSITORY https://github.com/p-ranav/argparse/ - GIT_TAG v3.1 + GIT_TAG v3.2 GIT_SHALLOW 1 ) FetchContent_MakeAvailable(argparse) From 7430221dbe67f9522e1f5523beceac2456ec7c6d Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Fri, 4 Apr 2025 16:40:48 -0400 Subject: [PATCH 22/33] set major version to 1.0 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 800be72..e673e15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.27 FATAL_ERROR) project(c++spec - VERSION 1.2.0 + VERSION 1.0.0 DESCRIPTION "BDD testing for C++" LANGUAGES CXX ) From e6f172ec215ab9865606fb6d909fde2a85c97229 Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Fri, 4 Apr 2025 23:17:03 -0400 Subject: [PATCH 23/33] Add runtime exception handling, formatter improvements --- examples/sample/example_spec.cpp | 7 +- include/argparse.hpp | 4 +- include/expectations/expectation.hpp | 18 +-- include/expectations/handler.hpp | 28 ++++- include/formatters/formatters_base.hpp | 26 ++++ include/formatters/progress.hpp | 61 +++++----- include/formatters/tap.hpp | 26 ++-- include/formatters/term_colors.hpp | 4 + include/formatters/verbose.hpp | 16 +-- include/it.hpp | 12 +- include/it_base.hpp | 4 +- include/matchers/equal.hpp | 12 +- include/matchers/matcher_base.hpp | 32 ++--- include/result.hpp | 133 ++++++++++++--------- include/runnable.hpp | 2 +- include/runner.hpp | 2 +- include/util.hpp | 13 ++ spec/describe_a_spec.cpp | 4 +- spec/expectations/expectation_spec.cpp | 10 +- spec/matchers/equal_spec.cpp | 3 +- spec/matchers/unhandled_exception_spec.cpp | 30 +++++ 21 files changed, 286 insertions(+), 161 deletions(-) create mode 100644 spec/matchers/unhandled_exception_spec.cpp diff --git a/examples/sample/example_spec.cpp b/examples/sample/example_spec.cpp index 97aef43..c802902 100644 --- a/examples/sample/example_spec.cpp +++ b/examples/sample/example_spec.cpp @@ -4,10 +4,11 @@ #include #include "cppspec.hpp" +// clang-format off describe bool_spec("Some Tests", $ { context("true is", _ { it("true", _ { - expect(static_cast(1)).to_equal(true); + expect(1).to_equal(true); }); it("true", _ { @@ -27,6 +28,10 @@ describe bool_spec("Some Tests", $ { it("plus 1 equals 5", _ { expect(i+1).to_equal(5); }); + + it("equals 6", _ { + expect(i).to_equal(6); + }); }); explain("0 is", _ { diff --git a/include/argparse.hpp b/include/argparse.hpp index 3e0c104..707512f 100644 --- a/include/argparse.hpp +++ b/include/argparse.hpp @@ -24,7 +24,9 @@ inline std::string file_name(std::string_view path) { } inline Runner parse(int argc, char** const argv) { - argparse::ArgumentParser program{file_name(argv[0])}; + std::filesystem::path executable_path = argv[0]; + std::string executable_name = executable_path.filename().string(); + argparse::ArgumentParser program{executable_name}; program.add_argument("-f", "--format") .default_value(std::string{"p"}) diff --git a/include/expectations/expectation.hpp b/include/expectations/expectation.hpp index 9b38754..795ae51 100755 --- a/include/expectations/expectation.hpp +++ b/include/expectations/expectation.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -50,10 +51,11 @@ class Expectation { protected: bool is_positive_ = true; // Have we been negated? - bool ignore_failure_ = false; + bool ignore_ = false; public: Expectation() = default; + explicit Expectation(std::source_location location) : location(location) {} /** * @brief Create an Expectation using a value. @@ -68,12 +70,12 @@ class Expectation { // virtual const A &get_target() const & { return target; } virtual A& get_target() & = 0; - ItBase* get_it() const { return it; } - std::source_location get_location() const { return location; } + [[nodiscard]] ItBase* get_it() const { return it; } + [[nodiscard]] std::source_location get_location() const { return location; } /** @brief Get whether the expectation is normal or negated. */ - [[nodiscard]] constexpr bool sign() const { return is_positive_; } - [[nodiscard]] constexpr bool ignore_failure() const { return ignore_failure_; } + [[nodiscard]] constexpr bool positive() const { return is_positive_; } + [[nodiscard]] constexpr bool ignored() const { return ignore_; } /********* Modifiers *********/ @@ -404,6 +406,8 @@ class ExpectationValue : public Expectation { * @return The constructed ExpectationValue. */ ExpectationValue(ItBase& it, A value, std::source_location location) : Expectation(it, location), value(value) {} + explicit ExpectationValue(A value, std::source_location location = std::source_location::current()) + : Expectation(location), value(value) {} /** * @brief Create an Expectation using an initializer list. @@ -425,7 +429,7 @@ class ExpectationValue : public Expectation { } ExpectationValue& ignore() override { - this->ignore_failure_ = true; + this->ignore_ = true; return *this; } }; @@ -484,7 +488,7 @@ class ExpectationFunc : public Expectation()())> { } ExpectationFunc& ignore() override { - this->ignore_failure_ = true; + this->ignore_ = true; return *this; } diff --git a/include/expectations/handler.hpp b/include/expectations/handler.hpp index a613d62..76dc679 100755 --- a/include/expectations/handler.hpp +++ b/include/expectations/handler.hpp @@ -9,6 +9,7 @@ #pragma once +#include #include #include "result.hpp" @@ -39,9 +40,17 @@ struct NegativeExpectationHandler { */ template Result PositiveExpectationHandler::handle_matcher(Matcher& matcher) { - // TODO: handle expectation failure here - return !matcher.match() ? Result::failure_with(matcher.get_location(), matcher.failure_message()) - : Result::success(matcher.get_location()); + bool matched = false; + try { + matched = matcher.match(); + } catch (std::exception& e) { + return Result::error_with(matcher.get_location(), e.what()); + } catch (...) { + return Result::error_with(matcher.get_location(), "Unknown exception thrown during matcher execution."); + } + + return !matched ? Result::failure_with(matcher.get_location(), matcher.failure_message()) + : Result::success(matcher.get_location()); } /** @@ -54,9 +63,16 @@ Result PositiveExpectationHandler::handle_matcher(Matcher& matcher) { */ template Result NegativeExpectationHandler::handle_matcher(Matcher& matcher) { - // TODO: handle expectation failure here - return !matcher.negated_match() ? Result::failure_with(matcher.get_location(), matcher.failure_message_when_negated()) - : Result::success(matcher.get_location()); + bool matched = false; + try { + matched = matcher.negated_match(); + } catch (std::exception& e) { + return Result::error_with(matcher.get_location(), e.what()); + } catch (...) { + return Result::error_with(matcher.get_location(), "Unhandled exception thrown during matcher execution."); + } + return !matched ? Result::failure_with(matcher.get_location(), matcher.failure_message_when_negated()) + : Result::success(matcher.get_location()); } } // namespace CppSpec diff --git a/include/formatters/formatters_base.hpp b/include/formatters/formatters_base.hpp index 7e0f102..142e777 100644 --- a/include/formatters/formatters_base.hpp +++ b/include/formatters/formatters_base.hpp @@ -7,6 +7,7 @@ #include "description.hpp" #include "it_base.hpp" #include "runnable.hpp" +#include "term_colors.hpp" extern "C" { #ifdef _WIN32 @@ -69,6 +70,31 @@ class BaseFormatter { int get_and_increment_test_counter() { return test_counter++; } void reset_test_counter() { test_counter = 1; } + + const char* set_color(const char* color) { + if (!color_output) { + return ""; // No color output + } + return color; + } + + const char* status_color(Result::Status status) { + if (!color_output) { + return ""; // No color output + } + switch (status) { + case Result::Status::Success: + return GREEN; + case Result::Status::Failure: + return RED; + case Result::Status::Error: + return MAGENTA; + case Result::Status::Skipped: + return YELLOW; + } + } + + const char* reset_color() { return color_output ? RESET : ""; } }; inline BaseFormatter& BaseFormatter::set_color_output(bool value) { diff --git a/include/formatters/progress.hpp b/include/formatters/progress.hpp index 7f86788..e10f1dc 100644 --- a/include/formatters/progress.hpp +++ b/include/formatters/progress.hpp @@ -13,7 +13,6 @@ namespace CppSpec::Formatters { // The TAP format makes things a little tricky class Progress : public BaseFormatter { std::list baked_failure_messages; - std::list raw_failure_messages; std::string prep_failure_helper(const ItBase& it); @@ -25,6 +24,20 @@ class Progress : public BaseFormatter { void format_failure_messages(); void prep_failure(const ItBase& it); + + static char status_char(Result::Status status) { + switch (status) { + case Result::Status::Success: + return '.'; + case Result::Status::Failure: + return 'F'; + case Result::Status::Error: + return 'E'; + case Result::Status::Skipped: + return 'S'; + } + return '.'; // Default to success if status is unknown + } }; /** @brief An assistant function for prep_failure to reduce complexity */ @@ -63,43 +76,33 @@ inline std::string Progress::prep_failure_helper(const ItBase& it) { } inline void Progress::prep_failure(const ItBase& it) { - std::ostringstream string_builder; // oss is used as the local string builder - if (color_output) { - string_builder << RED; // if we're doing color, make it red - } + std::list raw_failure_messages; // raw failure messages + std::ranges::transform(it.get_results(), std::back_inserter(raw_failure_messages), + [](const Result& result) { return result.get_message(); }); + + std::ostringstream string_builder; // oss is used as the local string builder + string_builder << set_color(RED); // if we're doing color, make it red string_builder << "Test number " << test_counter << " failed:"; // Tell us what test # failed - if (color_output) { - string_builder << RESET; // if we're doing color, reset the terminal - } + string_builder << reset_color(); // reset the color string_builder << prep_failure_helper(it); - if (color_output) { - string_builder << RED; - } - string_builder << Util::join(raw_failure_messages, "\n"); - if (color_output) { - string_builder << RESET; - } + string_builder << set_color(RED); + string_builder << Util::join_endl(raw_failure_messages); + string_builder << reset_color(); // reset the color + string_builder << std::endl; + raw_failure_messages.clear(); baked_failure_messages.push_back(string_builder.str()); } inline void Progress::format(const ItBase& it) { - if (it.get_result().status()) { - if (color_output) { - out_stream << GREEN; - } - out_stream << "."; - } else { - if (color_output) { - out_stream << RED; - } - out_stream << "F"; + out_stream << status_color(it.get_result().status()); + out_stream << status_char(it.get_result().status()); + out_stream << reset_color(); + out_stream << std::flush; + + if (it.get_result().status() == Result::Status::Failure) { prep_failure(it); } - if (color_output) { - out_stream << RESET; - } - out_stream << std::flush; get_and_increment_test_counter(); } diff --git a/include/formatters/tap.hpp b/include/formatters/tap.hpp index 0c6daa6..f79aa85 100644 --- a/include/formatters/tap.hpp +++ b/include/formatters/tap.hpp @@ -25,9 +25,23 @@ inline std::string TAP::result_to_yaml(const Result& result) { return {}; } + auto message = result.get_message(); + std::ostringstream oss; oss << " " << "---" << std::endl; - oss << " " << "message: " << "'" << result.get_message() << "'" << std::endl; + if (message.contains("\n")) { + oss << " " << "message: |" << std::endl; + std::string indented_message = message; // split on newlines and indent + std::string::size_type pos = 0; + while ((pos = indented_message.find('\n', pos)) != std::string::npos) { + indented_message.replace(pos, 1, "\n "); + pos += 2; // Skip over the newline and the space we just added + } + oss << " " << " " << indented_message << std::endl; + } else { + oss << " " << "message: " << '"' << result.get_message() << '"' << std::endl; + } + oss << " " << "severity: failure" << std::endl; oss << " " << "at:" << std::endl; oss << " " << " " << "file: " << result.get_location().file_name() << std::endl; @@ -88,13 +102,9 @@ inline void TAP::format(const ItBase& it) { std::string description = Util::join(descriptions, " "); - if (color_output) { - buffer << (it.get_result().status() ? GREEN : RED); - } - buffer << (it.get_result().status() ? "ok" : "not ok"); - if (color_output) { - buffer << RESET; - } + buffer << status_color(it.get_result().status()); + buffer << (it.get_result().is_success() ? "ok" : "not ok"); + buffer << reset_color(); buffer << " " << get_and_increment_test_counter() << " - " << description << std::endl; buffer << result_to_yaml(it.get_result()); } diff --git a/include/formatters/term_colors.hpp b/include/formatters/term_colors.hpp index 06ef72b..a44b763 100644 --- a/include/formatters/term_colors.hpp +++ b/include/formatters/term_colors.hpp @@ -1,6 +1,8 @@ /** @file */ #pragma once +namespace CppSpec { + // the following are Unix/BASH ONLY terminal color codes. constexpr auto RESET("\033[0m"); constexpr auto BLACK("\033[30m"); /* Black */ @@ -19,3 +21,5 @@ constexpr auto BOLDBLUE("\033[1m\033[34m"); /* Bold Blue */ constexpr auto BOLDMAGENTA("\033[1m\033[35m"); /* Bold Magenta */ constexpr auto BOLDCYAN("\033[1m\033[36m"); /* Bold Cyan */ constexpr auto BOLDWHITE("\033[1m\033[37m"); /* Bold White */ + +} // namespace CppSpec diff --git a/include/formatters/verbose.hpp b/include/formatters/verbose.hpp index f5e768a..b31d9cc 100644 --- a/include/formatters/verbose.hpp +++ b/include/formatters/verbose.hpp @@ -31,26 +31,18 @@ inline void Verbose::format(const Description& description) { } inline void Verbose::format(const ItBase& it) { - if (color_output) { - out_stream << (it.get_result().status() ? GREEN : RED); - } + out_stream << status_color(it.get_result().status()); out_stream << it.padding() << it.get_description() << std::endl; - if (color_output) { - out_stream << RESET; - } + out_stream << reset_color(); // Print any failures if we've got them // 'it' having a bad status necessarily // implies that there are failure messages for (const Result& result : it.get_results()) { if (result.is_failure()) { - if (color_output) { - out_stream << RED; // make them red - } + out_stream << set_color(RED); out_stream << result.get_message() << std::endl; - if (color_output) { - out_stream << RESET; - } + out_stream << reset_color(); } } diff --git a/include/it.hpp b/include/it.hpp index 8342596..d76fe15 100755 --- a/include/it.hpp +++ b/include/it.hpp @@ -111,7 +111,7 @@ class ItCD : public ItBase { ItCD(std::source_location location, T& subject, Block block) : ItBase(location), block(block), subject(subject) {} ExpectationValue is_expected(std::source_location location = std::source_location::current()) { - return ExpectationValue(*this, subject, location); + return {*this, subject, location}; } void run() override; }; @@ -127,17 +127,17 @@ class ItCD : public ItBase { */ template ExpectationValue ItBase::expect(T value, std::source_location location) { - return ExpectationValue(*this, value, location); + return {*this, value, location}; } template ExpectationFunc ItBase::expect(T block, std::source_location location) { - return ExpectationFunc(*this, block, location); + return {*this, block, location}; } template ExpectationValue ItBase::expect(Let& let, std::source_location location) { - return ExpectationValue(*this, let.value(), location); + return {*this, let.value(), location}; } /** @@ -150,11 +150,11 @@ ExpectationValue ItBase::expect(Let& let, std::source_location location) { template ExpectationValue> ItBase::expect(std::initializer_list init_list, std::source_location location) { - return ExpectationValue>(*this, init_list, location); + return {*this, init_list, location}; } inline ExpectationValue ItBase::expect(const char* str, std::source_location location) { - return ExpectationValue(*this, std::string(str), location); + return {*this, std::string(str), location}; } } // namespace CppSpec diff --git a/include/it_base.hpp b/include/it_base.hpp index 82a215d..2c1f3b1 100755 --- a/include/it_base.hpp +++ b/include/it_base.hpp @@ -133,7 +133,7 @@ class ItBase : public Runnable { void add_result(const Result& result) { results.push_back(result); } std::list& get_results() noexcept { return results; } - const std::list& get_results() const noexcept { return results; } + [[nodiscard]] const std::list& get_results() const noexcept { return results; } void clear_results() noexcept { results.clear(); } [[nodiscard]] Result get_result() const override { @@ -141,7 +141,7 @@ class ItBase : public Runnable { return Result::success(this->get_location()); } - return *std::ranges::fold_left_first(results, std::logical_and<>{}); + return *std::ranges::fold_left_first(results, &Result::reduce); } }; diff --git a/include/matchers/equal.hpp b/include/matchers/equal.hpp index 8ecaa82..c7fc82d 100644 --- a/include/matchers/equal.hpp +++ b/include/matchers/equal.hpp @@ -44,18 +44,18 @@ std::string Equal::failure_message() { template std::string Equal::failure_message_when_negated() { std::stringstream ss; - ss << "expected not " << Pretty::inspect_object(MatcherBase::expected()) << "\n" - << " got " << actual_inspected() << "\n" - << "Compared using `==`" << std::endl; + ss << "expected not " << Pretty::inspect_object(MatcherBase::expected()) << std::endl; + ss << " got " << actual_inspected() << std::endl; + ss << "Compared using `==`"; return ss.str(); } template std::string Equal::simple_failure_message() { std::stringstream ss; - ss << "expected " << Pretty::inspect_object(MatcherBase::expected()) << "\n" - << " got " << actual_inspected() << "\n" - << "Compared using `==`" << std::endl; + ss << "expected " << Pretty::inspect_object(MatcherBase::expected()) << std::endl; + ss << " got " << actual_inspected() << std::endl; + ss << "Compared using `==`"; return ss.str(); } diff --git a/include/matchers/matcher_base.hpp b/include/matchers/matcher_base.hpp index ed9aca0..fc5b4e1 100644 --- a/include/matchers/matcher_base.hpp +++ b/include/matchers/matcher_base.hpp @@ -56,6 +56,8 @@ class MatcherBase : public Pretty { // commonly used constructor MatcherBase(Expectation& expectation, Expected expected) : expected_(expected), expectation_(expectation) {} + virtual ~MatcherBase() = default; + /*--------- Helper functions -------------*/ virtual std::string failure_message(); @@ -82,7 +84,6 @@ class MatcherBase : public Pretty { // Run the matcher Result run(); - // TODO: match and negated match should return Result virtual bool match() = 0; virtual bool negated_match() { return !match(); } @@ -151,19 +152,8 @@ std::string MatcherBase::description() { */ template Result MatcherBase::run() { - ItBase* parent = expectation_.get_it(); - if (parent != nullptr) { - // If we need a description for our test, generate it - // unless we're ignoring the output. - if (parent->needs_description() && !expectation_.ignore_failure()) { - parent->set_description( - (expectation_.sign() ? PositiveExpectationHandler::verb() : NegativeExpectationHandler::verb()) + " " + - this->description()); - } - } - - Result result = expectation_.sign() ? PositiveExpectationHandler::handle_matcher(*this) - : NegativeExpectationHandler::handle_matcher(*this); + Result result = expectation_.positive() ? PositiveExpectationHandler::handle_matcher(*this) + : NegativeExpectationHandler::handle_matcher(*this); result.set_type(Util::demangle(typeid(*this).name())); @@ -176,7 +166,19 @@ Result MatcherBase::run() { "return a string?"); } - if (parent && !expectation_.ignore_failure()) { + if (expectation_.ignored()) { + result.set_status(Result::Status::Skipped); + } + + ItBase* parent = expectation_.get_it(); + if (parent != nullptr) { + // If we need a description for our test, generate it + // unless we're ignoring the output. + if (parent->needs_description() && !expectation_.ignored()) { + parent->set_description( + (expectation_.positive() ? PositiveExpectationHandler::verb() : NegativeExpectationHandler::verb()) + " " + + this->description()); + } parent->add_result(result); } return result; diff --git a/include/result.hpp b/include/result.hpp index d2338c0..3badd51 100644 --- a/include/result.hpp +++ b/include/result.hpp @@ -6,24 +6,37 @@ #include #include #include +#include namespace CppSpec { class Result { - bool value = false; - std::source_location location; - std::string message; - std::string type; - explicit Result(bool value, std::source_location location, const std::string& message = "") noexcept - : value(value), location(location), message(message) {} - public: + enum class Status { Success, Failure, Error, Skipped }; + Result() = default; /*--------- Status helper functions --------------*/ - [[nodiscard]] bool status() const noexcept { return value; } - [[nodiscard]] bool is_success() const noexcept { return value; } - [[nodiscard]] bool is_failure() const noexcept { return !value; } + void set_status(Status status) noexcept { status_ = status; } + [[nodiscard]] Status status() const noexcept { return status_; } + [[nodiscard]] bool is_success() const noexcept { return status_ == Status::Success; } + [[nodiscard]] bool is_failure() const noexcept { return status_ == Status::Failure; } + [[nodiscard]] bool skipped() const noexcept { return status_ == Status::Skipped; } + [[nodiscard]] bool is_error() const noexcept { return status_ == Status::Error; } + + static Result reduce(const Result& lhs, const Result& rhs) noexcept { + if (lhs.is_failure()) { + return lhs; + } else if (rhs.is_failure()) { + return rhs; + } else if (lhs.is_success()) { + return lhs; + } else if (rhs.is_success()) { + return rhs; + } else { + return lhs; + } + } /*--------- Location helper functions ------------*/ [[nodiscard]] std::source_location get_location() const noexcept { return location; } @@ -33,67 +46,71 @@ class Result { [[nodiscard]] std::string get_type() const noexcept { return type; } [[nodiscard]] std::string get_type() noexcept { return type; } - void set_type(const std::string& type) noexcept { this->type = type; } + void set_type(std::string type) noexcept { this->type = std::move(type); } /*--------- Message helper functions -------------*/ std::string get_message() noexcept { return message; } [[nodiscard]] std::string get_message() const noexcept { return message; } - Result& set_message(const std::string& message) noexcept; + Result& set_message(std::string message) noexcept { + this->message = std::move(message); + return *this; + } /*--------- Explicit constructor functions -------*/ - static Result success(std::source_location location) noexcept; - static Result failure(std::source_location location) noexcept; - static Result success_with(std::source_location location, const std::string& success_message) noexcept; - static Result failure_with(std::source_location location, const std::string& failure_message) noexcept; - - /*-------------- Friend functions ----------------*/ - - // Stream operator - friend std::ostream& operator<<(std::ostream& os, const Result& res); + static Result success(std::source_location location) noexcept { return {Status::Success, location}; } + static Result failure(std::source_location location) noexcept { return {Status::Failure, location}; } + static Result success_with(std::source_location location, std::string success_message) noexcept { + return {Status::Success, location, std::move(success_message)}; + } + static Result failure_with(std::source_location location, std::string failure_message) noexcept { + return {Status::Failure, location, std::move(failure_message)}; + } - friend Result& operator&=(Result& lhs, const Result& rhs) { - lhs.value = lhs.value && rhs.value; - return lhs; + static Result error(std::source_location location) noexcept { return {Status::Error, location}; } + static Result error_with(std::source_location location, std::string error_message) noexcept { + return {Status::Error, location, std::move(error_message)}; } - friend Result operator&&(const Result& lhs, const Result& rhs) { - Result result; - result.location = lhs.location; - result.message = lhs.message + " and\n" + rhs.message; - result.value = lhs.value && rhs.value; - return result; + static Result skipped(std::source_location location) noexcept { return {Status::Skipped, location}; } + static Result skipped_with(std::source_location location, std::string skipped_message) noexcept { + return {Status::Skipped, location, std::move(skipped_message)}; } -}; -inline Result& Result::set_message(const std::string& message) noexcept { - this->message = message; - return *this; -} - -inline Result Result::success(std::source_location location) noexcept { - return Result(true, location); -} -inline Result Result::success_with(std::source_location location, const std::string& success_message) noexcept { - return Result(true, location, success_message); -} - -inline Result Result::failure(std::source_location location) noexcept { - return Result(false, location); -} -inline Result Result::failure_with(std::source_location location, const std::string& failure_message) noexcept { - return Result(false, location, failure_message); -} - -inline std::ostream& operator<<(std::ostream& os, const Result& res) { - std::stringstream ss; - ss << (res.status() ? "Result::success" : "Result::failure"); - - if (not res.get_message().empty()) { - ss << "(\"" + res.get_message() + "\")"; + /*-------------- Friend functions ----------------*/ + + // Stream operator + friend std::ostream& operator<<(std::ostream& os, const Result& res) { + std::stringstream ss; + switch (res.status()) { + case Status::Success: + ss << "Success"; + break; + case Status::Failure: + ss << "Failure"; + break; + case Status::Error: + ss << "Error"; + break; + case Status::Skipped: + ss << "Skipped"; + break; + } + + if (not res.get_message().empty()) { + ss << "(\"" + res.get_message() + "\")"; + } + + return os << ss.str(); } - return os << ss.str(); -} + private: + Status status_ = Status::Success; + std::source_location location; + std::string message; + std::string type; + Result(Status status, std::source_location location, std::string message = "") noexcept + : status_(status), location(location), message(std::move(message)) {} +}; template constexpr bool is_result_v = std::is_same_v; diff --git a/include/runnable.hpp b/include/runnable.hpp index a9a9ca3..2623048 100755 --- a/include/runnable.hpp +++ b/include/runnable.hpp @@ -112,7 +112,7 @@ class Runnable { [[nodiscard]] virtual Result get_result() const { Result result = Result::success(location); for (const auto& child : get_children()) { - result &= child->get_result(); + result = Result::reduce(result, child->get_result()); } return result; } diff --git a/include/runner.hpp b/include/runner.hpp index c23b365..6703112 100644 --- a/include/runner.hpp +++ b/include/runner.hpp @@ -45,7 +45,7 @@ class Runner { bool success = true; for (Description* spec : specs) { spec->timed_run(); - success &= spec->get_result().status(); + success &= !spec->get_result().is_failure(); } for (auto& formatter : formatters) { for (Description* spec : specs) { diff --git a/include/util.hpp b/include/util.hpp index 209e9f2..287c5ba 100755 --- a/include/util.hpp +++ b/include/util.hpp @@ -129,4 +129,17 @@ concept not_c_string = !std::is_same_v && !std::is_same_v describe_a_syntax_spec("describe_a syntax", $ { }); // clang-format on -int main(int argc, char **argv) { +int main(int argc, char** argv) { return CppSpec::parse(argc, argv) .add_specs(describe_a_implicit_spec, describe_a_explicit_spec, describe_a_syntax_spec) .exec() - .status() + .is_success() ? EXIT_SUCCESS : EXIT_FAILURE; } diff --git a/spec/expectations/expectation_spec.cpp b/spec/expectations/expectation_spec.cpp index d379731..65d4f8e 100644 --- a/spec/expectations/expectation_spec.cpp +++ b/spec/expectations/expectation_spec.cpp @@ -6,7 +6,7 @@ using namespace CppSpec; struct CustomMatcher : public Matchers::MatcherBase { CustomMatcher(Expectation& expectation, int expected) : Matchers::MatcherBase(expectation, expected) {}; - bool match() { return expected() == actual(); } + bool match() override { return expected() == actual(); } }; // clang-format off @@ -82,15 +82,15 @@ describe expectation_spec("Expectation", $ { let(e, [&] { return i.expect(5); }); #define expect self.expect - it("flips the ignore_failure flag", _ { - expect(e->ignore_failure()).to_be_false(); - expect(e->ignore().ignore_failure()).to_be_true(); + it("flips the ignored flag", _ { + expect(e->ignored()).to_be_false(); + expect(e->ignore().ignored()).to_be_true(); }); it("makes it so that matches do not alter the status of the parent", _ { expect([=]() mutable { e->ignore().to_equal(4); - return i.get_result().status(); + return i.get_result().is_success(); }).to_be_true(); }); diff --git a/spec/matchers/equal_spec.cpp b/spec/matchers/equal_spec.cpp index 2053534..9c78887 100644 --- a/spec/matchers/equal_spec.cpp +++ b/spec/matchers/equal_spec.cpp @@ -2,6 +2,7 @@ using namespace CppSpec; +// clang-format off describe equal_spec("Equal", $ { it("matches when actual == expected", _ { expect(1).to_equal(1); @@ -26,4 +27,4 @@ describe equal_spec("Equal", $ { }); -CPPSPEC_MAIN(equal_spec); \ No newline at end of file +CPPSPEC_MAIN(equal_spec); diff --git a/spec/matchers/unhandled_exception_spec.cpp b/spec/matchers/unhandled_exception_spec.cpp new file mode 100644 index 0000000..a37bba4 --- /dev/null +++ b/spec/matchers/unhandled_exception_spec.cpp @@ -0,0 +1,30 @@ +#include "cppspec.hpp" + +using namespace CppSpec; + +class UnhandledExceptionMatcher : public CppSpec::Matchers::MatcherBase { + public: + UnhandledExceptionMatcher(CppSpec::Expectation& expectation, int expected) + : MatcherBase(expectation, expected) {} + + bool match() override { throw std::runtime_error("Unhandled exception"); } + + std::string failure_message() override { return "Expected no exception"; } + std::string failure_message_when_negated() override { return "Expected exception"; } + std::string description() override { return "unhandled exception"; } +}; +; + +// clang-format off +describe unhandled_exception_spec("Unhandled exceptions", $ { + it("are treated as errors", _ { + ExpectationValue expectation(0, std::source_location::current()); + UnhandledExceptionMatcher matcher(expectation, 0); + Result result = matcher.run(); + expect(result.is_error()).to_equal(true); + expect(result.get_message()).to_equal("Unhandled exception"); + }); +}); + + +CPPSPEC_MAIN(unhandled_exception_spec); From a126ead473fc6b6953ae863a48a2635f10db2c66 Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Fri, 4 Apr 2025 23:21:33 -0400 Subject: [PATCH 24/33] Fix be_within_spec --- spec/matchers/be_within_spec.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/matchers/be_within_spec.cpp b/spec/matchers/be_within_spec.cpp index bfb4b44..e709a09 100644 --- a/spec/matchers/be_within_spec.cpp +++ b/spec/matchers/be_within_spec.cpp @@ -75,7 +75,7 @@ describe be_within_spec("expect(actual).to_be_within(delta).of(expected)", $ { it("fails when actual is outside the given percent variance", _ { auto base_formatter = Formatters::BaseFormatter(); - auto ex = ExpectationValue(self, 20.1, std::source_location::current()).ignore(); + auto ex = ExpectationValue(20.1, std::source_location::current()); auto matcher = Matchers::BeWithinHelper(ex, 10).percent_of(10.0); expect(matcher.run()).to_fail_with("expected 20.1 to be within 10% of 10"); }); From 510c3c86928c2587e453de81423178f8234afe11 Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Sat, 5 Apr 2025 00:37:51 -0400 Subject: [PATCH 25/33] Truly optional CMake function --- CMakeLists.txt | 21 +++++++++++---------- include/formatters/formatters_base.hpp | 1 + spec/CMakeLists.txt | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e673e15..0bbf7e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,19 +72,20 @@ endfunction(add_spec) function(discover_specs spec_folder) file(GLOB_RECURSE specs ${spec_folder}/*_spec.cpp) - foreach(spec IN LISTS specs) - add_spec(${spec} "") - endforeach() -endfunction(discover_specs) - -function(discover_specs_output_junit spec_folder) - file(GLOB_RECURSE specs ${spec_folder}/*_spec.cpp) + if (${ARGC} GREATER 1) + set(output_junit ${ARGV1}) + else() + set(output_junit FALSE) + endif() foreach(spec IN LISTS specs) - cmake_path(GET spec STEM spec_name) - add_spec(${spec} "--output-junit;${CMAKE_CURRENT_BINARY_DIR}/results/${spec_name}.xml") + if (${output_junit}) + add_spec(${spec} "--output-junit;${CMAKE_CURRENT_BINARY_DIR}/results/${spec_name}.xml") + else() + add_spec(${spec} "") + endif() endforeach() -endfunction(discover_specs_output_junit) +endfunction() # OPTIONS option(CPPSPEC_BUILD_TESTS "Build C++Spec tests") diff --git a/include/formatters/formatters_base.hpp b/include/formatters/formatters_base.hpp index 142e777..623cc8e 100644 --- a/include/formatters/formatters_base.hpp +++ b/include/formatters/formatters_base.hpp @@ -92,6 +92,7 @@ class BaseFormatter { case Result::Status::Skipped: return YELLOW; } + return ""; // Default to no color } const char* reset_color() { return color_output ? RESET : ""; } diff --git a/spec/CMakeLists.txt b/spec/CMakeLists.txt index 040aa5d..2952429 100644 --- a/spec/CMakeLists.txt +++ b/spec/CMakeLists.txt @@ -2,4 +2,4 @@ if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") add_compile_options(-Wno-unused-parameter -Wno-missing-template-arg-list-after-template-kw -Wno-missing-template-keyword -Wno-unknown-warning-option) endif() -discover_specs_output_junit(${CMAKE_CURRENT_SOURCE_DIR}) +discover_specs(${CMAKE_CURRENT_SOURCE_DIR} TRUE) From 37eff630761a342568b55f48373baff6ceb776d9 Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Sat, 5 Apr 2025 01:14:54 -0400 Subject: [PATCH 26/33] spec results in folders --- CMakeLists.txt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0bbf7e5..68068d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,7 +70,7 @@ endfunction(add_spec) # Discover Specs function(discover_specs spec_folder) - file(GLOB_RECURSE specs ${spec_folder}/*_spec.cpp) + file(GLOB_RECURSE specs RELATIVE ${spec_folder} ${spec_folder}/*_spec.cpp) if (${ARGC} GREATER 1) set(output_junit ${ARGV1}) @@ -79,8 +79,11 @@ function(discover_specs spec_folder) endif() foreach(spec IN LISTS specs) + cmake_path(GET spec STEM spec_name) + cmake_path(GET spec PARENT_PATH spec_folder) + if (${output_junit}) - add_spec(${spec} "--output-junit;${CMAKE_CURRENT_BINARY_DIR}/results/${spec_name}.xml") + add_spec(${spec} "--output-junit;${CMAKE_CURRENT_BINARY_DIR}/results/${spec_folder}/${spec_name}.xml") else() add_spec(${spec} "") endif() From 54dace997134b53a6568603a7611e234733aa900 Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Sun, 6 Apr 2025 09:13:09 -0400 Subject: [PATCH 27/33] Add macOS testing to CI --- .github/workflows/test.yml | 64 ++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 83cdfd4..21d3742 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,40 +16,56 @@ jobs: strategy: fail-fast: false matrix: - compiler: [native, llvm] - os: [ubuntu-latest, windows-latest] + compiler: [native, llvm, gcc] + os: [ubuntu-latest, windows-latest, macos-13, macos-15] exclude: - os: windows-latest compiler: llvm + - os: ubuntu-latest + compiler: gcc # gcc is already the default steps: - - name: Checkout code - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 - - name: Install CMake - uses: lukka/get-cmake@latest + - name: Install CMake + uses: lukka/get-cmake@latest - - name: Use LLVM and Clang - run: | - echo "CC=clang-18" >> $GITHUB_ENV - echo "CXX=clang++-18" >> $GITHUB_ENV - if: ${{ matrix.compiler != 'native' }} + - name: Install Clang + if: ${{ matrix.compiler == 'llvm' && (matrix.os == 'macos-13' || matrix.os == 'macos-15') }} + run: | + brew install llvm@18 + brew link --force --overwrite llvm@18 + echo "CC=$(brew --prefix llvm@18)/bin/clang" >> $GITHUB_ENV + echo "CXX=$(brew --prefix llvm@18)/bin/clang++" >> $GITHUB_ENV - - name: Configure - run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES + - name: Use LLVM and Clang + run: | + echo "CC=clang-18" >> $GITHUB_ENV + echo "CXX=clang++-18" >> $GITHUB_ENV + if: ${{ matrix.compiler == 'llvm' && (matrix.os != 'macos-13' || matrix.os != 'macos-15') }} - - name: Build - run: cmake --build build --config Release + - name: Use GCC + run: | + echo "CC=gcc-14" >> $GITHUB_ENV + echo "CXX=g++-14" >> $GITHUB_ENV + if: ${{ matrix.compiler == 'gcc' && (matrix.os != 'ubuntu-latest') }} - - name: Test - run: ctest --test-dir build --build-config Release --output-on-failure + - name: Configure + run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES - - name: Upload Test Results - uses: actions/upload-artifact@v4 - if: always() - with: - name: Test Results (${{ matrix.os }} - ${{ matrix.compiler }}) - path: build/spec/results/*.xml + - name: Build + run: cmake --build build --config Release + + - name: Test + run: ctest --test-dir build --build-config Release --output-on-failure + + - name: Upload Test Results + uses: actions/upload-artifact@v4 + if: always() + with: + name: Test Results (${{ matrix.os }} - ${{ matrix.compiler }}) + path: build/spec/results/*.xml publish-test-results: name: "Publish Tests Results" @@ -72,4 +88,4 @@ jobs: - name: Publish Test Results uses: EnricoMi/publish-unit-test-result-action@v2 with: - files: "artifacts/**/*.xml" \ No newline at end of file + files: "artifacts/**/*.xml" From a248bd4c58c17d271fbba9ebdd9555256429e325 Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Sun, 6 Apr 2025 09:23:16 -0400 Subject: [PATCH 28/33] Remove non macOS compatible things --- .github/workflows/test.yml | 2 +- include/formatters/junit_xml.hpp | 18 ++++++++++++++---- include/it_base.hpp | 6 ++++-- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 21d3742..8a5898c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,7 +43,7 @@ jobs: run: | echo "CC=clang-18" >> $GITHUB_ENV echo "CXX=clang++-18" >> $GITHUB_ENV - if: ${{ matrix.compiler == 'llvm' && (matrix.os != 'macos-13' || matrix.os != 'macos-15') }} + if: ${{ matrix.compiler == 'llvm' && (matrix.os != 'macos-13' && matrix.os != 'macos-15') }} - name: Use GCC run: | diff --git a/include/formatters/junit_xml.hpp b/include/formatters/junit_xml.hpp index 72f0f37..fe87e8e 100644 --- a/include/formatters/junit_xml.hpp +++ b/include/formatters/junit_xml.hpp @@ -90,8 +90,8 @@ struct TestCase { std::stringstream ss; ss << start << ">" << std::endl; - ss << *std::ranges::fold_left_first(xml_results, - [](const std::string& acc, const std::string& r) { return acc + "\n" + r; }); + ss << std::accumulate(xml_results.begin(), xml_results.end(), std::string{}, + [](const std::string& acc, const std::string& r) { return acc + "\n" + r; }); ss << std::endl; ss << " "; return ss.str(); @@ -115,8 +115,18 @@ struct TestSuite { : id(get_next_id()), name(std::move(name)), time(time), timestamp(timestamp), tests(tests), failures(failures) {} [[nodiscard]] std::string to_xml() const { - auto timestamp_str = - std::format("{0:%F}T{0:%T}", std::chrono::zoned_time(std::chrono::current_zone(), timestamp).get_local_time()); + std::string timestamp_str; + if constexpr (requires { std::chrono::current_zone(); }) { + auto localtime = std::chrono::zoned_time(std::chrono::current_zone(), timestamp).get_local_time(); + timestamp_str = std::format("{0:%F}T{0:%T}", localtime); + } else { + // Cludge because macOS doesn't have std::chrono::current_zone() or std::chrono::zoned_time() + std::time_t time_t_timestamp = std::chrono::system_clock::to_time_t(timestamp); + std::tm localtime = *std::localtime(&time_t_timestamp); + std::ostringstream oss; + oss << std::put_time(&localtime, "%Y-%m-%dT%H:%M:%S"); + timestamp_str = oss.str(); + } std::stringstream ss; ss << " " diff --git a/include/it_base.hpp b/include/it_base.hpp index 2c1f3b1..4041476 100755 --- a/include/it_base.hpp +++ b/include/it_base.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -137,11 +138,12 @@ class ItBase : public Runnable { void clear_results() noexcept { results.clear(); } [[nodiscard]] Result get_result() const override { + auto default_result = Result::success(this->get_location()); if (results.empty()) { - return Result::success(this->get_location()); + return default_result; } - return *std::ranges::fold_left_first(results, &Result::reduce); + return std::accumulate(results.begin(), results.end(), default_result, &Result::reduce); } }; From f66223461bb7d2bbf92732f77c585f65608793c3 Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Sun, 6 Apr 2025 09:29:52 -0400 Subject: [PATCH 29/33] use __APPLE__ preprocessor macro --- include/formatters/junit_xml.hpp | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/include/formatters/junit_xml.hpp b/include/formatters/junit_xml.hpp index fe87e8e..c15e085 100644 --- a/include/formatters/junit_xml.hpp +++ b/include/formatters/junit_xml.hpp @@ -116,17 +116,18 @@ struct TestSuite { [[nodiscard]] std::string to_xml() const { std::string timestamp_str; - if constexpr (requires { std::chrono::current_zone(); }) { - auto localtime = std::chrono::zoned_time(std::chrono::current_zone(), timestamp).get_local_time(); - timestamp_str = std::format("{0:%F}T{0:%T}", localtime); - } else { - // Cludge because macOS doesn't have std::chrono::current_zone() or std::chrono::zoned_time() - std::time_t time_t_timestamp = std::chrono::system_clock::to_time_t(timestamp); - std::tm localtime = *std::localtime(&time_t_timestamp); - std::ostringstream oss; - oss << std::put_time(&localtime, "%Y-%m-%dT%H:%M:%S"); - timestamp_str = oss.str(); - } +#ifdef __APPLE__ + // Cludge because macOS doesn't have std::chrono::current_zone() or std::chrono::zoned_time() + std::time_t time_t_timestamp = std::chrono::system_clock::to_time_t(timestamp); + std::tm localtime = *std::localtime(&time_t_timestamp); + std::ostringstream oss; + oss << std::put_time(&localtime, "%Y-%m-%dT%H:%M:%S"); + timestamp_str = oss.str(); +#else + // Use std::chrono::current_zone() and std::chrono::zoned_time() if available (C++20) + auto localtime = std::chrono::zoned_time(std::chrono::current_zone(), timestamp).get_local_time(); + timestamp_str = std::format("{0:%F}T{0:%T}", localtime); +#endif std::stringstream ss; ss << " " From 0bd43ee791b0ae5296d36c4915c1017474d23215 Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Sun, 6 Apr 2025 09:34:11 -0400 Subject: [PATCH 30/33] Further tune test matrix --- .github/workflows/test.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a5898c..9d920db 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,13 +16,13 @@ jobs: strategy: fail-fast: false matrix: - compiler: [native, llvm, gcc] + compiler: [native, llvm-18, gcc-14] os: [ubuntu-latest, windows-latest, macos-13, macos-15] exclude: - os: windows-latest - compiler: llvm - - os: ubuntu-latest - compiler: gcc # gcc is already the default + compiler: llvm-18 + - os: macos-13 + compiler: native # AppleClang is too old steps: - name: Checkout code @@ -32,7 +32,7 @@ jobs: uses: lukka/get-cmake@latest - name: Install Clang - if: ${{ matrix.compiler == 'llvm' && (matrix.os == 'macos-13' || matrix.os == 'macos-15') }} + if: ${{ matrix.compiler == 'llvm-18' && (matrix.os == 'macos-13' || matrix.os == 'macos-15') }} run: | brew install llvm@18 brew link --force --overwrite llvm@18 @@ -43,13 +43,13 @@ jobs: run: | echo "CC=clang-18" >> $GITHUB_ENV echo "CXX=clang++-18" >> $GITHUB_ENV - if: ${{ matrix.compiler == 'llvm' && (matrix.os != 'macos-13' && matrix.os != 'macos-15') }} + if: ${{ matrix.compiler == 'llvm-18' && (matrix.os != 'macos-13' && matrix.os != 'macos-15') }} - name: Use GCC run: | echo "CC=gcc-14" >> $GITHUB_ENV echo "CXX=g++-14" >> $GITHUB_ENV - if: ${{ matrix.compiler == 'gcc' && (matrix.os != 'ubuntu-latest') }} + if: ${{ matrix.compiler == 'gcc-14' }} - name: Configure run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES From aca7b2ad5590d542939808e2ef5b468202e60537 Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Sun, 6 Apr 2025 09:43:29 -0400 Subject: [PATCH 31/33] Add Windows ClangCL --- .github/workflows/test.yml | 9 +++++++-- README.md | 10 +++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9d920db..a99e244 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: os: [ubuntu-latest, windows-latest, macos-13, macos-15] exclude: - os: windows-latest - compiler: llvm-18 + compiler: gcc-14 - os: macos-13 compiler: native # AppleClang is too old @@ -32,12 +32,12 @@ jobs: uses: lukka/get-cmake@latest - name: Install Clang - if: ${{ matrix.compiler == 'llvm-18' && (matrix.os == 'macos-13' || matrix.os == 'macos-15') }} run: | brew install llvm@18 brew link --force --overwrite llvm@18 echo "CC=$(brew --prefix llvm@18)/bin/clang" >> $GITHUB_ENV echo "CXX=$(brew --prefix llvm@18)/bin/clang++" >> $GITHUB_ENV + if: ${{ matrix.compiler == 'llvm-18' && (matrix.os == 'macos-13' || matrix.os == 'macos-15') }} - name: Use LLVM and Clang run: | @@ -53,6 +53,11 @@ jobs: - name: Configure run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES + if: ${{ matrix.compiler != 'llvm-18' || matrix.os != 'windows-latest' }} + + - name: Configure ClangCL + run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES -G "Visual Studio 17 2022" -T ClangCL + if: ${{ matrix.compiler == 'llvm-18' && matrix.os == 'windows-latest' }} - name: Build run: cmake --build build --config Release diff --git a/README.md b/README.md index b457ce6..4f4a2df 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# C++Spec +# C++Spec [![license](https://img.shields.io/badge/license-MIT-blue)](https://choosealicense.com/licenses/mit/) [![GitHub Action](https://github.com/toroidal-code/cppspec/actions/workflows/test.yml/badge.svg)]()  [![GitHub release](https://img.shields.io/github/release/toroidal-code/cppspec.svg)](https://github.com/toroidal-code/cppspec/releases/latest)  @@ -12,9 +12,13 @@ See [http://cppspec.readthedocs.org/](http://cppspec.readthedocs.org/) for full ## Requirements -C++Spec requires a compiler and standard library with support for C++20. +C++Spec requires a compiler and standard library with support for C++23: Currently tested and confirmed working are: +- LLVM/Clang 18 (on Linux, macOS, and Windows) +- GCC 14.2 (on Linux and macOS) +- MSVC 19.43 (on Windows) +- AppleClang 16 (on macOS) -__Note:__ Only the tests require being compiled with C++20 support (`-std=c++20`). No other part of an existing project's build must be modified. +__Note:__ Only the tests require being compiled with C++23 support (`-std=c++23`). No other part of an existing project's build must be modified. ## Usage The recommended usage is as a subproject integrated into your build system. For CMake this would look something like below: From 4416e6553e2239e3220887ad9252c090ef66d1f7 Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Sun, 6 Apr 2025 09:47:34 -0400 Subject: [PATCH 32/33] reorder be_within members --- include/matchers/numeric/be_within.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/matchers/numeric/be_within.hpp b/include/matchers/numeric/be_within.hpp index 3c78db6..98b9182 100644 --- a/include/matchers/numeric/be_within.hpp +++ b/include/matchers/numeric/be_within.hpp @@ -8,8 +8,8 @@ class BeWithin; template class BeWithinHelper { - E tolerance; Expectation& expectation; + E tolerance; std::string msg; public: @@ -23,8 +23,8 @@ class BeWithinHelper { template class BeWithin : public MatcherBase { - std::string unit; E tolerance; + std::string unit; public: BeWithin(Expectation& expectation, E tolerance, E value, std::string_view unit) From 6103762e8f9300f4130d39ffbf8ec332c49f42ad Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Sun, 6 Apr 2025 10:02:25 -0400 Subject: [PATCH 33/33] Add doxygen action --- .github/workflows/doxygen.yml | 54 +++++++++++++++++++++++++++++++++++ CMakeLists.txt | 5 ++-- 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/doxygen.yml diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml new file mode 100644 index 0000000..6a7af32 --- /dev/null +++ b/.github/workflows/doxygen.yml @@ -0,0 +1,54 @@ +name: Generate Doxygen Documentation + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + generate-docs: + runs-on: ubuntu-latest + + steps: + # Checkout the repository + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install CMake + uses: lukka/get-cmake@latest + + - name: Install doxygen + uses: ssciwr/doxygen-install@v1 + + - name: Install graphviz + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y graphviz + + - name: Run configure + run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DCPPSPEC_BUILD_DOCS=ON + + # Generate Doxygen documentation + - name: Generate Doxygen documentation + run: cmake --build build --target doxygen + + # Deploy to gh-pages branch + - name: Deploy to gh-pages branch + run: | + # Configure Git + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + # Create or switch to gh-pages branch + git fetch origin gh-pages || true + git checkout gh-pages || git checkout --orphan gh-pages + + # Remove old files and copy new documentation + git rm -rf doxygen || true + mv build/html doxygen + + # Commit and push changes + git add . + git commit -m "Update Doxygen documentation" + git push origin gh-pages --force diff --git a/CMakeLists.txt b/CMakeLists.txt index 68068d2..6849e15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,8 +121,9 @@ if(CPPSPEC_BUILD_DOCS) endif() FetchContent_Declare(doxygen-awesome-css - URL https://github.com/jothepro/doxygen-awesome-css/archive/refs/tags/v2.2.1.tar.gz - URL_HASH MD5=340d3a206794ac01a91791c2a513991f + GIT_REPOSITORY https://github.com/jothepro/doxygen-awesome-css/ + GIT_TAG v2.3.4 + GIT_SHALLOW 1 ) FetchContent_MakeAvailable(doxygen-awesome-css)