Added option fold_repeated_activities to sequence diagrams (#317)

This commit is contained in:
Bartek Kryza 2024-09-11 20:17:04 +02:00
parent b12f7b2c27
commit ddbc021cd9
No known key found for this signature in database
GPG Key ID: 241B25F44E85B4D7
17 changed files with 185 additions and 18 deletions

View File

@ -15,6 +15,7 @@ mermaid:
generate_links:
link: "{% if existsIn(element, \"doxygen_link\") %}{{ element.doxygen_link }}{% endif %}"
tooltip: "{% if existsIn(element, \"comment\") and existsIn(element.comment, \"brief\") %}{{ abbrv(trim(replace(element.comment.brief.0, \"\\n+\", \" \")), 256) }}{% else %}{{ element.name }}{% endif %}"
fold_repeated_activities: true
diagrams:
# Class diagrams
class_translation_unit_visitor:

View File

@ -9,6 +9,7 @@
* [Customizing participants order](#customizing-participants-order)
* [Generating return types](#generating-return-types)
* [Generating condition statements](#generating-condition-statements)
* [Folding repeated activities](#folding-repeated-activities)
* [Injecting call expressions manually through comments](#injecting-call-expressions-manually-through-comments)
* [Including comments in sequence diagrams](#including-comments-in-sequence-diagrams)
@ -325,6 +326,23 @@ generate_condition_statements: true
An example of a diagram with this feature enabled is presented below:
![extension](test_cases/t20033_sequence.svg)
## Folding repeated activities
If in a given sequence diagram functions or methods are called multiple times
from different branches, each of these activities will be rendered fully
which can mean that the diagram be very large.
In order to minimize the diagram size in such situations, an option can be set:
```yaml
fold_repeated_activities: true
```
which will render any activity only once, and any further calls to that activity
will only render a call to the activity and an indicator - a single `*` character
- in a note over the activity.
For an example of this see the test case [t20056](test_cases/t20056.md).
## Injecting call expressions manually through comments
In some cases, `clang-uml` is not yet able to discover a call expression target
in some line of code. This can include passing function or method address to

View File

@ -569,6 +569,7 @@ cli_flow_t cli_handler::add_config_diagram(
true;
doc["diagrams"][name]["inline_lambda_messages"] = false;
doc["diagrams"][name]["generate_message_comments"] = false;
doc["diagrams"][name]["fold_repeated_activities"] = false;
doc["diagrams"][name]["generate_condition_statements"] = false;
doc["diagrams"][name]["using_namespace"] =
std::vector<std::string>{{"myproject"}};

View File

@ -248,6 +248,7 @@ void inheritable_diagram_options::inherit(
puml.override(parent.puml);
mermaid.override(parent.mermaid);
generate_method_arguments.override(parent.generate_method_arguments);
fold_repeated_activities.override(parent.fold_repeated_activities);
generate_concept_requirements.override(
parent.generate_concept_requirements);
generate_packages.override(parent.generate_packages);

View File

@ -606,6 +606,7 @@ struct inheritable_diagram_options {
"generate_condition_statements", false};
option<std::vector<std::string>> participants_order{"participants_order"};
option<bool> generate_message_comments{"generate_message_comments", false};
option<bool> fold_repeated_activities{"fold_repeated_activities", false};
option<unsigned> message_comment_width{
"message_comment_width", clanguml::util::kDefaultMessageCommentWidth};
option<bool> debug_mode{"debug_mode", false};

View File

@ -249,6 +249,7 @@ types:
generate_return_types: !optional bool
generate_condition_statements: !optional bool
generate_message_comments: !optional bool
fold_repeated_activities: !optional bool
message_comment_width: !optional int
participants_order: !optional [string]
start_from: !optional [source_location_t] # deprecated -> 'from'
@ -377,6 +378,7 @@ root:
generate_return_types: !optional bool
generate_condition_statements: !optional bool
generate_message_comments: !optional bool
fold_repeated_activities: !optional bool
message_comment_width: !optional int
generate_packages: !optional bool
group_methods: !optional bool

View File

@ -730,6 +730,7 @@ template <> struct convert<sequence_diagram> {
get_option(node, rhs.participants_order);
get_option(node, rhs.generate_method_arguments);
get_option(node, rhs.generate_message_comments);
get_option(node, rhs.fold_repeated_activities);
get_option(node, rhs.message_comment_width);
get_option(node, rhs.type_aliases);
@ -909,6 +910,7 @@ template <> struct convert<config> {
get_option(node, rhs.generate_return_types);
get_option(node, rhs.generate_condition_statements);
get_option(node, rhs.generate_message_comments);
get_option(node, rhs.fold_repeated_activities);
get_option(node, rhs.message_comment_width);
get_option(node, rhs.type_aliases);

View File

@ -353,6 +353,7 @@ YAML::Emitter &operator<<(
out << c.generate_return_types;
out << c.participants_order;
out << c.generate_message_comments;
out << c.fold_repeated_activities;
out << c.message_comment_width;
}
else if (const auto *pd = dynamic_cast<const package_diagram *>(&c);

View File

@ -22,7 +22,6 @@ namespace clanguml::sequence_diagram::generators::mermaid {
using clanguml::common::model::message_t;
using clanguml::config::location_t;
using clanguml::sequence_diagram::model::activity;
using clanguml::sequence_diagram::model::message;
using namespace clanguml::util;
@ -195,8 +194,25 @@ void generator::generate_return(const message &m, std::ostream &ostr) const
}
void generator::generate_activity(
const activity &a, std::ostream &ostr, std::vector<eid_t> &visited) const
eid_t activity_id, std::ostream &ostr, std::vector<eid_t> &visited) const
{
const auto &a = model().get_activity(activity_id);
const auto [it, inserted] = generated_activities_.emplace(activity_id);
if (config().fold_repeated_activities() && !inserted &&
!a.messages().empty()) {
const auto &p =
model().get_participant<model::participant>(activity_id);
if (p.has_value()) {
ostr << indent(1) << "Note over " << generate_alias(p.value())
<< " : *\n";
}
return;
}
for (const auto &m : a.messages()) {
if (m.in_static_declaration_context()) {
if (util::contains(already_generated_in_static_context_, m))
@ -225,8 +241,7 @@ void generator::generate_activity(
.end()) { // break infinite recursion on recursive calls
LOG_DBG("Creating activity {} --> {} - missing sequence {}",
m.from(), m.to(), m.to());
generate_activity(
model().get_activity(m.to()), ostr, visited);
generate_activity(m.to(), ostr, visited);
}
}
else
@ -642,8 +657,7 @@ void generator::generate_diagram(std::ostream &ostr) const
ostr << indent(1) << "activate " << from_alias << '\n';
generate_activity(
model().get_activity(start_from), ostr, visited_participants);
generate_activity(start_from, ostr, visited_participants);
if (from.value().type_name() == "method" ||
config().combine_free_functions_into_file_participants()) {

View File

@ -113,13 +113,13 @@ public:
/**
* @brief Generate sequence diagram activity.
*
* @param a Activity model
* @param activity_id Activity id
* @param ostr Output stream
* @param visited List of already visited participants, this is necessary
* for breaking infinite recursion on recursive calls
*/
void generate_activity(const clanguml::sequence_diagram::model::activity &a,
std::ostream &ostr, std::vector<eid_t> &visited) const;
void generate_activity(eid_t activity_id, std::ostream &ostr,
std::vector<eid_t> &visited) const;
private:
/**
@ -158,6 +158,7 @@ private:
mutable std::set<eid_t> generated_participants_;
mutable std::set<unsigned int> generated_comment_ids_;
mutable std::vector<model::message> already_generated_in_static_context_;
mutable std::set<eid_t> generated_activities_;
};
} // namespace mermaid

View File

@ -23,7 +23,6 @@ namespace clanguml::sequence_diagram::generators::plantuml {
using clanguml::common::eid_t;
using clanguml::common::model::message_t;
using clanguml::config::location_t;
using clanguml::sequence_diagram::model::activity;
using clanguml::sequence_diagram::model::message;
using namespace clanguml::util;
@ -152,8 +151,27 @@ void generator::generate_return(const message &m, std::ostream &ostr) const
}
void generator::generate_activity(
const activity &a, std::ostream &ostr, std::vector<eid_t> &visited) const
eid_t activity_id, std::ostream &ostr, std::vector<eid_t> &visited) const
{
const auto &a = model().get_activity(activity_id);
const auto [it, inserted] = generated_activities_.emplace(activity_id);
if (config().fold_repeated_activities() && !inserted &&
!a.messages().empty()) {
const auto &p =
model().get_participant<model::participant>(activity_id);
if (p.has_value()) {
ostr << "hnote over " << generate_alias(p.value()) << " : *\n";
// This is necessary to keep the hnote over the activity life line
ostr << generate_alias(p.value()) << "-[hidden]->"
<< generate_alias(p.value()) << '\n';
}
return;
}
for (const auto &m : a.messages()) {
if (m.in_static_declaration_context()) {
if (util::contains(already_generated_in_static_context_, m))
@ -182,8 +200,7 @@ void generator::generate_activity(
.end()) { // break infinite recursion on recursive calls
LOG_DBG("Creating activity {} --> {} - missing sequence {}",
m.from(), m.to(), m.to());
generate_activity(
model().get_activity(m.to()), ostr, visited);
generate_activity(m.to(), ostr, visited);
}
}
else
@ -651,8 +668,7 @@ void generator::generate_diagram(std::ostream &ostr) const
ostr << "activate " << from_alias << '\n';
generate_activity(
model().get_activity(start_from), ostr, visited_participants);
generate_activity(start_from, ostr, visited_participants);
if (from.value().type_name() == "method" ||
config().combine_free_functions_into_file_participants()) {

View File

@ -107,13 +107,13 @@ public:
/**
* @brief Generate sequence diagram activity.
*
* @param a Activity model
* @param activity_id Activity id
* @param ostr Output stream
* @param visited List of already visited participants, this is necessary
* for breaking infinite recursion on recursive calls
*/
void generate_activity(const clanguml::sequence_diagram::model::activity &a,
std::ostream &ostr, std::vector<eid_t> &visited) const;
void generate_activity(eid_t activity_id, std::ostream &ostr,
std::vector<eid_t> &visited) const;
private:
/**
@ -160,6 +160,7 @@ private:
mutable std::set<eid_t> generated_participants_;
mutable std::set<unsigned int> generated_comment_ids_;
mutable std::vector<model::message> already_generated_in_static_context_;
mutable std::set<eid_t> generated_activities_;
};
} // namespace plantuml

12
tests/t20056/.clang-uml Normal file
View File

@ -0,0 +1,12 @@
diagrams:
t20056_sequence:
type: sequence
glob:
- t20056.cc
include:
namespaces:
- clanguml::t20056
using_namespace: clanguml::t20056
fold_repeated_activities: true
from:
- function: "clanguml::t20056::tmain()"

46
tests/t20056/t20056.cc Normal file
View File

@ -0,0 +1,46 @@
namespace clanguml {
namespace t20056 {
struct A {
void a() { aa(); }
void aa() { aaa(); }
void aaa() { }
};
struct B {
void b() { bb(); }
void bb() { bbb(); }
void bbb() { a.a(); }
A a;
};
struct C {
void c() { cc(); }
void cc() { ccc(); }
void ccc() { b.b(); }
B b;
};
void tmain()
{
A a;
B b;
C c;
c.c();
c.c();
c.c();
b.b();
a.a();
}
}
}

47
tests/t20056/test_case.h Normal file
View File

@ -0,0 +1,47 @@
/**
* tests/t20056/test_case.h
*
* Copyright (c) 2021-2024 Bartek Kryza <bkryza@gmail.com>
*
* 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("t20056")
{
using namespace clanguml::test;
using namespace std::string_literals;
auto [config, db, diagram, model] =
CHECK_SEQUENCE_MODEL("t20056", "t20056_sequence");
CHECK_SEQUENCE_DIAGRAM(*config, diagram, *model, [](const auto &src) {
REQUIRE(MessageOrder(src,
{
// clang-format off
{"tmain()", "C", "c()"},
{"C", "C", "cc()"},
{"C", "C", "ccc()"},
{"C", "B", "b()"},
{"B", "B", "bb()"},
{"B", "B", "bbb()"},
{"B", "A", "a()"},
{"A", "A", "aa()"},
{"A", "A", "aaa()"},
{"tmain()", "C", "c()"},
{"tmain()", "C", "c()"},
{"tmain()", "B", "b()"},
{"tmain()", "A", "a()"},
// clang-format on
}));
});
}

View File

@ -634,6 +634,7 @@ void CHECK_INCLUDE_DIAGRAM(const clanguml::config::config &config,
#include "t20053/test_case.h"
#include "t20054/test_case.h"
#include "t20055/test_case.h"
#include "t20056/test_case.h"
///
/// Package diagram tests

View File

@ -412,6 +412,8 @@ test_cases:
- name: t20055
title: Test case for advanced filter in sequence diagram
description:
- name: t20056
title: Test case for option to fold repeated activities in sequence diagram
Package diagrams:
- name: t30001
title: Basic package diagram test case