Trace $stdout/$stderr in Ruby - ruby

I'm attempting to test the output of my gem that is using GLI however I'm running into issues. In my test setup I'm redirecting $stdout and $stderr to StringIO instances however when I run my code I'm still getting a message printed to my screen rather than being redirected to my custom objects.
begin
old_stdout, old_stderr, $stdout, $stderr = $stdout, $stderr, StringIO.new, StringIO.new
#exit_code = described_class.run([command] + command_args)
#stdout = $stdout.string
#stderr = $stderr.string
ensure
$stdout = old_stdout
$stderror = old_stderr
end
I would expect that described_class.run([command] + command_args) which ends up calling GLI's exit_now! method would be redirected to $stderr however this does not seem to be the case.
Is there a way to "trace" where/how a string was printed to the screen?

It looks like you're using RSpec based on described_class. If you're just trying to test stdout/stderr, then there's a matcher for that.
it "does something" do
expect {
described_class.run([command] + command_args)
}.to output(/some string match/).to_stdout
end
The owner of the GLI project made some suggestions here about how to test GLI setup, perhaps that will help as well.

Related

Suppress STDOUT during RSpec, but not Pry

I'm testing a generator, which outputs a lot of stuff to STDOUT. I want to suppress this, and there are lots of answers for that.
But I want to still be able to use pry. Right now, I have to disable the suppression if I need to pry into the test state.
I was using this code. It bypassed pry entirely:
def suppress_output(&block)
#original_stderr = $stderr
#original_stdout = $stdout
$stderr = $stdout = StringIO.new
yield(block)
$stderr = #original_stderr
$stdout = #original_stdout
#original_stderr = nil
#original_stdout = nil
end
I replaced it with this. It stops at the pry, but continues to suppress output, so you can't do anything:
def suppress_output(&block)
orig_stderr = $stderr.clone
orig_stdout = $stdout.clone
$stderr.reopen File.new("/dev/null", "w")
$stdout.reopen File.new("/dev/null", "w")
yield(block)
rescue Exception => e
$stdout.reopen orig_stdout
$stderr.reopen orig_stderr
raise e
ensure
$stdout.reopen orig_stdout
$stderr.reopen orig_stderr
end
Is there any way to have my cake and eat it too?
I'd still like an answer to this question if someone can think of a way. This isn't the only time I've had to suppress STDOUT in tests, and the scenarios haven't always been the same as this one.
However, it occurred to me today that in this case, the easier solution is to change the code, rather than the testing setup.
The generators are using Thor, which is very powerful, but has very opaque documentation past the basics and hasn't really been updated in years. When I dug around in the docs, I found there is some muting capability.
By calling add_runtime_options! in my main Cli < Thor class, I get a global --quiet option. This suppresses a lot of output, but not everything I need. #say still prints. #run itself is muted, but whatever shell commands I pass it to run are not.
Overwriting these methods takes care of the rest of my issues:
no_commands do
def quiet?
!!options[:quiet]
end
def run(command, config = {})
config[:capture] ||= quiet?
super(command, config)
end
def say(message = "", color = nil, force_new_line = (message.to_s !~ /( |\t)\Z/))
super(message, color, force_new_line) unless quiet?
end
end
I don't currently have a use-case where I would only want to suppress some things, so making it all-or-nothing works for now.
Now, I have to explicitly create the Cli instances in my tests with quiet: true, but I can run RSpec without unwanted output and still use pry.
I found this solution by Chris Hough to work in my case, adding the following configuration to spec/spec_helper:
RSpec.configure do |config|
config.before(:each) do
allow($stdout).to receive(:write)
end
end
This replaced an :all before block, which was setting the following (and reversing the assignment in an after block):
$stderr = File.open(File::NULL, "w")
$stdout = File.open(File::NULL, "w")
The fix still suppresses output while allowing Pry to function as expected.

Capture Ruby Logger output for testing

