Mocking FileStatus
The unix package provides an API for standard POSIX operating system services, and the unix-compat package provides an API that also works with non-POSIX operating systems. I often use the file status API to get detailed information about files and directories that is not available in the directory API, and this blog entry is a simple tutorial about how to mock this functionality.
The file status API uses an opaque FileStatus data type. In the unix package, this data type wraps a low-level pointer to a (C) stat structure. This allows the API to work without having to translate to a Haskell representation, which would hurt performance. The unix-compat package re-exports the unix modules when on a POSIX platform.
When mocking, one cannot (easily) create a mocked FileStatus
value. One way to get around this limitation is to use an internal data
type in the application API and interface with FileStatus
as part of the IO
implementation. The internal data type
only needs to implement the features that are required by the
application. For example, consider the following internal data type that
provides support for (only) deviceID
and isDirectory.
import System.Posix.Types (DeviceID)
import qualified System.PosixCompat.Files as Files
data FileStatus
= FileStatus
deviceID :: !DeviceID
{ isDirectory :: !Bool
,
}
toFileStatus :: Files.FileStatus -> FileStatus
= FileStatus
toFileStatus status = Files.deviceID status
{ deviceID = Files.isDirectory status
, isDirectory }
The toFileStatus
helper function is used to translate
from a FileStatus
value to a value of the internal data type. It can be used as in the
following (subset of a) MonadFileSystem
type class, which
uses the DefaultSignatures
extension to provide a default implementation for MonadIO
monads.
class Monad m => MonadFileSystem m where
getFileStatus :: FilePath -> m (Either IOError FileStatus)
default getFileStatus :: MonadIO m
=> FilePath -> m (Either IOError FileStatus)
= liftIO . getFileStatus
getFileStatus {-# INLINE getFileStatus #-}
The implementation for IO
can be defined as follows:
instance MonadFileSystem IO where
= tryIOError . fmap toFileStatus . Files.getFileStatus
getFileStatus {-# INLINE getFileStatus #-}
Since the getFileStatus
function of
MonadFileSystem
uses the internal data type instead of FileStatus
directly, it can be easily mocked. For example, a test using HMock may contain a
line like the following:
$ GetFileStatus "foo.txt" |-> FS.FileStatus 42 False expect