diff --git a/.clang-uml b/.clang-uml index f515256c..5923c9df 100644 --- a/.clang-uml +++ b/.clang-uml @@ -1,8 +1,8 @@ compilation_database_dir: debug output_directory: docs/diagrams generate_links: - link: 'https://github.com/bkryza/clang-uml/blob/{{ git.commit }}/{{ element.source.path }}#L{{ element.source.line }}' - tooltip: '{% if existsIn(element, "comment") and existsIn(element.comment, "brief") %}{{ abbrv(trim(replace(element.comment.brief.0, "\n+", " ")), 256) }}{% else %}{{ element.name }}{% endif %}' + link: https://github.com/bkryza/clang-uml/blob/{{ git.commit }}/{{ element.source.path }}#L{{ element.source.line }} + tooltip: "{% if existsIn(element, \"comment\") and existsIn(element.comment, \"brief\") %}{{ abbrv(trim(replace(element.comment.brief.0, \"\\n+\", \" \")), 256) }}{% else %}{{ element.name }}{% endif %}" diagrams: main_package: include!: uml/main_package_diagram.yml @@ -26,3 +26,15 @@ diagrams: include!: uml/package_model_class_diagram.yml include_graph: include!: uml/include_diagram.yml + include3_diagram_parents_hierarchy: + type: class + include: + parents: [clanguml::config::include_diagram] + namespaces: [clanguml] + relationships: + - inheritance + exclude: + access: [public, protected, private] + plantuml: + before: + - left to right direction \ No newline at end of file diff --git a/src/cli/cli_handler.cc b/src/cli/cli_handler.cc new file mode 100644 index 00000000..18820674 --- /dev/null +++ b/src/cli/cli_handler.cc @@ -0,0 +1,561 @@ +/** + * src/options/cli_options.cc + * + * 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. + */ +#include "cli_handler.h" + +#include "class_diagram/generators/plantuml/class_diagram_generator.h" +#include "include_diagram/generators/plantuml/include_diagram_generator.h" +#include "package_diagram/generators/plantuml/package_diagram_generator.h" +#include "sequence_diagram/generators/plantuml/sequence_diagram_generator.h" +#include "util/util.h" +#include "version.h" + +#include +#include + +namespace clanguml::cli { +cli_handler::cli_handler( + std::ostream &ostr, std::shared_ptr logger) + : ostr_{ostr} + , logger_{std::move(logger)} +{ +} + +void cli_handler::setup_logging() +{ + spdlog::drop("clanguml-logger"); + spdlog::register_logger(logger_); + + logger_->set_pattern("[%^%l%^] [tid %t] %v"); + + if (verbose == 0) { + logger_->set_level(spdlog::level::err); + } + else if (verbose == 1) { + logger_->set_level(spdlog::level::info); + } + else if (verbose == 2) { + logger_->set_level(spdlog::level::debug); + } + else { + logger_->set_level(spdlog::level::trace); + } +} + +cli_flow_t cli_handler::parse(int argc, const char *argv[]) +{ + app.add_option("-c,--config", config_path, + "Location of configuration file, when '-' read from stdin"); + app.add_option("-d,--compile-database", compilation_database_dir, + "Location of compilation database directory"); + app.add_option("-n,--diagram-name", diagram_names, + "List of diagram names to generate"); + app.add_option("-o,--output-directory", output_directory, + "Override output directory specified in config file"); + app.add_option("-t,--thread-count", thread_count, + "Thread pool size (0 = hardware concurrency)"); + app.add_flag("-V,--version", show_version, "Print version and exit"); + app.add_flag("-v,--verbose", verbose, + "Verbose logging (use multiple times to increase - e.g. -vvv)"); + app.add_flag("-q,--quiet", quiet, "Minimal logging"); + app.add_flag("-l,--list-diagrams", list_diagrams, + "Print list of diagrams defined in the config file"); + app.add_flag("--init", initialize, "Initialize example config file"); + app.add_option( + "--add-class-diagram", add_class_diagram, "Add class diagram config"); + app.add_option("--add-sequence-diagram", add_sequence_diagram, + "Add sequence diagram config"); + app.add_option("--add-package-diagram", add_package_diagram, + "Add package diagram config"); + app.add_option("--add-include-diagram", add_include_diagram, + "Add include diagram config"); + app.add_option("--add-diagram-from-template", add_diagram_from_template, + "Add diagram config based on diagram template"); + app.add_option("--template-var", template_variables, + "Specify a value for a template variable"); + app.add_flag("--list-templates", list_templates, + "List all available diagram templates"); + app.add_option("--show-template", show_template, + "Show specific diagram template definition"); + app.add_flag( + "--dump-config", dump_config, "Print effective config to stdout"); + app.add_flag("--paths-relative-to-pwd", paths_relative_to_pwd, + "If true, all paths in configuration files are relative to the $PWD " + "instead of actual location of `.clang-uml` file."); + + try { + app.parse(argc, argv); + } + catch (const CLI::Success &e) { + return cli_flow_t::kExit; + } + catch (const CLI::ParseError &e) { + exit(app.exit(e)); + } + + if (quiet || dump_config) + verbose = 0; + else + verbose++; + + return cli_flow_t::kContinue; +} + +cli_flow_t cli_handler::handle_options(int argc, const char *argv[]) +{ + auto res = parse(argc, argv); + + if (res != cli_flow_t::kContinue) + return res; + + setup_logging(); + + res = handle_pre_config_options(); + + if (res != cli_flow_t::kContinue) + return res; + + res = load_config(); + + if (res != cli_flow_t::kContinue) + return res; + + res = handle_post_config_options(); + + return res; +} + +cli_flow_t cli_handler::handle_pre_config_options() +{ + if (show_version) { + return print_version(); + } + + if ((config_path == "-") && + (initialize || add_diagram_from_template || + add_class_diagram.has_value() || add_sequence_diagram.has_value() || + add_package_diagram.has_value() || + add_include_diagram.has_value())) { + + LOG_ERROR( + "ERROR: Cannot add a diagram config to configuration from stdin"); + + return cli_flow_t::kError; + } + + if (initialize) { + return create_config_file(); + } + + if (config_path != "-") { + if (add_class_diagram) { + return add_config_diagram( + clanguml::common::model::diagram_t::kClass, config_path, + *add_class_diagram); + } + + if (add_sequence_diagram) { + return add_config_diagram( + clanguml::common::model::diagram_t::kSequence, config_path, + *add_sequence_diagram); + } + + if (add_package_diagram) { + return add_config_diagram( + clanguml::common::model::diagram_t::kPackage, config_path, + *add_package_diagram); + } + + if (add_include_diagram) { + return add_config_diagram( + clanguml::common::model::diagram_t::kInclude, config_path, + *add_include_diagram); + } + } + + return cli_flow_t::kContinue; +} + +cli_flow_t cli_handler::load_config() +{ + try { + config = clanguml::config::load(config_path, paths_relative_to_pwd); + return cli_flow_t::kContinue; + } + catch (std::runtime_error &e) { + LOG_ERROR(e.what()); + } + + return cli_flow_t::kError; +} + +cli_flow_t cli_handler::handle_post_config_options() +{ + if (dump_config) { + return print_config(); + } + + if (list_diagrams) { + return print_diagrams_list(); + } + + if (list_templates) { + return print_diagram_templates(); + } + + if (show_template) { + return print_diagram_template(show_template.value()); + } + + if (config_path != "-" && add_diagram_from_template) { + return add_config_diagram_from_template( + config_path, add_diagram_from_template.value(), template_variables); + } + + LOG_INFO("Loaded clang-uml config from {}", config_path); + + // + // Override selected config options from command line + // + if (compilation_database_dir) { + config.compilation_database_dir.set( + util::ensure_path_is_absolute(compilation_database_dir.value()) + .string()); + } + + effective_output_directory = config.output_directory(); + + // Override the output directory from the config + // with the value from the command line if any + if (output_directory) + effective_output_directory = output_directory.value(); + + if (output_directory) { + config.output_directory.set( + util::ensure_path_is_absolute(output_directory.value()).string()); + } + + LOG_INFO("Loading compilation database from {} directory", + config.compilation_database_dir()); + + if (!ensure_output_directory_exists(effective_output_directory)) + return cli_flow_t::kError; + + return cli_flow_t::kContinue; +} + +cli_flow_t cli_handler::print_version() +{ + constexpr auto kLLVMBackendPackageStringLength{5}; + ostr_ << "clang-uml " << clanguml::version::CLANG_UML_VERSION << '\n'; + ostr_ << "Copyright (C) 2021-2023 Bartek Kryza " << '\n'; + ostr_ << "Built against LLVM/Clang libraries version: " + << std::string{BACKEND_PACKAGE_STRING}.substr( + kLLVMBackendPackageStringLength) + << std::endl; + ostr_ << "Using LLVM/Clang libraries version: " + << clang::getClangFullVersion() << std::endl; + + return cli_flow_t::kExit; +} + +bool cli_handler::ensure_output_directory_exists(const std::string &dir) +{ + namespace fs = std::filesystem; + using std::cout; + + fs::path output_dir{dir}; + + if (fs::exists(output_dir) && !fs::is_directory(output_dir)) { + cout << "ERROR: " << dir << " is not a directory...\n"; + return false; + } + + if (!fs::exists(output_dir)) { + return fs::create_directories(output_dir); + } + + return true; +} + +cli_flow_t cli_handler::print_diagrams_list() +{ + using std::cout; + + ostr_ << "The following diagrams are defined in the config file:\n"; + for (const auto &[name, diagram] : config.diagrams) { + ostr_ << " - " << name << " [" << to_string(diagram->type()) << "]"; + ostr_ << '\n'; + } + + return cli_flow_t::kExit; +} + +cli_flow_t cli_handler::print_diagram_templates() +{ + using std::cout; + + if (!config.diagram_templates) { + ostr_ << "No diagram templates are defined in the config file\n"; + return cli_flow_t::kExit; + } + + ostr_ << "The following diagram templates are available:\n"; + for (const auto &[name, diagram_template] : config.diagram_templates()) { + ostr_ << " - " << name << " [" << to_string(diagram_template.type) + << "]"; + if (!diagram_template.description.empty()) + ostr_ << ": " << diagram_template.description; + ostr_ << '\n'; + } + + return cli_flow_t::kExit; +} + +cli_flow_t cli_handler::print_diagram_template(const std::string &template_name) +{ + if (!config.diagram_templates || + config.diagram_templates().count(template_name) == 0) { + ostr_ << "No such diagram template: " << template_name << "\n"; + return cli_flow_t::kError; + } + + for (const auto &[name, diagram_template] : config.diagram_templates()) { + if (template_name == name) { + ostr_ << diagram_template.jinja_template << "\n"; + return cli_flow_t::kExit; + } + } + + return cli_flow_t::kError; +} + +cli_flow_t cli_handler::create_config_file() +{ + namespace fs = std::filesystem; + + fs::path config_file{"./.clang-uml"}; + + if (fs::exists(config_file)) { + ostr_ << "ERROR: .clang-uml file already exists\n"; + return cli_flow_t::kError; + } + + YAML::Emitter out; + out.SetIndent(2); + out << YAML::BeginMap; + out << YAML::Comment("Change to directory where compile_commands.json is"); + out << YAML::Key << "compilation_database_dir" << YAML::Value << "."; + out << YAML::Newline + << YAML::Comment("Change to directory where diagram should be written"); + out << YAML::Key << "output_directory" << YAML::Value << "docs/diagrams"; + out << YAML::Key << "diagrams" << YAML::Value; + out << YAML::BeginMap; + out << YAML::Key << "example_class_diagram" << YAML::Value; + out << YAML::BeginMap; + out << YAML::Key << "type" << YAML::Value << "class"; + out << YAML::Key << "glob" << YAML::Value; + out << YAML::BeginSeq << "src/*.cpp" << YAML::EndSeq; + out << YAML::Key << "using_namespace" << YAML::Value; + out << YAML::BeginSeq << "myproject" << YAML::EndSeq; + out << YAML::Key << "include"; + out << YAML::BeginMap; + out << YAML::Key << "namespaces"; + out << YAML::BeginSeq << "myproject" << YAML::EndSeq; + out << YAML::EndMap; + out << YAML::Key << "exclude"; + out << YAML::BeginMap; + out << YAML::Key << "namespaces"; + out << YAML::BeginSeq << "myproject::detail" << YAML::EndSeq; + out << YAML::EndMap; + out << YAML::EndMap; + out << YAML::EndMap; + out << YAML::EndMap; + out << YAML::Newline; + + std::ofstream ofs(config_file); + ofs << out.c_str(); + ofs.close(); + + return cli_flow_t::kExit; +} + +cli_flow_t cli_handler::add_config_diagram( + clanguml::common::model::diagram_t type, + const std::string &config_file_path, const std::string &name) +{ + namespace fs = std::filesystem; + + fs::path config_file{config_file_path}; + + if (!fs::exists(config_file)) { + std::cerr << "ERROR: " << config_file_path << " file doesn't exists\n"; + return cli_flow_t::kError; + } + + YAML::Node doc = YAML::LoadFile(config_file.string()); + + for (YAML::const_iterator it = doc["diagrams"].begin(); + it != doc["diagrams"].end(); ++it) { + if (it->first.as() == name) { + std::cerr << "ERROR: " << config_file_path + << " file already contains '" << name << "' diagram"; + return cli_flow_t::kError; + } + } + + if (type == clanguml::common::model::diagram_t::kClass) { + doc["diagrams"][name]["type"] = "class"; + doc["diagrams"][name]["glob"] = std::vector{{"src/*.cpp"}}; + doc["diagrams"][name]["using_namespace"] = + std::vector{{"myproject"}}; + doc["diagrams"][name]["include"]["namespaces"] = + std::vector{{"myproject"}}; + doc["diagrams"][name]["exclude"]["namespaces"] = + std::vector{{"myproject::detail"}}; + } + else if (type == clanguml::common::model::diagram_t::kSequence) { + doc["diagrams"][name]["type"] = "sequence"; + doc["diagrams"][name]["glob"] = std::vector{{"src/*.cpp"}}; + doc["diagrams"][name]["combine_free_functions_into_file_participants"] = + true; + doc["diagrams"][name]["using_namespace"] = + std::vector{{"myproject"}}; + doc["diagrams"][name]["include"]["paths"] = + std::vector{{"src"}}; + doc["diagrams"][name]["exclude"]["namespaces"] = + std::vector{{"myproject::detail"}}; + doc["diagrams"][name]["start_from"] = + std::vector>{ + {{"function", "main(int,const char **)"}}}; + } + else if (type == clanguml::common::model::diagram_t::kPackage) { + doc["diagrams"][name]["type"] = "package"; + doc["diagrams"][name]["glob"] = std::vector{{"src/*.cpp"}}; + doc["diagrams"][name]["using_namespace"] = + std::vector{{"myproject"}}; + doc["diagrams"][name]["include"]["namespaces"] = + std::vector{{"myproject"}}; + doc["diagrams"][name]["exclude"]["namespaces"] = + std::vector{{"myproject::detail"}}; + } + else if (type == clanguml::common::model::diagram_t::kInclude) { + doc["diagrams"][name]["type"] = "include"; + doc["diagrams"][name]["glob"] = std::vector{{"src/*.cpp"}}; + doc["diagrams"][name]["relative_to"] = "."; + doc["diagrams"][name]["include"]["paths"] = + std::vector{{"src"}}; + } + + YAML::Emitter out; + out.SetIndent(2); + + out << doc; + out << YAML::Newline; + + std::ofstream ofs(config_file); + ofs << out.c_str(); + ofs.close(); + + return cli_flow_t::kExit; +} + +cli_flow_t cli_handler::add_config_diagram_from_template( + const std::string &config_file_path, const std::string &template_name, + const std::vector &template_variables) +{ + if (!config.diagram_templates || + !(config.diagram_templates().find(template_name) != + config.diagram_templates().end())) { + std::cerr << "ERROR: No such diagram template: " << template_name + << "\n"; + return cli_flow_t::kError; + } + + // First, try to render the template using inja and create a YAML node from + // it + inja::json ctx; + for (const auto &tv : template_variables) { + const auto var = util::split(tv, "="); + if (var.size() != 2) { + std::cerr << "ERROR: Invalid template variable " << tv << "\n"; + return cli_flow_t::kError; + } + + ctx[var.at(0)] = var.at(1); + } + + auto diagram_template_str = + config.diagram_templates().at(template_name).jinja_template; + + YAML::Node diagram_node; + + try { + auto diagram_str = inja::render(diagram_template_str, ctx); + diagram_node = YAML::Load(diagram_str); + } + catch (inja::InjaError &e) { + std::cerr << "ERROR: Failed to generate diagram template '" + << template_name << "': " << e.what() << "\n"; + return cli_flow_t::kError; + } + catch (YAML::Exception &e) { + std::cerr << "ERROR: Rendering diagram template '" << template_name + << "' resulted in invalid YAML: " << e.what() << "\n"; + return cli_flow_t::kError; + } + + namespace fs = std::filesystem; + + fs::path config_file{config_file_path}; + + if (!fs::exists(config_file)) { + std::cerr << "ERROR: " << config_file_path << " file doesn't exists\n"; + return cli_flow_t::kError; + } + + YAML::Node doc = YAML::LoadFile(config_file.string()); + + const auto diagram_name = diagram_node.begin()->first.as(); + doc["diagrams"][diagram_name] = diagram_node.begin()->second; + + YAML::Emitter out; + out.SetIndent(2); + + out << doc; + out << YAML::Newline; + + std::ofstream ofs(config_file); + ofs << out.c_str(); + ofs.close(); + + return cli_flow_t::kExit; +} + +cli_flow_t cli_handler::print_config() +{ + YAML::Emitter out; + out.SetIndent(2); + + out << config; + out << YAML::Newline; + + ostr_ << out.c_str(); + + return cli_flow_t::kExit; +} +} // namespace clanguml::options \ No newline at end of file diff --git a/src/cli/cli_handler.h b/src/cli/cli_handler.h new file mode 100644 index 00000000..11633087 --- /dev/null +++ b/src/cli/cli_handler.h @@ -0,0 +1,153 @@ +/** + * src/options/cli_options.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. + */ +#pragma once + +#include "common/model/enums.h" +#include "config/config.h" + +#include + +#include + +namespace clanguml::cli { + +enum class cli_flow_t { kExit, kError, kContinue }; + +class cli_handler { +public: + cli_handler(std::ostream &ostr = std::cout, + std::shared_ptr logger = spdlog::stdout_color_mt( + "clanguml-logger", spdlog::color_mode::automatic)); + + /** + * Main CLI handling method. + * + * @param argc + * @param argv + * @return + */ + cli_flow_t handle_options(int argc, const char *argv[]); + + /** + * Print the program version and basic information + */ + cli_flow_t print_version(); + + /** + * Print list of diagrams available in the configuration file + */ + cli_flow_t print_diagrams_list(); + + /** + * Print list of available diagram templates, including their names + * and types. + */ + cli_flow_t print_diagram_templates(); + + /** + * Print definition of a specific diagram template. + * + * @param template_name + * @return + */ + cli_flow_t print_diagram_template(const std::string &template_name); + + /** + * Print effective config after loading and setting default values. + */ + cli_flow_t print_config(); + + /** + * Generate sample configuration file and exit. + * + * @return 0 on success or error code + */ + cli_flow_t create_config_file(); + + /** + * Add example diagram of given type to the config file. + * + * @param type Type of the sample diagram to add + * @param config_file_path Path to the config file + * @param name Name of the new diagram + * @return 0 on success or error code + */ + cli_flow_t add_config_diagram(clanguml::common::model::diagram_t type, + const std::string &config_file_path, const std::string &name); + + /** + * Add diagram based on template + * + * @param config_file_path + * @param template_name + * @param template_variables + * @return + */ + cli_flow_t add_config_diagram_from_template( + const std::string &config_file_path, const std::string &template_name, + const std::vector &template_variables); + + /** + * Check if diagram output directory exists, if not create it + * + * @param dir Path to the output directory + * @return True if directory exists or has been created + */ + bool ensure_output_directory_exists(const std::string &dir); + + std::string config_path{".clang-uml"}; + std::optional compilation_database_dir{}; + std::vector diagram_names{}; + std::optional output_directory{}; + std::string effective_output_directory{}; + unsigned int thread_count{}; + bool show_version{false}; + int verbose{}; + bool list_diagrams{false}; + bool quiet{false}; + bool initialize{false}; + std::optional add_class_diagram; + std::optional add_sequence_diagram; + std::optional add_package_diagram; + std::optional add_include_diagram; + std::optional add_diagram_from_template; + bool dump_config{false}; + std::optional paths_relative_to_pwd{}; + std::vector template_variables{}; + bool list_templates{false}; + std::optional show_template; + + clanguml::config::config config; + +private: + cli_flow_t parse(int argc, const char *argv[]); + + cli_flow_t handle_pre_config_options(); + + cli_flow_t load_config(); + + cli_flow_t handle_post_config_options(); + + void setup_logging(); + + std::ostream &ostr_; + std::shared_ptr logger_; + CLI::App app{"Clang-based UML diagram generator for C++"}; +}; + +} // namespace clanguml::options \ No newline at end of file diff --git a/src/config/config.cc b/src/config/config.cc index 21aebe84..7e2190ca 100644 --- a/src/config/config.cc +++ b/src/config/config.cc @@ -17,6 +17,7 @@ */ #include "config.h" +#include "diagram_templates.h" #include "glob/glob.hpp" #include diff --git a/src/config/config.h b/src/config/config.h index 71e7df42..f3a147bd 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -51,6 +51,7 @@ struct plantuml { }; struct diagram_template { + std::string description; common::model::diagram_t type; std::string jinja_template; }; @@ -227,6 +228,8 @@ struct config : public inheritable_diagram_options { "diagram_templates"}; std::map> diagrams; + + void initialize_diagram_templates(); }; // @@ -287,6 +290,8 @@ YAML::Emitter &operator<<(YAML::Emitter &out, const option &o) config load(const std::string &config_file, std::optional paths_relative_to_pwd = {}); + +config load_plain(const std::string &config_file); } // namespace config namespace common::model { diff --git a/src/config/diagram_templates.cc b/src/config/diagram_templates.cc new file mode 100644 index 00000000..1293eebe --- /dev/null +++ b/src/config/diagram_templates.cc @@ -0,0 +1,93 @@ +/** + * src/config/diagram_templates.cc + * + * 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. + */ + +#include "diagram_templates.h" + +namespace clanguml { +namespace config { + +const std::string &get_predefined_diagram_templates() +{ + static const std::string predefined_diagram_templates = + R"(# Predefined diagram templates +parents_hierarchy_tmpl: + description: Generate class parents inheritance diagram + type: class + template: | + {{ diagram_name }}: + type: class + {% if exists("glob") %} + glob: [{{ glob }}] + {% endif %} + {% if exists("using_namespace") %} + using_namespace: {{ using_namespace }} + {% endif %} + include: + parents: [{{ class_name }}] + namespaces: [{{ namespace_names }}] + relationships: + - inheritance + exclude: + access: [public, protected, private] + plantuml: + before: + - left to right direction +subclass_hierarchy_tmpl: + description: Generate class children inheritance diagram + type: class + template: | + {{ diagram_name }}: + type: class + {% if exists("glob") %} + glob: [{{ glob }}] + {% endif %} + {% if exists("using_namespace") %} + using_namespace: {{ using_namespace }} + {% endif %} + include: + parents: [{{ class_name }}] + namespaces: [{{ namespace_name }}] + relationships: + - inheritance + exclude: + access: [public, protected, private] + plantuml: + before: + - left to right direction +class_context_tmpl: + description: Generate class context diagram + type: class + template: | + "{{ diagram_name }}": + type: class + {% if exists("glob") %} + glob: [{{ glob }}] + {% endif %} + {% if exists("using_namespace") %} + using_namespace: {{ using_namespace }} + {% endif %} + include: + context: [{{ class_name }}] + namespaces: [{{ namespace_name }}] +)"; + + return predefined_diagram_templates; +} + +} // namespace config +} // namespace clanguml diff --git a/src/config/diagram_templates.h b/src/config/diagram_templates.h new file mode 100644 index 00000000..a3717e66 --- /dev/null +++ b/src/config/diagram_templates.h @@ -0,0 +1,28 @@ +/** + * src/config/diagram_templates.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. + */ +#pragma once + +#include + +namespace clanguml { +namespace config { + +const std::string &get_predefined_diagram_templates(); + +} // namespace config +} // namespace clanguml diff --git a/src/config/yaml_decoders.cc b/src/config/yaml_decoders.cc index d394b35d..52c7befb 100644 --- a/src/config/yaml_decoders.cc +++ b/src/config/yaml_decoders.cc @@ -17,6 +17,7 @@ */ #include "config.h" +#include "diagram_templates.h" namespace YAML { using clanguml::common::model::access_t; @@ -112,9 +113,24 @@ void get_option>( return; } - option.set( - node[option.name] - .as>()); + if (node[option.name].IsMap() && node[option.name]["include!"]) { + auto parent_path = node["__parent_path"].as(); + + // Load templates from file + auto include_path = std::filesystem::path{parent_path}; + include_path /= node[option.name]["include!"].as(); + + YAML::Node included_node = YAML::LoadFile(include_path.string()); + + // diagram_config = parse_diagram_config(included_node); + option.set( + included_node.as< + std::map>()); + } + else + option.set(node[option.name] + .as>()); } std::shared_ptr parse_diagram_config(const Node &d) @@ -548,28 +564,12 @@ template <> struct convert { template <> struct convert { static bool decode(const Node &node, diagram_template &rhs) { - assert(node.Type() == NodeType::Map || node.Type() == NodeType::Scalar); + assert(node.Type() == NodeType::Map); - if (node.Type() == NodeType::Scalar) { - // Check that the template provided as string is at least valid YAML - const auto yaml_node = Load(node.as()); - const auto template_root_it = yaml_node.begin(); - const auto diagram_name_template = - template_root_it->first.as(); - const auto diagram_type = - template_root_it->second["type"].as(); - rhs.type = clanguml::common::model::from_string(diagram_type); - rhs.jinja_template = Dump(yaml_node); - } - else { - const auto template_root_it = node.begin(); - const auto diagram_name_template = - template_root_it->first.as(); - const auto diagram_type = - template_root_it->second["type"].as(); - rhs.type = clanguml::common::model::from_string(diagram_type); - rhs.jinja_template = Dump(node); - } + rhs.type = clanguml::common::model::from_string( + node["type"].as()); + rhs.jinja_template = node["template"].as(); + rhs.description = node["description"].as(); return true; } @@ -611,10 +611,11 @@ template <> struct convert { auto include_path = std::filesystem::path{parent_path}; include_path /= d.second["include!"].as(); - YAML::Node node = YAML::LoadFile(include_path.string()); - node.force_insert("__parent_path", parent_path); + YAML::Node included_node = + YAML::LoadFile(include_path.string()); + included_node.force_insert("__parent_path", parent_path); - diagram_config = parse_diagram_config(node); + diagram_config = parse_diagram_config(included_node); } else { d.second.force_insert("__parent_path", parent_path); @@ -638,6 +639,20 @@ template <> struct convert { namespace clanguml::config { +void config::initialize_diagram_templates() +{ + const auto &predefined_templates_str = get_predefined_diagram_templates(); + + YAML::Node predefined_templates = YAML::Load(predefined_templates_str); + + if (!diagram_templates) { + diagram_templates.set({}); + } + + diagram_templates().merge( + predefined_templates.as>()); +} + namespace { void resolve_option_path(YAML::Node &doc, const std::string &option) { @@ -729,6 +744,30 @@ config load( doc["git"] = git_config; } + auto d = doc.as(); + + d.initialize_diagram_templates(); + + return d; + } + catch (YAML::BadFile &e) { + throw std::runtime_error(fmt::format( + "Could not open config file {}: {}", config_file, e.what())); + } + catch (YAML::Exception &e) { + throw std::runtime_error(fmt::format( + "Cannot parse YAML file {}: {}", config_file, e.what())); + } +} + +config load_plain(const std::string &config_file) +{ + try { + YAML::Node doc; + std::filesystem::path config_file_path{}; + + doc = YAML::LoadFile(config_file); + auto d = doc.as(); return d; } diff --git a/src/main.cc b/src/main.cc index d5824101..06ca3449 100644 --- a/src/main.cc +++ b/src/main.cc @@ -17,6 +17,7 @@ */ #include "class_diagram/generators/plantuml/class_diagram_generator.h" +#include "cli/cli_handler.h" #include "config/config.h" #include "include_diagram/generators/plantuml/include_diagram_generator.h" #include "package_diagram/generators/plantuml/package_diagram_generator.h" @@ -30,8 +31,6 @@ #include #endif -#include -#include #include #include @@ -50,71 +49,6 @@ backward::SignalHandling sh; // NOLINT using namespace clanguml; -/** - * Print the program version and basic information - */ -void print_version(); - -/** - * Print list of diagrams available in the configuration file - * - * @param cfg Configuration instance loaded from configuration file - */ -void print_diagrams_list(const clanguml::config::config &cfg); - -/** - * Print list of available diagram templates, including their names - * and types. - * - * @param cfg - */ -void print_diagram_templates(const clanguml::config::config &cfg); - -/** - * Print effective config after loading and setting default values. - * - * @param cfg Configuration instance loaded from configuration file - */ -void print_config(const clanguml::config::config &cfg); - -/** - * Generate sample configuration file and exit. - * - * @return 0 on success or error code - */ -int create_config_file(); - -/** - * Add example diagram of given type to the config file. - * - * @param type Type of the sample diagram to add - * @param config_file_path Path to the config file - * @param name Name of the new diagram - * @return 0 on success or error code - */ -int add_config_diagram(clanguml::common::model::diagram_t type, - const std::string &config_file_path, const std::string &name); - -/** - * Add diagram based on template - * @param config_file_path - * @param cfg - * @param template_name - * @param template_variables - * @return - */ -int add_config_diagram_from_template(const std::string &config_file_path, - const config::config &cfg, const std::string &template_name, - const std::vector &template_variables); - -/** - * Check if diagram output directory exists, if not create it - * - * @param dir Path to the output directory - * @return True if directory exists or has been created - */ -bool ensure_output_directory_exists(const std::string &dir); - /** * Generate specific diagram identified by name * @@ -176,182 +110,22 @@ void generate_diagrams(const std::vector &diagram_names, int main(int argc, const char *argv[]) { - CLI::App app{"Clang-based PlantUML diagram generator for C++"}; + cli::cli_handler cli; + auto res = cli.handle_options(argc, argv); - std::string config_path{".clang-uml"}; - std::optional compilation_database_dir{}; - std::vector diagram_names{}; - std::optional output_directory{}; - unsigned int thread_count{}; - bool show_version{false}; - int verbose{}; - bool list_diagrams{false}; - bool quiet{false}; - bool initialize{false}; - std::optional add_class_diagram; - std::optional add_sequence_diagram; - std::optional add_package_diagram; - std::optional add_include_diagram; - std::optional add_diagram_from_template; - bool dump_config{false}; - std::optional paths_relative_to_pwd{}; - std::vector template_variables{}; - bool list_templates{false}; - - app.add_option("-c,--config", config_path, - "Location of configuration file, when '-' read from stdin"); - app.add_option("-d,--compile-database", compilation_database_dir, - "Location of compilation database directory"); - app.add_option("-n,--diagram-name", diagram_names, - "List of diagram names to generate"); - app.add_option("-o,--output-directory", output_directory, - "Override output directory specified in config file"); - app.add_option("-t,--thread-count", thread_count, - "Thread pool size (0 = hardware concurrency)"); - app.add_flag("-V,--version", show_version, "Print version and exit"); - app.add_flag("-v,--verbose", verbose, - "Verbose logging (use multiple times to increase - e.g. -vvv)"); - app.add_flag("-q,--quiet", quiet, "Minimal logging"); - app.add_flag("-l,--list-diagrams", list_diagrams, - "Print list of diagrams defined in the config file"); - app.add_flag("--init", initialize, "Initialize example config file"); - app.add_option( - "--add-class-diagram", add_class_diagram, "Add class diagram config"); - app.add_option("--add-sequence-diagram", add_sequence_diagram, - "Add sequence diagram config"); - app.add_option("--add-package-diagram", add_package_diagram, - "Add package diagram config"); - app.add_option("--add-include-diagram", add_include_diagram, - "Add include diagram config"); - app.add_option("--add-diagram-from-template", add_diagram_from_template, - "Add diagram config based on diagram template"); - app.add_option("--template-variable", template_variables, - "Specify a value for a template variable"); - app.add_flag("--list-templates", list_templates, - "List all available diagram templates"); - app.add_flag( - "--dump-config", dump_config, "Print effective config to stdout"); - app.add_flag("--paths-relative-to-pwd", paths_relative_to_pwd, - "If true, all paths in configuration files are relative to the $PWD " - "instead of actual location of `.clang-uml` file."); - - CLI11_PARSE(app, argc, argv); - - if (quiet || dump_config) - verbose = 0; - else - verbose++; - - clanguml::util::setup_logging(verbose); - - if (show_version) { - print_version(); + if (res == cli::cli_flow_t::kExit) return 0; - } - if ((config_path == "-") && - (initialize || add_diagram_from_template || - add_class_diagram.has_value() || add_sequence_diagram.has_value() || - add_package_diagram.has_value() || - add_include_diagram.has_value())) { - - LOG_ERROR( - "ERROR: Cannot add a diagram config to configuration from stdin"); - - return 1; - } - - if (initialize) { - return create_config_file(); - } - - if (config_path != "-") { - if (add_class_diagram) { - return add_config_diagram( - clanguml::common::model::diagram_t::kClass, config_path, - *add_class_diagram); - } - - if (add_sequence_diagram) { - return add_config_diagram( - clanguml::common::model::diagram_t::kSequence, config_path, - *add_sequence_diagram); - } - - if (add_package_diagram) { - return add_config_diagram( - clanguml::common::model::diagram_t::kPackage, config_path, - *add_package_diagram); - } - - if (add_include_diagram) { - return add_config_diagram( - clanguml::common::model::diagram_t::kInclude, config_path, - *add_include_diagram); - } - } - - clanguml::config::config config; - try { - config = clanguml::config::load(config_path, paths_relative_to_pwd); - } - catch (std::runtime_error &e) { - LOG_ERROR(e.what()); - return 1; - } - - if (list_diagrams) { - print_diagrams_list(config); - return 0; - } - - if (list_templates) { - print_diagram_templates(config); - return 0; - } - - if (config_path != "-" && add_diagram_from_template) { - return add_config_diagram_from_template(config_path, config, - add_diagram_from_template.value(), template_variables); - } - - LOG_INFO("Loaded clang-uml config from {}", config_path); - - // - // Override selected config options from command line - // - if (compilation_database_dir) { - config.compilation_database_dir.set( - util::ensure_path_is_absolute(compilation_database_dir.value()) - .string()); - } - if (output_directory) { - config.output_directory.set( - util::ensure_path_is_absolute(output_directory.value()).string()); - } - - LOG_INFO("Loading compilation database from {} directory", - config.compilation_database_dir()); - - auto od = config.output_directory(); - if (output_directory) - od = output_directory.value(); - - if (dump_config) { - print_config(config); - return 0; - } - - if (!ensure_output_directory_exists(od)) + if (res == cli::cli_flow_t::kError) return 1; std::string err{}; auto db = clang::tooling::CompilationDatabase::autoDetectFromDirectory( - config.compilation_database_dir(), err); + cli.config.compilation_database_dir(), err); if (!err.empty()) { LOG_ERROR("Failed to load compilation database from {}", - config.compilation_database_dir()); + cli.config.compilation_database_dir()); return 1; } @@ -364,10 +138,11 @@ int main(int argc, const char *argv[]) // We have to generate the translation units list for each diagram before // scheduling tasks, because std::filesystem::current_path cannot be trusted // with multiple threads - find_translation_units_for_diagrams(diagram_names, config, + find_translation_units_for_diagrams(cli.diagram_names, cli.config, compilation_database_files, translation_units_map); - generate_diagrams(diagram_names, config, od, db, verbose, thread_count, + generate_diagrams(cli.diagram_names, cli.config, + cli.effective_output_directory, db, cli.verbose, cli.thread_count, translation_units_map); return 0; @@ -533,264 +308,3 @@ void find_translation_units_for_diagrams( } } } - -bool ensure_output_directory_exists(const std::string &dir) -{ - namespace fs = std::filesystem; - using std::cout; - - fs::path output_dir{dir}; - - if (fs::exists(output_dir) && !fs::is_directory(output_dir)) { - cout << "ERROR: " << dir << " is not a directory...\n"; - return false; - } - - if (!fs::exists(output_dir)) { - return fs::create_directories(output_dir); - } - - return true; -} - -void print_version() -{ - constexpr auto kLLVMBackendPackageStringLength{5}; - std::cout << "clang-uml " << clanguml::version::CLANG_UML_VERSION << '\n'; - std::cout << "Copyright (C) 2021-2023 Bartek Kryza " - << '\n'; - std::cout << "Built against LLVM/Clang libraries version: " - << std::string{BACKEND_PACKAGE_STRING}.substr( - kLLVMBackendPackageStringLength) - << std::endl; - std::cout << "Using LLVM/Clang libraries version: " - << clang::getClangFullVersion() << std::endl; -} - -void print_diagrams_list(const clanguml::config::config &cfg) -{ - using std::cout; - - cout << "The following diagrams are defined in the config file:\n"; - for (const auto &[name, diagram] : cfg.diagrams) { - cout << " - " << name << " [" << to_string(diagram->type()) << "]"; - cout << '\n'; - } -} - -void print_diagram_templates(const clanguml::config::config &cfg) -{ - using std::cout; - - if (!cfg.diagram_templates) { - cout << "No diagram templates are defined in the config file\n"; - return; - } - - cout << "The following diagram templates are available:\n"; - for (const auto &[name, diagram_template] : cfg.diagram_templates()) { - cout << " - " << name << " [" << to_string(diagram_template.type) - << "]"; - cout << '\n'; - } -} - -int create_config_file() -{ - namespace fs = std::filesystem; - using std::cout; - - fs::path config_file{"./.clang-uml"}; - - if (fs::exists(config_file)) { - cout << "ERROR: .clang-uml file already exists\n"; - return 1; - } - - YAML::Emitter out; - out.SetIndent(2); - out << YAML::BeginMap; - out << YAML::Comment("Change to directory where compile_commands.json is"); - out << YAML::Key << "compilation_database_dir" << YAML::Value << "."; - out << YAML::Newline - << YAML::Comment("Change to directory where diagram should be written"); - out << YAML::Key << "output_directory" << YAML::Value << "docs/diagrams"; - out << YAML::Key << "diagrams" << YAML::Value; - out << YAML::BeginMap; - out << YAML::Key << "example_class_diagram" << YAML::Value; - out << YAML::BeginMap; - out << YAML::Key << "type" << YAML::Value << "class"; - out << YAML::Key << "glob" << YAML::Value; - out << YAML::BeginSeq << "src/*.cpp" << YAML::EndSeq; - out << YAML::Key << "using_namespace" << YAML::Value; - out << YAML::BeginSeq << "myproject" << YAML::EndSeq; - out << YAML::Key << "include"; - out << YAML::BeginMap; - out << YAML::Key << "namespaces"; - out << YAML::BeginSeq << "myproject" << YAML::EndSeq; - out << YAML::EndMap; - out << YAML::Key << "exclude"; - out << YAML::BeginMap; - out << YAML::Key << "namespaces"; - out << YAML::BeginSeq << "myproject::detail" << YAML::EndSeq; - out << YAML::EndMap; - out << YAML::EndMap; - out << YAML::EndMap; - out << YAML::EndMap; - out << YAML::Newline; - - std::ofstream ofs(config_file); - ofs << out.c_str(); - ofs.close(); - - return 0; -} - -int add_config_diagram(clanguml::common::model::diagram_t type, - const std::string &config_file_path, const std::string &name) -{ - namespace fs = std::filesystem; - - fs::path config_file{config_file_path}; - - if (!fs::exists(config_file)) { - std::cerr << "ERROR: " << config_file_path << " file doesn't exists\n"; - return 1; - } - - YAML::Node doc = YAML::LoadFile(config_file.string()); - - for (YAML::const_iterator it = doc["diagrams"].begin(); - it != doc["diagrams"].end(); ++it) { - if (it->first.as() == name) { - std::cerr << "ERROR: " << config_file_path - << " file already contains '" << name << "' diagram"; - return 1; - } - } - - if (type == clanguml::common::model::diagram_t::kClass) { - doc["diagrams"][name]["type"] = "class"; - doc["diagrams"][name]["glob"] = std::vector{{"src/*.cpp"}}; - doc["diagrams"][name]["using_namespace"] = - std::vector{{"myproject"}}; - doc["diagrams"][name]["include"]["namespaces"] = - std::vector{{"myproject"}}; - doc["diagrams"][name]["exclude"]["namespaces"] = - std::vector{{"myproject::detail"}}; - } - else if (type == clanguml::common::model::diagram_t::kSequence) { - doc["diagrams"][name]["type"] = "sequence"; - doc["diagrams"][name]["glob"] = std::vector{{"src/*.cpp"}}; - doc["diagrams"][name]["combine_free_functions_into_file_participants"] = - true; - doc["diagrams"][name]["using_namespace"] = - std::vector{{"myproject"}}; - doc["diagrams"][name]["include"]["paths"] = - std::vector{{"src"}}; - doc["diagrams"][name]["exclude"]["namespaces"] = - std::vector{{"myproject::detail"}}; - doc["diagrams"][name]["start_from"] = - std::vector>{ - {{"function", "main(int,const char **)"}}}; - } - else if (type == clanguml::common::model::diagram_t::kPackage) { - doc["diagrams"][name]["type"] = "package"; - doc["diagrams"][name]["glob"] = std::vector{{"src/*.cpp"}}; - doc["diagrams"][name]["using_namespace"] = - std::vector{{"myproject"}}; - doc["diagrams"][name]["include"]["namespaces"] = - std::vector{{"myproject"}}; - doc["diagrams"][name]["exclude"]["namespaces"] = - std::vector{{"myproject::detail"}}; - } - else if (type == clanguml::common::model::diagram_t::kInclude) { - doc["diagrams"][name]["type"] = "include"; - doc["diagrams"][name]["glob"] = std::vector{{"src/*.cpp"}}; - doc["diagrams"][name]["relative_to"] = "."; - doc["diagrams"][name]["include"]["paths"] = - std::vector{{"src"}}; - } - - YAML::Emitter out; - out.SetIndent(2); - - out << doc; - out << YAML::Newline; - - std::ofstream ofs(config_file); - ofs << out.c_str(); - ofs.close(); - - return 0; -} - -int add_config_diagram_from_template(const std::string &config_file_path, - const config::config &cfg, const std::string &template_name, - const std::vector &template_variables) -{ - if (!cfg.diagram_templates || - !(cfg.diagram_templates().find(template_name) != - cfg.diagram_templates().end())) { - std::cerr << "ERROR: No such diagram template: " << template_name - << "\n"; - return 1; - } - - // First, try to render the template using inja and create a YAML node from - // it - inja::json ctx; - for (const auto &tv : template_variables) { - const auto var = util::split(tv, "="); - if (var.size() != 2) { - std::cerr << "ERROR: Invalid template variable " << tv << "\n"; - return 1; - } - - ctx[var.at(0)] = var.at(1); - } - - auto diagram_template_str = - cfg.diagram_templates().at(template_name).jinja_template; - - auto diagram_str = inja::render(diagram_template_str, ctx); - - auto diagram_node = YAML::Load(diagram_str); - - namespace fs = std::filesystem; - - fs::path config_file{config_file_path}; - - if (!fs::exists(config_file)) { - std::cerr << "ERROR: " << config_file_path << " file doesn't exists\n"; - return 1; - } - - YAML::Node doc = YAML::LoadFile(config_file.string()); - - const auto diagram_name = diagram_node.begin()->first.as(); - doc["diagrams"][diagram_name] = diagram_node.begin()->second; - - YAML::Emitter out; - out.SetIndent(2); - - out << doc; - out << YAML::Newline; - - std::ofstream ofs(config_file); - ofs << out.c_str(); - ofs.close(); - - return 0; -} - -void print_config(const clanguml::config::config &cfg) -{ - YAML::Emitter out; - out.SetIndent(2); - - out << cfg; - out << YAML::Newline; - - std::cout << out.c_str(); -} \ No newline at end of file diff --git a/src/util/util.cc b/src/util/util.cc index 94f1f5fd..76de73ea 100644 --- a/src/util/util.cc +++ b/src/util/util.cc @@ -25,27 +25,6 @@ namespace clanguml::util { static const auto WHITESPACE = " \n\r\t\f\v"; -void setup_logging(int verbose) -{ - auto console = - spdlog::stdout_color_mt("console", spdlog::color_mode::automatic); - - console->set_pattern("[%^%l%^] [tid %t] %v"); - - if (verbose == 0) { - console->set_level(spdlog::level::err); - } - else if (verbose == 1) { - console->set_level(spdlog::level::info); - } - else if (verbose == 2) { - console->set_level(spdlog::level::debug); - } - else { - console->set_level(spdlog::level::trace); - } -} - std::string get_process_output(const std::string &command) { constexpr size_t kBufferSize{1024}; diff --git a/src/util/util.h b/src/util/util.h index 15c12335..bfdbe29c 100644 --- a/src/util/util.h +++ b/src/util/util.h @@ -28,29 +28,29 @@ #include #define LOG_ERROR(fmt__, ...) \ - spdlog::get("console")->error( \ - fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, __LINE__, \ - ##__VA_ARGS__) + spdlog::get("clanguml-logger") \ + ->error(fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, \ + __LINE__, ##__VA_ARGS__) #define LOG_WARN(fmt__, ...) \ - spdlog::get("console")->warn( \ - fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, __LINE__, \ - ##__VA_ARGS__) + spdlog::get("clanguml-logger") \ + ->warn(fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, \ + __LINE__, ##__VA_ARGS__) #define LOG_INFO(fmt__, ...) \ - spdlog::get("console")->info( \ - fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, __LINE__, \ - ##__VA_ARGS__) + spdlog::get("clanguml-logger") \ + ->info(fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, \ + __LINE__, ##__VA_ARGS__) #define LOG_DBG(fmt__, ...) \ - spdlog::get("console")->debug( \ - fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, __LINE__, \ - ##__VA_ARGS__) + spdlog::get("clanguml-logger") \ + ->debug(fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, \ + __LINE__, ##__VA_ARGS__) #define LOG_TRACE(fmt__, ...) \ - spdlog::get("console")->trace( \ - fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, __LINE__, \ - ##__VA_ARGS__) + spdlog::get("clanguml-logger") \ + ->trace(fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, \ + __LINE__, ##__VA_ARGS__) namespace clanguml::util { @@ -61,13 +61,6 @@ std::string trim(const std::string &s); #define FILENAME_ \ (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__) -/** - * @brief Setup spdlog logger. - * - * @param verbose Whether the logging should be verbose or not. - */ -void setup_logging(int verbose); - std::string get_process_output(const std::string &command); std::string get_env(const std::string &name); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index daf64ccb..4c17b96e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -38,6 +38,7 @@ set(TEST_CASES test_cases test_decorator_parser test_config + test_cli_handler test_filters test_thread_pool_executor) diff --git a/tests/test_cases.cc b/tests/test_cases.cc index 10c4bff4..926083ff 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -17,6 +17,8 @@ */ #include "test_cases.h" + +#include "cli/cli_handler.h" #include "common/generators/plantuml/generator.h" #include @@ -345,7 +347,17 @@ int main(int argc, char *argv[]) if (returnCode != 0) return returnCode; - clanguml::util::setup_logging(debug_log ? 3 : 1); + clanguml::cli::cli_handler clih; + + std::vector argvv = { + "clang-uml", "--config", "./test_config_data/simple.yml"}; + + if (debug_log) + argvv.push_back("-vvv"); + else + argvv.push_back("-q"); + + clih.handle_options(argvv.size(), argvv.data()); return session.run(); } diff --git a/tests/test_cli_handler.cc b/tests/test_cli_handler.cc new file mode 100644 index 00000000..65ad3523 --- /dev/null +++ b/tests/test_cli_handler.cc @@ -0,0 +1,74 @@ +/** + * tests/test_cli_handler.cc + * + * 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. + */ +#define CATCH_CONFIG_MAIN + +#include "cli/cli_handler.h" +#include "version.h" + +#include "catch.h" + +#include +#include + +std::shared_ptr make_sstream_logger(std::ostream &ostr) +{ + auto oss_sink = std::make_shared(ostr); + return std::make_shared( + "clanguml-logger", std::move(oss_sink)); +} + +TEST_CASE("Test cli handler print_version", "[unit-test]") +{ + using clanguml::cli::cli_flow_t; + using clanguml::cli::cli_handler; + + std::vector argv = {"clang-uml", "--version"}; + + std::ostringstream ostr; + cli_handler cli{ostr, make_sstream_logger(ostr)}; + + auto res = cli.handle_options(argv.size(), argv.data()); + + REQUIRE(res == cli_flow_t::kExit); + + auto output = ostr.str(); + + REQUIRE(output.find(fmt::format( + "clang-uml {}", clanguml::version::CLANG_UML_VERSION)) == 0); +} + +TEST_CASE("Test cli handler print_config", "[unit-test]") +{ + using clanguml::cli::cli_flow_t; + using clanguml::cli::cli_handler; + + std::vector argv = {"clang-uml", "--config", + "./test_config_data/simple.yml", "--dump-config"}; + + std::ostringstream ostr; + cli_handler cli{ostr, make_sstream_logger(ostr)}; + + auto res = cli.handle_options(argv.size(), argv.data()); + + REQUIRE(res == cli_flow_t::kExit); + + auto output = ostr.str(); + YAML::Node doc = YAML::Load(output); + + REQUIRE(doc["diagrams"]["class_main"]); +} \ No newline at end of file diff --git a/tests/test_config.cc b/tests/test_config.cc index a3f92441..ab95a3d5 100644 --- a/tests/test_config.cc +++ b/tests/test_config.cc @@ -265,39 +265,55 @@ TEST_CASE("Test config diagram_templates", "[unit-test]") auto cfg = clanguml::config::load("./test_config_data/diagram_templates.yml"); - REQUIRE(cfg.diagram_templates().size() == 3); + REQUIRE(cfg.diagram_templates().size() == 4); - REQUIRE(cfg.diagram_templates()["bases_hierarchy_tmpl"].type == + REQUIRE(cfg.diagram_templates()["parents_hierarchy_tmpl"].type == clanguml::common::model::diagram_t::kClass); - REQUIRE(cfg.diagram_templates()["bases_hierarchy_tmpl"].jinja_template == - R"("{{ class_name }}_parents_hierarchy": + REQUIRE(cfg.diagram_templates()["parents_hierarchy_tmpl"].jinja_template == + "{{ diagram_name }}:" + R"( type: class + {% if exists("glob") %} + glob: [{{ glob }}] + {% endif %} + {% if exists("using_namespace") %} + using_namespace: {{ using_namespace }} + {% endif %} include: - parents: "{{ class_name }}" - namespaces: "{{ namespace_name }}" + parents: [{{ class_name }}] + namespaces: [{{ namespace_names }}] relationships: - inheritance exclude: access: [public, protected, private] plantuml: before: - - left to right direction)"); + - left to right direction +)"); REQUIRE(cfg.diagram_templates()["children_hierarchy_tmpl"].type == clanguml::common::model::diagram_t::kClass); - REQUIRE(cfg.diagram_templates()["children_hierarchy_tmpl"].jinja_template == - R"("{{ class_name }}_children_hierarchy": + REQUIRE(cfg.diagram_templates()["subclass_hierarchy_tmpl"].jinja_template == + "{{ diagram_name }}:" + R"( type: class + {% if exists("glob") %} + glob: [{{ glob }}] + {% endif %} + {% if exists("using_namespace") %} + using_namespace: {{ using_namespace }} + {% endif %} include: - subclasses: "{{ class_name }}" - namespaces: "{{ namespace_name }}" + parents: [{{ class_name }}] + namespaces: [{{ namespace_name }}] relationships: - inheritance exclude: access: [public, protected, private] plantuml: before: - - left to right direction)"); + - left to right direction +)"); REQUIRE(cfg.diagram_templates()["main_sequence_tmpl"].type == clanguml::common::model::diagram_t::kSequence); diff --git a/tests/test_config_data/diagram_templates.yml b/tests/test_config_data/diagram_templates.yml index 3c877b88..e1af0e62 100644 --- a/tests/test_config_data/diagram_templates.yml +++ b/tests/test_config_data/diagram_templates.yml @@ -2,33 +2,10 @@ compilation_database_dir: debug output_directory: output diagram_templates: - bases_hierarchy_tmpl: - '{{ class_name }}_parents_hierarchy': - type: class - include: - parents: '{{ class_name }}' - namespaces: '{{ namespace_name }}' - relationships: - - inheritance - exclude: - access: [public, protected, private] - plantuml: - before: - - left to right direction - children_hierarchy_tmpl: | - '{{ class_name }}_children_hierarchy': - type: class - include: - subclasses: '{{ class_name }}' - namespaces: '{{ namespace_name }}' - relationships: - - inheritance - exclude: - access: [public, protected, private] - plantuml: - before: - - left to right direction - main_sequence_tmpl: | + main_sequence_tmpl: + description: Sequence diagram of the main() function + type: sequence + template: | main_sequence_diagram: type: sequence glob: [ {{ glob }} ] diff --git a/tests/test_config_data/simple.yml b/tests/test_config_data/simple.yml index 805123c6..ebc9be14 100644 --- a/tests/test_config_data/simple.yml +++ b/tests/test_config_data/simple.yml @@ -1,6 +1,5 @@ compilation_database_dir: debug output_directory: output - diagrams: class_main: type: class