Skip to content

Commit 56ed298

Browse files
authored
Merge pull request #800 from elbeno/debug-flowgraph-target
✨ Generate flow diagram from code non-invasively
2 parents 7a8cdc8 + fe1eb93 commit 56ed298

File tree

9 files changed

+208
-2
lines changed

9 files changed

+208
-2
lines changed

.github/workflows/unit_tests.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,17 @@ jobs:
139139
${{ matrix.install }}
140140
sudo apt install -y ninja-build python3-venv python3-pip
141141
142+
- name: Setup Node.js
143+
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
144+
with:
145+
node-version: 22
146+
147+
- name: Install Asciidoctor & Mermaid
148+
run: |
149+
npm install -g @mermaid-js/[email protected]
150+
npx puppeteer browsers install chrome-headless-shell
151+
sudo gem install asciidoctor asciidoctor-diagram rouge
152+
142153
- name: Restore CPM cache
143154
env:
144155
cache-name: cpm-cache-0

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ find_package(
2020
COMPONENTS Interpreter
2121
REQUIRED)
2222

23+
include(cmake/debug_flow.cmake)
2324
include(cmake/string_catalog.cmake)
2425

2526
add_versioned_package("gh:boostorg/mp11#boost-1.83.0")

cmake/debug_flow.cmake

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
if(COMMAND generate_flow_graph)
2+
return()
3+
endif()
4+
5+
function(generate_flow_graph)
6+
cmake_parse_arguments(FG "" "TARGET" "" ${ARGN})
7+
message(
8+
STATUS
9+
"generate_flow_graph(${FG_TARGET}) is disabled because mmdc was not found."
10+
)
11+
endfunction()
12+
13+
find_program(MMDC_PROGRAM "mmdc")
14+
if(MMDC_PROGRAM)
15+
message(STATUS "mmdc found at ${MMDC_PROGRAM}.")
16+
else()
17+
message(STATUS "mmdc not found. generate_flow_graph will not be available.")
18+
return()
19+
endif()
20+
21+
function(copy_target_property OLD NEW PROP)
22+
get_target_property(val ${OLD} ${PROP})
23+
if(NOT "${val}" STREQUAL "val-NOTFOUND")
24+
set_target_properties(${NEW} PROPERTIES "${PROP}" "${val}")
25+
endif()
26+
endfunction(copy_target_property)
27+
28+
function(copy_target_properties OLD NEW)
29+
foreach(prop ${ARGN})
30+
copy_target_property(${OLD} ${NEW} ${prop})
31+
copy_target_property(${OLD} ${NEW} INTERFACE_${prop})
32+
endforeach(prop)
33+
endfunction(copy_target_properties)
34+
35+
function(make_flow_debug_target OLD NEW)
36+
add_library(${NEW} STATIC EXCLUDE_FROM_ALL
37+
$<TARGET_PROPERTY:${FG_TARGET},SOURCES>)
38+
copy_target_properties(
39+
${OLD}
40+
${NEW}
41+
COMPILE_DEFINITIONS
42+
COMPILE_FEATURES
43+
COMPILE_OPTIONS
44+
INCLUDE_DIRECTORIES
45+
LINK_LIBRARIES
46+
PRECOMPILE_HEADERS)
47+
endfunction()
48+
49+
function(generate_flow_graph)
50+
set(oneValueArgs TARGET FLOW_NAME START_NODE END_NODE)
51+
cmake_parse_arguments(FG "" "${oneValueArgs}" "" ${ARGN})
52+
set(target_stem "${FG_TARGET}.${FG_FLOW_NAME}")
53+
54+
# generate a header that overrides the flow service
55+
get_filename_component(header "${target_stem}.debug.hpp" ABSOLUTE BASE_DIR
56+
${CMAKE_CURRENT_BINARY_DIR})
57+
set(FLOW_NAME ${FG_FLOW_NAME})
58+
set(START_NODE ${FG_START_NODE})
59+
set(END_NODE ${FG_END_NODE})
60+
set(RENDERER "mermaid")
61+
configure_file("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/debug_flow.hpp.in"
62+
${header} @ONLY)
63+
64+
# set up a library that injects the header
65+
set(injection_lib "${target_stem}.lib")
66+
add_library(${injection_lib} INTERFACE)
67+
target_compile_options(${injection_lib} INTERFACE -include ${header})
68+
69+
# set up a new target from the existing one
70+
set(debug_target "${target_stem}.bin")
71+
make_flow_debug_target(${FG_TARGET} ${debug_target})
72+
target_link_libraries(${debug_target} PRIVATE ${injection_lib})
73+
74+
# generate the graph source
75+
set(graph_stem "${target_stem}.graph")
76+
set(graph_source "${graph_stem}.mmd")
77+
set(graph "${graph_stem}.svg")
78+
set(awk_gcc_cmd "awk '{gsub(\"\\\\\\\\012\",\"\\n\")}\;1'")
79+
set(awk_clang_cmd "awk '{gsub(\"\\\\\\\\n\",\"\\n\")}\;1'")
80+
add_custom_command(
81+
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${graph_source}
82+
DEPENDS $<TARGET_PROPERTY:${debug_target},SOURCES>
83+
DEPENDS ${header}
84+
COMMAND
85+
${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target
86+
${debug_target} | bash -c ${awk_gcc_cmd} | bash -c ${awk_clang_cmd}
87+
| tac | sed -n "/^~~~~END/,/^~~~~START/{p;/^~~~~START/q}" | tac |
88+
tail -n +2 | head -n -1 >
89+
"${CMAKE_CURRENT_BINARY_DIR}/${graph_source}"
90+
VERBATIM)
91+
92+
# build the graph from the source
93+
if(EXISTS "${CMAKE_SOURCE_DIR}/docs/mermaid.conf")
94+
set(mm_conf "-c;${CMAKE_SOURCE_DIR}/docs/mermaid.conf")
95+
endif()
96+
if(EXISTS "${CMAKE_SOURCE_DIR}/docs/puppeteer_config.json")
97+
set(puppeteer_conf "-p;${CMAKE_SOURCE_DIR}/docs/puppeteer_config.json")
98+
endif()
99+
add_custom_command(
100+
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${graph}
101+
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/${graph_source}
102+
COMMAND
103+
${MMDC_PROGRAM} -i "${CMAKE_CURRENT_BINARY_DIR}/${graph_source}" -o
104+
"${CMAKE_CURRENT_BINARY_DIR}/${graph}" ${mm_conf} ${puppeteer_conf}
105+
COMMAND_EXPAND_LISTS)
106+
add_custom_target("${graph_stem}"
107+
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/${graph})
108+
endfunction()

cmake/debug_flow.hpp.in

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#include <flow/debug_builder.hpp>
2+
#include <flow/service.hpp>
3+
#include <flow/viz_builder.hpp>
4+
5+
namespace flow {
6+
template <stdx::ct_string Start, stdx::ct_string End, typename Renderer>
7+
using debug_service
8+
= service_for<builder_for<debug_builder<Start, End, Renderer>>>;
9+
10+
template <typename LogPolicy>
11+
struct service<"@FLOW_NAME@", LogPolicy>
12+
: debug_service<"@START_NODE@", "@END_NODE@", @RENDERER@> {};
13+
} // namespace flow

include/flow/debug_builder.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ struct debug_builder {
4545

4646
using viz_t = viz_builder<name, Renderer>;
4747
STATIC_ASSERT(
48-
false, "subgraph debug\n\n{}",
48+
false, "subgraph debug\n~~~~START\n{}~~~~END",
4949
(viz_t::template visualize<subgraph_nodes_t, subgraph_edges_t>()));
5050
}
5151

include/flow/service.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@ template <typename Builder> struct service_for {
2222
};
2323

2424
template <stdx::ct_string Name = "", typename LogPolicy = log_policy_t<Name>>
25-
using service = service_for<builder<Name, LogPolicy>>;
25+
struct service : service_for<builder<Name, LogPolicy>> {};
2626
} // namespace flow

