How to test a script that generates files - ruby

I am creating a Rubygem that will let me generate jekyll post files. One of the reasons I am developing this project is to learn TDD. This gem is strictly functional on the command line, and it has to make a series of checks to make sure that it finds the _posts directory. This depends on two things:
Wether or not a location option was passed
Is that location option valid?
A location option was not passed
Is the posts dir in the current directory?
Is the posts dir the current working directory?
At that point, I am really having a hard time testing that part of the application. So I have two questions:
is it acceptable/okay to skip tests for small parts of the application like the one described above?
If not, how do you test file manipulation in ruby using minitest?

Some projects I've seen implement their command line tools as Command objects (for example: Rubygems and my linebreak gem). These objects are initialized with the ARGV simply have a call or execute method which then starts the whole process. This enables these projects to put their command line applications into a virtual environment. They could, for example hold the input and output stream objects in instance variables of the command object to make the application independant of using STDOUT/STDIN. And thus, making it possible to test the input/output of the command line application. In the same way I imagine, you could hold your current working directory in an instance variable to make your command line application independent of your real working directory. You could then create a temporary directory for each test and set this one as the working directory for your Command object.
And now some code:
require 'pathname'
class MyCommand
attr_accessor :input, :output, :error, :working_dir
def initialize(options = {})
#input = options[:input] ? options[:input] : STDIN
#output = options[:output] ? options[:output] : STDOUT
#error = options[:error] ? options[:error] : STDERR
#working_dir = options[:working_dir] ? Pathname.new(options[:working_dir]) : Pathname.pwd
end
# Override the puts method to use the specified output stream
def puts(output = nil)
#output.puts(output)
end
def execute(arguments = ARGV)
# Change to the given working directory
Dir.chdir(working_dir) do
# Analyze the arguments
if arguments[0] == '--readfile'
posts_dir = Pathname.new('posts')
my_file = posts_dir + 'myfile'
puts my_file.read
end
end
end
end
# Start the command without mockups if the ruby script is called directly
if __FILE__ == $PROGRAM_NAME
MyCommand.new.execute
end
Now in your test's setup and teardown methods you could do:
require 'pathname'
require 'tmpdir'
require 'stringio'
def setup
#working_dir = Pathname.new(Dir.mktmpdir('mycommand'))
#output = StringIO.new
#error = StringIO.new
#command = MyCommand.new(:working_dir => #working_dir, :output => #output, :error => #error)
end
def test_some_stuff
#command.execute(['--readfile'])
# ...
end
def teardown
#working_dir.rmtree
end
(In the example I'm using Pathname, which is a really nice object oriented file system API from Ruby's standard library and StringIO, which is useful for for mocking STDOUT as it's an IO object which streams into a simple String)
In the acutal test you could now use the #working_dir variable to test for existence or content of files:
path = #working_dir + 'posts' + 'myfile'
path.exist?
path.file?
path.directory?
path.read == "abc\n"

From my experience (and thus this is VERY subjective), I think it's ok sometimes to skip unit testing in some areas which are difficult to test. You need to find out what you get in return and the cost for testing or not. My rule of thumb is that the decision to not test a class should be very unusual (around less than 1 in 300 classes)
If what you're trying to test is very difficult, because of the dependencies with the file system, I think you could try to extract all the bits that interact with the file system.

Related

How do I share object from main file to supporting file in ruby?

I have something similar.
# MAIN.RB
require 'sockets'
require_relative 'replies.rb'
hostname = 'localhost'
port = 6500
s = TCPSocket.open(hostname, port)
$connected = 0
while line = s.gets # Read lines from the socket
#DO A BUNCH OF STUFF
if line == "Hi"
reply line
end
end
s.close
Then I have the reply function in a secondary file.
# REPLIES.RB
def reply(input)
if input == "Hi"
s.write("Hello my friend.\n"
end
end
However calling on the object s from the second file does not seem to work. How would I go about making this work. I'm new to Ruby. I've searched google for the answer, but the only results I have found is with sharing variables across files. I could always do a return "Hello my friend.\n", but I rather be able to write to the socket object directly from the function in REPLIES.rb
Remember that variables are strictly local unless you expressly pass them in. This means s only exists in the main context. You can fix this by passing it in:
reply(s, line)
And on the receiving side:
def reply(s, input)
# ...
end
I'd strongly encourage you to try and indent things consistently here, this code is really out of sorts, and avoid using global variables like $connected. Using a simple self-contained class you could clean up this code considerably.
Also, don't add .rb extensions when calling require. It's implied.

A ruby script to run other ruby scripts

If I want to run a bunch of ruby scripts (super similar, with maybe a number changed as a commandline argument) and still have them output to stdout, is there a way to do this?
i.e a script to run these:
ruby program1.rb input_1.txt
ruby program1.rb input_2.txt
ruby program1.rb input_3.txt
like
(1..3).each do |i|
ruby program1.rb input_#{i}'
end
in another script, so I can just run that script and see the output in a terminal from all 3 runs?
EDIT:
I'm struggling to implement the second highest voted suggested answer.
I don't have a main function within my program1.rb, whereas the suggested answer has one.
I've tried this, for script.rb:
require "program1.rb"
(1..6).each do |i|
driver("cmd_line_arg_#{i}","cmd_line_arg2")
end
but no luck. Is that right?
You can use load to run the script you need (the difference between load and require is that require will not run the script again if it has already been loaded).
To make each run have different arguments (given that they are read from the ARGV variable), you need to override the ARGV variable:
(1..6).each do |i|
ARGV = ["cmd_line_arg_#{i}","cmd_line_arg2"]
load 'program1.rb'
end
# script_runner.rb
require_relative 'program_1'
module ScriptRunner
class << self
def run
ARGV.each do | file |
SomeClass.new(file).process
end
end
end
end
ScriptRunner.run
.
# programe_1.rb
class SomeClass
attr_reader :file_path
def initialize(file_path)
#file_path = file_path
end
def process
puts File.open(file_path).read
end
end
Doing something like the code shown above would allow you to run:
ruby script_runner.rb input_1.txt input_2.txt input_3.txt
from the command line - useful if your input files change. Or even:
ruby script_runner.rb *.txt
if you want to run it on all text files. Or:
ruby script_runner.rb inputs/*
if you want to run it on all files in a specific dir (in this case called 'inputs').
This assumes whatever is in program_1.rb is not just a block of procedural code and instead provides some object (class) that encapsulates the logic you want to use on each file,meaning you can require program_1.rb once and then use the object it provides as many times as you like, otherwise you'll need to use #load:
# script_runner.rb
module ScriptRunner
class << self
def run
ARGV.each do | file |
load('program_1.rb', file)
end
end
end
end
ScriptRunner.run
.
# program_1.rb
puts File.open(ARGV[0]).read

How can I make Aruba use a different ENV["HOME"]?

From the docs:
Per default Aruba will create a directory tmp/aruba where it performs its file operations.
However, my application uses ENV["HOME"] to create and read a file (~/.foorc), so I need Aruba to use a fake ENV["HOME"].
Do I need to set it in some support-file, or is there a way to tell Aruba to its tmp/aruba for files in ENV["HOME"]?
Here is an excerpt of my code that I am testing (obviously I am testing this with Cucumber/Aruba on a much higher level, but the usage of ENV["HOME"] is what is important here.):
def initialize config_path = ""
if config_path.empty?
#config_path = File.join ENV["HOME"], ".todotxt.cfg"
else
#config_path = config_path
end
if file_exists?
super #config_path
validate
end
end
def file_exists?
File.exists? #config_path
end
#....
ask_to_create unless #config.file_exists?
#...
The Specification:
Scenario: todotxt
Given an empty installation
When I run `todotxt`
Then it should pass with:
"""
Should I create a sample config file? [Y/n]
"""
Looking into the implementation in Aruba itself, I could craft something very similar:
File features/support/aruba.rb, is autoloaded by cucumber and implements the Around hook:
# Temporarily enforce an isolated, fake, homedir.
around do |scenario, block|
#__aruba_original_home = ENV["HOME"]
ENV["HOME"] = File.expand_path(File.join("tmp", "aruba"))
block.call
ENV["HOME"] = #__aruba_original_home
end
From now on, a directory tmp/aruba is used as $HOME.
Note that in aruba, this temporary path is configurable, and that above code does not take that into consideration. It will break when the tmp path is configured elsewhere.
Aruba offers a step for just that:
Given a mocked home directory

Testing pure Ruby bin/my_app.rb application with RSpec?

I have a command line (NON-RAILS) application written in pure Ruby that I'm driving out through Cucumber and RSpec. It follows the typical application hierarchy of lib, bin, spec, and feature directories.
Up until now, I've followed the traditional process of writing a failing Cucumber feature/scenario, dropping down to RSpec to drive out the supporting lib files, then getting the scenario to pass.
Unfortunately, this doesn't seem to be as straight forward when driving out the main application entry point in "bin/my_application.rb". The main issue for me is that I'm not describing a class in RSpec, it's a sequential Ruby script for managing the application's classes and initialization via command line parameters and options.
"bin/my_application.rb" is just a small shell-executed wrapper for parsing command line options and passing them onto my main application class as initializer options. I'd still like to test the behavior of the bin script (e.g. MyApp.should_receive(option_a).with(parameter)).
Any suggestions/thoughts/advice? Is this a normal test strategy for driving out command line Ruby script behavior?
Thanks in advance.
Not sure I fully comprehend what you're asking, but I'd say that if you want to use RSpec to test your parameter passing it should be easy enough to do. Say you have your wrapper script:
# my_application.rb
command = Command.new
command.foo = true if ARGV[0]
command.bar = true if ARGV[1]
command.baz = false if ARGV[2]
command.make_dollars(1000000)
Just mix it up and make it a class suitable for testing.
# command_runner.rb
class CommandRunner
def run(args, command = Command.new)
command.foo = true if args[0]
command.bar = true if args[1]
command.baz = false if args[2]
command.make_dollars(1000000)
end
end
# my_application.rb
CommandRunner.new.run(ARGV)
Now the only thing you don't have tested is your default parameter on the run command and the one line in the file my_application.rb
Hope that helps.
Brandon

Good Way to Handle Many Different Files?

I'm building a specialized pipeline, and basically, every step in the pipeline involves taking one file as input and creating a different file as output. Not all files are in the same directory, all output files are of a different format, and because I'm using several different programs, different actions have to be taken to appease the different programs.
This has led to some complicated file management in my code, and the more I try to organize the file directories, the more ugly it's getting. Just about every class involves some sort of code like the following:
#fileName = File.basename(file)
#dataPath = "#{$path}/../data/"
MzmlToOther.new("mgf", "#{#dataPath}/spectra/#{#fileName}.mzML", 1, false).convert
system("wine readw.exe --mzXML #{#file}.raw #{$path}../data/spectra/#{File.basename(#file + ".raw", ".raw")}.mzXML 2>/dev/null")
fileName = "#{$path}../data/" + parts[0] + parts[1][6..parts[1].length-1].chomp(".pep.xml")
Is there some sort of design pattern, or ruby gem, or something to clean this up? I like writing clean code, so this is really starting to bother me.
You could use a Makefile.
Make is essential a DSL designed for handling converting one type of file to another type via running an external program. As an added bonus, it will handle only performing the steps necessary to incrementally update your output if some set of source files change.
If you really want to use Ruby, try a rakefile. Rake will do this, and it's still Ruby.
You can make this as sophisticated as you want but this basic script will match a file suffix to a method which you can then call with the file path.
# a conversion method can be used for each file type if you want to
# make the code more readable or if you need to rearrange filenames.
def htm_convert file
"HTML #{file}"
end
# file suffix as key, lambda as value, the last uses an external method
routines = {
:log => lambda {|file| puts "LOG #{file}"},
:rb => lambda {|file| puts "RUBY #{file}"},
:haml => lambda {|file| puts "HAML #{file}"},
:htm => lambda {|file| puts htm_convert(file) }
}
# this loops recursively through the directory and sub folders
Dir['**/*.*'].each do |f|
suffix = f.split(".")[-1]
if routine = routines[suffix.to_sym]
routine.call(f)
else
puts "UNPROCESSED -- #{f}"
end
end

Resources