Using puts.chomp after reading a File object on Ruby - ruby

I'm writing a Call Detail Record (CDR) parser on Ruby. CDRs are files with lines, where every line is an action, and the fields of these lines are separated by tabs.
My problem happens after reading every line of the file using CDRFileParser. When I'm using gets.chomp to get some interactivity with the user, gets.chomp won't wait until I press enter, instead it starts using the lines from the file that was already closed. It seems to be a IO buffer problem. (In C I use fflush() for this kind of issue.)
The files of my program are:
command_line_loader.rb:
def comm_line_loader
begin
raise ArgumentError, 'Invalid number of arguments' unless ARGV.length == 1
return File.new(ARGV[0],"r")
rescue ArgumentError
warn "Debe ingresar el nombre del archivo que contiene los CDR"
exit
rescue Errno::ENOENT
warn "El archivo no existe o hay un error en su lectura"
exit
end
end
objects.rb:
class CDRFileParser
def initialize(cdr_source)
#cdr_source = cdr_source
#cdr_list = cdr_to_array
end
def cdr_to_array
cdr_list_aux = Array.new
cdr_list_aux.push #cdr_source.readline.split("\t") unless #cdr_source.eof?
return cdr_list_aux
end
attr_accessor :cdr_list
end
cdr_parser.rb (useless program that shows the problem):
#!/usr/bin/env ruby
require "./command_line_loader"
require "./objects"
cdr = CDRFileParser.new (comm_line_loader)
loop do
a = gets.chomp
puts a
end
Your help and coding suggestions will be really appreciated :)

The Kernel#gets method only reads from standard input if ARGV is empty. Try using $stdin.gets.chomp instead. Alternatively, clear ARGV after you are done reading from it with ARGV.clear and gets will work as you expect.

Related

Using rescue and ensure in the middle of code

