Skip to main content

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
toFileStatus status = FileStatus
    { deviceID    = Files.deviceID status
    , isDirectory = Files.isDirectory status
    }

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)
  getFileStatus = liftIO . getFileStatus
  {-# INLINE getFileStatus #-}

The implementation for IO can be defined as follows:

instance MonadFileSystem IO where
  getFileStatus = tryIOError . fmap toFileStatus . Files.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:

expect $ GetFileStatus "foo.txt" |-> FS.FileStatus 42 False