Class: Unroller

Inherits:
Object
  • Object
show all
Defined in:
lib/unroller.rb

Defined Under Namespace

Classes: Call, ClassExclusion, Variables

Constant Summary collapse

@@instance =
nil
@@quiting =
false

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Unroller

Returns a new instance of Unroller.



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/unroller.rb', line 197

def initialize(options = {})
  # Defaults
  @@display_style ||= :show_entire_method_body
  @condition = Proc.new { true }  # Only trace if this condition is true. Useful if the place where you put your trace {} statement gets called a lot and you only want it to actually trace for some of those calls.
  @initial_depth = 1              # ("Call stack") depth to start at. Actually, you'll probably want this set considerably lower than the current call stack depth, so that the indentation isn't way off the screen.
  @max_lines = nil                # Stop tracing (permanently) after we have produced @max_lines lines of output. If you don't know where to place the trace(false) and you just want it to stop on its own after so many lines, you could use this...
  @max_depth = nil                # Don't trace anything when the depth is greater than this threshold. (This is *relative* to the starting depth, so whatever level you start at is considered depth "1".)
  @line_matches = nil             # The source code for that line matches this regular expression
  @presets = []
  @file_match = /./
  @exclude_classes = []
  @include_classes = []           # These will override classes that have been excluded via exclude_classes. So if you both exclude and include a class, it will be included.
  @exclude_methods = []
  @include_methods = []
  @show_args   = true
  @show_locals = false
  @show_filename_and_line_numbers = true
  @include_c_calls = false        # Might be useful if you're debugging your own C extension. Otherwise, we probably don't care about C-calls because we can't see the source for them anyway...
  @strip_comments = true          # :todo:
  @use_full_path = false          # :todo:
  @screen_width = 150
  @column_widths = [70]
  @indent_step =       ' ' + '|'.magenta + ' '
  @column_separator = '  ' + '|'.yellow.bold + ' '
  @always_show_raise_events = false
  @show_file_load_errors = false
  @interactive = false   # Set to true to make it more like an interactive debugger.
  @show_menu = true      # Set to false if you don't need the hints.
    # (In the future, might add "break out of this call" option to stop watching anything until we return from the current method... etc.)
  instance_variables.each do |v|
    self.class.class_eval do
      attr_accessor v.to_s.gsub!(/^@/, '')
    end
  end

  # "Presets"
  # Experimental -- subject to change a lot before it's finalized
  options[:presets]       = options.delete(:only)            if options.has_key?(:only)
  options[:presets]       = options.delete(:debugging)       if options.has_key?(:debugging)
  options[:presets]       = options.delete(:preset)          if options.has_key?(:preset)
  options[:presets] = [options[:presets]] unless options[:presets].is_a?(Array)
  [:rails, :dependencies].each do |preset|
    if options.has_key?(preset) || options[:presets].include?(preset)
      options.delete(preset)
      case preset
      when :dependencies    # Debugging ActiveSupport::Dependencies
        @exclude_classes.concat [
          /Gem/
        ].map {|e| ClassExclusion.new(e) }
      when :rails
        @exclude_classes.concat [
          /Benchmark/,
          /Gem/,
          /Dependencies/,
          /Logger/,
          /MonitorMixin/,
          /Set/,
          /HashWithIndifferentAccess/,
          /ERB/,
          /ActiveRecord/,
          /SQLite3/,
          /Class/,
          /ActiveSupport/,
          /ActiveSupport::Deprecation/,
          /Pathname/,
          /Object/,
          /Symbol/,
          /Kernel/,
          /Inflector/,
          /Webrick/
        ].map {|e| ClassExclusion.new(e) }
      end
    end
  end

  #-----------------------------------------------------------------------------------------------
  # Options

  # Aliases
  options[:max_lines]       = options.delete(:head)       if options.has_key?(:head)
  options[:condition]       = options.delete(:if)         if options.has_key?(:if)
  options[:initial_depth]   = options.delete(:depth)      if options.has_key?(:depth)
  options[:initial_depth]   = caller(0).size              if options[:initial_depth] == :use_call_stack_depth
  options[:file_match]      = options.delete(:file)       if options.has_key?(:file)
  options[:file_match]      = options.delete(:path)       if options.has_key?(:path)
  options[:file_match]      = options.delete(:path_match) if options.has_key?(:path_match)
  options[:dir_match]       = options.delete(:dir)        if options.has_key?(:dir)
  options[:dir_match]       = options.delete(:dir_match)  if options.has_key?(:dir_match)

  if (a = options.delete(:dir_match))
    unless a.is_a?(Regexp)
      if a =~ /.*\.rb/
        # They probably passed in __FILE__ and wanted us to File.expand_path(File.dirname()) it for them (and who can blame them? that's a lot of junk to type!!)
        a = File.expand_path(File.dirname(a))
      end
      a = /^#{Regexp.escape(a)}/ # Must start with that entire directory path
    end
    options[:file_match] = a
  end
  if (a = options.delete(:file_match))
    # Coerce it into a Regexp
    unless a.is_a?(Regexp)
      a = /#{Regexp.escape(a)}/ 
    end
    options[:file_match] = a
  end

  if options.has_key?(:exclude_classes)
    # Coerce it into an array of ClassExclusions
    a = options.delete(:exclude_classes)
    a = [a] unless a.is_a?(Array)
    a.map! {|e| e = ClassExclusion.new(e) unless e.is_a?(ClassExclusion); e }
    @exclude_classes.concat a
  end
  if options.has_key?(:include_classes)
    # :todo:
  end
  if options.has_key?(:exclude_methods)
    # Coerce it into an array of Regexp's
    a = options.delete(:exclude_methods)
    a = [a] unless a.is_a?(Array)
    a.map! {|e| e = /^#{e}$/ unless e.is_a?(Regexp); e }
    @exclude_methods.concat a
  end
  options[:line_matches]       = options.delete(:line_matches)       if options.has_key?(:line_matches)
  populate(options)

  #-----------------------------------------------------------------------------------------------
  # Private
  @call_stack = []        # Used to keep track of what method we're currently in so that when we hit a 'return' event we can display something useful.
                          # This is useful for two reasons:
                          # 1. Sometimes (and I don't know why), the code that gets shown for a 'return' event doesn't even look
                          #    like it has anything to do with a return... Having the call stack lets us intelligently say 'returning from ...'
                          # 2. If we've been stuck in this method for a long time and we're really deep, the user has probably forgotten by now which method we are returning from
                          #    (the filename may give some clue, but not enough), so this is likely to be a welcome reminder a lot of the time.
                          # Also using it to store line numbers, so that we can show the entire method definition each time we process a line,
                          # rather than just the current line itself.
                          # Its members are of type Call
  @internal_depth = 0     # This is the "true" depth. It is incremented/decremented for *every* call/return, even those that we're not displaying. It is necessary for the implementation of "silent_until_return_to_this_depth", to detect when we get back to interesting land.
  @depth = @initial_depth # This is the user-friendly depth. It only counts calls/returns that we *display*; it does not change when we enter into a call that we're not displaying (a "hidden" call).
  @output_line = ''
  @column_counter = 0
  @tracing = false
  @files = {}
  @lines_output = 0
  @silent_until_return_to_this_depth = nil
end

Instance Attribute Details

#depthObject

Returns the value of attribute depth.



192
193
194
# File 'lib/unroller.rb', line 192

def depth
  @depth
end

#tracingObject (readonly)

Returns the value of attribute tracing.



193
194
195
# File 'lib/unroller.rb', line 193

def tracing
  @tracing
end

Class Method Details

.debug(options = {}, &block) ⇒ Object



174
175
176
177
# File 'lib/unroller.rb', line 174

def self.debug(options = {}, &block)
  options.reverse_merge! :interactive => true, :display_style => :show_entire_method_body
  self.trace options, &block
end

.exclude(*args, &block) ⇒ Object



345
346
347
# File 'lib/unroller.rb', line 345

def self.exclude(*args, &block)
  @@instance.exclude(*args, &block) unless @@instance.nil?
end

.suspend(*args, &block) ⇒ Object



348
349
350
# File 'lib/unroller.rb', line 348

def self.suspend(*args, &block)
  @@instance.exclude(*args, &block) unless @@instance.nil?
end

.trace(options = {}, &block) ⇒ Object Also known as: trace_on



179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/unroller.rb', line 179

def self.trace(options = {}, &block)
  if @@instance and @@instance.tracing
    # In case they try to turn tracing on when it's already on... Assume it was an accident and don't do anything.
    #puts "@@instance.tracing=#{@@instance.tracing}"
    #return if @@instance and @@instance.tracing       
    #yield if block_given?
  else
    self.display_style = options.delete(:display_style) if options.has_key?(:display_style)
    @@instance = Unroller.new(options)
  end
  @@instance.trace &block
end

.trace_offObject



723
724
725
726
727
# File 'lib/unroller.rb', line 723

def self.trace_off
  if @@instance and @@instance.tracing
    @@instance.trace_off
  end
end

.watch_for_added_methods(mod, filter = //, &block) ⇒ Object




734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
# File 'lib/unroller.rb', line 734

def self.watch_for_added_methods(mod, filter = //, &block)
  mod.singleton_class.instance_eval do
    define_method :method_added_with_watch_for_added_methods do |name, *args|
      if name.to_s =~ filter
        puts "Method '#{name}' was defined at #{caller[0]}"
      end
    end
    alias_method_chain :method_added, :watch_for_added_methods, :create_target => true
  end

  yield if block_given?

#    mod.class.instance_eval do
#      alias_method :method_added, :method_added_without_watch_for_added_methods
#    end
end

Instance Method Details

#exclude(&block) ⇒ Object



351
352
353
354
355
356
# File 'lib/unroller.rb', line 351

def exclude(&block)
  old_tracing = @tracing
  (trace_off; puts 'Suspending tracing')
  yield
  (trace; puts 'Resuming tracing') if old_tracing
end

#trace(&block) ⇒ Object



358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
# File 'lib/unroller.rb', line 358

def trace(&block)
catch :quit do
  throw :quit if @@quiting
  if @tracing
    yield if block_given?
    # No need to call set_trace_func again; we're already tracing
    return
  end

  begin
    @tracing = true


    if @condition.call

      trap_chain("INT") do
        puts
        puts 'Exiting trace...'
        set_trace_func(nil)
        @@quiting = true
        throw :quit
      end









      # (This is the meat of the library right here, so let's set it off with at least 5 blank lines.)
      set_trace_func( lambda do |event, file, line, id, binding, klass|
        return if @@quiting
        begin # begin/rescue block
          @event, @file, @line, @id, @binding, @klass =
            event, file, line, id, binding, klass
          line_num = line
          current_call = Call.new(file, line, klass, id, fully_qualified_method)

          # Sometimes klass is false and id is nil. Not sure why, but that's the way it is.
          #printf "- (event=%8s) (klass=%10s) (id=%10s) (%s:%-2d)\n", event, klass, id, file, line #if klass.to_s == 'false'
          #puts 'false!!!!!!!'+klass.inspect if klass.to_s == 'false'

          return if ['c-call', 'c-return'].include?(event) unless include_c_calls
          #(puts 'exclude') if @silent_until_return_to_this_depth unless event == 'return' # Until we hit a return and can break out of this uninteresting call, we don't want to do *anything*.
          #return if uninteresting_class?(klass.to_s) unless (klass == false)

          if @only_makes_sense_if_next_event_is_call
            if event == 'call'
              @only_makes_sense_if_next_event_is_call = nil
            else
              # Cancel @silent_until_return_to_this_depth because it didn't make sense / wasn't necessary. They could have 
              # ("should have") simply done a 'step into', because unless it's a 'call', there's nothing to step over anyway...
              # Not only is it unnecessary, but it would cause confusing behavior unless we cancelled this. As in, it
              # wouldn't show *any* tracing for the remainder of the method, because it's kind of looking for a "return"...
              #puts "Cancelling @silent_until_return_to_this_depth..."
              @silent_until_return_to_this_depth = nil
            end
          end

          if too_far?
            puts "We've read #{@max_lines} (@max_lines) lines now. Turning off tracing..."
            trace_off
            return
          end

          case @@display_style

          # To do: remove massive duplication with the other (:concise) display style
          when :show_entire_method_body

            case event


              #zzz
              when 'call'
                unless skip_line?
                  depth_debug = '' #" (internal_depth about to be #{@internal_depth+1})"
                  column sprintf(' ' + '\\'.cyan + ' calling'.cyan + ' ' + '%s'.underline.cyan + depth_debug, 
                                 fully_qualified_method), @column_widths[0]
                  newline

                  #puts
                  #header_before_code_for_entire_method(file, line_num)
                  #do_show_locals if show_args
                  #ret = code_for_entire_method(file, line, klass, id, line, 0)
                  #puts ret unless ret.nil?
                  #puts

                  @lines_output += 1


                  @depth += 1
                end

                @call_stack.push current_call
                @internal_depth += 1
                #puts "(@internal_depth+=1 ... #{@internal_depth})"


              when 'class'
              when 'end'
              when 'line'
                unless skip_line?
                  # Show the state of the locals *before* executing the current line. (I might add the option to show it after instead/as well, but I don't think that would be easy...)

                  inside_of = @call_stack.last
                  #puts "inside_of=#{inside_of.inspect}"
                  if inside_of
                    unless @last_call == current_call  # Without this, I was seeing two consecutive events for the exact same line. This seems to be necessary because 'if foo()' is treated as two 'line' events: one for 'foo()' and one for 'if' (?)...
                      puts
                      header_before_code_for_entire_method(file, line_num)
                      do_show_locals if true #show_locals
                      ret = code_for_entire_method(inside_of.file, inside_of.line_num, @klass, @id, line, -1)
                      puts ret unless ret.nil?
                      puts
                    end
                  else
                    column pretty_code_for(file, line, ' ', :bold), @column_widths[0]
                    file_column file, line
                    newline
                  end

                  # Interactive debugger!
                  response = nil
                  if @interactive && !(@last_call == current_call)
                    #(print '(o = Step out of | s = Skip = Step over | default = Step into > '; response = $stdin.gets) if @interactive

                    while response.nil? or !response.in? ['i',' ',"\e[C","\e[19~", 'v',"\e[B","\e[20~", 'u',"\e[D", 'r', "\n", 'q'] do
                      print "Debugger (" +
                        "Step into (F8/Right/Space)".menu_item(:green, 'i') + ' | ' + 
                        "Step over (F9/Down/Enter)".menu_item(:cyan, 'v') + ' | ' +
                        "Step out (Left)".menu_item(:red, 'u') + ' | ' +
                        "show Locals".menu_item(:yellow, 'l') + ' | ' + 
                        "Run".menu_item(:blue) + ' | ' + 
                        "Quit".menu_item(:magenta) + 
                        ') > '
                      $stdout.flush

                      response = $stdin.getch.downcase

                      # Escape sequence such as the up arrow key ("\e[A")
                      if response == "\e"
                        response << (next_char = $stdin.getch)
                        if next_char == '['
                          response << (next_char = $stdin.getch)
                          if next_char.in? ['1', '2']
                            response << (next_char = $stdin.getch)
                            response << (next_char = $stdin.getch)
                          end
                        end
                      end

                      puts unless response == "\n"

                      case response
                      when 'l'
                        do_show_locals_verbosely
                        response = nil
                      end
                    end
                  end

                  if response
                    case response
                    when 'i', ' ', "\e[C", "\e[19~"  # (Right, F8)
                      # keep right on tracing...
                    when 'v', "\n", "\e[B", "\e[20~"  # (Down, F9) Step over = Ignore anything with a greater depth.
                      @only_makes_sense_if_next_event_is_call = true
                      @silent_until_return_to_this_depth = @internal_depth
                    when 'u', "\e[D" # (Left) Step out
                      @silent_until_return_to_this_depth = @internal_depth - 1
                      #puts "Setting @silent_until_return_to_this_depth = #{@silent_until_return_to_this_depth}"
                    when 'r' # Run
                      @interactive = false
                    when 'q'
                      @@quiting = true
                      throw :quit
                    else
                      # we shouldn't get here
                    end
                  end

                  @last_call = current_call
                  @lines_output += 1
                end # unless skip_line?


              when 'return'

                @internal_depth -= 1
                #puts "(@internal_depth-=1 ... #{@internal_depth})"

                # Did we just get out of an uninteresting call?? Are we back in interesting land again??
                if  @silent_until_return_to_this_depth and 
                    @silent_until_return_to_this_depth == @internal_depth
                  #puts "Yay, we're back in interesting land! (@internal_depth = #{@internal_depth})"
                  @silent_until_return_to_this_depth = nil
                end


                unless skip_line?
                  puts "Warning: @depth < 0. You may wish to call trace with a :depth => depth value greater than #{@initial_depth}" if @depth-1 < 0
                  @depth -= 1 unless @depth == 0
                  #puts "-- Decreased depth to #{depth}"
                  returning_from = @call_stack.last

                  depth_debug = '' #" (internal_depth was #{@internal_depth+1})"
                  column sprintf(' ' + '/'.cyan + ' returning from'.cyan + ' ' + '%s'.cyan + depth_debug,
                                 returning_from && returning_from.full_name), @column_widths[0]
                  newline

                  @lines_output += 1
                end

                @call_stack.pop


              when 'raise'
                if !skip_line? or @always_show_raise_events
                  # We probably always want to see these (?)... Never skip displaying them, even if we are "too deep".
                  column "Raising an error (#{$!}) from #{klass}".red.bold, @column_widths[0]
                  newline

                  column pretty_code_for(file, line, ' ').red, @column_widths[0]
                  file_column file, line
                  newline
                end

              else
                column sprintf("- (%8s) %10s %10s (%s:%-2d)", event, klass, id, file, line)
                newline
            end # case event

          # End when :show_entire_method_body

          when :concise
            case event


              when 'call'
                unless skip_line?
                  # :todo: use # instead of :: if klass.constantize.instance_methods.include?(id)
                  column sprintf(' ' + '+'.cyan + ' calling'.cyan + ' ' + '%s'.underline.cyan, fully_qualified_method), @column_widths[0]
                  newline

                  column pretty_code_for(file, line, '/'.magenta, :green), @column_widths[0]
                  file_column file, line
                  newline

                  @lines_output += 1

                  @call_stack.push Call.new(file, line, klass, id, fully_qualified_method)

                  @depth += 1
                  #puts "++ Increased depth to #{depth}"

                  # The locals at this point will be simply be the arguments that were passed in to this method.
                  do_show_locals if show_args
                end
                @internal_depth += 1


              when 'class'
              when 'end'
              when 'line'
                unless skip_line?
                  # Show the state of the locals *before* executing the current line. (I might add the option to show it after instead/as well, but I don't think that would be easy...)
                  do_show_locals if show_locals

                  column pretty_code_for(file, line, ' ', :bold), @column_widths[0]
                  file_column file, line
                  newline

                  @lines_output += 1
                end



              when 'return'

                @internal_depth -= 1
                unless skip_line?
                  puts "Warning: @depth < 0. You may wish to call trace with a :depth => depth value greater than #{@initial_depth}" if @depth-1 < 0
                  @depth -= 1 unless @depth == 0
                  #puts "-- Decreased depth to #{depth}"
                  returning_from = @call_stack.last

                  code = pretty_code_for(file, line, '\\'.magenta, :green, suffix = " (returning from #{returning_from && returning_from.full_name})".green)
                  code = pretty_code_for(file, line, '\\'.magenta + " (returning from #{returning_from && returning_from.full_name})".green, :green) unless code =~ /return|end/
                    # I've seen some really weird statements listed as "return" statements.
                    # I'm not really sure *why* it thinks these are returns, but let's at least identify those lines for the user. Examples:
                    # * must_be_open!
                    # * @db = db
                    # * stmt = @statement_factory.new( self, sql )
                    # I think some of the time this happens it might be because people pass the wrong line number to eval (__LINE__ instead of __LINE__ + 1, for example), so the line number is just not accurate.
                    # But I don't know if that explains all such cases or not...
                  column code, @column_widths[0]
                  file_column file, line
                  newline

                  @lines_output += 1
                end

                # Did we just get out of an uninteresting call?? Are we back in interesting land again??
                if  @silent_until_return_to_this_depth and 
                    @silent_until_return_to_this_depth == @internal_depth
                  puts "Yay, we're back in interesting land!"
                  @silent_until_return_to_this_depth = nil
                end
                @call_stack.pop


              when 'raise'
                if !skip_line? or @always_show_raise_events
                  # We probably always want to see these (?)... Never skip displaying them, even if we are "too deep".
                  column "Raising an error (#{$!}) from #{klass}".red.bold, @column_widths[0]
                  newline

                  column pretty_code_for(file, line, ' ').red, @column_widths[0]
                  file_column file, line
                  newline
                end

              else
                column sprintf("- (%8s) %10s %10s (%s:%-2d)", event, klass, id, file, line)
                newline
            end # case event

          # End default display style

          end # case @@display_style



        rescue Exception => exception
          puts exception.inspect
          raise
        end # begin/rescue block
      end) # set_trace_func








    end # if @condition.call

    if block_given?
      yield
    end

  ensure
    trace_off if block_given?
  end # rescue/ensure block
end
end

#trace_offObject



728
729
730
731
# File 'lib/unroller.rb', line 728

def trace_off
  @tracing = false
  set_trace_func(nil)
end