From 708fbba3480ffa6a930a62cf919d1c985c7b3843 Mon Sep 17 00:00:00 2001 From: gomfol12 Date: Tue, 24 Feb 2026 14:03:37 +0100 Subject: [PATCH 1/6] add cgvisual tool --- tools/CMakeLists.txt | 1 + tools/cgvisual/CMakeLists.txt | 21 +++++++++ tools/cgvisual/README.md | 12 +++++ tools/cgvisual/visual.cpp | 85 +++++++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+) create mode 100644 tools/cgvisual/CMakeLists.txt create mode 100644 tools/cgvisual/README.md create mode 100644 tools/cgvisual/visual.cpp diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 6ddc252a..b42f792b 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -4,6 +4,7 @@ add_subdirectory(cgconvert) add_subdirectory(cgformat) add_subdirectory(cgdiff) add_subdirectory(cgquery) +add_subdirectory(cgvisual) if(BUILD_CAGE) add_subdirectory(cage) endif() diff --git a/tools/cgvisual/CMakeLists.txt b/tools/cgvisual/CMakeLists.txt new file mode 100644 index 00000000..bbe7ea01 --- /dev/null +++ b/tools/cgvisual/CMakeLists.txt @@ -0,0 +1,21 @@ +set(PROJECT_NAME CGVisual) +set(TARGETS_EXPORT_NAME ${PROJECT_NAME}-target) +set(PROJECT_BINARY_NAME cgvisual) + +add_executable(${PROJECT_BINARY_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/visual.cpp) + +add_metacg(${PROJECT_BINARY_NAME}) +add_spdlog_libraries(${PROJECT_BINARY_NAME}) + +install( + TARGETS ${PROJECT_BINARY_NAME} + EXPORT ${TARGETS_EXPORT_NAME} + RUNTIME DESTINATION bin +) + +configure_package_config_file( + ${METACG_Directory}/cmake/Config.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake + INSTALL_DESTINATION lib/cmake +) + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake DESTINATION lib/cmake) diff --git a/tools/cgvisual/README.md b/tools/cgvisual/README.md new file mode 100644 index 00000000..8f62d87b --- /dev/null +++ b/tools/cgvisual/README.md @@ -0,0 +1,12 @@ +# CGVisual + +CGVisual is a simple tool to generate a DOT file representation of a given call +graph. It uses the existing DOT infrastructure already present in MetaCG. + +## Usage + +``` +cgvisual [options] +``` + +Use `cgvisual --help` to get a list of available options. diff --git a/tools/cgvisual/visual.cpp b/tools/cgvisual/visual.cpp new file mode 100644 index 00000000..0b8a95e7 --- /dev/null +++ b/tools/cgvisual/visual.cpp @@ -0,0 +1,85 @@ +/** + * File: visual.cpp + * License: Part of the MetaCG project. Licensed under BSD 3 clause license. See LICENSE.txt file at + * https://github.com/tudasc/metacg/LICENSE.txt + */ + +#include +#include +#include +#include +#include + +static auto console = metacg::MCGLogger::instance().getConsole(); +static auto errConsole = metacg::MCGLogger::instance().getErrConsole(); + +void printUsage() { + std::cout << "Usage: visuel \n\n"; + std::cout << "Reads a call graph from the specified file and generates a DOT file for visualization.\n\n"; + std::cout << "Options:\n"; + std::cout << " -h, --help Show this help message\n"; + std::cout << " -o, --output Specify output DOT file name (default: callgraph.dot)\n"; +} + +int main(int argc, char* argv[]) { + if (argc < 2) { + printUsage(); + return EXIT_FAILURE; + } + + std::string inputFile; + std::string outputFile = "callgraph.dot"; + + for (int i = 1; i < argc; i++) { + std::string arg = argv[i]; + + if (arg == "-h" || arg == "--help") { + printUsage(); + return EXIT_SUCCESS; + } else if (arg == "-o" || arg == "--output") { + if (i + 1 >= argc) { + errConsole->error("Output file name not specified after {}", arg); + return EXIT_FAILURE; + } + outputFile = argv[++i]; + } else { + inputFile = arg; + } + } + + if (inputFile.empty()) { + errConsole->error("No input call graph file specified."); + printUsage(); + return EXIT_FAILURE; + } + + metacg::io::FileSource fs1(inputFile); + + auto mcgReader1 = metacg::io::createReader(fs1); + if (!mcgReader1) { + errConsole->error("Failed to create MCG reader for file: {}", inputFile); + return EXIT_FAILURE; + } + + auto cg = mcgReader1->read(); + if (!cg) { + errConsole->error("Failed to read call graph from file."); + return EXIT_FAILURE; + } + + metacg::io::dot::DotGenerator dotGen(cg.get()); + dotGen.generate(); + + std::ofstream outFile(outputFile); + if (!outFile.is_open()) { + errConsole->error("Could not open output file for writing: {}", outputFile); + return EXIT_FAILURE; + } + outFile << dotGen.getDotString(); + + outFile.close(); + + console->info("DOT file generated successfully: {}", outputFile); + + return EXIT_SUCCESS; +} From 7cdca4aca4e3502afcf5e668968216dc05ba70a1 Mon Sep 17 00:00:00 2001 From: gomfol12 Date: Tue, 24 Feb 2026 16:28:25 +0100 Subject: [PATCH 2/6] adopted cxxopts --- tools/cgvisual/CMakeLists.txt | 1 + tools/cgvisual/README.md | 2 +- tools/cgvisual/visual.cpp | 66 +++++++++++++++-------------------- 3 files changed, 30 insertions(+), 39 deletions(-) diff --git a/tools/cgvisual/CMakeLists.txt b/tools/cgvisual/CMakeLists.txt index bbe7ea01..1d896070 100644 --- a/tools/cgvisual/CMakeLists.txt +++ b/tools/cgvisual/CMakeLists.txt @@ -5,6 +5,7 @@ set(PROJECT_BINARY_NAME cgvisual) add_executable(${PROJECT_BINARY_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/visual.cpp) add_metacg(${PROJECT_BINARY_NAME}) +add_cxxopts(${PROJECT_BINARY_NAME}) add_spdlog_libraries(${PROJECT_BINARY_NAME}) install( diff --git a/tools/cgvisual/README.md b/tools/cgvisual/README.md index 8f62d87b..cdd65a97 100644 --- a/tools/cgvisual/README.md +++ b/tools/cgvisual/README.md @@ -1,7 +1,7 @@ # CGVisual CGVisual is a simple tool to generate a DOT file representation of a given call -graph. It uses the existing DOT infrastructure already present in MetaCG. +graph. ## Usage diff --git a/tools/cgvisual/visual.cpp b/tools/cgvisual/visual.cpp index 0b8a95e7..e2dd8a34 100644 --- a/tools/cgvisual/visual.cpp +++ b/tools/cgvisual/visual.cpp @@ -7,61 +7,52 @@ #include #include #include +#include #include #include static auto console = metacg::MCGLogger::instance().getConsole(); static auto errConsole = metacg::MCGLogger::instance().getErrConsole(); -void printUsage() { - std::cout << "Usage: visuel \n\n"; - std::cout << "Reads a call graph from the specified file and generates a DOT file for visualization.\n\n"; - std::cout << "Options:\n"; - std::cout << " -h, --help Show this help message\n"; - std::cout << " -o, --output Specify output DOT file name (default: callgraph.dot)\n"; -} - int main(int argc, char* argv[]) { - if (argc < 2) { - printUsage(); - return EXIT_FAILURE; - } + cxxopts::Options options("visual", + "Reads a call graph from the specified file and generates a DOT file for visualization."); + + // clang-format off + options.add_options() + ("h,help", "Show this help message") + ("o,output", "Specify output DOT file name", cxxopts::value()->default_value("callgraph.dot")) + ("input", "Input call graph file", cxxopts::value()) + ; + // clang-format on + + options.parse_positional({"input"}); - std::string inputFile; - std::string outputFile = "callgraph.dot"; - - for (int i = 1; i < argc; i++) { - std::string arg = argv[i]; - - if (arg == "-h" || arg == "--help") { - printUsage(); - return EXIT_SUCCESS; - } else if (arg == "-o" || arg == "--output") { - if (i + 1 >= argc) { - errConsole->error("Output file name not specified after {}", arg); - return EXIT_FAILURE; - } - outputFile = argv[++i]; - } else { - inputFile = arg; - } + auto result = options.parse(argc, argv); + + if (result.count("help")) { + std::cout << options.help() << std::endl; + return EXIT_SUCCESS; } - if (inputFile.empty()) { + if (!result.count("input")) { errConsole->error("No input call graph file specified."); - printUsage(); + std::cout << options.help() << std::endl; return EXIT_FAILURE; } - metacg::io::FileSource fs1(inputFile); + std::string inputFile = result["input"].as(); + std::string outputFile = result["output"].as(); + + metacg::io::FileSource fs(inputFile); - auto mcgReader1 = metacg::io::createReader(fs1); - if (!mcgReader1) { + auto mcgReader = metacg::io::createReader(fs); + if (!mcgReader) { errConsole->error("Failed to create MCG reader for file: {}", inputFile); return EXIT_FAILURE; } - auto cg = mcgReader1->read(); + auto cg = mcgReader->read(); if (!cg) { errConsole->error("Failed to read call graph from file."); return EXIT_FAILURE; @@ -75,11 +66,10 @@ int main(int argc, char* argv[]) { errConsole->error("Could not open output file for writing: {}", outputFile); return EXIT_FAILURE; } - outFile << dotGen.getDotString(); + outFile << dotGen.getDotString(); outFile.close(); console->info("DOT file generated successfully: {}", outputFile); - return EXIT_SUCCESS; } From 699d9bc43cdb3dc15fecacf21df60a882e7a0299 Mon Sep 17 00:00:00 2001 From: gomfol12 Date: Tue, 24 Feb 2026 16:30:58 +0100 Subject: [PATCH 3/6] updated includes --- tools/cgvisual/visual.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tools/cgvisual/visual.cpp b/tools/cgvisual/visual.cpp index e2dd8a34..f86f3f60 100644 --- a/tools/cgvisual/visual.cpp +++ b/tools/cgvisual/visual.cpp @@ -4,12 +4,13 @@ * https://github.com/tudasc/metacg/LICENSE.txt */ -#include -#include -#include +#include "Callgraph.h" +#include "DotIO.h" +#include "LoggerUtil.h" +#include "io/MCGReader.h" + #include #include -#include static auto console = metacg::MCGLogger::instance().getConsole(); static auto errConsole = metacg::MCGLogger::instance().getErrConsole(); From e8a92bd205069c54b3b2cc1ae62459e53b5f8bab Mon Sep 17 00:00:00 2001 From: gomfol12 Date: Tue, 3 Mar 2026 11:28:10 +0100 Subject: [PATCH 4/6] Rename CGVisual to CGToDot --- tools/CMakeLists.txt | 2 +- tools/{cgvisual/visual.cpp => cgtodot/CGToDot.cpp} | 4 ++-- tools/{cgvisual => cgtodot}/CMakeLists.txt | 6 +++--- tools/cgtodot/README.md | 12 ++++++++++++ tools/cgvisual/README.md | 12 ------------ 5 files changed, 18 insertions(+), 18 deletions(-) rename tools/{cgvisual/visual.cpp => cgtodot/CGToDot.cpp} (97%) rename tools/{cgvisual => cgtodot}/CMakeLists.txt (88%) create mode 100644 tools/cgtodot/README.md delete mode 100644 tools/cgvisual/README.md diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index b42f792b..02ac6fe7 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -4,7 +4,7 @@ add_subdirectory(cgconvert) add_subdirectory(cgformat) add_subdirectory(cgdiff) add_subdirectory(cgquery) -add_subdirectory(cgvisual) +add_subdirectory(cgtodot) if(BUILD_CAGE) add_subdirectory(cage) endif() diff --git a/tools/cgvisual/visual.cpp b/tools/cgtodot/CGToDot.cpp similarity index 97% rename from tools/cgvisual/visual.cpp rename to tools/cgtodot/CGToDot.cpp index f86f3f60..66b3c33d 100644 --- a/tools/cgvisual/visual.cpp +++ b/tools/cgtodot/CGToDot.cpp @@ -1,5 +1,5 @@ /** - * File: visual.cpp + * File: CGToDot.cpp * License: Part of the MetaCG project. Licensed under BSD 3 clause license. See LICENSE.txt file at * https://github.com/tudasc/metacg/LICENSE.txt */ @@ -16,7 +16,7 @@ static auto console = metacg::MCGLogger::instance().getConsole(); static auto errConsole = metacg::MCGLogger::instance().getErrConsole(); int main(int argc, char* argv[]) { - cxxopts::Options options("visual", + cxxopts::Options options("CGToDot", "Reads a call graph from the specified file and generates a DOT file for visualization."); // clang-format off diff --git a/tools/cgvisual/CMakeLists.txt b/tools/cgtodot/CMakeLists.txt similarity index 88% rename from tools/cgvisual/CMakeLists.txt rename to tools/cgtodot/CMakeLists.txt index 1d896070..f788008a 100644 --- a/tools/cgvisual/CMakeLists.txt +++ b/tools/cgtodot/CMakeLists.txt @@ -1,8 +1,8 @@ -set(PROJECT_NAME CGVisual) +set(PROJECT_NAME CGToDot) set(TARGETS_EXPORT_NAME ${PROJECT_NAME}-target) -set(PROJECT_BINARY_NAME cgvisual) +set(PROJECT_BINARY_NAME cgtodot) -add_executable(${PROJECT_BINARY_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/visual.cpp) +add_executable(${PROJECT_BINARY_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/CGToDot.cpp) add_metacg(${PROJECT_BINARY_NAME}) add_cxxopts(${PROJECT_BINARY_NAME}) diff --git a/tools/cgtodot/README.md b/tools/cgtodot/README.md new file mode 100644 index 00000000..6048010c --- /dev/null +++ b/tools/cgtodot/README.md @@ -0,0 +1,12 @@ +# CGToDot + +CGToDot is a simple tool to generate a DOT file representation of a given call +graph. + +## Usage + +``` +cgtodot [options] +``` + +Use `cgtodot --help` to get a list of available options. diff --git a/tools/cgvisual/README.md b/tools/cgvisual/README.md deleted file mode 100644 index cdd65a97..00000000 --- a/tools/cgvisual/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# CGVisual - -CGVisual is a simple tool to generate a DOT file representation of a given call -graph. - -## Usage - -``` -cgvisual [options] -``` - -Use `cgvisual --help` to get a list of available options. From ed6c843ec0d68610e993941212b30369fc193202 Mon Sep 17 00:00:00 2001 From: gomfol12 Date: Tue, 3 Mar 2026 11:53:23 +0100 Subject: [PATCH 5/6] make use of std::filesystem::path --- tools/cgtodot/CGToDot.cpp | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/tools/cgtodot/CGToDot.cpp b/tools/cgtodot/CGToDot.cpp index 66b3c33d..d1b38f7e 100644 --- a/tools/cgtodot/CGToDot.cpp +++ b/tools/cgtodot/CGToDot.cpp @@ -15,6 +15,16 @@ static auto console = metacg::MCGLogger::instance().getConsole(); static auto errConsole = metacg::MCGLogger::instance().getErrConsole(); +template <> +struct fmt::formatter { + constexpr auto parse(fmt::format_parse_context& ctx) { return ctx.begin(); } + + template + auto format(const std::filesystem::path& p, FormatContext& ctx) const { + return fmt::format_to(ctx.out(), "{}", p.string()); + } +}; + int main(int argc, char* argv[]) { cxxopts::Options options("CGToDot", "Reads a call graph from the specified file and generates a DOT file for visualization."); @@ -22,14 +32,14 @@ int main(int argc, char* argv[]) { // clang-format off options.add_options() ("h,help", "Show this help message") - ("o,output", "Specify output DOT file name", cxxopts::value()->default_value("callgraph.dot")) + ("o,output", "Specify output DOT file name", cxxopts::value()) ("input", "Input call graph file", cxxopts::value()) ; // clang-format on options.parse_positional({"input"}); - auto result = options.parse(argc, argv); + cxxopts::ParseResult result = options.parse(argc, argv); if (result.count("help")) { std::cout << options.help() << std::endl; @@ -42,8 +52,21 @@ int main(int argc, char* argv[]) { return EXIT_FAILURE; } - std::string inputFile = result["input"].as(); - std::string outputFile = result["output"].as(); + std::filesystem::path inputFile = result["input"].as(); + if (!std::filesystem::exists(inputFile)) { + errConsole->error("Specified input file does not exist: {}", inputFile); + return EXIT_FAILURE; + } + + std::filesystem::path outputFile; + if (result.count("output")) { + outputFile = result["output"].as(); + } else { + outputFile = inputFile.stem().string() + ".dot"; + } + if (std::filesystem::exists(outputFile)) { + errConsole->warn("Output file already exists and will be overwritten: {}", outputFile); + } metacg::io::FileSource fs(inputFile); From 13dd0b8134b71ee78b03188a4c0c3d23456ecb74 Mon Sep 17 00:00:00 2001 From: gomfol12 Date: Tue, 3 Mar 2026 12:57:19 +0100 Subject: [PATCH 6/6] add tests for cgtodot --- .github/workflows/mcg-ci.yml | 9 +++- tools/cgtodot/CMakeLists.txt | 9 ++++ tools/cgtodot/test/data/input.json | 29 +++++++++++ tools/cgtodot/test/data/output.dot | 6 +++ tools/cgtodot/test/test_runner.sh.in | 78 ++++++++++++++++++++++++++++ 5 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 tools/cgtodot/test/data/input.json create mode 100644 tools/cgtodot/test/data/output.dot create mode 100755 tools/cgtodot/test/test_runner.sh.in diff --git a/.github/workflows/mcg-ci.yml b/.github/workflows/mcg-ci.yml index 776d638c..8b6db09c 100644 --- a/.github/workflows/mcg-ci.yml +++ b/.github/workflows/mcg-ci.yml @@ -93,6 +93,7 @@ jobs: stat /tmp/metacg/bin/cgquery stat /tmp/metacg/bin/cgformat stat /tmp/metacg/bin/cgconvert + stat /tmp/metacg/bin/cgtodot - name: Check for canonical test graph format uses: maus007/docker-run-action-fork@207a4e2a8ebf7e4b985656ba990b1e53715dce2a with: @@ -196,4 +197,10 @@ jobs: with: image: metacg-devel:latest run: /opt/metacg/build/tools/cgdiff/test/unit/cgdifftests - + - name: Run cgtodot tests + uses: maus007/docker-run-action-fork@207a4e2a8ebf7e4b985656ba990b1e53715dce2a + with: + image: metacg-devel:latest + run: | + cd /opt/metacg/build/tools/cgtodot/ + ctest . --verbose diff --git a/tools/cgtodot/CMakeLists.txt b/tools/cgtodot/CMakeLists.txt index f788008a..564fbac4 100644 --- a/tools/cgtodot/CMakeLists.txt +++ b/tools/cgtodot/CMakeLists.txt @@ -20,3 +20,12 @@ configure_package_config_file( ) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake DESTINATION lib/cmake) + +# tests + +set(CGTODOT_BIN ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_BINARY_NAME}) +set(CGTODOT_TEST_DATA_DIR ${CMAKE_CURRENT_SOURCE_DIR}/test/data) + +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/test/test_runner.sh.in" "test_runner.sh") + +add_test(NAME cgtodot_tests COMMAND ${CMAKE_CURRENT_BINARY_DIR}/test_runner.sh) diff --git a/tools/cgtodot/test/data/input.json b/tools/cgtodot/test/data/input.json new file mode 100644 index 00000000..c1b27b12 --- /dev/null +++ b/tools/cgtodot/test/data/input.json @@ -0,0 +1,29 @@ +{ + "_CG": { + "meta": {}, + "nodes": { + "0": { + "callees": {}, + "functionName": "_QFPprint_stars", + "hasBody": true, + "meta": {}, + "origin": "input.f90" + }, + "1": { + "callees": { "0": {} }, + "functionName": "_QQmain", + "hasBody": true, + "meta": {}, + "origin": "input.f90" + } + } + }, + "_MetaCG": { + "generator": { + "name": "MetaCG", + "sha": "68fb73aebcc0af419653b36a6b5e3e9668408d10", + "version": "0.9" + }, + "version": "4.0" + } +} diff --git a/tools/cgtodot/test/data/output.dot b/tools/cgtodot/test/data/output.dot new file mode 100644 index 00000000..0f99b5f9 --- /dev/null +++ b/tools/cgtodot/test/data/output.dot @@ -0,0 +1,6 @@ +digraph callgraph { + "_QFPprint_stars" + "_QQmain" + + _QQmain -> _QFPprint_stars +} diff --git a/tools/cgtodot/test/test_runner.sh.in b/tools/cgtodot/test/test_runner.sh.in new file mode 100755 index 00000000..9ddaac53 --- /dev/null +++ b/tools/cgtodot/test/test_runner.sh.in @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +# Test runner for CGToDot. +# +# Usage: +# test_runner.sh + +binary="@CGTODOT_BIN@" +data_dir="@CGTODOT_TEST_DATA_DIR@" +input_file="$data_dir/input.json" +expected_output_file="$data_dir/output.dot" + +tests_failed=0 +tests_passed=0 + +fail() +{ + echo "Test failed: $1" + tests_failed=$((tests_failed + 1)) +} + +pass() +{ + echo "Test passed: $1" + tests_passed=$((tests_passed + 1)) +} + +# Test: no input +if "$binary" >/dev/null 2>&1; then + fail "no input" +else + pass "no input" +fi + +# Test: non-existent input file +if "$binary" "non_existent_file.json" >/dev/null 2>&1; then + fail "non-existent input file" +else + pass "non-existent input file" +fi + +# Test: valid input file -> default output +default_output_file="input.dot" +if "$binary" "$input_file" >/dev/null 2>&1; then + if [ -f "$default_output_file" ] && diff -q "$default_output_file" "$expected_output_file"; then + pass "valid input file -> default output" + else + fail "valid input file -> default output" + fi +else + fail "valid input file -> default output" +fi + +rm -f "$default_output_file" + +# Test: option -o +custom_output_file_name="$(mktemp custom_output_file_name_XXXXXX.dot)" +if "$binary" -o "$custom_output_file_name" "$input_file" >/dev/null 2>&1; then + if diff -q "$custom_output_file_name" "$expected_output_file" >/dev/null; then + pass "option -o" + else + fail "option -o" + fi +else + fail "option -o" +fi + +rm -f "$custom_output_file_name" + +echo "" +echo "Tests passed: $tests_passed" +echo "Tests failed: $tests_failed" + +if [ $tests_failed -ne 0 ]; then + exit 1 +else + exit 0 +fi