From 28225854e073373db0a4c1e4c77b9d50a89583e5 Mon Sep 17 00:00:00 2001 From: Mike Blumtritt Date: Thu, 30 May 2024 21:03:07 +0200 Subject: [PATCH] Support to print thread and fiber varriables with .vars --- lib/im-lost.rb | 132 +++++++++++++++++++++++++++++---------- spec/lib/im-lost_spec.rb | 98 ++++++++++++++++++++++++++--- 2 files changed, 190 insertions(+), 40 deletions(-) diff --git a/lib/im-lost.rb b/lib/im-lost.rb index 2ec07c2..333b97c 100644 --- a/lib/im-lost.rb +++ b/lib/im-lost.rb @@ -243,23 +243,64 @@ def untrace_all! end # - # Inspect internal variables. + # Inspect internal variables of a given object. # - # @overload vars(binding) - # Inspect local variables of given Binding. - # @param binding [Binding] which local variables should be print - # @return [self] itself + # @example Inspect current instance variables + # @a = 22 + # b = 20 + # c = @a + b + # ImLost.vars(self) + # # => print value of `@a` # - # @overload vars(object) - # Inspect instance variables of given object. - # @param object [Object] which instance variables should be print - # @return [Object] the given object + # @example Inspect local variables + # @a = 22 + # b = 20 + # c = @a + b + # ImLost.vars(binding) + # # => print values of `b` and 'c' + # + # @example Inspect a thread's variables + # th = Thread.new { th[:var1] += 20 } + # th[:var1] = 22 + # ImLost.vars(th) + # # => print value of `var1` + # th.join + # ImLost.vars(th) + # + # @example Inspect the current fiber's storage + # Fiber[:var1] = 22 + # Fiber[:var2] = 20 + # Fiber[:var3] = Fiber[:var1] + Fiber[:var2] + # ImLost.vars(Fiber.current) + # + # When the given object is + # + # - a `Binding` it prints the local variables of the binding + # - a `Thread` it prints the fiber-local and thread variables + # - the current `Fiber` it prints the fibers' storage + # + # Be aware that only the current fiber can be inspected. + # + # When the given object can not be inspected it prints an error message. + # + # @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]) + return _local_vars(object) if Binding === object + location = Kernel.caller_locations(1, 1)[0] + @output.puts("= #{location.path}:#{location.lineno}") + if Thread === object + _thread_vars(object) + elsif Fiber === object + _fiber_vars(object) + elsif defined?(object.instance_variables) + _instance_vars(object) + else + @output.puts(' !!! unable to retrieve vars') + end + object ensure @trace[traced] = traced if traced end @@ -312,32 +353,59 @@ def _trace_all_b(args) 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(' ') - return obj + def _local_vars(binding) + @output.puts("= #{binding.source_location.join(':')}") + _print_vars('local variables', binding.local_variables) do |name| + binding.local_variable_get(name) end - @output.puts(' instance variables:') - vars.sort!.each do |name| - @output.puts(" #{name}: #{obj.instance_variable_get(name).inspect}") + binding + end + + def _thread_vars(thread) + @output.puts(" #{_thread_identifier(thread)}") + fvars = thread.keys + unless fvars.empty? + _print_vars('fiber-local variables', fvars) { thread[_1] } + end + _print_vars('thread variables', thread.thread_variables) do |name| + thread.thread_variable_get(name) end - obj end - def _local_vars(binding) - @output.puts("= #{binding.source_location.join(':')}") - vars = binding.local_variables - if vars.empty? - @output.puts(' ') - return self + def _fiber_vars(fiber) + if Fiber.current == fiber + storage = fiber.storage || {} + return _print_vars('fiber storage', storage.keys) { storage[_1] } + end + @output.puts( + ' !!! given Fiber is not the current Fiber', + " #{fiber}" + ) + end + + def _instance_vars(object) + _print_vars('instance variables', object.instance_variables) do |name| + object.instance_variable_get(name) end - @output.puts(' local variables:') - vars.sort!.each do |name| - @output.puts(" #{name}: #{binding.local_variable_get(name).inspect}") + end + + def _thread_identifier(thread) + if thread.native_thread_id + return( + "#{thread.status} Thread #{thread.native_thread_id} #{ + thread.name + }".rstrip + ) end - self + "#{ + { false => 'terminated', nil => 'aborted' }[thread.status] + } Thread #{thread.native_thread_id} #{thread.name}".rstrip + end + + def _print_vars(kind, names) + return @output.puts(" ") if names.empty? + @output.puts(" > #{kind}") + names.sort!.each { @output.puts(" #{_1}: #{yield(_1).inspect}") } end end diff --git a/spec/lib/im-lost_spec.rb b/spec/lib/im-lost_spec.rb index 9f7583a..240285e 100644 --- a/spec/lib/im-lost_spec.rb +++ b/spec/lib/im-lost_spec.rb @@ -351,9 +351,9 @@ def bar = :bar expect(output).to eq <<~OUTPUT = #{__FILE__}:#{__LINE__ - 3} - instance variables: - @result: 42 - @state: :created + > instance variables + @result: 42 + @state: :created OUTPUT end @@ -361,6 +361,19 @@ def bar = :bar expect(ImLost.vars(sample)).to be sample end + context 'when instance variables could not be determined' do + let(:sample) { BasicObject.new } + + it 'it prints an error message' do + ImLost.vars(sample) + + expect(output).to eq <<~OUTPUT + = #{__FILE__}:#{__LINE__ - 3} + !!! unable to retrieve vars + OUTPUT + end + end + context 'when a Binding is given' do it 'prints local variables' do test = :test @@ -370,14 +383,83 @@ def bar = :bar expect(output).to eq <<~OUTPUT = #{__FILE__}:#{__LINE__ - 3} - local variables: - sample: "test" - test: "test" + > local variables + sample: "test" + test: "test" + OUTPUT + end + + it 'returns given bindig' do + expect(ImLost.vars(binding)).to be_a Binding + end + end + + context 'when a Thread is given' do + let(:thread) do + Thread.new do + Thread.current[:var] = 21 + Thread.current.thread_variable_set(:result, 42) + end + end + + after { thread.join } + + it 'prints thread variables' do + thread[:var] = 41 + ImLost.vars(thread.join) + + expect(output).to eq <<~OUTPUT + = #{__FILE__}:#{__LINE__ - 3} + terminated Thread + > fiber-local variables + var: 21 + > thread variables + result: 42 OUTPUT end - it 'returns ImLost' do - expect(ImLost.vars(binding)).to be ImLost + it 'returns given thread' do + expect(ImLost.vars(thread)).to be thread + end + end + + context 'when the current Fiber is given' do + before do + Fiber[:var1] = 22 + Fiber[:var2] = 20 + Fiber[:var3] = Fiber[:var1] + Fiber[:var2] + end + + it 'prints the fiber storage' do + ImLost.vars(Fiber.current) + + expect(output).to eq <<~OUTPUT + = #{__FILE__}:#{__LINE__ - 3} + > fiber storage + var1: 22 + var2: 20 + var3: 42 + OUTPUT + end + + it 'returns given fiber' do + expect(ImLost.vars(Fiber.current)).to be Fiber.current + end + end + + context 'when a different Fiber is given' do + let(:fiber) { Fiber.new { 42 } } + + after { fiber.kill } + + it 'it prints an error message' do + ImLost.vars(fiber) + + expect(output).to eq <<~OUTPUT + = #{__FILE__}:#{__LINE__ - 3} + !!! given Fiber is not the current Fiber + #{fiber.inspect} + OUTPUT end end end