{-# LANGUAGE OverloadedStrings, ImplicitParams #-} module EventUtils where import Control.Applicative import Control.Concurrent import Control.Concurrent.Async hiding (poll) import Control.Monad import Data.IORef import Data.List (sortBy) import Data.Monoid import Data.Ord (comparing) import Prelude hiding (FilePath) import System.Directory import System.FSNotify import System.FilePath import System.IO.Unsafe import Test.Tasty.HUnit import Text.Printf delay :: (?timeInterval :: Int) => IO () delay = threadDelay ?timeInterval -- event patterns data EventPattern = EventPattern { patFile :: FilePath , patName :: String , patPredicate :: Event -> Bool } evAdded, evRemoved, evModified, evAddedOrModified :: Bool -> FilePath -> EventPattern evAdded isDirectory path = EventPattern path "Added" (\x -> case x of Added path' _ isDir | isDirectory == isDir -> pathMatches isDirectory path path' _ -> False ) evRemoved isDirectory path = EventPattern path "Removed" (\x -> case x of Removed path' _ isDir | isDirectory == isDir -> pathMatches isDirectory path path' _ -> False ) evModified isDirectory path = EventPattern path "Modified" (\x -> case x of Modified path' _ isDir | isDirectory == isDir -> pathMatches isDirectory path path' _ -> False ) evAddedOrModified isDirectory path = EventPattern path "AddedOrModified" (\x -> case x of Added path' _ isDir | isDirectory == isDir -> pathMatches isDirectory path path' Modified path' _ isDir | isDirectory == isDir -> pathMatches isDirectory path path' _ -> False ) pathMatches True path path' = path == path' || (path <> [pathSeparator]) == path' pathMatches False path path' = path == path' matchEvents :: [EventPattern] -> [Event] -> Assertion matchEvents expected actual = do unless (length expected == length actual) $ assertFailure $ printf "Unexpected number of events.\n Expected: %s\n Actual: %s\n" (show expected) (show actual) sequence_ $ (\f -> zipWith f expected actual) $ \pat ev -> assertBool (printf "Unexpected event.\n Expected: %s\n Actual: %s\n" (show expected) (show actual)) (patPredicate pat ev) instance Show EventPattern where show p = printf "%s %s" (patName p) (show $ patFile p) gatherEvents :: (?timeInterval :: Int) => Bool -- use polling? -> (WatchManager -> FilePath -> ActionPredicate -> Action -> IO StopListening) -- (^ this is the type of watchDir/watchTree) -> FilePath -> IO (Async [Event]) gatherEvents poll watch path = do mgr <- startManagerConf defaultConfig { confDebounce = NoDebounce , confUsePolling = poll , confPollInterval = 2 * 10^(5 :: Int) } eventsVar <- newIORef [] stop <- watch mgr path (const True) (\ev -> atomicModifyIORef eventsVar (\evs -> (ev:evs, ()))) async $ do delay stop reverse <$> readIORef eventsVar expectEvents :: (?timeInterval :: Int) => Bool -> (WatchManager -> FilePath -> ActionPredicate -> Action -> IO StopListening) -> FilePath -> [EventPattern] -> IO () -> Assertion expectEvents poll w path pats action = do a <- gatherEvents poll w path action evs <- wait a matchEvents pats $ sortBy (comparing eventTime) evs testDirPath :: FilePath testDirPath = (unsafePerformIO getCurrentDirectory) "testdir" expectEventsHere :: (?timeInterval::Int) => Bool -> [EventPattern] -> IO () -> Assertion expectEventsHere poll = expectEvents poll watchDir testDirPath expectEventsHereRec :: (?timeInterval::Int) => Bool -> [EventPattern] -> IO () -> Assertion expectEventsHereRec poll = expectEvents poll watchTree testDirPath