Random Reproducibility
My PhatSort project supports random ordering, so it is a good project for testing use of HMock with MonadRandom. It was quite straightforward to refactor the code to a tagless-final representation, but writing the tests did not go as well as I had hoped.
I abstracted the effects to the following four monads:
MonadFileSystem
is used for filesystem I/O.MonadProcess
is used for process execution, to call the sync command. Running a separate process just to call thesync
system call is quite undesirable, but theSystem.Posix.Unistd
module of the unix package does not provide async
function. I will likely change the implementation to useFFI
in the future.MonadRandom
is used for randomness.MonadStdio
is used for writing toSTDOUT
. Normally, it is used to display progress when theverbose
option is used. When thescript
option is used, the script it written toSTDOUT
. If the library were to support other types of user interfaces, it would be a good idea to handle these two cases separately. PhatSort is specifically a CLI utility, however, so I decided to just useMonadStdio
for simplicity.
When I wrote the initial tests, I decided to only mock
MonadFileSystem
and MonadProcess
. I cannot
mock MonadRandom
because of the reasons described in the HMock with
MonadRandom blog entry, and I accumulated the
MonadStdio
output in an attempt to make it less tightly
coupled with the filesystem I/O. While I am not a fan of stacking many
monad transformers in the actual program, I do not mind doing so in
tests.
For MonadRandom
, I simply tested with the RandT
monad transformer. Since I use MockT
as the outermost transformer, the following instance is required to lift
the MonadRandom
methods.
instance MonadRandom m => MonadRandom (MockT m) where
= lift . Rand.getRandomR
getRandomR = lift Rand.getRandom
getRandom = lift . Rand.getRandomRs
getRandomRs = lift Rand.getRandoms getRandoms
For MonadStdio
, I created an instance that accumulates
the output into a DList
String
using a WriterT
monad transformer.
instance Monad m => MonadStdio (MockT (WriterT (DList String) m)) where
putStrLn = lift . tell . DList.singleton
All tests are run in a concretely-specified monad using the
runTest
function. A test may terminate with an error
message or success (unit), and there may be output to
STDOUT
in either case, but my tests only check the error
messages in error cases.
runTest :: MockT (WriterT (DList String) (RandT StdGen IO)) (Either String ())
-> IO (Either String [String])
runTest= fmap getResult
. flip evalRandT (Rand.mkStdGen 42)
. runWriterT
. runMockT
where
getResult :: (Either String (), DList String) -> Either String [String]
Left err, _outLines) = Left err
getResult (Right (), outLines) = Right $ DList.toList outLines getResult (
The tests were pretty easy to write, and I really like the HMock design! The ExpectContext combinators are very useful.
Unfortunately, I ran into trouble when I started to test across many GHC versions. The StdGen pseudo-random number generator is not consistent across different versions of the random library. Searching the documentation, I found the following note about reproducibility:
If you have two builds of a particular piece of code against this library, any deterministic function call should give the same result in the two builds if the builds are
- compiled against the same major version of this library
- on the same architecture (32-bit or 64-bit)
I considered adding CPP
pragmas to specify test results
for different versions of the library, but I do not like that idea,
especially since results may also differ on different architectures.
Perhaps I could use a different pseudo-random number generator that
provides consistent results, but my searches did not come up with a
suitable one. (I have a different project in my queue that requires
reproducible randomness, so I will likely revisit this problem again
someday.)
For PhatSort, I
plan on refactoring the tests to just check that all items are processed
without checking the order when using random ordering. To do that
correctly, I will need to mock MonadStdio
after all. Since
there is no more need for deterministic randomness in this case, I will
remove RandT
and simply use the IO
instance of
MonadRandom
.