February 2011 Archives

Executing code at the end of runtime.

The Problem

There is a lot of call for the ability to execute code at the end of runtime. An example of this is Moose when you call __PACKAGE__->make_immutable(). Wouldn’t it be nice if this were just automatic? Other places this can be useful are in testing frameworks such as Test::Class or Fennec. These testing frameworks use a ‘runner’ to run test structures defined in your tests.

The obvious (but bad) solution

Depending on your use-case for code that runs at the end of runtime, there are a couple possibilities. One possibility is to use an END block. Most people should know by now that END {} blocks are bad, simply read the perlmod section. There are just too many edge cases to account for, not to mention conflicting END blocks.

How Test::Class does it

In the case of Test::Class you would create a wrapper script that loads your test module, and then runs Test::Class, which finds the test file and runs it. You can also add logic to run the test into the test file itself thanks to the fact that Test::Class defines tests as proper subroutines, not every project has this luxury.

The boilerplate solution

In most cases, such as Moose they simply tell you that best practice says to call a specific method at the end of your module. Test::More does this as well with done_testing().

What else can I do?

I have 2 more solutions for people, depending on your needs. One is Hook::AfterRuntime, which I wrote a short time back. The other is the solution baked into the most recent release of Fennec.

The Fennec solution

Fennec is similar to Test::Class in that tests are defined as structures in your module, and then a runner needs to actually run them. Unlike Test::Class however, these structures are not defined as subroutines in your packages namespace. I do not remember exactly what prompted me to come up with the solution I did, but I know it happened after using both Test::Class and exec in unrelated projects.

The Fennec solution is to check how perl was run. When you use Fennec; it checks if the file using fennec was run directly, or if it was called by the runner. If the file was called directly it knows it was too late for the runner to catch it. Fennec will then use exec() to re-run perl, telling it to load Fennec::Runner with your test file as an argument. Combined with some PERL5LIB environment magic and you have a solution that runs all your tests through the runner. This prevents the need to call Fennec::Runner->run() at the end of your Fennec test files.

Why is calling Fennec::Runner->run() manually such a big deal?

Essentially the Fennec runner adds parallelization and other great features not just on a per-file level, but also on the suite level. You can leverage Fennec’s runner to wrap your test suite in ways that expand your testing capabilities, specially when parallelization is involved. What these are is beyond the scope of this document, however suffice to say adding Fennec::Runner->run() to the end of each test file would be devastating to suite-level tools which rely on this being called once after the files are all loaded.


The other solution, Hook::AfterRuntime, seems too good to be true. It lets you define a package that adds code to the end of any module that imports it.

package My::Thing
use Hook::AfterRuntime;

sub import {
    my $class = shift;
    my $caller = caller;
    after_runtime { print "End of runtime\n" }

Then to use it:

END { print "END\n" }
use My::Thing;
print "Run-Time\n";

This will print “Run-Time”, “End of runtime”, and finally “END”.

So whats the catch?

Hook::AfterRuntime uses some dark magic including parser hooks like B::Hooks::EndOfScope. All to do one simple thing, that is to inject some code just after your use statement that instantiates an object that calls your hook on DESTROY.


use My::Thing;


use My::Thing; my $__ENDRUNXXXXXXXX = Hook::AfterRuntime->new($ID);

Your code is run at the end of the scope, at the file level that would be the end of the file. It will not work if you wrap that ‘use’ statement in braces. It will also fail if you use require and ->import() manually, likely even inside a BEGIN {} block (not tested)

Clearly there are not yet any 100% perfect solutions. Many great minds have approached this problem, I spoke with a few. But hopefully this shows that with a little innovation you can accommodate several use-cases.

About Chad 'Exodist' Granum

user-pic I write solutions to make things easier.