worrbase

Faking STDIN in Perl unit tests

2011-05-11

I had this question the other day when I was writing my Exherbo packager the other day. I never found a good, conclusive answer through Google or Identi.ca/Twitter, so I tried something fairly obvious, and it worked!

When doing unit tests for an interactive program, oftentimes the need comes up to fake STDIN, and provide some test interactions. In Perl, this is actually rather trivial.

A note on filehandles in Perl

In Perl, we often see this syntax for opening files:

open(HANDLE, 'path');
print while (<foo>);
close(HANDLE);

This is bad style for a number of reasons (not checking to see if open was successful, not using lexical filehandles, not using 3 param open()...), but regardless, there is a thing or two worth noting here that's relevant to the rest of the discussion.

When you open a filehandle in this manner, you aren't getting a scalar, an array, or even a hash back. You're getting a global typeglob. What the hell does that even mean?

Perl has three separate symbol tables for storing variables, one for each type. Typeglobs are a way to access all three, and are denoted by the asterisk sigil. Hopefully that'll clear up possible questions about the syntax below.

Another note about Perl, but this time about scope

Most newcomers to Perl understand lexical scope and global scope, but get confused about 'local' or dynamic scope.

Dynamic scope is pretty hackish, but also very useful. Dynamic variables (declared with the 'local' keyword) are lexical, however their scope also includes the bodies of any functions that get called within the lexical scope.

This simple example code should highlight what I mean.

#!/usr/bin/env perl

use strict;
use warnings;
use 5.010;

our $foo = "foo!";

sub foofunc {
    say $foo;
}

foofunc();

{
    local $foo = "bar!";
    foofunc();
}

foofunc();

foofunc is expecting global $foo, however localizing (giving it dynamic scope) in that block allow us to change the value of the global the function sees. Dynamic scope ends in the same place lexical scope would end.

Enough build-up

Now that you understand these two key concepts, it should be fairly easy to see how we're going to fake STDIN. Here's the code:

{
    open(FH, '<', 't/input.1') or die $!;
    local *STDIN;
    *STDIN = *FH;
    Exherbo::Packager::init_config('t/config.yml');
    close(FH);
}

Here, we open a new filehandle (lexical filehandles will not work here), and then give STDIN dynamic scope and set its value to our filehandle. It's a simple enough hack, but it works satisfactorily.

If this is the absolute wrong way to go about it, I'd love to know. This is just what I came up with when I couldn't find what I thought to be a reasonable solution with Google.