Run, IPC::Run
I do a lot of work that involves automation -- both product and software test. This week, my task was to automate some testing of Perl code which drives the Solaris 10 fault management log viewer fmdump. The fmdump utility allows adminstrators to view system errors and faults with varying levels of verbosity. In order to provide our manufacturing test process with information about faults that occurred during test, tools are required which can parse the varying information output by fmdump, and provide feedback to the test supervisor. Now, once you've created a tool which relies on a system utility, how do you test it in environments where that utility may not exist? Most often, this is done through the use of program stubs -- a program which behaves like the original, but is really just faking it.
Faking it in this case means that for any set of arguments, the stub program should return the same output as the original utility would. In our program stub terminology, this involves playing back a control file, which tells the stub what to output, when to output it, and what exit status to return at the end. I wanted my stubs for fmdump to be able to call on the real program in cases where an existing stub did not exist, and create a control file. Then the next time that same argument combination was passed to the stub, the generated control file would be replayed. While thinking about this problem, I happened across an extremely interesting Perl module called IPC::Run.
IPC::Run is a module which makes calling, managing, and collecting output from subprocesses easy. It is an extremely flexible module, which allows programmers to hook in at an API level that makes sense for their application. At it's simplest, the function run() behaves like system(), but inverts the result so the code makes more sense:
# Old way, using system
system("diff fileA fileB") and die "Failed to run diff!";
# Newer way, using IPC::Run::run
run("diff fileA fileB") or die "Failed to run diff!";
The run() function is probably as far down the IPC::Run API foodchain as most people need to go, as this function allows huge flexibility (which we will see shortly). This function is flexible enough to serve most tasks. For instance, if I wanted to collect the standard output of the above process in a scalar, I'd change the code to this:
# Create a scalar to store the output
my $out;
# Collect that output
run(['diff', 'fileA', 'fileB'], '>', \$out)
or die "Failed to collect diff output!";
Easy as that. Want the standard error too?
# Create a scalar to store the output
my ($err, $out);
# Collect that output
run(['diff', 'fileA', 'fileB'], '>', \$out, '2>', \$err)
or die "Failed to collect diff output!";
There are a large number of redirections that can be implemented using run, including standard input, output, error and more. Also, instead of scalars for data sources and destinations, you can specify filehandles and even subroutines. The latter is what I needed for my control file generation. I wanted to be notified each time a line of output was created on either the standard output or error, and needed to record the time a line came in, with reasonable precision. To do this, I used the following call to run:
# Get hi-res versions of sleep and time
use Time::HiRes qw/sleep time/;
use IPC::Run qw/run new_chunker timeout/;
# Command to execute
my @cmd = qw/fmdump -V/;
# Array to collect 'events'
my @events;
# First, record the command and args
push @events, [ time, 'cmdline', join(" ", @cmd) ];
# Create control events
eval {
run(\@cmd,
'>', new_chunker, sub { push @events, [ time, 'stdout', shift ] },
'2>', new_chunker, sub { push @events, [ time, 'stderr', shift ] },
timeout( 5 ),
);
push @events, [ time, 'status', $? >> 8 ];
};
die if $@;
This snippet of code runs the 'fmdump' argument combination I requested, and records information about how the command behaves. The new_chunker method in the filter chain of the run() command for both streams is a filter provided by IPC::Run. This filter brings in input from the process and splits it into lines (by default using the "\n" separator). Each line is then passed on to the anonymous sub, which records the stream source, time, and line in the event array. When the process exits, the routine then records the exit status in the event array. Note the use of the timeout() function. This specifies to run the number of seconds to allow the task to execute before giving up and throwing an exception. That exception is the main reason for invoking run() from within an eval block. This prevents misbehaving argument combinations from hanging forever. For instance, if someone specified fmdump -f, fmdump attempts to 'file tail' the fault log, and won't exit until it receives a signal. Using a timeout ensures that this will only last for 5 seconds, and will not generate an incomplete control file.
Now that we have the full details about what happened during the process run, it's time to do a bit of housekeeping. The first step is to go through the event list and calculate the line to line delay based on the timestamps. Times are then floor()d to a reasonable precision, rather than keeping the full Perl floating point precision. For this particular application, the precision is 4 digits (0.0001s or 100µs). The event list is then written to a control file on disk, encoded so that the same combination of program and arguments will generate the same control file name. All that is left to do now is to run the Perl module test suite, pointing it at our stubbed fmdump rather than the real one. This generates all of the control files necessary to run that suite of tests. I then can check these control files into revision control as part of the test suite. It is then possible to run the module test suite in any environment.


