diff --git a/docs/sequence_diagrams.md b/docs/sequence_diagrams.md index 31b484d0..7cc2872b 100644 --- a/docs/sequence_diagrams.md +++ b/docs/sequence_diagrams.md @@ -8,6 +8,7 @@ * [Lambda expressions in sequence diagrams](#lambda-expressions-in-sequence-diagrams) * [Customizing participants order](#customizing-participants-order) * [Generating return types](#generating-return-types) +* [Generating condition statements](#generating-condition-statements) @@ -263,3 +264,15 @@ generate_return_types: true This option only affects the `plantuml` generation, in `json` generator `return_type` property is always present in the message nodes. + +## Generating condition statements +Sometimes, it is useful to include actual condition statements (for instance +contents of the `if()` condition in the `alt` or `loop` blocks in the sequence +diagrams, to make them more readable. + +This can be enabled using the following option: + +```yaml +generate_condition_statements: true +``` + diff --git a/src/common/clang_utils.cc b/src/common/clang_utils.cc index 363ee80c..884dc3b1 100644 --- a/src/common/clang_utils.cc +++ b/src/common/clang_utils.cc @@ -564,6 +564,75 @@ bool is_type_token(const std::string &t) (is_identifier(t) && !is_qualifier(t) && !is_bracket(t)); } +std::string format_condition_text(const std::string &condition_text) +{ + std::string result{condition_text}; + + if (result.size() < 2) + return {}; + + std::vector text_lines = util::split(result, "\n", true); + + // Trim each line + for (auto &line : text_lines) { + line = util::trim(line); + } + + result = util::join(" ", text_lines); + + if (result.at(0) == '(' && result.back() == ')') + return result.substr(1, result.size() - 2); + + return result; +} + +std::string get_condition_text(clang::SourceManager &sm, clang::IfStmt *stmt) +{ + auto condition_range = + clang::SourceRange(stmt->getLParenLoc(), stmt->getRParenLoc()); + + return format_condition_text(get_source_text(condition_range, sm)); +} + +std::string get_condition_text(clang::SourceManager &sm, clang::WhileStmt *stmt) +{ + auto condition_range = + clang::SourceRange(stmt->getLParenLoc(), stmt->getRParenLoc()); + + return format_condition_text(get_source_text(condition_range, sm)); +} + +std::string get_condition_text( + clang::SourceManager &sm, clang::CXXForRangeStmt *stmt) +{ + auto condition_range = stmt->getRangeStmt()->getSourceRange(); + + return format_condition_text(get_source_text(condition_range, sm)); +} + +std::string get_condition_text(clang::SourceManager &sm, clang::ForStmt *stmt) +{ + auto condition_range = + clang::SourceRange(stmt->getLParenLoc(), stmt->getRParenLoc()); + + return format_condition_text(get_source_text(condition_range, sm)); +} + +std::string get_condition_text(clang::SourceManager &sm, clang::DoStmt *stmt) +{ + auto condition_range = stmt->getCond()->getSourceRange(); + + return format_condition_text(get_source_text(condition_range, sm)); +} + +std::string get_condition_text( + clang::SourceManager &sm, clang::ConditionalOperator *stmt) +{ + auto condition_range = stmt->getCond()->getSourceRange(); + + return format_condition_text(get_source_text(condition_range, sm)); +} + clang::QualType dereference(clang::QualType type) { auto res = type; diff --git a/src/common/clang_utils.h b/src/common/clang_utils.h index f87ded59..bb84460f 100644 --- a/src/common/clang_utils.h +++ b/src/common/clang_utils.h @@ -23,6 +23,7 @@ #include "types.h" #include "util/util.h" +#include #include #include @@ -248,6 +249,23 @@ bool is_qualified_identifier(const std::string &t); bool is_type_token(const std::string &t); +std::string format_condition_text(const std::string &condition_text); + +std::string get_condition_text(clang::SourceManager &sm, clang::IfStmt *stmt); + +std::string get_condition_text( + clang::SourceManager &sm, clang::WhileStmt *stmt); + +std::string get_condition_text( + clang::SourceManager &sm, clang::CXXForRangeStmt *stmt); + +std::string get_condition_text(clang::SourceManager &sm, clang::ForStmt *stmt); + +std::string get_condition_text(clang::SourceManager &sm, clang::DoStmt *stmt); + +std::string get_condition_text( + clang::SourceManager &sm, clang::ConditionalOperator *stmt); + clang::QualType dereference(clang::QualType type); /** diff --git a/src/config/config.cc b/src/config/config.cc index 2dab8e2b..e50d5170 100644 --- a/src/config/config.cc +++ b/src/config/config.cc @@ -168,6 +168,8 @@ void inheritable_diagram_options::inherit( combine_free_functions_into_file_participants.override( parent.combine_free_functions_into_file_participants); generate_return_types.override(parent.generate_return_types); + generate_condition_statements.override( + parent.generate_condition_statements); debug_mode.override(parent.debug_mode); generate_metadata.override(parent.generate_metadata); } diff --git a/src/config/config.h b/src/config/config.h index 526f02bc..735ca1dd 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -446,6 +446,8 @@ struct inheritable_diagram_options { option combine_free_functions_into_file_participants{ "combine_free_functions_into_file_participants", false}; option generate_return_types{"generate_return_types", false}; + option generate_condition_statements{ + "generate_condition_statements", false}; option> participants_order{"participants_order"}; option debug_mode{"debug_mode", false}; option generate_metadata{"generate_metadata", true}; diff --git a/src/config/yaml_decoders.cc b/src/config/yaml_decoders.cc index 25068b0f..4f596244 100644 --- a/src/config/yaml_decoders.cc +++ b/src/config/yaml_decoders.cc @@ -568,6 +568,7 @@ template <> struct convert { get_option(node, rhs.start_from); get_option(node, rhs.combine_free_functions_into_file_participants); get_option(node, rhs.generate_return_types); + get_option(node, rhs.generate_condition_statements); get_option(node, rhs.relative_to); get_option(node, rhs.participants_order); get_option(node, rhs.generate_method_arguments); @@ -756,6 +757,7 @@ template <> struct convert { get_option(node, rhs.generate_metadata); get_option(node, rhs.combine_free_functions_into_file_participants); get_option(node, rhs.generate_return_types); + get_option(node, rhs.generate_condition_statements); rhs.base_directory.set(node["__parent_path"].as()); get_option(node, rhs.relative_to); diff --git a/src/config/yaml_emitters.cc b/src/config/yaml_emitters.cc index 83f0d20f..46355f4b 100644 --- a/src/config/yaml_emitters.cc +++ b/src/config/yaml_emitters.cc @@ -290,6 +290,7 @@ YAML::Emitter &operator<<( out << c.comment_parser; out << c.combine_free_functions_into_file_participants; out << c.generate_return_types; + out << c.generate_condition_statements; out << c.participants_order; out << c.debug_mode; diff --git a/src/sequence_diagram/generators/json/sequence_diagram_generator.cc b/src/sequence_diagram/generators/json/sequence_diagram_generator.cc index 7f887a39..e0930781 100644 --- a/src/sequence_diagram/generators/json/sequence_diagram_generator.cc +++ b/src/sequence_diagram/generators/json/sequence_diagram_generator.cc @@ -231,7 +231,7 @@ void generator::generate_activity(const activity &a, process_conditional_message(m); break; case message_t::kConditionalElse: - process_conditional_else_message(); + process_conditional_else_message(m); break; case message_t::kConditionalEnd: process_end_conditional_message(); @@ -279,6 +279,8 @@ void generator::process_while_message(const message &m) const while_block["type"] = "loop"; while_block["name"] = "while"; while_block["activity_id"] = std::to_string(m.from()); + if (auto text = m.condition_text(); text.has_value()) + while_block["condition_text"] = *text; current_block_statement()["messages"].push_back(std::move(while_block)); @@ -298,6 +300,8 @@ void generator::process_for_message(const message &m) const for_block["type"] = "loop"; for_block["name"] = "for"; for_block["activity_id"] = std::to_string(m.from()); + if (auto text = m.condition_text(); text.has_value()) + for_block["condition_text"] = *text; current_block_statement()["messages"].push_back(std::move(for_block)); @@ -317,6 +321,8 @@ void generator::process_do_message(const message &m) const do_block["type"] = "loop"; do_block["name"] = "do"; do_block["activity_id"] = std::to_string(m.from()); + if (auto text = m.condition_text(); text.has_value()) + do_block["condition_text"] = *text; current_block_statement()["messages"].push_back(std::move(do_block)); @@ -413,6 +419,8 @@ void generator::process_conditional_message(const message &m) const if_block["type"] = "alt"; if_block["name"] = "conditional"; if_block["activity_id"] = std::to_string(m.from()); + if (auto text = m.condition_text(); text.has_value()) + if_block["condition_text"] = *text; current_block_statement()["messages"].push_back(std::move(if_block)); @@ -427,13 +435,15 @@ void generator::process_conditional_message(const message &m) const std::ref(current_block_statement()["branches"].back())); } -void generator::process_conditional_else_message() const +void generator::process_conditional_else_message(const message &m) const { // remove previous branch from the stack block_statements_stack_.pop_back(); nlohmann::json branch; branch["type"] = "alternative"; + if (auto text = m.condition_text(); text.has_value()) + branch["condition_text"] = *text; current_block_statement()["branches"].push_back(std::move(branch)); block_statements_stack_.push_back( @@ -477,6 +487,8 @@ void generator::process_if_message(const message &m) const if_block["type"] = "alt"; if_block["name"] = "if"; if_block["activity_id"] = std::to_string(m.from()); + if (auto text = m.condition_text(); text.has_value()) + if_block["condition_text"] = *text; current_block_statement()["messages"].push_back(std::move(if_block)); diff --git a/src/sequence_diagram/generators/json/sequence_diagram_generator.h b/src/sequence_diagram/generators/json/sequence_diagram_generator.h index dfbdbe3f..c0cd4ba8 100644 --- a/src/sequence_diagram/generators/json/sequence_diagram_generator.h +++ b/src/sequence_diagram/generators/json/sequence_diagram_generator.h @@ -158,8 +158,10 @@ private: /** * @brief Process conditional else statement message + * + * @param m Message model */ - void process_conditional_else_message() const; + void process_conditional_else_message(const model::message &m) const; /** * @brief Process `switch` statement message diff --git a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc index 1fb1d253..37562f94 100644 --- a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc +++ b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc @@ -178,11 +178,17 @@ void generator::generate_activity(const activity &a, std::ostream &ostr, } else if (m.type() == message_t::kIf) { print_debug(m, ostr); - ostr << "alt\n"; + ostr << "alt"; + if (m.condition_text()) + ostr << " " << m.condition_text().value(); + ostr << '\n'; } else if (m.type() == message_t::kElseIf) { print_debug(m, ostr); - ostr << "else\n"; + ostr << "else"; + if (m.condition_text()) + ostr << " " << m.condition_text().value(); + ostr << '\n'; } else if (m.type() == message_t::kElse) { print_debug(m, ostr); @@ -193,21 +199,30 @@ void generator::generate_activity(const activity &a, std::ostream &ostr, } else if (m.type() == message_t::kWhile) { print_debug(m, ostr); - ostr << "loop\n"; + ostr << "loop"; + if (m.condition_text()) + ostr << " " << m.condition_text().value(); + ostr << '\n'; } else if (m.type() == message_t::kWhileEnd) { ostr << "end\n"; } else if (m.type() == message_t::kFor) { print_debug(m, ostr); - ostr << "loop\n"; + ostr << "loop"; + if (m.condition_text()) + ostr << " " << m.condition_text().value(); + ostr << '\n'; } else if (m.type() == message_t::kForEnd) { ostr << "end\n"; } else if (m.type() == message_t::kDo) { print_debug(m, ostr); - ostr << "loop\n"; + ostr << "loop"; + if (m.condition_text()) + ostr << " " << m.condition_text().value(); + ostr << '\n'; } else if (m.type() == message_t::kDoEnd) { ostr << "end\n"; @@ -237,7 +252,10 @@ void generator::generate_activity(const activity &a, std::ostream &ostr, } else if (m.type() == message_t::kConditional) { print_debug(m, ostr); - ostr << "alt\n"; + ostr << "alt"; + if (m.condition_text()) + ostr << " " << m.condition_text().value(); + ostr << '\n'; } else if (m.type() == message_t::kConditionalElse) { print_debug(m, ostr); diff --git a/src/sequence_diagram/model/message.cc b/src/sequence_diagram/model/message.cc index ec49598f..71837849 100644 --- a/src/sequence_diagram/model/message.cc +++ b/src/sequence_diagram/model/message.cc @@ -57,4 +57,17 @@ void message::set_message_scope(common::model::message_scope_t scope) common::model::message_scope_t message::message_scope() const { return scope_; } +void message::condition_text(const std::string &condition_text) +{ + if (condition_text.empty()) + condition_text_ = std::nullopt; + else + condition_text_ = condition_text; +} + +std::optional message::condition_text() const +{ + return condition_text_; +} + } // namespace clanguml::sequence_diagram::model diff --git a/src/sequence_diagram/model/message.h b/src/sequence_diagram/model/message.h index 2de4cbc9..78fd680f 100644 --- a/src/sequence_diagram/model/message.h +++ b/src/sequence_diagram/model/message.h @@ -128,6 +128,20 @@ public: */ common::model::message_scope_t message_scope() const; + /** + * @brief Set condition text for block statements (e.g. if()) + * + * @param condition_text Condition text + */ + void condition_text(const std::string &condition_text); + + /** + * @brief Get condition text + * + * @return Block statement condition text + */ + std::optional condition_text() const; + private: common::model::message_t type_{common::model::message_t::kNone}; @@ -143,6 +157,8 @@ private: std::string message_name_{}; std::string return_type_{}; + + std::optional condition_text_; }; } // namespace clanguml::sequence_diagram::model diff --git a/src/sequence_diagram/visitor/call_expression_context.cc b/src/sequence_diagram/visitor/call_expression_context.cc index 094ca5a0..025b2d14 100644 --- a/src/sequence_diagram/visitor/call_expression_context.cc +++ b/src/sequence_diagram/visitor/call_expression_context.cc @@ -307,15 +307,25 @@ bool call_expression_context::is_expr_in_current_control_statement_condition( const clang::Stmt *stmt) const { if (current_ifstmt() != nullptr) { - if (common::is_subexpr_of(current_ifstmt()->getCond(), stmt)) { + if (common::is_subexpr_of(current_ifstmt()->getCond(), stmt)) return true; + + if (const auto *condition_decl_stmt = current_ifstmt()->getInit(); + condition_decl_stmt != nullptr) { + if (common::is_subexpr_of(condition_decl_stmt, stmt)) + return true; } } if (current_elseifstmt() != nullptr) { - if (common::is_subexpr_of(current_elseifstmt()->getCond(), stmt)) { + if (common::is_subexpr_of(current_elseifstmt()->getCond(), stmt)) + return true; + } + + if (current_conditionaloperator() != nullptr) { + if (common::is_subexpr_of( + current_conditionaloperator()->getCond(), stmt)) return true; - } } if (const auto *loop_stmt = current_loopstmt(); loop_stmt != nullptr) { diff --git a/src/sequence_diagram/visitor/translation_unit_visitor.cc b/src/sequence_diagram/visitor/translation_unit_visitor.cc index 273b8fa8..ac22dc5b 100644 --- a/src/sequence_diagram/visitor/translation_unit_visitor.cc +++ b/src/sequence_diagram/visitor/translation_unit_visitor.cc @@ -584,6 +584,10 @@ bool translation_unit_visitor::TraverseIfStmt(clang::IfStmt *stmt) const auto current_caller_id = context().caller_id(); const auto *current_ifstmt = context().current_ifstmt(); + std::string condition_text; + if (config().generate_condition_statements()) + condition_text = common::get_condition_text(source_manager(), stmt); + // Check if this is a beginning of a new if statement, or an // else if condition of the current if statement if (current_ifstmt != nullptr) { @@ -601,6 +605,7 @@ bool translation_unit_visitor::TraverseIfStmt(clang::IfStmt *stmt) message m{message_t::kElseIf, current_caller_id}; set_source_location(*stmt, m); + m.condition_text(condition_text); diagram().add_block_message(std::move(m)); } else { @@ -608,6 +613,7 @@ bool translation_unit_visitor::TraverseIfStmt(clang::IfStmt *stmt) message m{message_t::kIf, current_caller_id}; set_source_location(*stmt, m); + m.condition_text(condition_text); diagram().add_block_message(std::move(m)); } } @@ -631,10 +637,15 @@ bool translation_unit_visitor::TraverseWhileStmt(clang::WhileStmt *stmt) const auto current_caller_id = context().caller_id(); + std::string condition_text; + if (config().generate_condition_statements()) + condition_text = common::get_condition_text(source_manager(), stmt); + if (current_caller_id != 0) { context().enter_loopstmt(stmt); message m{message_t::kWhile, current_caller_id}; set_source_location(*stmt, m); + m.condition_text(condition_text); diagram().add_block_message(std::move(m)); } RecursiveASTVisitor::TraverseWhileStmt(stmt); @@ -656,10 +667,15 @@ bool translation_unit_visitor::TraverseDoStmt(clang::DoStmt *stmt) const auto current_caller_id = context().caller_id(); + std::string condition_text; + if (config().generate_condition_statements()) + condition_text = common::get_condition_text(source_manager(), stmt); + if (current_caller_id != 0) { context().enter_loopstmt(stmt); message m{message_t::kDo, current_caller_id}; set_source_location(*stmt, m); + m.condition_text(condition_text); diagram().add_block_message(std::move(m)); } @@ -682,10 +698,15 @@ bool translation_unit_visitor::TraverseForStmt(clang::ForStmt *stmt) const auto current_caller_id = context().caller_id(); + std::string condition_text; + if (config().generate_condition_statements()) + condition_text = common::get_condition_text(source_manager(), stmt); + if (current_caller_id != 0) { context().enter_loopstmt(stmt); message m{message_t::kFor, current_caller_id}; set_source_location(*stmt, m); + m.condition_text(condition_text); diagram().add_block_message(std::move(m)); } @@ -761,10 +782,15 @@ bool translation_unit_visitor::TraverseCXXForRangeStmt( const auto current_caller_id = context().caller_id(); + std::string condition_text; + if (config().generate_condition_statements()) + condition_text = common::get_condition_text(source_manager(), stmt); + if (current_caller_id != 0) { context().enter_loopstmt(stmt); message m{message_t::kFor, current_caller_id}; set_source_location(*stmt, m); + m.condition_text(condition_text); diagram().add_block_message(std::move(m)); } @@ -847,10 +873,15 @@ bool translation_unit_visitor::TraverseConditionalOperator( const auto current_caller_id = context().caller_id(); + std::string condition_text; + if (config().generate_condition_statements()) + condition_text = common::get_condition_text(source_manager(), stmt); + if (current_caller_id != 0) { context().enter_conditionaloperator(stmt); model::message m{message_t::kConditional, current_caller_id}; set_source_location(*stmt, m); + m.condition_text(condition_text); diagram().add_block_message(std::move(m)); } diff --git a/tests/t20028/test_case.h b/tests/t20028/test_case.h index f039da7e..89e49668 100644 --- a/tests/t20028/test_case.h +++ b/tests/t20028/test_case.h @@ -35,7 +35,8 @@ TEST_CASE("t20028", "[test-case][sequence]") REQUIRE_THAT(puml, EndsWith("@enduml\n")); // Check if all calls exist - REQUIRE_THAT(puml, HasCall(_A("tmain()"), _A("A"), "a()")); + REQUIRE_THAT( + puml, HasCallInControlCondition(_A("tmain()"), _A("A"), "a()")); REQUIRE_THAT(puml, HasCall(_A("tmain()"), _A("A"), "b()")); REQUIRE_THAT(puml, HasCall(_A("tmain()"), _A("A"), "c()")); REQUIRE_THAT(puml, HasCall(_A("tmain()"), _A("A"), "d()")); diff --git a/tests/t20033/.clang-uml b/tests/t20033/.clang-uml new file mode 100644 index 00000000..390e2d3a --- /dev/null +++ b/tests/t20033/.clang-uml @@ -0,0 +1,15 @@ +compilation_database_dir: .. +output_directory: puml +diagrams: + t20033_sequence: + type: sequence + glob: + - ../../tests/t20033/t20033.cc + include: + namespaces: + - clanguml::t20033 + using_namespace: + - clanguml::t20033 + generate_condition_statements: true + start_from: + - function: "clanguml::t20033::tmain()" \ No newline at end of file diff --git a/tests/t20033/t20033.cc b/tests/t20033/t20033.cc new file mode 100644 index 00000000..499b421e --- /dev/null +++ b/tests/t20033/t20033.cc @@ -0,0 +1,65 @@ +#include +#include +#include + +namespace clanguml { +namespace t20033 { +struct A { + int a1() { return 0; } + int a2() { return 1; } + int a3() { return 2; } + int a4() { return 3; } +}; + +int tmain() +{ + A a; + + int result{}; + // clang-format off + if(false) { + result = 0; + } + else if (reinterpret_cast(&a) % 100 == 0ULL) { + result = a.a1(); + } + else if (reinterpret_cast(&a) % 64 == 0ULL) { + result = a.a2(); + } + else if(a.a2() == 2 && + a.a3() == 3) { + result = a.a3(); + } + else { + result = a.a4(); + } + // clang-format on + + if (int i = a.a2(); i != 2) { + result += a.a3(); + } + + for (int i = 0; i < a.a2(); i++) { + result += i * a.a3(); + } + + int retry_count = a.a3(); + while (retry_count--) { + result -= a.a2(); + } + + do { + result += a.a4(); + } while (retry_count++ < a.a3()); + + result = a.a4() % 6 ? result * 2 : result; + + std::vector ints; + for (auto i : ints) { + result += a.a4(); + } + + return result; +} +} +} \ No newline at end of file diff --git a/tests/t20033/test_case.h b/tests/t20033/test_case.h new file mode 100644 index 00000000..36929b23 --- /dev/null +++ b/tests/t20033/test_case.h @@ -0,0 +1,58 @@ +/** + * tests/t20033/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("t20033", "[test-case][sequence]") +{ + auto [config, db] = load_config("t20033"); + + auto diagram = config.diagrams["t20033_sequence"]; + + REQUIRE(diagram->name == "t20033_sequence"); + + auto model = generate_sequence_diagram(*db, diagram); + + REQUIRE(model->name() == "t20033_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("tmain()"), _A("A"), "a1()")); + REQUIRE_THAT( + puml, HasCallInControlCondition(_A("tmain()"), _A("A"), "a2()")); + REQUIRE_THAT( + puml, HasCallInControlCondition(_A("tmain()"), _A("A"), "a3()")); + REQUIRE_THAT( + puml, HasCallInControlCondition(_A("tmain()"), _A("A"), "a4()")); + + 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 7171278e..97f6c370 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -346,6 +346,7 @@ using namespace clanguml::test::matchers; #include "t20030/test_case.h" #include "t20031/test_case.h" #include "t20032/test_case.h" +#include "t20033/test_case.h" /// /// Package diagram tests diff --git a/tests/test_cases.yaml b/tests/test_cases.yaml index d98bf626..82cbb91f 100644 --- a/tests/test_cases.yaml +++ b/tests/test_cases.yaml @@ -295,6 +295,9 @@ test_cases: - name: t20032 title: Return type generation option sequence diagram test case description: + - name: t20033 + title: Control statement text in sequence diagram test case + description: Package diagrams: - name: t30001 title: Basic package diagram test case