on master help sign in
Overhaul file system event testing

Remove watchRecursive from common fs event module as this is unreliable on Linux platform. Change default config to use non-recursive watch to make it same as the behavior of the common module.

commit4512fa476f7a787f1530298a79c14c8f91ac0b88 authorHarendra Kumar <harendra@composewell.com> dateTue 9 Nov 2021, 14:32 (5 years ago) parentbb74cdd
14 files changed+989−1334
hie.yaml+8 src/Streamly/Internal/FileSystem/Event.hs+38−7 src/Streamly/Internal/FileSystem/Event/Darwin.hs+46−42 src/Streamly/Internal/FileSystem/Event/Linux.hs+61−32 src/Streamly/Internal/FileSystem/Event/Windows.hs+120−59 streamly.cabal+2−2 test/Streamly/Test/FileSystem/Event.hs+16−11 test/Streamly/Test/FileSystem/Event/Common.hs+418 test/Streamly/Test/FileSystem/Event/Darwin.hs+85 test/Streamly/Test/FileSystem/Event/Linux.hs+132−812 test/Streamly/Test/FileSystem/Event/LinuxTest.hs−9 test/Streamly/Test/FileSystem/Event/Windows.hs+42−334 test/Streamly/Test/FileSystem/Event/WindowsTest.hs−9 test/streamly-tests.cabal+21−17
hie.yaml+8
@@ -84,6 +84,14 @@ cradle:
8484 component: "test:Data.Unfold"
8585 - path: "./test/Streamly/Test/FileSystem/Event.hs"
8686 component: "test:FileSystem.Event"
87+ - path: "./test/Streamly/Test/FileSystem/Event/Common.hs"
88+ component: "test:FileSystem.Event"
89+ - path: "./test/Streamly/Test/FileSystem/Event/Linux.hs"
90+ component: "test:FileSystem.Event.Linux"
91+ - path: "./test/Streamly/Test/FileSystem/Event/Windows.hs"
92+ component: "test:FileSystem.Event.Windows"
93+ - path: "./test/Streamly/Test/FileSystem/Event/Darwin.hs"
94+ component: "test:FileSystem.Event.Darwin"
8795 - path: "./test/Streamly/Test/FileSystem/Handle.hs"
8896 component: "test:FileSystem.Handle"
8997 - path: "./test/Streamly/Test/Network/Inet/TCP.hs"
src/Streamly/Internal/FileSystem/Event.hs+38−7
@@ -9,7 +9,12 @@
99-- File system event notification API portable across Linux, macOS and Windows
1010-- platforms.
1111--
12−-- For platform specific API please see the following modules:
12+-- Note that recursive directory tree watch does not work reliably on Linux
13+-- (see notes in the Linux module), therefore, recursive watch API is not
14+-- provided in this module. However, you can use it from the platform specific
15+-- modules.
16+--
17+-- For platform specific APIs please see the following modules:
1318--
1419-- * "Streamly.Internal.FileSystem.Event.Darwin"
1520-- * "Streamly.Internal.FileSystem.Event.Linux"
@@ -20,7 +25,7 @@ module Streamly.Internal.FileSystem.Event
2025 -- * Creating a Watch
2126
2227 watch
23− , watchRecursive
28+ -- , watchRecursive
2429
2530 -- * Handling Events
2631 , Event
@@ -100,12 +105,20 @@ import qualified Streamly.Internal.FileSystem.Event.Windows as Event
100105-- generated. No events are generated if the watch root itself is renamed or
101106-- deleted.
102107--
103−-- Note: not yet implemented on macOS, use watchRecursive instead.
108+-- This API watches for changes in the watch root directory only, any changes
109+-- in the subdirectories of the watch root are not watched. However, on macOS
110+-- the watch is always recursive, but do not rely on that behavior, it may
111+-- change without notice in future. If you want to use recursive watch please
112+-- use platform specific modules.
104113--
105114-- /Pre-release/
106115--
107116watch :: NonEmpty (Array Word8) -> SerialT IO Event
117+#if defined(CABAL_OS_DARWIN)
118+watch = Event.watchRecursive
119+#else
108120watch = Event.watch
121+#endif
109122
110123-- | Like 'watch' except that if a watched path is a directory the whole
111124-- directory tree under it is watched recursively.
@@ -114,16 +127,34 @@ watch = Event.watch
114127--
115128-- /Pre-release/
116129--
117−watchRecursive :: NonEmpty (Array Word8) -> SerialT IO Event
118−watchRecursive = Event.watchRecursive
130+_watchRecursive :: NonEmpty (Array Word8) -> SerialT IO Event
131+_watchRecursive = Event.watchRecursive
119132
120133-------------------------------------------------------------------------------
121134-- Handling Events
122135-------------------------------------------------------------------------------
123136
137+-- XXX Ensure that on all paltforms the path has same conventions. That is do
138+-- not have a trailing path separator on one platfrom and not on another.
139+--
140+-- XXX We should use getRelPath instead so that the behavior when the root is a
141+-- symlink becomes platform independent.
142+--
124143-- | Get the absolute path of the file system object for which the event is
125144-- generated. The path is a UTF-8 encoded array of bytes.
126145--
146+-- When the watch root is a symlink the behavior is different on different
147+-- platforms:
148+--
149+-- * On Linux and Windows, the absolute path returned is via the original
150+-- symlink.
151+--
152+-- * On macOS the absolute path returned is via the real path of the root after
153+-- resolving the symlink.
154+--
155+-- This API is subject to removal in future, to be replaced by a platform
156+-- independent @getRelPath@.
157+--
127158-- /Pre-release/
128159--
129160getAbsPath :: Event -> Array Word8
@@ -135,8 +166,8 @@ getAbsPath = Event.getAbsPath
135166--
136167-- For hard links the behavior is different on different operating systems. On
137168-- macOS hard linking does not generate a create event, it generates an
138−-- 'isInodeAttrsChanged' event on the directory instead. On Linux and Windows
139−-- hard linking generates a create event.
169+-- 'isInodeAttrsChanged' event on the directory instead (see the Darwin
170+-- module). On Linux and Windows hard linking generates a create event.
140171--
141172-- /Pre-release/
142173--
src/Streamly/Internal/FileSystem/Event/Darwin.hs+46−42
@@ -114,23 +114,23 @@ module Streamly.Internal.FileSystem.Event.Darwin
114114 , watchWith
115115
116116 -- * Handling Events
117− , Event(..)
117+ , Event
118118 , getEventId
119119 , getAbsPath
120120
121121 -- ** Root Level Events
122122 -- | Events that belong to the root path as a whole and not to specific
123− -- itmes contained in it.
124− , isMount -- XXX change to isRootMounted
125− , isUnmount -- XXX change to isRootUnmounted
123+ -- items contained in it.
124+ , isMounted
125+ , isUnmounted
126126 , isHistoryDone
127127 , isRootPathEvent
128128
129129 -- ** Item Level Metadata change
130− , isOwnerGroupModeChanged
131− , isInodeAttrsChanged
132− , isFinderInfoChanged
133− , isXAttrsChanged
130+ , isAttrsModified
131+ , isSecurityModified
132+ , isXAttrsModified
133+ , isFinderInfoModified
134134
135135 -- ** Item Level CRUD events
136136 , isCreated
@@ -212,6 +212,7 @@ foreign import ccall safe
212212 "FileSystem/Event/Darwin.h FSEventStreamCreateFlagNoDefer"
213213 kFSEventStreamCreateFlagNoDefer :: Word32
214214
215+-- | Determines how multiple events are batched or throttled.
215216data BatchInfo =
216217 Throttle Double -- ^ Deliver an event immediately but suppress the
217218 -- following events upto the specified time.
@@ -527,24 +528,24 @@ watchToStream (Watch handle _ _) =
527528-- From the observed behavior it seems macOS watches the paths, whatever they
528529-- are pointing to at any given point of time:
529530--
530−-- * If the object pointing to the watched path is deleted and then recreated,
531+-- /Watch root deletion:/ If the the watch root is deleted and then recreated,
531532-- the newly created file or directory is automatically watched.
532533--
533−-- * If the watched path is moved to a new path, the object will no longer be
534−-- watched unless the new path is also being watched and was pointing to an
535−-- existing file at the time of starting the watch (see notes about
536−-- non-existing paths below).
534+-- /Watch root moved:/ If the watch root is moved to a new path, the object
535+-- will no longer be watched unless the new path is also being watched and was
536+-- pointing to an existing file at the time of starting the watch (see notes
537+-- about non-existing paths below).
537538--
538−-- /Symbolic Links:/ If the path name to be watched is a symbolic link then the
539−-- target of the link is watched instead of the symbolic link itself. It is
540−-- equivalent to as if the target of the symbolic link itself was directly
541−-- added to the watch API. That is, the symbolic link is resolved at the time
542−-- of adding the watch.
539+-- /Symbolic link watch root:/ If the path name to be watched is a symbolic
540+-- link then the target of the link is watched instead of the symbolic link
541+-- itself. It is equivalent to as if the target of the symbolic link itself was
542+-- directly added to the watch API. That is, the symbolic link is resolved at
543+-- the time of adding the watch.
543544--
544−-- Note that if a watched path is deleted and recreated as a symbolic link to
545−-- another path then the symbolic link file itself is watched, it won't be
546−-- resolved. The symbolic link resolution happens only at the time of adding
547−-- the watch.
545+-- Note that if a watched path is deleted and recreated as a symbolic link
546+-- pointing to another path then the symbolic link file itself is watched, it
547+-- won't be resolved. The symbolic link resolution happens only at the time of
548+-- adding the watch.
548549--
549550-- /Non-existing Paths:/ If a watch is started on a non-existing path then the
550551-- path is not watched even if it is created later. The macOS API does not
@@ -618,6 +619,9 @@ isEventIdWrapped = getFlag kFSEventStreamEventFlagEventIdsWrapped
618619-- | Get the absolute path of the file system object for which the event is
619620-- generated. The path is a UTF-8 encoded array of bytes.
620621--
622+-- When the watch root is a symlink, the absolute path returned is via the real
623+-- path of the root after resolving the symlink.
624+--
621625-- /Pre-release/
622626--
623627getAbsPath :: Event -> Array Word8
@@ -741,8 +745,8 @@ foreign import ccall safe
741745--
742746-- /Pre-release/
743747--
744−isMount :: Event -> Bool
745−isMount = getFlag kFSEventStreamEventFlagMount
748+isMounted :: Event -> Bool
749+isMounted = getFlag kFSEventStreamEventFlagMount
746750
747751foreign import ccall safe
748752 "FSEventStreamEventFlagUnmount"
@@ -755,8 +759,8 @@ foreign import ccall safe
755759--
756760-- /Pre-release/
757761--
758−isUnmount :: Event -> Bool
759−isUnmount = getFlag kFSEventStreamEventFlagUnmount
762+isUnmounted :: Event -> Bool
763+isUnmounted = getFlag kFSEventStreamEventFlagUnmount
760764
761765-------------------------------------------------------------------------------
762766-- Metadata change Events (applicable only when 'setFileEvents' is 'On')
@@ -780,8 +784,8 @@ foreign import ccall safe
780784--
781785-- /Pre-release/
782786--
783−isOwnerGroupModeChanged :: Event -> Bool
784−isOwnerGroupModeChanged = getFlag kFSEventStreamEventFlagItemChangeOwner
787+isSecurityModified :: Event -> Bool
788+isSecurityModified = getFlag kFSEventStreamEventFlagItemChangeOwner
785789
786790foreign import ccall safe
787791 "FSEventStreamEventFlagItemInodeMetaMod"
@@ -801,8 +805,8 @@ foreign import ccall safe
801805--
802806-- /Pre-release/
803807--
804−isInodeAttrsChanged :: Event -> Bool
805−isInodeAttrsChanged = getFlag kFSEventStreamEventFlagItemInodeMetaMod
808+isAttrsModified :: Event -> Bool
809+isAttrsModified = getFlag kFSEventStreamEventFlagItemInodeMetaMod
806810
807811foreign import ccall safe
808812 "FSEventStreamEventFlagItemFinderInfoMod"
@@ -817,8 +821,8 @@ foreign import ccall safe
817821--
818822-- /Pre-release/
819823--
820−isFinderInfoChanged :: Event -> Bool
821−isFinderInfoChanged = getFlag kFSEventStreamEventFlagItemFinderInfoMod
824+isFinderInfoModified :: Event -> Bool
825+isFinderInfoModified = getFlag kFSEventStreamEventFlagItemFinderInfoMod
822826
823827foreign import ccall safe
824828 "FSEventStreamEventFlagItemXattrMod"
@@ -835,8 +839,8 @@ foreign import ccall safe
835839--
836840-- /Pre-release/
837841--
838−isXAttrsChanged :: Event -> Bool
839−isXAttrsChanged = getFlag kFSEventStreamEventFlagItemXattrMod
842+isXAttrsModified :: Event -> Bool
843+isXAttrsModified = getFlag kFSEventStreamEventFlagItemXattrMod
840844
841845-------------------------------------------------------------------------------
842846-- CRUD Events (applicable only when 'setFileEvents' is 'On')
@@ -848,7 +852,7 @@ foreign import ccall safe
848852
849853-- | Determine whether the event indicates creation of an object within the
850854-- monitored path. This event is generated when any file system object other
851−-- than a hard link is created. On hard linking only an 'isInodeAttrsChanged'
855+-- than a hard link is created. On hard linking only an 'isAttrsModified'
852856-- event on the directory is generated, it is not a create event. However, when
853857-- a hard link is deleted 'isDeleted' and 'isHardLink' both are true.
854858--
@@ -858,7 +862,7 @@ foreign import ccall safe
858862-- generated, the path is not watched.
859863--
860864-- BUGS: On 10.15.1 when we use a "touch x" to create a file for the first time
861−-- only an 'isInodeAttrsChanged' event occurs and there is no 'isCreated'
865+-- only an 'isAttrsModified' event occurs and there is no 'isCreated'
862866-- event. However, this seems to have been fixed on 10.15.6.
863867--
864868-- /Applicable only when 'setFileEvents' is 'On'/
@@ -1050,14 +1054,14 @@ showEvent ev@Event{..} =
10501054 ++ showev isUserDropped "UserDropped"
10511055
10521056 ++ showev isRootPathEvent "RootPathEvent"
1053− ++ showev isMount "Mount"
1054− ++ showev isUnmount "Unmount"
1057+ ++ showev isMounted "Mounted"
1058+ ++ showev isUnmounted "Unmounted"
10551059 ++ showev isHistoryDone "HistoryDone"
10561060
1057− ++ showev isOwnerGroupModeChanged "OwnerGroupModeChanged"
1058− ++ showev isInodeAttrsChanged "InodeAttrsChanged"
1059− ++ showev isFinderInfoChanged "FinderInfoChanged"
1060− ++ showev isXAttrsChanged "XAttrsChanged"
1061+ ++ showev isSecurityModified "SecurityModified"
1062+ ++ showev isAttrsModified "AttrsModified"
1063+ ++ showev isFinderInfoModified "FinderInfoChanged"
1064+ ++ showev isXAttrsModified "XAttrsModified"
10611065
10621066 ++ showev isCreated "Created"
10631067 ++ showev isDeleted "Deleted"
src/Streamly/Internal/FileSystem/Event/Linux.hs+61−32
@@ -83,7 +83,7 @@ module Streamly.Internal.FileSystem.Event.Linux
8383 , setRootPathEvents
8484
8585 -- *** Item Level Metadata change
86− , setMetadataChanged
86+ , setAttrsModified
8787
8888 -- *** Item Level Access
8989 , setAccessed
@@ -124,7 +124,7 @@ module Streamly.Internal.FileSystem.Event.Linux
124124 , isRootUnmounted
125125
126126 -- ** Item Level Metadata change
127− , isMetadataChanged
127+ , isAttrsModified
128128
129129 -- ** Item Level Access
130130 , isAccessed
@@ -174,7 +174,7 @@ import GHC.IO.FD (fdFD, mkFD)
174174import GHC.IO.Handle.FD (mkHandleFromFD)
175175import Streamly.Prelude (SerialT)
176176import Streamly.Internal.Data.Parser (Parser)
177−import Streamly.Internal.Data.Array.Foreign.Type (Array(..))
177+import Streamly.Internal.Data.Array.Foreign.Type (Array(..), byteLength)
178178import System.IO (Handle, hClose, IOMode(ReadMode))
179179#if !MIN_VERSION_base(4,10,0)
180180import Control.Concurrent.MVar (readMVar)
@@ -254,6 +254,9 @@ foreign import capi
254254-- | If the pathname to be watched is a symbolic link then watch the target of
255255-- the symbolic link instead of the symbolic link itself.
256256--
257+-- Note that the path location in the events is through the original symbolic
258+-- link path rather than the resolved path.
259+--
257260-- /default: On/
258261--
259262-- /Pre-release/
@@ -382,8 +385,8 @@ foreign import capi
382385--
383386-- /Pre-release/
384387--
385−setMetadataChanged :: Toggle -> Config -> Config
386−setMetadataChanged = setFlag iN_ATTRIB
388+setAttrsModified :: Toggle -> Config -> Config
389+setAttrsModified = setFlag iN_ATTRIB
387390
388391foreign import capi
389392 "sys/inotify.h value IN_ACCESS" iN_ACCESS :: Word32
@@ -497,7 +500,7 @@ setModified = setFlag iN_MODIFY
497500--
498501-- * setRootDeleted
499502-- * setRootMoved
500−-- * setMetadataChanged
503+-- * setAttrsModified
501504-- * setAccessed
502505-- * setOpened
503506-- * setWriteClosed
@@ -511,10 +514,10 @@ setModified = setFlag iN_MODIFY
511514-- /Pre-release/
512515--
513516setAllEvents :: Toggle -> Config -> Config
514−setAllEvents s cfg =
515− ( setRootDeleted s
517+setAllEvents s =
518+ setRootDeleted s
516519 . setRootMoved s
517− . setMetadataChanged s
520+ . setAttrsModified s
518521 . setAccessed s
519522 . setOpened s
520523 . setWriteClosed s
@@ -524,7 +527,6 @@ setAllEvents s cfg =
524527 . setMovedFrom s
525528 . setMovedTo s
526529 . setModified s
527− ) cfg
528530
529531-------------------------------------------------------------------------------
530532-- Default config
@@ -655,9 +657,9 @@ handleToFd h = case h of
655657--
656658ensureTrailingSlash :: Array Word8 -> Array Word8
657659ensureTrailingSlash path =
658− if A.length path /= 0
660+ if byteLength path /= 0
659661 then
660− let mx = A.getIndex path (A.length path - 1)
662+ let mx = A.getIndex path (byteLength path - 1)
661663 in case mx of
662664 Nothing -> error "ensureTrailingSlash: Bug: Invalid index"
663665 Just x ->
@@ -666,6 +668,20 @@ ensureTrailingSlash path =
666668 else path
667669 else path
668670
671+removeTrailingSlash :: Array Word8 -> Array Word8
672+removeTrailingSlash path =
673+ if byteLength path /= 0
674+ then
675+ let n = byteLength path - 1
676+ mx = A.getIndex path n
677+ in case mx of
678+ Nothing -> error "removeTrailingSlash: Bug: Invalid index"
679+ Just x ->
680+ if x == fromIntegral (ord '/')
681+ then A.getSliceUnsafe 0 n path
682+ else path
683+ else path
684+
669685-- | @addToWatch cfg watch root subpath@ adds @subpath@ to the list of paths
670686-- being monitored under @root@ via the watch handle @watch@. @root@ must be
671687-- an absolute path and @subpath@ must be relative to @root@.
@@ -746,7 +762,7 @@ removeFromWatch (Watch handle wdMap) path = do
746762 where
747763
748764 step fd newMap (wd, v) = do
749− if (fst v) == path
765+ if fst v == path
750766 then do
751767 let err = "removeFromWatch: " ++ show (utf8ToString path)
752768 rm = c_inotify_rm_watch (fdFD fd) (fromIntegral wd)
@@ -808,7 +824,7 @@ data Event = Event
808824--
809825readOneEvent :: Config -> Watch -> Parser IO Word8 Event
810826readOneEvent cfg wt@(Watch _ wdMap) = do
811− let headerLen = (sizeOf (undefined :: CInt)) + 12
827+ let headerLen = sizeOf (undefined :: CInt) + 12
812828 arr <- PR.takeEQ headerLen (A.writeN headerLen)
813829 (ewd, eflags, cookie, pathLen) <- PR.fromEffect $ A.unsafeAsPtr arr readHeader
814830 -- XXX need the "initial" in parsers to return a step type so that "take 0"
@@ -824,7 +840,7 @@ readOneEvent cfg wt@(Watch _ wdMap) = do
824840 PR.fromFold
825841 $ FL.takeEndBy_ (== 0)
826842 $ FL.take pathLen (A.writeN pathLen)
827− let remaining = pathLen - A.length pth - 1
843+ let remaining = pathLen - byteLength pth - 1
828844 when (remaining /= 0) $ PR.takeEQ remaining FL.drain
829845 return pth
830846 else return $ A.fromList []
@@ -847,7 +863,7 @@ readOneEvent cfg wt@(Watch _ wdMap) = do
847863 -- What if a large dir tree gets moved in to our hierarchy? Do we get a
848864 -- single event for the top level dir in this case?
849865 return $ Event
850− { eventWd = (fromIntegral ewd)
866+ { eventWd = fromIntegral ewd
851867 , eventFlags = eflags
852868 , eventCookie = cookie
853869 , eventRelPath = sub1
@@ -888,18 +904,21 @@ watchToStream cfg wt@(Watch handle _) = do
888904-- recursively. Monitoring starts from the current time onwards. The paths are
889905-- specified as UTF-8 encoded 'Array' of 'Word8'.
890906--
891−-- Note that recrusive watch on a large directory tree could be expensive. When
892−-- starting a watch, the whole tree must be read and watches are started on
893−-- each directory in the tree. The initial time to start the watch as well as
894−-- the memory required is proportional to the number of directories in the
895−-- tree.
907+-- /Non-existing Paths:/ the API fails if a watch is started on a non-exsting
908+-- path.
909+--
910+-- /Performance:/ Note that recursive watch on a large directory tree could be
911+-- expensive. When starting a watch, the whole tree must be read and watches
912+-- are started on each directory in the tree. The initial time to start the
913+-- watch as well as the memory required is proportional to the number of
914+-- directories in the tree.
896915--
897−-- When new directories are created under the tree they are added to the watch
898−-- on receiving the directory create event. However, the creation of a dir and
899−-- adding a watch for it is not atomic. The implementation takes care of this
900−-- and makes sure that watches are added for all directories. However, In the
901−-- mean time, the directory may have received more events which may get lost.
902−-- Handling of any such lost events is yet to be implemented.
916+-- /Bugs:/ When new directories are created under the tree they are added to
917+-- the watch on receiving the directory create event. However, the creation of
918+-- a dir and adding a watch for it is not atomic. The implementation takes
919+-- care of this and makes sure that watches are added for all directories.
920+-- However, In the mean time, the directory may have received more events which
921+-- may get lost. Handling of any such lost events is yet to be implemented.
903922--
904923-- See the Linux __inotify__ man page for more details.
905924--
@@ -924,6 +943,8 @@ watchWith f paths = S.bracket before after (watchToStream cfg)
924943--
925944-- >>> watchRecursive = watchWith id
926945--
946+-- See 'watchWith' for pitfalls and bugs when using recursive watch on Linux.
947+--
927948-- /Pre-release/
928949--
929950watchRecursive :: NonEmpty (Array Word8) -> SerialT IO Event
@@ -953,7 +974,7 @@ watch = watchWith (setRecursiveMode False)
953974--
954975getRoot :: Event -> Array Word8
955976getRoot Event{..} =
956− if (eventWd >= 1)
977+ if eventWd >= 1
957978 then
958979 case Map.lookup (fromIntegral eventWd) eventMap of
959980 Just path -> fst path
@@ -973,10 +994,18 @@ getRelPath Event{..} = eventRelPath
973994
974995-- | Get the absolute file system object path for which the event is generated.
975996--
997+-- When the watch root is a symlink, the absolute path returned is via the
998+-- original symlink and not through the resolved path.
999+--
9761000-- /Pre-release/
9771001--
9781002getAbsPath :: Event -> Array Word8
979−getAbsPath ev = ensureTrailingSlash (getRoot ev) <> getRelPath ev
1003+getAbsPath ev =
1004+ let relpath = getRelPath ev
1005+ root = getRoot ev
1006+ in if byteLength relpath /= 0
1007+ then ensureTrailingSlash root <> relpath
1008+ else removeTrailingSlash root
9801009
9811010-- XXX should we use a Maybe?
9821011-- | Cookie is set when a rename occurs. The cookie value can be used to
@@ -1100,8 +1129,8 @@ isRootPathEvent = getFlag (iN_DELETE_SELF .|. iN_MOVE_SELF .|. iN_UNMOUNT)
11001129--
11011130-- /Pre-release/
11021131--
1103−isMetadataChanged :: Event -> Bool
1104−isMetadataChanged = getFlag iN_ATTRIB
1132+isAttrsModified :: Event -> Bool
1133+isAttrsModified = getFlag iN_ATTRIB
11051134
11061135-------------------------------------------------------------------------------
11071136-- Access
@@ -1251,7 +1280,7 @@ showEvent ev@Event{..} =
12511280 ++ showev isRootMoved "RootMoved"
12521281 ++ showev isRootUnmounted "RootUnmounted"
12531282
1254− ++ showev isMetadataChanged "MetadataChanged"
1283+ ++ showev isAttrsModified "AttrsModified"
12551284
12561285 ++ showev isAccessed "Accessed"
12571286 ++ showev isOpened "Opened"
src/Streamly/Internal/FileSystem/Event/Windows.hs+120−59
@@ -55,12 +55,13 @@ module Streamly.Internal.FileSystem.Event.Windows
5555 , setRecursiveMode
5656
5757 -- ** Events of Interest
58− , setRootMoved
59− , setModifiedFileName
60− , setModifiedAttribute
61− , setModifiedSize
62− , setModifiedLastWrite
63− , setModifiedSecurity
58+ , setFileNameEvents
59+ , setDirNameEvents
60+ , setAttrsModified
61+ , setSecurityModified
62+ , setSizeModified
63+ , setLastWriteTimeModified
64+ , setAllEvents
6465
6566 -- ** Watch APIs
6667 , watch
@@ -142,7 +143,7 @@ data Config = Config
142143--
143144-- /Pre-release/
144145--
145−data Toggle = On | Off
146+data Toggle = On | Off deriving (Show, Eq)
146147
147148setFlag :: DWORD -> Toggle -> Config -> Config
148149setFlag mask status cfg@Config{..} =
@@ -154,97 +155,127 @@ setFlag mask status cfg@Config{..} =
154155
155156-- | Set watch event on directory recursively.
156157--
157−-- /default: On/
158+-- /default: Off/
158159--
159160-- /Pre-release/
160161--
161−setRecursiveMode :: BOOL -> Config -> Config
162−setRecursiveMode rec cfg@Config{} = cfg {watchRec = rec}
162+setRecursiveMode :: Toggle -> Config -> Config
163+setRecursiveMode rec cfg@Config{} = cfg {watchRec = rec == On}
163164
164−-- | Report when a file name is modified.
165+-- | Generate notify events on file create, rename or delete.
166+--
167+-- From Windows API documentation: Any file name change in the watched
168+-- directory or subtree causes a change notification wait operation to return.
169+-- Changes include renaming, creating, or deleting a file.
165170--
166171-- /default: On/
167172--
168173-- /Pre-release/
169174--
170−setModifiedFileName :: Toggle -> Config -> Config
171−setModifiedFileName = setFlag fILE_NOTIFY_CHANGE_FILE_NAME
175+setFileNameEvents :: Toggle -> Config -> Config
176+setFileNameEvents = setFlag fILE_NOTIFY_CHANGE_FILE_NAME
172177
173−-- | Report when a directory name is modified.
178+-- | Generate notify events on directory create, rename or delete.
179+--
180+-- From Windows API documentaiton: Any directory-name change in the watched
181+-- directory or subtree causes a change notification wait operation to return.
182+-- Changes include creating or deleting a directory.
174183--
175184-- /default: On/
176185--
177186-- /Pre-release/
178187--
179−setRootMoved :: Toggle -> Config -> Config
180−setRootMoved = setFlag fILE_NOTIFY_CHANGE_DIR_NAME
188+setDirNameEvents :: Toggle -> Config -> Config
189+setDirNameEvents = setFlag fILE_NOTIFY_CHANGE_DIR_NAME
181190
182−-- | Report when a file attribute is modified.
191+-- | Generate an 'isModified' event on any attribute change in the watched
192+-- directory or subtree.
183193--
184−-- /default: On/
194+-- /default: Off/
185195--
186196-- /Pre-release/
187197--
188−setModifiedAttribute :: Toggle -> Config -> Config
189−setModifiedAttribute = setFlag fILE_NOTIFY_CHANGE_ATTRIBUTES
198+setAttrsModified :: Toggle -> Config -> Config
199+setAttrsModified = setFlag fILE_NOTIFY_CHANGE_ATTRIBUTES
190200
191−-- | Report when a file size is changed.
201+-- | Generate an 'isModified' event when the file size is changed.
192202--
193−-- /default: On/
203+-- From Windows API documentation: Any file-size change in the watched
204+-- directory or subtree causes a change notification wait operation to return.
205+-- The operating system detects a change in file size only when the file is
206+-- written to the disk. For operating systems that use extensive caching,
207+-- detection occurs only when the cache is sufficiently flushed.
208+--
209+-- /default: Off/
194210--
195211-- /Pre-release/
196212--
197−setModifiedSize :: Toggle -> Config -> Config
198−setModifiedSize = setFlag fILE_NOTIFY_CHANGE_SIZE
213+setSizeModified :: Toggle -> Config -> Config
214+setSizeModified = setFlag fILE_NOTIFY_CHANGE_SIZE
199215
200−-- | Report when a file last write time is changed.
216+-- | Generate an 'isModified' event when the last write timestamp of the file
217+-- inode is changed.
201218--
202−-- /default: On/
219+-- From Windows API documentation: Any change to the last write-time of files
220+-- in the watched directory or subtree causes a change notification wait
221+-- operation to return. The operating system detects a change to the last
222+-- write-time only when the file is written to the disk. For operating systems
223+-- that use extensive caching, detection occurs only when the cache is
224+-- sufficiently flushed.
225+--
226+-- /default: Off/
203227--
204228-- /Pre-release/
205229--
206−setModifiedLastWrite :: Toggle -> Config -> Config
207−setModifiedLastWrite = setFlag fILE_NOTIFY_CHANGE_LAST_WRITE
230+setLastWriteTimeModified :: Toggle -> Config -> Config
231+setLastWriteTimeModified = setFlag fILE_NOTIFY_CHANGE_LAST_WRITE
208232
209−-- | Report when a file Security attribute changes.
233+-- | Generate an 'isModified' event when any security-descriptor change occurs
234+-- in the watched directory or subtree.
210235--
211−-- /default: On/
236+-- /default: Off/
212237--
213238-- /Pre-release/
214239--
215−setModifiedSecurity :: Toggle -> Config -> Config
216−setModifiedSecurity = setFlag fILE_NOTIFY_CHANGE_SECURITY
240+setSecurityModified :: Toggle -> Config -> Config
241+setSecurityModified = setFlag fILE_NOTIFY_CHANGE_SECURITY
217242
218243-- | Set all tunable events 'On' or 'Off'. Equivalent to setting:
219244--
220−-- * setModifiedFileName
221−-- * setRootMoved
222−-- * setModifiedAttribute
223−-- * setModifiedSize
224−-- * setModifiedLastWrite
225−-- * setModifiedSecurity
245+-- * setFileNameEvents
246+-- * setDirNameEvents
247+-- * setAttrsModified
248+-- * setSizeModified
249+-- * setLastWriteTimeModified
250+-- * setSecurityModified
226251--
227252-- /Pre-release/
228253--
229254setAllEvents :: Toggle -> Config -> Config
230255setAllEvents s =
231− setModifiedFileName s
232− . setRootMoved s
233− . setModifiedAttribute s
234− . setModifiedSize s
235− . setModifiedLastWrite s
236− . setModifiedSecurity s
256+ setFileNameEvents s
257+ . setDirNameEvents s
258+ . setAttrsModified s
259+ . setSizeModified s
260+ . setLastWriteTimeModified s
261+ . setSecurityModified s
237262
238263-- | The tunable events that are enabled by default are:
239264--
240−-- * setModifiedFileName On
265+-- * setFileNameEvents On
266+-- * setDirNameEvents On
267+-- * setSizeModified On
268+-- * setLastWriteTimeModified On
241269--
242270-- /Pre-release/
243271--
244272defaultConfig :: Config
245273defaultConfig =
246− setModifiedFileName On
247− $ Config {watchRec = True, createFlags = 0}
274+ setFileNameEvents On
275+ $ setDirNameEvents On
276+ $ setSizeModified On
277+ $ setLastWriteTimeModified On
278+ $ Config {watchRec = False, createFlags = 0}
248279
249280getConfigFlag :: Config -> DWORD
250281getConfigFlag Config{..} = createFlags
@@ -361,6 +392,7 @@ readDirectoryChanges root h wst mask = do
361392 bytesRet <- peekByteOff bret 0
362393 readChangeEvents buffer root bytesRet
363394
395+-- XXX Try to get these from windows header files
364396type FileAction = DWORD
365397
366398fILE_ACTION_ADDED :: FileAction
@@ -426,9 +458,14 @@ closePathHandleStream = S.mapM_ (\(h, _, _) -> closeHandle h)
426458-- monitored. Monitoring starts from the current time onwards. The paths are
427459-- specified as UTF-8 encoded 'Array' of 'Word8'.
428460--
461+-- /Symbolic Links:/ If the pathname to be watched is a symbolic link then
462+-- watch the target of the symbolic link instead of the symbolic link itself.
463+-- Note that the path location in the events is through the original symbolic
464+-- link path rather than the resolved path.
465+--
429466-- @
430467-- watchWith
431−-- ('setModifiedAttribute' On . 'setModifiedLastWrite' Off)
468+-- ('setAttrsModified' On . 'setLastWriteTimeModified' Off)
432469-- [Array.fromCString\# "dir"#]
433470-- @
434471--
@@ -445,19 +482,21 @@ watchWith f paths =
445482
446483-- | Same as 'watchWith' using 'defaultConfig' and recursive mode.
447484--
448−-- >>> watchRecursive = watchWith id
485+-- >>> watchRecursive = watchWith (setRecursiveMode On)
449486--
450487-- /Pre-release/
451488--
452489watchRecursive :: NonEmpty (Array Word8) -> SerialT IO Event
453−watchRecursive = watchWith id
490+watchRecursive = watchWith (setRecursiveMode On)
454491
455492-- | Same as 'watchWith' using defaultConfig and non-recursive mode.
456493--
494+-- >>> watch = watchWith id
495+--
457496-- /Pre-release/
458497--
459498watch :: NonEmpty (Array Word8) -> SerialT IO Event
460−watch = watchWith (setRecursiveMode False)
499+watch = watchWith id
461500
462501getFlag :: DWORD -> Event -> Bool
463502getFlag mask Event{..} = eventFlags == mask
@@ -481,6 +520,13 @@ getRelPath Event{..} = (UTF8.toArray . UTF8.pack) eventRelPath
481520getRoot :: Event -> Array Word8
482521getRoot Event{..} = (UTF8.toArray . UTF8.pack) eventRootPath
483522
523+-- | Get the absolute file system object path for which the event is generated.
524+--
525+-- When the watch root is a symlink, the absolute path returned is via the
526+-- original symlink and not through the resolved path.
527+--
528+-- /Pre-release/
529+--
484530getAbsPath :: Event -> Array Word8
485531getAbsPath ev = getRoot ev <> A.fromCString# "\\"# <> getRelPath ev
486532
@@ -490,6 +536,8 @@ getAbsPath ev = getRoot ev <> A.fromCString# "\\"# <> getRelPath ev
490536-- directory or directory tree when in recursive watch mode. Creating a hard
491537-- link also generates this event.
492538--
539+-- /Occurs when either 'setFileNameEvents' or 'setDirNameEvents' is enabled/
540+--
493541-- /Pre-release/
494542--
495543isCreated :: Event -> Bool
@@ -499,6 +547,8 @@ isCreated = getFlag fILE_ACTION_ADDED
499547-- watched directory or directory tree when in recursive mode. This event is
500548-- generated even when a hard link is deleted.
501549--
550+-- /Occurs when either 'setFileNameEvents' or 'setDirNameEvents' is enabled/
551+--
502552-- /Pre-release/
503553--
504554isDeleted :: Event -> Bool
@@ -507,6 +557,8 @@ isDeleted = getFlag fILE_ACTION_REMOVED
507557-- | Generated for the original path when an object is moved from under a
508558-- monitored directory.
509559--
560+-- /Occurs when either 'setFileNameEvents' or 'setDirNameEvents' is enabled/
561+--
510562-- /Pre-release/
511563--
512564isMovedFrom :: Event -> Bool
@@ -515,6 +567,8 @@ isMovedFrom = getFlag fILE_ACTION_RENAMED_OLD_NAME
515567-- | Generated for the new path when an object is moved under a monitored
516568-- directory.
517569--
570+-- /Occurs when either 'setFileNameEvents' or 'setDirNameEvents' is enabled/
571+--
518572-- /Pre-release/
519573--
520574isMovedTo :: Event -> Bool
@@ -525,6 +579,7 @@ isMovedTo = getFlag fILE_ACTION_RENAMED_NEW_NAME
525579--
526580-- >>> isMoved ev = isMovedFrom ev || isMovedTo ev
527581--
582+-- /Occurs when either 'setFileNameEvents' or 'setDirNameEvents' is enabled/
528583-- /Occurs only for an object inside the watched directory/
529584--
530585-- /Pre-release/
@@ -534,20 +589,26 @@ isMoved ev = isMovedFrom ev || isMovedTo ev
534589
535590-- XXX This event is generated only for files and not directories?
536591--
537−-- | Determine whether the event indicates modification of an object within the
538−-- monitored path. This event is generated when a file or directory contents
539−-- are modified. In non-recursive mode this event is not generated for
540−-- directories. In recursive mode this event is generated for the parent
541−-- directory if a file or directory inside it is created or renamed.
592+-- | This event occurs when a file or directory contents, timestamps or
593+-- attributes are modified. Since it can occur on multiple changes, you may
594+-- have to check the attributes to know what exactly changed when multiple type
595+-- of modified events are enabled.
596+--
597+-- In non-recursive mode this event does not occur for directories. In
598+-- recursive mode this event occurs for the parent directory if a file or
599+-- directory inside it is created or renamed.
600+--
601+-- /Occurs when one of the @set*Modified@ events is enabled/
542602--
543603-- /Pre-release/
544604--
545605isModified :: Event -> Bool
546606isModified = getFlag fILE_ACTION_MODIFIED
547607
548−-- | If the buffer overflows, entire contents of the buffer are discarded,
549−-- therefore, events are lost. The user application must scan everything under
550−-- the watched paths to know the current state.
608+-- | If the kernel event buffer overflows, entire contents of the buffer are
609+-- discarded, therefore, events are lost. The user application must scan
610+-- everything under the watched paths to know the current state of the file
611+-- system tree.
551612--
552613-- /Pre-release/
553614--
streamly.cabal+2−2
@@ -93,10 +93,10 @@ extra-source-files:
9393 test/Streamly/Test/Data/Array/Stream/Foreign.hs
9494 test/Streamly/Test/Data/Parser/ParserD.hs
9595 test/Streamly/Test/FileSystem/Event.hs
96+ test/Streamly/Test/FileSystem/Event/Common.hs
97+ test/Streamly/Test/FileSystem/Event/Darwin.hs
9698 test/Streamly/Test/FileSystem/Event/Windows.hs
9799 test/Streamly/Test/FileSystem/Event/Linux.hs
98− test/Streamly/Test/FileSystem/Event/WindowsTest.hs
99− test/Streamly/Test/FileSystem/Event/LinuxTest.hs
100100 test/Streamly/Test/FileSystem/Handle.hs
101101 test/Streamly/Test/Network/Socket.hs
102102 test/Streamly/Test/Network/Inet/TCP.hs
test/Streamly/Test/FileSystem/Event.hs+16−11
@@ -1,23 +1,28 @@
11-- |
22-- Module : Streamly.Test.FileSystem.Event
3−-- Copyright : (c) 2020 Composewell Technologies
3+-- Copyright : (c) 2021 Composewell Technologies
44-- License : BSD-3-Clause
55-- Maintainer : streamly@composewell.com
66-- Stability : experimental
77-- Portability : GHC
88--
99
10−module Main (main) where
10+module Streamly.Test.FileSystem.Event (main) where
1111
12−import System.IO (BufferMode(..), hSetBuffering, stdout)
13−#if defined(CABAL_OS_LINUX)
14−import qualified Streamly.Test.FileSystem.Event.Linux as Event
15−#elif defined(CABAL_OS_WINDOWS)
16−import qualified Streamly.Test.FileSystem.Event.Windows as Event
17−#endif
12+import qualified Streamly.Internal.FileSystem.Event as Event
13+import Streamly.Test.FileSystem.Event.Common
14+
15+moduleName :: String
16+moduleName = "FileSystem.Event"
1817
1918main :: IO ()
2019main = do
21− hSetBuffering stdout NoBuffering
22− Event.testCommonEvents
23−
20+ let run = runTests moduleName "non-recursive" Event.watch
21+ run DirType commonTests
22+ run
23+#if defined(CABAL_OS_DARWIN)
24+ SymLinkResolvedPath
25+#else
26+ SymLinkOrigPath
27+#endif
28+ commonTests
test/Streamly/Test/FileSystem/Event/Common.hsnew file+418
@@ -0,0 +1,418 @@
1+-- |
2+-- Module : Streamly.Test.FileSystem.Event.Common
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 Streamly.Test.FileSystem.Event.Common
10+ ( TestDesc
11+
12+ -- * Running tests
13+ , runTests
14+ , WatchRootType (..)
15+
16+ -- * Event predicates
17+ , dirEvent
18+ , fileEvent
19+
20+ -- * Tests
21+ , dirCreate
22+ , dirCreateWithParent
23+ , dirDelete
24+ , dirMove
25+ , rootDirMove
26+
27+ , fileCreate
28+ , fileCreateWithParent
29+ , fileDelete
30+ , fileModify
31+ , fileMove
32+ , rootFileMove
33+
34+ , commonTests
35+ , commonRecTests
36+ )
37+
38+where
39+
40+import Debug.Trace (trace)
41+import Control.Concurrent (MVar, newEmptyMVar, putMVar, takeMVar, threadDelay)
42+import Control.Monad (when)
43+import Control.Monad.IO.Class (MonadIO)
44+import Data.Bifunctor (first)
45+import Data.Function ((&))
46+import Data.Functor.Identity (runIdentity)
47+import Data.List.NonEmpty (NonEmpty)
48+import Data.Word (Word8)
49+import Streamly.Data.Array.Foreign (Array)
50+import System.Directory
51+ ( createDirectory
52+ , createDirectoryIfMissing
53+ , createDirectoryLink
54+ , removeFile
55+ , removePathForcibly
56+ , renameDirectory
57+ , renamePath
58+ )
59+import System.FilePath ((</>), takeDirectory)
60+import System.IO
61+ ( BufferMode(..), hSetBuffering, stdout, IOMode (WriteMode), openFile
62+ , hClose)
63+import System.IO.Temp (withSystemTempDirectory)
64+
65+import qualified Data.List.NonEmpty as NonEmpty
66+import qualified Streamly.Internal.Data.Array.Foreign as Array
67+import qualified Streamly.Internal.Data.Stream.IsStream as Stream
68+import qualified Streamly.Unicode.Stream as Unicode
69+
70+#if defined(FILESYSTEM_EVENT_LINUX)
71+import Streamly.Internal.FileSystem.Event.Linux (Event)
72+import qualified Streamly.Internal.FileSystem.Event.Linux as Event
73+#elif defined(FILESYSTEM_EVENT_DARWIN)
74+import Streamly.Internal.FileSystem.Event.Darwin (Event)
75+import qualified Streamly.Internal.FileSystem.Event.Darwin as Event
76+#elif defined(FILESYSTEM_EVENT_WINDOWS)
77+import Streamly.Internal.FileSystem.Event.Windows (Event)
78+import qualified Streamly.Internal.FileSystem.Event.Windows as Event
79+#else
80+import Streamly.Internal.FileSystem.Event (Event)
81+import qualified Streamly.Internal.FileSystem.Event as Event
82+#endif
83+
84+import Test.Hspec
85+
86+-------------------------------------------------------------------------------
87+-- Check generated events
88+-------------------------------------------------------------------------------
89+
90+type EventChecker =
91+ FilePath -- watch root
92+ -> FilePath -- watch target, target dir if root is a symlink
93+ -> MVar () -- mvar to sync file system ops and the watch
94+ -> [(String, Event -> Bool)] -- expected events
95+ -> IO ()
96+type EventWatcher = NonEmpty (Array Word8) -> Stream.SerialT IO Event.Event
97+
98+eventMatches :: Event -> (String, Event -> Bool) -> Bool
99+eventMatches ev (expectedPath, f) =
100+ trace ("paths: " ++ show evPath ++ " " ++ show expectedPath)
101+ (evPath == expectedPath && f ev)
102+
103+ where
104+
105+ utf8ToString :: Array Word8 -> String
106+ utf8ToString =
107+ runIdentity . Stream.toList . Unicode.decodeUtf8' . Array.toStream
108+
109+ evPath = utf8ToString (Event.getAbsPath ev)
110+
111+toUtf8 :: MonadIO m => String -> m (Array Word8)
112+toUtf8 = Array.fromStream . Unicode.encodeUtf8' . Stream.fromList
113+
114+checkEvents :: EventWatcher -> EventChecker
115+checkEvents watcher rootPath targetPath mvar matchList = do
116+ putStrLn ("Watching on root [" <> rootPath
117+ <> "] for [" <> targetPath <> "]")
118+
119+ let matchList1 = fmap (first (targetPath </>)) matchList
120+ finder xs ev = filter (not . eventMatches ev) xs
121+
122+ paths <- mapM toUtf8 [rootPath]
123+ watcher (NonEmpty.fromList paths)
124+ & Stream.before (putMVar mvar ())
125+ & Stream.trace (putStrLn . Event.showEvent)
126+ & Stream.scanl' finder matchList1
127+ & Stream.takeWhile (not . null)
128+ & Stream.drain
129+
130+-------------------------------------------------------------------------------
131+-- Run tests
132+-------------------------------------------------------------------------------
133+
134+type TestDesc =
135+ ( String -- test description
136+ , FilePath -> IO () -- pre test operation, arg is path of the watch root.
137+ , FilePath -> IO () -- file system action, arg is path of the watch root.
138+ , [(String, Event -> Bool)] -- expected events (Absolute Path, predicate)
139+ )
140+
141+-- | Whether watch root is a symlink and if so then what's the behavior
142+data WatchRootType =
143+ DirType -- watch root is a directory
144+ | FileType -- watch root is a file
145+ | SymLinkResolvedPath -- symlink to a directory,
146+ -- Event contains path via the resolved dir
147+ | SymLinkOrigPath -- symlink to a directory,
148+ -- Event contains path via the original symlink
149+ deriving Show
150+
151+driver :: EventChecker -> WatchRootType -> TestDesc -> SpecWith ()
152+driver checker symlinkStyle (desc, pre, ops, expected) =
153+ it desc $ runOneTest `shouldReturn` ()
154+
155+ where
156+
157+ fseventDir :: String
158+ fseventDir = "fsevent_dir"
159+
160+ runOneTest = do
161+ sync <- newEmptyMVar
162+ withSystemTempDirectory fseventDir $ \fp -> do
163+ let root = fp </> "watch-root"
164+ target <-
165+ case symlinkStyle of
166+ DirType -> do
167+ createDirectory root
168+ return root
169+ FileType -> do
170+ openFile root WriteMode >>= hClose
171+ return root
172+ SymLinkResolvedPath -> do
173+ let tgt = fp </> "watch-root-real"
174+ createDirectory tgt
175+ createDirectoryLink tgt root
176+ return tgt
177+ SymLinkOrigPath -> do
178+ let tgt = fp </> "watch-root-real"
179+ createDirectory tgt
180+ createDirectoryLink tgt root
181+ return root
182+
183+ -- XXX On macOS we seem to get the watch root create events
184+ -- even though they occur before the watch is started. Even if
185+ -- we add a delay here.
186+ startWatchAndCheck root target sync
187+
188+ startWatchAndCheck root target sync = do
189+ pre root
190+ -- XXX On macOS the events from pre ops also seem to be bundled
191+ -- with the events occurred after the watch is started.
192+ let check = checker root target sync expected
193+ fsOps = Stream.fromEffect $ runFSOps root sync
194+ Stream.drain $ Stream.fromEffect check `Stream.parallelFst` fsOps
195+
196+ runFSOps fp sync = do
197+ -- We put the MVar before the event watcher starts to run but that does
198+ -- not ensure that the event watcher has actually started. So we need a
199+ -- delay as well. Do we?
200+ takeMVar sync >> threadDelay 200000
201+ ops fp
202+ threadDelay 10000000
203+ error "Time out occurred before event watcher could terminate"
204+
205+-------------------------------------------------------------------------------
206+-- Test descriptions
207+-------------------------------------------------------------------------------
208+
209+dirEvent, fileEvent :: (Event -> Bool) -> Event -> Bool
210+
211+#if defined(FILESYSTEM_EVENT_DARWIN)
212+dirEvent f ev = Event.isDir ev && f ev
213+fileEvent f ev = Event.isFile ev && f ev
214+#elif defined(FILESYSTEM_EVENT_LINUX)
215+dirEvent f ev = Event.isDir ev && f ev
216+fileEvent f ev = not (Event.isDir ev) && f ev
217+#else
218+dirEvent = id
219+fileEvent = id
220+#endif
221+
222+-- XXX Tests for root as a regular file instead of dir?
223+-- XXX Add symlink and hardlink tests
224+-- XXX Add exception condition tests
225+
226+-------------------------------------------------------------------------------
227+-- Dir tests
228+-------------------------------------------------------------------------------
229+
230+createParent :: FilePath -> FilePath -> IO ()
231+createParent file parent = do
232+ createDirectoryIfMissing True (parent </> takeDirectory file)
233+
234+createDirWithParent :: FilePath -> FilePath -> IO ()
235+createDirWithParent dir parent =
236+ when (not (null dir)) $ createDirectoryIfMissing True (parent </> dir)
237+
238+createDir :: FilePath -> FilePath -> IO ()
239+createDir dir parent =
240+ when (not (null dir)) $ createDirectory (parent </> dir)
241+
242+-- | Create the dir along with its parent dir during the test. This is
243+-- especially useful to detect a race in Linux case where the watch for a
244+-- directory is added in event processing, so if the the child directory is
245+-- created before the watch was installed then we may miss some events.
246+dirCreateWithParent ::
247+ String -> (String -> [([Char], Event -> Bool)]) -> TestDesc
248+dirCreateWithParent dir events =
249+ ( "dir created (" ++ dir ++ ")"
250+ , const (return ())
251+ , createDirWithParent dir
252+ , events dir
253+ )
254+
255+dirCreate :: String -> (String -> [([Char], Event -> Bool)]) -> TestDesc
256+dirCreate dir events =
257+ ( "dir created (" ++ dir ++ ")"
258+ , createParent dir
259+ , createDir dir
260+ , events dir
261+ )
262+
263+dirDelete :: String -> (String -> [([Char], Event -> Bool)]) -> TestDesc
264+dirDelete dir events =
265+ ( "dir deleted (" ++ dir ++ ")"
266+ , createDirWithParent dir
267+ , \fp -> removePathForcibly (fp </> dir)
268+ , events dir
269+ )
270+
271+dirMove ::
272+ String
273+ -> String
274+ -> (String -> String -> [(String, Event -> Bool)])
275+ -> TestDesc
276+dirMove dir1 dir2 events =
277+ ( "dir moved (" ++ dir1 ++ " " ++ dir2 ++ ")"
278+ , createDirWithParent dir1
279+ , \fp -> renameDirectory (fp </> dir1) (fp </> dir2)
280+ , events dir1 dir2
281+ )
282+
283+rootDirMove ::
284+ String
285+ -> (String -> [(String, Event -> Bool)])
286+ -> TestDesc
287+rootDirMove suffix events =
288+ ( "root dir moved" ++ "(" ++ suffix ++ ")"
289+ , const (return ())
290+ , \fp -> renameDirectory fp (fp <> suffix)
291+ , events ""
292+ )
293+
294+-------------------------------------------------------------------------------
295+-- File tests
296+-------------------------------------------------------------------------------
297+
298+createFileWithParent :: FilePath -> FilePath -> IO ()
299+createFileWithParent file parent = do
300+ when (not (null file)) $
301+ createDirectoryIfMissing True (parent </> takeDirectory file)
302+ openFile (parent </> file) WriteMode >>= hClose
303+
304+createFile :: FilePath -> FilePath -> IO ()
305+createFile file parent =
306+ openFile (parent </> file) WriteMode >>= hClose
307+
308+fileCreate :: String -> (String -> [(String, Event -> Bool)]) -> TestDesc
309+fileCreate file1 events =
310+ ( "File created (" ++ file1 ++ ")"
311+ , createParent file1
312+ , createFile file1
313+ , (file1, fileEvent Event.isCreated) : events file1
314+ )
315+
316+-- | See comments in dirCreateWithParent
317+fileCreateWithParent ::
318+ String -> (String -> [(String, Event -> Bool)]) -> TestDesc
319+fileCreateWithParent file1 events =
320+ ( "File created (" ++ file1 ++ ")"
321+ , const (return ())
322+ , createFileWithParent file1
323+ , (file1, fileEvent Event.isCreated) : events file1
324+ )
325+
326+fileDelete :: String -> (String -> [(String, Event -> Bool)]) -> TestDesc
327+fileDelete file1 events =
328+ ( "File deleted (" ++ file1 ++ ")"
329+ , createFileWithParent file1
330+ , \fp -> removeFile (fp </> file1)
331+ , events file1
332+ )
333+
334+fileModify :: String -> (String -> [(String, Event -> Bool)]) -> TestDesc
335+fileModify file1 events =
336+ ( "File modified (" ++ file1 ++ ")"
337+ , createFileWithParent file1
338+ , \fp -> writeFile (fp </> file1) "Test Data"
339+ , (file1, fileEvent Event.isModified) : events file1
340+ )
341+
342+fileMove ::
343+ String
344+ -> String
345+ -> (String -> String -> [(String, Event -> Bool)])
346+ -> TestDesc
347+fileMove file1 file2 events =
348+ ( "File moved (" ++ file1 ++ " " ++ file2 ++ ")"
349+ , createFileWithParent file1
350+ , \fp -> renamePath (fp </> file1) (fp </> file2)
351+ , (file1, fileEvent Event.isMoved)
352+ : (file2, fileEvent Event.isMoved)
353+ : events file1 file2
354+ )
355+
356+rootFileMove :: String -> (String -> [(String, Event -> Bool)]) -> TestDesc
357+rootFileMove suffix events =
358+ ( "File moved (" ++ suffix ++ ")"
359+ , const (return ())
360+ , \fp -> renamePath fp (fp <> suffix)
361+ , events ""
362+ )
363+
364+-------------------------------------------------------------------------------
365+-- Common test bundles
366+-------------------------------------------------------------------------------
367+
368+withParent :: [Char] -> [Char] -> [Char]
369+withParent parent file = if null parent then file else parent </> file
370+
371+testsWithParent :: String -> [TestDesc]
372+testsWithParent p =
373+ [ dirCreate
374+ (withParent p "dir") (\dir -> [(dir, dirEvent Event.isCreated)])
375+ , dirDelete
376+ (withParent p "dir") (\dir -> [(dir, dirEvent Event.isDeleted)])
377+ , dirMove
378+ (withParent p "dir1")
379+ (withParent p "dir2")
380+ (\dir1 dir2 ->
381+ [ (dir1, dirEvent Event.isMoved)
382+ , (dir2, dirEvent Event.isMoved)
383+ ]
384+ )
385+ , fileCreate (withParent p "file1") (const [])
386+ , fileDelete
387+ (withParent p "file1")
388+ (\file -> [(file, fileEvent Event.isDeleted)])
389+ , fileModify (withParent p "file1") (const [])
390+ , fileMove (withParent p "file1") (withParent p "file2") (\_ _ -> [])
391+ ]
392+
393+commonTests :: [TestDesc]
394+commonTests = testsWithParent ""
395+
396+commonRecTests :: [TestDesc]
397+commonRecTests = testsWithParent "subdir"
398+
399+-------------------------------------------------------------------------------
400+-- Running tests
401+-------------------------------------------------------------------------------
402+
403+runTests ::
404+ String
405+ -> String
406+ -> EventWatcher
407+ -> WatchRootType
408+ -> [TestDesc]
409+ -> IO ()
410+runTests modName watchType watcher rootType tests = do
411+ hSetBuffering stdout NoBuffering
412+ hspec
413+ $ describe modName
414+ $ describe watchType
415+ $ do
416+ let checker = checkEvents watcher
417+ describe ("Root type " ++ show rootType)
418+ $ mapM_ (driver checker rootType) tests
test/Streamly/Test/FileSystem/Event/Darwin.hsnew file+85
@@ -0,0 +1,85 @@
1+-- |
2+-- Module : Streamly.Test.FileSystem.Event.Darwin
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 Streamly.Test.FileSystem.Event.Darwin (main) where
10+
11+import qualified Streamly.Internal.FileSystem.Event.Darwin as Event
12+
13+import Streamly.Test.FileSystem.Event.Common
14+
15+moduleName :: String
16+moduleName = "FileSystem.Event.Darwin"
17+
18+-- TODO:
19+-- Creation/deletion/move of hard links
20+-- Deletion of last hard link
21+-- If a directory is a symlink is it followed in recursive mode?
22+
23+main :: IO ()
24+main = do
25+ let symLinkRootTests =
26+ dirCreateWithParent "subdir/dir"
27+ (\dir -> [(dir, dirEvent Event.isCreated)])
28+ : fileCreateWithParent "subdir/file"
29+ (\file ->
30+ [ (file, fileEvent Event.isCreated)
31+ , (file, fileEvent Event.isAttrsModified)
32+ ]
33+ )
34+ : fileCreate "file-create-attrs"
35+ (\file ->
36+ [ (file, fileEvent Event.isCreated)
37+ , (file, fileEvent Event.isAttrsModified)
38+ ]
39+ )
40+ : fileModify
41+ "file-mod-attrs"
42+ (\file -> [(file, fileEvent Event.isAttrsModified)])
43+ : dirDelete "dir-sec" (\dir ->
44+ [ (dir, dirEvent Event.isDeleted)
45+ , (dir, dirEvent Event.isSecurityModified)
46+ ]
47+ )
48+ : commonTests
49+ ++ commonRecTests
50+ let regularRootTests =
51+ dirDelete "" (\dir ->
52+ [ (dir, dirEvent Event.isDeleted)
53+ , (dir, dirEvent Event.isSecurityModified)
54+ , (dir, Event.isRootPathEvent)
55+ ]
56+ )
57+ -- The watch root create event always seems to come even though the
58+ -- root is created before the watch is started. That may be because
59+ -- of batching?
60+ -- XXX Need to create watch root after adding the watch
61+ : dirCreate "" (\dir -> [(dir, dirEvent Event.isCreated)])
62+ : rootDirMove "moved" (\src ->
63+ [ (src, Event.isRootPathEvent)
64+ , (src, Event.isMoved)
65+ ])
66+ : symLinkRootTests
67+
68+ let fileRootTests =
69+ [ fileDelete "" (\path ->
70+ [ (path, Event.isRootPathEvent)
71+ , (path, Event.isDeleted)
72+ ])
73+ , rootFileMove "moved" (\path ->
74+ [(path, Event.isRootPathEvent)
75+ , (path, Event.isMoved)
76+ ])
77+ , fileModify "" (const [])
78+ ]
79+
80+ let w = Event.watchWith (Event.setAllEvents Event.On)
81+ run = runTests moduleName "recursive" w
82+
83+ run DirType regularRootTests
84+ run SymLinkResolvedPath symLinkRootTests
85+ run FileType fileRootTests
test/Streamly/Test/FileSystem/Event/Linux.hs+132−812
@@ -6,827 +6,147 @@
66-- Stability : experimental
77-- Portability : GHC
88--
9−module Streamly.Test.FileSystem.Event.Linux (testAllEvents, testCommonEvents) where
9+module Streamly.Test.FileSystem.Event.Linux (main) where
1010
11−import Control.Concurrent (MVar, newEmptyMVar, putMVar, takeMVar, threadDelay)
12−import Control.Monad.IO.Class (MonadIO)
13−import Data.Char (ord)
14−import Data.Functor.Identity (runIdentity)
15−import Data.List.NonEmpty (NonEmpty)
16−import Data.Maybe (fromJust)
17−import Data.Word (Word8)
18−import System.Directory
19− ( createDirectoryIfMissing
20− , createDirectoryLink
21− , removeFile
22− , removeDirectory
23− , removePathForcibly
24− , renameDirectory
25− , renamePath
26− )
27−import System.FilePath ((</>))
28−import System.IO.Temp (withSystemTempDirectory)
29−import System.IO.Unsafe (unsafePerformIO)
30−import Streamly.Internal.Data.Array.Foreign (Array)
31−
32−import qualified Data.List.NonEmpty as NonEmpty
33−import qualified Data.Set as Set
34−import qualified Streamly.Unicode.Stream as Unicode
35−import qualified Streamly.Internal.Data.Array.Foreign as Array
36−import qualified Streamly.Internal.Data.Fold as FL
37−import qualified Streamly.Internal.Data.Parser as PR
38−import qualified Streamly.Internal.Data.Stream.IsStream as S
39−import qualified Streamly.Internal.Unicode.Stream as U
11+import Streamly.Internal.FileSystem.Event.Linux (Event)
4012import qualified Streamly.Internal.FileSystem.Event.Linux as Event
4113
42−import Test.Hspec
43−
44−-------------------------------------------------------------------------------
45−-- Utilities
46−-------------------------------------------------------------------------------
47−
48−toUtf8 :: MonadIO m => String -> m (Array Word8)
49−toUtf8 = Array.fromStream . Unicode.encodeUtf8' . S.fromList
50−
51−utf8ToString :: Array Word8 -> String
52−utf8ToString = runIdentity . S.toList . U.decodeUtf8' . Array.toStream
53−
54−timeout :: IO String
55−timeout = threadDelay 5000000 >> return "Timeout"
56−
57−fseventDir :: String
58−fseventDir = "fsevent_dir"
59−
60−eoTask :: String
61−eoTask = "EOTask"
62−
63−-- XXX Make the getRelPath type same on windows and other platforms
64−eventPredicate :: Event.Event -> Bool
65−eventPredicate ev =
66− if (utf8ToString $ Event.getRelPath ev) == eoTask
67− then False
68− else True
69−
70−-------------------------------------------------------------------------------
71−-- Event matching utilities
72−-------------------------------------------------------------------------------
73−
74−removeTrailingSlash :: Array Word8 -> Array Word8
75−removeTrailingSlash path =
76− if Array.length path == 0
77− then path
78− else
79− let mx = Array.getIndex path (Array.length path - 1)
80− in case mx of
81− Nothing -> error "removeTrailingSlash: Bug: Invalid index"
82− Just x ->
83− if x == fromIntegral (ord '/')
84− -- XXX need array slicing
85− then unsafePerformIO
86− $ Array.fromStreamN (Array.length path - 1)
87− $ Array.toStream path
88− else path
89−
90−
91−-- XXX Return a tuple (path, flags) instead of appending flags to path. And
92−-- then check the flags using an event mask.
93−
94−showEventShort :: Event.Event -> String
95−showEventShort ev@Event.Event{..} =
96− utf8ToString (removeTrailingSlash $ Event.getRelPath ev)
97− ++ "_" ++ show eventFlags
98− -- ++ showev Event.isDir "DirX"
99− ++ showev Event.isEventsLost "Overflow"
100−
101− ++ showev Event.isRootUnwatched "RootUnwatched"
102− ++ showev Event.isRootDeleted "RootDeleted"
103− ++ showev Event.isRootMoved "RootMoved"
104− ++ showev Event.isRootUnmounted "RootUnmounted"
105−
106− ++ showev Event.isMetadataChanged "MetadataChanged"
107−
108− ++ showev Event.isAccessed "Accessed"
109− ++ showev Event.isOpened "Opened"
110− ++ showev Event.isWriteClosed "WriteClosed"
111− ++ showev Event.isNonWriteClosed "NonWriteClosed"
112−
113− ++ showev Event.isCreated "Created"
114− ++ showev Event.isDeleted "Deleted"
115− ++ showev Event.isModified "Modified"
116− ++ showev Event.isMovedFrom "MovedFrom"
117− ++ showev Event.isMovedTo "MovedTo"
118− ++ showev Event.isDir "Dir"
119−
120− where showev f str = if f ev then "_" ++ str else ""
121−
122−type EventChecker = FilePath -> MVar () -> [String] -> IO String
123−type EventWatch = NonEmpty (Array Word8) -> S.SerialT IO Event.Event
124−type ToEventList = S.SerialT IO Event.Event -> IO [Event.Event]
125−
126−
127−
128−
129−-------------------------------------------------------------------------------
130−-- Event Watcher
131−-------------------------------------------------------------------------------
132−
133−eventListWithFixLen :: Int -> ToEventList
134−eventListWithFixLen count = S.toList . S.take count --51/20
135−
136−eventListWithEOtask :: ToEventList
137−eventListWithEOtask = S.parse (PR.takeWhile eventPredicate FL.toList)
138−
139−checkEvents :: ToEventList -> EventWatch -> EventChecker
140−checkEvents toEL ew rootPath m matchList = do
141− let args = [rootPath]
142− paths <- mapM toUtf8 args
143− putStrLn ("Watch started !!!! on Path " ++ rootPath)
144− events <- toEL
145− $ S.before (putMVar m ())
146− $ ew (NonEmpty.fromList paths)
147− let eventStr = map showEventShort events
148− let baseSet = Set.fromList matchList
149− resultSet = Set.fromList eventStr
150− if baseSet `Set.isSubsetOf` resultSet
151− then
152− return "PASS"
153− else do
154− putStrLn $ "baseSet " ++ show matchList
155− putStrLn $ "resultSet " ++ show eventStr
156− return "Mismatch"
157−
158−-------------------------------------------------------------------------------
159−-- FS Event Generators
160−-------------------------------------------------------------------------------
161−
162−checker :: S.IsStream t =>
163− EventChecker -> FilePath -> MVar () -> [String] -> t IO String
164−checker ec rootPath synch matchList =
165− S.fromEffect (ec rootPath synch matchList)
166− `S.parallelFst`
167− S.fromEffect timeout
168−
169−-------------------------------------------------------------------------------
170−-- Test Drivers
171−-------------------------------------------------------------------------------
172−
173−driver ::
174− EventChecker
175− -> ( String
176− , FilePath -> IO ()
177− , FilePath -> IO ()
178− , [String]
179− , Bool
180− )
181− -> SpecWith ()
182−driver ec (desc, pre, ops, events, sym) = it desc $ runTest `shouldReturn` "PASS"
183−
184− where
14+import Streamly.Test.FileSystem.Event.Common
18515
186− runTest = do
187− sync <- newEmptyMVar
188− withSystemTempDirectory fseventDir $ \fp ->
189− if sym
190− then do
191− createDirectoryLink fp (fp ++ "SymLink")
192− runTest0 (fp ++ "SymLink") sync
193− else runTest0 fp sync
194−
195− runTest0 fp sync = do
196− pre fp
197− let eventStream = checker ec fp sync events
198− fsOps = S.fromEffect $ runFSOps fp sync
199− fmap fromJust $ S.head $ eventStream `S.parallelFst` fsOps
200−
201− runFSOps fp sync = do
202− _ <- takeMVar sync
203− threadDelay 200000
204− ops fp
205− -- 'EOTask Created dir' event gets out of order so need to wait here
206− threadDelay 200000 -- Why this delay?
207− createDirectoryIfMissing True (fp </> eoTask)
208− threadDelay 10000000
209− error "fs ops timed out"
210−
211−-------------------------------------------------------------------------------
212−-- Main
213−-------------------------------------------------------------------------------
16+moduleName :: String
17+moduleName = "FileSystem.Event.Linux"
21418
215−testDesc,
216− testDescRemoveRootDir,
217− testDescRemoveRootDirSymLink,
218− testDescMoveRootDir,
219− testDescCommon,
220− testDescMoveRootDirCommon,
221− testDescRemoveRootDirCommon,
222− testDescRemoveRootDirSymLinkCommon ::
223− [ ( String -- test description
224− , FilePath -> IO () -- pre test operation
225− , FilePath -> IO () -- file system actions
226− , [String] -- expected events
227− , Bool ) -- SymLink
19+dirTouchEvents :: String -> [([Char], Event -> Bool)]
20+dirTouchEvents dir =
21+ [ (dir, dirEvent Event.isOpened)
22+ , (dir, dirEvent Event.isAccessed)
23+ , (dir, dirEvent Event.isNonWriteClosed)
22824 ]
229−testDesc =
230− [
231− ( "Create a single directory"
232− , const (return ())
233− , \fp -> createDirectoryIfMissing True (fp </> "dir1Single")
234− , [ "_1073741856_Opened_Dir"
235− , "_1073741825_Accessed_Dir"
236− , "_1073741840_NonWriteClosed_Dir"
237− , "dir1Single_1073742080_Created_Dir"
238− , "dir1Single_1073741856_Opened_Dir"
239− , "dir1Single_1073741825_Accessed_Dir"
240− , "dir1Single_1073741840_NonWriteClosed_Dir"
241− ]
242− , False
243− )
244− , ( "Remove a single directory"
245− , \fp -> createDirectoryIfMissing True (fp </> "dir1Single")
246− , \fp -> removeDirectory (fp </> "dir1Single")
247− , [ "_1073741856_Opened_Dir"
248− , "_1073741825_Accessed_Dir"
249− , "_1073741840_NonWriteClosed_Dir"
250− , "dir1Single_1073741856_Opened_Dir"
251− , "dir1Single_1073741825_Accessed_Dir"
252− , "dir1Single_1073741840_NonWriteClosed_Dir"
253− , "dir1Single_1024_RootDeleted"
254− , "dir1Single_32768_RootUnwatched"
255− , "dir1Single_1073742336_Deleted_Dir"
256− ]
257− , False
258− )
259− , ( "Rename a single directory"
260− , \fp -> createDirectoryIfMissing True (fp </> "dir1Single")
261− , \fp ->
262− let spath = fp </> "dir1Single"
263− tpath = fp </> "dir1SingleRenamed"
264− in renameDirectory spath tpath
265− , [ "_1073741856_Opened_Dir"
266− , "_1073741825_Accessed_Dir"
267− , "_1073741840_NonWriteClosed_Dir"
268− , "dir1Single_1073741856_Opened_Dir"
269− , "dir1Single_1073741825_Accessed_Dir"
270− , "dir1Single_1073741840_NonWriteClosed_Dir"
271− , "dir1Single_1073741888_MovedFrom_Dir"
272− , "dir1SingleRenamed_1073741952_MovedTo_Dir"
273− , "dir1Single_2048_RootMoved"
274− ]
275− , False
276− )
277− , ( "Create a nested directory"
278− , const (return ())
279− , \fp ->
280− createDirectoryIfMissing True (fp </> "dir1" </> "dir2" </> "dir3")
281− , [ "_1073741856_Opened_Dir"
282− , "_1073741825_Accessed_Dir"
283− , "_1073741840_NonWriteClosed_Dir"
284− , "dir1_1073742080_Created_Dir"
285− , "dir1_1073741856_Opened_Dir"
286− , "dir1_1073741825_Accessed_Dir"
287− , "dir1_1073741840_NonWriteClosed_Dir"
288− , "dir1/dir2_1073741856_Opened_Dir"
289− , "dir1/dir2_1073741825_Accessed_Dir"
290− , "dir1/dir2_1073741840_NonWriteClosed_Dir"
291− , "dir1/dir2/dir3_1073741856_Opened_Dir"
292− , "dir1/dir2/dir3_1073741825_Accessed_Dir"
293− , "dir1/dir2/dir3_1073741840_NonWriteClosed_Dir"
294− ]
295− , False
296− )
297− , ( "Rename a nested directory"
298− , \fp -> createDirectoryIfMissing True
299− (fp </> "dir1" </> "dir2" </> "dir3")
300− , \fp ->
301− let spath = fp </> "dir1" </> "dir2" </> "dir3"
302− tpath = fp </> "dir1" </> "dir2" </> "dir3Renamed"
303− in renameDirectory spath tpath
304− , [ "_1073741856_Opened_Dir"
305− , "_1073741825_Accessed_Dir"
306− , "_1073741840_NonWriteClosed_Dir"
307− , "dir1_1073741856_Opened_Dir"
308− , "dir1_1073741825_Accessed_Dir"
309− , "dir1_1073741840_NonWriteClosed_Dir"
310− , "dir1/dir2_1073741856_Opened_Dir"
311− , "dir1/dir2_1073741825_Accessed_Dir"
312− , "dir1/dir2_1073741840_NonWriteClosed_Dir"
313− , "dir1/dir2/dir3_1073741856_Opened_Dir"
314− , "dir1/dir2/dir3_1073741825_Accessed_Dir"
315− , "dir1/dir2/dir3_1073741840_NonWriteClosed_Dir"
316− , "dir1/dir2/dir3_1073741888_MovedFrom_Dir"
317− , "dir1/dir2/dir3Renamed_1073741952_MovedTo_Dir"
318− , "dir1/dir2/dir3_2048_RootMoved"
319− ]
320− , False
321− )
322− , ( "Create a file in root Dir"
323− , const (return ())
324− , \fp -> writeFile (fp </> "FileCreated.txt") "Test Data"
325− , [ "_1073741856_Opened_Dir"
326− , "_1073741825_Accessed_Dir"
327− , "_1073741840_NonWriteClosed_Dir"
328− , "FileCreated.txt_256_Created"
329− , "FileCreated.txt_32_Opened"
330− , "FileCreated.txt_2_Modified"
331− , "FileCreated.txt_8_WriteClosed"
332− ]
333− , False
334− )
335− , ( "Remove a file in root Dir"
336− , \fp -> writeFile (fp </> "FileCreated.txt") "Test Data"
337− , \fp -> removeFile (fp </> "FileCreated.txt")
338− , [ "_1073741856_Opened_Dir"
339− , "_1073741825_Accessed_Dir"
340− , "_1073741840_NonWriteClosed_Dir"
341− , "FileCreated.txt_512_Deleted"
342− ]
343− , False
344− )
345− , ( "Rename a file in root Dir"
346− , \fp -> writeFile (fp </> "FileCreated.txt") "Test Data"
347− , \fp ->
348− let spath = (fp </> "FileCreated.txt")
349− tpath = (fp </> "FileRenamed.txt")
350− in renamePath spath tpath
351− , [ "_1073741856_Opened_Dir"
352− , "_1073741825_Accessed_Dir"
353− , "_1073741840_NonWriteClosed_Dir"
354− , "FileCreated.txt_64_MovedFrom"
355− , "FileRenamed.txt_128_MovedTo"
356− ]
357− , False
358− )
359− , ( "Create a file in a nested Dir"
360− , \fp ->
361− createDirectoryIfMissing True (fp </> "dir1" </> "dir2" </> "dir3")
362− , \fp ->
363− let p = fp </> "dir1" </> "dir2" </> "dir3" </> "FileCreated.txt"
364− in writeFile p "Test Data"
365− , [ "_1073741856_Opened_Dir"
366− , "_1073741825_Accessed_Dir"
367− , "_1073741840_NonWriteClosed_Dir"
368− , "dir1_1073741856_Opened_Dir"
369− , "dir1_1073741825_Accessed_Dir"
370− , "dir1_1073741840_NonWriteClosed_Dir"
371− , "dir1/dir2_1073741856_Opened_Dir"
372− , "dir1/dir2_1073741825_Accessed_Dir"
373− , "dir1/dir2_1073741840_NonWriteClosed_Dir"
374− , "dir1/dir2/dir3_1073741856_Opened_Dir"
375− , "dir1/dir2/dir3_1073741825_Accessed_Dir"
376− , "dir1/dir2/dir3_1073741840_NonWriteClosed_Dir"
377− , "dir1/dir2/dir3/FileCreated.txt_256_Created"
378− , "dir1/dir2/dir3/FileCreated.txt_32_Opened"
379− , "dir1/dir2/dir3/FileCreated.txt_2_Modified"
380− , "dir1/dir2/dir3/FileCreated.txt_8_WriteClosed"
381− ]
382− , False
383− )
384− , ( "Remove a file in a nested Dir"
385− , \fp ->
386− let nestedDir = fp </> "dir1" </> "dir2" </> "dir3"
387− fpath = nestedDir </> "FileCreated.txt"
388− in do
389− createDirectoryIfMissing True nestedDir
390− writeFile fpath "Test Data"
391− , \fp ->
392− let p = fp </> "dir1" </> "dir2" </> "dir3" </> "FileCreated.txt"
393− in removeFile p
394− , [ "_1073741856_Opened_Dir"
395− , "_1073741825_Accessed_Dir"
396− , "_1073741840_NonWriteClosed_Dir"
397− , "dir1_1073741856_Opened_Dir"
398− , "dir1_1073741825_Accessed_Dir"
399− , "dir1_1073741840_NonWriteClosed_Dir"
400− , "dir1/dir2_1073741856_Opened_Dir"
401− , "dir1/dir2_1073741825_Accessed_Dir"
402− , "dir1/dir2_1073741840_NonWriteClosed_Dir"
403− , "dir1/dir2/dir3_1073741856_Opened_Dir"
404− , "dir1/dir2/dir3_1073741825_Accessed_Dir"
405− , "dir1/dir2/dir3_1073741840_NonWriteClosed_Dir"
406− , "dir1/dir2/dir3/FileCreated.txt_512_Deleted"
407− ]
408− , False
409− )
410− , ( "Rename a file in a nested Dir"
411− , \fp ->
412− let nestedDir = fp </> "dir1" </> "dir2" </> "dir3"
413− fpath = nestedDir </> "FileCreated.txt"
414− in do
415− createDirectoryIfMissing True nestedDir
416− writeFile fpath "Test Data"
417− , \fp ->
418− let s = (fp </> "dir1" </> "dir2" </> "dir3" </> "FileCreated.txt")
419− t = (fp </> "dir1" </> "dir2" </> "dir3" </> "FileRenamed.txt")
420− in renamePath s t
421− , [ "_1073741856_Opened_Dir"
422− , "_1073741825_Accessed_Dir"
423− , "_1073741840_NonWriteClosed_Dir"
424− , "dir1_1073741856_Opened_Dir"
425− , "dir1_1073741825_Accessed_Dir"
426− , "dir1_1073741840_NonWriteClosed_Dir"
427− , "dir1/dir2_1073741856_Opened_Dir"
428− , "dir1/dir2_1073741825_Accessed_Dir"
429− , "dir1/dir2_1073741840_NonWriteClosed_Dir"
430− , "dir1/dir2/dir3_1073741856_Opened_Dir"
431− , "dir1/dir2/dir3_1073741825_Accessed_Dir"
432− , "dir1/dir2/dir3_1073741840_NonWriteClosed_Dir"
433− , "dir1/dir2/dir3/FileCreated.txt_64_MovedFrom"
434− , "dir1/dir2/dir3/FileRenamed.txt_128_MovedTo"
435− ]
436− , False
437− )
438− , ( "Remove the nested directory"
439− , \fp ->
440− createDirectoryIfMissing True (fp </> "dir1" </> "dir2" </> "dir3")
441− , \fp -> removePathForcibly (fp </> "dir1")
44225
443− , [ "_1073741856_Opened_Dir"
444− , "_1073741825_Accessed_Dir"
445− , "_1073741840_NonWriteClosed_Dir"
446− , "dir1_1073741856_Opened_Dir"
447− , "dir1_1073741825_Accessed_Dir"
448− , "dir1_1073741840_NonWriteClosed_Dir"
449− , "dir1/dir2_1073741856_Opened_Dir"
450− , "dir1/dir2_1073741825_Accessed_Dir"
451− , "dir1/dir2_1073741840_NonWriteClosed_Dir"
452− , "dir1/dir2/dir3_1073741856_Opened_Dir"
453− , "dir1/dir2/dir3_1073741825_Accessed_Dir"
454− , "dir1/dir2/dir3_1073741840_NonWriteClosed_Dir"
455− , "dir1_1073741828_MetadataChanged_Dir"
456− , "dir1/dir2_1073741828_MetadataChanged_Dir"
457− , "dir1/dir2/dir3_1073741828_MetadataChanged_Dir"
458− , "dir1/dir2/dir3_1024_RootDeleted"
459− , "dir1/dir2/dir3_32768_RootUnwatched"
460− , "dir1/dir2/dir3_1073742336_Deleted_Dir"
461− , "dir1/dir2_1024_RootDeleted"
462− , "dir1/dir2_32768_RootUnwatched"
463− , "dir1/dir2_1073742336_Deleted_Dir"
464− , "dir1_1024_RootDeleted"
465− , "dir1_32768_RootUnwatched"
466− , "dir1_1073742336_Deleted_Dir"
467− ]
468− , False
469− )
26+dirDelEvents :: String -> [([Char], Event -> Bool)]
27+dirDelEvents dir =
28+ (dir, dirEvent Event.isDeleted)
29+ : (dir, dirEvent Event.isAttrsModified)
30+ : dirTouchEvents dir
31+
32+rootDirDelEvents :: String -> [([Char], Event -> Bool)]
33+rootDirDelEvents root =
34+ (root, Event.isRootUnwatched)
35+ : (root, Event.isRootDeleted)
36+ : (root, dirEvent Event.isAttrsModified)
37+ : dirTouchEvents root
38+
39+dirMoveEvents :: [Char] -> [Char] -> [([Char], Event -> Bool)]
40+dirMoveEvents src dst =
41+ [ (src, dirEvent Event.isMoved)
42+ , (src, dirEvent Event.isMovedFrom)
43+ , (dst, dirEvent Event.isMoved)
44+ , (dst, dirEvent Event.isMovedTo)
47045 ]
47146
472−testDescRemoveRootDir =
473− [ ( "Remove the root directory"
474− , \fp ->
475− createDirectoryIfMissing True (fp </> "dir1" </> "dir2")
476− , \fp -> removePathForcibly fp
477− , [ "_1073741856_Opened_Dir"
478− , "_1073741825_Accessed_Dir"
479− , "_1073741840_NonWriteClosed_Dir"
480− , "dir1_1073741856_Opened_Dir"
481− , "dir1_1073741825_Accessed_Dir"
482− , "dir1_1073741840_NonWriteClosed_Dir"
483− , "dir1/dir2_1073741856_Opened_Dir"
484− , "dir1/dir2_1073741825_Accessed_Dir"
485− , "dir1/dir2_1073741840_NonWriteClosed_Dir"
486− , "_1073741828_MetadataChanged_Dir"
487− , "dir1_1073741828_MetadataChanged_Dir"
488− , "dir1/dir2_1073741828_MetadataChanged_Dir"
489− , "dir1/dir2_1024_RootDeleted"
490− , "dir1/dir2_32768_RootUnwatched"
491− , "dir1/dir2_1073742336_Deleted_Dir"
492− , "dir1_1024_RootDeleted"
493− , "dir1_32768_RootUnwatched"
494− , "dir1_1073742336_Deleted_Dir"
495− , "_1024_RootDeleted"
496− ]
497− , False
498− )
499− ]
500−
501−testDescMoveRootDir =
502− [
503− ( "Moved the root directory"
504− , \fp ->
505− createDirectoryIfMissing True (fp </> "dir1" </> "dir2")
506− , \fp -> renameDirectory (fp </> "dir1" </> "dir2") (fp </> "dir1" </> "dir2_Moved")
507− , [ "_1073741856_Opened_Dir"
508− ,"_1073741825_Accessed_Dir"
509− ,"_1073741840_NonWriteClosed_Dir"
510− ,"dir1_1073741856_Opened_Dir"
511− ,"dir1_1073741825_Accessed_Dir"
512− ,"dir1_1073741840_NonWriteClosed_Dir"
513− ,"dir1/dir2_1073741856_Opened_Dir"
514− ,"dir1/dir2_1073741825_Accessed_Dir"
515− ,"dir1/dir2_1073741840_NonWriteClosed_Dir"
516− ,"dir1/dir2_1073741888_MovedFrom_Dir"
517− ,"dir1/dir2_Moved_1073741952_MovedTo_Dir"
518− ,"dir1/dir2_2048_RootMoved"
519− ]
520− , False
521− )
522− ]
523−
524−testDescRemoveRootDirSymLink =
525− [ ( "Remove the root directory as SymLink"
526− , \fp ->
527− createDirectoryIfMissing True (fp </> "dir1" </> "dir2")
528− , \fp -> removePathForcibly fp
529− , [ "_1073741856_Opened_Dir"
530− , "_1073741825_Accessed_Dir"
531− , "_1073741840_NonWriteClosed_Dir"
532− , "dir1_1073741856_Opened_Dir"
533− , "dir1_1073741825_Accessed_Dir"
534− , "dir1_1073741840_NonWriteClosed_Dir"
535− , "dir1/dir2_1073741856_Opened_Dir"
536− , "dir1/dir2_1073741825_Accessed_Dir"
537− , "dir1/dir2_1073741840_NonWriteClosed_Dir"
538− , "_1073741828_MetadataChanged_Dir"
539− ]
540− , True
541− )
542− ]
543−
544−-------------------------Test Descriptor for common Event Types ---------------
545−
546−testDescCommon =
547− [
548− ( "Create a single directory"
549− , const (return ())
550− , \fp -> createDirectoryIfMissing True (fp </> "dir1Single")
551− , ["dir1Single_1073742080_Created_Dir"]
552− , False
553− )
554− , ( "Remove a single directory"
555− , \fp -> createDirectoryIfMissing True (fp </> "dir1Single")
556− , \fp -> removeDirectory (fp </> "dir1Single")
557− , [ "dir1Single_1024_RootDeleted"
558− , "dir1Single_32768_RootUnwatched"
559− , "dir1Single_1073742336_Deleted_Dir"
560− ]
561− , False
562− )
563− , ( "Rename a single directory"
564− , \fp -> createDirectoryIfMissing True (fp </> "dir1Single")
565− , \fp ->
566− let spath = fp </> "dir1Single"
567− tpath = fp </> "dir1SingleRenamed"
568− in renameDirectory spath tpath
569− , [ "dir1Single_1073741888_MovedFrom_Dir"
570− , "dir1SingleRenamed_1073741952_MovedTo_Dir"
571− , "dir1Single_2048_RootMoved"
572− ]
573− , False
574− )
575− , ( "Create a nested directory"
576− , const (return ())
577− , \fp ->
578− createDirectoryIfMissing True (fp </> "dir1" </> "dir2" </> "dir3")
579− , ["dir1_1073742080_Created_Dir"]
580− , False
581− )
582− , ( "Rename a nested directory"
583− , \fp -> createDirectoryIfMissing True
584− (fp </> "dir1" </> "dir2" </> "dir3")
585− , \fp ->
586− let spath = fp </> "dir1" </> "dir2" </> "dir3"
587− tpath = fp </> "dir1" </> "dir2" </> "dir3Renamed"
588− in renameDirectory spath tpath
589− , [ "dir1/dir2/dir3_1073741888_MovedFrom_Dir"
590− , "dir1/dir2/dir3Renamed_1073741952_MovedTo_Dir"
591− , "dir1/dir2/dir3_2048_RootMoved"
592− ]
593− , False
594− )
595− , ( "Create a file in root Dir"
596− , const (return ())
597− , \fp -> writeFile (fp </> "FileCreated.txt") "Test Data"
598− , [ "FileCreated.txt_256_Created"
599− , "FileCreated.txt_2_Modified"
600− ]
601− , False
602− )
603− , ( "Remove a file in root Dir"
604− , \fp -> writeFile (fp </> "FileCreated.txt") "Test Data"
605− , \fp -> removeFile (fp </> "FileCreated.txt")
606− , ["FileCreated.txt_512_Deleted"]
607− , False
608− )
609− , ( "Rename a file in root Dir"
610− , \fp -> writeFile (fp </> "FileCreated.txt") "Test Data"
611− , \fp ->
612− let spath = (fp </> "FileCreated.txt")
613− tpath = (fp </> "FileRenamed.txt")
614− in renamePath spath tpath
615− , [ "FileCreated.txt_64_MovedFrom"
616− , "FileRenamed.txt_128_MovedTo"
617− ]
618− , False
619− )
620− , ( "Create a file in a nested Dir"
621− , \fp ->
622− createDirectoryIfMissing True (fp </> "dir1" </> "dir2" </> "dir3")
623− , \fp ->
624− let p = fp </> "dir1" </> "dir2" </> "dir3" </> "FileCreated.txt"
625− in writeFile p "Test Data"
626− , [ "dir1/dir2/dir3/FileCreated.txt_256_Created"
627− , "dir1/dir2/dir3/FileCreated.txt_2_Modified"
628− ]
629− , False
630− )
631− , ( "Remove a file in a nested Dir"
632− , \fp ->
633− let nestedDir = fp </> "dir1" </> "dir2" </> "dir3"
634− fpath = nestedDir </> "FileCreated.txt"
635− in do
636− createDirectoryIfMissing True nestedDir
637− writeFile fpath "Test Data"
638− , \fp ->
639− let p = fp </> "dir1" </> "dir2" </> "dir3" </> "FileCreated.txt"
640− in removeFile p
641− , ["dir1/dir2/dir3/FileCreated.txt_512_Deleted"]
642− , False
643− )
644− , ( "Rename a file in a nested Dir"
645− , \fp ->
646− let nestedDir = fp </> "dir1" </> "dir2" </> "dir3"
647− fpath = nestedDir </> "FileCreated.txt"
648− in do
649− createDirectoryIfMissing True nestedDir
650− writeFile fpath "Test Data"
651− , \fp ->
652− let s = (fp </> "dir1" </> "dir2" </> "dir3" </> "FileCreated.txt")
653− t = (fp </> "dir1" </> "dir2" </> "dir3" </> "FileRenamed.txt")
654− in renamePath s t
655− , [ "dir1/dir2/dir3/FileCreated.txt_64_MovedFrom"
656− , "dir1/dir2/dir3/FileRenamed.txt_128_MovedTo"
657− ]
658− , False
659− )
660− , ( "Remove the nested directory"
661− , \fp ->
662− createDirectoryIfMissing True (fp </> "dir1" </> "dir2" </> "dir3")
663− , \fp -> removePathForcibly (fp </> "dir1")
664−
665− , [ "dir1_1073741828_MetadataChanged_Dir"
666− , "dir1/dir2_1073741828_MetadataChanged_Dir"
667− , "dir1/dir2/dir3_1073741828_MetadataChanged_Dir"
668− , "dir1/dir2/dir3_1024_RootDeleted"
669− , "dir1/dir2/dir3_32768_RootUnwatched"
670− , "dir1/dir2/dir3_1073742336_Deleted_Dir"
671− , "dir1/dir2_1024_RootDeleted"
672− , "dir1/dir2_32768_RootUnwatched"
673− , "dir1/dir2_1073742336_Deleted_Dir"
674− , "dir1_1024_RootDeleted"
675− , "dir1_32768_RootUnwatched"
676− , "dir1_1073742336_Deleted_Dir"
677− ]
678− , False
679− )
47+-- In recursive mode all subdirectories are roots therefore they will generate
48+-- isRootMoved.
49+rootDirMoveEvents :: [Char] -> [Char] -> [([Char], Event -> Bool)]
50+rootDirMoveEvents root _ =
51+ (root, Event.isRootMoved)
52+ : dirTouchEvents root
53+
54+recDirMoveEvents :: [Char] -> [Char] -> [([Char], Event -> Bool)]
55+recDirMoveEvents src dst = dirMoveEvents src dst ++ rootDirMoveEvents src dst
56+
57+fileTouchEvents :: String -> [([Char], Event -> Bool)]
58+fileTouchEvents file =
59+ [ (file, fileEvent Event.isOpened)
60+ , (file, fileEvent Event.isModified)
61+ , (file, fileEvent Event.isWriteClosed)
68062 ]
68163
682−testDescRemoveRootDirCommon =
683− [ ( "Remove the root directory"
684− , \fp ->
685− createDirectoryIfMissing True (fp </> "dir1" </> "dir2")
686− , \fp -> removePathForcibly fp
687− , [ "_1073741828_MetadataChanged_Dir"
688− , "dir1_1073741828_MetadataChanged_Dir"
689− , "dir1/dir2_1073741828_MetadataChanged_Dir"
690− , "dir1/dir2_1024_RootDeleted"
691− , "dir1/dir2_32768_RootUnwatched"
692− , "dir1/dir2_1073742336_Deleted_Dir"
693− , "dir1_1024_RootDeleted"
694− , "dir1_32768_RootUnwatched"
695− , "dir1_1073742336_Deleted_Dir"
696− , "_1024_RootDeleted"
697− ]
698− , False
699− )
700− ]
701−
702−testDescMoveRootDirCommon =
703− [
704− ( "Moved the root directory"
705− , \fp ->
706− createDirectoryIfMissing True (fp </> "dir1" </> "dir2")
707− , \fp ->
708− renameDirectory
709− (fp </> "dir1" </> "dir2")
710− (fp </> "dir1" </> "dir2_Moved")
711− , [ "dir1/dir2_1073741888_MovedFrom_Dir"
712− , "dir1/dir2_Moved_1073741952_MovedTo_Dir"
713− , "dir1/dir2_2048_RootMoved"
714− ]
715− , False
716− )
717− ]
718−
719−testDescRemoveRootDirSymLinkCommon =
720− [ ( "Remove the root directory as SymLink"
721− , \fp ->
722− createDirectoryIfMissing True (fp </> "dir1" </> "dir2")
723− , \fp -> removePathForcibly fp
724− , ["_1073741828_MetadataChanged_Dir"]
725− , True
726− )
727− ]
728−-------------------------------------------------------------------------------
729−moduleName :: String
730−moduleName = "FileSystem.Event.Linux"
731−
732−setAllEvents :: Event.Config -> Event.Config
733−setAllEvents cfg = ( Event.setAccessed Event.On
734− . Event.setOpened Event.On
735− . Event.setWriteClosed Event.On
736− . Event.setNonWriteClosed Event.On
737− )cfg
738−
739−allEvents :: NonEmpty (Array Word8) -> S.SerialT IO Event.Event
740−allEvents = Event.watchWith setAllEvents
741−
742−-- Test cases for Linux platform
743−--
744−testAllEvents :: IO ()
745−testAllEvents = do
746− -- Test cases for regular path
747− hspec
748− $ describe moduleName
749− $ mapM_
750− (driver $ checkEvents eventListWithEOtask allEvents)
751− testDesc
752−
753− -- Test cases for SymLink path
754− hspec
755− $ describe moduleName
756− $ mapM_
757− (driver $ checkEvents eventListWithEOtask allEvents)
758− (map (\(a, b, c, d, _) -> (a, b, c, d, True)) testDesc)
759−
760− -- Test cases for moving root path
761− hspec
762− $ describe moduleName
763− $ mapM_
764− (driver $ checkEvents eventListWithEOtask allEvents)
765− testDescMoveRootDir
766−
767− -- Test cases for moving root path as SymLink
768− hspec
769− $ describe moduleName
770− $ mapM_
771− (driver $ checkEvents eventListWithEOtask allEvents)
772− (map (\(a, b, c, d, _) -> (a, b, c, d, True)) testDescMoveRootDir)
773−
774− -- Test cases for removing root path
775− hspec
776− $ describe moduleName
777− $ mapM_
778− (driver $ checkEvents (eventListWithFixLen 51) allEvents)
779− testDescRemoveRootDir
780−
781− -- Test cases for removing root path as Symlink
782− hspec
783− $ describe moduleName
784− $ mapM_
785− (driver $ checkEvents (eventListWithFixLen 20) allEvents)
786− testDescRemoveRootDirSymLink
787−
788−-- Test cases for Events which are common across the platforms
789−--
790−testCommonEvents :: IO ()
791−testCommonEvents = do
792− -- Test cases for regular path
793− hspec
794− $ describe moduleName
795− $ mapM_
796− (driver $ checkEvents eventListWithEOtask $ Event.watchRecursive)
797− testDescCommon
798−
799− -- Test cases for SymLink path
800− hspec
801− $ describe moduleName
802− $ mapM_
803− (driver $ checkEvents eventListWithEOtask Event.watchRecursive)
804− (map (\(a, b, c, d, _) -> (a, b, c, d, True)) testDescCommon)
805−
806− -- Test cases for moving root path
807− hspec
808− $ describe moduleName
809− $ mapM_
810− (driver $ checkEvents eventListWithEOtask Event.watchRecursive)
811− testDescMoveRootDirCommon
812−
813− -- Test cases for moving root path as SymLink
814− hspec
815− $ describe moduleName
816− $ mapM_
817− (driver $ checkEvents eventListWithEOtask Event.watchRecursive)
818− (map (\(a, b, c, d, _) -> (a, b, c, d, True)) testDescMoveRootDirCommon)
819−
820− -- Test cases for removing root path
821− hspec
822− $ describe moduleName
823− $ mapM_
824− (driver $ checkEvents (eventListWithFixLen 12) Event.watchRecursive)
825− testDescRemoveRootDirCommon
64+fileMoveEvents :: [Char] -> [Char] -> [([Char], Event -> Bool)]
65+fileMoveEvents src dst =
66+ [ (src, fileEvent Event.isMoved)
67+ , (src, fileEvent Event.isMovedFrom)
68+ , (dst, fileEvent Event.isMoved)
69+ , (dst, fileEvent Event.isMovedTo)
70+ ]
82671
827− -- Test cases for removing root path as Symlink
828− hspec
829− $ describe moduleName
830− $ mapM_
831− (driver $ checkEvents (eventListWithFixLen 1) Event.watchRecursive)
832− testDescRemoveRootDirSymLinkCommon
72+-- TODO: add fileRoot tests from macOS test suite
73+
74+main :: IO ()
75+main = do
76+ -- We ignore the events on root/parent dir during regular non-root dir/file
77+ -- tests.
78+
79+ -- Tests common to regular root and symlink root cases
80+ let regSymTests =
81+ fileCreate "file" fileTouchEvents
82+ : fileMove "file1" "file2" fileMoveEvents
83+ : dirMove "dir1" "dir2" dirMoveEvents
84+ : dirDelete "dir" dirDelEvents
85+ : commonTests
86+
87+ let regTests =
88+ dirDelete "" rootDirDelEvents
89+ : rootDirMove "moved" (\src -> [(src, Event.isRootMoved)])
90+ : regSymTests
91+
92+ let symTests =
93+ -- when root is a symlinked dir, it does not recv touch, isDeleted
94+ -- or rootDeleted, rootUnwatched events.
95+ dirDelete "" (\dir -> [(dir, dirEvent Event.isAttrsModified)])
96+ -- No events occur when a symlink root is moved
97+ : regSymTests
98+
99+ let w = Event.watchWith (Event.setAllEvents Event.On)
100+ run = runTests moduleName "non-recursive" w
101+
102+ run DirType regTests
103+ run SymLinkOrigPath symTests
104+
105+ let fileRootTests =
106+ [ fileDelete "" (\path ->
107+ [ (path, Event.isAttrsModified)
108+ , (path, Event.isRootDeleted)
109+ , (path, Event.isRootUnwatched)
110+ ])
111+ , rootFileMove "moved" (\path -> [(path, Event.isRootMoved)])
112+ , fileModify "" (\path -> [(path, Event.isOpened)])
113+ ]
114+
115+ run FileType fileRootTests
116+
117+ -- In recursive mode all subdirectories are roots therefore they will
118+ -- generate isRootDeleted/isRootUnwatched. Also, for subdirectories
119+ -- multiple events are generated, once in the parent watch and once in the
120+ -- self watch as a root of the watch. Therefore, additional touchEvents are
121+ -- generated in this case.
122+ --
123+ -- XXX We can possibly filter out the duplicate events either from the
124+ -- parent or self.
125+ let regSymRecTests =
126+ -- XXX Nested file create misses the create event due to a race
127+ -- : fileCreateWithParent "subdir/file" fileTouchEvents
128+ fileCreate "subdir/file" fileTouchEvents
129+ : fileMove "subdir/file1" "subdir/file2" fileMoveEvents
130+ : dirMove "dir1" "dir2" recDirMoveEvents
131+ : dirMove "subdir/dir1" "subdir/dir2" recDirMoveEvents
132+ : dirDelete "dir" (\d -> rootDirDelEvents d ++ dirDelEvents d)
133+ : dirDelete "subdir/dir" (\d -> rootDirDelEvents d ++ dirDelEvents d)
134+ -- XXX Nested dir create misses the create event due to a race
135+ -- : dirCreateWithParent "subdir/dir" dirTouchEvents
136+ : dirCreate "subdir/dir"
137+ (\dir -> (dir, dirEvent Event.isCreated) : dirTouchEvents dir)
138+ : dirCreate "dir"
139+ (\dir -> (dir, dirEvent Event.isCreated) : dirTouchEvents dir)
140+ : commonRecTests
141+ recRegTests = regTests ++ regSymRecTests
142+ recSymTests = symTests ++ regSymRecTests
143+
144+ let recw = Event.watchWith
145+ (Event.setAllEvents Event.On . Event.setRecursiveMode Event.On)
146+ runRec = runTests moduleName "recursive" recw
147+
148+ runRec DirType recRegTests
149+ runRec SymLinkOrigPath recSymTests
150+ -- XXX This fails with exceptions, ideally it should work the same as in
151+ -- non-recursive mode
152+ -- runRec FileType fileRootTests
test/Streamly/Test/FileSystem/Event/LinuxTest.hsdeleted−9
@@ -1,9 +0,0 @@
1−module Main (main) where
2−
3−import System.IO (BufferMode(..), hSetBuffering, stdout)
4−import qualified Streamly.Test.FileSystem.Event.Linux as Event
5−
6−main :: IO ()
7−main = do
8− hSetBuffering stdout NoBuffering
9− Event.testAllEvents
test/Streamly/Test/FileSystem/Event/Windows.hs+42−334
@@ -6,341 +6,49 @@
66-- Stability : experimental
77-- Portability : GHC
88--
9−module Streamly.Test.FileSystem.Event.Windows (testCommonEvents) where
10−
11−import Control.Concurrent (MVar, newEmptyMVar, putMVar, takeMVar, threadDelay)
12−import Control.Monad.IO.Class (MonadIO)
13−
14−import Data.Functor.Identity (runIdentity)
15−import Data.Maybe (fromJust)
16−import Data.Word (Word8)
17−import System.Directory
18− ( createDirectoryIfMissing
19− , createDirectoryLink
20− , removeFile
21− , removeDirectory
22− , removePathForcibly
23− , renameDirectory
24− , renamePath
25− )
26−import System.FilePath ((</>))
27−import System.IO.Temp (withSystemTempDirectory)
28−
29−import Streamly.Internal.Data.Array.Foreign (Array)
30−
31−import qualified Data.List.NonEmpty as NonEmpty
32−import Data.List.NonEmpty (NonEmpty)
33−import qualified Data.Set as Set
34−import qualified Streamly.Unicode.Stream as Unicode
35−import qualified Streamly.Internal.Data.Array.Foreign as Array
36−import qualified Streamly.Internal.Data.Fold as FL
37−import qualified Streamly.Internal.Data.Parser as PR
38−import qualified Streamly.Internal.Data.Stream.IsStream as S
39−import qualified Streamly.Internal.Unicode.Stream as U
9+module Streamly.Test.FileSystem.Event.Windows (main) where
4010
4111import qualified Streamly.Internal.FileSystem.Event.Windows as Event
42−
43−
44−import Test.Hspec
45−
46−-------------------------------------------------------------------------------
47−-- Utilities
48−-------------------------------------------------------------------------------
49−
50−toUtf8 :: MonadIO m => String -> m (Array Word8)
51−toUtf8 = Array.fromStream . Unicode.encodeUtf8' . S.fromList
52−
53−utf8ToString :: Array Word8 -> String
54−utf8ToString = runIdentity . S.toList . U.decodeUtf8' . Array.toStream
55−
56−timeout :: IO String
57−timeout = threadDelay 5000000 >> return "Timeout"
58−
59−fseventDir :: String
60−fseventDir = "fsevent_dir"
61−
62−eoTask :: String
63−eoTask = "EOTask"
64−
65−-- XXX Make the getRelPath type same on windows and other platforms
66−eventPredicate :: Event.Event -> Bool
67−eventPredicate ev =
68− if (utf8ToString $ Event.getRelPath ev) == eoTask
69− then False
70− else True
71−
72−-------------------------------------------------------------------------------
73−-- Event matching utilities
74−-------------------------------------------------------------------------------
75−
76−
77−-- XXX Return a tuple (path, flags) instead of appending flags to path. And
78−-- then check the flags using an event mask.
79−
80−showEventShort :: Event.Event -> String
81−-- | Convert an 'Event' record to a short representation for unit test.
82−showEventShort ev@Event.Event{..} =
83− (utf8ToString $ Event.getRelPath ev) ++ "_" ++ show eventFlags
84−
85−-------------------------------------------------------------------------------
86−-- Event Watcher
87−-------------------------------------------------------------------------------
88−
89−type EventChecker = FilePath -> MVar () -> [String] -> IO String
90−type EventWatch = NonEmpty (Array Word8) -> S.SerialT IO Event.Event
91−type ToEventList = S.SerialT IO Event.Event -> IO [Event.Event]
92−
93−eventListWithFixLen :: Int -> ToEventList
94−eventListWithFixLen count = S.toList . S.take count
95−
96−eventListWithEOtask :: ToEventList
97−eventListWithEOtask = S.parse (PR.takeWhile eventPredicate FL.toList)
98−
99−checkEvents :: ToEventList -> EventWatch -> EventChecker
100−checkEvents toEL ew rootPath m matchList = do
101− let args = [rootPath]
102− paths <- mapM toUtf8 args
103− putStrLn ("Watch started !!!! on Path " ++ rootPath)
104− events <- toEL
105− $ S.before (putMVar m ())
106− $ ew (NonEmpty.fromList paths)
107− let eventStr = map showEventShort events
108− let baseSet = Set.fromList matchList
109− resultSet = Set.fromList eventStr
110− if baseSet `Set.isSubsetOf` resultSet
111− then
112− return "PASS"
113− else do
114− putStrLn $ "baseSet " ++ show matchList
115− putStrLn $ "resultSet " ++ show eventStr
116− return "Mismatch"
117−
118−-------------------------------------------------------------------------------
119−-- FS Event Generators
120−-------------------------------------------------------------------------------
121−
122−checker :: S.IsStream t =>
123− EventChecker -> FilePath -> MVar () -> [String] -> t IO String
124−checker ec rootPath synch matchList =
125− S.fromEffect (ec rootPath synch matchList)
126− `S.parallelFst`
127− S.fromEffect timeout
128−
129−-------------------------------------------------------------------------------
130−-- Test Drivers
131−-------------------------------------------------------------------------------
132−
133−driver ::
134− EventChecker
135− -> ( String
136− , FilePath -> IO ()
137− , FilePath -> IO ()
138− , [String]
139− , Bool
140− )
141− -> SpecWith ()
142−driver ec (desc, pre, ops, events, sym) = it desc $ runTest `shouldReturn` "PASS"
143−
144− where
145−
146− runTest = do
147− sync <- newEmptyMVar
148− withSystemTempDirectory fseventDir $ \fp ->
149− if sym
150− then do
151− createDirectoryLink fp (fp ++ "SymLink")
152− runTest0 (fp ++ "SymLink") sync
153− else runTest0 fp sync
154−
155− runTest0 fp sync = do
156− pre fp
157− let eventStream = checker ec fp sync events
158− fsOps = S.fromEffect $ runFSOps fp sync
159− fmap fromJust $ S.head $ eventStream `S.parallelFst` fsOps
160−
161− runFSOps fp sync = do
162− _ <- takeMVar sync
163− threadDelay 200000
164− ops fp
165− -- 'EOTask Created dir' event gets out of order so need to wait here
166− threadDelay 200000 -- Why this delay?
167− createDirectoryIfMissing True (fp </> eoTask)
168− threadDelay 10000000
169− error "fs ops timed out"
170−
171−-------------------------------------------------------------------------------
172−-- Main
173−-------------------------------------------------------------------------------
174−
175−testDesc, testDescRootDir ::
176− [ ( String -- test description
177− , FilePath -> IO () -- pre test operation
178− , FilePath -> IO () -- file system actions
179− , [String] -- expected events
180− , Bool ) -- SymLink
181− ]
182−testDesc =
183− [
184− ( "Create a single directory"
185− , const (return ())
186− , \fp -> createDirectoryIfMissing True (fp </> "dir1Single")
187− , [ "dir1Single_1" ]
188− , False
189− )
190− , ( "Remove a single directory"
191− , \fp -> createDirectoryIfMissing True (fp </> "dir1Single")
192− , \fp -> removeDirectory (fp </> "dir1Single")
193− , [ "dir1Single_2" ]
194− , False
195− )
196− , ( "Rename a single directory"
197− , \fp -> createDirectoryIfMissing True (fp </> "dir1Single")
198− , \fp ->
199− let spath = fp </> "dir1Single"
200− tpath = fp </> "dir1SingleRenamed"
201− in renameDirectory spath tpath
202− , [ "dir1Single_4"
203− , "dir1SingleRenamed_5"
204− ]
205− , False
206− )
207− , ( "Create a nested directory"
208− , const (return ())
209− , \fp ->
210− createDirectoryIfMissing True (fp </> "dir1" </> "dir2" </> "dir3")
211− , [ "dir1_1"
212− , "dir1\\dir2_1"
213− , "dir1\\dir2\\dir3_1"
214− ]
215− , False
216− )
217− , ( "Rename a nested directory"
218− , \fp -> createDirectoryIfMissing True
219− (fp </> "dir1" </> "dir2" </> "dir3")
220− , \fp ->
221− let spath = fp </> "dir1" </> "dir2" </> "dir3"
222− tpath = fp </> "dir1" </> "dir2" </> "dir3Renamed"
223− in renameDirectory spath tpath
224− , [ "dir1\\dir2_3"
225− , "dir1\\dir2\\dir3_4"
226− , "dir1\\dir2\\dir3Renamed_5"
227− , "dir1\\dir2_3"
228− ]
229− , False
230− )
231− , ( "Create a file in root Dir"
232− , const (return ())
233− , \fp -> writeFile (fp </> "FileCreated.txt") "Test Data"
234− , [ "FileCreated.txt_1"
235− , "FileCreated.txt_3"
236− , "FileCreated.txt_3"
237− ]
238− , False
239− )
240− , ( "Remove a file in root Dir"
241− , \fp -> writeFile (fp </> "FileCreated.txt") "Test Data"
242− , \fp -> removeFile (fp </> "FileCreated.txt")
243− , [ "FileCreated.txt_2" ]
244− , False
245− )
246− , ( "Rename a file in root Dir"
247− , \fp -> writeFile (fp </> "FileCreated.txt") "Test Data"
248− , \fp ->
249− let spath = (fp </> "FileCreated.txt")
250− tpath = (fp </> "FileRenamed.txt")
251− in renamePath spath tpath
252− , [ "FileCreated.txt_4"
253− , "FileRenamed.txt_5"
254− ]
255− , False
256− )
257− , ( "Create a file in a nested Dir"
258− , \fp ->
259− createDirectoryIfMissing True (fp </> "dir1" </> "dir2" </> "dir3")
260− , \fp ->
261− let p = fp </> "dir1" </> "dir2" </> "dir3" </> "FileCreated.txt"
262− in writeFile p "Test Data"
263−
264− , [ "dir1\\dir2\\dir3\\FileCreated.txt_1"
265− , "dir1\\dir2\\dir3\\FileCreated.txt_3"
266− ]
267−
268− , False
269− )
270− , ( "Remove a file in a nested Dir"
271− , \fp ->
272− let nestedDir = fp </> "dir1" </> "dir2" </> "dir3"
273− fpath = nestedDir </> "FileCreated.txt"
274− in do
275− createDirectoryIfMissing True nestedDir
276− writeFile fpath "Test Data"
277− , \fp ->
278− let p = fp </> "dir1" </> "dir2" </> "dir3" </> "FileCreated.txt"
279− in removeFile p
280− , ["dir1\\dir2\\dir3\\FileCreated.txt_2"]
281− , False
282− )
283− , ( "Rename a file in a nested Dir"
284− , \fp ->
285− let nestedDir = fp </> "dir1" </> "dir2" </> "dir3"
286− fpath = nestedDir </> "FileCreated.txt"
287− in do
288− createDirectoryIfMissing True nestedDir
289− writeFile fpath "Test Data"
290− , \fp ->
291− let s = (fp </> "dir1" </> "dir2" </> "dir3" </> "FileCreated.txt")
292− t = (fp </> "dir1" </> "dir2" </> "dir3" </> "FileRenamed.txt")
293− in renamePath s t
294− , [ "dir1\\dir2\\dir3_3"
295− , "dir1\\dir2\\dir3\\FileCreated.txt_4"
296− , "dir1\\dir2\\dir3\\FileRenamed.txt_5"
297− ]
298− , False
299− )
300− , ( "Remove the nested directory"
301− , \fp ->
302− createDirectoryIfMissing True (fp </> "dir1" </> "dir2" </> "dir3")
303− , \fp -> removePathForcibly (fp </> "dir1")
304− , [ "dir1_3"
305− , "dir1\\dir2_3"
306− , "dir1\\dir2\\dir3_2"
307− , "dir1\\dir2_2","dir1_2"
308− ]
309− , False
310− )
311− ]
312−
313−-- Since removel of root path as Symlink hangs for ever, testDescRootDir
314−-- is provided seperately.
315−testDescRootDir =
316− [ ( "Remove the root directory"
317− , \fp ->
318− createDirectoryIfMissing True (fp </> "dir1" </> "dir2")
319− , \fp -> removePathForcibly fp
320− , ["dir1_2", "dir1_3", "dir1\\dir2_2"]
321− , False
322− )
323− ]
12+import Streamly.Test.FileSystem.Event.Common
32413
32514moduleName :: String
326−moduleName = "FileSystem.Event"
327−
328−testCommonEvents :: IO ()
329−testCommonEvents = do
330− hspec
331− $ describe moduleName
332− $ mapM_
333− (driver $ checkEvents eventListWithEOtask Event.watchRecursive)
334− testDesc
335− hspec
336− $ describe moduleName
337− $ mapM_
338− (driver $ checkEvents (eventListWithFixLen 3) Event.watchRecursive)
339− testDescRootDir
340− -- Run test cases for SymLink
341− -- Remove the root directory as SymLink is not allowed, it hangs and Timeout.
342− hspec
343− $ describe moduleName
344− $ mapM_
345− (driver $ checkEvents eventListWithEOtask Event.watchRecursive)
346− (map (\(a, b, c, d, _) -> (a, b, c, d, True)) testDesc)
15+moduleName = "FileSystem.Event.Windows"
16+
17+-- TODO Test isModified event for parent directories when a file is created or
18+-- deleted.
19+
20+main :: IO ()
21+main = do
22+ let regularRootTests =
23+ commonTests
24+ -- The following watch-root deletion test results in:
25+ -- C:\tmp\fsevent_dir-e061f0b0a00e1696\watch-root:
26+ -- removePathForcibly:RemoveDirectory
27+ -- "\\\\?\\C:\\tmp\\fsevent_dir-e061f0b0a00e1696\\watch-root":
28+ -- permission denied (The process cannot access the file because it is
29+ -- being used by another process.)
30+ --
31+ -- ++ dirDelete "" (\dir -> [(dir, Event.isDeleted)])
32+
33+ let w = Event.watchWith (Event.setAllEvents Event.On)
34+ run = runTests moduleName "non-recursive" w
35+
36+ run DirType regularRootTests
37+ run SymLinkOrigPath commonTests
38+ -- TODO create/modify/delete/move with root path as a regular file, copy
39+ -- from macOS tests
40+ -- run FileType fileRootTests
41+
42+ let recTests =
43+ [ dirCreateWithParent "subdir\\dir"
44+ (\dir -> [(dir, dirEvent Event.isCreated)])
45+ , fileCreateWithParent "subdir\\file"
46+ (\file -> [(file, fileEvent Event.isCreated)])
47+ ] ++ commonRecTests
48+
49+ let recw = Event.watchWith
50+ (Event.setAllEvents Event.On . Event.setRecursiveMode Event.On)
51+ runRec = runTests moduleName "recursive" recw
52+
53+ runRec DirType (regularRootTests ++ recTests)
54+ runRec SymLinkOrigPath (commonTests ++ recTests)
test/Streamly/Test/FileSystem/Event/WindowsTest.hsdeleted−9
@@ -1,9 +0,0 @@
1−module Main (main) where
2−
3−import System.IO (BufferMode(..), hSetBuffering, stdout)
4−import qualified Streamly.Test.FileSystem.Event.Windows as Event
5−
6−main :: IO ()
7−main = do
8− hSetBuffering stdout NoBuffering
9− Event.testCommonEvents
test/streamly-tests.cabal+21−17
@@ -277,35 +277,39 @@ test-suite Data.Unfold
277277test-suite FileSystem.Event
278278 import: test-options
279279 type: exitcode-stdio-1.0
280+ ghc-options: -main-is Streamly.Test.FileSystem.Event
280281 main-is: Streamly/Test/FileSystem/Event.hs
281− if os(linux)
282− other-modules: Streamly.Test.FileSystem.Event.Linux
283− if os(windows)
284− other-modules: Streamly.Test.FileSystem.Event.Windows
285− if os(darwin)
282+ other-modules: Streamly.Test.FileSystem.Event.Common
283+
284+test-suite FileSystem.Event.Darwin
285+ import: test-options
286+ type: exitcode-stdio-1.0
287+ main-is: Streamly/Test/FileSystem/Event/Darwin.hs
288+ other-modules: Streamly.Test.FileSystem.Event.Common
289+ cpp-options: -DFILESYSTEM_EVENT_DARWIN
290+ ghc-options: -main-is Streamly.Test.FileSystem.Event.Darwin
291+ if !os(darwin)
286292 buildable: False
287293
288294test-suite FileSystem.Event.Linux
289295 import: test-options
290296 type: exitcode-stdio-1.0
291− main-is: Streamly/Test/FileSystem/Event/LinuxTest.hs
292− if os(linux)
293− other-modules: Streamly.Test.FileSystem.Event.Linux
294− if os(windows)
297+ main-is: Streamly/Test/FileSystem/Event/Linux.hs
298+ other-modules: Streamly.Test.FileSystem.Event.Common
299+ cpp-options: -DFILESYSTEM_EVENT_LINUX
300+ ghc-options: -main-is Streamly.Test.FileSystem.Event.Linux
301+ if !os(linux)
295302 buildable: False
296− if os(darwin)
297− buildable: False
298303
299304test-suite FileSystem.Event.Windows
300305 import: test-options
301306 type: exitcode-stdio-1.0
302− main-is: Streamly/Test/FileSystem/Event/WindowsTest.hs
303− if os(windows)
304− other-modules: Streamly.Test.FileSystem.Event.Windows
305− if os(linux)
307+ main-is: Streamly/Test/FileSystem/Event/Windows.hs
308+ other-modules: Streamly.Test.FileSystem.Event.Common
309+ cpp-options: -DFILESYSTEM_EVENT_WINDOWS
310+ ghc-options: -main-is Streamly.Test.FileSystem.Event.Windows
311+ if !os(windows)
306312 buildable: False
307− if os(darwin)
308− buildable: False
309313
310314test-suite FileSystem.Handle
311315 import: test-options