Forking tests

While testing CtrlO::Crypt::XkcdPassword, I wanted to test how the code behaves in a forked environment. This is quite common, as most Perl web setups are based on a pre-forking server, i.e. a parent process that preloads and setups the app and then forks a few child process to actually handle the incoming requests. But if you connect to some resource in the parent process, you have to make sure that this connection survives the fork and is handled correctly in the child processes.

For some basic understanding of how fork() works, I recommend you read this Perlmaven article.

So, how do you test a fork?

My first try went something like this:

 #!/usr/bin/perl
 use Test::More;
 use CtrlO::Crypt::XkcdPassword;
 
 my $pwgen = CtrlO::Crypt::XkcdPassword->new;
 my $pid   = fork();
 if ( not $pid ) {    # in the child process
     my $child_pw1 = $pwgen->xkcd( words => 1 );
     ok("child $child_pw1");
     sleep 1;
     my $child_pw2 = $pwgen->xkcd( words => 1 );
     ok("child $child_pw2");
     exit;
 }
 
  # in the parent
 my $parent_pw1 = $pwgen->xkcd( words => 1 );
 ok("parent $parent_pw1");
 sleep 1;
 wait();    # no zombies, please
 my $parent_pw2 = $pwgen->xkcd( words => 1 );
 ok("parent $parent_pw2");

 done_testing();

This proved that I actually had a bug: Because the source of entropy is not re-initiated after the fork, both the parent and the child get the same "randomness", and thus produce the same not-so-random passwords:

 prove -vl t/fork.t
 t/fork.t .. 
 ok 1 - child Sanserif
 ok 1 - parent Sanserif
 ok 2 - child Flourish
 ok 2 - parent Flourish

But the above code sample isn't a real test, it's just a script producing some output that I as a human can interpret. To actually convert this into a unit test, I had to overcome a few issues:

fork() confuses TAP

The first problem I encountered was that fork() confuses TAP, as indicated by the messed-up test numbering and some warnings:

 Test Summary Report
 -------------------
 t/fork.t (Wstat: 0 Tests: 4 Failed: 0)
   Parse errors: Tests out of sequence.  Found (1) but expected (2)
                 Tests out of sequence.  Found (2) but expected (3)
                 Tests out of sequence.  Found (2) but expected (4)
                 Bad plan.  You planned 2 tests but ran 4.

Luckily I'm not the first person to have this problem, so CPAN to the rescue: Just use Test::SharedFork and we're done!

 carton exec prove -vl t/fork.t 
 t/fork.t .. 
 ok 1 - parent Cowardly
 ok 2 - child Cowardly
 ok 3 - child Fireman
 ok 4 - parent Fireman
 1..4
 ok
 All tests successful.

Of course the tests are NOT successful, as both the parent and the child produce the same passwords.

Communicate between child and parent

So what I needed to do is to collect the passwords generated in the child process and the parent process and then make sure that they are not the same. I needed what is called IPC or Inter Process Communication. Which is ugly and messy...

I played a bit with IPC::Shareable but did not get it working, so I resorted to a simple and battle-tested way to share data between processes: the file system!

I open a temp-file and write each password into this file (in the child and the parent process). After closing all the child processes, I read the file and can now inspect the passwords and make sure they do not repeat.

So I was happy and pushed the test to CPAN, only to get some FAILs back from the glorious CPAN Testers, because...

Windows does not like to fork()

Another easy fix: just skip the test if we're running on windows:

if ( $^O eq 'MSWin32' ) {
     plan( skip_all => 'skip fork tests on MSWin32' ) ;
 }

Summary

Testing code that fork()s is a bit harder then testing regular code, but luckily Perl and CPAN do make hard things possible!

You can view the whole test here on github.

Update

You can find a response by A. Sinan Unur here (not sure why he did not post a comment, but whatever). As I find the tone of his post a bit passive-aggressive, I'm currently not in the mood to evaluate his suggestions, but I guess I'll take a look at pipe() and also Proc::FastSpawn (as suggested by Mark in a comment) some time in the future. Thanks to Daxim for mentioning the reply in #Austria.pm...