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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/Gemfile.lock +1 -1
  4. data/Rakefile +5 -4
  5. data/bats/block-type-ux-echo-hash-transform.bats +1 -1
  6. data/bats/block-type-ux-exec-hash-transform.bats +8 -0
  7. data/bats/block-type-ux-exec-hash.bats +15 -0
  8. data/bats/block-type-ux-force.bats +9 -0
  9. data/bats/block-type-ux-formats.bats +8 -0
  10. data/bats/block-type-ux-no-name.bats +8 -0
  11. data/bats/block-type-ux-readonly.bats +1 -1
  12. data/bats/block-type-ux-row-format.bats +1 -1
  13. data/bats/command-substitution-options.bats +2 -2
  14. data/bats/import-directive-line-continuation.bats +9 -0
  15. data/bats/import-directive-parameter-symbols.bats +9 -0
  16. data/bats/import-duplicates.bats +4 -2
  17. data/bats/import-parameter-symbols.bats +8 -0
  18. data/bats/markup.bats +1 -1
  19. data/bats/option-expansion.bats +1 -1
  20. data/bats/options.bats +1 -1
  21. data/bats/table-column-truncate.bats +1 -1
  22. data/bats/table.bats +1 -1
  23. data/bats/test_helper.bash +4 -3
  24. data/bin/tab_completion.sh +5 -1
  25. data/docs/dev/block-type-ux-echo-hash-transform.md +14 -12
  26. data/docs/dev/block-type-ux-exec-hash-transform.md +37 -0
  27. data/docs/dev/block-type-ux-exec-hash.md +93 -0
  28. data/docs/dev/block-type-ux-force.md +20 -0
  29. data/docs/dev/block-type-ux-formats.md +58 -0
  30. data/docs/dev/block-type-ux-no-name.md +17 -0
  31. data/docs/dev/block-type-ux-row-format.md +1 -1
  32. data/docs/dev/hexdump_format.md +267 -0
  33. data/docs/dev/import/parameter-symbols.md +6 -0
  34. data/docs/dev/import-directive-line-continuation.md +6 -0
  35. data/docs/dev/import-directive-parameter-symbols.md +9 -0
  36. data/docs/dev/import-parameter-symbols-template.md +24 -0
  37. data/docs/dev/import-parameter-symbols.md +6 -0
  38. data/docs/dev/load-vars-state-demo.md +35 -0
  39. data/docs/dev/table-column-truncate.md +1 -1
  40. data/docs/ux-blocks-examples.md +2 -3
  41. data/examples/import_with_substitution_demo.md +130 -26
  42. data/examples/imports/organism_template.md +86 -29
  43. data/lib/cached_nested_file_reader.rb +279 -29
  44. data/lib/constants.rb +8 -1
  45. data/lib/env_interface.rb +13 -7
  46. data/lib/evaluate_shell_expressions.rb +1 -0
  47. data/lib/fcb.rb +133 -33
  48. data/lib/format_table.rb +77 -29
  49. data/lib/fout.rb +5 -0
  50. data/lib/hash_delegator.rb +1159 -348
  51. data/lib/markdown_exec/version.rb +1 -1
  52. data/lib/markdown_exec.rb +2 -0
  53. data/lib/mdoc.rb +13 -11
  54. data/lib/menu.src.yml +166 -62
  55. data/lib/menu.yml +143 -59
  56. data/lib/string_util.rb +80 -0
  57. data/lib/table_extractor.rb +170 -64
  58. data/lib/ww.rb +810 -36
  59. 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
- $debug = $DEBUG || !ENV['WW'].nil?
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 $debug && ENV['WW_MINIMUM'].nil?
14
- warn "WW Debugging per $DEBUG ('ruby --debug')" if $DEBUG
15
- warn 'WW Debugging per environment variable WW' unless ENV['WW'].nil?
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
- # return the last item in the list, as the label is usually first
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
- # select enabled, for exceptions
27
- # print a data object for the error, and the failing line
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[:error]
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
- # print the failing line
105
+ # output data and the caller's location
38
106
  def wwp(*objs, **kwargs)
39
107
  return objs.last unless $debug
40
108
 
41
- ww(*objs, **kwargs.merge(locations: caller_locations[0..0]))
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
- # selectively enabled, for tagged
45
- # print the failing line
46
- # eg wwt :line, 'data:', data
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
- return objs.last if !$debug || %i[env].include?(objs.first)
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
- ww(*formatted, **kwargs.merge(locations: caller_locations[0..0]))
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: nil,
170
+ category: $ww_category,
171
+ context: nil,
56
172
  full_backtrace: false,
57
173
  level: :debug,
58
174
  locations: caller_locations,
59
- log_file: nil,
60
- output: $stderr,
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
- def caller_info_line(caller_info)
65
- "#{caller_info.lineno} : #{caller_info.path.sub(%r{^#{Dir.pwd}},
66
- '')} : #{caller_info.label}"
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 { |caller_info| caller_info_line(caller_info) }
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 category prefix
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
- return objs.size == 1 ? objs.first : objs unless log_file
104
-
105
- File.open(log_file, 'a') do |file|
106
- file.puts(formatted_message)
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