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.IsStream as Stream
70import qualified Streamly.Unicode.Stream as Unicode
72#if defined(FILESYSTEM_EVENT_LINUX)
73import Streamly.Internal.FileSystem.Event.Linux (Event)
74import qualified Streamly.Internal.FileSystem.Event.Linux as Event
75#elif defined(FILESYSTEM_EVENT_DARWIN)
76import Streamly.Internal.FileSystem.Event.Darwin (Event)
77import qualified Streamly.Internal.FileSystem.Event.Darwin as Event
78#elif defined(FILESYSTEM_EVENT_WINDOWS)
79import Streamly.Internal.FileSystem.Event.Windows (Event)
80import qualified Streamly.Internal.FileSystem.Event.Windows as Event
82import Streamly.Internal.FileSystem.Event (Event)
83import qualified Streamly.Internal.FileSystem.Event as Event
88-------------------------------------------------------------------------------
89-- Check generated events
90-------------------------------------------------------------------------------
93 FilePath -- watch root
94 -> FilePath -- watch target, target dir if root is a symlink
95 -> MVar () -- mvar to sync file system ops and the watch
96 -> [(String, Event -> Bool)] -- expected events
98type EventWatcher = NonEmpty (Array Word8) -> Stream.SerialT IO Event.Event
100eventMatches :: Event -> (String, Event -> Bool) -> Bool
101eventMatches ev (expectedPath, f) =
102 trace ("paths: " ++ show evPath ++ " " ++ show expectedPath)
103 (evPath == expectedPath && f ev)
107 utf8ToString :: Array Word8 -> String
109 runIdentity . Stream.toList . Unicode.decodeUtf8' . Array.read
111 evPath = utf8ToString (Event.getAbsPath ev)
113toUtf8 :: MonadIO m => String -> m (Array Word8)
114toUtf8 = Array.fromStream . Unicode.encodeUtf8' . Stream.fromList
116checkEvents :: EventWatcher -> EventChecker
117checkEvents watcher rootPath targetPath mvar matchList = do
118 putStrLn ("Watching on root [" <> rootPath
119 <> "] for [" <> targetPath <> "]")
121 let matchList1 = fmap (first (targetPath </>)) matchList
122 finder xs ev = filter (not . eventMatches ev) xs
124 paths <- mapM toUtf8 [rootPath]
125 watcher (NonEmpty.fromList paths)
126 & Stream.before (putMVar mvar ())
127 & Stream.trace (putStrLn . Event.showEvent)
128 & Stream.scanl' finder matchList1
129 & Stream.takeWhile (not . null)
132-------------------------------------------------------------------------------
134-------------------------------------------------------------------------------
137 ( String -- test description
138 , FilePath -> IO () -- pre test operation, arg is path of the watch root.
139 , FilePath -> IO () -- file system action, arg is path of the watch root.
140 , [(String, Event -> Bool)] -- expected events (Absolute Path, predicate)
143-- | Whether watch root is a symlink and if so then what's the behavior
145 DirType -- watch root is a directory
146 | FileType -- watch root is a file
147 | SymLinkResolvedPath -- symlink to a directory,
148 -- Event contains path via the resolved dir
149 | SymLinkOrigPath -- symlink to a directory,
150 -- Event contains path via the original symlink
153driver :: EventChecker -> WatchRootType -> TestDesc -> SpecWith ()
154driver checker symlinkStyle (desc, pre, ops, expected) =
155 it desc $ runOneTest `shouldReturn` ()
160 fseventDir = "fsevent_dir"
164 withSystemTempDirectory fseventDir $ \fp -> do
165 let root = fp </> "watch-root"
172 openFile root WriteMode >>= hClose
174 SymLinkResolvedPath -> do
175 let tgt = fp </> "watch-root-real"
177 createDirectoryLink tgt root
179 SymLinkOrigPath -> do
180 let tgt = fp </> "watch-root-real"
182 createDirectoryLink tgt root
185 -- XXX On macOS we seem to get the watch root create events
186 -- even though they occur before the watch is started. Even if
187 -- we add a delay here.
188 startWatchAndCheck root target sync
190 startWatchAndCheck root target sync = do
192 -- XXX On macOS the events from pre ops also seem to be bundled
193 -- with the events occurred after the watch is started.
194 let check = checker root target sync expected
195 fsOps = Stream.fromEffect $ runFSOps root sync
196 Stream.drain $ Stream.fromEffect check `Stream.parallelFst` fsOps
198 runFSOps fp sync = do
199 -- We put the MVar before the event watcher starts to run but that does
200 -- not ensure that the event watcher has actually started. So we need a
201 -- delay as well. Do we?
202 takeMVar sync >> threadDelay 200000
205 error "Time out occurred before event watcher could terminate"
207-------------------------------------------------------------------------------
209-------------------------------------------------------------------------------
211dirEvent, fileEvent :: (Event -> Bool) -> Event -> Bool
213#if defined(FILESYSTEM_EVENT_DARWIN)
214dirEvent f ev = Event.isDir ev && f ev
215fileEvent f ev = Event.isFile ev && f ev
216#elif defined(FILESYSTEM_EVENT_LINUX)
217dirEvent f ev = Event.isDir ev && f ev
218fileEvent f ev = not (Event.isDir ev) && f ev
224-- XXX Tests for root as a regular file instead of dir?
225-- XXX Add symlink and hardlink tests
226-- XXX Add exception condition tests
228-------------------------------------------------------------------------------
230-------------------------------------------------------------------------------
232createParent :: FilePath -> FilePath -> IO ()
233createParent file parent = do
234 createDirectoryIfMissing True (parent </> takeDirectory file)
236createDirWithParent :: FilePath -> FilePath -> IO ()
237createDirWithParent dir parent =
238 when (not (null dir)) $ createDirectoryIfMissing True (parent </> dir)
240createDir :: FilePath -> FilePath -> IO ()
241createDir dir parent =
242 when (not (null dir)) $ createDirectory (parent </> dir)
244-- | Create the dir along with its parent dir during the test. This is
245-- especially useful to detect a race in Linux case where the watch for a
246-- directory is added in event processing, so if the the child directory is
247-- created before the watch was installed then we may miss some events.
248dirCreateWithParent ::
249 String -> (String -> [([Char], Event -> Bool)]) -> TestDesc
250dirCreateWithParent dir events =
251 ( "dir created (" ++ dir ++ ")"
253 , createDirWithParent dir
257dirCreate :: String -> (String -> [([Char], Event -> Bool)]) -> TestDesc
258dirCreate dir events =
259 ( "dir created (" ++ dir ++ ")"
265dirDelete :: String -> (String -> [([Char], Event -> Bool)]) -> TestDesc
266dirDelete dir events =
267 ( "dir deleted (" ++ dir ++ ")"
268 , createDirWithParent dir
269 , \fp -> removePathForcibly (fp </> dir)
276 -> (String -> String -> [(String, Event -> Bool)])
278dirMove dir1 dir2 events =
279 ( "dir moved (" ++ dir1 ++ " " ++ dir2 ++ ")"
280 , createDirWithParent dir1
281 , \fp -> renameDirectory (fp </> dir1) (fp </> dir2)
287 -> (String -> [(String, Event -> Bool)])
289rootDirMove suffix events =
290 ( "root dir moved" ++ "(" ++ suffix ++ ")"
292 , \fp -> renameDirectory fp (fp <> suffix)
296-------------------------------------------------------------------------------
298-------------------------------------------------------------------------------
300createFileWithParent :: FilePath -> FilePath -> IO ()
301createFileWithParent file parent = do
302 when (not (null file)) $
303 createDirectoryIfMissing True (parent </> takeDirectory file)
304 openFile (parent </> file) WriteMode >>= hClose
306createFile :: FilePath -> FilePath -> IO ()
307createFile file parent =
308 openFile (parent </> file) WriteMode >>= hClose
310fileCreate :: String -> (String -> [(String, Event -> Bool)]) -> TestDesc
311fileCreate file1 events =
312 ( "File created (" ++ file1 ++ ")"
315 , (file1, fileEvent Event.isCreated) : events file1
318-- | See comments in dirCreateWithParent
319fileCreateWithParent ::
320 String -> (String -> [(String, Event -> Bool)]) -> TestDesc
321fileCreateWithParent file1 events =
322 ( "File created (" ++ file1 ++ ")"
324 , createFileWithParent file1
325 , (file1, fileEvent Event.isCreated) : events file1
328fileDelete :: String -> (String -> [(String, Event -> Bool)]) -> TestDesc
329fileDelete file1 events =
330 ( "File deleted (" ++ file1 ++ ")"
331 , createFileWithParent file1
332 , \fp -> removeFile (fp </> file1)
336fileModify :: String -> (String -> [(String, Event -> Bool)]) -> TestDesc
337fileModify file1 events =
338 ( "File modified (" ++ file1 ++ ")"
339 , createFileWithParent file1
340 , \fp -> writeFile (fp </> file1) "Test Data"
341 , (file1, fileEvent Event.isModified) : events file1
347 -> (String -> String -> [(String, Event -> Bool)])
349fileMove file1 file2 events =
350 ( "File moved (" ++ file1 ++ " " ++ file2 ++ ")"
351 , createFileWithParent file1
352 , \fp -> renamePath (fp </> file1) (fp </> file2)
353 , (file1, fileEvent Event.isMoved)
354 : (file2, fileEvent Event.isMoved)
358rootFileMove :: String -> (String -> [(String, Event -> Bool)]) -> TestDesc
359rootFileMove suffix events =
360 ( "File moved (" ++ suffix ++ ")"
362 , \fp -> renamePath fp (fp <> suffix)
366-------------------------------------------------------------------------------
367-- Common test bundles
368-------------------------------------------------------------------------------
370withParent :: [Char] -> [Char] -> [Char]
371withParent parent file = if null parent then file else parent </> file
373testsWithParent :: String -> [TestDesc]
376 (withParent p "dir") (\dir -> [(dir, dirEvent Event.isCreated)])
378 (withParent p "dir") (\dir -> [(dir, dirEvent Event.isDeleted)])
380 (withParent p "dir1")
381 (withParent p "dir2")
383 [ (dir1, dirEvent Event.isMoved)
384 , (dir2, dirEvent Event.isMoved)
387 , fileCreate (withParent p "file1") (const [])
389 (withParent p "file1")
390 (\file -> [(file, fileEvent Event.isDeleted)])
391 , fileModify (withParent p "file1") (const [])
392 , fileMove (withParent p "file1") (withParent p "file2") (\_ _ -> [])
395commonTests :: [TestDesc]
396commonTests = testsWithParent ""
398commonRecTests :: [TestDesc]
399commonRecTests = testsWithParent "subdir"
401-------------------------------------------------------------------------------
403-------------------------------------------------------------------------------
412runTests modName watchType watcher rootType tests = do
413 hSetBuffering stdout NoBuffering
418 let checker = checkEvents watcher
419 describe ("Root type " ++ show rootType)
420 $ 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