Skip to content

Commit

Permalink
Support to print thread and fiber varriables with .vars
Browse files Browse the repository at this point in the history
  • Loading branch information
mblumtritt committed May 30, 2024
1 parent 2e0dd11 commit 2822585
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 40 deletions.
132 changes: 100 additions & 32 deletions lib/im-lost.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(' <no instance variables defined>')
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(' <no local variables>')
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(" <no #{kind} defined>") if names.empty?
@output.puts(" > #{kind}")
names.sort!.each { @output.puts(" #{_1}: #{yield(_1).inspect}") }
end
end

Expand Down
98 changes: 90 additions & 8 deletions spec/lib/im-lost_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -351,16 +351,29 @@ 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

it 'returns given object' do
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
Expand All @@ -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
Expand Down

0 comments on commit 2822585

Please sign in to comment.