test/flow/CMakeLists.txt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,45 @@ add_tests(
2121
cib)
2222

2323
add_subdirectory(fail)
24+
25+
function(add_debug_flow_test)
26+
add_executable(debug_flow EXCLUDE_FROM_ALL debug_flow.cpp)
27+
target_link_libraries(debug_flow PRIVATE cib)
28+
generate_flow_graph(
29+
TARGET
30+
debug_flow
31+
FLOW_NAME
32+
test
33+
START_NODE
34+
b
35+
END_NODE
36+
c)
37+
38+
if(TARGET debug_flow.test.graph)
39+
add_custom_target(run_debug_flow_test DEPENDS debug_flow_test.passed)
40+
add_custom_command(
41+
OUTPUT debug_flow_test.passed
42+
COMMAND diff ${CMAKE_CURRENT_BINARY_DIR}/debug_flow.test.graph.mmd
43+
${CMAKE_CURRENT_SOURCE_DIR}/debug_flow.test.graph.mmd
44+
COMMAND ${CMAKE_COMMAND} "-E" "touch" "debug_flow_test.passed"
45+
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/debug_flow.test.graph.mmd
46+
${CMAKE_CURRENT_SOURCE_DIR}/debug_flow.test.graph.mmd)
47+
add_dependencies(${INFRA_TARGET_NAMESPACE}build_unit_tests
48+
run_debug_flow_test)
49+
add_dependencies(${INFRA_TARGET_NAMESPACE}cpp_tests run_debug_flow_test)
50+
51+
add_test(
52+
NAME debug_flow_test
53+
COMMAND diff ${CMAKE_CURRENT_BINARY_DIR}/debug_flow.test.graph.mmd
54+
${CMAKE_CURRENT_SOURCE_DIR}/debug_flow.test.graph.mmd)
55+
endif()
56+
endfunction()
57+
58+
if(${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang" AND ${CMAKE_CXX_COMPILER_VERSION}
59+
VERSION_GREATER_EQUAL 15)
60+
add_debug_flow_test()
61+
endif()
62+
if(${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU" AND ${CMAKE_CXX_COMPILER_VERSION}
63+
VERSION_GREATER_EQUAL 13.2)
64+
add_debug_flow_test()
65+
endif()

test/flow/debug_flow.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#include <cib/cib.hpp>
2+
#include <flow/flow.hpp>
3+
4+
namespace {
5+
using namespace flow::literals;
6+
7+
constexpr auto a = flow::action<"a">([] {});
8+
constexpr auto b = flow::action<"b">([] {});
9+
constexpr auto c = flow::action<"c">([] {});
10+
constexpr auto d = flow::action<"d">([] {});
11+
12+
struct DebugFlow : public flow::service<"test"> {};
13+
struct DebugConfig {
14+
constexpr static auto config = cib::config(
15+
cib::exports<DebugFlow>, cib::extend<DebugFlow>(*a, *b, *c, *d),
16+
cib::extend<DebugFlow>(a >> b >> c >> d));
17+
};
18+
} // namespace
19+
20+
auto main() -> int {
21+
using namespace std::string_view_literals;
22+
cib::nexus<DebugConfig> nexus{};
23+
nexus.service<DebugFlow>();
24+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
title: b -> c
3+
---
4+
flowchart TD
5+
_b(b)
6+
_c(c)
7+
_b --> _c

0 commit comments

Comments
 (0)