How do I get ruby to print a full backtrace that includes arguments passed to functions? - ruby

Sometimes backtrace is enough to diagnose problem. But sometimes reason of crash is not obvious without knowledge what was passed to function.
Getting information what was passed to function that caused crash would be quite useful, especially in cases where reproducing is not obvious because it was caused by for example exception in network connection, weird user input or because program is depends on randomisation or processes data from external sensor.
Lets say that there is following program
def handle_changed_input(changed_input)
raise 'ops' if changed_input =~ /magic/
end
def do_something_with_user_input(input)
input = "#{input.strip}c"
handle_changed_input(input)
end
input = gets
do_something_with_user_input(input)
where user typed "magic" as input. Normally one has
test.rb:2:in `handle_changed_input': ops (RuntimeError)
from test.rb:7:in `do_something_with_user_input'
from test.rb:11:in `<main>'
as output. What one may do to show also what was passed to function? Something like
test.rb:2:in `handle_changed_input("magic")': ops (RuntimeError)
from test.rb:7:in `do_something_with_user_input("magi\n")'
from test.rb:11:in `<main>'
It would be useful in many situations (and not truly useful where parameters are not representable as strings of reasonable legth, there is a good reason why it is not enabled by default).
How one may add this functionality? It is necessary that program works as usually during normal operation and preferably there is no additional output before crash.
I tried for example
def do_something_with_user_input(input)
method(__method__).parameters.map do |_, name|
puts "#{name}=#{binding.local_variable_get(name)}"
end
raise 'ops' if input =~ /magic/
end
input = gets
found in Is there a way to access method arguments in Ruby? but it would print on every single entrance to function what both would flood output and make program significantly slower.

I don't have a complete solution but... But you can get method arguments of all called methods in controlled environment with TracePoint class from Ruby core lib.
Look at the example:
trace = TracePoint.new(:call) do |tp|
puts "===================== #{tp.method_id}"
b_self = tp.binding.eval('self')
names = b_self.method(tp.method_id).parameters.map(&:last)
values = names.map { |name| tp.binding.eval(name.to_s) }
p names.zip(values)
end
trace.enable
def method_a(p1, p2, p3)
end
method_a(1, "foobar", false)
#=> ===================== method_a
#=> [[:p1, 1], [:p2, "foobar"], [:p3, false]]

To print exception backtraces, Ruby uses the C function exc_backtrace from error.c (exc_backtrace on github). Unless you patch Ruby with the functionality you need, I don't think there a way to change exception backtrace outputs.
Here is a snippet (trace.rb) you might find useful:
set_trace_func -> (event, file, line, id, binding, classname) do
if event == 'call' && meth = binding.eval('__method__')
params = binding.method(meth).parameters.select{|e| e[0] != :block}
values = params.map{|_, var| [var, binding.local_variable_get(var)]}
printf "%8s %s:%-2d %15s %8s %s\n", event, file, line, id, classname, values.inspect
else
printf "%8s %s:%-2d %15s %8s\n", event, file, line, id, classname
end
end
def foo(a,b = 0)
bar(a, foo: true)
end
def bar(c, d = {})
puts "!!!buz!!!\n"
end
foo('lol')
The output of that snippet is:
c-return /path/to/trace.rb:1 set_trace_func Kernel
line /path/to/trace.rb:12
c-call /path/to/trace.rb:12 method_added Module
c-return /path/to/trace.rb:12 method_added Module
line /path/to/trace.rb:16
c-call /path/to/trace.rb:16 method_added Module
c-return /path/to/trace.rb:16 method_added Module
line /path/to/trace.rb:20
call /path/to/trace.rb:12 foo Object [[:a, "lol"], [:b, 0]]
line /path/to/trace.rb:13 foo Object
call /path/to/trace.rb:16 bar Object [[:c, "lol"], [:d, {:foo=>true}]]
line /path/to/trace.rb:17 bar Object
c-call /path/to/trace.rb:17 puts Kernel
c-call /path/to/trace.rb:17 puts IO
c-call /path/to/trace.rb:17 write IO
!!!buz!!!
c-return /path/to/trace.rb:17 write IO
c-return /path/to/trace.rb:17 puts IO
c-return /path/to/trace.rb:17 puts Kernel
return /path/to/trace.rb:18 bar Object
return /path/to/trace.rb:14 foo Object
I hope that helps you as much as it helped me.

