jkeenan1 has asked for the wisdom of the Perl Monks concerning the following question:

I am requesting feedback on code I have written to test subroutines which, in normal use, require the user to enter information at the command-prompt.

Consider this subroutine, which requires the user to enter a 4-digit number beginning with the numeral '4':

sub enter_number { local $_; while () { print "Enter new room number: "; chomp ($_ = <STDIN>); if ($_ =~ /^4\d\d\d$/) { return $_; } else { print "\nMust be a 4-digit number beginning with '4'; try again. +\n"; next; } } }

How would I test this routine using the standard Test::Harness/Test::Builder-based framework? Specifically, how would I have the test framework do something automatically at <STDIN> -- a place where the actual program pauses for user intervention?

I grepped the documentation for 'STDIN'. I googled comp.lang.perl.misc, c.l.p.modules and the perl.module-authors and perl.qa archives. I couldn't find an explicit solution.

I eventually formulated my problem as: ''I want to provide the subroutines with a list of arguments that will be processed as if they were being entered from <STDIN>. I want to tie an array holding that list to the STDIN filehandle.'' Once I had phrased it that way, I studied the section, ''Tying Filehandles'' in Chap. 14 of Programming Perl and wrote the following tie package:

package Preempt::Stdin; use strict; sub TIEHANDLE { my $class = shift; my @lines = @_; bless \@lines, $class; } sub READLINE { my $self = shift; shift @$self; } 1;
This echoes the TieOut.pm module which Michael Schwern uses in Test::More's own test suite -- but this focuses more on working around STDIN rather than STDOUT.

I now can write a script which begins with the package above and proceeds as follows:

package main; use strict; use warnings; my @lines = ( 'a string with numbers and letters 4678', '5678', '46789', ' 4678', '4678', ); tie *STDIN, 'Preempt::Stdin', @lines; my $room = enter_number(); untie *STDIN; print "Room: $room\n";

The script DWIMs. Instead of pausing for user intervention at the prompt, the script shifts elements off @lines and supplies them to enter_room() at <STDIN>. (You will note that I have first supplied four failing values before supplying a value that will succeed.)

How would this look in a testing context? Just throw in some standard code from Test::More.

package main; use strict; use warnings; use Test::More tests => 2; my @lines = ( 'a string with numbers and letters 4678', '5678', '46789', ' 4678', '4678', ); tie *STDIN, 'Preempt::Stdin', @lines; my $room; ok($room = enter_number(), 'enter_number executed'); is(4678, $room, 'predicted value achieved'); untie *STDIN; print "Room: $room\n"; sub enter_number { local $_; while () { print "Enter new room number: "; chomp ($_ = <STDIN>); if ($_ =~ /^4\d\d\d$/) { return $_; } else { print "\nMust be a 4-digit number beginning with '4'; try +again.\n"; next; } } }

If you wanted to clean this up further, you could capture all those prompt messages going to STDOUT with IO::Capture::Stdout -- a CPAN module I was recently turned on to (cf. perl.qa mailing list).

But there are problems.

First, this doesn't work if I have used Perl's 'diamond' line-input operator <> instead of <STDIN>. Tying to STDIN appears to have no effect on the line-input operator; the line-input operator simply pauses and waits for input from the regular STDIN.

Second, if the code being tested calls for input from STDIN more times than there are elements in the array tied to STDIN (or if those elements are undef), and if the user prompt is located (as is likely) within a while (<>) loop, then the program goes into an infinite loop because all elements of the tied array have been shifted off.

So I pose this question to the monastery: Has anyone else come up with a better way to pre-empt STDIN for testing purposes?

Thanks in advance.

Jim Keenan

