I've got some code that I cobbled together from hints found that worked. But something is going wrong, and I am baffled. Nothing is sent to the screen, and file is empty.
Here's the program:
#!/usr/bin/env ruby -w
require "stringio"
class Tee
def initialize
date_str = `date '+%Y%m%d_%H%M%S'`.chomp
#log = File.new("tee_output_example_#{date_str}.log","w")
end
["$stdout", "$stderr"].each do |std|
io = eval(std)
old_write = io.method(:write)
class << io
self
end.module_eval do
define_method(:write) do |text|
unless text =~ /^[\r\n]+$/ # Because puts calls twice.
File.open(#log, "a") do |f|
# f.puts [std[1..-1].upcase, caller[2], text].join(" ")
f.puts text
end
end
old_write.call(text)
end
end
end
end
logger = Tee.new()
logger.puts "text on stdout"
logger.puts "Something else"
$stdout = STDOUT
$stderr = STDERR
$stdout.puts "plain puts to $stdout"
$stderr.puts "plain puts to $stderr"
I managed to solve it simply with this command:
STDOUT.reopen IO.popen "tee stdout.log", "a"
STDERR.reopen IO.popen "tee stderr.log", "a"
Your expectations aren't very clear to me, but this seems to be a reasonable start (with several directions you could take it depending on your ultimate goal).
#!/usr/bin/env ruby -w
class Tee
attr_accessor :log_file
def initialize()
self.log_file = File.open "tee_output_example_#{date_str}.log", "w"
at_exit { log_file.close }
end
def date_str
Time.now.strftime "%Y%m%d_%H%M%S"
end
def puts(*strings)
log_file.puts(*strings)
$stdout.puts(*strings)
end
end
# will be sent to both $stdout and the logfile
logger = Tee.new
logger.puts "text on stdout"
logger.puts "Something else"
# will only be sent to $stdout or $stderr
$stdout.puts "plain puts to $stdout"
$stderr.puts "plain puts to $stderr"
I could give some tips on debugging techniques, but instead, I'll suggest an alternate solution which might meet your needs:
class Tee
def initialize(a,b); #a,#b = a,b; end
def method_missing(m,*args,&b)
#a.send(m,*args,&b)
#b.send(m,*args,&b)
end
end
This class is more generally useful than what you were trying to write; it takes 2 objects, and passes all method calls (including arguments and block) on to BOTH of them. So you could do something like:
tee = Tee.new(File.open("log","w"), $stdout)
tee.puts "Hello world AND log file!"
Related
Ruby has the File class that can be initialized using the normal new() method, or using open() and passing a block. How would I write a class that behaved like this?
File.open("myfile.txt","r") do |f|
...
end
This is a simple example of passing a block to new/open method
class Foo
def initialize(args, &block)
if block_given?
p block.call(args) # or do_something
else
#do_something else
end
end
def self.open(args, &block)
if block_given?
a = new(args, &block) # or do_something
else
#do_something else
end
ensure
a.close
end
def close
puts "closing"
end
end
Foo.new("foo") { |x| "This is #{x} in new" }
# >> "This is foo in new"
Foo.open("foo") { |x| "This is #{x} in open" }
# >> "This is foo in open"
# >> closing
The general outline of File.open is something like this:
def open(foo, bar)
f = do_opening_stuff(foo, bar)
begin
yield f
ensure
do_closing_stuff(f)
end
end
It is yield that invokes the block passed by the caller. Putting do_closing_stuff within an ensure guarantees that the file gets closed even if the block raises an exception.
More nitty-gritty on the various ways of calling blocks here: http://innig.net/software/ruby/closures-in-ruby
You can create a class method which creates an instance, yields it, and then then performs cleanup after the yield.
class MyResource
def self.open(thing, otherthing)
r = self.new(thing, otherthing)
yield r
r.close
end
def initialize(thing, otherthing)
#thing = thing
#otherthing = otherthing
end
def do_stuff
puts "Doing stuff with #{#thing} and #{#otherthing}"
end
def close
end
end
Now, you can either use it with a constructor:
r = MyResource.new(1, 2)
r.do_stuff
r.close
or using a block, which automatically closes the object:
MyResource.open(1, 2) do |r|
r.do_stuff
end
I have a simple function using gets.chomp like this:
def welcome_user
puts "Welcome! What would you like to do?"
action = gets.chomp
end
I'd like to test it using ruby's built in TestCase suite like this:
class ViewTest < Test::Unit::TestCase
def test_welcome
welcome_user
end
end
The problem is, when I run that test, the gets.chomp stops the test because it needs the user to enter in something. Is there a way I can simulate user inputs using just ruby?
You could create a pipe and assign its "read end" to $stdin. Writing to the pipe's "write end" then simulates user input.
Here's an example with a little helper method with_stdin for setting up the pipe:
require 'test/unit'
class View
def read_user_input
gets.chomp
end
end
class ViewTest < Test::Unit::TestCase
def test_read_user_input
with_stdin do |user|
user.puts "user input"
assert_equal(View.new.read_user_input, "user input")
end
end
def with_stdin
stdin = $stdin # remember $stdin
$stdin, write = IO.pipe # create pipe assigning its "read end" to $stdin
yield write # pass pipe's "write end" to block
ensure
write.close # close pipe
$stdin = stdin # restore $stdin
end
end
You first separate the 2 concerns of the method:
def get_action
gets.chomp
end
def welcome_user
puts "Welcome to Jamaica and have a nice day!"
action = get_action
return "Required action was #{action}."
end
And then you test the second one separately.
require 'minitest/spec'
require 'minitest/autorun'
describe "Welcoming users" do
before do
def get_action; "test string" end
end
it "should work" do
welcome_user.must_equal "Required action was test string."
end
end
As for the first one, you can
Test it by hand and rely that it won't break (recommended approach, TDD is not a religion).
Get the subverted version of the shell in question and make it imitate the user, and compare
whether get_action indeed gets what the user types.
While this is a practical answer to your problem, I do not know how to do 2., I only know how to imitate the user behind the browser (watir-webdriver) and not behind the shell session.
You could inject the IO dependency. gets reads from STDIN, which is class IO. If you inject another IO object into your class, you can use StringIO in your tests. Something like this:
class Whatever
attr_reader :action
def initialize(input_stream, output_stream)
#input_stream = input_stream
#output_stream = output_stream
end
def welcome_user
#output_stream.puts "Welcome! What would you like to do?"
#action = get_input
end
private
def get_input
#input_stream.gets.chomp
end
end
Tests:
require 'test/unit'
require 'stringio'
require 'whatever'
class WhateverTest < Test::Unit::TestCase
def test_welcome_user
input = StringIO.new("something\n")
output = StringIO.new
whatever = Whatever.new(input, output)
whatever.welcome_user
assert_equal "Welcome! What would you like to do?\n", output.string
assert_equal "something", whatever.action
end
end
This allows your class to interact with any IO stream (TTY, file, network, etc.).
To use it on the console in production code, pass in STDIN and STDOUT:
require 'whatever'
whatever = Whatever.new STDIN, STDOUT
whatever.welcome_user
puts "Your action was #{whatever.action}"
In order to handle a -q (quiet) option in my script, I did this:
class NoWrite
def write(*args)
end
end
$stdout = NoWrite.new if $options[:quiet]
It works fine.
However I'm wondering if there isn't a metaprogramming way of making the write method moot.
remove_method and undef_method can't be used because the write method has to be defined, I just want to make it do nothing.
Also, $stderr can't be impacted.
Alternatively, you can redirect stdout to /dev/null
def stfu
begin
orig_stdout = $stdout.clone
$stdout.reopen File.new('/dev/null', 'w')
retval = yield
rescue Exception => e
$stdout.reopen orig_stdout
raise e
ensure
$stdout.reopen orig_stdout
end
retval
end
require 'some_noisy_gem'
stfu do
some_function_that_generates_a_lot_of_cruft_on_stdout
end
Original snippet is at: https://jacob.hoffman-andrews.com/README/index.php/2011/04/14/ruby-function-stfu-temporarily-redirect-noisy-stdout-writes-to-devnull/
You can use instance_eval for this:
$stdout.instance_eval{ def write(*args); end } if $options[:quiet]
Or you can make an anonymous class:
$stdout = Class.new{ def self.write(*args); end } if $options[:quiet]
I am trying to write fails and errors that occur in the test to a log file, so that they don't appear on-screen, but it appears as though errors and assert failures write to STDOUT instead of STDERR. I have been unable to find information on how to redirect this output after hours of googling, and would greatly appreciate help.
Why should the errors not be on stdout?
Up to now I have no prepared solution to suppress the output for specific errors.
If you accept the unchanged output, I have a solution to store errors and failures in a file. It will be no problem to create a 2nd file for Notifications...
gem 'test-unit'
require 'test/unit'
module Test
module Unit
class TestSuite
alias :old_run :run
def run(result, &progress_block)
old_run(result, &progress_block)
File.open('test.log', 'w'){|f|
result.faults.each{|err|
case err
when Test::Unit::Error, Test::Unit::Failure
f << err.long_display
f << "\n===========\n"
#not in log file
when Test::Unit::Pending, Test::Unit::Notification, Test::Unit::Omission
end
}
}
end
end
end
end
class MyTest < Test::Unit::TestCase
def test_1()
assert_equal( 3, 1+1) #failure
end
def test_2()
1 / 0 #force an error
end
def test_3()
notify 'sss'
end
def test_4()
pend "MeineKlasse.new"
end
def test_5
omit 'aaa' if RUBY_VERSION == '1.9.2'
end
def test_5
assert_in_delta( 0.1, 0.00001, 1.0/10)
end
end
Have you tried redirecting the output to StringIO, to write it to a logfile later?
original_stdout = $stdout
original_stderr = $stderr
fake_stdout = StringIO.new
fake_stderr = StringIO.new
$stdout = fake_stdout
$stderr = fake_stderr
Then after you've ran the tests:
$stdout = original_stdout
$stderr = original_stderr
#stdout = fake_stdout.string
#stderr = fake_stderr.string
I'm not really sure this will work though...
I hope I understood yor question correct.
Do you mean something like this:
gem 'test-unit'
require 'test/unit'
class StdOutLogger < IO
def initialize(*)
super
#file = File.open('log.txt', 'w')
#stdout = true
end
#write to stdout and file
def write(s)
$stdout << s
#file << s
end
end
STDOUT = StdOutLogger.new(1)
class MyTest < Test::Unit::TestCase
def test_1()
assert_equal( 2, 1+1)
assert_equal( 2, 4/2)
assert_equal( 1, 3/2)
assert_equal( 1.5, 3/2.0)
end
end
But I would recommend to copy stdout on operation system level
ruby mytest.rb > log.txt
Here is a version to skip between stdout and file output. The output switch is only a interim solution - perhaps it can be made better.
class StdOutLogger < IO
def initialize(*)
super
#file = File.open('log.txt', 'w')
#stdout = true
end
def write(s)
case s
when /\AError:/
#stdout = false #change to file
when /\A(Pending):/
#stdout = true #change to file
end
if #stdout
$stdout << s
else
#file << s
end
end
end
STDOUT = StdOutLogger.new(1)
class DobbsyKretts
def initialize
#Receive idea
puts "Enter an idea, a secret or anything else you want to secretize; hit enter to stop typing and save the file"
(#idea = gets).reverse.upcase
#Filename and saving - to encrypt the file
puts "Enter the file name you'd like to have this saved as; Type PLAN at the beginning for plans and REM for reminders"
(#file_name = gets.chomp.upcase)
File::open("DobbsyKrett-"+ #file_name + ".txt", "w") do |f|
f << #idea
end
end
def unzip
puts "Do you want to withdraw PLAN or REM"
response = gets.chomp.upcase!
puts "Invalid" if !["PLAN","REM"].include?(response)
file_contents = nil
Dir['DobbsyKrett-'+response+"*.txt"].each do |file_nom|
file_contents = File.read(file_nom)
end
puts file_contents
end
end
somethingsomething1 = DobbsyKretts.new
somethingsomething1.unzip
def unzip
puts "Do you want to withdraw PLAN or REM"
#response = gets.strip
if #response.downcase != "plan" and #response.downcase != "rem"
puts "Invalid" end
Dir["DobbsyKrett-"+#response+".txt"].each do |file_nom|
#value = file.read(file_nom)
end
puts #value
end
end
The function gets will return a string with the line-ending character at the end which is not what you expected. To remove it, use the chomp function:
#response = gets.chomp
It is okay for a method (e.g. unzip) to create new instance variables (e.g. #valueholder). In general it's always better for your variables to have the smallest possible scope, so unless you need to read valueholder later, you should just use a local variable (remove the # from the name):
Dir["DobbsyKrett-"+#response+".txt"].each do |file_nom|
valueholder = File.read(file_nom)
end
puts valueholder
Also, valueholder is a terrible name for a variable but if you made it a local variable that could be excused.
Also, your block startings/endings are mismatched. Here's a fixed version of your function that shouldn't result in syntax errors:
def unzip
puts "Do you want to withdraw PLAN or REM"
response = gets.chomp.downcase
if !["plan","rem"].include? response
puts "Invalid"
else
Dir["DobbsyKrett-#{response}.txt"].each do |file_nom|
valueholder = file.read(file_nom)
end
puts valueholder
end
end
Edit: You should capitalize File to correctly call File.read.