From ae55b7c054bb00d4ef455c73288b22806bf7a308 Mon Sep 17 00:00:00 2001 From: Bartek Kryza Date: Sun, 27 Aug 2023 17:48:33 +0200 Subject: [PATCH] Added from_to sequence diagram generator for plantuml --- .../plantuml/sequence_diagram_generator.cc | 155 ++---------------- .../plantuml/sequence_diagram_generator.h | 3 - src/sequence_diagram/model/diagram.cc | 155 ++++++++++++++++++ src/sequence_diagram/model/diagram.h | 12 ++ src/util/util.h | 9 + tests/t20034/t20034.cc | 15 ++ tests/t20034/test_case.h | 21 ++- 7 files changed, 212 insertions(+), 158 deletions(-) diff --git a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc index 82209d67..f60438b4 100644 --- a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc +++ b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc @@ -416,152 +416,19 @@ 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) && - (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}; + auto message_chains_unique = + model().get_all_from_to_message_chains(from_location, to_location); - 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 '{}': {}", - vfrom, k); - from_activity = k; - break; - } + bool first_separator_skipped{false}; + for (const auto &mc : message_chains_unique) { + if (!first_separator_skipped) + first_separator_skipped = true; + else + ostr << "====\n"; + + for (const auto &m : mc) { + generate_call(m, ostr); } - - if (from_activity == 0) { - LOG_WARN("Failed to find 'from' participant {} for start_from " - "condition", - from_location.location); - continue; - } - - 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 'to' participant {} for from_to " - "condition", - to_location.location); - continue; - } - - // Message (call) chains matching the specified from_to condition - std::vector message_chains; - - // 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; } } diff --git a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.h b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.h index 0ef5631f..2519a081 100644 --- a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.h +++ b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.h @@ -42,9 +42,6 @@ template using common_generator = clanguml::common::generators::plantuml::generator; -using message_chain_t = - std::vector; - /** * @brief Sequence diagram PlantUML generator */ diff --git a/src/sequence_diagram/model/diagram.cc b/src/sequence_diagram/model/diagram.cc index 630722dc..c31cbe24 100644 --- a/src/sequence_diagram/model/diagram.cc +++ b/src/sequence_diagram/model/diagram.cc @@ -209,6 +209,161 @@ std::vector diagram::list_start_from_values() const return result; } +std::unordered_set diagram::get_all_from_to_message_chains( + const config::source_location &from_location, + const config::source_location &to_location) const +{ + std::unordered_set message_chains_unique{}; + + if ((from_location.location_type == config::location_t::function) && + (to_location.location_type == config::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] : sequences()) { + const auto &caller = *participants().at(v.from()); + std::string vfrom = caller.full_name(false); + if (vfrom == from_location.location) { + LOG_DBG( + "Found sequence diagram start point '{}': {}", vfrom, k); + from_activity = k; + break; + } + } + + if (from_activity == 0) { + LOG_WARN("Failed to find 'from' participant {} for start_from " + "condition", + from_location.location); + return {}; + } + + for (const auto &[k, v] : sequences()) { + for (const auto &m : v.messages()) { + if (m.type() != common::model::message_t::kCall) + continue; + const auto &callee = *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 'to' participant {} for from_to " + "condition", + to_location.location); + return {}; + } + + // Message (call) chains matching the specified from_to condition + std::vector message_chains; + + // 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] : sequences()) { + for (const auto &m : v.messages()) { + if (m.type() != common::model::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; + + int iter = 0; + 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 found on previous iteration - append + if (!calls_to_current_chain.empty()) { + for (auto &[message_chain_index, messages] : + calls_to_current_chain) { + for (auto i = 0U; i < messages.size(); i++) { + message_chains.push_back( + current_chain[message_chain_index]); + + message_chains.back().push_back(std::move(messages[i])); + } + } + calls_to_current_chain.clear(); + } + + LOG_TRACE("Message chains after iteration {}", iter++); + int message_chain_index{}; + for (const auto &mc : message_chains) { + LOG_TRACE("\t{}: {}", message_chain_index++, + fmt::join(util::map(mc, + [](const model::message &m) -> std::string { + return m.message_name(); + }), + "<-")); + } + + for (auto i = 0U; i < message_chains.size(); i++) { + auto &mc = message_chains[i]; + current_chain[i] = mc; + for (const auto &[k, v] : sequences()) { + for (const auto &m : v.messages()) { + if (m.type() != common::model::message_t::kCall) + continue; + + // Ignore recursive calls and call loops + if (m.to() == m.from() || + std::any_of( + cbegin(mc), cend(mc), [&m](const auto &msg) { + return msg.to() == m.from(); + })) { + continue; + } + + if (m.to() == mc.back().from()) { + calls_to_current_chain[i].push_back(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.count(i) > 0 && + calls_to_current_chain[i].size() >= 1) { + mc.push_back(calls_to_current_chain[i][0]); + calls_to_current_chain[i].erase( + calls_to_current_chain[i].begin()); + } + } + + // 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()); + + std::copy_if(message_chains.begin(), message_chains.end(), + std::inserter(message_chains_unique, message_chains_unique.begin()), + [from_activity](const message_chain_t &mc) { + return !mc.empty() && (mc.front().from() == from_activity); + }); + } + + return message_chains_unique; +} + void diagram::print() const { LOG_TRACE(" --- Participants ---"); diff --git a/src/sequence_diagram/model/diagram.h b/src/sequence_diagram/model/diagram.h index 30cc81b4..a99c656d 100644 --- a/src/sequence_diagram/model/diagram.h +++ b/src/sequence_diagram/model/diagram.h @@ -20,6 +20,7 @@ #include "activity.h" #include "common/model/diagram.h" #include "common/types.h" +#include "config/config.h" #include "participant.h" #include @@ -27,6 +28,8 @@ namespace clanguml::sequence_diagram::model { +using message_chain_t = std::vector; + /** * @brief Model of a sequence diagram * @@ -235,6 +238,15 @@ public: */ std::vector list_start_from_values() const; + /** + * @brief Generate a list of message chains matching a from_to constraint + * + * @return List of message chains + */ + std::unordered_set get_all_from_to_message_chains( + const config::source_location &from, + const config::source_location &to) const; + /** * @brief Once the diagram is complete, run any final processing. * diff --git a/src/util/util.h b/src/util/util.h index f7e2040b..bff44542 100644 --- a/src/util/util.h +++ b/src/util/util.h @@ -351,6 +351,15 @@ void for_each_if(const T &collection, C &&cond, F &&func) }); } +template +std::vector map(const std::vector &in, F &&f) +{ + std::vector out; + std::transform( + in.cbegin(), in.cend(), std::back_inserter(out), std::forward(f)); + return out; +} + template void if_not_null(const T *pointer, F &&func, FElse &&func_else) { diff --git a/tests/t20034/t20034.cc b/tests/t20034/t20034.cc index b11b35a4..369b6072 100644 --- a/tests/t20034/t20034.cc +++ b/tests/t20034/t20034.cc @@ -7,6 +7,8 @@ struct A { void a3() { } }; +struct C; + struct B { void b1() { @@ -15,8 +17,11 @@ struct B { } void b2() { a.a2(); } void b3() { a.a3(); } + void b4(); A a; + + C *c; }; struct C { @@ -30,6 +35,8 @@ struct C { c2(); } + void c4() { b.b4(); } + B b; }; @@ -43,6 +50,8 @@ struct D { c.c3(); a.a2(); + c.c4(); + auto l = [this]() { a.a2(); }; l(); } @@ -51,5 +60,11 @@ struct D { A a; C c; }; + +void B::b4() +{ + c->c4(); + b2(); +} } } \ No newline at end of file diff --git a/tests/t20034/test_case.h b/tests/t20034/test_case.h index eb1b9651..bf7d5e54 100644 --- a/tests/t20034/test_case.h +++ b/tests/t20034/test_case.h @@ -43,21 +43,20 @@ TEST_CASE("t20034", "[test-case][sequence]") 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"), "c4()")); - - REQUIRE_THAT(puml, HasCall(_A("D"), _A("C"), "c2()")); - REQUIRE_THAT(puml, !HasCall(_A("D"), _A("C"), "c1()")); - + REQUIRE_THAT(puml, !HasCall(_A("C"), _A("B"), "b3()")); 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); -// } + // { + // 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