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:
MonadFileSystemis used for filesystem I/O.MonadProcessis used for process execution, to call the sync command. Running a separate process just to call thesyncsystem call is quite undesirable, but theSystem.Posix.Unistdmodule of the unix package does not provide asyncfunction. I will likely change the implementation to useFFIin the future.MonadRandomis used for randomness.MonadStdiois used for writing toSTDOUT. Normally, it is used to display progress when theverboseoption is used. When thescriptoption 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 useMonadStdiofor 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
getRandomR = lift . Rand.getRandomR
getRandom = lift Rand.getRandom
getRandomRs = lift . Rand.getRandomRs
getRandoms = lift Rand.getRandomsFor 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.singletonAll 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]
getResult (Left err, _outLines) = Left err
getResult (Right (), outLines) = Right $ DList.toList outLinesThe 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.