on master help sign in
Simplify the implementation

* Do not use a seperate traversal of dirs, instead just call addToWatch recursively. * Keep rel path of subdir in the map instead of keeping absolute path * Keep the same default for recursive watch on Windows/Linux * Use watchTreesWith to implement watchPathsWith using recursive flag Refactor, add some todos for test code

commitb85002bddb8ada911ae203f29ec5091114ed7f94 authorHarendra Kumar <harendra@composewell.com> dateSun 15 Nov 2020, 13:04 (6 years ago) parentf00008b
4 files changed+234−203
src/Streamly/Internal/FileSystem/Event/Linux.hs+153−169 src/Streamly/Internal/FileSystem/Event/Windows.hs+1−5 streamly.cabal+4−10 test/Streamly/Test/FileSystem/Event.hs+76−19
src/Streamly/Internal/FileSystem/Event/Linux.hs+153−169
@@ -99,6 +99,8 @@ module Streamly.Internal.FileSystem.Event.Linux
9999 , setAllEvents
100100
101101 -- ** Watch APIs
102+ -- XXX watchPaths is redundant now because we can use watchTrees with
103+ -- setRecursiveMode False. Perhaps we can use a common "watch" API.
102104 , watchPathsWith
103105 , watchPaths
104106 , watchTreesWith
@@ -107,7 +109,7 @@ module Streamly.Internal.FileSystem.Event.Linux
107109 , removeFromWatch
108110
109111 -- * Handling Events
110− , Event
112+ , Event(..)
111113 , getRoot
112114 , getRelPath
113115 , getCookie
@@ -142,19 +144,17 @@ module Streamly.Internal.FileSystem.Event.Linux
142144
143145 -- * Debugging
144146 , showEvent
145− , showEventShort
146147 )
147148where
148149
149150import Control.Monad (void, when)
150151import Control.Monad.IO.Class (MonadIO, liftIO)
151−import Data.Bifunctor (bimap)
152152import Data.Bits ((.|.), (.&.), complement)
153+import Data.Char (ord)
153154import Data.Foldable (foldlM)
154155import Data.Functor.Identity (runIdentity)
155156import Data.IntMap.Lazy (IntMap)
156−import Data.IORef (IORef, newIORef, readIORef, writeIORef)
157−import Data.Function ((&))
157+import Data.IORef (IORef, newIORef, readIORef, writeIORef, modifyIORef)
158158import Data.List.NonEmpty (NonEmpty)
159159import Data.Word (Word8, Word32)
160160import Foreign.C.Error (throwErrnoIfMinus1)
@@ -168,7 +168,6 @@ import GHC.IO.Handle.FD (mkHandleFromFD)
168168import Streamly.Prelude (SerialT)
169169import Streamly.Internal.Data.Parser (Parser)
170170import Streamly.Internal.Data.Array.Storable.Foreign.Types (Array(..))
171−import System.FilePath ((</>))
172171import System.IO (Handle, hClose, IOMode(ReadMode))
173172#if !MIN_VERSION_base(4,10,0)
174173import Control.Concurrent.MVar (readMVar)
@@ -201,7 +200,7 @@ import qualified Streamly.Internal.Unicode.Stream as U
201200--
202201data Config = Config
203202 { watchRec :: Bool
204− , createFlags :: Word32
203+ , createFlags :: Word32
205204 }
206205
207206-------------------------------------------------------------------------------
@@ -229,56 +228,19 @@ setFlag mask status cfg@Config{..} =
229228-------------------------------------------------------------------------------
230229-- Utilities
231230-------------------------------------------------------------------------------
232−splitPath :: (MonadIO m) => FilePath -> FilePath -> m [FilePath]
233−splitPath pat xs = S.toList
234− $ S.splitOnSeq (A.fromList pat) (FL.toList) (S.fromList xs)
235−
236−pathDiff :: (MonadIO m) => FilePath -> FilePath -> m FilePath
237−pathDiff pat xs = do
238− splits <- splitPath pat xs
239− let j = case length splits of
240− 0 -> ""
241− _ -> last splits
242− t = case length j of
243− 0 -> ""
244− _ -> tail j
245− return t
246−
247−dirListing :: String -> SerialT IO (Either String String)
248−dirListing s = do
249− S.filter dirFilter
250− $ S.iterateMapLeftsWith S.ahead listDir (S.yield $ (Left s))
251231
252− where
253−
254− dirFilter x =
255− case x of
256− Left _ -> True
257− Right _ -> False
258−
259− listDir dir =
260− Dir.toEither dir -- SerialT IO (Either String String)
261− & S.map (bimap prefix prefix) -- SerialT IO (Either String String)
262−
263− where
264−
265− prefix x = dir ++ "/" ++ x
266−
267−toPathList :: String -> IO [String]
268−toPathList root = do
269− S.toList $ S.map toStr $ dirListing root
270−
271− where
272−
273− toStr x =
274− case x of
275− Left d -> d
276− Right d -> d
232+-- XXX we really do not know the path encoding, all we know is that it is "/"
233+-- separated bytes. So these may fail or convert the path in an unexpected
234+-- manner. We should ultimately remove all usage of these.
277235
278236toUtf8 :: MonadIO m => String -> m (Array Word8)
279237toUtf8 = A.fromStream . U.encodeUtf8 . S.fromList
280238
281−-- | Set watch event on directory recursively.
239+utf8ToString :: Array Word8 -> String
240+utf8ToString = runIdentity . S.toList . U.decodeUtf8' . A.toStream
241+
242+-- | Watch the whole directory tree recursively instead of watching just one
243+-- level of directory.
282244--
283245-- /default: Off/
284246--
@@ -571,14 +533,23 @@ defaultConfig :: Config
571533defaultConfig =
572534 setWhenExists AddIfExists
573535 $ setAllEvents On
574− $ Config {watchRec = False, createFlags = 0}
536+ $ Config {watchRec = True, createFlags = 0}
575537
576538-------------------------------------------------------------------------------
577539-- Open an event stream
578540-------------------------------------------------------------------------------
579541
580542-- | A handle for a watch.
581−data Watch = Watch Handle (IORef (IntMap (Array Word8, Array Word8)))
543+data Watch =
544+ Watch
545+ Handle -- File handle for the watch
546+ (IORef
547+ (IntMap -- Key is the watch descriptor
548+ ( Array Word8 -- Absolute path of the watch root
549+ , Array Word8 -- Path of subdir relative to watch root
550+ )
551+ )
552+ )
582553
583554-- Instead of using the watch descriptor we can provide APIs that use the path
584555-- itself to identify the watch. That will require us to maintain a map from wd
@@ -615,15 +586,13 @@ createWatch = do
615586 ReadMode
616587 True -- use non-blocking IO
617588 Nothing -- TextEncoding (binary)
618− emptyMapRef <- newIORef Map.empty
589+ emptyMapRef <- newIORef Map.empty
619590 return $ Watch h emptyMapRef
620591
621592foreign import ccall unsafe
622593 "sys/inotify.h inotify_add_watch" c_inotify_add_watch
623594 :: CInt -> CString -> CUInt -> IO CInt
624595
625−utf8ToString :: Array Word8 -> String
626−utf8ToString = runIdentity . S.toList . U.decodeUtf8' . A.toStream
627596
628597#if !MIN_VERSION_base(4,10,0)
629598-- | Turn an existing Handle into a file descriptor. This function throws an
@@ -643,72 +612,105 @@ handleToFd h = case h of
643612 InappropriateType "handleToFd" msg Nothing Nothing
644613#endif
645614
646−-- | Given a 'Config' and a @path@ supplied as a UTF-8 encoded byte array, add
647−-- the path to the list of paths being monitored by the given watch 'Handle'.
648−-- Returns a "watch descriptor" which is used as an idenitfier in the events
649−-- generated for this particular path and also to remove the path from this
650−-- watch.
615+-- | Add a trailing "/" at the end of the path if there is none. Do not add a
616+-- "/" if the path is empty.
617+--
618+ensureTrailingSlash :: Array Word8 -> Array Word8
619+ensureTrailingSlash path =
620+ if A.length path /= 0
621+ then
622+ let mx = A.readIndex path (A.length path - 1)
623+ in case mx of
624+ Nothing -> error "ensureTrailingSlash: Bug: Invalid index"
625+ Just x ->
626+ if x /= fromIntegral (ord '/')
627+ then path <> A.fromCString# "/"#
628+ else path
629+ else path
630+
631+-- | @addToWatch cfg watch root subpath@ adds @subpath@ to the list of paths
632+-- being monitored under @root@ via the watch handle @watch@. @root@ must be
633+-- an absolute path and @subpath@ must be relative to @root@.
651634--
652635-- /Internal/
653636--
654−addToWatchExecutor ::
655− Config
656− -> Watch
657− -> Array Word8
658− -> Array Word8
659− -> IO (Config, Watch)
660−addToWatchExecutor cfg@Config{..} (Watch handle wdMap) pathArr root = do
637+addToWatch :: Config -> Watch -> Array Word8 -> Array Word8 -> IO ()
638+addToWatch cfg@Config{..} watch@(Watch handle wdMap) root0 path0 = do
639+ -- XXX do not add if the path is already added
640+ -- XXX if the watch is added by the scan and not via an event we can
641+ -- generate a create event assuming that the create may have been lost. We
642+ -- can also mark in the map that this entry was added by the scan. So if an
643+ -- actual create event later comes and tries to add this again then we can
644+ -- ignore that and drop the create event to avoid duplicate create, because
645+ -- we have already emitted it.
646+ --
647+ -- When a directory is added by the scan we should also emit create events
648+ -- for files that may have got added to the dir. However, such create
649+ -- events may get duplicated because of a race between the scan generated
650+ -- versus real events.
651+ --
652+ -- Or we may distinguish between scan generated events and real events so
653+ -- that the application can assume that other events may been lost and
654+ -- handle it. For example, if it is a dir create the application can read
655+ -- the dir to scan the files in it.
656+ --
657+ let root = ensureTrailingSlash root0
658+ path = ensureTrailingSlash path0
659+ absPath = root <> path
661660 fd <- handleToFd handle
662− wd <- A.asCString pathArr $ \pathPtr ->
663− throwErrnoIfMinus1 ("addToWatch " ++ utf8ToString pathArr) $
664− c_inotify_add_watch (fdFD fd) pathPtr (CUInt createFlags)
665− km <- readIORef wdMap
666− let k = (Map.insert (fromIntegral wd) (root, pathArr) km)
667− writeIORef wdMap k
668− return $ (cfg, Watch handle wdMap)
669661
670−addToWatch :: Config -> Watch -> Array Word8 -> IO (Config, Watch)
671−addToWatch cfg@Config{..} (Watch handle wdMap) path = do
672− fd <- handleToFd handle
673− wd <- A.asCString path $ \pathPtr ->
674− throwErrnoIfMinus1 ("addToWatch " ++ utf8ToString path) $
662+ -- XXX we need to tolerate an error where we are adding a watch for a
663+ -- non-existing file because the file may have got deleted by the time we
664+ -- added the watch. Perhaps we can have a flag in config for this and keep
665+ -- the default value to tolerate the error.
666+ --
667+ -- XXX The file may have even got deleted and then recreated which we will
668+ -- never get to know, document this.
669+ wd <- A.asCString absPath $ \pathPtr ->
670+ throwErrnoIfMinus1 ("addToWatch: " ++ utf8ToString absPath) $
675671 c_inotify_add_watch (fdFD fd) pathPtr (CUInt createFlags)
676− km <- readIORef wdMap
677− let k = (Map.insert (fromIntegral wd) (path, path) km)
678− writeIORef wdMap k
679− return $ (cfg, Watch handle wdMap)
680−
681−addToWatchRec :: Config -> Watch -> FilePath -> FilePath -> IO (Config, Watch)
682−addToWatchRec cfg watch path origRoot= do
683− subdirs <- toPathList path
684− pathArrList <- mapM toUtf8 subdirs
685− rootArr <- toUtf8 origRoot
686− foldlM (\(cfg1, w1) pathArr -> addToWatchExecutor cfg1 w1 pathArr rootArr)
687− (cfg, watch) pathArrList
672+
673+ -- We add the parent first so that we start getting events for any new
674+ -- creates and add the new subdirectories on creates while we are adding
675+ -- the children.
676+ modifyIORef wdMap (Map.insert (fromIntegral wd) (root, path))
677+
678+ -- Now add the children. If we missed any creates while we were adding the
679+ -- parent, this will make sure they are added too.
680+ --
681+ -- XXX Ensure that we generate events that we may have missed while we were
682+ -- adding the dirs.
683+ --
684+ -- XXX toDirs currently uses paths as String, we need to convert it
685+ -- to "/" separated by byte arrays.
686+ when watchRec $ do
687+ S.mapM_ (\p -> addToWatch cfg watch root (path <> p))
688+ $ S.mapM toUtf8
689+ $ Dir.toDirs $ utf8ToString absPath
688690
689691foreign import ccall unsafe
690692 "sys/inotify.h inotify_rm_watch" c_inotify_rm_watch
691693 :: CInt -> CInt -> IO CInt
692694
693−-- | Remove a path from a 'Watch', if a path was moved after adding you need to
694−-- need to provide the original path which was used to add the Watch.
695+-- | Remove an absolute root path from a 'Watch', if a path was moved after
696+-- adding you need to provide the original path which was used to add the
697+-- Watch.
695698--
696699-- /Internal/
697700--
698−removeFromWatch :: Watch -> Array Word8 -> IO Watch
701+removeFromWatch :: Watch -> Array Word8 -> IO ()
699702removeFromWatch (Watch handle wdMap) path = do
700703 fd <- handleToFd handle
701704 km <- readIORef wdMap
702705 wdMap1 <- foldlM (step fd) Map.empty (Map.toList km)
703706 writeIORef wdMap wdMap1
704− return $ Watch handle wdMap
705707
706708 where
707709
708710 step fd newMap (wd, v) = do
709711 if (fst v) == path
710712 then do
711− let err = "removeFromWatch " ++ show (utf8ToString path)
713+ let err = "removeFromWatch: " ++ show (utf8ToString path)
712714 rm = c_inotify_rm_watch (fdFD fd) (fromIntegral wd)
713715 void $ throwErrnoIfMinus1 err rm
714716 return newMap
@@ -720,32 +722,18 @@ removeFromWatch (Watch handle wdMap) path = do
720722--
721723-- /Internal/
722724--
723−openWatch ::
724− Config
725− -> NonEmpty (Array Word8)
726− -> IO (Config, Watch)
725+openWatch :: Config -> NonEmpty (Array Word8) -> IO Watch
727726openWatch cfg paths = do
728− let pathList = NonEmpty.toList paths
729− w <- createWatch
730− foldlM (\(cfg1, w1) path -> addToWatch cfg1 w1 path)
731− (cfg, w) pathList
732−
733−openWatchRec ::
734− Config
735− -> NonEmpty (Array Word8)
736− -> IO (Config, Watch)
737−openWatchRec cfg paths = do
738− let pathList = map utf8ToString $ NonEmpty.toList paths
739727 w <- createWatch
740− foldlM (\(cfg1, w1) path -> addToWatchRec cfg1 w1 path path)
741− (cfg, w) pathList
728+ mapM_ (\p -> addToWatch cfg w p (A.fromList [])) $ NonEmpty.toList paths
729+ return w
742730
743731-- | Close a 'Watch' handle.
744732--
745733-- /Internal/
746734--
747−closeWatch :: (Config, Watch) -> IO ()
748−closeWatch (_, Watch h _) = hClose h
735+closeWatch :: Watch -> IO ()
736+closeWatch (Watch h _) = hClose h
749737
750738-------------------------------------------------------------------------------
751739-- Raw events read from the watch file handle
@@ -788,8 +776,6 @@ readOneEvent cfg wt@(Watch _ wdMap) = do
788776 -- XXX need the "initial" in parsers to return a step type so that "take 0"
789777 -- can return without an input. otherwise if pathLen is 0 we will keep
790778 -- waiting to read one more char before we return this event.
791− -- PR.yieldM $ print ("Flag1 = " ++ show eflags)
792− -- PR.yieldM $ print ("pathLen = " ++ show pathLen)
793779 path <-
794780 if pathLen /= 0
795781 then do
@@ -798,30 +784,33 @@ readOneEvent cfg wt@(Watch _ wdMap) = do
798784 -- takeP
799785 pth <- PR.sliceSepByMax (== 0) pathLen (A.writeN pathLen)
800786 let remaining = pathLen - A.length pth - 1
801− -- PR.yieldM $ print ("remaining = " ++ show remaining)
802787 when (remaining /= 0) $ PR.takeEQ remaining FL.drain
803− -- PR.yieldM $ print ("takeEQ = " ++ show remaining)
804788 return pth
805789 else return $ A.fromList []
806− -- PR.yieldM $ print ("Flag2 = " ++ show eflags)
807− xm <- PR.yieldM $ readIORef wdMap
808− let base = case Map.lookup (fromIntegral ewd) xm of
809− Just path1 -> path1
810− Nothing -> (path, path)
811− let xpath = utf8ToString (snd base) </> utf8ToString path
812− _ <- if eflags .&. iN_CREATE /= 0 && eflags .&. iN_ISDIR /= 0
813− then
814− processEvent cfg wt xpath (utf8ToString $ fst base)
815− else return (cfg, wt)
816− rm2 <- PR.yieldM $ readIORef wdMap
817− pdiff <- PR.yieldM $ pathDiff (utf8ToString $ fst base) xpath
818− relPath <- PR.yieldM $ toUtf8 (pdiff)
790+ wdm <- PR.yieldM $ readIORef wdMap
791+ let (root, sub) =
792+ case Map.lookup (fromIntegral ewd) wdm of
793+ Just pair -> pair
794+ Nothing ->
795+ error $ "readOneEvent: "
796+ <> "Unknown watch descriptor: "
797+ <> show ewd
798+ let -- "sub" is guaranteed to have a trailing "/"
799+ sub1 = sub <> path
800+ -- Check for "ISDIR" first because it is less likely
801+ isDirCreate = eflags .&. iN_ISDIR /= 0 && eflags .&. iN_CREATE /= 0
802+ when (watchRec cfg && isDirCreate)
803+ $ PR.yieldM $ addToWatch cfg wt root sub1
804+ -- XXX Handle IN_DELETE, IN_DELETE_SELF, IN_MOVE_SELF, IN_MOVED_FROM,
805+ -- IN_MOVED_TO
806+ -- What if a large dir tree gets moved in to our hierarchy? Do we get a
807+ -- single event for the top level dir in this case?
819808 return $ Event
820809 { eventWd = (fromIntegral ewd)
821810 , eventFlags = eflags
822811 , eventCookie = cookie
823− , eventRelPath = relPath
824− , eventMap = rm2
812+ , eventRelPath = sub1
813+ , eventMap = wdm
825814 }
826815
827816 where
@@ -834,21 +823,8 @@ readOneEvent cfg wt@(Watch _ wdMap) = do
834823 pathLen :: Word32 <- peekByteOff ptr (len + 8)
835824 return (ewd, eflags, cookie, fromIntegral pathLen)
836825
837− processEvent ::
838− Config
839− -> Watch
840− -> FilePath
841− -> FilePath
842− -> Parser IO Word8 (Config, Watch)
843− processEvent cfg1@Config{..} wt1 xpath root = do
844− if watchRec
845− then
846− PR.yieldM $ addToWatchRec cfg1 wt1 xpath root
847− else
848− PR.yieldM $ return (cfg1, wt1)
849−
850−watchToStream :: (Config, Watch) -> SerialT IO Event
851−watchToStream (cfg, wt@(Watch handle _)) = do
826+watchToStream :: Config -> Watch -> SerialT IO Event
827+watchToStream cfg wt@(Watch handle _) = do
852828 -- Do not use too small a buffer. As per inotify man page:
853829 --
854830 -- The behavior when the buffer given to read(2) is too small to return
@@ -879,12 +855,7 @@ watchToStream (cfg, wt@(Watch handle _)) = do
879855--
880856watchPathsWith ::
881857 (Config -> Config) -> NonEmpty (Array Word8) -> SerialT IO Event
882−watchPathsWith f paths = S.bracket before after watchToStream
883−
884− where
885−
886− before = liftIO $ openWatch (f defaultConfig) paths
887− after = liftIO . closeWatch
858+watchPathsWith f = watchTreesWith (f . setRecursiveMode False)
888859
889860-- | Like 'watchPathsWith' but uses the 'defaultConfig' options.
890861--
@@ -897,22 +868,40 @@ watchPathsWith f paths = S.bracket before after watchToStream
897868watchPaths :: NonEmpty (Array Word8) -> SerialT IO Event
898869watchPaths = watchPathsWith id
899870
871+-- XXX We should not go across the mount points of network file systems or file
872+-- systems that are known to not generate any events.
873+--
900874-- | Start monitoring a list of file system paths for file system events with
901875-- the supplied configuration operation over the 'defaultConfig'. The
902876-- paths could be files or directories. When the path is a directory, the
903877-- whole directory tree under it is watched recursively. Monitoring starts from
904878-- the current time onwards.
905879--
880+-- Note that recrusive watch on a large directory tree could be expensive. When
881+-- starting a watch, the whole tree must be read and watches are started on
882+-- each directory in the tree. The initial time to start the watch as well as
883+-- the memory required is proportional to the number of directories in the
884+-- tree.
885+--
886+-- When new directories are created under the tree they are added to the watch
887+-- on receiving the directory create event. However, the creation of a dir and
888+-- adding a watch for it is not atomic. The implementation takes care of this
889+-- and makes sure that watches are added for all directories. However, In the
890+-- mean time, the directory may have received more events which may get lost.
891+-- Handling of any such lost events is yet to be implemented.
892+--
893+-- See the Linux __inotify__ man page for more details.
894+--
906895-- /Internal/
907896--
908897watchTreesWith ::
909898 (Config -> Config) -> NonEmpty (Array Word8) -> SerialT IO Event
910−watchTreesWith f paths = S.bracket before after watchToStream
899+watchTreesWith f paths = S.bracket before after (watchToStream cfg)
911900
912901 where
913902
914− before = liftIO
915− $ openWatchRec (f $ setRecursiveMode True defaultConfig) paths
903+ cfg = f defaultConfig
904+ before = liftIO $ openWatch cfg paths
916905 after = liftIO . closeWatch
917906
918907-- | Like 'watchTreesWith' but uses the 'defaultConfig' options.
@@ -928,12 +917,13 @@ watchTrees = watchTreesWith id
928917-- Examine event stream
929918-------------------------------------------------------------------------------
930919
931−-- | Get the watch descriptor corresponding to the 'Event'. A watch descriptor
932−-- identifies which of the paths being monitored has generated this event.
920+-- | Get the watch root corresponding to the 'Event'.
933921--
934922-- Note that if a path was moved after adding to the watch, this will give the
935923-- original path and not the new path after moving.
936924--
925+-- TBD: we can possibly update the watch root on a move self event.
926+--
937927-- /Internal/
938928--
939929getRoot :: Event -> Array Word8
@@ -1188,12 +1178,6 @@ isDir = getFlag iN_ISDIR
11881178-------------------------------------------------------------------------------
11891179-- Debugging
11901180-------------------------------------------------------------------------------
1191−showEventShort :: Event -> String
1192−showEventShort ev@Event{..} = (utf8ToString $ getRelPath ev)
1193− ++ "_" ++ show eventFlags
1194− ++ showev isDir "Dir"
1195−
1196− where showev f str = if f ev then "_" ++ str else ""
11971181
11981182-- | Convert an 'Event' record to a String representation.
11991183showEvent :: Event -> String
@@ -1232,4 +1216,4 @@ showEvent ev@Event{..} =
12321216#else
12331217#warning "Disabling module Streamly.Internal.FileSystem.Event.Linux. Does not support kernels older than 2.6.36."
12341218module Streamly.Internal.FileSystem.Event.Linux () where
1235−#endif
1219+#endif
src/Streamly/Internal/FileSystem/Event/Windows.hs+1−5
@@ -48,7 +48,7 @@ module Streamly.Internal.FileSystem.Event.Windows
4848
4949 -- ** Default configuration
5050 Config
51− , Event
51+ , Event (..)
5252 , Toggle (..)
5353 , setFlag
5454 , defaultConfig
@@ -89,7 +89,6 @@ module Streamly.Internal.FileSystem.Event.Windows
8989
9090 -- * Debugging
9191 , showEvent
92− , showEventShort
9392 )
9493where
9594
@@ -540,9 +539,6 @@ isOverflow Event{..} = totalBytes == 0
540539-------------------------------------------------------------------------------
541540-- Debugging
542541-------------------------------------------------------------------------------
543−-- | Convert an 'Event' record to a short representation for unit test.
544−showEventShort :: Event -> String
545−showEventShort ev@Event{..} = getRelPath ev ++ "_" ++ show eventFlags
546542
547543-- | Convert an 'Event' record to a String representation.
548544showEvent :: Event -> String
streamly.cabal+4−10
@@ -207,11 +207,6 @@ flag use-c-malloc
207207 manual: True
208208 default: False
209209
210−flag manual-tests
211− description: Builds tests that are to be run manually
212− manual: True
213− default: False
214−
215210-------------------------------------------------------------------------------
216211-- Common stanzas
217212-------------------------------------------------------------------------------
@@ -329,8 +324,8 @@ common test-dependencies
329324 , transformers >= 0.4 && < 0.6
330325 , QuickCheck >= 2.13 && < 2.15
331326 , directory >= 1.2.2 && < 1.4
332− , filepath >= 1.4.1
333− , temporary >= 1.3
327+ , filepath >= 1.4.1 && < 1.5
328+ , temporary >= 1.3 && < 1.4
334329
335330-- Compilation for coverage builds on CI machines takes too long without -O0
336331-- XXX we should use coverage flag for that, -O0 may take too long to run tests
@@ -389,7 +384,6 @@ library
389384
390385 if os(linux)
391386 exposed-modules: Streamly.Internal.FileSystem.Event.Linux
392− build-depends: unix >= 2.7.2, filepath >= 1.4.1
393387
394388 hs-source-dirs: src
395389 other-modules:
@@ -741,7 +735,7 @@ test-suite FileSystem.Event
741735 import: test-options
742736 type: exitcode-stdio-1.0
743737 main-is: Streamly/Test/FileSystem/Event.hs
744− hs-source-dirs: test
738+ hs-source-dirs: test
745739 default-language: Haskell2010
746740 if os(darwin)
747− buildable: False
741+ buildable: False
test/Streamly/Test/FileSystem/Event.hs+76−19
@@ -1,20 +1,32 @@
1+-- |
2+-- Module : Streamly.Test.FileSystem.Event
3+-- Copyright : (c) 2020 Composewell Technologies
4+-- License : BSD-3-Clause
5+-- Maintainer : streamly@composewell.com
6+-- Stability : experimental
7+-- Portability : GHC
8+--
9+module Main (main) where
10+
111import Control.Concurrent (MVar, newEmptyMVar, putMVar, takeMVar, threadDelay)
212import Control.Monad.IO.Class (MonadIO)
3−import Data.List.NonEmpty (NonEmpty)
13+import Data.Char (ord)
414import Data.Maybe (fromJust)
515import Data.Word (Word8)
6−import System.Directory ( createDirectoryIfMissing
7− , removeFile
8− , removeDirectory
9− , removePathForcibly
10− , renameDirectory
11− , renamePath
12− )
16+import System.Directory
17+ ( createDirectoryIfMissing
18+ , removeFile
19+ , removeDirectory
20+ , removePathForcibly
21+ , renameDirectory
22+ , renamePath
23+ )
1324import System.FilePath ((</>))
1425import System.IO (BufferMode(..), hSetBuffering, stdout)
1526import System.IO.Temp (withSystemTempDirectory)
16−import Streamly.Prelude (SerialT)
27+import System.IO.Unsafe (unsafePerformIO)
1728import Streamly.Internal.Data.Array.Storable.Foreign (Array)
29+
1830import Test.Hspec
1931import Test.Hspec.QuickCheck
2032
@@ -39,6 +51,7 @@ import qualified Streamly.Internal.FileSystem.Event.Windows as Event
3951import Data.Functor.Identity (runIdentity)
4052import qualified Streamly.Internal.Unicode.Stream as U
4153#endif
54+
4255-------------------------------------------------------------------------------
4356-- Utilities
4457-------------------------------------------------------------------------------
@@ -50,15 +63,13 @@ utf8ToString :: Array Word8 -> String
5063utf8ToString = runIdentity . S.toList . U.decodeUtf8' . Array.toStream
5164#endif
5265
53−watchPaths :: NonEmpty (Array Word8) -> SerialT IO Event.Event
54−watchPaths = Event.watchTrees
55−
5666timeout :: IO String
5767timeout = threadDelay 5000000 >> return "Timeout"
5868
5969fseventDir :: String
6070fseventDir = "fsevent_dir"
6171
72+-- XXX Make the getRelPath type same on windows and other platforms
6273eventPredicate :: Event.Event -> Bool
6374eventPredicate ev =
6475#if defined(CABAL_OS_WINDOWS)
@@ -72,6 +83,9 @@ eventPredicate ev =
7283-------------------------------------------------------------------------------
7384-- Event lists to be matched with
7485-------------------------------------------------------------------------------
86+
87+-- XXX Use a tuple (path, flags) instead of a string with flags
88+
7589#if defined(CABAL_OS_WINDOWS)
7690
7791singleDirCreateEvents :: [String]
@@ -145,6 +159,10 @@ renameFileNestedDirEvents =
145159 , "dir1\\dir2\\dir3\\FileRenamed.txt_5"
146160 ]
147161
162+-- | Convert an 'Event' record to a short representation for unit test.
163+showEventShort :: Event -> String
164+showEventShort ev@Event{..} = getRelPath ev ++ "_" ++ show eventFlags
165+
148166#else
149167
150168singleDirCreateEvents :: [String]
@@ -222,11 +240,37 @@ renameFileNestedDirEvents =
222240 [ "dir1/dir2/dir3/FileCreated.txt_64"
223241 , "dir1/dir2/dir3/FileRenamed.txt_128"
224242 ]
243+
244+removeTrailingSlash :: Array Word8 -> Array Word8
245+removeTrailingSlash path =
246+ if Array.length path == 0
247+ then path
248+ else
249+ let mx = Array.readIndex path (Array.length path - 1)
250+ in case mx of
251+ Nothing -> error "removeTrailingSlash: Bug: Invalid index"
252+ Just x ->
253+ if x == fromIntegral (ord '/')
254+ -- XXX need array slicing
255+ then unsafePerformIO
256+ $ Array.fromStreamN (Array.length path - 1)
257+ $ Array.toStream path
258+ else path
259+
260+showEventShort :: Event.Event -> String
261+showEventShort ev@Event.Event{..} =
262+ (utf8ToString $ removeTrailingSlash $ Event.getRelPath ev)
263+ ++ "_" ++ show eventFlags
264+ ++ showev Event.isDir "Dir"
265+
266+ where showev f str = if f ev then "_" ++ str else ""
267+
225268#endif
226269
227270-------------------------------------------------------------------------------
228271-- Event Watcher
229272-------------------------------------------------------------------------------
273+
230274checkEvents :: FilePath -> MVar () -> [String] -> IO String
231275checkEvents rootPath m matchList = do
232276 let args = [rootPath]
@@ -234,20 +278,25 @@ checkEvents rootPath m matchList = do
234278 putStrLn ("Watch started !!!! on Path " ++ rootPath)
235279 events <- S.parse (PR.takeWhile eventPredicate FL.toList)
236280 $ S.before (putMVar m ())
237− $ watchPaths (NonEmpty.fromList paths)
238− let eventStr = map Event.showEventShort events
239− putStrLn $ show (eventStr)
281+ $ Event.watchTrees (NonEmpty.fromList paths)
282+ let eventStr = map showEventShort events
240283 let baseSet = Set.fromList matchList
241284 resultSet = Set.fromList eventStr
242285 if (baseSet `Set.isSubsetOf` resultSet)
243286 then
244287 return "PASS"
245− else
288+ else do
289+ putStrLn $ "baseSet " ++ show matchList
290+ putStrLn $ "resultSet " ++ show eventStr
246291 return "Mismatch"
247292
248293-------------------------------------------------------------------------------
249294-- FS Event Generators
250295-------------------------------------------------------------------------------
296+
297+-- XXX Factor out common code from all these functions. The specific operation
298+-- can be passed to a common function.
299+
251300fsOpsCreateSingleDir :: FilePath -> MVar () -> IO ()
252301fsOpsCreateSingleDir fp m = do
253302 takeMVar m
@@ -319,7 +368,7 @@ fsOpsRemoveFileInRootDir fp m = do
319368 takeMVar m
320369 let tpath = (fp </> "FileCreated.txt")
321370 putStrLn ("Remove a File on " ++ fp)
322− threadDelay 200000
371+ threadDelay 200000
323372 >> removeFile tpath
324373 >> threadDelay 200000
325374 >> createDirectoryIfMissing True (fp </> "EOTask")
@@ -366,7 +415,6 @@ fsOpsRenameFileInNestedDir fp m = do
366415 >> threadDelay 200000
367416 >> createDirectoryIfMissing True (fp </> "EOTask")
368417
369−
370418checker :: S.IsStream t =>
371419 FilePath -> MVar () -> [String] -> t IO String
372420checker rootPath synch matchList =
@@ -383,6 +431,10 @@ driverInit = do
383431-------------------------------------------------------------------------------
384432-- Test Drivers
385433-------------------------------------------------------------------------------
434+
435+-- XXX Factor out common code from all these. Pass a specific fsops function to
436+-- a common functions.
437+
386438driverCreateSingleDir :: IO String
387439driverCreateSingleDir = do
388440 sync <- driverInit
@@ -553,6 +605,10 @@ driverRenameFileInNestedDir = do
553605-------------------------------------------------------------------------------
554606-- Test Cases
555607-------------------------------------------------------------------------------
608+
609+-- XXX These can either be directly inlined instead of creating a wrapper
610+-- function or the wrapper could be common passing the function being checked.
611+
556612testCreateSingleDir :: Expectation
557613testCreateSingleDir = driverCreateSingleDir `shouldReturn` "PASS"
558614
@@ -592,6 +648,7 @@ testRenameFileInNestedDir = driverRenameFileInNestedDir `shouldReturn` "PASS"
592648-------------------------------------------------------------------------------
593649-- Main
594650-------------------------------------------------------------------------------
651+
595652main :: IO ()
596653main = hspec $ do
597654 prop "Create a single directory" testCreateSingleDir
@@ -605,4 +662,4 @@ main = hspec $ do
605662 prop "Rename a file in root Dir" testRenameFileInRootDir
606663 prop "Create a file in a nested Dir" testCreateFileInNestedDir
607664 prop "Remove a file in a nested Dir" testRemoveFileInNestedDir
608− prop "Rename a file in a nested Dir" testRenameFileInNestedDir
665+ prop "Rename a file in a nested Dir" testRenameFileInNestedDir