1{-# OPTIONS_GHC -Wno-deprecations #-}
4-- Module : Streamly.Test.FileSystem.Event.Common
5-- Copyright : (c) 2020 Composewell Technologies
6-- License : BSD-3-Clause
7-- Maintainer : streamly@composewell.com
8-- Stability : experimental
11module Streamly.Test.FileSystem.Event.Common
30 , fileCreateWithParent
42import Debug.Trace (trace)
43import Control.Concurrent (MVar, newEmptyMVar, putMVar, takeMVar, threadDelay)
44import Control.Monad (when)
45import Control.Monad.IO.Class (MonadIO)
46import Data.Bifunctor (first)
47import Data.Function ((&))
48import Data.Functor.Identity (runIdentity)
49import Data.List.NonEmpty (NonEmpty)
50import Data.Word (Word8)
51import Streamly.Data.Array (Array)
52import System.Directory
54 , createDirectoryIfMissing
61import System.FilePath ((</>), takeDirectory)
63 ( BufferMode(..), hSetBuffering, stdout, IOMode (WriteMode), openFile
65import System.IO.Temp (withSystemTempDirectory)
67import qualified Data.List.NonEmpty as NonEmpty
68import qualified Streamly.Internal.Data.Array as Array
69import qualified Streamly.Internal.Data.Stream as Stream
70import qualified Streamly.Internal.Data.Stream.Concurrent as Stream
71import qualified Streamly.Unicode.Stream as Unicode
73#if defined(FILESYSTEM_EVENT_LINUX)
74import Streamly.Internal.FileSystem.Event.Linux (Event)
75import qualified Streamly.Internal.FileSystem.Event.Linux as Event
76#elif defined(FILESYSTEM_EVENT_DARWIN)
77import Streamly.Internal.FileSystem.Event.Darwin (Event)
78import qualified Streamly.Internal.FileSystem.Event.Darwin as Event
79#elif defined(FILESYSTEM_EVENT_WINDOWS)
80import Streamly.Internal.FileSystem.Event.Windows (Event)
81import qualified Streamly.Internal.FileSystem.Event.Windows as Event
83import Streamly.Internal.FileSystem.Event (Event)
84import qualified Streamly.Internal.FileSystem.Event as Event
89-------------------------------------------------------------------------------
90-- Check generated events
91-------------------------------------------------------------------------------
94 FilePath -- watch root
95 -> FilePath -- watch target, target dir if root is a symlink
96 -> MVar () -- mvar to sync file system ops and the watch
97 -> [(String, Event -> Bool)] -- expected events
99type EventWatcher = NonEmpty (Array Word8) -> Stream.Stream IO Event.Event
101eventMatches :: Event -> (String, Event -> Bool) -> Bool
102eventMatches ev (expectedPath, f) =
103 trace ("paths: " ++ show evPath ++ " " ++ show expectedPath)
104 (evPath == expectedPath && f ev)
108 utf8ToString :: Array Word8 -> String
110 runIdentity . Stream.toList . Unicode.decodeUtf8' . Array.read
112 evPath = utf8ToString (Event.getAbsPath ev)
114toUtf8 :: MonadIO m => String -> m (Array Word8)
115toUtf8 = Array.fromStream . Unicode.encodeUtf8' . Stream.fromList
117checkEvents :: EventWatcher -> EventChecker
118checkEvents watcher rootPath targetPath mvar matchList = do
119 putStrLn ("Watching on root [" <> rootPath
120 <> "] for [" <> targetPath <> "]")
122 let matchList1 = fmap (first (targetPath </>)) matchList
123 finder xs ev = filter (not . eventMatches ev) xs
125 paths <- mapM toUtf8 [rootPath]
126 watcher (NonEmpty.fromList paths)
127 & Stream.before (putMVar mvar ())
128 & Stream.trace (putStrLn . Event.showEvent)
129 & Stream.scanl' finder matchList1
130 & Stream.takeWhile (not . null)
133-------------------------------------------------------------------------------
135-------------------------------------------------------------------------------
138 ( String -- test description
139 , FilePath -> IO () -- pre test operation, arg is path of the watch root.
140 , FilePath -> IO () -- file system action, arg is path of the watch root.
141 , [(String, Event -> Bool)] -- expected events (Absolute Path, predicate)
144-- | Whether watch root is a symlink and if so then what's the behavior
146 DirType -- watch root is a directory
147 | FileType -- watch root is a file
148 | SymLinkResolvedPath -- symlink to a directory,
149 -- Event contains path via the resolved dir
150 | SymLinkOrigPath -- symlink to a directory,
151 -- Event contains path via the original symlink
154driver :: EventChecker -> WatchRootType -> TestDesc -> SpecWith ()
155driver checker symlinkStyle (desc, pre, ops, expected) =
156 it desc $ runOneTest `shouldReturn` ()
161 fseventDir = "fsevent_dir"
165 withSystemTempDirectory fseventDir $ \fp -> do
166 let root = fp </> "watch-root"
173 openFile root WriteMode >>= hClose
175 SymLinkResolvedPath -> do
176 let tgt = fp </> "watch-root-real"
178 createDirectoryLink tgt root
180 SymLinkOrigPath -> do
181 let tgt = fp </> "watch-root-real"
183 createDirectoryLink tgt root
186 -- XXX On macOS we seem to get the watch root create events
187 -- even though they occur before the watch is started. Even if
188 -- we add a delay here.
189 startWatchAndCheck root target sync
191 startWatchAndCheck root target sync = do
193 -- XXX On macOS the events from pre ops also seem to be bundled
194 -- with the events occurred after the watch is started.
195 let check = checker root target sync expected
196 fsOps = Stream.fromEffect $ runFSOps root sync
198 $ Stream.parListEagerFst [Stream.fromEffect check, fsOps]
200 runFSOps fp sync = do
201 -- We put the MVar before the event watcher starts to run but that does
202 -- not ensure that the event watcher has actually started. So we need a
203 -- delay as well. Do we?
204 takeMVar sync >> threadDelay 200000
207 error "Time out occurred before event watcher could terminate"
209-------------------------------------------------------------------------------
211-------------------------------------------------------------------------------
213dirEvent, fileEvent :: (Event -> Bool) -> Event -> Bool
215#if defined(FILESYSTEM_EVENT_DARWIN)
216dirEvent f ev = Event.isDir ev && f ev
217fileEvent f ev = Event.isFile ev && f ev
218#elif defined(FILESYSTEM_EVENT_LINUX)
219dirEvent f ev = Event.isDir ev && f ev
220fileEvent f ev = not (Event.isDir ev) && f ev
226-- XXX Tests for root as a regular file instead of dir?
227-- XXX Add symlink and hardlink tests
228-- XXX Add exception condition tests
230-------------------------------------------------------------------------------
232-------------------------------------------------------------------------------
234createParent :: FilePath -> FilePath -> IO ()
235createParent file parent = do
236 createDirectoryIfMissing True (parent </> takeDirectory file)
238createDirWithParent :: FilePath -> FilePath -> IO ()
239createDirWithParent dir parent =
240 when (not (null dir)) $ createDirectoryIfMissing True (parent </> dir)
242createDir :: FilePath -> FilePath -> IO ()
243createDir dir parent =
244 when (not (null dir)) $ createDirectory (parent </> dir)
246-- | Create the dir along with its parent dir during the test. This is
247-- especially useful to detect a race in Linux case where the watch for a
248-- directory is added in event processing, so if the the child directory is
249-- created before the watch was installed then we may miss some events.
250dirCreateWithParent ::
251 String -> (String -> [([Char], Event -> Bool)]) -> TestDesc
252dirCreateWithParent dir events =
253 ( "dir created (" ++ dir ++ ")"
255 , createDirWithParent dir
259dirCreate :: String -> (String -> [([Char], Event -> Bool)]) -> TestDesc
260dirCreate dir events =
261 ( "dir created (" ++ dir ++ ")"
267dirDelete :: String -> (String -> [([Char], Event -> Bool)]) -> TestDesc
268dirDelete dir events =
269 ( "dir deleted (" ++ dir ++ ")"
270 , createDirWithParent dir
271 , \fp -> removePathForcibly (fp </> dir)
278 -> (String -> String -> [(String, Event -> Bool)])
280dirMove dir1 dir2 events =
281 ( "dir moved (" ++ dir1 ++ " " ++ dir2 ++ ")"
282 , createDirWithParent dir1
283 , \fp -> renameDirectory (fp </> dir1) (fp </> dir2)
289 -> (String -> [(String, Event -> Bool)])
291rootDirMove suffix events =
292 ( "root dir moved" ++ "(" ++ suffix ++ ")"
294 , \fp -> renameDirectory fp (fp <> suffix)
298-------------------------------------------------------------------------------
300-------------------------------------------------------------------------------
302createFileWithParent :: FilePath -> FilePath -> IO ()
303createFileWithParent file parent = do
304 when (not (null file)) $
305 createDirectoryIfMissing True (parent </> takeDirectory file)
306 openFile (parent </> file) WriteMode >>= hClose
308createFile :: FilePath -> FilePath -> IO ()
309createFile file parent =
310 openFile (parent </> file) WriteMode >>= hClose
312fileCreate :: String -> (String -> [(String, Event -> Bool)]) -> TestDesc
313fileCreate file1 events =
314 ( "File created (" ++ file1 ++ ")"
317 , (file1, fileEvent Event.isCreated) : events file1
320-- | See comments in dirCreateWithParent
321fileCreateWithParent ::
322 String -> (String -> [(String, Event -> Bool)]) -> TestDesc
323fileCreateWithParent file1 events =
324 ( "File created (" ++ file1 ++ ")"
326 , createFileWithParent file1
327 , (file1, fileEvent Event.isCreated) : events file1
330fileDelete :: String -> (String -> [(String, Event -> Bool)]) -> TestDesc
331fileDelete file1 events =
332 ( "File deleted (" ++ file1 ++ ")"
333 , createFileWithParent file1
334 , \fp -> removeFile (fp </> file1)
338fileModify :: String -> (String -> [(String, Event -> Bool)]) -> TestDesc
339fileModify file1 events =
340 ( "File modified (" ++ file1 ++ ")"
341 , createFileWithParent file1
342 , \fp -> writeFile (fp </> file1) "Test Data"
343 , (file1, fileEvent Event.isModified) : events file1
349 -> (String -> String -> [(String, Event -> Bool)])
351fileMove file1 file2 events =
352 ( "File moved (" ++ file1 ++ " " ++ file2 ++ ")"
353 , createFileWithParent file1
354 , \fp -> renamePath (fp </> file1) (fp </> file2)
355 , (file1, fileEvent Event.isMoved)
356 : (file2, fileEvent Event.isMoved)
360rootFileMove :: String -> (String -> [(String, Event -> Bool)]) -> TestDesc
361rootFileMove suffix events =
362 ( "File moved (" ++ suffix ++ ")"
364 , \fp -> renamePath fp (fp <> suffix)
368-------------------------------------------------------------------------------
369-- Common test bundles
370-------------------------------------------------------------------------------
372withParent :: [Char] -> [Char] -> [Char]
373withParent parent file = if null parent then file else parent </> file
375testsWithParent :: String -> [TestDesc]
378 (withParent p "dir") (\dir -> [(dir, dirEvent Event.isCreated)])
380 (withParent p "dir") (\dir -> [(dir, dirEvent Event.isDeleted)])
382 (withParent p "dir1")
383 (withParent p "dir2")
385 [ (dir1, dirEvent Event.isMoved)
386 , (dir2, dirEvent Event.isMoved)
389 , fileCreate (withParent p "file1") (const [])
391 (withParent p "file1")
392 (\file -> [(file, fileEvent Event.isDeleted)])
393 , fileModify (withParent p "file1") (const [])
394 , fileMove (withParent p "file1") (withParent p "file2") (\_ _ -> [])
397commonTests :: [TestDesc]
398commonTests = testsWithParent ""
400commonRecTests :: [TestDesc]
401commonRecTests = testsWithParent "subdir"
403-------------------------------------------------------------------------------
405-------------------------------------------------------------------------------
414runTests modName watchType watcher rootType tests = do
415 hSetBuffering stdout NoBuffering
420 let checker = checkEvents watcher
421 describe ("Root type " ++ show rootType)
422 $ mapM_ (driver checker rootType) tests
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list