Still new to Ruby - I've had a look at some of the answers to seemingly similar questions but, to be honest, I couldn't get my head around them.
I have some code that reads a .csv file. The data is split into groups of 40-50 rows per user record and validates data in the rows against a database accessed via a website.
A login is required for each record, but once that user has logged in each row in the .csv file can be checked until the next user is reached, at which point the user logs out.
All that's working, however, if an error occurs (e.g. a different result on the website than the expected result on the .csv file) the program stops.
I need something that will
a) at tell me which line on the file the error occurred
b) log the row to be output when it's finished running, and
iii) restart the program from the next line in the .csv file
The code I have so far is below
Thanks in advance,
Peter
require 'csv-mapper'
loginrequired = true
Given(/^I compare the User Details from rows "(.*?)" to "(.*?)"$/) do |firstrow, lastrow|
data = CsvMapper.import('C:/auto_test_data/User Data csv.csv') do
[dln, nino, pcode, endor_cd, ct_cd]
end
#Row number changed because Excel starts at 'row 1' and Ruby starts counting at 'row 0'
(firstrow.to_i-1..lastrow.to_i-1).each do |row|
#licnum1 = data.at(row).dln
#licnum2 = data.at(row+1).dln
#nino = data.at(row).nino
#postcode = data.at(row).pcode
#endor_cd = data.at(row).endor_cd
#ct_cd = data.at(row).ct_cd
#Login only required once for each new user-account
if
loginrequired == true
logon_to_vdr #def for this is in hooks
click_on 'P and D'
loginrequired = false
end
#This is the check against the database and is required for every line in the .csv file
check_ctcd #def for this is in hooks
#Need something in here to log errors and move on to the next line in the .csv file
#Compare the ID for the next record and logout if they're different
if #licnum1 == #licnum2
loginrequired = false
else
loginrequired = true`enter code here`
click_on 'Logout'
end
end
end
It seems like you need some error logging since you apparently don't know what type of error you're receiving or where. If this script is standalone you can redirect $stderr to file so that you can read what went wrong.
# put this line at the top of your script
$stderr = File.open("/path/to/your/logfile.log","a")
When an error occurs, ruby will automatically write the error message, class, and backtrace to the log file you specify so that you can trace back the line where things are not going as expected. (When you run a script from the command line, normally this information will just get blurted back to the terminal when an error happens.)
For example, on my desktop I created a file log_stderr.rb with the following (line numbers included):
1 $stderr = File.open("C:/Users/me/Desktop/my_log.log","w")
2
3 #require a file which will raise an error to see the backtrace
4 require_relative 'raise_error.rb'
5
6 puts "code that will never be reached"
Also on my desktop I created the file raise_error.rb with the following (to deepen the backtrace for better example output):
1 # call raise to generate an error arbitrarily
2 # to halt execution and exit the program.
3 raise RuntimeError, 'the program stopped working!'
When I run ruby log_stderr.rb from the command line, my_log.log is created on my desktop with the following:
C:/Users/me/Desktop/raise_error.rb:3:in `<top (required)>': the program stopped working! (RuntimeError)
from C:/Users/me/Desktop/log_stderr.rb:4:in `require_relative'
from C:/Users/me/Desktop/log_stderr.rb:4:in `<main>'
If you are working in a larger environment where your script is being called amidst other scripts then you probably do not want to redirect $stderr because this would affect everything else running in the environment. ($stderr is global as indicated by the $ variable prefix.) If this is the case you would want to implement a begin; rescue; end structure and also make your own logfile so that you don't affect $stderr.
Again, since you don't know where the error is happening you want to wrap the whole script with begin; end
# at the very top of the script, begin watching for weirdness
begin
logfile = File.open("/path/to/your/logfile.log", "w")
require 'csv-mapper'
#. . .
# rescue and end at the very bottom to capture any errors that have happened
rescue => e
# capture details about the error in your logfile
logfile.puts "ERROR:", e.class, e.message, e.backtrace
# pass the error along since you don't know what it is
# and there may have been a very good reason to stop the program
raise e
end
If you find that your error is happening only in the block (firstrow.to_i-1..lastrow.to_i-1).each do |row| you can place the begin; end inside of this block to have access to the local row variable, or else create a top level variable independent of the block and assign it during each iteration of the block to report to your logfile:
begin
logfile = File.open("/path/to/your/logfile.log", "w")
csv_row = "before csv"
#. . .
(firstrow.to_i-1..lastrow.to_i-1).each do |row|
csv_row = row
#. . .
end
csv_row = "after csv"
rescue => e
logfile.puts "ERROR AT ROW: #{csv_row}", e.class, e.message, e.backtrace
raise e
end
I hope this helps!
It doesn't seem like you need to rescue exception here. But what you could do is in your check_ctcd method, raise error if records doesn't match. Then you can rescue from it. In order to know which line it is, in your iteration, you could use #each_with_index and log the index when things go wrong.
(firstrow.to_i-1..lastrow.to_i-1).each_with_index do |row, i|
#licnum1 = data.at(row).dln
#licnum2 = data.at(row+1).dln
#nino = data.at(row).nino
#postcode = data.at(row).pcode
#endor_cd = data.at(row).endor_cd
#ct_cd = data.at(row).ct_cd
#Login only required once for each new user-account
if
loginrequired == true
logon_to_vdr #def for this is in hooks
click_on 'P and D'
loginrequired = false
end
#This is the check against the database and is required for every line in the .csv file
check_ctcd #def for this is in hooks
rescue => e
# log the error and index here
...
And you can make your own custom error, and rescue only the certain type so that you don't silently rescue other errors.

end to end test of a ruby console app

I have a ruby console app that you run with an argument, then once running outputs some text to the screen, asks for some more user input and then outputs some more text to the screen. I want to do an end to end test on this app and I don't know how. If I were writing an end to end test for an REST API, I would just hit the public endpoint, follow the links and then have an expect statement on the output. Easy. But on a console app I have no idea how to do the same thing. Are there any gems for stepping through a console app in the context of a test? I've been looking all day but can't find anything.
ANY help appreciated.
Inspired by this gem which has a fairly simple implementation, I wrote a method which captures console input & output and can, therefore, be used in tests:
require 'stringio'
module Kernel
def emulate_console(console_input)
$stdin = StringIO.new(console_input)
out = StringIO.new
$stdout = out
yield
return out
ensure
$stdout = STDOUT
$stdin = STDIN
end
end
This method captures console output, and also provides as input the string value which you specify in the console_input parameter.
Basic usage
Here's a simple usage of the emulate_console method:
out = emulate_console("abc\n") do
input = gets.chomp
puts "You entered: #{input}!"
end
The return value out is a StringIO object. To access its value, use the #string method:
out.string
=> "You entered: abc!\n"
Note that the input contains a newline character (\n) to simulate pressing the ENTER key.
Testing
Now, let's assume that you want to test this method, that uses both stdin and stdout:
def console_add_numbers
x = Integer(gets)
y = Integer(gets)
puts x + y
end
The following RSpec test tests the happy path of this code:
require 'rspec/autorun'
RSpec.describe '#console_add_numbers' do
it 'computes correct result' do
input = <<-EOS
2
3
EOS
output = emulate_console(input) { console_add_numbers }
expect(output.string.chomp).to eql '5'
end
end

Catch errors in vim/ruby

I'm writing a vim plugin using the ruby interface.
When I execute VIM::command(...), how can I detect if vim raised an error during execution of this command, so that I can skip further commands and also present a better message to the user?
Vim's global variable v:errmsg will give you the last error. If you want to check whether an error occured, you can first set it to an empty string and then check for it:
let v:errmsg = ""
" issue your command
if v:errmsg != ""
" handle the error
endif;
I'll leave it up to you to transfer this to the Ruby API. Also see :h v:errmsg from inside Vim. Other useful global variables may be:
v:exception
v:throwpoint
Edit – this should work (caution: some magic involved):
module VIM
class Error < StandardError; end
class << self
def command_with_error *args
command('let v:errmsg=""')
command(*args)
msg = evaluate('v:errmsg')
raise ::VIM::Error, msg unless msg.empty?
end
end
end
# Usage
# use sil[ent]! or the error will bubble up to Vim
begin
VIM::command_with_error('sil! foobar')
rescue VIM::Error => e
puts 'Rescued from: ' + e.message;
end
# Output
Rescued from: E492: Not an editor command: sil! foobar

Ruby command line program that saves and updates users/preferences

I'm building an application that takes in stdin to save a user and their preferences. Should I write the stdin to a text file and save the user input there?
commandline.rb
class CommandLine
def initialize(filename)
#file = File.open(filename, 'w')
end
def add_user(input)
#file = File.open('new_accounts.txt', 'r+')
#file.write(input)
puts input
end
def run
puts "Welcome to the Command Line Client!"
command = ''
while command != 'quit'
printf "enter command: "
input = gets.chomp
parts = input.split
command = parts[0]
case command
when 'quit' then puts 'Goodbye!'
when '-a' then add_user(parts[1..-1].join(" "))
else
puts 'Invalid command #{command}, please try again.'
end
end
end
end
a = CommandLine.new('new_accounts.txt')
a.run
Let's say I want the user to enter '-a tommy likes apples' in the command line, I want it to output:
tommy likes apples
The same user tommy could also input '-a tommy likes oranges' which would then update his previous preference:
tommy likes oranges
Any help/direction is appreciated, thanks!
I don't see a problem with using a text file if you are doing something simple. Alternatives are many and without more detail I'm afraid I can't make a good recommendation.
def add_user(input)
File.open('new_accounts.txt', 'w') {|file|
file.write(input)
}
puts input
end
FYI: This will make it so that your text file updates. :-)
EDIT: Changed the add_user method.

Discussion with a sub-process, using Ruby with IO and threading

I am trying to use IO.popen in order to put (with .puts method) and to get (with .gets method) messages from a process to its sub-process.
I am not very experimented and I have a question about. Having the following code, I have an error because it is not possible to write in a closed stream.
class Interface
def initialize(path)
#sub_process = IO.popen(path, 'w+')
end
def start!
if ok?
#sub_process.puts 'Hello', 'my name is ...'
# and more...
end
end
protected
def ok?
is_ready?(#sub_process) && is_cool?(#sub_process)
end
def is_ready?(sub_process)
reply = process_command(sub_process, 'are u ready?')
reply.chomp.match(/yes_i_am_ready$/)
end
def is_cool?(sub_process)
reply = process_command(sub_process, 'are u cool?')
reply.chomp.match(/yes_i_am_cool$/)
end
def process_command(sub_process, command)
rdr = Thread.new { sub_process.read } # alternative: io.readlines
sub_process.puts "#{command}"
sub_process.close_write
rdr.value # joins and fetches the result
end
end
a = Interface.new("./program")
a.start!
(...) in `write': not opened for writing (IOError)
As we can see, this error occur during is_cool? test (as explained at: http://ruby-doc.org/core/classes/IO.html#M002289).
But if I try to comment in process_command method the line:
# sub_process.close_write
the script seems to sleep... infinitely :s
I believe that it is not possible to open again a closed stream. And I can't create an other IO.popen instance of my program "./program" because it needs to be initialized with some command (like 'are u ready?' and 'are u cool?') at the beginning, before I use it (by sending and receiving messages like a simple discussion).
How changes can I do over the current code in order to solve this problem?
Edit: in other words, I would like to establish a such communication (according to a given protocol):
Parent message: Child answer:
-------------- ------------
'are u ready?' 'yes_i_am_ready'
'are u cool?' 'yes_i_am_cool'
'Hello' 'foo'
'my name is ...' 'bar'
Many thanks for any help.
Perhaps it will help to have a working example. Here's one, tested and known to work in MRI 1.8.7 on Linux.
bar.rb
#!/usr/bin/ruby1.8
begin
loop do
puts "You said: #{gets}"
$stdout.flush
end
rescue Errno::EPIPE
end
foo.rb
#!/usr/bin/ruby1.8
class Parent
def initialize
#pipe = IO.popen(CHILD_COMMAND, 'w+')
end
def talk(message)
#pipe.puts(message)
response = #pipe.gets
if response.nil?
$stderr.puts "Failed: #{CHILD_COMMAND}"
exit(1)
end
response.chomp
end
private
CHILD_COMMAND = './bar.rb'
end
parent = Parent.new
puts parent.talk('blah blah blah')
puts parent.talk('foo bar baz')
foo.rb output
You said: blah blah blah
You said: foo bar baz
A closed IO can not be used anymore. You should not close an IO if you intend on still using it.
If you remove the IO#close_write there still remains the problem with your code in the following line.
rdr = Thread.new { sub_process.read }
IO#read read's until EOF. So until the stream get's closed it never terminates. You mentioned IO#readline in your code, this would be the better alternative. Using IO#readline your program would only hang if the popend process never send's a newline.
Another problem with popen is the following. IO#popen create's a new process. Process's may be killed by you, other users, memory shortages, …. Don't expect your process to always run all the time. If the process is killed IO#readline will throw an EOFError, IO#read will return imidiatley. You can determine the termination reason with the following code.
Process::wait(io.pid)
status= $?
status.class # => Process::Status
status.signaled? # killed by signal?
status.stopsig # the signal which killed it
status.exited # terminated normal
status.exitstatus # the return value
status.ki
Does it help to use this form of Thread.new?
rdr = Thread.new(sub_process) {|x| x.readlines }

Resources