The Case of the Incompatible Safe
When I entered our rooms, I found C. Auguste Dupin lounging in his fauteuil by the window. His eyes were on the paper in his hand, but he seemed to be gazing beyond it in abstracted thought.
"A death?" I asked, smiling, remembering the events of a few days previous.
"Non, mon ami, simply a puzzle, and a rather pretty one. It appears that one M. Tueur, in attempting to improve his Perl, has entrapped himself in a web of conflicting requirements from which he can not extricate himself."
"Better him than me," I observed. "How did this come about?"
"Very simply. He found himself in a position where he had a string containing Data::Dumper
output, which he needed to load back into his code. Now, normally this requires a stringy eval
, but since he was justly cautious about doing this, he decided to use the Safe
module instead."
"A wise precaution," I remarked, trying to hold up my end of the conversation.
"But then he wanted to do coverage testing."
"Another wise decision. It is a well-known axiom that untested code is buggy code. But when do the conflicting requirements come in?"
"Ah, they have already done so, mon ami. Devel::Cover
, in order not to pollute the name spaces in which it runs, makes fully-qualified calls to itself, which can not be resolved from inside a Safe
compartment. So however great his test coverage might have been, by simply trying to measure it he ensures that code below his calls to Safe
's reval()
method is not executed."
"By blazes! That is a poser. What can he do? I suppose he could change his code to detect the loading of Devel::Cover
, and fall back to a stringy eval. But no, you don't want to change production code just to test it."
"Non, non that is never wise," replied Dupin.
"Wait. He could write a mock Safe
object, and use that in his testing. His new()
would return a blessed reference to an empty hash, and his reval()
could simply do a stringy eval
on its argument. Other methods could simply return."
"How, then, will he know that his calls to Safe
actually function as he expects? It may be that he needs to adjust the operand mask allowed by his Safe
compartment. If he uses a mock object in his normal testing, he will not discover this."
"You're right. Whatever he does he gives up something. Do you have any thoughts, Dupin?"
"A small one only, I fear. Your idea of the mock object has merit, but it must be more selective in its application. Suppose M. Tueur had a way to use the mock object only when he is doing coverage testing, but without modifying his production code?"
"Can this thing be done be done?" I asked. "It seems impossible".
"Not impossible. He can simply place his mock object in a directory of its own, mock/cover/
for example, and insert that directory into @INC
only when he is doing coverage testing.
"There are a number of ways to do this. He could detect the loading of Devel::Cover
in his test modules, and modify @INC
there, either directly or by calling lib->import()
.
"But if he is using Module::Build
, a better way is simply to subclass that module, and add the following code to the subclass:
sub ACTION_testcover {
my ( $self, @args ) = @_;
local @INC = ( 'mock/cover', @INC );
return $self->SUPER::ACTION_testcover( @args );
}
"In this way, the change is centralized to the one place where it is
known that a coverage test is being requested."
"Well, I'll be ..." But I was at a loss for words, and the sentence was left dangling in the air of Paris.
I confess to being not entirely satisfied with the solution that I have placed into the mouth of M. Dupin (which is a workaround rather than a real solution), and am left with the feeling that the shade of Edgar Allen Poe, in his capacity as analyst of Maelzel's chess-playing engine, deserves better. But after a day or so of ringing the changes on Safe's share_from()
method, a mock object was the best I could do. Maybe there is a simpler solution, but the RT queue for the Safe
distribution says that at least one other person is as fuddled as I am. Readers?
I cannot fathom how awesome these posts are... :)
Unfortunately I'm not familiar with Safe at all, so I can't advise much.
Thanks. I'm not sure how long I can keep it up, though. What has happened for the last couple is that I have run into a problem, solved it (sort of) and written it up. But not everything I run into is suitable. You want something obscure, but of general interest. The first Dupin post was pretty much ideal. This one was less so (how many people use
Safe
?), but finding the RT ticket decided me in favor of writing it up.What actually happened here was that (for reasons that must have been clear at the time) I was trying to load the data from the CPAN file
modules/03modlist.data.gz
. That file is, for some reason, not aData::Dumper
dump, but it does appear to need a stringy eval for interpretation. That seemed to be a bit of a crock to me, so I thought I would look to see howCPAN.pm
does it. When I saw it usedSafe
, I thought "Ah. Here's an opportunity to take a couple minutes to learn something new and sexy." But then I decided I wanted coverage testing. And the rest, as they say, is history.Being a crime novels (and TV shows) fan, I get a tingling little feeling every time I see a post like this (especially with titles that begin with "the case of"). Kinky? I think so!
Anyway, I don't think you need to limit it to the really really weird cases. I think there's a lot of stuff that beginners do not understand, and while experts might see through (such as me when sometimes I guess the killer right away, but not always), beginners might still learn from it in a *very* compelling way.
I should probably point out I still enjoy reading/watching the rest even when I know the end-result. Sometimes the story is rather how you made a discovery, ya know? :)
The Opcode module may provide a way out of M. Tueur's predicament.
Thank you for the suggestion. The internals seem to be fairly heavy wizardry, but the public interface seems to call for something like
Doing this, I find:
@ops = ( ':default' )
does the job, but while coverage testing, it needs to be@ops = ( ':default', ':load' )
opmask_add()
, there is no way to turn them back on, at least not for the life of the interpreter. And it is easy to see why.Safe
makes use of a non-public interface toOpcode
, which ends up doingopmask_addlocal()
, but I have not been able to get access to this from outsideOpcode
.