A Date with CPAN, Part 10: Cleanliness Is Next to Timeliness

[This is a post in my latest long-ass series.  You may want to begin at the beginning.  I do not promise that the next post in the series will be next week.  Just that I will eventually finish it, someday.  Unless I get hit by a bus.

IMPORTANT NOTE!  When I provide you links to code on GitHub, I’m giving you links to particular commits.  This allows me to show you the code as it was at the time the blog post was written and insures that the code references will make sense in the context of this post.  Just be aware that the latest version of the code may be very different.]


Last time I rearranged our UI to be (hopefully) a bit more intuitive.  This time I want to clean up the remainder of those pesky CPAN Testers failures on our way to the next solid release.

So, the first thing I needed to do was to figure out what all those failures were.  See, as I touched on before, CPAN Testers failures tend to come in groups.  The trickiest part (usually) is identifying the patterns and figuring out what they have in common, which usually tells you what caused the failures.  Once you know what the failures are, it’s often trivial to actually fix them.  Often, but not always.  We have a bit of a mix here, as it turns out.  But first I had to analyze the failures and pick out the patterns.

Except I didn’t have to.  Because the Perl community is awesome, and someone thoughtfully did it for me.  Literally a day or two before I was about to start wading into the fail reports myself, Slaven Rezić (a.k.a. SREZIC on CPAN, a.k.a. eserte on GitHub) opened three GitHub issues wherein he’d already done all the work for me.  So I have to give a big shout out to Slaven: he did the hard part.  And, with the hard work done for me, all I had to do was buckle down and just fix the errors.

The first error was the one I already knew about: timing issues.  Now, timing issues are always a pain when you’re doing anything involving datetimes and resolutions down to the second.  Let’s say you have two values, and you expect them to match.  You’ve generated those two values, somehow, and now you’re testing to see if they’re exactly equal.  But you have a problem: there’s a chance—a small chance, but a chance nonetheless—that, in between generating the first value and the second value, the system clock rolled over to a new second.  Which means the seconds don’t match: they’re one second off from each other.  Which means your test fails.  Even though there’s nothing wrong.  Which is annoying.

There are various ways to work around this.  For anything other than an actual date module, I would suggest something along the lines of Test::Approx, or perhaps Test::Number::Delta.1  But, since this is an actual date module, I fear that allowing arbitrary datetime comparisons to be up to a second off might lead to missing valid failures.

Another way around this is exactly what I did in the one case where I realized I might have this problem:
# We'd like to test equivalence between the following 3 things.
# Unfortunately, we have no way to guarantee that the clock doesn't rollover to a new second in
# between assigning two of them.  So we're going to try up to, say, 10 times.  It's probably safe to
# say after that many attempts any continued discrepancy is not due to random chance.
my ($now_ctor, $now_func, $now_time);
my $success = 0;
for (1..10)
{
    $now_ctor = Date::Easy::Datetime->new;
    $now_func = now;
    $now_time = time;
    if ($now_ctor->epoch == $now_func->epoch && $now_func->epoch == $now_time)
    {
        $success = 1;
        last;
    }
}
is $success, 1, "now function matches default ctor matches return from time()"
    or diag("ctor: ", $now_ctor->epoch, " func: ", $now_func->epoch, " time: ", $now_time);

See what I’m doing there?  I want to make sure that now returns the same as time, and also that calling the Date::Easy::Datetime constructor with no arguments does the same thing.  But of course the “current” time is a fleeting thing.  I feel fairly confident that all three of those function calls can take place within a single second, no matter how slow your machine is.  But will they?  The clock can roll over to a new second at any point, including somewhere in the middle of those 3 calls.  If it does, 2 of the values get one second, and the other gets another (either the previous second or the following second).  So I figured, just keep trying until they match.  Or until we’ve tried so many times that it becomes obvious that they’re never going to match.  Why 10?  Honestly, it’s completely arbitrary.  Just felt like a good number.  In all likelihood, 2 is probably plenty.  But why take chances?

So, if I’d already accounted for the possibility of timing issues, where’s the problem?  Well, the problem, of course, is that I only accounted for the obvious one.  There’s always a few sneaky ones hiding out there.

In this case, it has to do with the large raft of test cases I imported from Time::ParseDate.2  Some of the strings that TPD can parse are “relative” times, such as “3 months, 7 days,” which means “3 months and 7 days from right now.” And of course “right now” means “right this second” and we’ve already seen that “this second” is subject to change.  Time will go marching on and all that.  So here I am, in a loop, comparing times and, every once in a great while, coming up short.  See, the awesome people at CPAN Testers run my test over and over again so much that, even though there’s only a very small chance that we hit this issue, it’s still going to generate a few failures here and there.  This is compounded by the fact that, in my original code to run through the TPD tests, my calls to generate the two times I’m comparing are not right next to each other as they are in the code I reference up above.  That only increases my chances of hitting this issue.