Replies are listed 'Best First'.
Re: Pre-empting STDIN during Testing
by chromatic (Archbishop) on Feb 15, 2005 at 01:25 UTC

    That's the best way I've found. You might find the tests for Term::Complete interesting; that was the most challenging test I've ever written, I/O wise.

    I don't have time to check right now, but can you tie *ARGV to fix readline() without a filehandle or a scalar? Something in the back of my brain thinks I've done this before.

      In my posting on pre-empting STDIN during testing yesterday, I noted that my provisional solution had the following problem:

      If the code being tested calls for input from STDIN more times than there are elements in the array tied to STDIN (or if those elements are undef), and if the user prompt is located (as is likely) within a while (<>) loop, then the program goes into an infinite loop because all elements of the tied array have been shifted off.

      A simple change in Preempt::Stdin::READLINE() solves this problem: Croak if there are no elements left in the list provided to the subroutine; then capture and examine the error in the testing script.

      Here's the revised Preempt::Stdin:

      And here's the revised testing script, starting with the two passing tests from yesterday and followed by a new test in which the error is captured and examined.

      Two additional points.

      First, as mentioned yesterday, if you wanted to capture all those prompt messages going to STDOUT, you could do so with IO::Capture::Stdout.

      Second, if the subroutine being tested were more complex than the one above, there is a good chance that, in the case of insufficient arguments, warnings would be generated due to uninitialized variables. Since those warnings would be expected, you would probably want to suppress them during testing. You could so by defining a subroutine

      sub _capture { my $str = $_[0]; }

      and then calling

      local $SIG{__WARN__} = \&_capture; tie *STDIN, 'Preempt::Stdin', @lines; ... # as before

      at the top of the block.

      To give credit where credit is due: The idea of capturing the error from a deliberately failing test and examining it with Test::More::like() was picked up from monk dha during our joint work on the Phalanx project. And the idea of capturing warnings within a block was picked up from (IIRC) Benjamin Goldberg on c.l.p.misc a couple of years back.

      Jim Keenan

