markdown-run 0.1.11 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.tool-versions +1 -1
- data/CHANGELOG.md +7 -0
- data/README.md +74 -7
- data/Rakefile +64 -0
- data/lib/code_block_helper.rb +15 -7
- data/lib/code_block_parser.rb +11 -4
- data/lib/code_executor.rb +118 -31
- data/lib/dalibo_helper.rb +2 -2
- data/lib/execution_decider.rb +108 -10
- data/lib/flamegraph_helper.rb +39 -0
- data/lib/frontmatter_parser.rb +14 -6
- data/lib/language_configs.rb +30 -22
- data/lib/language_resolver.rb +17 -0
- data/lib/markdown/run/version.rb +1 -1
- data/lib/markdown_file_writer.rb +5 -2
- data/lib/markdown_processor.rb +5 -5
- data/lib/pg_flamegraph_svg.rb +221 -0
- data/lib/postgres_helper.rb +94 -0
- data/lib/result_helper.rb +78 -18
- data/lib/test_silencer.rb +41 -0
- data/markdown-run-sample/markdown-run-sample-flamegraph-20250604-233020-29c2a34ec32b.svg +29 -0
- data/markdown-run-sample.md +9 -0
- metadata +23 -6
@@ -0,0 +1,221 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
class PostgreSQLFlameGraphSVG
|
5
|
+
def initialize(explain_json, width = 1200, height = 600)
|
6
|
+
@explain_data = JSON.parse(explain_json)
|
7
|
+
@width = width
|
8
|
+
@height = height
|
9
|
+
@font_size = 20
|
10
|
+
@min_width = 1
|
11
|
+
@colors = [
|
12
|
+
'#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6',
|
13
|
+
'#1abc9c', '#e67e22', '#95a5a6', '#34495e', '#e91e63'
|
14
|
+
]
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate_svg
|
18
|
+
plan = @explain_data[0]['Plan']
|
19
|
+
|
20
|
+
# Calculate the layout
|
21
|
+
flamegraph_data = build_flamegraph_data(plan)
|
22
|
+
|
23
|
+
# Generate SVG
|
24
|
+
generate_svg_content(flamegraph_data)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def build_flamegraph_data(plan, depth = 0, start_time = 0)
|
30
|
+
node_name = format_node_name(plan)
|
31
|
+
actual_time = plan['Actual Total Time'] || 0
|
32
|
+
|
33
|
+
# Create the current node
|
34
|
+
current_node = {
|
35
|
+
name: node_name,
|
36
|
+
time: actual_time,
|
37
|
+
depth: depth,
|
38
|
+
start: start_time,
|
39
|
+
children: []
|
40
|
+
}
|
41
|
+
|
42
|
+
# Process children
|
43
|
+
if plan['Plans']
|
44
|
+
child_start = start_time
|
45
|
+
plan['Plans'].each do |child_plan|
|
46
|
+
child_node = build_flamegraph_data(child_plan, depth + 1, child_start)
|
47
|
+
current_node[:children] << child_node
|
48
|
+
child_start += child_node[:time]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
current_node
|
53
|
+
end
|
54
|
+
|
55
|
+
def format_node_name(node)
|
56
|
+
node_type = node['Node Type']
|
57
|
+
|
58
|
+
# Add relevant details
|
59
|
+
details = []
|
60
|
+
|
61
|
+
if node['Relation Name']
|
62
|
+
details << node['Relation Name']
|
63
|
+
end
|
64
|
+
|
65
|
+
if node['Index Name']
|
66
|
+
details << "idx:#{node['Index Name']}"
|
67
|
+
end
|
68
|
+
|
69
|
+
if node['Join Type']
|
70
|
+
details << node['Join Type']
|
71
|
+
end
|
72
|
+
|
73
|
+
# Build the final name
|
74
|
+
name = node_type
|
75
|
+
unless details.empty?
|
76
|
+
name += " (#{details.join(', ')})"
|
77
|
+
end
|
78
|
+
|
79
|
+
# Add timing info
|
80
|
+
if node['Actual Total Time']
|
81
|
+
name += " [#{node['Actual Total Time'].round(2)}ms]"
|
82
|
+
end
|
83
|
+
|
84
|
+
name
|
85
|
+
end
|
86
|
+
|
87
|
+
def generate_svg_content(flamegraph_data)
|
88
|
+
max_depth = calculate_max_depth(flamegraph_data)
|
89
|
+
total_time = flamegraph_data[:time]
|
90
|
+
|
91
|
+
svg_height = (max_depth + 1) * (@font_size + 4) + 80
|
92
|
+
|
93
|
+
svg = <<~SVG
|
94
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
95
|
+
<svg width="#{@width}" height="#{svg_height}" xmlns="http://www.w3.org/2000/svg">
|
96
|
+
<style>
|
97
|
+
.frame { stroke: white; stroke-width: 1; cursor: pointer; }
|
98
|
+
.frame:hover { stroke: black; stroke-width: 2; }
|
99
|
+
.frame-text { font-family: monospace; font-size: #{@font_size}px; fill: white; pointer-events: none; }
|
100
|
+
.title { font-family: Arial; font-size: 16px; font-weight: bold; fill: #333; }
|
101
|
+
.subtitle { font-family: Arial; font-size: 12px; fill: #666; }
|
102
|
+
</style>
|
103
|
+
|
104
|
+
<!-- Title -->
|
105
|
+
<text x="#{@width/2}" y="20" class="title" text-anchor="middle">PostgreSQL Query Execution Plan Flamegraph</text>
|
106
|
+
<text x="#{@width/2}" y="35" class="subtitle" text-anchor="middle">Total Execution Time: #{total_time.round(2)}ms</text>
|
107
|
+
|
108
|
+
<!-- Flamegraph -->
|
109
|
+
<g transform="translate(0, 45)">
|
110
|
+
SVG
|
111
|
+
|
112
|
+
# Generate rectangles recursively
|
113
|
+
svg += generate_rectangles(flamegraph_data, total_time, 0)
|
114
|
+
|
115
|
+
svg += <<~SVG
|
116
|
+
</g>
|
117
|
+
</svg>
|
118
|
+
SVG
|
119
|
+
|
120
|
+
svg
|
121
|
+
end
|
122
|
+
|
123
|
+
def generate_rectangles(node, total_time, y_offset)
|
124
|
+
return "" if node[:time] <= 0
|
125
|
+
|
126
|
+
# Calculate dimensions
|
127
|
+
width_ratio = node[:time] / total_time
|
128
|
+
rect_width = [@width * width_ratio, @min_width].max
|
129
|
+
rect_height = @font_size + 4
|
130
|
+
|
131
|
+
x_position = (node[:start] / total_time) * @width
|
132
|
+
y_position = y_offset
|
133
|
+
|
134
|
+
# Choose color based on node type
|
135
|
+
color = get_node_color(node[:name])
|
136
|
+
|
137
|
+
# Generate rectangle and text
|
138
|
+
svg = <<~SVG
|
139
|
+
<rect class="frame"
|
140
|
+
x="#{x_position}"
|
141
|
+
y="#{y_position}"
|
142
|
+
width="#{rect_width}"
|
143
|
+
height="#{rect_height}"
|
144
|
+
fill="#{color}">
|
145
|
+
<title>#{escape_xml(node[:name])}
|
146
|
+
Time: #{node[:time].round(2)}ms
|
147
|
+
Percentage: #{((node[:time] / total_time) * 100).round(1)}%</title>
|
148
|
+
</rect>
|
149
|
+
SVG
|
150
|
+
|
151
|
+
# Add text if rectangle is wide enough
|
152
|
+
if rect_width > 50
|
153
|
+
text_x = x_position + 4
|
154
|
+
text_y = y_position + @font_size + 1
|
155
|
+
|
156
|
+
# Truncate text if necessary
|
157
|
+
display_text = truncate_text(node[:name], rect_width - 8)
|
158
|
+
|
159
|
+
svg += <<~SVG
|
160
|
+
<text class="frame-text" x="#{text_x}" y="#{text_y}">#{escape_xml(display_text)}</text>
|
161
|
+
SVG
|
162
|
+
end
|
163
|
+
|
164
|
+
# Generate children
|
165
|
+
child_y = y_position + rect_height + 2
|
166
|
+
node[:children].each do |child|
|
167
|
+
svg += generate_rectangles(child, total_time, child_y)
|
168
|
+
end
|
169
|
+
|
170
|
+
svg
|
171
|
+
end
|
172
|
+
|
173
|
+
def calculate_max_depth(node, current_depth = 0)
|
174
|
+
max_child_depth = current_depth
|
175
|
+
|
176
|
+
node[:children].each do |child|
|
177
|
+
child_depth = calculate_max_depth(child, current_depth + 1)
|
178
|
+
max_child_depth = [max_child_depth, child_depth].max
|
179
|
+
end
|
180
|
+
|
181
|
+
max_child_depth
|
182
|
+
end
|
183
|
+
|
184
|
+
def get_node_color(node_name)
|
185
|
+
# Color code by operation type
|
186
|
+
case node_name
|
187
|
+
when /Seq Scan/
|
188
|
+
'#e74c3c' # Red - potentially slow
|
189
|
+
when /Index.*Scan/
|
190
|
+
'#2ecc71' # Green - good
|
191
|
+
when /Hash Join|Nested Loop|Merge Join/
|
192
|
+
'#3498db' # Blue - joins
|
193
|
+
when /Sort|Aggregate/
|
194
|
+
'#f39c12' # Orange - processing
|
195
|
+
when /Result/
|
196
|
+
'#95a5a6' # Gray - simple
|
197
|
+
else
|
198
|
+
# Cycle through colors based on hash
|
199
|
+
@colors[node_name.hash.abs % @colors.length]
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def truncate_text(text, max_width)
|
204
|
+
# Rough estimate: 1 character ≈ 7 pixels in monospace
|
205
|
+
max_chars = (max_width / 7).to_i
|
206
|
+
|
207
|
+
if text.length <= max_chars
|
208
|
+
text
|
209
|
+
else
|
210
|
+
text[0, max_chars - 3] + "..."
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def escape_xml(text)
|
215
|
+
text.gsub('&', '&')
|
216
|
+
.gsub('<', '<')
|
217
|
+
.gsub('>', '>')
|
218
|
+
.gsub('"', '"')
|
219
|
+
.gsub("'", ''')
|
220
|
+
end
|
221
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require_relative "test_silencer"
|
2
|
+
|
3
|
+
module PostgresHelper
|
4
|
+
class << self
|
5
|
+
def psql_command
|
6
|
+
@psql_command ||= detect_psql_command
|
7
|
+
end
|
8
|
+
|
9
|
+
def available?
|
10
|
+
return false if psql_command.nil?
|
11
|
+
|
12
|
+
# If using Docker, check required env vars are present
|
13
|
+
if using_docker?
|
14
|
+
has_required_env_vars?
|
15
|
+
else
|
16
|
+
true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def using_docker?
|
21
|
+
cmd = psql_command
|
22
|
+
cmd && cmd.start_with?("docker exec")
|
23
|
+
end
|
24
|
+
|
25
|
+
# Reset cached command detection (for tests)
|
26
|
+
def reset_cache!
|
27
|
+
@psql_command = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate_env_vars!
|
31
|
+
return unless using_docker?
|
32
|
+
|
33
|
+
required_vars = ["PGUSER", "PGDATABASE"]
|
34
|
+
missing_vars = required_vars.reject { |var| ENV[var] }
|
35
|
+
|
36
|
+
if missing_vars.any?
|
37
|
+
TestSilencer.abort_unless_testing "Error: PostgreSQL is running in Docker but required environment variables are missing: #{missing_vars.join(", ")}. Please set these variables before running psql commands."
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def has_required_env_vars?
|
44
|
+
required_vars = ["PGUSER", "PGDATABASE"]
|
45
|
+
required_vars.all? { |var| ENV[var] }
|
46
|
+
end
|
47
|
+
|
48
|
+
def detect_psql_command
|
49
|
+
# Check if psql is available locally
|
50
|
+
return "psql" if system("command -v psql > /dev/null 2>&1")
|
51
|
+
|
52
|
+
# Check if docker is available
|
53
|
+
return nil unless system("command -v docker > /dev/null 2>&1")
|
54
|
+
|
55
|
+
# Find running postgres container
|
56
|
+
container_id = find_postgres_container
|
57
|
+
return nil if container_id.nil? || container_id.empty?
|
58
|
+
|
59
|
+
# Build docker exec command with environment variables passed through
|
60
|
+
env_vars = build_env_vars
|
61
|
+
"docker exec -i -u postgres #{env_vars}#{container_id} psql"
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_env_vars
|
65
|
+
# Pass through PostgreSQL environment variables if they exist
|
66
|
+
pg_env_vars = [ "PGUSER", "PGPASSWORD", "PGDATABASE", "PGHOST", "PGPORT" ]
|
67
|
+
env_flags = pg_env_vars.map do |var|
|
68
|
+
value = ENV[var]
|
69
|
+
"-e #{var}=\"#{value}\" " if value
|
70
|
+
end.compact.join
|
71
|
+
env_flags.empty? ? "" : "#{env_flags} "
|
72
|
+
end
|
73
|
+
|
74
|
+
def find_postgres_container
|
75
|
+
# Try to find a running postgres container
|
76
|
+
# First, check for container with postgres in the name
|
77
|
+
output = `docker ps --filter "ancestor=postgres" --format "{{.ID}}" 2>/dev/null`.strip
|
78
|
+
return output.split("\n").first unless output.empty?
|
79
|
+
|
80
|
+
# Alternative: check for containers with "postgres" in name
|
81
|
+
output = `docker ps --filter "name=postgres" --format "{{.ID}}" 2>/dev/null`.strip
|
82
|
+
return output.split("\n").first unless output.empty?
|
83
|
+
|
84
|
+
# Last resort: check if any running container has psql command
|
85
|
+
containers = `docker ps --format "{{.ID}}" 2>/dev/null`.strip.split("\n")
|
86
|
+
containers.each do |container_id|
|
87
|
+
has_psql = system("docker exec #{container_id} which psql > /dev/null 2>&1")
|
88
|
+
return container_id if has_psql
|
89
|
+
end
|
90
|
+
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/result_helper.rb
CHANGED
@@ -1,4 +1,9 @@
|
|
1
|
+
require_relative "flamegraph_helper"
|
2
|
+
require_relative "test_silencer"
|
3
|
+
|
1
4
|
module ResultHelper
|
5
|
+
include FlamegraphHelper
|
6
|
+
|
2
7
|
private
|
3
8
|
|
4
9
|
def ruby_style_result?(lang)
|
@@ -12,7 +17,7 @@ module ResultHelper
|
|
12
17
|
end
|
13
18
|
|
14
19
|
def result_block_header(lang)
|
15
|
-
ruby_style_result?(lang) ? "```ruby
|
20
|
+
ruby_style_result?(lang) ? "```ruby\n" : "``` {result}\n"
|
16
21
|
end
|
17
22
|
|
18
23
|
def result_block_regex(lang)
|
@@ -20,14 +25,21 @@ module ResultHelper
|
|
20
25
|
# For mermaid, look for existing image tags with .svg extension
|
21
26
|
/^!\[.*\]\(.*\.svg\)$/i
|
22
27
|
elsif ruby_style_result?(lang)
|
28
|
+
# For ruby, check for old-style ```ruby RESULT blocks (for backward compatibility during migration)
|
23
29
|
/^```ruby\s+RESULT$/i
|
24
30
|
else
|
25
|
-
|
31
|
+
/^```\s*\{result\}$/i
|
26
32
|
end
|
27
33
|
end
|
28
34
|
|
29
|
-
def add_result_block(result_output, blank_line_before_new_result)
|
30
|
-
if
|
35
|
+
def add_result_block(result_output, blank_line_before_new_result, closing_line = nil)
|
36
|
+
if ruby_style_result?(@current_block_lang)
|
37
|
+
# For ruby, replace the code block content with xmpfilter output
|
38
|
+
@output_lines << result_output
|
39
|
+
@output_lines << "\n" unless result_output.empty? || result_output.end_with?("\n")
|
40
|
+
@output_lines << (closing_line || "```\n")
|
41
|
+
@output_lines << "\n"
|
42
|
+
elsif mermaid_style_result?(@current_block_lang)
|
31
43
|
# For mermaid, add the image tag directly without a result block
|
32
44
|
@output_lines << "\n" if blank_line_before_new_result.nil?
|
33
45
|
@output_lines << result_output
|
@@ -51,25 +63,34 @@ module ResultHelper
|
|
51
63
|
end
|
52
64
|
|
53
65
|
def handle_existing_ruby_result_block(current_line, file_enum)
|
54
|
-
|
66
|
+
TestSilencer.warn_unless_testing("Found existing '```ruby RESULT' or '```ruby' result block, passing through.")
|
55
67
|
@output_lines << current_line
|
56
68
|
@state = :inside_result_block
|
57
69
|
end
|
58
70
|
|
59
71
|
|
60
|
-
def execute_and_add_result(blank_line_before_new_result)
|
61
|
-
|
72
|
+
def execute_and_add_result(blank_line_before_new_result, closing_line = nil)
|
73
|
+
TestSilencer.warn_unless_testing("Skipping empty code block for language '#{@current_block_lang}'.") && return unless has_content?(@current_code_content)
|
62
74
|
|
63
|
-
|
75
|
+
# For ruby blocks, don't add blank line since we're replacing the code block inline, not adding a RESULT block
|
76
|
+
@output_lines << blank_line_before_new_result if blank_line_before_new_result && !ruby_style_result?(@current_block_lang)
|
64
77
|
|
65
|
-
result_output = CodeExecutor.execute(@current_code_content, @current_block_lang, @temp_dir, @input_file_path, @current_block_explain)
|
78
|
+
result_output = CodeExecutor.execute(@current_code_content, @current_block_lang, @temp_dir, @input_file_path, @current_block_explain, @current_block_flamegraph)
|
66
79
|
|
67
80
|
# Check if result contains a Dalibo link for psql explain queries
|
68
|
-
dalibo_link,
|
81
|
+
dalibo_link, result_after_dalibo = extract_dalibo_link(result_output)
|
82
|
+
|
83
|
+
# Check if result contains a flamegraph link for psql flamegraph queries
|
84
|
+
flamegraph_link, clean_result = extract_flamegraph_link(result_after_dalibo)
|
69
85
|
|
70
86
|
# Add the result block only if result=true (default)
|
71
87
|
if @current_block_result
|
72
|
-
add_result_block(clean_result ||
|
88
|
+
add_result_block(clean_result || result_after_dalibo, blank_line_before_new_result, closing_line)
|
89
|
+
elsif ruby_style_result?(@current_block_lang)
|
90
|
+
# For ruby blocks with result=false, output the original code content without xmpfilter results
|
91
|
+
@output_lines << @current_code_content
|
92
|
+
@output_lines << (closing_line || "```\n")
|
93
|
+
@output_lines << "\n"
|
73
94
|
end
|
74
95
|
|
75
96
|
# Always add Dalibo link if it exists, even when result=false
|
@@ -81,30 +102,62 @@ module ResultHelper
|
|
81
102
|
@output_lines << "\n#{dalibo_link}\n\n"
|
82
103
|
end
|
83
104
|
end
|
105
|
+
|
106
|
+
# Always add flamegraph link if it exists, even when result=false
|
107
|
+
if flamegraph_link
|
108
|
+
# Add appropriate spacing based on whether result block was shown
|
109
|
+
if @current_block_result || dalibo_link
|
110
|
+
@output_lines << "#{flamegraph_link}\n\n"
|
111
|
+
else
|
112
|
+
@output_lines << "\n#{flamegraph_link}\n\n"
|
113
|
+
end
|
114
|
+
end
|
84
115
|
end
|
85
116
|
|
86
|
-
def skip_and_pass_through_result(lines_to_pass_through, file_enum, decision = nil)
|
117
|
+
def skip_and_pass_through_result(lines_to_pass_through, file_enum, decision = nil, closing_line = nil)
|
87
118
|
# Handle run=false case where there are no lines to pass through
|
88
119
|
if lines_to_pass_through.empty?
|
89
|
-
|
120
|
+
TestSilencer.warn_unless_testing("Skipping execution due to run=false option.")
|
121
|
+
# For ruby blocks, we still need to output the code content and closing line
|
122
|
+
if ruby_style_result?(@current_block_lang)
|
123
|
+
@output_lines << @current_code_content
|
124
|
+
@output_lines << (closing_line || "```\n")
|
125
|
+
@output_lines << "\n"
|
126
|
+
end
|
90
127
|
return
|
91
128
|
end
|
92
129
|
|
93
130
|
# Check if this is Dalibo content
|
94
131
|
if decision && decision[:dalibo_content]
|
95
|
-
|
132
|
+
TestSilencer.warn_unless_testing("Found existing Dalibo link for current #{@current_block_lang} block, skipping execution.")
|
96
133
|
@output_lines.concat(lines_to_pass_through)
|
97
134
|
# No additional consumption needed for Dalibo links
|
98
135
|
return
|
99
136
|
end
|
100
137
|
|
138
|
+
# Check if this is flamegraph content
|
139
|
+
if decision && decision[:flamegraph_content]
|
140
|
+
TestSilencer.warn_unless_testing("Found existing flamegraph for current #{@current_block_lang} block, skipping execution.")
|
141
|
+
@output_lines.concat(lines_to_pass_through)
|
142
|
+
# No additional consumption needed for flamegraph links
|
143
|
+
return
|
144
|
+
end
|
145
|
+
|
101
146
|
if mermaid_style_result?(@current_block_lang)
|
102
|
-
|
147
|
+
TestSilencer.warn_unless_testing("Found existing mermaid SVG image for current #{@current_block_lang} block, skipping execution.")
|
103
148
|
@output_lines.concat(lines_to_pass_through)
|
104
149
|
# For mermaid, no additional consumption needed since it's just an image line
|
150
|
+
elsif ruby_style_result?(@current_block_lang)
|
151
|
+
# For ruby blocks with existing old-style RESULT blocks, discard them and output inline results
|
152
|
+
TestSilencer.warn_unless_testing("Found existing ruby result for current #{@current_block_lang} block, skipping execution.")
|
153
|
+
@output_lines << @current_code_content
|
154
|
+
@output_lines << (closing_line || "```\n")
|
155
|
+
@output_lines << "\n"
|
156
|
+
# Always consume and discard the old-style RESULT block
|
157
|
+
consume_and_discard_result_block(file_enum)
|
105
158
|
else
|
106
|
-
lang_specific_result_type =
|
107
|
-
|
159
|
+
lang_specific_result_type = "``` {result}"
|
160
|
+
TestSilencer.warn_unless_testing("Found existing '#{lang_specific_result_type}' block for current #{@current_block_lang} block, skipping execution.")
|
108
161
|
@output_lines.concat(lines_to_pass_through)
|
109
162
|
consume_result_block_content(file_enum)
|
110
163
|
end
|
@@ -116,6 +169,12 @@ module ResultHelper
|
|
116
169
|
end
|
117
170
|
end
|
118
171
|
|
172
|
+
def consume_and_discard_result_block(file_enum)
|
173
|
+
consume_block_lines(file_enum) do |line|
|
174
|
+
# Discard the line - don't add to output
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
119
178
|
def consume_existing_result_block(file_enum, consumed_lines)
|
120
179
|
return if mermaid_style_result?(@current_block_lang)
|
121
180
|
|
@@ -124,6 +183,7 @@ module ResultHelper
|
|
124
183
|
end
|
125
184
|
|
126
185
|
consume_dalibo_link_if_present(file_enum, consumed_lines)
|
186
|
+
consume_flamegraph_link_if_present(file_enum, consumed_lines)
|
127
187
|
end
|
128
188
|
|
129
189
|
|
@@ -135,7 +195,7 @@ module ResultHelper
|
|
135
195
|
break if is_block_end?(result_block_line)
|
136
196
|
end
|
137
197
|
rescue StopIteration
|
138
|
-
|
198
|
+
TestSilencer.warn_unless_testing "Warning: End of file reached while consuming result block."
|
139
199
|
end
|
140
200
|
end
|
141
201
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# Module to provide test-aware warning functionality
|
2
|
+
module TestSilencer
|
3
|
+
# Helper method to detect if we're running in a test environment
|
4
|
+
def self.running_tests?
|
5
|
+
# Check for common test environment indicators
|
6
|
+
@running_tests ||= begin
|
7
|
+
caller.any? { |line| line.include?('/test/') || line.include?('minitest') || line.include?('rspec') } ||
|
8
|
+
defined?(Minitest) ||
|
9
|
+
ENV['RAILS_ENV'] == 'test' ||
|
10
|
+
ENV['RACK_ENV'] == 'test' ||
|
11
|
+
($PROGRAM_NAME.include?('rake') && ARGV.include?('test')) ||
|
12
|
+
$PROGRAM_NAME.include?('rake_test_loader')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Generic warn method that silences output during tests
|
17
|
+
def self.warn_unless_testing(message)
|
18
|
+
warn message unless running_tests?
|
19
|
+
end
|
20
|
+
|
21
|
+
# Silently abort during tests by raising SystemExit without printing the message
|
22
|
+
def self.abort_unless_testing(message)
|
23
|
+
if running_tests?
|
24
|
+
# During tests, raise SystemExit without printing the error message
|
25
|
+
raise SystemExit.new(1)
|
26
|
+
else
|
27
|
+
# In production, use normal abort which prints the message and exits
|
28
|
+
abort message
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Suppress Ruby warnings during tests
|
33
|
+
def self.setup_warning_suppression
|
34
|
+
if running_tests?
|
35
|
+
# Temporarily reduce verbosity during tests
|
36
|
+
original_verbose = $VERBOSE
|
37
|
+
$VERBOSE = nil
|
38
|
+
at_exit { $VERBOSE = original_verbose }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<svg width="1200" height="104" xmlns="http://www.w3.org/2000/svg">
|
3
|
+
<style>
|
4
|
+
.frame { stroke: white; stroke-width: 1; cursor: pointer; }
|
5
|
+
.frame:hover { stroke: black; stroke-width: 2; }
|
6
|
+
.frame-text { font-family: monospace; font-size: 20px; fill: white; pointer-events: none; }
|
7
|
+
.title { font-family: Arial; font-size: 16px; font-weight: bold; fill: #333; }
|
8
|
+
.subtitle { font-family: Arial; font-size: 12px; fill: #666; }
|
9
|
+
</style>
|
10
|
+
|
11
|
+
<!-- Title -->
|
12
|
+
<text x="600" y="20" class="title" text-anchor="middle">PostgreSQL Query Execution Plan Flamegraph</text>
|
13
|
+
<text x="600" y="35" class="subtitle" text-anchor="middle">Total Execution Time: 0.0ms</text>
|
14
|
+
|
15
|
+
<!-- Flamegraph -->
|
16
|
+
<g transform="translate(0, 45)">
|
17
|
+
<rect class="frame"
|
18
|
+
x="0.0"
|
19
|
+
y="0"
|
20
|
+
width="1200.0"
|
21
|
+
height="24"
|
22
|
+
fill="#95a5a6">
|
23
|
+
<title>Result [0.0ms]
|
24
|
+
Time: 0.0ms
|
25
|
+
Percentage: 100.0%</title>
|
26
|
+
</rect>
|
27
|
+
<text class="frame-text" x="4.0" y="21">Result [0.0ms]</text>
|
28
|
+
</g>
|
29
|
+
</svg>
|
data/markdown-run-sample.md
CHANGED
@@ -76,3 +76,12 @@ Page cache spills: 0
|
|
76
76
|
Schema Heap Usage: 0 bytes
|
77
77
|
Statement Heap/Lookaside Usage: 0 bytes
|
78
78
|
```
|
79
|
+
|
80
|
+
```psql rerun flamegraph
|
81
|
+
select 42 as answer;
|
82
|
+
```
|
83
|
+
|
84
|
+
```RESULT
|
85
|
+

|
86
|
+
```
|
87
|
+
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: markdown-run
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aurélien Bottazini
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: rcodetools
|
@@ -80,6 +79,20 @@ dependencies:
|
|
80
79
|
- - ">="
|
81
80
|
- !ruby/object:Gem::Version
|
82
81
|
version: '0'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: simplecov
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0.22'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0.22'
|
83
96
|
description: Run code blocks in Markdown files for Ruby, JavaScript, sqlite, psql,
|
84
97
|
bash, zsh, and mermaid. Insert execution results next to the original code blocks.
|
85
98
|
Generate SVG diagrams from mermaid blocks.
|
@@ -104,14 +117,20 @@ files:
|
|
104
117
|
- lib/dalibo_helper.rb
|
105
118
|
- lib/enum_helper.rb
|
106
119
|
- lib/execution_decider.rb
|
120
|
+
- lib/flamegraph_helper.rb
|
107
121
|
- lib/frontmatter_parser.rb
|
108
122
|
- lib/language_configs.rb
|
123
|
+
- lib/language_resolver.rb
|
109
124
|
- lib/markdown/run/version.rb
|
110
125
|
- lib/markdown_file_writer.rb
|
111
126
|
- lib/markdown_processor.rb
|
112
127
|
- lib/markdown_run.rb
|
128
|
+
- lib/pg_flamegraph_svg.rb
|
129
|
+
- lib/postgres_helper.rb
|
113
130
|
- lib/result_helper.rb
|
131
|
+
- lib/test_silencer.rb
|
114
132
|
- markdown-run-sample.md
|
133
|
+
- markdown-run-sample/markdown-run-sample-flamegraph-20250604-233020-29c2a34ec32b.svg
|
115
134
|
homepage: https://github.com/aurelienbottazini/markdown-run
|
116
135
|
licenses:
|
117
136
|
- MIT
|
@@ -119,7 +138,6 @@ metadata:
|
|
119
138
|
homepage_uri: https://github.com/aurelienbottazini/markdown-run
|
120
139
|
source_code_uri: https://github.com/aurelienbottazini/markdown-run
|
121
140
|
changelog_uri: https://github.com/aurelienbottazini/markdown-run/blob/main/CHANGELOG.md
|
122
|
-
post_install_message:
|
123
141
|
rdoc_options: []
|
124
142
|
require_paths:
|
125
143
|
- lib
|
@@ -134,8 +152,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
134
152
|
- !ruby/object:Gem::Version
|
135
153
|
version: '0'
|
136
154
|
requirements: []
|
137
|
-
rubygems_version: 3.
|
138
|
-
signing_key:
|
155
|
+
rubygems_version: 3.6.9
|
139
156
|
specification_version: 4
|
140
157
|
summary: Run code blocks in Markdown files
|
141
158
|
test_files: []
|