So I decided to expand my “try 10 times” solution into a test utility function.  I already have a function which helps me paper over some of the differences between objects and epoch seconds and all that.  Calls to that function might look like this, using the 3 example calls from the code up above:
$now_ctor = Date::Easy::Datetime->new;
$now_func = now;
$now_time = time;
compare_times($now_ctor, $now_func,
        "default ctor matches now function");
compare_times($now_ctor, local => $now_time,
        "default ctor matches return from time()");
(I wasn’t doing it this way originally because I needed to compare them up to 10 times, preferably without getting up to 9 test failures.) But suppose I could change that to look something like this instead:
generate_times_and_compare { Date::Easy::Datetime->new, now }
        "default ctor matches now function";
generate_times_and_compare { Date::Easy::Datetime->new, local => time }
        "default ctor matches return from time()";

So, there are a few differences here.  The first is that I had to give up using temporary variables.  I need to generate the values fresh on every try, so passing in pregenerated values via temp vars is not getting me anywhere.  But the big change is that, instead of passing a couple of values into my function, I’m passing a code block which generates a couple of values.  That way, my function can call this code block up to 10 times and hopefully come up with a match eventually.

How did I do this?  Well, there’s a link to the full code at the bottom of the post, but the simple answer is, I gutted compare_times and made it a wrapper around a new, private function which I called _render_times.  The private function just does the dirty work of forcing whatever you pass it into a common format (including figuring out what that format should be).  And the new compare_times just takes the two returned values and slaps them into an is.  On the other hand, generate_times_and_compare will call the code block and pass its return values to render_times over and over again until they match (or until we give up), and only then hand it over to an is check.

So far there’s not a huge amount of interesting here.  However, there are two aspects of this that are worth noting.

First of all, we want to make sure that, when a test fails, we don’t get the line number for the is inside generate_times_and_compare (or the one inside compare_times, for that matter).  This is similar to using croak instead of die: you want to report the error from the perspective of the caller, not the utility function itself.  How do we do this?  Well, it’s a simple enough incantation ... once you know it:
    local $Test::Builder::Level = $Test::Builder::Level + 1;
(and of course you have to use Test::Builder somewhere in your code).3  This is one of those things I had to figure out a few years back when I started writing test utility functions for my team (at $lastjob).  It wasn’t particularly obvious, so I thought it worth sharing here.


Secondly, you may be wondering how I got the nifty syntax where I essentially just replaced my parends with curly braces.4  This one is less obscure, but still worth sharing, I think: prototypes.  Now, prototypes are sort of like goto, in that they are very, very bad and you should never use them ... except when you should, at which point you sort of have to.  Prototypes are so much like goto, in fact, that many years ago Tom Christiansen wrote an article called Prototypes Considered Harmful as part of his “FMTEYEWTK” series,5 and every Perl programmer should be required by law to read that, in its entirety (yes, it’s long, but well worth the effort), before coding a single prototype in the Perl language.  Since there is no such law,6 I’ll just strongly advise you to read it, even before proceeding to the next paragraph.

Read it?  Okay, good.  So now you know one of the very few things that prototypes are actually good for (excellent for, even) is to do exactly what I’ve done up above: create a function, which takes a code block, and does so in a way which allows just curly braces (i.e. no sub required), thus creating a very uncluttered and pleasing syntax.  So all I did was this:
sub generate_times_and_compare (&$)
where the & represents my code block (remember, the code block has to come first7), and the $ represents my test name.  I’ve made the test name required, because I hate having unit tests with no names.  But, if you were adapting this to your own purposes and wanted to allow the test name to be optional, you could easily do that with:8
sub your_test_func (&;$)
Hopefully that’s a bit of knowledge that’ll come in handy someday.


Now moving on to our second problem.

This one was a little more difficult to figure out what to do.  Here’s a typical example of the failure:
#   Failed test 'successful parse: 28991231'
#   at t/date-parse.t line 39.
#          got: '1901-12-13'
#     expected: '2899-12-31'
 
#   Failed test 'successful parse: 10000101'
#   at t/date-parse.t line 39.
#          got: '1901-12-13'
#     expected: '1000-01-01'
# Looks like you failed 2 tests of 733.
The key, of course, was what Slaven noted right in the title of the GitHub issue he opened: this only happens on 32-bit systems.  Now, you may recall waaay back in part 2 that I talked just a bit about the range of dates that Date::Easy should be able to handle, and what happens to that range if you’re stuck with a 32-bit machine:
My preliminary poking around indicates I should be able to handle dates from 1-Jan-1000 to 31-Dec-2899, which is a pretty big range.
If you’re stuck with 32-bit ints, I’m pretty sure your range is going to be much smaller—should be 13-Dec-1901 to 18-Jan-2038 ...

