markdown_exec 3.5.1 → 3.5.2
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/.ai-agent-instructions +54 -0
- data/.cursorrules +198 -0
- data/.rubocop.wide.yml +5 -0
- data/.rubocop.yml +7 -2
- data/CHANGELOG.md +12 -1
- data/Gemfile.lock +1 -1
- data/Rakefile +2 -0
- data/ai-principles.md +516 -0
- data/architecture-decisions.md +190 -0
- data/bats/block-hide.bats +1 -1
- data/bats/block-type-bash.bats +5 -5
- data/bats/block-type-link.bats +1 -1
- data/bats/block-type-opts.bats +3 -3
- data/bats/block-type-port.bats +2 -2
- data/bats/block-type-shell-require-ux.bats +2 -2
- data/bats/block-type-ux-allowed.bats +4 -4
- data/bats/block-type-ux-auto.bats +1 -1
- data/bats/block-type-ux-chained.bats +1 -1
- data/bats/block-type-ux-default.bats +1 -1
- data/bats/block-type-ux-echo-hash-transform.bats +1 -1
- data/bats/block-type-ux-echo-hash.bats +2 -2
- data/bats/block-type-ux-echo.bats +3 -3
- data/bats/block-type-ux-exec-hash-transform.bats +1 -1
- data/bats/block-type-ux-exec-hash.bats +2 -2
- data/bats/block-type-ux-exec.bats +1 -1
- data/bats/block-type-ux-force.bats +1 -1
- data/bats/block-type-ux-formats.bats +1 -1
- data/bats/block-type-ux-hidden.bats +1 -1
- data/bats/block-type-ux-invalid.bats +1 -1
- data/bats/block-type-ux-readonly.bats +1 -1
- data/bats/block-type-ux-require-chained.bats +2 -2
- data/bats/block-type-ux-require-context.bats +2 -2
- data/bats/block-type-ux-require.bats +2 -2
- data/bats/block-type-ux-required-variables.bats +1 -1
- data/bats/block-type-ux-row-format.bats +1 -1
- data/bats/block-type-ux-sources.bats +4 -4
- data/bats/block-type-ux-transform.bats +1 -1
- data/bats/block-type-vars.bats +3 -3
- data/bats/border.bats +1 -1
- data/bats/cli.bats +11 -11
- data/bats/command-substitution-options.bats +2 -2
- data/bats/command-substitution.bats +1 -1
- data/bats/document-shell.bats +1 -1
- data/bats/history.bats +5 -5
- data/bats/import-conflict.bats +1 -1
- data/bats/import-directive-line-continuation.bats +1 -1
- data/bats/import-directive-parameter-symbols.bats +1 -1
- data/bats/import-duplicates.bats +6 -6
- data/bats/import-parameter-symbols.bats +1 -1
- data/bats/import-with-text-substitution.bats +1 -1
- data/bats/import.bats +3 -3
- data/bats/indented-block-type-vars.bats +1 -1
- data/bats/indented-multi-line-output.bats +1 -1
- data/bats/line-decor-dynamic.bats +1 -1
- data/bats/line-wrapping.bats +1 -1
- data/bats/load-vars-state-demo.bats +4 -4
- data/bats/markup.bats +4 -4
- data/bats/mde.bats +4 -4
- data/bats/option-expansion.bats +1 -1
- data/bats/options-collapse.bats +4 -4
- data/bats/options.bats +47 -17
- data/bats/plain.bats +1 -1
- data/bats/publish.bats +2 -2
- data/bats/table-column-truncate.bats +1 -1
- data/bats/table.bats +2 -2
- data/bats/variable-expansion-multiline.bats +1 -1
- data/bats/variable-expansion.bats +6 -6
- data/conversation-template.md +611 -0
- data/docs/block-execution-modes.md +177 -0
- data/docs/block-filtering.md +252 -0
- data/docs/block-naming-patterns.md +210 -0
- data/docs/block-scanning-patterns.md +248 -0
- data/docs/cli-reference.md +370 -0
- data/docs/dev/block-hide.md +1 -1
- data/docs/dev/block-type-ux-transform.md +5 -4
- data/docs/dev/print_bytes.md +3 -0
- data/docs/dev/shebang.md +6 -0
- data/docs/docker-testing.md +5 -0
- data/docs/execution-control.md +384 -0
- data/docs/getting-started.md +209 -0
- data/docs/import-options.md +391 -0
- data/docs/tab-completion.md +7 -0
- data/docs/ux-blocks.md +376 -0
- data/examples/linked1.md +8 -1
- data/implementation-decisions.md +212 -0
- data/lib/cached_nested_file_reader.rb +138 -1
- data/lib/command_result.rb +27 -6
- data/lib/executed_shell_command.rb +512 -0
- data/lib/filter.rb +7 -7
- data/lib/hash_delegator.rb +403 -350
- data/lib/link_history.rb +22 -11
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/mdoc.rb +103 -44
- data/lib/menu.src.yml +110 -83
- data/lib/menu.yml +149 -83
- data/lib/transformed_shell_command.rb +449 -0
- data/lib/wl.rb +15 -0
- data/lib/ww.rb +16 -5
- data/requirements.md +111 -0
- data/semantic-tokens.md +132 -0
- data/tasks.md +69 -0
- metadata +26 -4
- data/docs/ux-blocks-examples.md +0 -120
- data/docs/ux-blocks-init-act.md +0 -100
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
#!/usr/bin/env -S bundle exec ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# encoding=utf-8
|
|
5
|
+
|
|
6
|
+
require 'open3'
|
|
7
|
+
|
|
8
|
+
##
|
|
9
|
+
# ExecutedShellCommand wraps the execution of a shell command and captures:
|
|
10
|
+
#
|
|
11
|
+
# * The original command string
|
|
12
|
+
# * STDOUT and STDERR
|
|
13
|
+
# * Exit status (Process::Status and numeric exit code)
|
|
14
|
+
# * Success / failure convenience predicates
|
|
15
|
+
# * Start and finish timestamps and derived duration
|
|
16
|
+
# * PID of the spawned process
|
|
17
|
+
# * Optional environment and working directory
|
|
18
|
+
#
|
|
19
|
+
# The command is executed automatically during initialization. The result
|
|
20
|
+
# is memoized and can be accessed immediately. Subsequent calls to `run`
|
|
21
|
+
# will re-execute by default (force: true), unless `force: false` is
|
|
22
|
+
# explicitly passed to use the memoized result.
|
|
23
|
+
#
|
|
24
|
+
# Basic usage:
|
|
25
|
+
#
|
|
26
|
+
# cmd = ExecutedShellCommand.new("ls -l /tmp") # executes immediately
|
|
27
|
+
# result = cmd.result # access memoized result
|
|
28
|
+
# fresh = cmd.run # re-executes (force: true by default)
|
|
29
|
+
# cached = cmd.run(force: false) # returns memoized result
|
|
30
|
+
#
|
|
31
|
+
class ExecutedShellCommand
|
|
32
|
+
##
|
|
33
|
+
# Immutable value object representing the outcome of a command execution.
|
|
34
|
+
#
|
|
35
|
+
Result = Struct.new(
|
|
36
|
+
:command,
|
|
37
|
+
:stdout,
|
|
38
|
+
:stderr,
|
|
39
|
+
:status,
|
|
40
|
+
:started_at,
|
|
41
|
+
:finished_at,
|
|
42
|
+
:pid,
|
|
43
|
+
:env,
|
|
44
|
+
:chdir,
|
|
45
|
+
keyword_init: true
|
|
46
|
+
) do
|
|
47
|
+
def success?
|
|
48
|
+
status&.success?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def exit_code
|
|
52
|
+
status&.exitstatus
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def duration
|
|
56
|
+
return nil unless started_at && finished_at
|
|
57
|
+
|
|
58
|
+
finished_at - started_at
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def signaled?
|
|
62
|
+
status&.signaled?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def termsig
|
|
66
|
+
status&.termsig
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_h
|
|
70
|
+
{
|
|
71
|
+
command: command,
|
|
72
|
+
stdout: stdout,
|
|
73
|
+
stderr: stderr,
|
|
74
|
+
exit_code: exit_code,
|
|
75
|
+
success: success?,
|
|
76
|
+
started_at: started_at,
|
|
77
|
+
finished_at: finished_at,
|
|
78
|
+
duration: duration,
|
|
79
|
+
pid: pid,
|
|
80
|
+
env: env,
|
|
81
|
+
chdir: chdir,
|
|
82
|
+
status: status
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
attr_reader :command, :env, :chdir, :result
|
|
88
|
+
|
|
89
|
+
def initialize(command, chdir: nil, env: {})
|
|
90
|
+
@command = command
|
|
91
|
+
@chdir = chdir
|
|
92
|
+
@env = env
|
|
93
|
+
@result = nil
|
|
94
|
+
run # Execute command immediately during initialization
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
##
|
|
98
|
+
# Execute the command, capture all outputs, and return a Result.
|
|
99
|
+
#
|
|
100
|
+
# By default (force: true), the command is executed again and the
|
|
101
|
+
# memoized Result is replaced.
|
|
102
|
+
#
|
|
103
|
+
# If `force: false` is passed and a result already exists, the
|
|
104
|
+
# memoized Result is returned without re-executing.
|
|
105
|
+
#
|
|
106
|
+
def run(force: true)
|
|
107
|
+
return @result if @result && !force
|
|
108
|
+
|
|
109
|
+
raise ArgumentError, 'command cannot be nil' if command.nil?
|
|
110
|
+
|
|
111
|
+
started_at = Time.now
|
|
112
|
+
stdout_str = +''
|
|
113
|
+
stderr_str = +''
|
|
114
|
+
status = nil
|
|
115
|
+
pid = nil
|
|
116
|
+
|
|
117
|
+
popen_args = env.empty? ? [command] : [env, command]
|
|
118
|
+
popen_opts = chdir ? { chdir: chdir } : {}
|
|
119
|
+
|
|
120
|
+
Open3.popen3(*popen_args, popen_opts) do |stdin, stdout, stderr, wait_thr|
|
|
121
|
+
stdin.close
|
|
122
|
+
stdout_str = stdout.read
|
|
123
|
+
stderr_str = stderr.read
|
|
124
|
+
pid = wait_thr.pid
|
|
125
|
+
status = wait_thr.value
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
finished_at = Time.now
|
|
129
|
+
|
|
130
|
+
@result = Result.new(
|
|
131
|
+
command: command,
|
|
132
|
+
stdout: stdout_str,
|
|
133
|
+
stderr: stderr_str,
|
|
134
|
+
status: status,
|
|
135
|
+
started_at: started_at,
|
|
136
|
+
finished_at: finished_at,
|
|
137
|
+
pid: pid,
|
|
138
|
+
env: env,
|
|
139
|
+
chdir: chdir
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
##
|
|
144
|
+
# Return the memoized result. Since the command runs at initialization,
|
|
145
|
+
# this will always return the memoized result unless run(force: true)
|
|
146
|
+
# was called to update it.
|
|
147
|
+
#
|
|
148
|
+
def fetch_result
|
|
149
|
+
@result
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Convenience delegators to the last / memoized result:
|
|
153
|
+
|
|
154
|
+
def stdout
|
|
155
|
+
fetch_result.stdout
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def stderr
|
|
159
|
+
fetch_result.stderr
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def exit_code
|
|
163
|
+
fetch_result.exit_code
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def success?
|
|
167
|
+
fetch_result.success?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def failure?
|
|
171
|
+
!fetch_result.success?
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def duration
|
|
175
|
+
fetch_result.duration
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def started_at
|
|
179
|
+
fetch_result.started_at
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def finished_at
|
|
183
|
+
fetch_result.finished_at
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def pid
|
|
187
|
+
fetch_result.pid
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Test suite when running as a script
|
|
192
|
+
return if $PROGRAM_NAME != __FILE__
|
|
193
|
+
|
|
194
|
+
require 'bundler/setup'
|
|
195
|
+
Bundler.require(:default)
|
|
196
|
+
|
|
197
|
+
require 'minitest/autorun'
|
|
198
|
+
require 'tmpdir'
|
|
199
|
+
require 'fileutils'
|
|
200
|
+
|
|
201
|
+
# [TEST:SHELL_COMMAND] Comprehensive test suite for ExecutedShellCommand class
|
|
202
|
+
class ExecutedShellCommandTest < Minitest::Test
|
|
203
|
+
# [TEST:SHELL_COMMAND] Test successful command execution (runs at initialization)
|
|
204
|
+
def test_successful_command_execution
|
|
205
|
+
cmd = ExecutedShellCommand.new("echo 'success'")
|
|
206
|
+
result = cmd.result # Command already executed at initialization
|
|
207
|
+
|
|
208
|
+
assert result.success?, 'Command should succeed'
|
|
209
|
+
assert_equal 0, result.exit_code
|
|
210
|
+
assert_equal "success\n", result.stdout
|
|
211
|
+
assert_equal '', result.stderr
|
|
212
|
+
assert_kind_of Process::Status, result.status
|
|
213
|
+
assert result.status.success?
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# [TEST:SHELL_COMMAND] Test failed command execution with exit code (runs at initialization)
|
|
217
|
+
def test_failed_command_execution
|
|
218
|
+
cmd = ExecutedShellCommand.new('exit 3')
|
|
219
|
+
result = cmd.result # Command already executed at initialization
|
|
220
|
+
|
|
221
|
+
refute result.success?, 'Command should fail'
|
|
222
|
+
assert_equal 3, result.exit_code
|
|
223
|
+
assert_equal '', result.stdout
|
|
224
|
+
assert_equal '', result.stderr
|
|
225
|
+
assert_kind_of Process::Status, result.status
|
|
226
|
+
refute result.status.success?
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# [TEST:SHELL_COMMAND] Test command with both STDOUT and STDERR (runs at initialization)
|
|
230
|
+
def test_command_with_stdout_and_stderr
|
|
231
|
+
cmd = ExecutedShellCommand.new(
|
|
232
|
+
"echo 'hello from shell' && >&2 echo 'oops' && exit 3"
|
|
233
|
+
)
|
|
234
|
+
result = cmd.result # Command already executed at initialization
|
|
235
|
+
|
|
236
|
+
refute result.success?, 'Command should fail'
|
|
237
|
+
assert_equal 3, result.exit_code
|
|
238
|
+
assert_equal "hello from shell\n", result.stdout
|
|
239
|
+
assert_equal "oops\n", result.stderr
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# [TEST:SHELL_COMMAND] Test result memoization - accessing result returns same object
|
|
243
|
+
def test_result_memoization
|
|
244
|
+
cmd = ExecutedShellCommand.new("echo 'test'")
|
|
245
|
+
first = cmd.result # Access memoized result from initialization
|
|
246
|
+
second = cmd.result # Should return same memoized result
|
|
247
|
+
|
|
248
|
+
assert_equal first.object_id, second.object_id,
|
|
249
|
+
'Should return same memoized result object'
|
|
250
|
+
assert_equal first.stdout, second.stdout
|
|
251
|
+
assert_equal first.pid, second.pid
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# [TEST:SHELL_COMMAND] Test run defaults to force: true and creates new result object
|
|
255
|
+
def test_run_defaults_to_force_true
|
|
256
|
+
cmd = ExecutedShellCommand.new("echo 'test'")
|
|
257
|
+
first = cmd.result # Initial execution result
|
|
258
|
+
fresh = cmd.run # Defaults to force: true, so re-executes
|
|
259
|
+
|
|
260
|
+
refute_equal first.object_id, fresh.object_id,
|
|
261
|
+
'Run should create new result object (force: true by default)'
|
|
262
|
+
assert_equal first.stdout, fresh.stdout, 'Output should be the same'
|
|
263
|
+
# PIDs will be different as it's a new process
|
|
264
|
+
refute_equal first.pid, fresh.pid,
|
|
265
|
+
'PID should be different for new execution'
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# [TEST:SHELL_COMMAND] Test run with force: false returns memoized result
|
|
269
|
+
def test_run_with_force_false_returns_memoized
|
|
270
|
+
cmd = ExecutedShellCommand.new("echo 'test'")
|
|
271
|
+
first = cmd.result # Initial execution result
|
|
272
|
+
cached = cmd.run(force: false) # Should return memoized result
|
|
273
|
+
|
|
274
|
+
assert_equal first.object_id, cached.object_id,
|
|
275
|
+
'Run with force: false should return memoized result'
|
|
276
|
+
assert_equal first.stdout, cached.stdout
|
|
277
|
+
assert_equal first.pid, cached.pid
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# [TEST:SHELL_COMMAND] Test fetch_result returns memoized result
|
|
281
|
+
def test_fetch_result_returns_memoized
|
|
282
|
+
cmd = ExecutedShellCommand.new("echo 'test'")
|
|
283
|
+
first = cmd.result # Initial execution result
|
|
284
|
+
fetched = cmd.fetch_result
|
|
285
|
+
|
|
286
|
+
assert_equal first.object_id, fetched.object_id,
|
|
287
|
+
'fetch_result should return memoized result'
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# [TEST:SHELL_COMMAND] Test convenience delegator methods (command runs at initialization)
|
|
291
|
+
def test_convenience_delegators
|
|
292
|
+
cmd = ExecutedShellCommand.new(
|
|
293
|
+
"echo 'output' && >&2 echo 'error' && exit 5"
|
|
294
|
+
)
|
|
295
|
+
# Command already executed at initialization, delegators access memoized result
|
|
296
|
+
|
|
297
|
+
assert_equal "output\n", cmd.stdout
|
|
298
|
+
assert_equal "error\n", cmd.stderr
|
|
299
|
+
assert_equal 5, cmd.exit_code
|
|
300
|
+
refute cmd.success?
|
|
301
|
+
assert_kind_of Numeric, cmd.duration
|
|
302
|
+
assert_kind_of Time, cmd.started_at
|
|
303
|
+
assert_kind_of Time, cmd.finished_at
|
|
304
|
+
assert_kind_of Integer, cmd.pid
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# [TEST:SHELL_COMMAND] Test PID capture (command runs at initialization)
|
|
308
|
+
def test_pid_capture
|
|
309
|
+
cmd = ExecutedShellCommand.new("echo 'test'")
|
|
310
|
+
result = cmd.result # Command already executed at initialization
|
|
311
|
+
|
|
312
|
+
assert_kind_of Integer, result.pid
|
|
313
|
+
assert result.pid.positive?, 'PID should be positive'
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# [TEST:SHELL_COMMAND] Test duration calculation (command runs at initialization)
|
|
317
|
+
def test_duration_calculation
|
|
318
|
+
cmd = ExecutedShellCommand.new('sleep 0.1')
|
|
319
|
+
result = cmd.result # Command already executed at initialization
|
|
320
|
+
|
|
321
|
+
assert_kind_of Numeric, result.duration
|
|
322
|
+
assert result.duration >= 0.1, 'Duration should be at least 0.1 seconds'
|
|
323
|
+
assert result.duration < 1.0, 'Duration should be less than 1 second'
|
|
324
|
+
assert result.started_at < result.finished_at,
|
|
325
|
+
'Started at should be before finished at'
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# [TEST:SHELL_COMMAND] Test timestamps (command runs at initialization)
|
|
329
|
+
def test_timestamps
|
|
330
|
+
before = Time.now
|
|
331
|
+
cmd = ExecutedShellCommand.new("echo 'test'")
|
|
332
|
+
after = Time.now
|
|
333
|
+
result = cmd.result # Command already executed at initialization
|
|
334
|
+
|
|
335
|
+
assert result.started_at >= before,
|
|
336
|
+
'Started at should be after test start'
|
|
337
|
+
assert result.finished_at <= after,
|
|
338
|
+
'Finished at should be before test end'
|
|
339
|
+
assert result.started_at <= result.finished_at,
|
|
340
|
+
'Started at should be before finished at'
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# [TEST:SHELL_COMMAND] Test environment variable passing (command runs at initialization)
|
|
344
|
+
def test_environment_variables
|
|
345
|
+
env = { 'TEST_VAR' => 'test_value', 'ANOTHER_VAR' => 'another_value' }
|
|
346
|
+
cmd = ExecutedShellCommand.new('echo $TEST_VAR && echo $ANOTHER_VAR',
|
|
347
|
+
env: env)
|
|
348
|
+
result = cmd.result # Command already executed at initialization
|
|
349
|
+
|
|
350
|
+
assert result.success?
|
|
351
|
+
assert_includes result.stdout, 'test_value'
|
|
352
|
+
assert_includes result.stdout, 'another_value'
|
|
353
|
+
assert_equal env, result.env
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# [TEST:SHELL_COMMAND] Test working directory change (command runs at initialization)
|
|
357
|
+
def test_working_directory_change
|
|
358
|
+
Dir.mktmpdir do |tmpdir|
|
|
359
|
+
test_file = File.join(tmpdir, 'test.txt')
|
|
360
|
+
File.write(test_file, 'test content')
|
|
361
|
+
|
|
362
|
+
cmd = ExecutedShellCommand.new('cat test.txt', chdir: tmpdir)
|
|
363
|
+
result = cmd.result # Command already executed at initialization
|
|
364
|
+
|
|
365
|
+
assert result.success?
|
|
366
|
+
assert_equal 'test content', result.stdout
|
|
367
|
+
assert_equal tmpdir, result.chdir
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# [TEST:SHELL_COMMAND] Test command attribute
|
|
372
|
+
def test_command_attribute
|
|
373
|
+
command_str = "echo 'test'"
|
|
374
|
+
cmd = ExecutedShellCommand.new(command_str)
|
|
375
|
+
|
|
376
|
+
assert_equal command_str, cmd.command
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# [TEST:SHELL_COMMAND] Test nil command raises ArgumentError at initialization
|
|
380
|
+
def test_nil_command_raises_error
|
|
381
|
+
assert_raises(ArgumentError, 'command cannot be nil') do
|
|
382
|
+
ExecutedShellCommand.new(nil) # Error raised during initialization when run is called
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# [TEST:SHELL_COMMAND] Test Result#to_h method (command runs at initialization)
|
|
387
|
+
def test_result_to_h
|
|
388
|
+
cmd = ExecutedShellCommand.new("echo 'test'")
|
|
389
|
+
result = cmd.result # Command already executed at initialization
|
|
390
|
+
hash = result.to_h
|
|
391
|
+
|
|
392
|
+
assert_kind_of Hash, hash
|
|
393
|
+
assert_equal cmd.command, hash[:command]
|
|
394
|
+
assert_equal result.stdout, hash[:stdout]
|
|
395
|
+
assert_equal result.stderr, hash[:stderr]
|
|
396
|
+
assert_equal result.exit_code, hash[:exit_code]
|
|
397
|
+
assert_equal result.success?, hash[:success]
|
|
398
|
+
assert_equal result.started_at, hash[:started_at]
|
|
399
|
+
assert_equal result.finished_at, hash[:finished_at]
|
|
400
|
+
assert_equal result.duration, hash[:duration]
|
|
401
|
+
assert_equal result.pid, hash[:pid]
|
|
402
|
+
assert_equal result.env, hash[:env]
|
|
403
|
+
assert_equal result.chdir, hash[:chdir]
|
|
404
|
+
assert_equal result.status, hash[:status]
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# [TEST:SHELL_COMMAND] Test Result#signaled? method (command runs at initialization)
|
|
408
|
+
def test_result_signaled
|
|
409
|
+
cmd = ExecutedShellCommand.new("echo 'test'")
|
|
410
|
+
result = cmd.result # Command already executed at initialization
|
|
411
|
+
|
|
412
|
+
# Normal exit should not be signaled
|
|
413
|
+
refute result.signaled?
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# [TEST:SHELL_COMMAND] Test Result#termsig method (command runs at initialization)
|
|
417
|
+
def test_result_termsig
|
|
418
|
+
cmd = ExecutedShellCommand.new("echo 'test'")
|
|
419
|
+
result = cmd.result # Command already executed at initialization
|
|
420
|
+
|
|
421
|
+
# Normal exit should have nil termsig
|
|
422
|
+
assert_nil result.termsig, 'Normal exit should have nil termsig'
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# [TEST:SHELL_COMMAND] Test empty environment hash (command runs at initialization)
|
|
426
|
+
def test_empty_environment_hash
|
|
427
|
+
cmd = ExecutedShellCommand.new("echo 'test'", env: {})
|
|
428
|
+
result = cmd.result # Command already executed at initialization
|
|
429
|
+
|
|
430
|
+
assert result.success?
|
|
431
|
+
assert_equal({}, result.env)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# [TEST:SHELL_COMMAND] Test command with complex shell syntax (command runs at initialization)
|
|
435
|
+
def test_complex_shell_command
|
|
436
|
+
cmd = ExecutedShellCommand.new(
|
|
437
|
+
"echo 'line1' && echo 'line2' && echo 'line3'"
|
|
438
|
+
)
|
|
439
|
+
result = cmd.result # Command already executed at initialization
|
|
440
|
+
|
|
441
|
+
assert result.success?
|
|
442
|
+
assert_includes result.stdout, 'line1'
|
|
443
|
+
assert_includes result.stdout, 'line2'
|
|
444
|
+
assert_includes result.stdout, 'line3'
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# [TEST:SHELL_COMMAND] Test command with no output (command runs at initialization)
|
|
448
|
+
def test_command_with_no_output
|
|
449
|
+
cmd = ExecutedShellCommand.new('true')
|
|
450
|
+
result = cmd.result # Command already executed at initialization
|
|
451
|
+
|
|
452
|
+
assert result.success?
|
|
453
|
+
assert_equal 0, result.exit_code
|
|
454
|
+
assert_equal '', result.stdout
|
|
455
|
+
assert_equal '', result.stderr
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# [TEST:SHELL_COMMAND] Test multiple runs (default force: true creates new results)
|
|
459
|
+
def test_multiple_runs_create_new_results
|
|
460
|
+
cmd = ExecutedShellCommand.new("echo 'test'")
|
|
461
|
+
first = cmd.result # Initial execution result
|
|
462
|
+
second = cmd.run # Defaults to force: true, creates new result
|
|
463
|
+
third = cmd.run # Defaults to force: true, creates new result
|
|
464
|
+
|
|
465
|
+
refute_equal first.object_id, second.object_id,
|
|
466
|
+
'Each run creates new result'
|
|
467
|
+
refute_equal second.object_id, third.object_id,
|
|
468
|
+
'Each run creates new result'
|
|
469
|
+
# All should have same output
|
|
470
|
+
assert_equal first.stdout, second.stdout
|
|
471
|
+
assert_equal second.stdout, third.stdout
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# [TEST:SHELL_COMMAND] Test command with newlines in output (command runs at initialization)
|
|
475
|
+
def test_command_with_newlines
|
|
476
|
+
cmd = ExecutedShellCommand.new("echo -e 'line1\nline2\nline3'")
|
|
477
|
+
result = cmd.result # Command already executed at initialization
|
|
478
|
+
|
|
479
|
+
assert result.success?
|
|
480
|
+
assert_includes result.stdout, 'line1'
|
|
481
|
+
assert_includes result.stdout, 'line2'
|
|
482
|
+
assert_includes result.stdout, 'line3'
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# [TEST:SHELL_COMMAND] Test command attribute is immutable (command runs at initialization)
|
|
486
|
+
def test_command_immutability
|
|
487
|
+
original_command = "echo 'original'"
|
|
488
|
+
cmd = ExecutedShellCommand.new(original_command)
|
|
489
|
+
|
|
490
|
+
# Command should remain unchanged
|
|
491
|
+
assert_equal original_command, cmd.command
|
|
492
|
+
result = cmd.result # Command already executed at initialization
|
|
493
|
+
assert_equal original_command, cmd.command
|
|
494
|
+
assert_equal original_command, result.command
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# [TEST:SHELL_COMMAND] Test command executes automatically at initialization
|
|
498
|
+
def test_command_executes_at_initialization
|
|
499
|
+
before = Time.now
|
|
500
|
+
cmd = ExecutedShellCommand.new("echo 'auto-executed'")
|
|
501
|
+
after = Time.now
|
|
502
|
+
|
|
503
|
+
# Result should be available immediately without calling run
|
|
504
|
+
refute_nil cmd.result,
|
|
505
|
+
'Result should be available immediately after initialization'
|
|
506
|
+
assert_equal "auto-executed\n", cmd.result.stdout
|
|
507
|
+
assert cmd.result.started_at >= before,
|
|
508
|
+
'Command should have started during initialization'
|
|
509
|
+
assert cmd.result.finished_at <= after,
|
|
510
|
+
'Command should have finished during initialization'
|
|
511
|
+
end
|
|
512
|
+
end
|
data/lib/filter.rb
CHANGED
|
@@ -135,12 +135,12 @@ module MarkdownExec
|
|
|
135
135
|
|
|
136
136
|
if options[:hide_blocks_by_name]
|
|
137
137
|
filters[:hidden_name] =
|
|
138
|
-
!!(options[:
|
|
139
|
-
fcb.code_name_exp?(options[:
|
|
138
|
+
!!(options[:block_name_hide_custom_match].present? &&
|
|
139
|
+
fcb.code_name_exp?(options[:block_name_hide_custom_match]))
|
|
140
140
|
end
|
|
141
141
|
filters[:include_name] =
|
|
142
|
-
!!(options[:
|
|
143
|
-
fcb.code_name_exp?(options[:
|
|
142
|
+
!!(options[:block_name_hidden_match].present? &&
|
|
143
|
+
fcb.code_name_exp?(options[:block_name_hidden_match]))
|
|
144
144
|
filters[:wrap_name] =
|
|
145
145
|
!!(options[:block_name_wrapper_match].present? &&
|
|
146
146
|
fcb.code_name_exp?(options[:block_name_wrapper_match]))
|
|
@@ -251,7 +251,7 @@ if $PROGRAM_NAME == __FILE__
|
|
|
251
251
|
|
|
252
252
|
def test_hidden_name_condition
|
|
253
253
|
@options[:hide_blocks_by_name] = true
|
|
254
|
-
@options[:
|
|
254
|
+
@options[:block_name_hide_custom_match] = 'hidden'
|
|
255
255
|
@fcb.oname = 'hidden_block'
|
|
256
256
|
refute Filter.fcb_select?(@options, @fcb)
|
|
257
257
|
end
|
|
@@ -336,7 +336,7 @@ if $PROGRAM_NAME == __FILE__
|
|
|
336
336
|
end
|
|
337
337
|
|
|
338
338
|
def test_hidden_block_by_name
|
|
339
|
-
@options[:
|
|
339
|
+
@options[:block_name_hide_custom_match] = 'hidden_block'
|
|
340
340
|
@options[:hide_blocks_by_name] = true
|
|
341
341
|
fcb = FCB.new(oname: 'hidden_block', shell: ShellType::BASH,
|
|
342
342
|
start_line: '')
|
|
@@ -345,7 +345,7 @@ if $PROGRAM_NAME == __FILE__
|
|
|
345
345
|
end
|
|
346
346
|
|
|
347
347
|
def test_include_block_by_name
|
|
348
|
-
@options[:
|
|
348
|
+
@options[:block_name_hidden_match] = 'include_block'
|
|
349
349
|
fcb = FCB.new(oname: 'include_block', shell: ShellType::BASH,
|
|
350
350
|
start_line: '')
|
|
351
351
|
Filter.apply_other_filters(@options, @filters, fcb)
|