Capture Ruby Logger output for testing - ruby

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.

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.

Rspec cannot check logger info output

I have an non-rails ruby application, that uses logger to handle log output to stdout.
I want to add specs for checking that output and found two ways to implement this and seems both ways don't work.
So both ways are in this spec.rb file:
require 'rspec'
require 'logger'
describe 'Logger' do
it 'expect to output' do
expect { Logger.new(STDOUT).info('Hello') }.to output.to_stdout
end
it 'expect to recieve' do
expect(Logger.new(STDOUT).info('Hello')).to receive(:info)
end
end
I have output to terminal with log messages, but both verification are failed
Replace STDOUT with $stdout. This works:
expect { Logger.new($stdout).info('Hello') }.to output.to_stdout
The reason is that Rspec internally modifies the global variable $stdout to capture its contents with an StringIO. When using the constant, this mechanism does not work because STDOUT keeps the original value of $stdout.
Notice that you will have a similar problem is you initialize the logger outside of the block. The logger object will store internally the original reference and it won't work:
logger = Logger.new($stdout)
expect { logger.info('Hello') }.to output.to_stdout # This won't work
As per the second question:
expect(Logger.new(STDOUT).info('Hello')).to receive(:info)
This is just a wrong usage of Rspec. It should be something like:
logger = Logger.new(STDOUT)
expect(logger).to receive(:info)
logger.info("Hello")
I typically just use doubles like this:
describe 'logging' do
it 'handles output' do
logger = double('logger')
allow(Logger).to receive(:new).and_return(logger)
run_some_code
expect(logger).to receive(:info).with("Hello")
end
end

Suppress warning messages in Thor CLI app