Note that my tests are attempting to check the lower and upper bound of my expected date range, and note that they both turn into the lower bound for a 32-bit time_t.9  So it’s pretty obvious what’s going on here: my edge cases are only edge cases for a 64-bit machine ... they’re way over the edge for a 32-bit machine.  The question is, how do we fix it?

I did some Googling, and I couldn’t really see any way to verify the size of a time_t from within Perl.  But I did stumble upon this very neat way to check the size of your pointers from Tux over on PerlMonks.  So I put together this simple little function:
# Hopefully tells us whether we're running on a 32-bit machine or not.  No arguments, returns true
# or false.  Technically speaking, this checks the size of pointers, which I suppose might not be
# the same as the size of a time_t.  But hopefully this is close enough.
# Idea stolen from PerlMonks (thanks Tux (H.Merijn)!):
# http://www.perlmonks.org/?node_id=1054237
sub is_32bit
{
    require Config;
    return $Config::Config{ptrsize} == 4;
}

After that, it was simple enough to catch any test with a year prior to 1901 or after 2038 and just mark them as “todo” tests if we’re running on a 32-bit system.10  This is still not perfect (even above and beyond the problems I’ve already noted) because the Perl docs tell us that, after 5.12, Perl no longer cares what size the underlying time_t is on your system, so theoretically my “64-bit only” tests should work fine on a 32-bit machine running 5.12 or higher.  But this is a decent start on getting me a clean bill of health from CPAN Testers, which is my primary goal at the moment.

The final failure is trivial: even though I properly identified that I needed version 1.29 of List::Util for the pairs function, I didn’t realize that I needed version 1.39 if I want to treat the variables as objects, like so:
foreach (pairs @TIME_PARSE_DATE_TESTS)
{
    my ($str, $orig_t, @args) = ( $_->key, @{ $_->value } );
    :
    :
Since this is only a prerequisite for tests, I don’t mind just bumping up the required version and calling it a day.

The full code for Date::Easy so far is here, and there’s also a new developer’s version.  Of special note:

  • the refactor of the exisitng loop to use the new generate_times_and_compare function
  • my rewrite of the Time::ParseDate unit tests to avoid timing issues (yes, there’s another test inside the code block I’m passing to generate_times_and_compare)
  • the implementation of the generate_times_and_compare function itself
  • checking certain tests for being out-of-bounds on a 32-bit machine
  • another problem that 32-bit machines had was that I was using a test leap year of 2052, which is of course beyond what a 32-bit machine can handle; I simply changed the year
  • while trying to fix the 32-bit issue, I discovered some incorrect error messages, so I added a couple of new unit tests and then some corrected code

Next time, I’m going to finally get around to putting that POD together and, CPAN Testers willing, turn this into the next official release.



__________

1 Although the latter is primarily focussed on floating point equality tests, so perhaps that wouldn’t work here.


2 You may recall we discussed this at some length back in part 5.


3 Also note that this works in the current version of Perl testing infrastructure.  Chad Granum (EXODIST) is currently working on a rewrite called “Test2” and I’m not sure if this particular incantation is one of the things that will change in the new world order.  So just a heads-up there.


4 Or you may have known instantaneously.


5 That’s “Far More Than Everything You’ve Ever Wanted to Know,” for the uninitiated.


6 Yet.  One can dream.


7 Don’t ask why.  It just does.


8 This is the exact prototype used by lives_ok from Test::Exception, in fact.


9 I’m not entirely sure why they both turned into the lower bound.  Must be something to do with how timelocal and timegm handle out-of-bounds years.


10 What I really should be doing is replacing them with lower-and-upper-bound tests for 32-bit machines.  But I’m not sure what the exact bounds are.  Oh, sure: I can calculate them mathematically.  But that may not necessarily be correct.  What matters is not what datetime will fit in a 32-bit signed integer, but rather what are the smallest and largest numbers that timegm and timelocal will return.  In particular, note that 1/1/1000 is not the smallest number that will fit into a signed 64-bit int, and 12/31/2899 is not the largest.  Those are just the biggest and smallest years timegm and timelocal will take on a 64-bit machine, for whatever reason.  So what I really want to do is get access to a 32-bit machine (or maybe make a 32-bit virtual machine) and test it out for myself.  Which I hope to do at some point.






2 Comments

Why not mock time, localtime, etc? There are modules on CPAN for that.

Why TODO and not SKIP for 64bit only tests?

Leave a comment

About Buddy Burden

user-pic 9 years in California, 20 years in Perl, 29 years in computers, 50 years in bare feet.