Re: Pre-empting STDIN during Testing
by Revelation (Deacon) on Feb 15, 2005 at 01:38 UTC
    The easiest way to do this is to use IO::Handle:
    #!/usr/bin/perl use strict; use warnings; use IO::Handle; open (INPUT, "myfile") || die $!; if (my $fh = STDIN->fdopen(\*INPUT, "r")) { print <>; $fh->close; } close INPUT;
    You'll notice that this (at the least) solves your problem with the diamond operator. With regards to a  while (<>) loop, for which I assume, you have a  last() somewhere, the solution is not as clear. Obviously, any file has a limited length. Perhaps, there's some way to use IO::Handle as a base and generate some sort of callback routine that could generate more data, but that's a little too dirty for me. Realistically, are you going to need more than, say, 2,000 lines? If not, then just create a 2,000 line file.

    What's nice about this approach is that IO::Handle handles reverting STDIN to its previous state, after  $fh->close for you. Moreover, it's also a rather simple approach. However, in addition, you could also always localize STDIN, like so:
    { local *STDIN; open(STDIN,'myfile'); #local *STDIN = *MYFILE; print <>; close(STDIN); }
    Both of these aproaches will work whether you're calling STDIN in a used module or in the test script, itself. However, I assume each approach has different limitations as to backwards compatibility which I have not explored as of yet.

    P.S: FYI, this is technique is generally called 'overloading STDIN'. Your searches would have been more successful, even on perlmonks, if you had used the proper vocabulary. :)

    Update: With regards to your specific question, here's a possible (untested--closer to pseudocode) solution, which mimics your own (in functionality.)
    my @lines = ( 'a string with numbers and letters 4678', '5678', '46789', ' 4678', '4678', ); open (INPUT, '+<', "tmpfile") || die $!; print INPUT $_."\n" for @lines; seek(INPUT,0,0); if (my $fh = STDIN->fdopen(\*INPUT, "r")) { my $room = enter_number(); $fh->close; print "Room: $room\n"; } close INPUT; unlink 'tmpfile';
    Moreover, and the true beauty of IO::Handle, is that you can inherit from it. If you want to have a retrieve values from a list, rather than using my silly workaround, then feel free to write a module inheriting from IO::Handle that does so (my advice would be to overload perhaps fdopen and definitely getline and getlines)! Otherwise, IO::Handle _at least_ gives you a template for extending Perl's IO system in a more flexible manner than tying a hash. Perhaps, another monk will have an in-the-box workaround on IO::Handle to allow you to use a callback to a subroutine that shifts from an array (look at IO::String to see how this is handled for strings--you could modify that), but until then, you now have a pretty flexible solution that only involves a temporary file (which can be created using IO::File); I'm going to sleep now, but perhaps tomorrow, I'll think of a better solution, or somebody smarter than myself, like merlyn will do so.

    Gyan Kapur
    The Worst Nodes Upvoter!
      Revelation wrote: P.S: FYI, this is technique is generally called 'overloading STDIN'. Your searches would have been more successful, even on perlmonks, if you had used the proper vocabulary. :)

      Really? What makes you think that?

      I took your suggestion and did a Google Groups search for the exact phrase "overloading STDIN." Nothing on c.l.p.misc, c.l.p.modules or perl.qa.

      I then Super Searched the monastery archives for that phrase. Had to hit the button at least 5 times. Had to go back to an April 29 2001 posting by chromatic (who else?) , Re: Problem with tie *STDIN, 'IO::Scalar', \$text;, to find a node that had contained both words (albeit separated from each other) and had some relevance.

      Now, if I wanted to overcome the problem with the diamond operator, I would certainly have thought in terms of overloading, because operators are what one overloads. And in the node cited above, chromatic does speak of "overloading TIEHANDLE." But I don't think "overloading STDIN" is a phrase that people have commonly used.

        Calm down, man. I wasn't trying to insult you or anything, just giving you a hint at a resource, which comes up if you search overloading STDIN: how do i redirect STDOUT, STDIN, or STDERR to a FILE? . There's something else that comes up on google, I believe, but legal issues prevent me from posting it.
      I looked through the documentation for IO::Handle, IO::File, IO::Scalar, etc. while working on this ... and I confess I can't see the connection to my problem.

      Both of your code examples refer to opening files. I don't see the connection to reading from STDIN. How, for example, would you rework my code samples with IO::Handle or the localization of STDIN?

      Thanks for looking at this problem.

      Jim Keenan

    A reply falls below the community's threshold of quality. You may see it by logging in.
