debugtrace 0.1.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.
data/lib/debugtrace.rb ADDED
@@ -0,0 +1,669 @@
1
+ # main.rb
2
+ # (C) 2025 Masato Kokubo
3
+ require 'thread'
4
+ require 'logger'
5
+
6
+ # Require necessary files
7
+ require_relative 'debugtrace/version'
8
+ require_relative 'debugtrace/common'
9
+ require_relative 'debugtrace/config'
10
+ require_relative 'debugtrace/log_buffer'
11
+ require_relative 'debugtrace/loggers'
12
+ require_relative 'debugtrace/print_options'
13
+ require_relative 'debugtrace/state'
14
+
15
+ module DebugTrace
16
+ # class Error < StandardError; end
17
+ @@AUTHOR = 'Masato Kokubo <masatokokubo@gmail.com>'
18
+ # @@VERSION = '1.0.0 dev 1'
19
+
20
+ # Configuration values
21
+ @@config = nil
22
+
23
+ def self.config
24
+ return @@config
25
+ end
26
+
27
+ # A Mutex for thread safety
28
+ @@thread_mutex = Mutex.new
29
+
30
+ # Hash (int: State) of thread id to a trace state
31
+ @@state_hash = {}
32
+
33
+ # Before thread id
34
+ @@before_thread_id = 0
35
+
36
+ # The last output content
37
+ @@last_log_buff = nil
38
+
39
+ # Reflected objects
40
+ @@reflected_objects = []
41
+
42
+ # The logger used by DebugTrace-py
43
+ @@logger = nil
44
+
45
+ def self.initialize(config_path = './debugtrace.yml')
46
+ @@config = Config.new(config_path)
47
+
48
+ @@last_log_buff = LogBuffer.new(@@config.maximum_data_output_width)
49
+
50
+ # Decide the logger class
51
+ case @@config.logger_name.downcase
52
+ when 'stdout'
53
+ @@logger = StdOutLogger.new(@@config)
54
+ when 'stderr'
55
+ @@logger = StdErrLogger.new(@@config)
56
+ when 'logger'
57
+ @@logger = LoggerLogger.new(@@config)
58
+ when /^file:/
59
+ @@logger = FileLogger.new(@@config)
60
+ else
61
+ Pr._print("debugtrace: (#{@@config.config_path}) logger = #{@@config.logger_name} is unknown", STDERR)
62
+ end
63
+
64
+ if @@config.enabled?
65
+ ruby_version = RUBY_VERSION
66
+ @@logger.print("DebugTrace-rb #{DebugTrace::VERSION} on Ruby #{ruby_version}")
67
+ @@logger.print(" config file path: #{@@config.config_path}")
68
+ @@logger.print(" logger: #{@@logger}")
69
+ end
70
+ end
71
+
72
+ class PrintOptions
73
+ attr_reader :force_reflection, :output_private, :output_method, :minimum_output_count, :minimum_output_length,
74
+ :collection_limit, :bytes_limit, :string_limit, :reflection_nest_limit
75
+
76
+ def initialize(
77
+ force_reflection,
78
+ output_private,
79
+ output_method,
80
+ minimum_output_count,
81
+ minimum_output_length,
82
+ collection_limit,
83
+ bytes_limit,
84
+ string_limit,
85
+ reflection_nest_limit
86
+ )
87
+ @force_reflection = force_reflection
88
+ @output_private = output_private
89
+ @output_method = output_method
90
+ @minimum_output_count = minimum_output_count == -1 ? DebugTrace.config.minimum_output_count : minimum_output_count
91
+ @minimum_output_length = minimum_output_length == -1 ? DebugTrace.config.minimum_output_length : minimum_output_length
92
+ @collection_limit = collection_limit == -1 ? DebugTrace.config.collection_limit : collection_limit
93
+ @bytes_limit = bytes_limit == -1 ? DebugTrace.config.bytes_limit : bytes_limit
94
+ @string_limit = string_limit == -1 ? DebugTrace.config.string_limit : string_limit
95
+ @reflection_nest_limit = reflection_nest_limit == -1 ? DebugTrace.config.reflection_nest_limit : reflection_nest_limit
96
+ end
97
+ end
98
+
99
+ def self.current_state
100
+ thread_id = Thread.current.object_id
101
+
102
+ if @@state_hash.key?(thread_id)
103
+ state = @@state_hash[thread_id]
104
+ else
105
+ state = State.new(thread_id)
106
+ @@state_hash[thread_id] = state
107
+ end
108
+
109
+ state
110
+ end
111
+
112
+ def self.get_indent_string(nest_level, data_nest_level)
113
+ indent_str = @@config.indent_string * [[0, nest_level].max, @@config.maximum_indents].min
114
+ data_indent_str = @@config.data_indent_string * [[0, data_nest_level].max, @@config.maximum_indents].min
115
+ indent_str + data_indent_str
116
+ end
117
+
118
+ def self.to_string(name, value, print_options)
119
+ buff = LogBuffer.new(@@config.maximum_data_output_width)
120
+
121
+ separator = ''
122
+ unless name.empty?
123
+ buff.append(name)
124
+ separator = @@config.varname_value_separator
125
+ end
126
+
127
+ case value
128
+ when nil
129
+ # None
130
+ buff.no_break_append(separator).append('nil')
131
+ when String
132
+ # String
133
+ value_buff = to_string_str(value, print_options)
134
+ buff.append_buffer(separator, value_buff)
135
+ when Integer, Float, Date, Time, DateTime
136
+ # int, float, Date, Time, DateTime
137
+ buff.no_break_append(separator).append(value.to_s)
138
+ when Array, Set, Hash
139
+ # list, set, tuple, dict
140
+ value_buff = to_string_iterable(value, print_options)
141
+ buff.append_buffer(separator, value_buff)
142
+ else
143
+ has_str, has_repr = has_str_repr_method(value)
144
+ value_buff = LogBuffer.new(@@config.maximum_data_output_width)
145
+ if !print_options.force_reflection && (has_str || has_repr)
146
+ # has to_s or inspect method
147
+ if has_repr
148
+ value_buff.append('inspect(): ')
149
+ value_buff.no_break_append(value.inspect)
150
+ else
151
+ value_buff.append('to_s(): ')
152
+ value_buff.no_break_append(value.to_s)
153
+ end
154
+ buff.append_buffer(separator, value_buff)
155
+ else
156
+ # use reflection
157
+ if @@reflected_objects.any? { |obj| value.equal?(obj) }
158
+ # cyclic reference
159
+ value_buff.no_break_append(@@config.cyclic_reference_string)
160
+ elsif @@reflected_objects.length > print_options.reflection_nest_limit
161
+ # over reflection level limitation
162
+ value_buff.no_break_append(@@config.limit_string)
163
+ else
164
+ @@reflected_objects.push(value)
165
+ value_buff = to_string_reflection(value, print_options)
166
+ @@reflected_objects.pop
167
+ end
168
+ buff.append_buffer(separator, value_buff)
169
+ end
170
+ end
171
+
172
+ buff
173
+ end
174
+
175
+ def self.to_string_str(value, print_options)
176
+ has_single_quote = false
177
+ has_double_quote = false
178
+ single_quote_buff = LogBuffer.new(@@config.maximum_data_output_width)
179
+ double_quote_buff = LogBuffer.new(@@config.maximum_data_output_width)
180
+
181
+ if value.length >= @@config.minimum_output_length
182
+ single_quote_buff.no_break_append("(")
183
+ single_quote_buff.no_break_append(sprintf(@@config.length_format, value.length))
184
+ single_quote_buff.no_break_append(")")
185
+ double_quote_buff.no_break_append("(")
186
+ double_quote_buff.no_break_append(sprintf(@@config.length_format, value.length))
187
+ double_quote_buff.no_break_append(")")
188
+ end
189
+
190
+ single_quote_buff.no_break_append("'")
191
+ double_quote_buff.no_break_append('"')
192
+
193
+ count = 1
194
+ value.each_char do |char|
195
+ if count > print_options.string_limit
196
+ single_quote_buff.no_break_append(@@config.limit_string)
197
+ double_quote_buff.no_break_append(@@config.limit_string)
198
+ break
199
+ end
200
+ case char
201
+ when "'"
202
+ single_quote_buff.no_break_append("\\'")
203
+ double_quote_buff.no_break_append(char)
204
+ has_single_quote = true
205
+ when '"'
206
+ single_quote_buff.no_break_append(char)
207
+ double_quote_buff.no_break_append("\\\"")
208
+ has_double_quote = true
209
+ when '\\'
210
+ single_quote_buff.no_break_append('\\\\')
211
+ double_quote_buff.no_break_append('\\\\')
212
+ when '\n'
213
+ single_quote_buff.no_break_append('\\n')
214
+ double_quote_buff.no_break_append('\\n')
215
+ when '\r'
216
+ single_quote_buff.no_break_append('\\r')
217
+ double_quote_buff.no_break_append('\\r')
218
+ when '\t'
219
+ single_quote_buff.no_break_append('\\t')
220
+ double_quote_buff.no_break_append('\\t')
221
+ when ("\0".."\37").include?(char)
222
+ num_str = format('%02X', char.ord)
223
+ single_quote_buff.no_break_append("\\x" + num_str)
224
+ double_quote_buff.no_break_append("\\x" + num_str)
225
+ else
226
+ single_quote_buff.no_break_append(char)
227
+ double_quote_buff.no_break_append(char)
228
+ end
229
+ count += 1
230
+ end
231
+
232
+ double_quote_buff.no_break_append('"')
233
+ single_quote_buff.no_break_append("'")
234
+
235
+ return double_quote_buff if has_single_quote && !has_double_quote
236
+ single_quote_buff
237
+ end
238
+
239
+ def self.to_string_bytes(value, print_options)
240
+ bytes_length = value.length
241
+ buff = LogBuffer.new(@@config.maximum_data_output_width)
242
+ buff.no_break_append('(')
243
+
244
+ if value.is_a?(String)
245
+ buff.no_break_append('bytes')
246
+ elsif value.is_a?(Array)
247
+ buff.no_break_append('bytearray')
248
+ end
249
+
250
+ if bytes_length >= @@config.minimum_output_length
251
+ buff.no_break_append(' ')
252
+ buff.no_break_append(sprintf(@@config.length_format, bytes_length))
253
+ end
254
+
255
+ buff.no_break_append(') [')
256
+
257
+ multi_lines = bytes_length >= @@config.bytes_count_in_line
258
+
259
+ if multi_lines
260
+ buff.line_feed
261
+ buff.up_nest
262
+ end
263
+
264
+ chars = ''
265
+ count = 0
266
+ value.each_byte do |element|
267
+ if count != 0 && count % @@config.bytes_count_in_line == 0
268
+ if multi_lines
269
+ buff.no_break_append('| ')
270
+ buff.no_break_append(chars)
271
+ buff.line_feed
272
+ chars = ''
273
+ end
274
+ end
275
+ if count >= print_options.bytes_limit
276
+ buff.no_break_append(@@config.limit_string)
277
+ break
278
+ end
279
+ buff.no_break_append(sprintf('%02X ', element))
280
+ chars += (element >= 0x20 && element <= 0x7E) ? element.chr : '.'
281
+ count += 1
282
+ end
283
+
284
+ if multi_lines
285
+ # padding
286
+ full_length = 3 * @@config.bytes_count_in_line
287
+ current_length = buff.length
288
+ current_length = full_length if current_length == 0
289
+ buff.no_break_append(' ' * (full_length - current_length))
290
+ end
291
+ buff.no_break_append('| ')
292
+ buff.no_break_append(chars)
293
+
294
+ if multi_lines
295
+ buff.line_feed
296
+ buff.down_nest
297
+ end
298
+ buff.no_break_append(']')
299
+
300
+ return buff
301
+ end
302
+
303
+ def self.to_string_reflection(value, print_options)
304
+ buff = LogBuffer.new(@@config.maximum_data_output_width)
305
+
306
+ buff.append(_get_type_name(value))
307
+
308
+ body_buff = to_string_reflection_body(value, print_options)
309
+
310
+ multi_lines = body_buff.multi_lines? || buff.length + body_buff.length > @@config.maximum_data_output_width
311
+
312
+ buff.no_break_append('{')
313
+ if multi_lines
314
+ buff.line_feed
315
+ buff.up_nest
316
+ end
317
+
318
+ buff.append_buffer('', body_buff)
319
+
320
+ if multi_lines
321
+ buff.line_feed if buff.length > 0
322
+ buff.down_nest
323
+ end
324
+ buff.no_break_append('}')
325
+
326
+ return buff
327
+ end
328
+
329
+ def self.to_string_reflection_body(value, print_options)
330
+ buff = LogBuffer.new(@@config.maximum_data_output_width)
331
+
332
+ members = []
333
+ begin
334
+ base_members = value.methods(false).map { |m| [m, value.send(m)] }
335
+ members = base_members.select do |m|
336
+ name, _ = m
337
+ !name.start_with?('__') || !name.end_with?('__') &&
338
+ (print_options.output_method || !value.method(name).owner.nil?) &&
339
+ (print_options.output_private || !name.start_with?('_'))
340
+ end
341
+ rescue => ex
342
+ buff.append(ex.to_s)
343
+ return buff
344
+ end
345
+
346
+ multi_lines = false
347
+ index = 0
348
+ members.each do |member|
349
+ if index > 0
350
+ buff.no_break_append(', ')
351
+ end
352
+
353
+ name, value = member
354
+ member_buff = LogBuffer.new(@@config.maximum_data_output_width)
355
+ member_buff.append(name)
356
+ member_buff.append_buffer(@@config.key_value_separator, to_string('', value, print_options))
357
+ if index > 0 && (multi_lines || member_buff.multi_lines?)
358
+ buff.line_feed
359
+ end
360
+ buff.append_buffer('', member_buff)
361
+
362
+ multi_lines = member_buff.multi_lines?
363
+ index += 1
364
+ end
365
+
366
+ return buff
367
+ end
368
+
369
+ def self.to_string_iterable(values, print_options)
370
+ open_char = '{' # set, frozenset, dict
371
+ close_char = '}'
372
+
373
+ if values.is_a?(Array)
374
+ # list
375
+ open_char = '['
376
+ close_char = ']'
377
+ elsif values.is_a?(Tuple)
378
+ # tuple
379
+ open_char = '('
380
+ close_char = ')'
381
+ end
382
+
383
+ buff = LogBuffer.new(@@config.maximum_data_output_width)
384
+ buff.append(_get_type_name(values, values.length))
385
+ buff.no_break_append(open_char)
386
+
387
+ body_buff = to_string_iterable_body(values, print_options)
388
+ if open_char == '(' && values.length == 1
389
+ # A tuple with 1 element
390
+ body_buff.no_break_append(',')
391
+ end
392
+
393
+ multi_lines = body_buff.multi_lines? || buff.length + body_buff.length > @@config.maximum_data_output_width
394
+
395
+ if multi_lines
396
+ buff.line_feed
397
+ buff.up_nest
398
+ end
399
+
400
+ buff.append_buffer('', body_buff)
401
+
402
+ if multi_lines
403
+ buff.line_feed
404
+ buff.down_nest
405
+ end
406
+
407
+ buff.no_break_append(close_char)
408
+
409
+ return buff
410
+ end
411
+
412
+ def self.to_string_iterable_body(values, print_options)
413
+ buff = LogBuffer.new(@@config.maximum_data_output_width)
414
+
415
+ multi_lines = false
416
+ index = 0
417
+
418
+ values.each do |element|
419
+ if index > 0
420
+ buff.no_break_append(', ')
421
+ end
422
+
423
+ if index >= print_options.collection_limit
424
+ buff.append(@@config.limit_string)
425
+ break
426
+ end
427
+
428
+ element_buff = LogBuffer.new(@@config.maximum_data_output_width)
429
+ if values.is_a?(Hash)
430
+ # dictionary
431
+ element_buff = to_string_key_value(element, values[element], print_options)
432
+ else
433
+ # list, set, frozenset, or tuple
434
+ element_buff = to_string('', element, print_options)
435
+ end
436
+
437
+ if index > 0 && (multi_lines || element_buff.multi_lines?)
438
+ buff.line_feed
439
+ end
440
+ buff.append_buffer('', element_buff)
441
+
442
+ multi_lines = element_buff.multi_lines?
443
+ index += 1
444
+ end
445
+
446
+ if values.is_a?(Hash) && values.empty?
447
+ buff.no_break_append(':')
448
+ end
449
+
450
+ return buff
451
+ end
452
+
453
+ def self.to_string_key_value(key, value, print_options)
454
+ buff = LogBuffer.new(@@config.maximum_data_output_width)
455
+ key_buff = to_string('', key, print_options)
456
+ value_buff = to_string('', value, print_options)
457
+ buff.append_buffer('', key_buff).append_buffer(@@config.key_value_separator, value_buff)
458
+ return buff
459
+ end
460
+
461
+ def self.get_type_name(value, count = -1)
462
+ value_type = value.class
463
+ type_name = get_simple_type_name(value.class, 0)
464
+ if ['Array', 'Hash', 'Set', 'Tuple'].include?(type_name)
465
+ type_name = ''
466
+ end
467
+
468
+ if count >= @@config.minimum_output_count
469
+ type_name += ' ' unless type_name.empty?
470
+ type_name += @@config.count_format % count
471
+ end
472
+
473
+ if !type_name.empty?
474
+ type_name = "(#{type_name})"
475
+ end
476
+ return type_name
477
+ end
478
+
479
+ def self.get_simple_type_name(value_type, nest)
480
+ type_name = nest == 0 ? value_type.to_s : value_type.name
481
+ if type_name.start_with?("<Class '")
482
+ type_name = type_name[8..-1]
483
+ elsif type_name.start_with?("<Enum '")
484
+ type_name = 'enum ' + type_name[7..-1]
485
+ end
486
+ if type_name.end_with?("'>")
487
+ type_name = type_name[0..-3]
488
+ end
489
+
490
+ base_names = value_type.ancestors.reject { |base| base == Object }
491
+ base_names = base_names.map { |base| get_simple_type_name(base, nest + 1) }
492
+
493
+ if base_names.any?
494
+ type_name += '(' + base_names.join(', ') + ')'
495
+ end
496
+
497
+ return type_name
498
+ end
499
+
500
+ def self.has_str_repr_method(value)
501
+ begin
502
+ members = value.methods
503
+ has_str = members.include?(:to_s)
504
+ has_repr = members.include?(:inspect)
505
+ return has_str, has_repr
506
+ rescue
507
+ return false, false
508
+ end
509
+ end
510
+
511
+ # def self.get_frame_summary(limit)
512
+ # begin
513
+ # raise 'RuntimeError'
514
+ # rescue => e
515
+ # return caller_locations(limit: limit).first
516
+ # end
517
+ # return nil
518
+ # end
519
+
520
+ @@before_thread_id = nil
521
+
522
+ def self.print_start
523
+ thread = Thread.current
524
+ thread_id = thread.object_id
525
+ if thread_id != @@before_thread_id
526
+ # Thread changing
527
+ @@logger.print('')
528
+ @@logger.print(@@config.thread_boundary_format % [thread.name, thread.object_id])
529
+ @@logger.print('')
530
+ @@before_thread_id = thread_id
531
+ end
532
+ end
533
+
534
+ @@DO_NOT_OUTPUT = 'Do not output'
535
+
536
+ def self.print(name, value = @@DO_NOT_OUTPUT, force_reflection: false,
537
+ output_private: false, output_method: false,
538
+ minimum_output_count: -1, minimum_output_length: -1,
539
+ collection_limit: -1, bytes_limit: -1,
540
+ string_limit: -1, reflection_nest_limit: -1)
541
+
542
+ return value unless @@config.enabled?
543
+
544
+ Mutex.new.synchronize do
545
+ print_start
546
+
547
+ state = current_state
548
+ @@reflected_objects.clear
549
+
550
+ last_multi_lines = @@last_log_buff.multi_lines?
551
+
552
+ if value.equal? @@DO_NOT_OUTPUT
553
+ # without value
554
+ @@last_log_buff = LogBuffer.new(@@config.maximum_data_output_width)
555
+ @@last_log_buff.no_break_append(name)
556
+ else
557
+ # with value
558
+ print_options = PrintOptions.new(
559
+ force_reflection, output_private, output_method,
560
+ minimum_output_count, minimum_output_length,
561
+ collection_limit, bytes_limit, string_limit, reflection_nest_limit
562
+ )
563
+ @@last_log_buff = to_string(name, value, print_options)
564
+ end
565
+
566
+ # append print suffix
567
+ # frame_summary = get_frame_summary(3)
568
+ location = caller_locations(3, 3)[0]
569
+ name = location != nil ? location.base_label : ''
570
+ filename = location != nil ? File.basename(location.absolute_path) : ''
571
+ lineno = location != nil ? location.lineno : 0
572
+
573
+ @@last_log_buff.no_break_append(
574
+ @@config.print_suffix_format % [name, filename, lineno]
575
+ )
576
+
577
+ @@last_log_buff.line_feed
578
+
579
+ if last_multi_lines || @@last_log_buff.multi_lines?
580
+ @@logger.print(get_indent_string(state.nest_level, 0)) # Empty Line
581
+ end
582
+
583
+ @@last_log_buff.lines.each do |line|
584
+ @@logger.print(get_indent_string(state.nest_level, line.nest_level) + line.log)
585
+ end
586
+ end
587
+
588
+ return value
589
+ end
590
+
591
+
592
+ def self.enter
593
+ return unless @@config.enabled?
594
+ Mutex.new.synchronize do
595
+ print_start
596
+
597
+ state = current_state
598
+
599
+ # frame_summary = get_frame_summary(4)
600
+ location = caller_locations(3, 3)[0]
601
+ name = location != nil ? location.base_label : ''
602
+ filename = location != nil ? File.basename(location.absolute_path) : ''
603
+ lineno = location != nil ? location.lineno : 0
604
+
605
+ # parent_frame_summary = get_frame_summary(5)
606
+ parent_location = caller_locations(4, 4)[0]
607
+ parent_name = parent_location != nil ? parent_location.base_label : ''
608
+ parent_filename = parent_location != nil ? File.basename(parent_location.absolute_path) : ''
609
+ parent_lineno = parent_location != nil ? parent_location.lineno : 0
610
+
611
+ indent_string = get_indent_string(state.nest_level, 0)
612
+ if state.nest_level < state.previous_nest_level || @@last_log_buff.multi_lines?
613
+ @@logger.print(indent_string) # Empty Line
614
+ end
615
+
616
+ @@last_log_buff = LogBuffer.new(@@config.maximum_data_output_width)
617
+ @@last_log_buff.no_break_append(
618
+ @@config.enter_format % [name, filename, lineno, parent_name, parent_filename, parent_lineno]
619
+ )
620
+ @@last_log_buff.line_feed
621
+ @@logger.print(indent_string + @@last_log_buff.lines[0].log)
622
+
623
+ state.up_nest
624
+ end
625
+ end
626
+
627
+ def self.leave
628
+ return unless @@config.enabled?
629
+
630
+ Mutex.new.synchronize do
631
+ print_start
632
+
633
+ state = current_state
634
+
635
+ location = caller_locations(3, 3)[0]
636
+ name = location.base_label
637
+ filename = File.basename(location.absolute_path)
638
+ lineno = location.lineno
639
+
640
+ if @@last_log_buff.multi_lines?
641
+ @@logger.print(get_indent_string(state.nest_level, 0)) # Empty Line
642
+ end
643
+
644
+ time = Time.now.utc - state.down_nest
645
+
646
+ @@last_log_buff = LogBuffer.new(@@config.maximum_data_output_width)
647
+ @@last_log_buff.no_break_append(
648
+ @@config.leave_format % [name, filename, lineno, time]
649
+ )
650
+ @@last_log_buff.line_feed
651
+ @@logger.print(get_indent_string(state.nest_level, 0) + @@last_log_buff.lines[0].log)
652
+ end
653
+ end
654
+
655
+ def self.last_print_string
656
+ lines = @@last_log_buff.lines
657
+ buff_string = lines.map { |line| _config.data_indent_string * line[0] + line[1] }.join("\n")
658
+
659
+ state = nil
660
+ Mutex.new.synchronize do
661
+ state = current_state
662
+ end
663
+
664
+ "#{get_indent_string(state.nest_level, 0)}#{buff_string}"
665
+ end
666
+ end
667
+
668
+ DebugTrace.initialize
669
+