Localizing variables in another stack frame?

Ovid on 2008-10-14T09:18:25

The latest development version of Test::Aggregate is out. I've tried to be somewhat conservative about what I add to this module as my primary consideration is "ease of use". This time, I've made such an obvious change that I can't believe I didn't do it earlier.

One problem with aggregating code in Perl is the built-in globals. We're constantly admonished to localize these whenever we use them, but we see things like $| = 1 in plenty of code. If it's a small test program, this doesn't look like a bad thing, but under aggregation it, altering globals like this is a bad thing. In several of our test programs, we've deliberately localized some environment variables to avoid this, but it's important that aggregating tests be (as much as is possible) a simple matter of moving the test from the t/ directory to the aggregated directory. As a result, now when I rewrite the tests, I have this at the top of all of them:

no warnings 'uninitialized';   # I ALWAYS misspell this
local %ENV = %ENV;
local $/   = $/;
local @INC = @INC;
local %INC = %INC;
local $_   = $_;
local $|   = $|;
local %SIG = %SIG;
use warnings 'uninitialized';

I went through perlvar and tried to find the most commonly used globals for this. It's getting closer and closer to emulating separate processes for every test, without the overhead of launching separate processes. Of course, we can't go the full route of separate processes because this kills the performance benefits of aggregation, but it's a decent compromise.

However, I hate having that block of variables there. Even though this is in auto-generated code that the end user doesn't see, sometimes you need to have Test::Aggregate write out the code so you can debug it manually and having all of this code duplication is a frustration. Sure, I could use something stupid like vim folding to hide it, but what I really want is the ability to run local, eval and other bits of code in a higher stack frame. I've tried various tricks to get around this, but so far it eludes me. Is there a solution? (And haven't I asked this before? I don't see that I have, but I know it's been on my mind a number of times)


Probably a dumb idea...

RMGir on 2008-10-14T14:34:29

...but how about a source filter?

Filtering "$.==0" and inserting your block of localizers might work.

Even dumber idea...

RMGir on 2008-10-14T14:55:23

Ah, I missed that these were generated blocks.

Goto's are evil. But this might be a place to use them, perhaps...

Why not put them at the bottom of the file? Then stick a

# GENERATED GOTO - Localize important perl vars
goto LOCALIZE_VARS;
VARS_LOCALIZED:

at the top, and generate this at the bottom:

goto PAST_LOCALIZERS; # so we don't change return
                                            # value of code...
LOCALIZE_VARS:
no warnings 'uninitialized'; # I ALWAYS misspell this
local %ENV = %ENV;
local $/ = $/;
local @INC = @INC;
local %INC = %INC;
local $_ = $_;
local $| = $|;
local %SIG = %SIG;
use warnings 'uninitialized';
goto VARS_LOCALIZED; # jump back to top of code
PAST_LOCALIZERS:

  at the bottom of the localizer block...

This way, the localizers aren't in your face as you're looking at the important part -- the tests.

In fact, any amount of scaffolding could be moved down into that block, if there's any other prologue stuff your framework generates...

Re:Even dumber idea...

Ovid on 2008-10-14T15:29:21

Actually, Andy Armstrong pointed out that I could just use a closure for tricking the stack frame. I felt really stupid once I saw that :)

In other news, turns out the code has a SERIOUS bug. Localizing %INC causes all sorts of nasty things when you try to reload code. Oops!