From 0773e268dbcced8d62de1c0a57d3d2ebe5aa998f Mon Sep 17 00:00:00 2001 From: Mike Blumtritt Date: Sun, 12 May 2024 15:06:18 +0000 Subject: [PATCH] v1.01 - inspect vars (#3) - add `ImLost.vars` to inspect local or instance variables - extend help --- README.md | 80 +++++++++++++++++------------ examples/foo.rb | 63 +++++++++++------------ lib/im-lost.rb | 67 +++++++++++++++++++++++-- lib/im-lost/version.rb | 2 +- spec/lib/im-lost_spec.rb | 106 +++++++++++++++++++++++++++++++++------ 5 files changed, 233 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 50d99a8..7721ba4 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,28 @@ When you like to know if and when a code point is reached, `ImLost.here` will he ImLost.here ``` +If you like to know the instance variables values of an object, use +`ImLost.vars`: + +```ruby +ImLost.vars(self) +``` + +Or you can print the current local variables: + +```ruby +ImLost.vars(binding) +``` + +See the [online help](https://rubydoc.info/gems/im-lost/ImLost) for more! + ## Example ```ruby require 'im-lost' +require_relative '../lib/im-lost' + class Foo def self.create(value:) = new(value) @@ -82,43 +99,40 @@ my_foo = Foo.create(value: :foo!) ImLost.trace(my_foo) my_foo.foo(1, key: :none) +ImLost.vars(my_foo) + my_foo.foo(2, :a, :b, :c, key: :some, name: :value) +ImLost.vars(my_foo) + my_foo.foo(3) { puts _1 } +ImLost.vars(my_foo) # output will look like -# > Foo.create(:foo!) -# /projects/foo.rb:25 -# > Foo.new(*) -# /projects/foo.rb:6 -# < Foo.new(*) -# = # -# < Foo.create(:foo!) -# = # -# > Foo#foo(1, *[], :none, **{}, &nil) -# /projects/foo.rb:28 -# > Foo#bar() -# /projects/foo.rb:15 -# < Foo#bar() -# = :bar -# < Foo#foo(1, *[], :none, **{}, &nil) -# = "1-none-[]-{}-bar" -# > Foo#foo(2, *[:a, :b, :c], :some, **{:name=>:value}, &nil) -# /projects/foo.rb:29 -# > Foo#bar() -# /projects/foo.rb:15 -# < Foo#bar() -# = :bar -# < Foo#foo(2, *[:a, :b, :c], :some, **{:name=>:value}, &nil) -# = "2-some-[a,b,c]-{:name=>:value}-bar" -# > Foo#foo(3, *[], nil, **{}, &#) -# /projects/foo.rb:30 -# > Foo#bar() -# /projects/foo.rb:15 -# < Foo#bar() -# = :bar -# 3--[]-{}-bar -# < Foo#foo(3, *[], nil, **{}, &#) -# = nil +# > Foo.create(:foo!) +# /projects/foo.rb25 +# > Foo.new(*) +# /projects/foo.rb6 +# < Foo.new(*) +# = # +# < Foo.create(:foo!) +# = # +# > Foo#foo(1, *[], :none, **{}, &nil) +# /projects/foo.rb28 +# > Foo#bar() +# /projects/foo.rb15 +# < Foo#bar() +# = :bar +# < Foo#foo(1, *[], :none, **{}, &nil) +# = "1-none-[]-{}-bar" +# = /projects/foo.rb29 +# instance variables: +# @value: "1-none-[]-{}-bar" +# = /projects/foo.rb32 +# instance variables: +# @value: "2-some-[a,b,c]-{:name=>:value}-bar" +# = /projects/foo.rb35 +# instance variables: +# @value: "3--[]-{}-bar" ``` See [examples dir](./examples) for moreā€¦ diff --git a/examples/foo.rb b/examples/foo.rb index 03d79e7..4c8e085 100644 --- a/examples/foo.rb +++ b/examples/foo.rb @@ -26,40 +26,37 @@ def bar = :bar ImLost.trace(my_foo) my_foo.foo(1, key: :none) +ImLost.vars(my_foo) + my_foo.foo(2, :a, :b, :c, key: :some, name: :value) +ImLost.vars(my_foo) + my_foo.foo(3) { puts _1 } +ImLost.vars(my_foo) # output will look like -# > Foo.create(:foo!) -# /projects/foo.rb:25 -# > Foo.new(*) -# /projects/foo.rb:6 -# < Foo.new(*) -# = # -# < Foo.create(:foo!) -# = # -# > Foo#foo(1, *[], :none, **{}, &nil) -# /projects/foo.rb:28 -# > Foo#bar() -# /projects/foo.rb:15 -# < Foo#bar() -# = :bar -# < Foo#foo(1, *[], :none, **{}, &nil) -# = "1-none-[]-{}-bar" -# > Foo#foo(2, *[:a, :b, :c], :some, **{:name=>:value}, &nil) -# /projects/foo.rb:29 -# > Foo#bar() -# /projects/foo.rb:15 -# < Foo#bar() -# = :bar -# < Foo#foo(2, *[:a, :b, :c], :some, **{:name=>:value}, &nil) -# = "2-some-[a,b,c]-{:name=>:value}-bar" -# > Foo#foo(3, *[], nil, **{}, &#) -# /projects/foo.rb:30 -# > Foo#bar() -# /projects/foo.rb:15 -# < Foo#bar() -# = :bar -# 3--[]-{}-bar -# < Foo#foo(3, *[], nil, **{}, &#) -# = nil +# > Foo.create(:foo!) +# /projects/foo.rb25 +# > Foo.new(*) +# /projects/foo.rb6 +# < Foo.new(*) +# = # +# < Foo.create(:foo!) +# = # +# > Foo#foo(1, *[], :none, **{}, &nil) +# /projects/foo.rb28 +# > Foo#bar() +# /projects/foo.rb15 +# < Foo#bar() +# = :bar +# < Foo#foo(1, *[], :none, **{}, &nil) +# = "1-none-[]-{}-bar" +# = /projects/foo.rb29 +# instance variables: +# @value: "1-none-[]-{}-bar" +# = /projects/foo.rb32 +# instance variables: +# @value: "2-some-[a,b,c]-{:name=>:value}-bar" +# = /projects/foo.rb35 +# instance variables: +# @value: "3--[]-{}-bar" diff --git a/lib/im-lost.rb b/lib/im-lost.rb index 93a9a5c..c166124 100644 --- a/lib/im-lost.rb +++ b/lib/im-lost.rb @@ -1,5 +1,14 @@ # frozen_string_literal: true +# If you have overlooked something again and don't really understand what your +# code is doing. If you have to maintain this application but can't really find +# your way around and certainly can't track down that stupid error. If you feel +# lost in all that code, here's the gem to help you out! +# +# ImLost helps you by analyzing function calls of objects, informing you about +# exceptions and logging your way through your code. In short, ImLost is your +# debugging helper! +# module ImLost class << self # @@ -226,6 +235,28 @@ def untrace_all! self end + # + # Inspect internal variables. + # + # @overload vars(binding) + # Inspect local variables of given Binding. + # @param binding [Binding] which local variables should be print + # @return [self] itself + # + # @overload vars(object) + # Inspect instance variables of given object. + # @param object [Object] which instance variables should be print + # @return [Object] the given object + # + def vars(object) + traced = @trace.delete(object.__id__) + return _local_vars(object) if object.is_a?(Binding) + return unless object.respond_to?(:instance_variables) + _vars(object, Kernel.caller_locations(1, 1)[0]) + ensure + @trace[traced] = 1 if traced + end + protected def as_sig(prefix, info, args) @@ -270,6 +301,34 @@ def _trace_all_b(args) ensure ids.each { @trace.delete(_1) } end + + def _vars(obj, location) + @output.puts("= #{location.path}:#{location.lineno}") + vars = obj.instance_variables + if vars.empty? + @output.puts(' ') + else + @output.puts(' instance variables:') + vars.sort!.each do |name| + @output.puts(" #{name}: #{obj.instance_variable_get(name).inspect}") + end + end + obj + end + + def _local_vars(binding) + @output.puts("= #{binding.source_location.join(':')}") + vars = binding.local_variables + if vars.empty? + @output.puts(' ') + else + @output.puts(' local variables:') + vars.sort!.each do |name| + @output.puts(" #{name}: #{binding.local_variable_get(name).inspect}") + end + end + self + end end ARG_SIG = { rest: '*', keyrest: '**', block: '&' }.compare_by_identity.freeze @@ -283,12 +342,12 @@ def _trace_all_b(args) @trace_calls = [ TracePoint.new(:c_call) do |tp| - next unless @trace.key?(tp.self.__id__) + next if !@trace.key?(tp.self.__id__) || tp.path == __FILE__ @output.puts(as_sig('>', tp, tp.parameters.map { ARG_SIG[_1[0]] || '?' })) @output.puts(" #{tp.path}:#{tp.lineno}") if @caller_locations end, TracePoint.new(:call) do |tp| - next unless @trace.key?(tp.self.__id__) + next if !@trace.key?(tp.self.__id__) || tp.path == __FILE__ ctx = tp.binding @output.puts( as_sig( @@ -308,12 +367,12 @@ def _trace_all_b(args) @trace_results = [ TracePoint.new(:c_return) do |tp| - next unless @trace.key?(tp.self.__id__) + next if !@trace.key?(tp.self.__id__) || tp.path == __FILE__ @output.puts(as_sig('<', tp, tp.parameters.map { ARG_SIG[_1[0]] || '?' })) @output.puts(" = #{tp.return_value.inspect}") end, TracePoint.new(:return) do |tp| - next unless @trace.key?(tp.self.__id__) + next if !@trace.key?(tp.self.__id__) || tp.path == __FILE__ ctx = tp.binding @output.puts( as_sig( diff --git a/lib/im-lost/version.rb b/lib/im-lost/version.rb index 6c49d6d..e1744dc 100644 --- a/lib/im-lost/version.rb +++ b/lib/im-lost/version.rb @@ -2,5 +2,5 @@ module ImLost # The version number of the gem. - VERSION = '1.0.0' + VERSION = '1.0.1' end diff --git a/spec/lib/im-lost_spec.rb b/spec/lib/im-lost_spec.rb index 622c04b..26ae900 100644 --- a/spec/lib/im-lost_spec.rb +++ b/spec/lib/im-lost_spec.rb @@ -1,14 +1,24 @@ # frozen_string_literal: true class TestSample - def foo = :foo - def bar = :bar - def add(arg0, arg1) = arg0 + arg1 - def add_kw(arg0:, arg1:) = arg0 + arg1 + def initialize + @state = :created + end + + def add(arg0, arg1) + @result = arg0 + arg1 + end + + def add_kw(arg0:, arg1:) + @result = arg0 + arg1 + end + def add_block(arg0, &block) = add(arg0, block&.call || 42) def map(*args) = args.map(&:to_s) def insp(**kw_args) = kw_args.inspect def fwd(...) = add(...) + def foo = :foo + def bar = :bar end RSpec.describe ImLost do @@ -39,22 +49,26 @@ def fwd(...) = add(...) it 'traces method calls' do sample.foo sample.bar + expect(output).to eq "> TestSample#foo()\n> TestSample#bar()\n" end it 'includes arguments in call signatures' do sample.add(21, 21) + expect(output).to eq "> TestSample#add(21, 21)\n" end it 'includes keyword arguments in call signatures' do sample.add_kw(arg0: 21, arg1: 21) + expect(output).to eq "> TestSample#add_kw(21, 21)\n" end it 'includes block arguments in call signatures' do block = proc { 42 } sample.add_block(21, &block) + expect(output).to eq <<~OUTPUT > TestSample#add_block(21, &#{block.inspect}) > TestSample#add(21, 42) @@ -63,27 +77,32 @@ def fwd(...) = add(...) it 'includes splat arguments' do sample.map(1, 2, 3, 4) + expect(output).to eq "> TestSample#map(*[1, 2, 3, 4])\n" end it 'includes empty splat arguments' do sample.map + expect(output).to eq "> TestSample#map(*[])\n" end it 'includes keyword splat arguments' do sample.insp(a: 1, b: 2) + expect(output).to eq "> TestSample#insp(**{:a=>1, :b=>2})\n" end it 'includes empty keyword splat arguments' do sample.insp + expect(output).to eq "> TestSample#insp(**{})\n" end if RUBY_VERSION < '3.1.0' it 'handles argument forwarding' do sample.fwd(40, 2) + expect(output).to eq <<~OUTPUT > TestSample#fwd(*, &) > TestSample#add(40, 2) @@ -92,6 +111,7 @@ def fwd(...) = add(...) else it 'handles argument forwarding' do sample.fwd(40, 2) + expect(output).to eq <<~OUTPUT > TestSample#fwd(*, **, &) > TestSample#add(40, 2) @@ -104,15 +124,17 @@ def fwd(...) = add(...) example.foo ImLost.trace(example) { |obj| obj.add(20, 22) } example.foo + expect(output).to eq "> TestSample#add(20, 22)\n" end it 'can include caller locations' do ImLost.caller_locations = true sample.foo + expect(output).to eq <<~OUTPUT > TestSample#foo() - #{__FILE__}:#{__LINE__ - 3} + #{__FILE__}:#{__LINE__ - 4} OUTPUT end end @@ -128,6 +150,7 @@ def fwd(...) = add(...) it 'traces method call results' do sample.foo sample.bar + expect(output).to eq <<~OUTPUT < TestSample#foo() = :foo @@ -138,12 +161,14 @@ def fwd(...) = add(...) it 'includes arguments in call signatures' do sample.add(21, 21) + expect(output).to eq "< TestSample#add(21, 21)\n = 42\n" end it 'includes block arguments in call signatures' do block = proc { 42 } sample.add_block(21, &block) + expect(output).to eq <<~OUTPUT < TestSample#add(21, 42) = 63 @@ -154,6 +179,7 @@ def fwd(...) = add(...) it 'includes splat arguments' do sample.map(1, 2, 3, 4) + expect(output).to eq <<~OUTPUT < TestSample#map(*[1, 2, 3, 4]) = ["1", "2", "3", "4"] @@ -167,6 +193,7 @@ def fwd(...) = add(...) it 'includes keyword splat arguments' do sample.insp(a: 1, b: 2) + expect(output).to eq <<~OUTPUT < TestSample#insp(**{:a=>1, :b=>2}) = "{:a=>1, :b=>2}" @@ -175,12 +202,14 @@ def fwd(...) = add(...) it 'includes empty keyword splat arguments' do sample.insp + expect(output).to eq "< TestSample#insp(**{})\n = \"{}\"\n" end if RUBY_VERSION < '3.1.0' it 'handles argument forwarding' do sample.fwd(40, 2) + expect(output).to eq <<~OUTPUT < TestSample#add(40, 2) = 42 @@ -191,6 +220,7 @@ def fwd(...) = add(...) else it 'handles argument forwarding' do sample.fwd(40, 2) + expect(output).to eq <<~OUTPUT < TestSample#add(40, 2) = 42 @@ -205,6 +235,7 @@ def fwd(...) = add(...) example.foo ImLost.trace(example) { |obj| obj.add(20, 22) } example.foo + expect(output).to eq "< TestSample#add(20, 22)\n = 42\n" end end @@ -217,11 +248,12 @@ def fwd(...) = add(...) rescue ArgumentError # nop end + expect(output).to eq <<~OUTPUT x ArgumentError: not the answer - 21 - #{__FILE__}:#{__LINE__ - 6} - ! ArgumentError: not the answer - 21 #{__FILE__}:#{__LINE__ - 7} + ! ArgumentError: not the answer - 21 + #{__FILE__}:#{__LINE__ - 8} OUTPUT end @@ -231,6 +263,7 @@ def fwd(...) = add(...) rescue ArgumentError # nop end + expect(output).to eq <<~OUTPUT x ArgumentError: not the answer - 21 ! ArgumentError: not the answer - 21 @@ -253,11 +286,12 @@ def fwd(...) = add(...) rescue NotImplementedError # nop end + expect(output).to eq <<~OUTPUT x ArgumentError: not the answer - 42 - #{__FILE__}:#{__LINE__ - 15} - ! ArgumentError: not the answer - 42 #{__FILE__}:#{__LINE__ - 16} + ! ArgumentError: not the answer - 42 + #{__FILE__}:#{__LINE__ - 17} x ArgumentError: not the answer - 21 ! ArgumentError: not the answer - 21 OUTPUT @@ -273,9 +307,10 @@ def fwd(...) = add(...) rescue ArgumentError # nop end + expect(output).to eq <<~OUTPUT x ArgumentError: not the answer - 21 - #{__FILE__}:#{__LINE__ - 6} + #{__FILE__}:#{__LINE__ - 7} OUTPUT end @@ -285,6 +320,7 @@ def fwd(...) = add(...) rescue ArgumentError # nop end + expect(output).to eq "x ArgumentError: not the answer - 21\n" end @@ -304,9 +340,10 @@ def fwd(...) = add(...) rescue NotImplementedError # nop end + expect(output).to eq <<~OUTPUT x ArgumentError: not the answer - 42 - #{__FILE__}:#{__LINE__ - 15} + #{__FILE__}:#{__LINE__ - 16} x ArgumentError: not the answer - 21 OUTPUT end @@ -316,13 +353,15 @@ def fwd(...) = add(...) context 'trace locations' do it 'writes call location' do ImLost.here - expect(output).to eq ": #{__FILE__}:#{__LINE__ - 1}\n" + + expect(output).to eq ": #{__FILE__}:#{__LINE__ - 2}\n" end it 'writes only when given condition is truethy' do ImLost.here(1 < 2) ImLost.here(1 > 2) - expect(output).to eq ": #{__FILE__}:#{__LINE__ - 2}\n" + + expect(output).to eq ": #{__FILE__}:#{__LINE__ - 3}\n" end it 'returns given argument' do @@ -333,7 +372,8 @@ def fwd(...) = add(...) it 'writes only when given block result is truethy' do ImLost.here { 1 < 2 } ImLost.here { 1 > 2 } - expect(output).to eq ": #{__FILE__}:#{__LINE__ - 2}\n" + + expect(output).to eq ": #{__FILE__}:#{__LINE__ - 3}\n" end it 'returns block result' do @@ -341,4 +381,42 @@ def fwd(...) = add(...) expect(output).to eq ": #{__FILE__}:#{__LINE__ - 1}\n" end end + + context 'dump vars' do + it 'prints instance variables' do + sample.add(22, 20) + ImLost.vars(sample) + + expect(output).to eq <<~OUTPUT + = #{__FILE__}:#{__LINE__ - 3} + instance variables: + @result: 42 + @state: :created + OUTPUT + end + + it 'returns given object' do + expect(ImLost.vars(sample)).to be sample + end + + context 'when a Binding is given' do + it 'prints local variables' do + test = :test + sample = test.to_s + test = sample + ImLost.vars(binding) + + expect(output).to eq <<~OUTPUT + = #{__FILE__}:#{__LINE__ - 3} + local variables: + sample: "test" + test: "test" + OUTPUT + end + + it 'returns ImLost' do + expect(ImLost.vars(binding)).to be ImLost + end + end + end end