I think that it is possible. The code below is not perfect and would require some additional work, but it caputers the primary idea of a stacktrace with argument values. Please note, that in order to know the call site, I am zipping the original stacktrace with the entry sites catched by trace function. To distinguishe these entries I use '>' and '<' respectively.
class Reporting
def self.info(arg1)
puts "*** #{arg1} ***"
end
end
def read_byte(arg1)
Reporting.info(arg1)
raise Exception.new("File not found")
end
def read_input(arg1)
read_byte(arg1)
end
def main(arg1)
read_input(arg1)
end
class BetterStacktrace
def self.enable
set_trace_func -> (event, file, line, id, binding, classname) do
case event
when 'call'
receiver_type = binding.eval('self.class')
if receiver_type == Object
meth = binding.eval('__method__')
params = binding.method(meth).parameters.select{|e| e[0] != :block}
values = params.map{|_, var| [var, binding.local_variable_get(var)]}
self.push(event, file, line, id, classname, values)
else
self.push(event, file, line, id, classname)
end
when 'return'
self.pop
when 'raise'
self.push(event, file, line, id, classname)
Thread.current[:_keep_stacktrace] = true
end
end
end
def self.push(event, file, line, id, classname, values=nil)
Thread.current[:_saved_stacktrace] = [] unless Thread.current.key?(:_saved_stacktrace)
unless Thread.current[:_keep_stacktrace]
if values
values_msg = values.map(&:last).join(", ")
msg = "%s:%d:in `%s(%s)'" % [file, line, id, values_msg]
else
msg = "%s:%d:in `%s'" % [file, line, id]
end
Thread.current[:_saved_stacktrace] << msg
end
end
def self.pop()
Thread.current[:_saved_stacktrace] = [] unless Thread.current.key?(:_saved_stacktrace)
unless Thread.current[:_keep_stacktrace]
value = Thread.current[:_saved_stacktrace].pop
end
end
def self.disable
set_trace_func nil
end
def self.print_stacktrace(calls)
enters = Thread.current[:_saved_stacktrace].reverse
calls.zip(enters).each do |call, enter|
STDERR.puts "> #{enter}"
STDERR.puts "< #{call}"
end
Thread.current[:_saved_stacktrace] = []
end
end
BetterStacktrace.enable
begin
main(10)
rescue Exception => ex
puts "--- Catched ---"
puts ex
BetterStacktrace.print_stacktrace(ex.backtrace)
end
BetterStacktrace.disable
begin
main(10)
rescue Exception
puts "--- Catched ---"
puts ex
puts ex.backtrace
end
The output of the above code is as follows:
*** 10 ***
--- Catched ---
File not found
> work/tracing_with_params.rb:10:in `read_byte'
< work/tracing_with_params.rb:10:in `read_byte'
> work/tracing_with_params.rb:8:in `read_byte(10)'
< work/tracing_with_params.rb:14:in `read_input'
> work/tracing_with_params.rb:13:in `read_input(10)'
< work/tracing_with_params.rb:18:in `main'
> work/tracing_with_params.rb:17:in `main(10)'
< work/tracing_with_params.rb:82:in `<main>'
*** 10 ***
--- Catched ---
File not found
work/tracing_with_params.rb:10:in `read_byte'
work/tracing_with_params.rb:14:in `read_input'
work/tracing_with_params.rb:18:in `main'
work/tracing_with_params.rb:82:in `<main>'
EDIT:
The calls to class functions are not recorded. This has to be fixed in order for the stacktrace printing function not to get invalid output.
Moreover I used the STDERR as output to easily get one or the other output. You can change it if you wish.

MAX_STACK_SIZE = 200
tracer = proc do |event|
if event == 'call' && caller_locations.length > MAX_STACK_SIZE
fail "Probable Stack Overflow"
end
end
set_trace_func(tracer)

Related

Why is my current stub in my RSpec test not properly stubbing the call to the method? Currently getting No such file or directory # rb_sysopen

