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/CMakeLists.txt b/tools/CMakeLists.txt index 6ddc252a..02ac6fe7 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(cgtodot) if(BUILD_CAGE) add_subdirectory(cage) endif() diff --git a/tools/cgtodot/CGToDot.cpp b/tools/cgtodot/CGToDot.cpp new file mode 100644 index 00000000..d1b38f7e --- /dev/null +++ b/tools/cgtodot/CGToDot.cpp @@ -0,0 +1,99 @@ +/** + * 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 + */ + +#include "Callgraph.h" +#include "DotIO.h" +#include "LoggerUtil.h" +#include "io/MCGReader.h" + +#include +#include + +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."); + + // clang-format off + options.add_options() + ("h,help", "Show this help message") + ("o,output", "Specify output DOT file name", cxxopts::value()) + ("input", "Input call graph file", cxxopts::value()) + ; + // clang-format on + + options.parse_positional({"input"}); + + cxxopts::ParseResult result = options.parse(argc, argv); + + if (result.count("help")) { + std::cout << options.help() << std::endl; + return EXIT_SUCCESS; + } + + if (!result.count("input")) { + errConsole->error("No input call graph file specified."); + std::cout << options.help() << std::endl; + return EXIT_FAILURE; + } + + 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); + + auto mcgReader = metacg::io::createReader(fs); + if (!mcgReader) { + errConsole->error("Failed to create MCG reader for file: {}", inputFile); + return EXIT_FAILURE; + } + + auto cg = mcgReader->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; +} diff --git a/tools/cgtodot/CMakeLists.txt b/tools/cgtodot/CMakeLists.txt new file mode 100644 index 00000000..564fbac4 --- /dev/null +++ b/tools/cgtodot/CMakeLists.txt @@ -0,0 +1,31 @@ +set(PROJECT_NAME CGToDot) +set(TARGETS_EXPORT_NAME ${PROJECT_NAME}-target) +set(PROJECT_BINARY_NAME cgtodot) + +add_executable(${PROJECT_BINARY_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/CGToDot.cpp) + +add_metacg(${PROJECT_BINARY_NAME}) +add_cxxopts(${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) + +# 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/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/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