I've built a Thor CLI app that uses a number of external gems. When I run it I get warning messages from those gems cluttering my output- how can I suppress this?
Clarification: I want to suppress the warning messages only, but still receive the standard output for my app, including errors and puts results.
For example, when I use these same gems in my Sinatra app, I don't get all the warning messages emanating from the gem code like I do with Thor.
EXAMPLE (Derived from http://willschenk.com/making-a-command-line-utility-with-gems-and-thor/)
require 'thor'
require 'safe_yaml'
module Socialinvestigator
class HammerOfTheGods < Thor
desc "hello NAME", "This will greet you"
long_desc <<-HELLO_WORLD
`hello NAME` will print out a message to the person of your choosing.
HELLO_WORLD
option :upcase
def hello( name )
greeting = "Hello, #{name}"
greeting.upcase! if options[:upcase]
puts greeting
end
end
end
In this case, because we're requiring the safe_yaml gem, every time we run a command we'll get the following warnings in our output:
/usr/local/lib/ruby/gems/2.3.0/gems/safe_yaml-1.0.4/lib/safe_yaml.rb:28:
warning: method redefined; discarding old safe_load
/usr/local/Cellar/ruby/2.3.0/lib/ruby/2.3.0/psych.rb:290: warning:
previous definition of safe_load was here
/usr/local/lib/ruby/gems/2.3.0/gems/safe_yaml-1.0.4/lib/safe_yaml.rb:52:
warning: method redefined; discarding old load_file
/usr/local/Cellar/ruby/2.3.0/lib/ruby/2.3.0/psych.rb:470: warning:
previous definition of load_file was here
We're using a number of different gems and getting a whole array of warnings that are cluttering our output...
Firstly, you could potentially submit a Pull request and suppress the message in the dependency or raise an issue asking the gem developers to sort it out for you
Otherwise, this is something I have used before - It's probably from somewhere on SO (or the internet in general) but I can't remember where...
So you basically wrap the noisy method from the dependency with a silence method which simply pushes the STDOUT to a StringIO object and then back again to STDOUT...
require 'stringio'
def silence
$stdout = temp_out = StringIO.new
yield
temp_out.string
ensure
$stdout = STDOUT
end
out = silence { run_dependency_method }
puts out # if in dev mode etc...
Ruby has a global variable to define the verboseness of the output. nil means no warnings, false is the "normal" mode and true is extra verbose (adds some additional runtime information, equivalent to running ruby --verbose):
def without_warnings
verboseness_level = $VERBOSE
$VERBOSE = nil
yield
ensure
$VERBOSE = verboseness_level
end
# Doesn't show warnings about overwriting the constant
without_warnings do
Foo = 42
Foo = 'bar'
end
# Still shows normal output
without_warnings { puts 42 }
# Still shows and throws errors
without_warnings { raise 'wtf' }
If you have control over how the program is ran, you can instead use ruby's -W flag with respective values 0, 1 or 2 (in this case ruby -W 0).
#Yarin,
Following up on #Ismail's answer, you could extend this solution by overloading the new "temporary" StringIO inside the silence method to filter out messages that contain the word "warning".
Here's an example:
require 'stringio'
class FooIO < StringIO
def puts s
super unless s.start_with? "WARN"
end
end
def silence
$stdout = temp_out = FooIO.new
yield
temp_out.string
ensure
$stdout = STDOUT
end
def foo
puts "INFO : Hello World!"
puts "WARN : Oops.. This is a warning..."
puts "ALRT : EVERYONE IS GOING TO DIE!!!"
end
out = silence { foo }
puts out

Share state between test classes and tested classes

I'm experimenting with RSpec.
Since I don't like mocks, I would like to emulate a console print using a StringIO object.
So, I want to test that the Logger class writes Welcome to the console. To do so, my idea was to override the puts method used inside Logger from within the spec file, so that nothing actually changes when using Logger elsewhere.
Here's some code:
describe Logger do
Logger.class_eval do
def puts(*args)
???.puts(*args)
end
end
it 'says "Welcome"' do
end
Doing this way, I need to share some StringIO object (which would go where the question marks are now) between the Logger class and the test class.
I found out that when I'm inside RSpec tests, self is an instance of Class. What I thought initially was to do something like this:
Class.class_eval do
attr_accessor :my_io
#my_io = StringIO.new
end
and then replace ??? with Class.my_io.
When I do this, a thousand bells ring in my head telling me it's not a clean way to do this.
What can I do?
PS: I still don't get this:
a = StringIO.new
a.print('a')
a.string # => "a"
a.read # => "" ??? WHY???
a.readlines # => [] ???
Still: StringIO.new('hello').readlines # => ["hello"]
To respond to your last concern, StringIO simulates file behavior. When you write/print to it, the input cursor is positioned after the last thing you wrote. If you write something and want to read it back, you need to reposition yourself (e.g. with rewind, seek, etc.), per http://ruby-doc.org/stdlib-1.9.3/libdoc/stringio/rdoc/StringIO.html
In contrast, StringIO.new('hello') establishes hello as the initial contents of the string while leaving in the position at 0. In any event, the string method just returns the contents, independent of position.
It's not clear why you have an issue with the test double mechanism in RSpec.
That said, your approach for sharing a method works, although:
The fact that self is an anonymous class within RSpec's describe is not really relevant
Instead of using an instance method of Class, you can define your own class and associated class method and "share" that instead, as in the following:
class Foo
def self.bar(arg)
puts(arg)
end
end
describe "Sharing stringio" do
Foo.class_eval do
def self.puts(*args)
MyStringIO.my_io.print(*args)
end
end
class MyStringIO
#my_io = StringIO.new
def self.my_io ; #my_io ; end
end
it 'says "Welcome"' do
Foo.bar("Welcome")
expect(MyStringIO.my_io.string).to eql "Welcome"
end
end
Logger already allows the output device to be specified on construction, so you can easily pass in your StringIO directly without having to redefine anything:
require 'logger'
describe Logger do
let(:my_io) { StringIO.new }
let(:log) { Logger.new(my_io) }
it 'says welcome' do
log.error('Welcome')
expect(my_io.string).to include('ERROR -- : Welcome')
end
end
As other posters have mentioned, it's unclear whether you're intending to test Logger or some code that uses it. In the case of the latter, consider injecting the logger into the client code.
The answers to this SO question also show several ways to share a common Logger between clients.

How can I copy STDOUT to a file without stopping it showing onscreen using Ruby

I'm attempting to copy stdout to a file for logging purposes. I also want it to display in the Ruby console of the IDE I'm using.
I inserted this code into my script and it redirects $stdout to the my.log file:
$stdout.reopen("my.log", "w")
Does anyone know of a gem or technique to copy the contents of $stdout to a file and not redirect it to a file? Also, I am not using Rails just Ruby.
Something like this might help you:
class TeeIO < IO
def initialize orig, file
#orig = orig
#file = file
end
def write string
#file.write string
#orig.write string
end
end
Most of the methods in IO that do output ultimately use write, so you only have to override this one method. You can use it like this:
#setup
tee = TeeIO.new $stdout, File.new('out.txt', 'w')
$stdout = tee
# Now lots of example uses:
puts "Hello"
$stdout.puts "Extending IO allows us to expicitly use $stdout"
print "heres", :an, :example, "using", 'print', "\n"
48.upto(57) do |i|
putc i
end
putc 10 #newline
printf "%s works as well - %d\n", "printf", 42
$stdout.write "Goodbye\n"
After this example, the following is written identically to both the console and to the file:
Hello
Extending IO allows us to expicitly use $stdout
heresanexampleusingprint
0123456789
printf works as well - 42
Goodbye
I won't claim this technique is fail proof, but it should work for simple uses of stdout. Test it for your use.
Note that you don't have to use reopen on $stdout unless you want to redirect output from a child process or an uncooperative extension. Simply assigning a different IO object to it will work for most uses.
RSpec
The RSpec command line takes a reference to $stdout before you can get any code to run to reassign it, so this doesn't work. reopen still works in this case as you're changing the actual object pointed to by both $stdout and the reference that RSpec has, but this doesn't give you output to both.
One solution is to monkey-patch $stdout like this:
$out_file = File.new('out.txt', 'w')
def $stdout.write string
$out_file.write string
super
end
This works, but as with all monkey patching, be careful. It would be safer to use your OS's tee command.
If you are using Linux or Mac OS, the tee command available in the OS makes it easy to do this. From its man page:
NAME
tee -- pipe fitting
SYNOPSIS
tee [-ai] [file ...]
DESCRIPTION
The tee utility copies standard input to standard output, making a copy in zero or more files. The output is unbuffered.
So something like:
echo '>foo bar' | tee tmp.out
>foo bar
echos the output to STDOUT and to the file. Catting the file gives me:
cat tmp.out
>foo bar
Otherwise, if you want to do it inside your code, it's a simple task:
def tee_output(logfile)
log_output = File.open(logfile, 'w+')
->(o) {
log_output.puts o
puts o
}
end
tee = tee_output('tmp.out')
tee.call('foo bar')
Running it:
>ruby test.rb
foo bar
And checking the output file:
>cat tmp.out
foo bar
I'd use "w+" for my file access to append to the output file, rather than over-write it.
CAVEAT: This opens the file and leaves it open during the life of the code after you've called the tee_output method. That bothers some people, but, personally, it doesn't bother me because Ruby will close the file when the script exits. In general we want to close files as soon as we're done with them, but in your code, it makes more sense to open it and leave it open, than to repeatedly open and close the output file, but your mileage might vary.
EDIT:
For Ruby 1.8.7, use lambda instead of the new -> syntax:
def tee_output(logfile)
log_output = File.open(logfile, 'w+')
lambda { |o|
log_output.puts o
puts o
}
end
tee = tee_output('tmp.out')
tee.call('foo bar')
I know it's a old question, but I found myself in the same situation. I wrote a Multi-IO Class which extends File and overrode write puts and close methods, I also made sure its thread safe:
require 'singleton'
class MultiIO < File
include Singleton
##targets = []
##mutex = Mutex.new
def self.instance
self.open('/dev/null','w+')
end
def puts(str)
write "#{str}\n"
end
def write(str)
##mutex.synchronize do
##targets.each { |t| t.write str; flush }
end
end
def setTargets(targets)
raise 'setTargets is a one-off operation' unless ##targets.length < 1
targets.each do |t|
##targets.push STDOUT.clone if t == STDOUT
##targets.push STDERR.clone if t == STDERR
break if t == STDOUT or t == STDERR
##targets.push(File.open(t,'w+'))
end
self
end
def close
##targets.each {|t| f.close}
end
end
STDOUT.reopen MultiIO.instance.setTargets(['/tmp/1.log',STDOUT,STDERR])
STDERR.reopen STDOUT
threads = []
5.times.each do |i|
threads.push(
Thread.new do
10000.times.each do |j|
STDOUT.puts "out#{i}:#{j}"
end
end
)
end
5.times.each do |i|
threads.push(
Thread.new do
10000.times.each do |j|
STDERR.puts "err#{i}:#{j}"
end
end
)
end
threads.each {|t| t.join}
Tiny improvement from matts answer:
class TeeIO < IO
def initialize(ios)
#ios = ios
end
def write(string)
#ios.each { |io| io.write string }
end
end

Resources