From 8a362c3c7a1ddb36e9745abecefd30fe33db71bf Mon Sep 17 00:00:00 2001 From: Bartek Kryza Date: Fri, 25 Aug 2023 19:53:59 +0200 Subject: [PATCH] Added from_to test case --- src/config/schema.h | 1 + .../plantuml/sequence_diagram_generator.cc | 172 ++++++++++++++---- .../plantuml/sequence_diagram_generator.h | 5 +- src/sequence_diagram/model/message.h | 30 +++ tests/t20034/.clang-uml | 15 ++ tests/t20034/t20034.cc | 55 ++++++ tests/t20034/test_case.h | 63 +++++++ tests/test_cases.cc | 1 + 8 files changed, 306 insertions(+), 36 deletions(-) create mode 100644 tests/t20034/.clang-uml create mode 100644 tests/t20034/t20034.cc create mode 100644 tests/t20034/test_case.h diff --git a/src/config/schema.h b/src/config/schema.h index 1ff18c05..fad26094 100644 --- a/src/config/schema.h +++ b/src/config/schema.h @@ -195,6 +195,7 @@ types: generate_condition_statements: !optional bool participants_order: !optional [string] start_from: !optional [source_location_t] + from_to: !optional [[source_location_t]] package_diagram_t: type: !variant [package] # diff --git a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc index 4bbb4b24..82209d67 100644 --- a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc +++ b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc @@ -59,14 +59,7 @@ void generator::generate_call(const message &m, std::ostream &ostr) const std::string message; model::function::message_render_mode render_mode = - model::function::message_render_mode::full; - - if (config().generate_method_arguments() == - config::method_arguments::abbreviated) - render_mode = model::function::message_render_mode::abbreviated; - else if (config().generate_method_arguments() == - config::method_arguments::none) - render_mode = model::function::message_render_mode::no_arguments; + select_method_arguments_render_mode(); if (to.value().type_name() == "method") { const auto &f = dynamic_cast(to.value()); @@ -415,7 +408,7 @@ void generator::generate_diagram(std::ostream &ostr) const } } - for (const auto &ft : m_config.from_to()) { + for (const auto &ft : config().from_to()) { // First, find the sequence of activities from 'from' location // to 'to' location assert(ft.size() == 2); @@ -423,52 +416,156 @@ void generator::generate_diagram(std::ostream &ostr) const const auto &from_location = ft.front(); const auto &to_location = ft.back(); - if (from_location.location_type == location_t::function) { + if ((from_location.location_type == location_t::function) && + (to_location.location_type == location_t::function)) { common::model::diagram_element::id_t from_activity{0}; common::model::diagram_element::id_t to_activity{0}; - for (const auto &[k, v] : m_model.sequences()) { - const auto &caller = *m_model.participants().at(v.from()); + for (const auto &[k, v] : model().sequences()) { + const auto &caller = *model().participants().at(v.from()); std::string vfrom = caller.full_name(false); if (vfrom == from_location.location) { - LOG_DBG("Found sequence diagram start point: {}", k); + LOG_DBG("Found sequence diagram start point '{}': {}", + vfrom, k); from_activity = k; break; } } if (from_activity == 0) { - LOG_WARN("Failed to find participant with {} for start_from " + LOG_WARN("Failed to find 'from' participant {} for start_from " "condition", from_location.location); continue; } - for (const auto &[k, v] : m_model.sequences()) { - const auto &caller = *m_model.participants().at(v.from()); - std::string vfrom = caller.full_name(false); - if (vfrom == to_location.location) { - LOG_DBG("Found sequence diagram end point: {}", k); - to_activity = k; - break; + for (const auto &[k, v] : model().sequences()) { + for (const auto &m : v.messages()) { + if (m.type() != message_t::kCall) + continue; + const auto &callee = *model().participants().at(m.to()); + std::string vto = callee.full_name(false); + if (vto == to_location.location) { + LOG_DBG("Found sequence diagram end point '{}': {}", + vto, m.to()); + to_activity = m.to(); + break; + } } } if (to_activity == 0) { - LOG_WARN("Failed to find participant with {} for from_to " + LOG_WARN("Failed to find 'to' participant {} for from_to " "condition", to_location.location); continue; } - call_chain_t activity_path; - activity_path.push_back(from_activity); + // Message (call) chains matching the specified from_to condition + std::vector message_chains; - auto found = search_path_to(activity_path, to_activity); + // First find all 'to_activity' call targets in the sequences, i.e. + // all messages pointing to the final 'to_activity' activity + for (const auto &[k, v] : model().sequences()) { + for (const auto &m : v.messages()) { + if (m.type() != message_t::kCall) + continue; + + if (m.to() == to_activity) { + message_chains.push_back(message_chain_t{}); + message_chains.back().push_back(m); + } + } + } + + std::map calls_to_current_chain; + std::map current_chain; + + while (true) { + bool added_message_to_some_chain{false}; + // If target of current message matches any of the + // 'from' constraints in the last messages in + // current chains - append + + if (!calls_to_current_chain.empty()) { + for (auto &[message_chain_index, message] : + calls_to_current_chain) { + message_chains.push_back( + current_chain[message_chain_index]); + message_chains.back().push_back(std::move( + calls_to_current_chain[message_chain_index])); + } + calls_to_current_chain.clear(); + } + + for (auto i = 0U; i < message_chains.size(); i++) { + auto &mc = message_chains[i]; + current_chain[i] = mc; + for (const auto &[k, v] : model().sequences()) { + + for (const auto &m : v.messages()) { + if (m.type() != message_t::kCall) + continue; + + // Ignore recursive calls + if (m.to() == m.from()) { + continue; + } + + if (m.to() == mc.back().from()) { + calls_to_current_chain[i] = m; + added_message_to_some_chain = true; + } + } + } + + // If there are more than one call to the current chain, + // duplicate it as many times as there are calls - 1 + if (calls_to_current_chain.size() >= 1) { + mc.push_back(calls_to_current_chain[i]); + calls_to_current_chain.erase(i); + } + } + + // There is nothing more to find + if (!added_message_to_some_chain) + break; + } + + + + // Reverse the message chains order (they were added starting from + // the destination activity) + for (auto &mc : message_chains) + std::reverse(mc.begin(), mc.end()); + + // Remove identical chains + std::unordered_set message_chains_unique{ + message_chains.begin(), message_chains.end()}; + + auto idx{0U}; + for (const auto &mc : message_chains_unique) { + if (mc.empty()) + continue; + + if (mc.front().from() != from_activity) + continue; + + for (const auto &m : mc) { + generate_call(m, ostr); + } + + if (idx++ < message_chains_unique.size() - 1) + ostr << "====\n"; + } + } + else { + // TODO: Add support for other sequence start location types + continue; } } - for (const auto &sf : m_config.start_from()) { + for (const auto &sf : config().start_from()) { if (sf.location_type == location_t::function) { common::model::diagram_element::id_t start_from{0}; for (const auto &[k, v] : model().sequences()) { @@ -507,15 +604,7 @@ void generator::generate_diagram(std::ostream &ostr) const std::string from_alias = generate_alias(from.value()); model::function::message_render_mode render_mode = - model::function::message_render_mode::full; - - if (config().generate_method_arguments() == - config::method_arguments::abbreviated) - render_mode = model::function::message_render_mode::abbreviated; - else if (config().generate_method_arguments() == - config::method_arguments::none) - render_mode = - model::function::message_render_mode::no_arguments; + select_method_arguments_render_mode(); // For methods or functions in diagrams where they are combined into // file participants, we need to add an 'entry' point call to know @@ -556,4 +645,17 @@ void generator::generate_diagram(std::ostream &ostr) const } } +model::function::message_render_mode +generator::select_method_arguments_render_mode() const +{ + if (config().generate_method_arguments() == + config::method_arguments::abbreviated) + return model::function::message_render_mode::abbreviated; + + if (config().generate_method_arguments() == config::method_arguments::none) + return model::function::message_render_mode::no_arguments; + + return model::function::message_render_mode::full; +} + } // namespace clanguml::sequence_diagram::generators::plantuml diff --git a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.h b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.h index c6546030..0ef5631f 100644 --- a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.h +++ b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.h @@ -42,7 +42,8 @@ template using common_generator = clanguml::common::generators::plantuml::generator; -using call_chain_t = std::vector; +using message_chain_t = + std::vector; /** * @brief Sequence diagram PlantUML generator @@ -142,6 +143,8 @@ private: std::string render_name(std::string name) const; mutable std::set generated_participants_; + model::function::message_render_mode + select_method_arguments_render_mode() const; }; } // namespace plantuml diff --git a/src/sequence_diagram/model/message.h b/src/sequence_diagram/model/message.h index 78fd680f..ac0a55fd 100644 --- a/src/sequence_diagram/model/message.h +++ b/src/sequence_diagram/model/message.h @@ -162,3 +162,33 @@ private: }; } // namespace clanguml::sequence_diagram::model + +namespace std { + +template <> struct hash { + std::size_t operator()( + const clanguml::sequence_diagram::model::message &m) const + { + std::size_t seed = m.from() << 2; + seed ^= m.to(); + seed += std::hash{}(m.full_name(true)); + + return seed; + } +}; + +template <> +struct hash> { + std::size_t operator()( + const std::vector &msgs) + const + { + std::size_t seed = msgs.size() << 8; + for (const auto &m : msgs) { + seed ^= std::hash{}(m); + } + return seed; + } +}; + +} // namespace std diff --git a/tests/t20034/.clang-uml b/tests/t20034/.clang-uml new file mode 100644 index 00000000..83d964c9 --- /dev/null +++ b/tests/t20034/.clang-uml @@ -0,0 +1,15 @@ +compilation_database_dir: .. +output_directory: puml +diagrams: + t20034_sequence: + type: sequence + glob: + - ../../tests/t20034/t20034.cc + include: + namespaces: + - clanguml::t20034 + using_namespace: + - clanguml::t20034 + from_to: + - [function: "clanguml::t20034::D::d2()", + function: "clanguml::t20034::A::a2()"] \ No newline at end of file diff --git a/tests/t20034/t20034.cc b/tests/t20034/t20034.cc new file mode 100644 index 00000000..b11b35a4 --- /dev/null +++ b/tests/t20034/t20034.cc @@ -0,0 +1,55 @@ +#include +namespace clanguml { +namespace t20034 { +struct A { + void a1() { } + void a2() { } + void a3() { } +}; + +struct B { + void b1() + { + a.a1(); + a.a2(); + } + void b2() { a.a2(); } + void b3() { a.a3(); } + + A a; +}; + +struct C { + void c1() { b.b1(); } + void c2() { b.b2(); } + void c3() + { + if (reinterpret_cast(&b) == 0xbadc0de) + c3(); + else + c2(); + } + + B b; +}; + +struct D { + void d1() { c.c1(); } + void d2() + { + c.c1(); + a.a2(); + c.c2(); + c.c3(); + a.a2(); + + auto l = [this]() { a.a2(); }; + l(); + } + void d3() { c.c3(); } + + A a; + C c; +}; +} +} \ No newline at end of file diff --git a/tests/t20034/test_case.h b/tests/t20034/test_case.h new file mode 100644 index 00000000..eb1b9651 --- /dev/null +++ b/tests/t20034/test_case.h @@ -0,0 +1,63 @@ +/** + * tests/t20034/test_case.h + * + * Copyright (c) 2021-2023 Bartek Kryza + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +TEST_CASE("t20034", "[test-case][sequence]") +{ + auto [config, db] = load_config("t20034"); + + auto diagram = config.diagrams["t20034_sequence"]; + + REQUIRE(diagram->name == "t20034_sequence"); + + auto model = generate_sequence_diagram(*db, diagram); + + REQUIRE(model->name() == "t20034_sequence"); + + { + auto puml = generate_sequence_puml(diagram, *model); + AliasMatcher _A(puml); + + REQUIRE_THAT(puml, StartsWith("@startuml")); + REQUIRE_THAT(puml, EndsWith("@enduml\n")); + + // Check if all calls exist + REQUIRE_THAT(puml, HasCall(_A("D"), _A("A"), "a2()")); + + REQUIRE_THAT(puml, HasCall(_A("D"), _A("C"), "c3()")); + REQUIRE_THAT(puml, HasCall(_A("C"), _A("C"), "c2()")); + REQUIRE_THAT(puml, HasCall(_A("C"), _A("B"), "b2()")); + REQUIRE_THAT(puml, HasCall(_A("B"), _A("A"), "a2()")); + + + + REQUIRE_THAT(puml, HasCall(_A("D"), _A("C"), "c2()")); + REQUIRE_THAT(puml, !HasCall(_A("D"), _A("C"), "c1()")); + + + save_puml( + config.output_directory() + "/" + diagram->name + ".puml", puml); + } + +// { +// auto j = generate_sequence_json(diagram, *model); +// +// using namespace json; +// +// save_json(config.output_directory() + "/" + diagram->name + ".json", j); +// } +} \ No newline at end of file diff --git a/tests/test_cases.cc b/tests/test_cases.cc index 97f6c370..45e2eda4 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -347,6 +347,7 @@ using namespace clanguml::test::matchers; #include "t20031/test_case.h" #include "t20032/test_case.h" #include "t20033/test_case.h" +#include "t20034/test_case.h" /// /// Package diagram tests