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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72c9c060b9169413ee686c9a4958b36b11d5271a4222d598bfe0c847364488e0
|
4
|
+
data.tar.gz: 56bfea5eb072bfbf7db692a6df64c08e901f7a77551f2b17491a4a84201220fb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f4b2c6aa7b3fec529749f56e4a64c186060097b94f64678c96742759d51f4b7dbb1d2014c98abb4ff047f8ba62ef15d1f32612e2b19cbdafa480664f3556a420
|
7
|
+
data.tar.gz: a7bb6b114beaea5f0857534cee766f9dcb157ab39949f433a5965acb558996f30a3fcbd6a13a3df3a264ddf64b1115dab089af7b674b68195467b7a2e482ef5f
|
data/.tool-versions
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby 3.
|
1
|
+
ruby 3.4.6
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.1.12] - 2025-06-04
|
4
|
+
|
5
|
+
- Added `flamegraph` option for psql code blocks to generate PostgreSQL query execution plan flamegraphs as SVG images
|
6
|
+
- PostgreSQL flamegraphs provide interactive, color-coded visualization of query performance with hover tooltips
|
7
|
+
- Flamegraph SVG files follow same directory structure as mermaid diagrams (organized by markdown file basename)
|
8
|
+
- Added flamegraph option support to frontmatter defaults and language-specific configurations
|
9
|
+
|
3
10
|
## [0.1.11] - 2025-06-04
|
4
11
|
|
5
12
|
- options are customizable with the yaml frontmatter
|
data/README.md
CHANGED
@@ -55,10 +55,21 @@ example vscode keybinding
|
|
55
55
|
|
56
56
|
### Code block options
|
57
57
|
|
58
|
+
Options are specified using Pandoc-style curly braces after the language identifier:
|
59
|
+
|
60
|
+
```markdown
|
61
|
+
```ruby {rerun=true}
|
62
|
+
puts "Hello World"
|
63
|
+
```
|
64
|
+
```
|
65
|
+
|
66
|
+
**Available options:**
|
67
|
+
|
58
68
|
- `run=true` or `run=false` to control whether a code block should be executed at all. `run=true` is the default if not specified
|
59
69
|
- `rerun=true` or `rerun=false` to control whether a code block should be re-executed if a result block already exists. `rerun=false` is the default if not specified
|
60
70
|
- `result=true` or `result=false` to control whether the result block should be displayed after execution. `result=true` is the default if not specified. When `result=false`, the code still executes but the result block is hidden
|
61
71
|
- `explain=true` or `explain=false` for psql code blocks to generate query execution plans with Dalibo visualization links. `explain=false` is the default if not specified
|
72
|
+
- `flamegraph=true` or `flamegraph=false` for psql code blocks to generate PostgreSQL query execution plan flamegraphs as SVG images. `flamegraph=false` is the default if not specified
|
62
73
|
|
63
74
|
Options can be combined. If `run=false` is specified, the code block will not execute regardless of the `rerun` setting. The `result` option only affects display of the result block, not code execution.
|
64
75
|
|
@@ -70,40 +81,87 @@ Options can also be specified using standalone syntax without explicit `=true`:
|
|
70
81
|
- `rerun` is equivalent to `rerun=true`
|
71
82
|
- `result` is equivalent to `result=true`
|
72
83
|
- `explain` is equivalent to `explain=true`
|
84
|
+
- `flamegraph` is equivalent to `flamegraph=true`
|
73
85
|
|
74
86
|
Explicit assignments (e.g., `run=false`) take precedence over standalone options.
|
75
87
|
|
76
88
|
Examples:
|
77
89
|
|
78
|
-
```js run=false
|
90
|
+
```js {run=false}
|
79
91
|
console.log("This will not execute at all");
|
80
92
|
```
|
81
93
|
|
82
|
-
```js rerun
|
94
|
+
```js {rerun}
|
83
95
|
console.log("This will re-execute even if result exists");
|
84
96
|
```
|
85
97
|
|
86
|
-
```js run=true rerun=false
|
98
|
+
```js {run=true rerun=false}
|
87
99
|
console.log("This will execute only if no result exists");
|
88
100
|
```
|
89
101
|
|
90
|
-
```ruby result=false run
|
102
|
+
```ruby {result=false run}
|
91
103
|
puts "This executes but the result block is hidden"
|
92
104
|
```
|
93
105
|
|
94
|
-
```psql explain
|
106
|
+
```psql {explain}
|
95
107
|
SELECT * FROM users WHERE id = 1;
|
96
108
|
```
|
97
109
|
|
98
|
-
```psql explain=true
|
110
|
+
```psql {explain=true}
|
99
111
|
EXPLAIN (ANALYZE) SELECT * FROM large_table;
|
100
112
|
```
|
101
113
|
|
102
|
-
```psql
|
114
|
+
```psql {flamegraph}
|
115
|
+
SELECT u.name, COUNT(o.id) as order_count
|
116
|
+
FROM users u
|
117
|
+
LEFT JOIN orders o ON u.id = o.user_id
|
118
|
+
WHERE u.created_at > '2024-01-01'
|
119
|
+
GROUP BY u.id, u.name
|
120
|
+
ORDER BY order_count DESC
|
121
|
+
LIMIT 10;
|
122
|
+
```
|
123
|
+
|
124
|
+
```psql {flamegraph=true result=false}
|
125
|
+
-- This will generate a flamegraph but hide the JSON result block
|
126
|
+
SELECT * FROM complex_query_with_joins;
|
127
|
+
```
|
128
|
+
|
129
|
+
```psql {result=false explain}
|
103
130
|
SELECT * FROM large_table;
|
104
131
|
-- This will execute the explain query and show the Dalibo link but hide the result block
|
105
132
|
```
|
106
133
|
|
134
|
+
### PostgreSQL Flamegraphs
|
135
|
+
|
136
|
+
PostgreSQL flamegraph blocks generate interactive SVG flamegraphs from query execution plans:
|
137
|
+
|
138
|
+
```psql {flamegraph}
|
139
|
+
SELECT users.*, orders.total
|
140
|
+
FROM users
|
141
|
+
JOIN orders ON users.id = orders.user_id
|
142
|
+
WHERE users.created_at > '2024-01-01';
|
143
|
+
```
|
144
|
+
|
145
|
+
This generates:
|
146
|
+
|
147
|
+
- An SVG flamegraph file in a directory named after the markdown file
|
148
|
+
- Filename format: `my-document-flamegraph-20250118-143022-a1b2c3.svg`
|
149
|
+
- Embedded image tag: ``
|
150
|
+
|
151
|
+
**Flamegraph features:**
|
152
|
+
|
153
|
+
- **Interactive**: Hover over rectangles to see detailed timing information
|
154
|
+
- **Color-coded**: Different colors for operation types (red=seq scans, green=index scans, blue=joins, etc.)
|
155
|
+
- **Hierarchical**: Shows query plan structure visually
|
156
|
+
- **Performance insights**: Width represents execution time, making bottlenecks immediately visible
|
157
|
+
|
158
|
+
**What flamegraphs help identify:**
|
159
|
+
|
160
|
+
- Slow operations (widest rectangles)
|
161
|
+
- Query plan structure (nested relationships)
|
162
|
+
- Inefficient operations (color-coded by type)
|
163
|
+
- Execution time distribution across plan nodes
|
164
|
+
|
107
165
|
### Mermaid diagrams
|
108
166
|
|
109
167
|
Mermaid blocks generate SVG files and insert markdown image tags:
|
@@ -163,6 +221,7 @@ markdown-run:
|
|
163
221
|
- `rerun`: Control whether to re-execute if result exists (default: `false`)
|
164
222
|
- `result`: Control whether to show result blocks (default: `true`)
|
165
223
|
- `explain`: For psql blocks, generate explain plans (default: `false`)
|
224
|
+
- `flamegraph`: For psql blocks, generate flamegraph SVGs (default: `false`)
|
166
225
|
|
167
226
|
**Examples:**
|
168
227
|
|
@@ -184,6 +243,14 @@ markdown-run:
|
|
184
243
|
explain: true
|
185
244
|
```
|
186
245
|
|
246
|
+
Enable flamegraphs by default for all psql blocks:
|
247
|
+
|
248
|
+
```yaml
|
249
|
+
markdown-run:
|
250
|
+
psql:
|
251
|
+
flamegraph: true
|
252
|
+
```
|
253
|
+
|
187
254
|
Language-specific settings override global defaults:
|
188
255
|
|
189
256
|
```yaml
|
data/Rakefile
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
1
2
|
require "rake/testtask"
|
2
3
|
|
3
4
|
Rake::TestTask.new(:test) do |t|
|
@@ -19,4 +20,67 @@ task :flog_detailed do
|
|
19
20
|
system("flog -d lib/ exe/ test/")
|
20
21
|
end
|
21
22
|
|
23
|
+
desc "Run tests with detailed timing information"
|
24
|
+
task :test_profile do
|
25
|
+
require 'benchmark'
|
26
|
+
|
27
|
+
puts "Running tests with detailed profiling..."
|
28
|
+
|
29
|
+
# Run tests with verbose output and capture timing
|
30
|
+
output = `bundle exec rake test TESTOPTS="-v" 2>&1`
|
31
|
+
|
32
|
+
# Extract test timings
|
33
|
+
timings = []
|
34
|
+
output.scan(/^(.+) = ([0-9]+\.[0-9]+) s = \.$/) do |test_name, time|
|
35
|
+
timings << [test_name, time.to_f]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Sort by time (descending)
|
39
|
+
timings.sort_by! { |_, time| -time }
|
40
|
+
|
41
|
+
puts "\n" + "="*80
|
42
|
+
puts "TOP 15 SLOWEST TESTS"
|
43
|
+
puts "="*80
|
44
|
+
|
45
|
+
timings.first(15).each_with_index do |(test_name, time), index|
|
46
|
+
test_display = test_name.length > 60 ? test_name[0...60] + "..." : test_name
|
47
|
+
printf "%2d. %-63s %6.2f s\n", index + 1, test_display, time
|
48
|
+
end
|
49
|
+
|
50
|
+
puts "\n" + "="*80
|
51
|
+
puts "SUMMARY"
|
52
|
+
puts "="*80
|
53
|
+
total_time = timings.sum { |_, time| time }
|
54
|
+
slow_tests = timings.select { |_, time| time > 0.1 }
|
55
|
+
|
56
|
+
puts "Total test time: #{total_time.round(2)} seconds"
|
57
|
+
puts "Tests slower than 0.1s: #{slow_tests.count}"
|
58
|
+
puts "Time spent in slow tests: #{slow_tests.sum { |_, time| time }.round(2)} seconds (#{((slow_tests.sum { |_, time| time } / total_time) * 100).round(1)}%)"
|
59
|
+
end
|
60
|
+
|
61
|
+
desc "Release"
|
62
|
+
task :release do
|
63
|
+
`gem bump`
|
64
|
+
`bundle`
|
65
|
+
`git commit --amend`
|
66
|
+
`git push`
|
67
|
+
`git push --tags`
|
68
|
+
`gem release`
|
69
|
+
end
|
70
|
+
|
71
|
+
# Coverage task
|
72
|
+
desc "Run tests with SimpleCov coverage report"
|
73
|
+
task :coverage do
|
74
|
+
ENV['COVERAGE'] = 'true'
|
75
|
+
Rake::Task[:test].invoke
|
76
|
+
|
77
|
+
puts "\n🎯 Coverage Report Generated!"
|
78
|
+
puts "📊 Open coverage/index.html to view detailed coverage report"
|
79
|
+
|
80
|
+
# Try to open coverage report automatically (works on macOS)
|
81
|
+
if RUBY_PLATFORM.include?('darwin')
|
82
|
+
system('open coverage/index.html')
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
22
86
|
task default: :test
|
data/lib/code_block_helper.rb
CHANGED
@@ -10,16 +10,18 @@ module CodeBlockHelper
|
|
10
10
|
@current_block_rerun = false
|
11
11
|
@current_block_run = true
|
12
12
|
@current_block_explain = false
|
13
|
+
@current_block_flamegraph = false
|
13
14
|
@current_block_result = true
|
14
15
|
end
|
15
16
|
|
16
17
|
|
17
18
|
def start_code_block(current_line, lang, options_string = nil)
|
18
19
|
@output_lines << current_line
|
19
|
-
@current_block_lang = resolve_language(lang)
|
20
|
+
@current_block_lang = @language_resolver.resolve_language(lang)
|
20
21
|
@current_block_rerun = @code_block_parser.parse_rerun_option(options_string, @current_block_lang)
|
21
22
|
@current_block_run = @code_block_parser.parse_run_option(options_string, @current_block_lang)
|
22
23
|
@current_block_explain = @code_block_parser.parse_explain_option(options_string, @current_block_lang)
|
24
|
+
@current_block_flamegraph = @code_block_parser.parse_flamegraph_option(options_string, @current_block_lang)
|
23
25
|
@current_block_result = @code_block_parser.parse_result_option(options_string, @current_block_lang)
|
24
26
|
@state = :inside_code_block
|
25
27
|
@current_code_content = ""
|
@@ -27,27 +29,33 @@ module CodeBlockHelper
|
|
27
29
|
|
28
30
|
def accumulate_code_content(current_line)
|
29
31
|
@current_code_content += current_line
|
30
|
-
|
32
|
+
# For ruby blocks, don't output code content yet - we'll replace it with xmpfilter output
|
33
|
+
unless ruby_style_result?(@current_block_lang)
|
34
|
+
@output_lines << current_line
|
35
|
+
end
|
31
36
|
end
|
32
37
|
|
33
38
|
def end_code_block(current_line, file_enum)
|
34
|
-
|
39
|
+
# For ruby blocks, don't output closing ``` yet - we'll add it after the xmpfilter output
|
40
|
+
unless ruby_style_result?(@current_block_lang)
|
41
|
+
@output_lines << current_line
|
42
|
+
end
|
35
43
|
|
36
44
|
decision = decide_execution(file_enum)
|
37
45
|
|
38
46
|
if decision[:execute]
|
39
47
|
# If we consumed lines for rerun, don't add them to output (they'll be replaced)
|
40
|
-
execute_and_add_result(decision[:blank_line])
|
48
|
+
execute_and_add_result(decision[:blank_line], current_line)
|
41
49
|
else
|
42
|
-
skip_and_pass_through_result(decision[:lines_to_pass_through], file_enum, decision)
|
50
|
+
skip_and_pass_through_result(decision[:lines_to_pass_through], file_enum, decision, current_line)
|
43
51
|
end
|
44
52
|
|
45
53
|
reset_code_block_state
|
46
54
|
end
|
47
55
|
|
48
56
|
def decide_execution(file_enum)
|
49
|
-
decider = ExecutionDecider.new(@current_block_run, @current_block_rerun, @current_block_lang, @current_block_explain, @current_block_result)
|
50
|
-
decision = decider.decide(file_enum, method(:result_block_regex))
|
57
|
+
decider = ExecutionDecider.new(@current_block_run, @current_block_rerun, @current_block_lang, @current_block_explain, @current_block_flamegraph, @current_block_result)
|
58
|
+
decision = decider.decide(file_enum, method(:result_block_regex), @current_code_content)
|
51
59
|
|
52
60
|
# Handle the consume_existing flag for rerun scenarios
|
53
61
|
if decision[:consume_existing]
|
data/lib/code_block_parser.rb
CHANGED
@@ -1,13 +1,15 @@
|
|
1
1
|
require_relative "language_configs"
|
2
2
|
|
3
3
|
class CodeBlockParser
|
4
|
-
# Code block header pattern: ```language options
|
5
|
-
|
4
|
+
# Code block header pattern: ```language {options}
|
5
|
+
# Supports both ```language and ```language {options} formats
|
6
|
+
CODE_BLOCK_START_PATTERN = /^```(\w+)(?:\s*\{(.*)\})?$/i
|
6
7
|
RUBY_RESULT_BLOCK_PATTERN = /^```ruby\s+RESULT$/i
|
7
8
|
BLOCK_END_PATTERN = "```"
|
8
9
|
|
9
|
-
def initialize(frontmatter_parser)
|
10
|
+
def initialize(frontmatter_parser, language_resolver)
|
10
11
|
@frontmatter_parser = frontmatter_parser
|
12
|
+
@language_resolver = language_resolver
|
11
13
|
end
|
12
14
|
|
13
15
|
def parse_code_block_header(line)
|
@@ -49,6 +51,11 @@ class CodeBlockParser
|
|
49
51
|
parse_boolean_option(options_string, "explain", default_value)
|
50
52
|
end
|
51
53
|
|
54
|
+
def parse_flamegraph_option(options_string, language = nil)
|
55
|
+
default_value = @frontmatter_parser.get_default_value("flamegraph", language, false)
|
56
|
+
parse_boolean_option(options_string, "flamegraph", default_value)
|
57
|
+
end
|
58
|
+
|
52
59
|
def parse_result_option(options_string, language = nil)
|
53
60
|
default_value = @frontmatter_parser.get_default_value("result", language, true)
|
54
61
|
parse_boolean_option(options_string, "result", default_value)
|
@@ -57,7 +64,7 @@ class CodeBlockParser
|
|
57
64
|
private
|
58
65
|
|
59
66
|
def resolve_language(lang)
|
60
|
-
@
|
67
|
+
@language_resolver.resolve_language(lang)
|
61
68
|
end
|
62
69
|
|
63
70
|
def parse_boolean_option(options_string, option_name, default_value)
|
data/lib/code_executor.rb
CHANGED
@@ -1,54 +1,59 @@
|
|
1
1
|
require "tempfile"
|
2
2
|
require "open3"
|
3
3
|
require_relative "language_configs"
|
4
|
+
require_relative "test_silencer"
|
4
5
|
|
5
6
|
class CodeExecutor
|
6
|
-
def self.execute(code_content, lang, temp_dir, input_file_path = nil, explain = false)
|
7
|
-
new.execute(code_content, lang, temp_dir, input_file_path, explain)
|
7
|
+
def self.execute(code_content, lang, temp_dir, input_file_path = nil, explain = false, flamegraph = false)
|
8
|
+
new.execute(code_content, lang, temp_dir, input_file_path, explain, flamegraph)
|
8
9
|
end
|
9
10
|
|
10
|
-
def execute(code_content, lang, temp_dir, input_file_path = nil, explain = false)
|
11
|
+
def execute(code_content, lang, temp_dir, input_file_path = nil, explain = false, flamegraph = false)
|
11
12
|
lang_key = lang.downcase
|
12
13
|
lang_config = SUPPORTED_LANGUAGES[lang_key]
|
13
14
|
|
14
15
|
return handle_unsupported_language(lang) unless lang_config
|
15
16
|
|
16
|
-
|
17
|
+
TestSilencer.warn_unless_testing("Executing #{lang_key} code block...")
|
17
18
|
|
18
|
-
result = execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path, explain)
|
19
|
-
process_execution_result(result, lang_config, lang_key, explain)
|
19
|
+
result = execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path, explain, flamegraph)
|
20
|
+
process_execution_result(result, lang_config, lang_key, explain, flamegraph)
|
20
21
|
end
|
21
22
|
|
22
23
|
private
|
23
24
|
|
24
|
-
|
25
|
-
|
26
25
|
def stderr_has_content?(stderr_output)
|
27
26
|
stderr_output && !stderr_output.strip.empty?
|
28
27
|
end
|
29
28
|
|
30
29
|
def handle_unsupported_language(lang)
|
31
|
-
|
30
|
+
TestSilencer.warn_unless_testing "Unsupported language: #{lang}"
|
32
31
|
"ERROR: Unsupported language: #{lang}"
|
33
32
|
end
|
34
33
|
|
35
|
-
def execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path = nil, explain = false)
|
34
|
+
def execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path = nil, explain = false, flamegraph = false)
|
36
35
|
cmd_lambda = lang_config[:command]
|
37
36
|
temp_file_suffix = lang_config[:temp_file_suffix]
|
38
37
|
|
39
38
|
if temp_file_suffix
|
40
|
-
execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path, explain)
|
39
|
+
execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path, explain, flamegraph)
|
41
40
|
else
|
42
|
-
execute_direct_command(code_content, cmd_lambda, explain)
|
41
|
+
execute_direct_command(code_content, cmd_lambda, input_file_path, explain, flamegraph)
|
43
42
|
end
|
44
43
|
end
|
45
44
|
|
46
|
-
def execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path = nil, explain = false)
|
45
|
+
def execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path = nil, explain = false, flamegraph = false)
|
47
46
|
result = nil
|
48
47
|
Tempfile.create([lang_key, temp_file_suffix], temp_dir) do |temp_file|
|
49
48
|
temp_file.write(code_content)
|
50
49
|
temp_file.close
|
51
|
-
command_to_run, exec_options = cmd_lambda.call(
|
50
|
+
command_to_run, exec_options = cmd_lambda.call(**{
|
51
|
+
code_content: code_content,
|
52
|
+
temp_file_path: temp_file.path,
|
53
|
+
input_file_path: input_file_path,
|
54
|
+
explain: explain,
|
55
|
+
flamegraph: flamegraph
|
56
|
+
})
|
52
57
|
|
53
58
|
# Extract output_path if present (for mermaid)
|
54
59
|
output_path = exec_options.delete(:output_path) if exec_options.is_a?(Hash)
|
@@ -58,27 +63,41 @@ class CodeExecutor
|
|
58
63
|
stdout: captured_stdout,
|
59
64
|
stderr: captured_stderr,
|
60
65
|
status: captured_status_obj,
|
61
|
-
output_path: output_path # For mermaid SVG output
|
66
|
+
output_path: output_path, # For mermaid SVG output
|
67
|
+
input_file_path: input_file_path # Pass through for flamegraph generation
|
62
68
|
}
|
63
69
|
end
|
64
70
|
result
|
65
71
|
end
|
66
72
|
|
67
|
-
def execute_direct_command(code_content, cmd_lambda, explain = false)
|
68
|
-
command_to_run, exec_options = cmd_lambda.call(
|
73
|
+
def execute_direct_command(code_content, cmd_lambda, input_file_path = nil, explain = false, flamegraph = false)
|
74
|
+
command_to_run, exec_options = cmd_lambda.call(**{
|
75
|
+
code_content: code_content,
|
76
|
+
temp_file_path: nil,
|
77
|
+
input_file_path: input_file_path,
|
78
|
+
explain: explain,
|
79
|
+
flamegraph: flamegraph
|
80
|
+
})
|
69
81
|
captured_stdout, captured_stderr, captured_status_obj = Open3.capture3(command_to_run, **exec_options)
|
70
|
-
{ stdout: captured_stdout, stderr: captured_stderr, status: captured_status_obj }
|
82
|
+
{ stdout: captured_stdout, stderr: captured_stderr, status: captured_status_obj, input_file_path: input_file_path }
|
71
83
|
end
|
72
84
|
|
73
|
-
def process_execution_result(result, lang_config, lang_key, explain = false)
|
85
|
+
def process_execution_result(result, lang_config, lang_key, explain = false, flamegraph = false)
|
74
86
|
exit_status, result_output, stderr_output = format_captured_output(result, lang_config)
|
75
87
|
|
76
88
|
if exit_status != 0
|
77
89
|
result_output = add_error_to_output(exit_status, lang_config, lang_key, result_output, stderr_output)
|
78
90
|
elsif lang_config && lang_config[:result_handling] == :mermaid_svg
|
79
91
|
result_output = handle_mermaid_svg_result(result, lang_key)
|
80
|
-
|
81
|
-
|
92
|
+
else
|
93
|
+
# Handle psql explain and flamegraph processing (both can be enabled)
|
94
|
+
if explain && lang_key == "psql"
|
95
|
+
result_output = handle_psql_explain_result(result_output)
|
96
|
+
end
|
97
|
+
|
98
|
+
if flamegraph && lang_key == "psql"
|
99
|
+
result_output = handle_psql_flamegraph_result(result_output, result[:input_file_path])
|
100
|
+
end
|
82
101
|
end
|
83
102
|
|
84
103
|
result_output
|
@@ -98,8 +117,8 @@ class CodeExecutor
|
|
98
117
|
end
|
99
118
|
|
100
119
|
def add_error_to_output(exit_status, lang_config, lang_key, result_output, stderr_output)
|
101
|
-
|
102
|
-
|
120
|
+
TestSilencer.warn_unless_testing "Code execution failed for language '#{lang_key}' with status #{exit_status}."
|
121
|
+
TestSilencer.warn_unless_testing "Stderr:\n#{stderr_output}" if stderr_has_content?(stderr_output)
|
103
122
|
|
104
123
|
is_js_error_already_formatted = lang_config && lang_config[:error_handling] == :js_specific && result_output.include?("Stderr:")
|
105
124
|
unless result_output.downcase.include?("error:") || is_js_error_already_formatted
|
@@ -110,15 +129,11 @@ class CodeExecutor
|
|
110
129
|
result_output
|
111
130
|
end
|
112
131
|
|
113
|
-
def stderr_has_content?(stderr_output)
|
114
|
-
stderr_output && !stderr_output.strip.empty?
|
115
|
-
end
|
116
|
-
|
117
132
|
def handle_mermaid_svg_result(result, lang_key)
|
118
133
|
output_path = result[:output_path]
|
119
134
|
|
120
135
|
unless output_path && File.exist?(output_path)
|
121
|
-
|
136
|
+
TestSilencer.warn_unless_testing "Warning: Mermaid SVG file not generated at expected path: #{output_path}"
|
122
137
|
return "Error: SVG file not generated"
|
123
138
|
end
|
124
139
|
|
@@ -137,7 +152,7 @@ class CodeExecutor
|
|
137
152
|
relative_path = svg_filename
|
138
153
|
end
|
139
154
|
|
140
|
-
|
155
|
+
TestSilencer.warn_unless_testing("Generated Mermaid SVG: #{relative_path}")
|
141
156
|
|
142
157
|
# Return markdown image tag instead of typical result content
|
143
158
|
""
|
@@ -169,6 +184,78 @@ class CodeExecutor
|
|
169
184
|
end
|
170
185
|
end
|
171
186
|
|
187
|
+
def handle_psql_flamegraph_result(result_output, input_file_path = nil)
|
188
|
+
require_relative 'pg_flamegraph_svg'
|
189
|
+
|
190
|
+
begin
|
191
|
+
# Extract clean JSON from result_output (might contain Dalibo link prefix)
|
192
|
+
json_text = if result_output.start_with?("DALIBO_LINK:")
|
193
|
+
# Extract the JSON part after the Dalibo link line
|
194
|
+
lines = result_output.split("\n", 2)
|
195
|
+
lines[1] || ""
|
196
|
+
else
|
197
|
+
result_output.strip
|
198
|
+
end
|
199
|
+
|
200
|
+
# Parse the EXPLAIN JSON output
|
201
|
+
json_data = JSON.parse(json_text)
|
202
|
+
|
203
|
+
# Generate SVG flamegraph
|
204
|
+
flamegraph_generator = PostgreSQLFlameGraphSVG.new(JSON.generate(json_data))
|
205
|
+
svg_content = flamegraph_generator.generate_svg
|
206
|
+
|
207
|
+
# Save SVG file following same pattern as mermaid
|
208
|
+
if input_file_path
|
209
|
+
# Extract markdown file basename without extension
|
210
|
+
md_basename = File.basename(input_file_path, ".*")
|
211
|
+
|
212
|
+
# Create directory named after the markdown file
|
213
|
+
output_dir = File.join(File.dirname(input_file_path), md_basename)
|
214
|
+
Dir.mkdir(output_dir) unless Dir.exist?(output_dir)
|
215
|
+
|
216
|
+
# Generate unique filename with markdown basename prefix
|
217
|
+
timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
|
218
|
+
random_suffix = SecureRandom.hex(6)
|
219
|
+
svg_filename = "#{md_basename}-flamegraph-#{timestamp}-#{random_suffix}.svg"
|
220
|
+
output_path = File.join(output_dir, svg_filename)
|
221
|
+
else
|
222
|
+
# Fallback to simple naming
|
223
|
+
timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
|
224
|
+
output_path = "pg-flamegraph-#{timestamp}.svg"
|
225
|
+
end
|
226
|
+
|
227
|
+
# Write SVG file
|
228
|
+
File.write(output_path, svg_content)
|
229
|
+
|
230
|
+
# Generate relative path for markdown
|
231
|
+
if input_file_path
|
232
|
+
relative_path = "#{File.basename(output_dir)}/#{File.basename(output_path)}"
|
233
|
+
else
|
234
|
+
relative_path = File.basename(output_path)
|
235
|
+
end
|
236
|
+
|
237
|
+
TestSilencer.warn_unless_testing("Generated PostgreSQL flamegraph: #{relative_path}")
|
238
|
+
|
239
|
+
# Return a special format that the markdown processor can parse
|
240
|
+
# Preserve any existing Dalibo link prefix
|
241
|
+
if result_output.start_with?("DALIBO_LINK:")
|
242
|
+
lines = result_output.split("\n", 2)
|
243
|
+
dalibo_part = lines[0]
|
244
|
+
json_part = lines[1] || ""
|
245
|
+
"#{dalibo_part}\nFLAMEGRAPH_LINK:#{relative_path}\n#{json_part}"
|
246
|
+
else
|
247
|
+
"FLAMEGRAPH_LINK:#{relative_path}\n#{json_text}"
|
248
|
+
end
|
249
|
+
|
250
|
+
rescue JSON::ParserError => e
|
251
|
+
TestSilencer.warn_unless_testing "Error parsing EXPLAIN JSON: #{e.message}"
|
252
|
+
result_output
|
253
|
+
rescue => e
|
254
|
+
TestSilencer.warn_unless_testing "Error generating flamegraph: #{e.message}"
|
255
|
+
result_output
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
172
259
|
private
|
173
260
|
|
174
261
|
def submit_plan_to_dalibo(plan_json)
|
@@ -201,11 +288,11 @@ class CodeExecutor
|
|
201
288
|
nil
|
202
289
|
end
|
203
290
|
else
|
204
|
-
|
291
|
+
TestSilencer.warn_unless_testing "Failed to submit plan to Dalibo: #{response.code} #{response.message}"
|
205
292
|
nil
|
206
293
|
end
|
207
294
|
rescue => e
|
208
|
-
|
295
|
+
TestSilencer.warn_unless_testing "Error submitting plan to Dalibo: #{e.message}"
|
209
296
|
nil
|
210
297
|
end
|
211
298
|
end
|
data/lib/dalibo_helper.rb
CHANGED
@@ -9,7 +9,7 @@ module DaliboHelper
|
|
9
9
|
lines = result_output.split("\n", 2)
|
10
10
|
dalibo_url = lines[0].sub(DALIBO_LINK_PREFIX, "")
|
11
11
|
clean_result = lines[1] || ""
|
12
|
-
dalibo_link = "
|
12
|
+
dalibo_link = "[Dalibo](#{dalibo_url})"
|
13
13
|
[dalibo_link, clean_result]
|
14
14
|
else
|
15
15
|
[nil, result_output]
|
@@ -25,7 +25,7 @@ module DaliboHelper
|
|
25
25
|
|
26
26
|
if is_blank_line?(next_line)
|
27
27
|
consumed_lines << file_enum.next
|
28
|
-
elsif next_line&.start_with?("
|
28
|
+
elsif next_line&.start_with?("[Dalibo]")
|
29
29
|
consumed_lines << file_enum.next
|
30
30
|
else
|
31
31
|
# Hit something that's not a blank line or Dalibo link, stop consuming
|