Re: Pre-empting STDIN during Testing
by tmoertel (Chaplain) on Feb 15, 2005 at 03:57 UTC
    If you are on a Unix system, you can redirect STDIN to pull input from a file. You can also open the file a second time for appending to obtain an independent file descriptor. You can write into this descriptor to inject input into STDIN. The OS will keep track of the input and output file positions separately, effectively turning the file into a fifo queue with a history.

    Using this approach, you can inject on the fly, interleave injections and reads, and also simulate EOF input conditions.

    The following code, which should work back to Perl 5.6, shows one way to do it.

    #! perl use warnings; use strict; use File::Temp qw( tempfile ); # this function encapsulates the technique. # it returns a function that can be used to # inject input into the targeted file handle sub inject_input { my $target_fh = shift; my ($temp_fh, $temp_fn) = tempfile(); my $temp_fd = fileno $temp_fh; local *SAVED; local *TARGET = $target_fh; local *INJECT; open SAVED, "<&TARGET" or die "can't remember target"; open TARGET, "<&=$temp_fd" or die "can't redirect target"; open INJECT, "+>>$temp_fn" or die "can't open injector"; unlink $temp_fn; # we don't need the directory entry any more select((select(INJECT), $| = 1)[0]); my $saved_fh = *SAVED; my $inject_fh = *INJECT; return sub { if (@_) { print $inject_fh @_; } else { seek $inject_fh, 0, 0 or die "can't seek"; # rewind my $injected_output = do { local $/; <$inject_fh> }; close $temp_fh or die "can't close temp file handle"; local (*SAVED, *TARGET, *INJECT) = ($saved_fh, $target_fh, $inject_fh); open TARGET, "<&SAVED" or die "can't restore target"; close SAVED or die "can't close SAVED"; close INJECT or die "can't close INJECT"; return $injected_output; } } }

    Here is an example of how to use the function:

    # let us inject input into STDIN my $injector = inject_input(*STDIN); # the resulting injector takes a string to inject $injector->("1234\n"); # inject 1234 print "r: ", scalar <STDIN>; # read 1234 back in # now we can inject 5678, a, and b # we can read them back in via a loop $injector->("5678\n"); # inject 5678 $injector->("a\n", "b\n"); # inject a and b while (<>) { print "R: $_"; } # after the read loop reaches the EOF, we # can still inject more input and read it $injector->("we're outta here\n"); print "r: ", <>; # when we are done, we call the injector with # no arguments; this will close down the injection # apparatus, restore STDIN to its pre-injection # condition, and return a copy of what was injected # for flight-recording purposes print "\n\nwe injected:\n\n", $injector->(); # now STDIN is restored print "\n\nEnter text by hand:\n"; while (<>) { print "You entered: $_"; } # actual output follows: __END__ r: 1234 R: 5678 R: a R: b r: we're outta here we injected: 1234 5678 a b we're outta here Enter text by hand: blah You entered: blah blah You entered: blah blah You entered: blah

    I hope this helps.

    Cheers,
    Tom

      Tom Moertel wrote:

      If you are on a Unix system, you can redirect STDIN to pull input from a file. You can also open the file a second time for appending to obtain an independent file descriptor. You can write into this descriptor to inject input into STDIN. ... Using this approach, you can inject on the fly, interleave injections and reads, and also simulate EOF input conditions.

      Yesterday I was frustrated by the fact that, while there are several ways to redirect where STDIN is going to (by the command-line; by IO::Handle; etc.), there didn't seem to be a robust way of changing where it is coming from. After I logged off, I got to thinking about File::Temp (a module I've been using a lot lately as part of test building in the Phalanx project). I realized that there probably would be a way to use File::Temp to, in effect, fake out STDIN. But I then thought, "That's a lot of work for a simple problem."

      Well, your subroutine does do a lot of work, but it has the virtue of hiding the work in a subroutine. I'll try to check it out today. Thanks for taking the time to present your approach.

      Jim Keenan

Re: Pre-empting STDIN during Testing
by NiJo (Friar) on Feb 15, 2005 at 17:53 UTC
    Jim,

    This looks like what `expect` / Expect.pm is made for. It talks to the unit under test, waiting for a string and answering with another one.

    Refactoring the verification into subroutines might be the best long term solution. Do you know that your program never ever will use a different form of input? What if you need to verify e. g. XML file input?

      I did look at Expect.pm. I suppose my immediate reaction was: "This is immense. Which one of these dozens of functions could be the one I need?" In other words, the learning curve I would face for Expect seemed much greater than what I thought was needed for the task at hand. So I passed on it, but will definitely keep it in mind for similar projects in the future.

      Thanks for responding.

      jimk

        You'll want to start with Expect::Simple. And, frankly, you can start with the following methods and probably never need more:
        • send() - this is what you use to interact with STDIN
        • before() - this is what the prompt was that you just sent stuff to with send()
        • after() - this is what the program blabs to you after you send the stuff with send()

        If you need more, then more reading is the thing to do. Note: As you've found out, interacting with STDIN is hard. Don't complain if you have to read for a few hours to have someone else take all the craziness out of it for you.

        Being right, does not endow the right to be rude; politeness costs nothing.
        Being unknowing, is not the same as being stupid.
        Expressing a contrary opinion, whether to the individual or the group, is more often a sign of deeper thought than of cantankerous belligerence.
        Do not mistake your goals as the only goals; your opinion as the only opinion; your confidence as correctness. Saying you know better is not the same as explaining you know better.