QuickCheck is an awesome
library, so I decided to play with it a bit and also refresh my memory of Haskell.
Intro
To, warm up, let’s remind ourselves how insane
floating point arithmetic is. Let’s test if addition of floating point numbers
is commutative (x + y = y + x).
Ok, so far so good. What about associativity ((x + y) + z == x + (y + z))?
Nice! Let’s try a few more.
This is one more reminder not to use double and float types to represent money. If you have X dollars and then someone gives you Y dollars and then changes their
mind and takes these Y dollars back, you can end up with 0 dollars in your pocket,
even though X was greater than zero. Before taking money from someone, make sure
they don’t use doubles for money.
Testing Date and Time
Not only floating-pointer numbers are insane. Date and time are extremely weird
as well. All these time zones, daylight saving time, leap years, leap seconds. Pure madness. Many of our
assumptions about time are wrong.
In order to make my life more complicated, I decided to check whether
ActiveSupport::Duration
from Rails is associative, but I want to use Haskell’s QuickCheck to do that.
Here are a few instances of the ActiveSupport::Duration class:
In which case the time to wait is longer: when you first wait D1 and then D2,
or when you first wait D2 and then D1? Common sense suggests that it’s the
same, but let’s check it.
Let’s get started.
There are a few predefined tests in test/Spec.hs, let’s remove them all. Now,
testing pure properties is easy. This time, however, we need to communicate with
Ruby, thus we need quickcheck with IO. After 15 seconds of searching, we can find
this.
Looks like this is exactly what we need. Let’s adapt it to our needs.
Simpru, right?
Let’s run it:
And check it in Ruby:
Wow, 3 days difference! Not bad. This is because adding the actual duration of
adding months depends on the timestamp we are adding these months to.
Now, what if we remove months out of the equation? Let’s comment out pure Months
from Arbitrary instance definition for Duration and rerun the test.
Let’s have a look.
1 day difference. This time, it’s a leap year problem: if we travel from 23 December 2016
to 49 weeks in past, we will get to 15 January (because 2016 is a leap year and 29 February
is a thing). Then 17 years to the future - it’s 15 Jan 2033.
On the other hand, if we first go to the future (23 December 2033), and then 49 weeks
back, then we’ll get to 14 January (because the 2033 year is not leap and Feb 29 doesn’t exit).
So the problem now is very similar to the previous problem we had with month durations.
year durations depend on the year of the date they are applied to. Let’s comment out
pure Years and rerun tests.
Hmm, looks promising. But maybe I overlooked something? Let’s increase the number
of tests and run it again.
What is this?
1 hour difference. DST? Most likely. If you run stack test a few times, you’ll
see that x and y will be around mid March or mid November. This confirms our
suspicion. Well, the problem is the same: 1.day doesn’t actually have
a constant length. It can be 23 or 25 hours long, if we jump over DST point.
Let’s exclude days and weeks and rerun tests.
I ran stack test with the new settings multiple times and didn’t get any new
failures. It doesn’t mean that the commutative property holds for second/minute/hour
durations. It just means that QuickCheck didn’t find any counterexamples.
Conclusion
Current implementation of the test is very inefficient: it starts a process for
each single check. My initial implementation was more ugly and a lot faster. Its
source code is there.
I checked a few more properties there. In that implementation, the Ruby process
is started once and it communicates with Haskell via pipe.