I have a ruby class like this:
require 'logger'
class T
def do_something
log = Logger.new(STDERR)
log.info("Here is an info message")
end
end
And a test script line this:
#!/usr/bin/env ruby
gem "minitest"
require 'minitest/autorun'
require_relative 't'
class TestMailProcessorClasses < Minitest::Test
def test_it
me = T.new
out, err = capture_io do
me.do_something
end
puts "Out is '#{out}'"
puts "err is '#{err}'"
end
end
When I run this test, both out and err are empty strings. I see the message printed on stderr (on the terminal). Is there a way to make Logger and capture_io to play nicely together?
I'm in a straight Ruby environment, not Ruby on Rails.
The magic is to use capture_subprocess_io
out, err = capture_subprocess_io do
do_stuff
end
MiniTest's #capture_io temporarily switches $stdout and $stderr for StringIO objects to capture output written to $stdout or $stderr. But Logger has its own reference to the original standard error stream, which it will write to happily. I think you can consider this a bug or at least a limitation of MiniTest's #capture_io.
In your case, you're creating the Logger inside the block to #capture_io with the argument STDERR. STDERR still points to the original standard error stream, which is why it doesn't work as expected.
Changing STDERR to $stderr (which at that points does point to a StringIO object) works around this problem, but only if the Logger is actually created in the #capture_io block, since outside that block it points to the original standard error stream.
class T
def do_something
log = Logger.new($stderr)
log.info("Here is an info message")
end
end
Documentation of capture_subprocess_io
Basically Leonard's example fleshed out and commented with working code and pointing to the docs.
Captures $stdout and $stderr into strings, using Tempfile to ensure that subprocess IO is captured as well.
out, err = capture_subprocess_io do
system "echo Some info" # echos to standard out
system "echo You did a bad thing 1>&2" # echos to standard error
end
assert_match %r%info%, out
assert_match %r%bad%, err
NOTE: This method is approximately 10x slower than #capture_io so only use it when you need to test the output of a subprocess.
See Documentation
This is an old question, but one way we do this is to mock out the logger with an expects. Something like
logger.expects(:info).with("Here is an info message")
This allows us to assert the code under test without changing how logger works out of the box.
As an example of capture_io, we have a logger implementation to allow us to pass in hashes and output them to json. When we test that implementation we use capture_io. This is possible because we initialize the logger implementation in our subject line with $stdout.
subject { CustomLogging.new(ActiveSupport::Logger.new($stdout)) }
in the test
it 'processes a string message' do
msg = "stuff"
out, err = capture_io do
subject.info(msg)
end
out.must_equal "#{msg}\n"
end
You need to provide a different StringIO object while initializing Logger.new to capture the output, rather than the usual: STDERR which actually points to the console.
I modified the above two files a bit and made into a single file so that you can copy and test easily:
require 'logger'
require 'minitest'
class T
def do_something(io = nil)
io ||= STDERR
log = Logger.new io
log.info("Here is an info message")
end
end
class TestT < Minitest::Test
def test_something
t = T.new
string_io = StringIO.new
t.do_something string_io
puts "Out: #{string_io.string}"
end
end
Minitest.autorun
Explanation:
Method do_something will function normally in all other code when used without the argument.
When a StringIO method is provided, it uses that instead of the typical STDERR thus enabling to capture output like into a file or in this case for testing.

Thread-safe: Capturing the output of $stdout

