markdown_exec 3.2.0 → 3.4.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/CHANGELOG.md +40 -0
- data/Gemfile.lock +1 -1
- data/Rakefile +5 -4
- data/bats/block-type-ux-echo-hash-transform.bats +1 -1
- data/bats/block-type-ux-exec-hash-transform.bats +8 -0
- data/bats/block-type-ux-exec-hash.bats +15 -0
- data/bats/block-type-ux-force.bats +9 -0
- data/bats/block-type-ux-formats.bats +8 -0
- data/bats/block-type-ux-no-name.bats +8 -0
- data/bats/block-type-ux-readonly.bats +1 -1
- data/bats/block-type-ux-row-format.bats +1 -1
- data/bats/command-substitution-options.bats +2 -2
- data/bats/import-directive-line-continuation.bats +9 -0
- data/bats/import-directive-parameter-symbols.bats +9 -0
- data/bats/import-duplicates.bats +4 -2
- data/bats/import-parameter-symbols.bats +8 -0
- data/bats/markup.bats +1 -1
- data/bats/option-expansion.bats +1 -1
- data/bats/options.bats +1 -1
- data/bats/table-column-truncate.bats +1 -1
- data/bats/table.bats +1 -1
- data/bats/test_helper.bash +4 -3
- data/bin/tab_completion.sh +5 -1
- data/docs/dev/block-type-ux-echo-hash-transform.md +14 -12
- data/docs/dev/block-type-ux-exec-hash-transform.md +37 -0
- data/docs/dev/block-type-ux-exec-hash.md +93 -0
- data/docs/dev/block-type-ux-force.md +20 -0
- data/docs/dev/block-type-ux-formats.md +58 -0
- data/docs/dev/block-type-ux-no-name.md +17 -0
- data/docs/dev/block-type-ux-row-format.md +1 -1
- data/docs/dev/hexdump_format.md +267 -0
- data/docs/dev/import/parameter-symbols.md +6 -0
- data/docs/dev/import-directive-line-continuation.md +6 -0
- data/docs/dev/import-directive-parameter-symbols.md +9 -0
- data/docs/dev/import-parameter-symbols-template.md +24 -0
- data/docs/dev/import-parameter-symbols.md +6 -0
- data/docs/dev/load-vars-state-demo.md +35 -0
- data/docs/dev/table-column-truncate.md +1 -1
- data/docs/ux-blocks-examples.md +2 -3
- data/examples/import_with_substitution_demo.md +130 -26
- data/examples/imports/organism_template.md +86 -29
- data/lib/cached_nested_file_reader.rb +279 -29
- data/lib/constants.rb +8 -1
- data/lib/env_interface.rb +13 -7
- data/lib/evaluate_shell_expressions.rb +1 -0
- data/lib/fcb.rb +133 -33
- data/lib/format_table.rb +77 -29
- data/lib/fout.rb +5 -0
- data/lib/hash_delegator.rb +1159 -348
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +2 -0
- data/lib/mdoc.rb +13 -11
- data/lib/menu.src.yml +166 -62
- data/lib/menu.yml +143 -59
- data/lib/string_util.rb +80 -0
- data/lib/table_extractor.rb +170 -64
- data/lib/ww.rb +810 -36
- metadata +22 -2
data/lib/ww.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
#!/usr/bin/env -S bundle exec ruby
|
1
2
|
# frozen_string_literal: true
|
2
3
|
|
3
4
|
# encoding=utf-8
|
@@ -5,65 +6,185 @@ require 'bundler/setup' # Bundler enforces gem versions
|
|
5
6
|
require 'pp'
|
6
7
|
require 'stringio'
|
7
8
|
|
9
|
+
# call depth icons
|
10
|
+
DEPTH_ICON = '›'
|
11
|
+
|
12
|
+
# log levels
|
8
13
|
LOG_LEVELS = %i[debug info warn error fatal].freeze
|
9
14
|
|
10
|
-
|
15
|
+
# is enabled
|
16
|
+
private def enable_debugging
|
17
|
+
ENV.fetch('WW', '0').to_i.positive?
|
18
|
+
end
|
19
|
+
|
20
|
+
# is enabled, not silent
|
21
|
+
private def env_show_attribution
|
22
|
+
ENV['WW'] != '0'
|
23
|
+
end
|
24
|
+
|
25
|
+
private def is_new_alg?
|
26
|
+
# use the new algo only if env var is ALG is not empty
|
27
|
+
!ENV.fetch('ALG', '').empty?
|
28
|
+
|
29
|
+
# use the new algo if ALG != 0
|
30
|
+
# ENV.fetch('ALG', '') != '0'
|
31
|
+
end
|
32
|
+
|
33
|
+
# enable application-wide debugging
|
34
|
+
$debug = $DEBUG || enable_debugging
|
35
|
+
|
36
|
+
# no default category
|
37
|
+
$ww_category = nil
|
38
|
+
|
39
|
+
# no default log file
|
40
|
+
$ww_log_file = nil
|
41
|
+
|
42
|
+
# default output to $stderr
|
43
|
+
$ww_output = $stderr
|
44
|
+
unless ($id = ENV.fetch('WW_LOG', '')).empty?
|
45
|
+
alg = is_new_alg? ? '1' : '0'
|
46
|
+
# local log file with timestamp and algo name
|
47
|
+
$ww_log_file = "#{Time.now.utc.strftime '%H-%M-%S'}-#{$id}-#{alg}.log"
|
48
|
+
end
|
11
49
|
|
12
50
|
# attribution in output unless disabled
|
13
|
-
if
|
14
|
-
|
15
|
-
|
51
|
+
if env_show_attribution
|
52
|
+
if $debug
|
53
|
+
# not silent, display notice
|
54
|
+
if $DEBUG
|
55
|
+
# debugging triggered by Ruby debug
|
56
|
+
warn "WW Debugging per $DEBUG ('ruby --debug')"
|
57
|
+
else
|
58
|
+
# debugging triggered by WW environment variable
|
59
|
+
warn 'WW Debugging per environment variable WW'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
if is_new_alg?
|
63
|
+
warn "WW Testing a new algorithm. Control with env var 'ALG'"
|
64
|
+
end
|
16
65
|
end
|
17
66
|
|
67
|
+
# selectively enabled, for general debugging
|
68
|
+
# return the last item in the list
|
18
69
|
def ww(*objs, **kwargs)
|
19
|
-
#
|
70
|
+
# assume the final item is the significant one
|
71
|
+
# allows prefixing to an existing expression and forwarding the result
|
20
72
|
return objs.last unless $debug
|
21
73
|
|
22
74
|
locations = kwargs[:locations] || caller_locations
|
23
75
|
ww0(*objs, **kwargs.merge(locations: locations))
|
24
76
|
end
|
25
77
|
|
26
|
-
#
|
27
|
-
#
|
78
|
+
# output the object and backtrace for the error
|
79
|
+
# abort
|
80
|
+
def wwa(*objs, **kwargs)
|
81
|
+
ww0(*objs,
|
82
|
+
**kwargs.merge(full_backtrace: true,
|
83
|
+
locations: caller_locations))
|
84
|
+
|
85
|
+
exit 1
|
86
|
+
end
|
87
|
+
|
88
|
+
# break into the debugger if enabled
|
89
|
+
def wwb
|
90
|
+
binding.irb if $debug
|
91
|
+
end
|
92
|
+
|
93
|
+
# output the object and backtrace for the error
|
94
|
+
# raise the error for the caller to handle
|
28
95
|
def wwe(*objs, **kwargs)
|
29
96
|
ww0(*objs,
|
30
97
|
**kwargs.merge(full_backtrace: true,
|
31
98
|
locations: caller_locations))
|
32
99
|
|
33
|
-
raise StandardError, objs.first
|
100
|
+
# raise StandardError, objs.first.fetch(:error) || objs.first
|
101
|
+
raise StandardError, objs.first
|
34
102
|
end
|
35
103
|
|
36
104
|
# selectively enabled, for process tracking
|
37
|
-
#
|
105
|
+
# output data and the caller's location
|
38
106
|
def wwp(*objs, **kwargs)
|
39
107
|
return objs.last unless $debug
|
40
108
|
|
41
|
-
|
109
|
+
ww0(*objs,
|
110
|
+
**kwargs.merge(
|
111
|
+
locations: caller_locations[0..0],
|
112
|
+
location_offset: caller_locations.count
|
113
|
+
))
|
42
114
|
end
|
43
115
|
|
44
|
-
#
|
45
|
-
|
46
|
-
#
|
116
|
+
# the return value for a function
|
117
|
+
def wwr(*objs, **kwargs)
|
118
|
+
# assume the final item is the significant one
|
119
|
+
# allows prefixing to an existing expression and forwarding the result
|
120
|
+
return objs.last unless $debug
|
121
|
+
|
122
|
+
ww0(*objs,
|
123
|
+
**kwargs.merge(
|
124
|
+
locations: caller_locations[0..0],
|
125
|
+
location_offset: caller_locations.count
|
126
|
+
))
|
127
|
+
end
|
128
|
+
|
129
|
+
# selectively enabled, for tagged data
|
130
|
+
# the first item is the tag, the rest is data
|
131
|
+
# exclude tags in the list of tags to skip
|
132
|
+
# output data and the caller's location
|
47
133
|
def wwt(*objs, **kwargs)
|
48
|
-
|
134
|
+
# tags to skip
|
135
|
+
return objs.last if !$debug || %i[blocks env fcb].include?(objs.first)
|
49
136
|
|
50
137
|
formatted = ['Tagged', objs.first] + objs[1..]
|
51
|
-
|
138
|
+
ww0(*formatted,
|
139
|
+
**kwargs.merge(
|
140
|
+
locations: caller_locations[0..0],
|
141
|
+
location_offset: caller_locations.count
|
142
|
+
))
|
143
|
+
end
|
144
|
+
|
145
|
+
# enhanced expression wrapper with better context
|
146
|
+
# usage: wwx { some_expression } or wwx(expression)
|
147
|
+
def wwx(expression = nil, **kwargs, &block)
|
148
|
+
if block_given?
|
149
|
+
# Block form: wwx { some_expression }
|
150
|
+
result = block.call
|
151
|
+
return result unless $debug
|
152
|
+
|
153
|
+
# Capture the source location of the block
|
154
|
+
locations = kwargs[:locations] || caller_locations
|
155
|
+
ww0(result, **kwargs.merge(locations: locations, context: 'block'))
|
156
|
+
elsif expression
|
157
|
+
# Direct form: wwx(some_expression)
|
158
|
+
return expression unless $debug
|
159
|
+
|
160
|
+
locations = kwargs[:locations] || caller_locations
|
161
|
+
ww0(expression, **kwargs.merge(locations: locations, context: 'direct'))
|
162
|
+
else
|
163
|
+
# No arguments - just return nil
|
164
|
+
nil
|
165
|
+
end
|
52
166
|
end
|
53
167
|
|
168
|
+
# output the formatted data and location
|
54
169
|
def ww0(*objs,
|
55
|
-
category:
|
170
|
+
category: $ww_category,
|
171
|
+
context: nil,
|
56
172
|
full_backtrace: false,
|
57
173
|
level: :debug,
|
58
174
|
locations: caller_locations,
|
59
|
-
log_file:
|
60
|
-
output: $
|
175
|
+
log_file: $ww_log_file,
|
176
|
+
output: $ww_output,
|
61
177
|
single_line: false,
|
62
|
-
timestamp: false
|
178
|
+
timestamp: false,
|
179
|
+
location_offset: 0)
|
63
180
|
# Format caller information line
|
64
|
-
|
65
|
-
|
66
|
-
|
181
|
+
caller_info_line = lambda do |caller_info, ind|
|
182
|
+
[
|
183
|
+
DEPTH_ICON * (location_offset + locations.count - ind),
|
184
|
+
caller_info.path.deref,
|
185
|
+
caller_info.lineno,
|
186
|
+
caller_info.label
|
187
|
+
].join(' : ')
|
67
188
|
end
|
68
189
|
# Validate log level
|
69
190
|
raise ArgumentError,
|
@@ -71,20 +192,23 @@ def ww0(*objs,
|
|
71
192
|
|
72
193
|
# Generate backtrace
|
73
194
|
backtrace = if full_backtrace
|
74
|
-
locations.map
|
195
|
+
locations.map.with_index do |caller_info, ind|
|
196
|
+
caller_info_line.call(caller_info, ind)
|
197
|
+
end
|
75
198
|
else
|
76
|
-
[caller_info_line(locations.first)]
|
199
|
+
[caller_info_line.call(locations.first, 0)]
|
77
200
|
end
|
78
201
|
|
79
202
|
# Add optional timestamp
|
80
203
|
time_prefix = timestamp ? "[#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}] " : ''
|
81
204
|
|
82
|
-
# Add log level and
|
205
|
+
# Add log level, category, and context prefix
|
83
206
|
level_prefix = "[#{level.to_s.upcase}]"
|
84
207
|
category_prefix = category ? "[#{category}] " : ''
|
208
|
+
context_prefix = context ? "[#{context}] " : ''
|
85
209
|
|
86
210
|
# Combine all parts into the final message
|
87
|
-
header = "#{time_prefix}#{level_prefix} #{category_prefix}"
|
211
|
+
header = "#{time_prefix}#{level_prefix} #{category_prefix}#{context_prefix}"
|
88
212
|
trace = backtrace + objs
|
89
213
|
io = StringIO.new
|
90
214
|
formatted_message = if single_line
|
@@ -95,27 +219,32 @@ def ww0(*objs,
|
|
95
219
|
"#{header}\n#{io.string}"
|
96
220
|
end
|
97
221
|
|
222
|
+
# prefix each line in formatted_message
|
223
|
+
prefix = (' ' * 8).freeze
|
224
|
+
formatted_message = prefix + formatted_message.gsub("\n", "\n#{prefix}")
|
225
|
+
|
98
226
|
# Output to $stderr or specified IO object
|
99
|
-
output.puts formatted_message
|
227
|
+
output.puts "\033[38;2;128;191;191m#{formatted_message}\033[0m"
|
100
228
|
output.flush
|
101
229
|
|
102
230
|
# Optionally log to a file
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
end
|
108
|
-
|
109
|
-
def wwb
|
110
|
-
binding.irb if $debug
|
231
|
+
if log_file
|
232
|
+
File.open(log_file, 'a') do |file|
|
233
|
+
file.puts(formatted_message)
|
234
|
+
end
|
111
235
|
end
|
112
236
|
|
113
|
-
# return the last item in the list, as the label is usually first
|
237
|
+
# Always return the last item in the list, as the label is usually first
|
114
238
|
objs.last
|
115
239
|
end
|
116
240
|
|
117
241
|
class Array
|
118
242
|
unless defined?(deref)
|
243
|
+
|
244
|
+
# trim the backtrace to project source files
|
245
|
+
# exclude vendor and .bundle directories
|
246
|
+
# limit the count to 4
|
247
|
+
# replace the home directory with a .
|
119
248
|
def deref(count = 4)
|
120
249
|
dir_pwd = Dir.pwd
|
121
250
|
map(&:deref).reject do |line|
|
@@ -129,8 +258,653 @@ end
|
|
129
258
|
|
130
259
|
class String
|
131
260
|
unless defined?(deref)
|
261
|
+
# replace the app's directory with a .
|
132
262
|
def deref
|
133
263
|
sub(%r{^#{Dir.pwd}}, '')
|
134
264
|
end
|
135
265
|
end
|
136
266
|
end
|
267
|
+
|
268
|
+
class Array
|
269
|
+
# Count occurrences by method result using tally (Ruby 2.7+)
|
270
|
+
#
|
271
|
+
# @param method [Symbol, String] The method to call on each element
|
272
|
+
# @return [Hash] Hash of method result => count pairs
|
273
|
+
def count_by(method)
|
274
|
+
raise ArgumentError,
|
275
|
+
'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
|
276
|
+
|
277
|
+
if respond_to?(:tally) # Ruby 2.7+
|
278
|
+
map do |item|
|
279
|
+
item.respond_to?(method) ? item.send(method) : nil
|
280
|
+
end.compact.tally
|
281
|
+
else
|
282
|
+
# Fallback for older Ruby versions
|
283
|
+
result = Hash.new(0)
|
284
|
+
each do |item|
|
285
|
+
if item.respond_to?(method)
|
286
|
+
value = item.send(method)
|
287
|
+
result[value] += 1
|
288
|
+
end
|
289
|
+
end
|
290
|
+
result
|
291
|
+
end
|
292
|
+
rescue NoMethodError => err
|
293
|
+
warn "Method #{method} not available on some items: #{err.message}"
|
294
|
+
{}
|
295
|
+
end
|
296
|
+
|
297
|
+
# Use filter_map for combined filtering and mapping (Ruby 2.7+)
|
298
|
+
# Filters elements and transforms them in one pass
|
299
|
+
#
|
300
|
+
# @param method [Symbol, String] The method to call on each element
|
301
|
+
# @param value [Object] The value to match against
|
302
|
+
# @param transform_method [Symbol, String, nil] Optional
|
303
|
+
# method to call on matching elements
|
304
|
+
# @return [Array] Array of transformed matching elements
|
305
|
+
def filter_map_by(method, value, transform_method = nil)
|
306
|
+
raise ArgumentError,
|
307
|
+
'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
|
308
|
+
|
309
|
+
if respond_to?(:filter_map) # Ruby 2.7+
|
310
|
+
filter_map do |item|
|
311
|
+
if item.respond_to?(method) && item.send(method) == value
|
312
|
+
transform_method ? item.send(transform_method) : item
|
313
|
+
end
|
314
|
+
end
|
315
|
+
else
|
316
|
+
# Fallback for older Ruby versions
|
317
|
+
result = []
|
318
|
+
each do |item|
|
319
|
+
if item.respond_to?(method) && item.send(method) == value
|
320
|
+
result << (transform_method ? item.send(transform_method) : item)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
result
|
324
|
+
end
|
325
|
+
rescue NoMethodError => err
|
326
|
+
warn "Method #{method} not available on some items: #{err.message}"
|
327
|
+
[]
|
328
|
+
end
|
329
|
+
|
330
|
+
# Find the first element where the specified method matches a value
|
331
|
+
# Supports both method-based and block-based filtering
|
332
|
+
#
|
333
|
+
# @param method [Symbol, String, nil] The method to call on each element
|
334
|
+
# @param value [Object, nil] The value to match against
|
335
|
+
# @param default [Object, nil] Default value if no match found
|
336
|
+
# @param block [Proc, nil] Optional block for custom filtering logic
|
337
|
+
# @return [Object, nil] The first matching element or default
|
338
|
+
def find_by(method = nil, value = nil, default = nil, &block)
|
339
|
+
if block_given?
|
340
|
+
find(&block)
|
341
|
+
else
|
342
|
+
raise ArgumentError,
|
343
|
+
'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
|
344
|
+
|
345
|
+
find do |item|
|
346
|
+
item.respond_to?(method) && item.send(method) == value
|
347
|
+
end || default
|
348
|
+
end
|
349
|
+
rescue NoMethodError => err
|
350
|
+
warn "Method #{method} not available on some items: #{err.message}"
|
351
|
+
default
|
352
|
+
end
|
353
|
+
|
354
|
+
# Find elements using hash-based conditions
|
355
|
+
# All conditions must match for an element to be included
|
356
|
+
#
|
357
|
+
# @param conditions [Hash] Hash of method => value pairs
|
358
|
+
# @return [Array] Array of matching elements
|
359
|
+
def find_where(conditions = {})
|
360
|
+
find do |item|
|
361
|
+
conditions.all? do |method, value|
|
362
|
+
item.respond_to?(method) && item.send(method) == value
|
363
|
+
end
|
364
|
+
end
|
365
|
+
rescue NoMethodError => err
|
366
|
+
warn "Some methods not available on items: #{err.message}"
|
367
|
+
nil
|
368
|
+
end
|
369
|
+
|
370
|
+
# Match elements using pattern matching (Ruby 2.7+)
|
371
|
+
# Uses grep for pattern matching against method results
|
372
|
+
#
|
373
|
+
# @param method [Symbol, String] The method to call on each element
|
374
|
+
# @param pattern [Regexp, Object] Pattern to match against
|
375
|
+
# @return [Array] Array of matching elements
|
376
|
+
def match_by(method, pattern)
|
377
|
+
raise ArgumentError,
|
378
|
+
'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
|
379
|
+
|
380
|
+
grep { |item| pattern === item.send(method) }
|
381
|
+
rescue NoMethodError => err
|
382
|
+
warn "Method #{method} not available on some items: #{err.message}"
|
383
|
+
[]
|
384
|
+
end
|
385
|
+
|
386
|
+
# Partition elements based on method result
|
387
|
+
#
|
388
|
+
# @param method [Symbol, String] The method to call on each element
|
389
|
+
# @param value [Object] The value to partition by
|
390
|
+
# @return [Array] Array containing [matching_elements, non_matching_elements]
|
391
|
+
def partition_by(method, value)
|
392
|
+
raise ArgumentError,
|
393
|
+
'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
|
394
|
+
|
395
|
+
partition { |item| item.respond_to?(method) && item.send(method) == value }
|
396
|
+
rescue NoMethodError => err
|
397
|
+
warn "Method #{method} not available on some items: #{err.message}"
|
398
|
+
[[], self]
|
399
|
+
end
|
400
|
+
|
401
|
+
# Reject elements where the specified method matches a value
|
402
|
+
# Supports both method-based and block-based filtering
|
403
|
+
#
|
404
|
+
# @param method [Symbol, String, nil] The method to call on each element
|
405
|
+
# @param value [Object, nil] The value to match against
|
406
|
+
# @param block [Proc, nil] Optional block for custom filtering logic
|
407
|
+
# @return [Array] Array of non-matching elements
|
408
|
+
def reject_by(method = nil, value = nil, &block)
|
409
|
+
if block_given?
|
410
|
+
reject(&block)
|
411
|
+
else
|
412
|
+
raise ArgumentError,
|
413
|
+
'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
|
414
|
+
|
415
|
+
reject { |item| item.respond_to?(method) && item.send(method) == value }
|
416
|
+
end
|
417
|
+
rescue NoMethodError => err
|
418
|
+
warn "Method #{method} not available on some items: #{err.message}"
|
419
|
+
self
|
420
|
+
end
|
421
|
+
|
422
|
+
# Select elements where the specified method matches a value
|
423
|
+
# Supports both method-based and block-based filtering
|
424
|
+
#
|
425
|
+
# @param method [Symbol, String, nil] The method to call on each element
|
426
|
+
# @param value [Object, nil] The value to match against
|
427
|
+
# @param block [Proc, nil] Optional block for custom filtering logic
|
428
|
+
# @return [Array] Array of matching elements
|
429
|
+
def select_by(method = nil, value = nil, &block)
|
430
|
+
if block_given?
|
431
|
+
select(&block)
|
432
|
+
else
|
433
|
+
raise ArgumentError,
|
434
|
+
'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
|
435
|
+
|
436
|
+
select { |item| item.respond_to?(method) && item.send(method) == value }
|
437
|
+
end
|
438
|
+
rescue NoMethodError => err
|
439
|
+
warn "Method #{method} not available on some items: #{err.message}"
|
440
|
+
[]
|
441
|
+
end
|
442
|
+
|
443
|
+
# Select elements using hash-based conditions
|
444
|
+
# All conditions must match for an element to be included
|
445
|
+
#
|
446
|
+
# @param conditions [Hash] Hash of method => value pairs
|
447
|
+
# @return [Array] Array of matching elements
|
448
|
+
def select_where(conditions = {})
|
449
|
+
select do |item|
|
450
|
+
conditions.all? do |method, value|
|
451
|
+
item.respond_to?(method) && item.send(method) == value
|
452
|
+
end
|
453
|
+
end
|
454
|
+
rescue NoMethodError => err
|
455
|
+
warn "Some methods not available on items: #{err.message}"
|
456
|
+
[]
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
return if $PROGRAM_NAME != __FILE__
|
461
|
+
|
462
|
+
require 'minitest/autorun'
|
463
|
+
|
464
|
+
class TestWwFunction < Minitest::Test
|
465
|
+
def setup
|
466
|
+
# Save original global state
|
467
|
+
@original_debug = $debug
|
468
|
+
@original_ww_log_file = $ww_log_file
|
469
|
+
@original_ww_output = $ww_output
|
470
|
+
@original_ww_category = $ww_category
|
471
|
+
|
472
|
+
# Redirect output to capture it - ensure it's writable
|
473
|
+
@output_buffer = StringIO.new
|
474
|
+
@output_buffer.sync = true
|
475
|
+
$ww_output = @output_buffer
|
476
|
+
$ww_category = nil
|
477
|
+
end
|
478
|
+
|
479
|
+
def teardown
|
480
|
+
# Restore original global state
|
481
|
+
$debug = @original_debug
|
482
|
+
$ww_log_file = @original_ww_log_file
|
483
|
+
$ww_output = @original_ww_output
|
484
|
+
$ww_category = @original_ww_category
|
485
|
+
|
486
|
+
# Clean up any test log files
|
487
|
+
Dir.glob('test*.log').each { |f| FileUtils.rm_f(f) }
|
488
|
+
end
|
489
|
+
|
490
|
+
# Core functionality tests
|
491
|
+
def test_ww_returns_last_item_with_debug_disabled
|
492
|
+
$debug = false
|
493
|
+
$ww_log_file = nil
|
494
|
+
|
495
|
+
# Test with single item
|
496
|
+
result = ww('single_item')
|
497
|
+
assert_equal 'single_item', result,
|
498
|
+
'ww should return the single item when debug is disabled'
|
499
|
+
|
500
|
+
# Test with multiple items
|
501
|
+
result = ww('first', 'second', 'third')
|
502
|
+
assert_equal 'third', result,
|
503
|
+
'ww should return the last item when debug is disabled'
|
504
|
+
|
505
|
+
# Test with various data types
|
506
|
+
result = ww(1, 'string', :symbol, [1, 2, 3])
|
507
|
+
assert_equal [1, 2, 3], result,
|
508
|
+
'ww should return the last item regardless of type'
|
509
|
+
|
510
|
+
# Verify no output when debug is disabled
|
511
|
+
assert_empty @output_buffer.string,
|
512
|
+
'No output should be generated when debug is disabled'
|
513
|
+
end
|
514
|
+
|
515
|
+
def test_ww_returns_last_item_with_debug_enabled_no_log_file
|
516
|
+
$debug = true
|
517
|
+
$ww_log_file = nil
|
518
|
+
|
519
|
+
# Test with single item
|
520
|
+
result = ww('single_item')
|
521
|
+
assert_equal 'single_item', result,
|
522
|
+
'ww should return the single item when debug is enabled and no log file'
|
523
|
+
|
524
|
+
# Test with multiple items
|
525
|
+
result = ww('first', 'second', 'third')
|
526
|
+
assert_equal 'third', result,
|
527
|
+
'ww should return the last item when debug is enabled and no log file'
|
528
|
+
|
529
|
+
# Test with mixed types
|
530
|
+
result = ww(42, 'hello', :world, { key: 'value' })
|
531
|
+
assert_equal({ key: 'value' }, result,
|
532
|
+
'ww should return the last item even with hash')
|
533
|
+
|
534
|
+
# Verify output is generated when debug is enabled
|
535
|
+
refute_empty @output_buffer.string,
|
536
|
+
'Output should be generated when debug is enabled'
|
537
|
+
end
|
538
|
+
|
539
|
+
def test_ww_returns_last_item_with_debug_enabled_with_log_file
|
540
|
+
$debug = true
|
541
|
+
$ww_log_file = 'test_ww.log'
|
542
|
+
|
543
|
+
begin
|
544
|
+
# Test with single item
|
545
|
+
result = ww('single_item')
|
546
|
+
assert_equal 'single_item', result,
|
547
|
+
'ww should return the single item when debug is enabled with log file'
|
548
|
+
|
549
|
+
# Test with multiple items
|
550
|
+
result = ww('first', 'second', 'third')
|
551
|
+
assert_equal 'third', result,
|
552
|
+
'ww should return the last item when debug is enabled with log file'
|
553
|
+
|
554
|
+
# Test with various data types
|
555
|
+
result = ww(1, 'string', :symbol, [1, 2, 3])
|
556
|
+
assert_equal [1, 2, 3], result,
|
557
|
+
'ww should return the last item regardless of type with log file'
|
558
|
+
|
559
|
+
# Verify log file was created and contains content
|
560
|
+
assert File.exist?('test_ww.log'), 'Log file should be created'
|
561
|
+
refute_empty File.read('test_ww.log'), 'Log file should contain content'
|
562
|
+
ensure
|
563
|
+
# Clean up test log file
|
564
|
+
FileUtils.rm_f('test_ww.log')
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
def test_ww_with_named_options
|
569
|
+
$debug = false
|
570
|
+
|
571
|
+
# Test that named options don't affect the return value
|
572
|
+
result = ww('first', 'second', category: 'test', level: :info)
|
573
|
+
assert_equal 'second', result,
|
574
|
+
'ww should return the last item even with named options'
|
575
|
+
|
576
|
+
# Test with single item and options
|
577
|
+
result = ww('only_item', timestamp: true, single_line: true)
|
578
|
+
assert_equal 'only_item', result,
|
579
|
+
'ww should return the single item even with named options'
|
580
|
+
|
581
|
+
# Test with various option combinations
|
582
|
+
result = ww('a', 'b', 'c', category: 'testing', level: :warn,
|
583
|
+
timestamp: true, single_line: false)
|
584
|
+
assert_equal 'c', result,
|
585
|
+
'ww should return the last item with complex options'
|
586
|
+
end
|
587
|
+
|
588
|
+
# Test all ww function variants
|
589
|
+
def test_wwr_function_returns_last_item
|
590
|
+
$debug = true
|
591
|
+
$ww_log_file = nil
|
592
|
+
|
593
|
+
# Test wwr with multiple items
|
594
|
+
result = wwr('first', 'second', 'third')
|
595
|
+
assert_equal 'third', result, 'wwr should return the last item'
|
596
|
+
|
597
|
+
# Test wwr with single item
|
598
|
+
result = wwr('only_item')
|
599
|
+
assert_equal 'only_item', result, 'wwr should return the single item'
|
600
|
+
|
601
|
+
# Test wwr with debug disabled
|
602
|
+
$debug = false
|
603
|
+
result = wwr('a', 'b', 'c')
|
604
|
+
assert_equal 'c', result,
|
605
|
+
'wwr should return the last item even when debug is disabled'
|
606
|
+
end
|
607
|
+
|
608
|
+
def test_wwp_function_returns_last_item
|
609
|
+
$debug = true
|
610
|
+
$ww_log_file = nil
|
611
|
+
|
612
|
+
# Test wwp with multiple items
|
613
|
+
result = wwp('first', 'second', 'third')
|
614
|
+
assert_equal 'third', result, 'wwp should return the last item'
|
615
|
+
|
616
|
+
# Test wwp with single item
|
617
|
+
result = wwp('only_item')
|
618
|
+
assert_equal 'only_item', result, 'wwp should return the single item'
|
619
|
+
|
620
|
+
# Test wwp with debug disabled
|
621
|
+
$debug = false
|
622
|
+
result = wwp('a', 'b', 'c')
|
623
|
+
assert_equal 'c', result,
|
624
|
+
'wwp should return the last item even when debug is disabled'
|
625
|
+
end
|
626
|
+
|
627
|
+
def test_wwt_function_returns_last_item
|
628
|
+
$debug = true
|
629
|
+
$ww_log_file = nil
|
630
|
+
|
631
|
+
# Test wwt with multiple items (first item is tag)
|
632
|
+
result = wwt(:test_tag, 'first', 'second', 'third')
|
633
|
+
assert_equal 'third', result, 'wwt should return the last item'
|
634
|
+
|
635
|
+
# Test wwt with single data item after tag
|
636
|
+
result = wwt(:tag, 'only_data_item')
|
637
|
+
assert_equal 'only_data_item', result,
|
638
|
+
'wwt should return the single data item'
|
639
|
+
|
640
|
+
# Test wwt with skipped tags
|
641
|
+
result = wwt(:blocks, 'data')
|
642
|
+
assert_equal 'data', result, 'wwt should return data even for skipped tags'
|
643
|
+
|
644
|
+
# Test wwt with debug disabled
|
645
|
+
$debug = false
|
646
|
+
result = wwt(:any_tag, 'a', 'b', 'c')
|
647
|
+
assert_equal 'c', result,
|
648
|
+
'wwt should return the last item even when debug is disabled'
|
649
|
+
end
|
650
|
+
|
651
|
+
def test_wwt_multiline_continuation
|
652
|
+
$debug = true
|
653
|
+
$ww_log_file = nil
|
654
|
+
|
655
|
+
# Test multiline continuation pattern
|
656
|
+
var1 = 'test_value'
|
657
|
+
result = wwt :tag1, \
|
658
|
+
var1
|
659
|
+
assert_equal 'test_value', result,
|
660
|
+
'wwt should work with multiline continuation'
|
661
|
+
|
662
|
+
# Test with complex expression
|
663
|
+
complex_var = [1, 2, 3]
|
664
|
+
result = wwt(:processing, \
|
665
|
+
complex_var.map { |x| x * 2 })
|
666
|
+
assert_equal [2, 4, 6], result,
|
667
|
+
'wwt should work with complex expressions in multiline'
|
668
|
+
|
669
|
+
# Test with debug disabled
|
670
|
+
$debug = false
|
671
|
+
simple_var = 'no_debug'
|
672
|
+
result = wwt :disabled, \
|
673
|
+
simple_var
|
674
|
+
assert_equal 'no_debug', result,
|
675
|
+
'wwt should return correct value even with debug disabled'
|
676
|
+
end
|
677
|
+
|
678
|
+
def test_ww_multiline_continuation
|
679
|
+
$debug = true
|
680
|
+
$ww_log_file = nil
|
681
|
+
|
682
|
+
# Test ww with multiline continuation
|
683
|
+
var1 = 'test_value'
|
684
|
+
result = ww \
|
685
|
+
var1
|
686
|
+
assert_equal 'test_value', result,
|
687
|
+
'ww should work with multiline continuation'
|
688
|
+
|
689
|
+
# Test with multiple items in multiline
|
690
|
+
result = ww 'prefix', \
|
691
|
+
'value'
|
692
|
+
assert_equal 'value', result,
|
693
|
+
'ww should work with multiple items in multiline'
|
694
|
+
|
695
|
+
# Test with debug disabled
|
696
|
+
$debug = false
|
697
|
+
result = ww \
|
698
|
+
'no_debug_value'
|
699
|
+
assert_equal 'no_debug_value', result,
|
700
|
+
'ww should return correct value with debug disabled'
|
701
|
+
end
|
702
|
+
|
703
|
+
def test_wwa_function_behavior
|
704
|
+
$debug = true
|
705
|
+
|
706
|
+
# Test that wwa exits (we can't easily test exit behavior in minitest)
|
707
|
+
# So we'll just verify it would call ww0 properly by testing the structure
|
708
|
+
# Note: wwa calls exit, so we can't test it directly without special handling
|
709
|
+
skip 'wwa exits the program, cannot test directly in minitest'
|
710
|
+
end
|
711
|
+
|
712
|
+
def test_wwe_function_behavior
|
713
|
+
$debug = true
|
714
|
+
|
715
|
+
# Test that wwe raises an error
|
716
|
+
assert_raises(StandardError) do
|
717
|
+
wwe('error message')
|
718
|
+
end
|
719
|
+
|
720
|
+
# Test that wwe raises with the first object as the error message
|
721
|
+
error = assert_raises(StandardError) do
|
722
|
+
wwe('custom error', 'additional', 'data')
|
723
|
+
end
|
724
|
+
assert_equal 'custom error', error.message
|
725
|
+
end
|
726
|
+
|
727
|
+
# Edge case tests
|
728
|
+
def test_edge_cases
|
729
|
+
$debug = false
|
730
|
+
|
731
|
+
# Test with nil values
|
732
|
+
result = ww(nil, 'not_nil')
|
733
|
+
assert_equal 'not_nil', result, 'ww should handle nil values correctly'
|
734
|
+
|
735
|
+
# Test with empty array as last item
|
736
|
+
result = ww('first', [])
|
737
|
+
assert_equal [], result,
|
738
|
+
'ww should return empty array if it is the last item'
|
739
|
+
|
740
|
+
# Test with false as last item
|
741
|
+
result = ww('first', false)
|
742
|
+
assert_equal false, result, 'ww should return false if it is the last item'
|
743
|
+
|
744
|
+
# Test with zero as last item
|
745
|
+
result = ww('first', 0)
|
746
|
+
assert_equal 0, result, 'ww should return zero if it is the last item'
|
747
|
+
|
748
|
+
# Test with empty string as last item
|
749
|
+
result = ww('first', '')
|
750
|
+
assert_equal '', result,
|
751
|
+
'ww should return empty string if it is the last item'
|
752
|
+
end
|
753
|
+
|
754
|
+
def test_complex_data_structures
|
755
|
+
$debug = false
|
756
|
+
|
757
|
+
# Test with nested arrays
|
758
|
+
nested = [1, [2, [3, 4]], 5]
|
759
|
+
result = ww('start', nested)
|
760
|
+
assert_equal nested, result, 'ww should handle nested arrays'
|
761
|
+
|
762
|
+
# Test with complex hash
|
763
|
+
complex_hash = {
|
764
|
+
users: [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }],
|
765
|
+
metadata: { created: Time.now, version: '1.0' }
|
766
|
+
}
|
767
|
+
result = ww('prefix', complex_hash)
|
768
|
+
assert_equal complex_hash, result, 'ww should handle complex hashes'
|
769
|
+
|
770
|
+
# Test with objects
|
771
|
+
string_obj = String.new('test')
|
772
|
+
result = ww('object', string_obj)
|
773
|
+
assert_equal string_obj, result, 'ww should handle object instances'
|
774
|
+
end
|
775
|
+
|
776
|
+
def test_options_validation
|
777
|
+
$debug = true
|
778
|
+
$ww_log_file = nil
|
779
|
+
|
780
|
+
# Test valid log levels
|
781
|
+
%i[debug info warn error fatal].each do |level|
|
782
|
+
result = ww('test', level: level)
|
783
|
+
assert_equal 'test', result, "ww should work with log level #{level}"
|
784
|
+
end
|
785
|
+
|
786
|
+
# Test invalid log level raises error
|
787
|
+
assert_raises(ArgumentError) do
|
788
|
+
ww('test', level: :invalid)
|
789
|
+
end
|
790
|
+
end
|
791
|
+
|
792
|
+
def test_output_formatting_options
|
793
|
+
$debug = true
|
794
|
+
$ww_log_file = nil
|
795
|
+
|
796
|
+
# Create fresh buffer for this test
|
797
|
+
fresh_buffer = StringIO.new
|
798
|
+
$ww_output = fresh_buffer
|
799
|
+
|
800
|
+
# Test single_line option
|
801
|
+
ww('test', 'data', single_line: true)
|
802
|
+
output = fresh_buffer.string
|
803
|
+
refute_includes output, "\n [",
|
804
|
+
'single_line should format output on one line'
|
805
|
+
|
806
|
+
# Reset buffer
|
807
|
+
fresh_buffer = StringIO.new
|
808
|
+
$ww_output = fresh_buffer
|
809
|
+
|
810
|
+
# Test timestamp option
|
811
|
+
ww('test', timestamp: true)
|
812
|
+
output = fresh_buffer.string
|
813
|
+
assert_match(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]/, output,
|
814
|
+
'timestamp should be included in output')
|
815
|
+
|
816
|
+
# Reset buffer
|
817
|
+
fresh_buffer = StringIO.new
|
818
|
+
$ww_output = fresh_buffer
|
819
|
+
|
820
|
+
# Test category option
|
821
|
+
ww('test', category: 'MYCATEGORY')
|
822
|
+
output = fresh_buffer.string
|
823
|
+
assert_includes output, '[MYCATEGORY]',
|
824
|
+
'category should be included in output'
|
825
|
+
end
|
826
|
+
|
827
|
+
def test_location_information
|
828
|
+
$debug = true
|
829
|
+
$ww_log_file = nil
|
830
|
+
|
831
|
+
# Create fresh buffer for this test
|
832
|
+
fresh_buffer = StringIO.new
|
833
|
+
$ww_output = fresh_buffer
|
834
|
+
|
835
|
+
# Test that location information is included
|
836
|
+
ww('location test')
|
837
|
+
output = fresh_buffer.string
|
838
|
+
|
839
|
+
# Should include file path and line number information
|
840
|
+
assert_includes output, 'lib/ww.rb', 'output should include filename'
|
841
|
+
assert_match(/\s:\s\d+\s:/, output,
|
842
|
+
'output should include line number with format')
|
843
|
+
end
|
844
|
+
|
845
|
+
def test_full_backtrace_option
|
846
|
+
$debug = true
|
847
|
+
$ww_log_file = nil
|
848
|
+
|
849
|
+
# Create fresh buffer for this test
|
850
|
+
fresh_buffer = StringIO.new
|
851
|
+
$ww_output = fresh_buffer
|
852
|
+
|
853
|
+
# Test full_backtrace option
|
854
|
+
ww('test', full_backtrace: true)
|
855
|
+
output = fresh_buffer.string
|
856
|
+
|
857
|
+
# With full backtrace, we should see multiple stack levels
|
858
|
+
# (This is hard to test precisely since it depends on call stack depth)
|
859
|
+
refute_empty output, 'full_backtrace should produce output'
|
860
|
+
end
|
861
|
+
|
862
|
+
def test_multiple_consecutive_calls
|
863
|
+
$debug = false
|
864
|
+
|
865
|
+
# Test that multiple calls work correctly
|
866
|
+
results = []
|
867
|
+
results << ww('call1', 'result1')
|
868
|
+
results << ww('call2a', 'call2b', 'result2')
|
869
|
+
results << ww('call3a', 'call3b', 'call3c', 'result3')
|
870
|
+
|
871
|
+
assert_equal %w[result1 result2 result3], results,
|
872
|
+
'Multiple consecutive calls should work correctly'
|
873
|
+
end
|
874
|
+
|
875
|
+
def test_return_value_consistency_across_debug_states
|
876
|
+
# Test the same call with debug enabled and disabled
|
877
|
+
test_args = %w[first second third]
|
878
|
+
|
879
|
+
# With debug disabled
|
880
|
+
$debug = false
|
881
|
+
$ww_log_file = nil
|
882
|
+
result_no_debug = ww(*test_args)
|
883
|
+
|
884
|
+
# With debug enabled, no log file
|
885
|
+
$debug = true
|
886
|
+
$ww_log_file = nil
|
887
|
+
result_debug_no_log = ww(*test_args)
|
888
|
+
|
889
|
+
# With debug enabled, with log file
|
890
|
+
$debug = true
|
891
|
+
$ww_log_file = 'consistency_test.log'
|
892
|
+
begin
|
893
|
+
result_debug_with_log = ww(*test_args)
|
894
|
+
ensure
|
895
|
+
FileUtils.rm_f('consistency_test.log')
|
896
|
+
end
|
897
|
+
|
898
|
+
# All should return the same value
|
899
|
+
assert_equal 'third', result_no_debug,
|
900
|
+
'Should return last item with debug disabled'
|
901
|
+
assert_equal 'third', result_debug_no_log,
|
902
|
+
'Should return last item with debug enabled, no log'
|
903
|
+
assert_equal 'third', result_debug_with_log,
|
904
|
+
'Should return last item with debug enabled, with log'
|
905
|
+
assert_equal result_no_debug, result_debug_no_log,
|
906
|
+
'Results should be consistent across debug states'
|
907
|
+
assert_equal result_no_debug, result_debug_with_log,
|
908
|
+
'Results should be consistent across log file states'
|
909
|
+
end
|
910
|
+
end
|