diff --git a/include/mimic++/reporting/StringifyReports.hpp b/include/mimic++/reporting/StringifyReports.hpp index 1952b003b..5dd88e301 100644 --- a/include/mimic++/reporting/StringifyReports.hpp +++ b/include/mimic++/reporting/StringifyReports.hpp @@ -142,6 +142,39 @@ namespace mimicpp::reporting::detail return std::move(ss).str(); } + struct inapplicable_reason_printer + { + template + OutIter operator()([[maybe_unused]] OutIter out, [[maybe_unused]] state_applicable const& state) const + { + unreachable(); + } + + template + OutIter operator()(OutIter out, const state_inapplicable& state) const + { + auto const totalSequences = std::ranges::ssize(state.sequenceRatings) + + std::ranges::ssize(state.inapplicableSequences); + return format::format_to( + std::move(out), + "it's not head of {} Sequence(s) ({} total).", + std::ranges::ssize(state.inapplicableSequences), + totalSequences); + } + + template + OutIter operator()(OutIter out, const state_saturated& state) const + { + out = format::format_to( + std::move(out), + "it's already saturated (matched {} out of {} times).", + state.count, + state.max); + + return out; + } + }; + [[nodiscard]] inline StringT stringify_inapplicable_matches(CallReport const& call, std::span expectations) { @@ -160,14 +193,20 @@ namespace mimicpp::reporting::detail stringify_call_report_arguments(std::ostreambuf_iterator{ss}, call, "\t\t"); } - ss << expectations.size() << " inapplicable but otherwise matching Expectation(s):\n"; + ss << expectations.size() << " inapplicable but otherwise matching Expectation(s):"; for (int i{}; auto& expReport : expectations) { - ss << "\t#" << ++i << " "; + ss << "\n\t#" << ++i << " "; stringify_expectation_report_from(std::ostreambuf_iterator{ss}, expReport); + ss << "\tBecause "; + std::visit( + std::bind_front(inapplicable_reason_printer{}, std::ostreambuf_iterator{ss}), + expReport.controlReport); + ss << "\n"; + if (!expReport.requirementDescriptions.empty()) { ss << "\t" << "With Adherence(s):\n"; @@ -177,10 +216,6 @@ namespace mimicpp::reporting::detail expReport.requirementDescriptions, "\t + "); } - else - { - ss << "\t" << "With any Requirements.\n"; - } } stringify_stacktrace( @@ -212,12 +247,12 @@ namespace mimicpp::reporting::detail } else { - ss << noMatchReports.size() << " non-matching Expectation(s):\n"; + ss << noMatchReports.size() << " non-matching Expectation(s):"; for (int i{}; auto& [expReport, outcomes] : noMatchReports) { - ss << "\t#" << ++i << " "; + ss << "\n\t#" << ++i << " "; stringify_expectation_report_from(std::ostreambuf_iterator{ss}, expReport); ss << "\t" << "Due to Violation(s):\n"; diff --git a/test/unit-tests/reporting/StringifyReports.cpp b/test/unit-tests/reporting/StringifyReports.cpp index eec0db6e5..544934a27 100644 --- a/test/unit-tests/reporting/StringifyReports.cpp +++ b/test/unit-tests/reporting/StringifyReports.cpp @@ -10,6 +10,9 @@ using namespace mimicpp; namespace { inline reporting::control_state_t const commonApplicableState = reporting::state_applicable{13, 1337, 42}; + inline reporting::control_state_t const commonInapplicableState = reporting::state_inapplicable{13, 1337, 42, {sequence::rating{1}}, {{sequence::Tag{1337}}}}; + inline reporting::control_state_t const commonSaturatedState = reporting::state_saturated{42, 42, 42}; + [[nodiscard]] Stacktrace make_shallow_stacktrace() { @@ -170,3 +173,164 @@ TEST_CASE( } #endif + +TEST_CASE( + "detail::stringify_inapplicable_matches converts the information to a pretty formatted text.", + "[reporting][detail]") +{ + reporting::CallReport const callReport{ + .returnTypeInfo = reporting::TypeReport::make(), + .argDetails = { + {{reporting::TypeReport::make(), "1337"}, + {reporting::TypeReport::make(), "\"Hello, World!\""}}}, + .fromLoc = std::source_location::current(), + .fromCategory = ValueCategory::any, + .fromConstness = Constness::any}; + + reporting::ExpectationReport const expectationReport1{ + .info = {.sourceLocation = std::source_location::current(), .mockName = "Mock-Name"}, + .controlReport = commonInapplicableState, + .finalizerDescription = std::nullopt, + .requirementDescriptions = { + {"expect: arg[1] not empty", + std::nullopt, + "expect: arg[0] > 0"}} + }; + + reporting::ExpectationReport const expectationReport2{ + .info = {.sourceLocation = std::source_location::current(), .mockName = "Mock-Name2"}, + .controlReport = commonSaturatedState, + .finalizerDescription = std::nullopt, + .requirementDescriptions = {{"expect: test"}} + }; + + std::vector expectationReports{expectationReport1, expectationReport2}; + auto const text = reporting::detail::stringify_inapplicable_matches(callReport, expectationReports); + + // note the Adherence reordering + std::string const regex = + R"(Unmatched Call from `.+`#L\d+, `.+` + Where: + arg\[0\] => int: 1337 + arg\[1\] => std::string: "Hello, World!" +2 inapplicable but otherwise matching Expectation\(s\): + #1 Expectation from `.+`#L\d+, `.+` + Because it's not head of 1 Sequence\(s\) \(2 total\). + With Adherence\(s\): + \+ expect: arg\[0\] > 0 + \+ expect: arg\[1\] not empty + + #2 Expectation from `.+`#L\d+, `.+` + Because it's already saturated \(matched 42 out of 42 times\). + With Adherence\(s\): + \+ expect: test +)"; + REQUIRE_THAT( + text, + Catch::Matchers::Matches(regex)); +} + +TEST_CASE( + "detail::stringify_inapplicable_matches omits \"Where\"-Section, when no arguments exist.", + "[reporting][detail]") +{ + reporting::CallReport const callReport{ + .returnTypeInfo = reporting::TypeReport::make(), + .argDetails = {}, + .fromLoc = std::source_location::current(), + .fromCategory = ValueCategory::any, + .fromConstness = Constness::any}; + + reporting::ExpectationReport const expectationReport{ + .info = {.sourceLocation = std::source_location::current(), .mockName = "Mock-Name"}, + .controlReport = commonSaturatedState, + .finalizerDescription = std::nullopt, + .requirementDescriptions = {{"expect: some requirement"}} + }; + + std::vector expectationReports{expectationReport}; + auto const text = reporting::detail::stringify_inapplicable_matches(callReport, expectationReports); + + std::string const regex = + R"(Unmatched Call from `.+`#L\d+, `.+` +1 inapplicable but otherwise matching Expectation\(s\): + #1 Expectation from `.+`#L\d+, `.+` + Because it's already saturated \(matched 42 out of 42 times\). + With Adherence\(s\): + \+ expect: some requirement +)"; + REQUIRE_THAT( + text, + Catch::Matchers::Matches(regex)); +} + +TEST_CASE( + "detail::stringify_inapplicable_matches omits \"With Adherence(s)\"-Section, when no requirements exist.", + "[reporting][detail]") +{ + reporting::CallReport const callReport{ + .returnTypeInfo = reporting::TypeReport::make(), + .argDetails = {}, + .fromLoc = std::source_location::current(), + .fromCategory = ValueCategory::any, + .fromConstness = Constness::any}; + + reporting::ExpectationReport const expectationReport{ + .info = {.sourceLocation = std::source_location::current(), .mockName = "Mock-Name"}, + .controlReport = commonSaturatedState, + .finalizerDescription = std::nullopt + }; + + std::vector expectationReports{expectationReport}; + auto const text = reporting::detail::stringify_inapplicable_matches(callReport, expectationReports); + + std::string const regex = + R"(Unmatched Call from `.+`#L\d+, `.+` +1 inapplicable but otherwise matching Expectation\(s\): + #1 Expectation from `.+`#L\d+, `.+` + Because it's already saturated \(matched 42 out of 42 times\). +)"; + REQUIRE_THAT( + text, + Catch::Matchers::Matches(regex)); +} + +#if MIMICPP_DETAIL_HAS_WORKING_STACKTRACE_BACKEND + +TEST_CASE( + "detail::stringify_inapplicable_matches adds the Stacktrace, if existing.", + "[reporting][detail]") +{ + reporting::CallReport const callReport{ + .returnTypeInfo = reporting::TypeReport::make(), + .argDetails = {}, + .fromLoc = std::source_location::current(), + .stacktrace = make_shallow_stacktrace(), + .fromCategory = ValueCategory::any, + .fromConstness = Constness::any}; + + reporting::ExpectationReport const expectationReport{ + .info = {.sourceLocation = std::source_location::current(), .mockName = "Mock-Name"}, + .controlReport = commonInapplicableState, + .finalizerDescription = std::nullopt + }; + + std::vector expectationReports{expectationReport}; + auto const text = reporting::detail::stringify_inapplicable_matches(callReport, expectationReports); + CAPTURE(text); + auto const stacktraceBegin = std::ranges::search(text, std::string_view{"Stacktrace:\n"}).begin(); + REQUIRE(stacktraceBegin != text.cend()); + REQUIRE_THAT( + (std::string{text.cbegin(), stacktraceBegin}), + Catch::Matchers::EndsWith("Because it's not head of 1 Sequence(s) (2 total).\n\n")); + + std::string const stacktraceRegex = + R"(Stacktrace: +#0 `.+`#L\d+, `.+` +(?:#\d+ `.*`#L\d+, `.*`\n)*)"; + REQUIRE_THAT( + (std::string{stacktraceBegin, text.cend()}), + Catch::Matchers::Matches(stacktraceRegex)); +} + +#endif