packages feed

achille-0.0.0: tests/FakeIO.hs

{-# LANGUAGE GADTs, ScopedTypeVariables #-}

module FakeIO where


import Data.Time.Clock  (UTCTime)
import Data.Map.Strict as M
import Data.Maybe
import Data.Bifunctor     (bimap)
import Control.Monad.Fail (MonadFail, fail)

import qualified System.Directory     as Directory
import qualified System.FilePath      as FilePath
import qualified System.FilePath.Glob as Glob
import qualified Data.ByteString      as BS
import qualified Data.ByteString.Lazy as LBS
import Control.Monad.State

import Test.Tasty.HUnit

import Achille.Recipe
import Achille.Internal
import Achille.Internal.IO as AchilleIO


data FakeIO a where
    ReadFile            :: FilePath -> FakeIO BS.ByteString
    ReadFileLazy        :: FilePath -> FakeIO LBS.ByteString
    CopyFile            :: FilePath -> FilePath       -> FakeIO ()
    WriteFile           :: FilePath -> BS.ByteString  -> FakeIO ()
    WriteFileLazy       :: FilePath -> LBS.ByteString -> FakeIO ()
    DoesFileExist       :: FilePath -> FakeIO Bool
    DoesDirExist        :: FilePath -> FakeIO Bool
    CallCommand         :: String   -> FakeIO ()
    Log                 :: String   -> FakeIO ()
    Glob                :: FilePath -> Glob.Pattern -> FakeIO [FilePath]
    GetModificationTime :: FilePath -> FakeIO UTCTime
    Fail                :: String -> FakeIO a

    SeqAp               :: FakeIO (a -> b) -> FakeIO a -> FakeIO b
    Fmap                :: (a -> b) -> FakeIO a -> FakeIO b
    Pure                :: a -> FakeIO a
    Bind                :: FakeIO a -> (a -> FakeIO b) -> FakeIO b


instance Functor FakeIO where
    fmap = Fmap

instance Applicative FakeIO where
    pure  = Pure
    (<*>) = SeqAp

instance Monad FakeIO where
    (>>=) = Bind

instance MonadFail FakeIO where
    fail = Fail


instance AchilleIO FakeIO where
    readFile            = ReadFile
    readFileLazy        = ReadFileLazy
    copyFile            = CopyFile
    writeFile           = WriteFile
    writeFileLazy       = WriteFileLazy
    doesFileExist       = DoesFileExist
    doesDirExist        = DoesDirExist
    callCommand         = CallCommand
    log                 = Log
    glob                = Glob
    getModificationTime = GetModificationTime


data FakeIOActions
    = WrittenFile          FilePath BS.ByteString
    | WrittenFileLazy      FilePath LBS.ByteString
    | HasReadFile          FilePath
    | HasReadFileLazy      FilePath
    | CopiedFile FilePath  FilePath
    | CalledCommand        String
    | Failed               String
    deriving (Eq, Show)


type FileSystem = Map FilePath (UTCTime, BS.ByteString)

defMTime :: UTCTime
defMTime = undefined

getMTime :: FilePath -> FileSystem -> UTCTime
getMTime p fs = fromMaybe defMTime (fst <$> M.lookup p fs)

getBS :: FilePath -> FileSystem -> BS.ByteString
getBS p fs = fromMaybe BS.empty (snd <$> M.lookup p fs)

retrieveFakeIOActions :: FakeIO (a, Cache)    -- the fake IO computation
                      -> FileSystem           -- the underlying input FS
                      -> (Maybe a, [FakeIOActions])
retrieveFakeIOActions t fs = bimap (fmap fst) reverse $ runState (retrieve t) []
    where
        retrieve :: FakeIO a -> State [FakeIOActions] (Maybe a)
        retrieve (ReadFile key) = do
            modify (HasReadFile key :)
            pure (Just $ getBS key fs)

        retrieve (ReadFileLazy key) = do
            modify (HasReadFileLazy key :)
            pure (Just $ LBS.fromStrict $ getBS key fs)

        retrieve (CopyFile from to)  = Just <$> modify (CopiedFile from to :)
        retrieve (WriteFile p c)     = Just <$> modify (WrittenFile p c :)
        retrieve (WriteFileLazy p c) = Just <$> modify (WrittenFileLazy p c :)
        retrieve (DoesFileExist p)   = undefined

        retrieve (CallCommand cmd)   = Just <$> modify (CalledCommand cmd :)
        retrieve (Log str)           = pure $ Just ()
        retrieve (Glob dir p)        = undefined
        retrieve (GetModificationTime p) = pure $ Just (getMTime p fs)

        retrieve (SeqAp f x) = do
            actions <- get
            let (Just f', actions')  = runState (retrieve f) actions
            let (Just x', actions'') = runState (retrieve x) actions'
            put actions''
            pure $ Just (f' x')

        retrieve (Fmap f x)  = do
            actions <- get
            let (Just x', actions') = runState (retrieve x) actions
            put actions'
            pure $ Just (f x')

        retrieve (Pure x)   = pure (Just x)
        retrieve (Fail str) = modify (Failed str :) >> pure Nothing

        retrieve (Bind (Fail str) f) = modify (Failed str :) >> pure Nothing

        retrieve (Bind x f) = do
            actions <- get
            let (Just x', actions') = runState (retrieve x) actions
            put actions'
            retrieve (f x')


exactRun :: (Show b, Eq b)
    => FileSystem
    -> Context a
    -> Recipe FakeIO a b
    -> (Maybe b, [FakeIOActions])
    -> Assertion
exactRun fs ctx r expected =
    let fakeIO = runRecipe r ctx
        trace  = retrieveFakeIOActions fakeIO fs
    in trace @?= expected