I wonder how to capture the output of $stdout in a threaded environment in ruby.
A few details. I use the capture for logging purpose and I have sidekiq to process jobs in the background so the threads. Ive coded:
#previous_stdout, $stdout = $stdout, StringIO.new
self.stdout = $stdout.string
It throws me (for certain threads):
WARN: undefined method `string' for #<IO:<STDOUT>>
If you are logging things yourself, keep a StringIO log per job and write to it:
#log = StringIO.new
#log.write 'debug message'
Sidekiq also offers logging options:
https://github.com/mperham/sidekiq/wiki/Logging
Otherwise you could try something real hacky like overwriting the printing methods on Kernel and synchronizing it so you never accidentally write to the wrong $stdout. Something like:
require 'stringio'
module Kernel
##io_semaphore = Mutex.new
[ :printf, :p, :print, :puts ].each do |io_write|
hidden_io_write = "__#{io_write}__"
alias_method hidden_io_write, io_write
define_method(io_write) do |*args|
##io_semaphore.synchronize do
$stdout = Thread.current[:log] || STDOUT
self.__send__(hidden_io_write, *args)
$stdout = STDOUT
end
end
end
end
threads = 3.times.map do
Thread.new do
Thread.current[:log] = log = StringIO.new
sleep(rand)
puts "testing..."
log.string
end
end
logs = threads.map(&:value)
p logs
# => ["testing...\n", "testing...\n", "testing...\n"]

Suppress console output during RSpec tests

I am testing the class which put on the console some messages (with puts, p warnings and etc.). I am just wondering if there is any ability to suppress this output during RSpec tests ?
I suppress puts output in my classes by redirecting $stout to a text file. That way, if I need to see the output for any reason, it is there but it doesn't muddy up my test results.
#spec_helper.rb
RSpec.configure do |config|
config.before(:all, &:silence_output)
config.after(:all, &:enable_output)
end
public
# Redirects stderr and stout to /dev/null.txt
def silence_output
# Store the original stderr and stdout in order to restore them later
#original_stderr = $stderr
#original_stdout = $stdout
# Redirect stderr and stdout
$stderr = File.new(File.join(File.dirname(__FILE__), 'dev', 'null.txt'), 'w')
$stdout = File.new(File.join(File.dirname(__FILE__), 'dev', 'null.txt'), 'w')
end
# Replace stderr and stdout so anything else is output correctly
def enable_output
$stderr = #original_stderr
$stdout = #original_stdout
#original_stderr = nil
#original_stdout = nil
end
EDIT:
In response to the comment by #MyronMarston, it probably would be smarter to just insert the methods directly into before and after as blocks.
#spec_helper.rb
RSpec.configure do |config|
original_stderr = $stderr
original_stdout = $stdout
config.before(:all) do
# Redirect stderr and stdout
$stderr = File.new(File.join(File.dirname(__FILE__), 'dev', 'null.txt'), 'w')
$stdout = File.new(File.join(File.dirname(__FILE__), 'dev', 'null.txt'), 'w')
end
config.after(:all) do
$stderr = original_stderr
$stdout = original_stdout
end
end
It looks a little cleaner and keeps methods off of main.
Also, note that if you are using Ruby 2.0, you can use __dir__ instead of File.dirname(__FILE__).
EDIT2
Also it should be mentioned, that you can forward to true os /dev/null by using File::NULL as it was introduced in Ruby v 1.9.3. (jruby 1.7)
Then the code snippet will look as following:
#spec_helper.rb
RSpec.configure do |config|
original_stderr = $stderr
original_stdout = $stdout
config.before(:all) do
# Redirect stderr and stdout
$stderr = File.open(File::NULL, "w")
$stdout = File.open(File::NULL, "w")
end
config.after(:all) do
$stderr = original_stderr
$stdout = original_stdout
end
end
Try stubbing methods that make the output in a before block, e.g.
before do
IO.any_instance.stub(:puts) # globally
YourClass.any_instance.stub(:puts) # or for just one class
end
This is explicit, so you won't miss anything you don't want to miss. If you don't care about any output and the method above doesn't work you can always stub the IO object itself:
before do
$stdout.stub(:write) # and/or $stderr if needed
end
An Rspec3.0 Version would be => in spec_helper.rb
RSpec.configure do |c|
c.before { allow($stdout).to receive(:puts) }
end
it will act as before(:each)
but :each is default, so no need to write it explicitly
Tested with rspec-core (~> 3.4.0)
In describe block you could do
# spec_helper.rb
def suppress_log_output
allow(STDOUT).to receive(:puts) # this disables puts
logger = double('Logger').as_null_object
allow(Logger).to receive(:new).and_return(logger)
end
# some_class_spec.rb
RSpec.describe SomeClass do
before do
suppress_log_output
end
end
This way you have the advantage of toggling log output for specific tests. Note, this will not suppress rspec warnings, or messages from rspec.
Another way to disable warnings coming from gems:
add config.warnings = false to spec_helper
If you wanted to suppress only certain logger methods, like error, info, or warn you could do
allow_any_instance_of(Logger).to receive(:warn).and_return(nil)
To disable warnings coming from the rspec gem
allow(RSpec::Support).to receive(:warning_notifier).and_return(nil)
but this is generally discouraged because it is meant as a way to let you know you are doing something smelly in your tests.
If you want to suppress output for a single test, there is a more concise way:
it "should do something with printing" do
silence_stream(STDOUT) do
foo.print.should be_true
end
end
You may want to change STDOUT to STDERR if your test prints an error.
Updated answer for Rails 5, in a one-off situation:
before do
RSpec::Mocks.with_temporary_scope do
allow(STDOUT).to receive(:puts)
end
end
You can make this into a method in spec_helper if you'll be doing this a lot.
After trying all of these examples, I ended up using this varation which does not silence or mute binding.pry
# frozen_string_literal: true
RSpec.configure do |config|
config.before(:each) do
allow($stdout).to receive(:puts)
allow($stdout).to receive(:write)
end
end
It can be useful to inject an IO object defaulting to STDOUT. This also makes it easier to assert on the output if you want to.
E.g.
def my_method(arg, io: STDOUT)
io.puts "hello"
arg.reverse
end
And then in your test:
# Suppress it.
my_method("hi", io: StringIO.new)
# Assert on it.
io = StringIO.new
my_method("hi", io: io)
output = io.tap(&:rewind).read
expect(output).to include("hello")
You could have the object itself supress based on the environment:
class Foo
def call
puts("blah blah")
# ...
end
private
def puts(msg)
return if ENV['APP_ENV'] == 'test'
super
end
end

Reassigning global $stdout to console - ruby

I am trying to set $stdout to write to a file temporarily and then back to a file.
test.rb :
old_stdout = $stdout
$stdout.reopen("mytestfile.out",'w+')
puts "this goes in mytestfile"
$stdout= old_stdout
puts "this should be on the console"
$stdout.reopen("mytestfile1.out",'w+')
puts "this goes in mytestfile1:"
$stdout = old_stdout
puts "this should be back on the console"
Here is the output.
ruby test.rb => no output on the console
cat mytestfile.out
this goes in mytestfile
this should be on the console
cat mytestfile1.out
this goes in mytestfile1:
this should be back on the console
I am not sure why $stdout is not getting reset to console ?
This problem can be resolved by calling dup on $stdout before changing it:
old_stdout = $stdout.dup
$stdout.reopen("mytestfile.out",'w+')
puts "this goes in mytestfile"
$stdout = old_stdout.dup
puts "this should be on the console"
$stdout.reopen("mytestfile1.out",'w+')
puts "this goes in mytestfile1:"
$stdout = old_stdout
puts "this should be back on the console"
Output:
ruby test.rb
# => this should be on the console
# => this should be back on the console
cat mytestfile.out
# => this goes in mytestfile
cat mytestfile1.out
# => this goes in mytestfile1
Here's how I usually package this functionality into a function:
# Runs a block of code while blocking stdout.
# Note that /dev/null should be changed to NUL on Windows.
def silence_stdout(log = '/dev/null')
old = $stdout.dup
$stdout.reopen(File.new(log, 'w'))
yield
$stdout = old
end
Usage:
silence_stdout 'mytestfile.out' do
puts "this goes in mytestfile"
end
puts "this should be on the console"
silence_stdout 'mytestfile1.out' do
puts "this goes in mytestfile1"
end
puts "this should be back on the console"
Edit: as another poster mentioned, using reopen is only necessary when working with pure Ruby code. The function above works both with pure Ruby code and when using, for example, C extensions that write to STDOUT.
You don't need to use reopen if you're just using Ruby code. puts and other Ruby methods will use the current value of $stdout so you can just reassign it.
old_stdout = $stdout
$stdout = File.new("mytestfile.out",'w+')
puts "this goes in mytestfile"
$stdout = old_stdout
puts "this should be on the console"
$stdout = File.new("mytestfile1.out",'w+')
puts "this goes in mytestfile1:"
$stdout = old_stdout
puts "this should be back on the console"
You only need to use reopen if you're doing something like creating child processes (e.g. with fork) and want the child's output to go elsewhere, or if you have an extension that writes directly to standard out without using Ruby's $stdout global.
In your code, when you call reopen you are redirecting both $stdout and old_stdout, as they are both just references to the same IO object, which is why you aren't getting output back to the console when you assign old_stdout back to stdout.

Resources