Ruby Version 2.7.2
Rspec Version 3.12.0
So I'm currently working through App Academy Open and I'm at the point where we're creating a tic tac toe game. I've written out all my tests and they pass except for the last few.
I have successfully stubbed method calls in the past but for whatever reason, I'm not getting it to work here.
I have 3 classes Board, HumanPlayer, and Game. The method I am currently testing is #play from within the Gameclass:
def play
while #board.empty_positions?
puts #board.print
position = #current_player.get_position
#board.place_mark(position, #current_player.mark)
if #board.win?(#current_player.mark)
puts "Player #{#current_player.mark} wins!"
return
else
switch_turn
end
end
puts "The game ended in a draw!"
end
Here is what my test looks like:
RSpec.describe Game do
let(:game) { Game.new(:X, :O) }
# ...
describe "#play" do
before :each do
#board = game.instance_variable_get(:#board)
#board.place_mark([0, 0], :X)
#board.place_mark([0, 1], :O)
#board.place_mark([0, 2], :X)
#board.place_mark([1, 0], :O)
#board.place_mark([2, 0], :X)
#board.place_mark([1, 1], :O)
#board.place_mark([2, 2], :X)
end
it "should call Board#place_mark" do
#current_player = game.instance_variable_get(:#current_player)
allow(#current_player).to receive(:get_position).and_return([1, 2])
expect(#board).to receive(:place_mark)
game.play
end
end
end
Here is the HumanPlayer#get_position method:
def get_position
puts "Player #{#mark}, enter two numbers representing a position in the format `row col`"
position = gets.chomp.split(" ")
if position.length != 2 || # not 2 characters
position.any? { |n| n.to_i.to_s != n } # not all numeric
raise "Invalid Position"
end
position.map(&:to_i)
end
Here is the Board#place_mark method:
def place_mark(position, mark)
raise "Placement Invalid" if !valid?(position) || !empty?(position)
row = position[0]
col = position[1]
#grid[row][col] = mark
end
So whenever I run the tests I always get the error:
Game Instance Methods #play should call Board#place_mark
Failure/Error: position = gets.chomp.split(" ")
Errno::ENOENT:
No such file or directory # rb_sysopen - spec/2_game_spec.rb:85
# ./lib/human_player.rb:11:in `gets'
# ./lib/human_player.rb:11:in `gets'
# ./lib/human_player.rb:11:in `get_position'
# ./lib/game.rb:20:in `play'
# ./spec/2_game_spec.rb:92:in `block (4 levels) in <top (required)>'
I believe I'm stubbing the HumanPlayer.get_position method to return [1, 2] when called but for whatever reason, the Board.place_mark method does not successfully place the piece on the board and thus, the HumanPlayer.get_position gets called again because of the loop and when it hits the gets call, it produces that error output.
I've tried stubbing the gets call with this:
it "should call Board#place_mark" do
#current_player = game.instance_variable_get(:#current_player)
allow(#current_player).to receive(:gets).and_return("1 2")
expect(#board).to receive(:place_mark)
game.play
end
I also tired allow_any_instance_of(HumanPlayer) but it just prints the board in an endless loop:
it "should call Board#place_mark" do
#current_player = game.instance_variable_get(:#current_player)
allow_any_instance_of(HumanPlayer).to receive(:get_position).and_return([1, 2])
expect(#board).to receive(:place_mark)
game.play
end
This is my first question on SO, so if there is anything I need to add please let me know. Thanks in advance.
If you can't figure out the Errno::ENOENT error, you might try a slightly different design, which I think is easier to stub.
class GameCLI
def gets
Kernel.gets
end
end
class HumanPlayer
def get_position
# ..
cli = GameCLI.new # or, pass cli instance as argument to e.g. get_position
position = cli.gets # ..
# ..
end
end
RSpec.describe HumanPlayer do
describe '#get_position' do
it '..' do
allow_any_instance_of(GameCLI).to receive(:gets).and_return('..')
# ..
end
end
end

Ruby Error: wrong number of arguments (1 for 0) (ArgumentError)

I am run this code but I am getting an error.
code here :-
class Text
def post(success, error)
if authenticate?(#user, #password)
success.call
else
erro.call
end
end
end
text = Text.new('Ruby Bits!')
success = ->{ puts "Sent!"}
error = ->{ raise 'Auth error'}
text.post(success,error)
Please tell us. How to solve this problem?
Look closely at which line number the ArgumentError is raised from (in your case, line 25).
You are passing one argument to the Text#initialize, but have not defined a version of initialize that takes one argument.
Try this instead (calling the default, zero argument Text constructor):
class Text
def post(success, error)
if authenticate?(#user, #password)
success.call
else
error.call
end
end
end
text = Text.new
success = ->{ puts "Sent!"}
error = ->{ raise 'Auth error'}
text.post(success, error)
Or define initialize with one argument:
class Text
def initialize(your_meaningful_argument)
# do stuff
end
def post(success, error)
if authenticate?(#user, #password)
success.call
else
error.call
end
end
end

How do you test whether a Ruby destructor will be called?

I have created a class which I want to hang on to a file descriptor and close it when the instance is GC-ed.
I created a class that looks something like this:
class DataWriter
def initialize(file)
# open file
#file = File.open(file, 'wb')
# create destructor
ObjectSpace.define_finalizer(self, self.class.finalize(#file))
end
# write
def write(line)
#file.puts(line)
#file.flush
end
# close file descriptor, note, important that it is a class method
def self.finalize(file)
proc { file.close; p "file closed"; p file.inspect}
end
end
I then tried to test the destructor method like so:
RSpec.describe DataWriter do
context 'it should call its destructor' do
it 'calls the destructor' do
data_writer = DataWriter.new('/tmp/example.txt')
expect(DataWriter).to receive(:finalize)
data_writer = nil
GC.start
end
end
end
When running this test, even though the "file closed" is printed along with the file.inspect, the test fails with the following output:
1) DataWriter it should call its destructor calls the destructor
Failure/Error: expect(DataWriter).to receive(:finalize)
(DataWriter (class)).finalize(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
# ./spec/utils/data_writer_spec.rb:23:in `block (3 levels) in <top (required)>'
finalize is called in initialize, returns the proc, and is never called again, so you can't expect it to be called at finalization time. It's the proc that's called when the instance is finalized. To check that, have the proc call a method instead of doing the work itself. This passes:
class DataWriter
# initialize and write same as above
def self.finalize(file)
proc { actually_finalize file }
end
def self.actually_finalize(file)
file.close
end
end
RSpec.describe DataWriter do
context 'it should call its destructor' do
it 'calls the destructor' do
data_writer = DataWriter.new('/tmp/example.txt')
expect(DataWriter).to receive(:actually_finalize)
data_writer = nil
GC.start
end
end
end
even though the "file closed" is printed along with the file.inspect, the test fails with the following output
I threw your code into a single file and ran it. It appears that the finalize code isn't being cleaned up until rspec exits given the output I'm receiving:
Failures:
F
1) DataWriter it should call its destructor calls the destructor
Failure/Error: expect(DataWriter).to receive(:finalize)
(DataWriter (class)).finalize(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
# /scratch/data_writer.rb:27:in `block (3 levels) in <top (required)>'
Finished in 0.01066 seconds (files took 0.16847 seconds to load)
1 example, 1 failure
Failed examples:
rspec /scratch/data_writer.rb:25 # DataWriter it should call its destructor calls the destructor
"file closed"
"#<File:/tmp/example.txt (closed)>"
As to the why of it, I can't tell right now. Dave is right you're asserting on something that's already happened, so your test is never going to pass. You can observe this by changing your test to:
it 'calls the destructor' do
expect(DataWriter).to receive(:finalize).and_call_original
data_writer = DataWriter.new('/tmp/example.txt')
data_writer = nil
GC.start
end
IMHO you should not rely on the finalizer to run exactly when GC runs. They will run, eventually. But perhaps only when the Process finishes. As far as I can tell this is also dependent on the Ruby implementation and the GC implementation.
1.8 has different behavior than 1.9+, Rubinius and JRuby might be different as well.
Making sure that a resource is released can be achieved by a block, which will also take care that the resource is released as soon as not needed anymore.
Multiple APIs have the same style in Ruby:
File.open('thing.txt', 'wb') do |file| # file is passed to block
# do something with file
end # file will be closed when block ends
Instead of doing this (as you showed in your gist)
(1..100_000).each do |i|
File.open(filename, 'ab') do |file|
file.puts "line: #{i}"
end
end
I'd do it this way:
File.open(filename, 'wb') do |file|
(1..100_000).each do |i|
file.puts "line: #{i}"
end
end
I rewrote my working solution below, bit I have not ran this code.
RSpec.describe DataWriter do
context 'it should call its destructor' do
it 'calls the destructor' do
# creating pipe for IPC to get result from child process
# after it garbaged
# http://ruby-doc.org/core-2.0.0/IO.html#method-c-pipe
rd, wr = IO.pipe
# forking
# https://ruby-doc.org/core-2.1.2/Process.html#method-c-fork
if fork
wr.close
called = rd.read
Process.wait
expect(called).to eq('/tmp/example.txt')
rd.close
else
rd.close
# overriding DataWriter.actually_finalize(file)
DataWriter.singleton_class.class_eval do
define_method(:actually_finalize) do |arg|
wr.write arg
wr.close
end
end
data_writer = DataWriter.new('/tmp/example.txt')
data_writer = nil
GC.start
end
end
end
end
The main thing is I catch that GC.start call performs real work exactly when exiting from process. I have tried blocks and threads but in my case (ruby 2.2.4p230 # Ubuntu x86_64) it works only when a process finished.
I suggest, that there may exist a better way to get results from child process, but I used inter-process communication (IPC).
And I have not got result with building the rspec expectation on destructor method call in form like expect(DataWriter).to receive(:actually_finalize).with('/tmp/example.txt') - I don't know why, but I suppose that wrappers created by Rspec have been garbaged or infringed before calling of destructor of a class.
Hope this helps!

Collecting exceptions in ruby script

I'm writing a script which collects data from various url's. I want to collect errors from begin rescue blocks into an array to output them when the program runs in verbose mode. With normal use, a failed connection is ignored and the script moves on to the next url.
I thought the best way to do this would be to create an array errArray = Array.new at the top of the script to hold errors, and then do:
rescue Exception => e
errArray << e.message
in various functions to log errors. The die function outputs the array using p unless it is empty. However, I get the error
Undefined local variable or method 'errArray'
Any help (and constructive criticism) appreciated.
EDIT: die function:
def die(e)
p errorArray unless errorArray.empty?
# Some other irrelevant code
end
errArray is not global variable and therefore methods have no access to it. You can declare it as a global variable by $err_array.
However the best solution would be create a simple class:
class ExceptionCollector
def collect
yield
rescue => e
errors << e.message
end
def errors
#errors ||= []
end
end
And then simple:
$logger = ExceptionCollector.new
$logger.collect do
# this may raise an exception
end
def foo
$logger.collect do
# another exception
end
end
$logger.errors #=> list of errors

rspec puts output test

I'm having trouble understanding how to test for output with puts. I need to know what I need to do in my RSPEC file.
This is my RSPEC file:
require 'game_io'
require 'board'
describe GameIO do
before(:each) do
#gameio = GameIO.new
#board = Board.new
end
context 'welcome_message' do
it 'should display a welcome message' do
test_in = StringIO.new("some test input\n")
test_out = StringIO.new
test_io = GameIO.new(test_in, test_out)
test_io.welcome_message
test_io.game_output.string.should == "Hey, welcome to my game. Get ready to be defeated"
end
end
end
This is the file it is testing against:
class GameIO
attr_reader :game_input, :game_output
def initialize(game_input = $stdin, game_output = $stdout)
#stdin = game_input
#stdout = game_output
end
def welcome_message
output "Hey, welcome to my game. Get ready to be defeated"
end
def output(msg)
#stdout.puts msg
end
def input
#stdin.gets
end
end
NOTE: I updated my RSPEC code to reflect changes I made to my test file given suggestions found elsewhere. To resolve the poblem completly I used the changes suggested by Chris Heald in my main file. Thank you all and thank you Chris.
Your initializer should be:
def initialize(game_input = $stdin, game_output = $stdout)
#game_input = game_input
#game_output = game_output
end
The reason for this is that attr_accessor generates methods like this:
# attr_accessor :game_output
def game_output
#game_output
end
def game_output=(output)
#game_output = output
end
(attr_reader generates only the reader method)
Thus, since you never assign #game_output, your game_output method will always return nil.
Just check you are sending it the message:
#gameio.should_receive(:puts).with("Hey, welcome to my game. Get ready to be defeated")
You could stub puts and print.
Perhaps the most fundamental way is to temporarily reassign STDOUT to a variable, and confirm the variable matches what you expect for output.
And Minitest has must_output as an assertion/spec.
The code is thus:
##
# Fails if stdout or stderr do not output the expected results.
# Pass in nil if you don't care about that streams output. Pass in
# "" if you require it to be silent. Pass in a regexp if you want
# to pattern match.
#
# NOTE: this uses #capture_io, not #capture_subprocess_io.
#
# See also: #assert_silent
def assert_output stdout = nil, stderr = nil
out, err = capture_io do
yield
end
err_msg = Regexp === stderr ? :assert_match : :assert_equal if stderr
out_msg = Regexp === stdout ? :assert_match : :assert_equal if stdout
y = send err_msg, stderr, err, "In stderr" if err_msg
x = send out_msg, stdout, out, "In stdout" if out_msg
(!stdout || x) && (!stderr || y)
end

Resources