3 module UnitTests
.Distribution
.Client
.FileMonitor
(tests
) where
5 import Distribution
.Parsec
(simpleParsec
)
7 import Control
.Concurrent
(threadDelay
)
8 import Control
.Exception
10 import Data
.Proxy
(Proxy
(..))
11 import qualified Data
.Set
as Set
12 import qualified System
.Directory
as IO
13 import System
.FilePath
14 import Prelude
hiding (writeFile)
15 import qualified Prelude
as IO (writeFile)
17 import Distribution
.Compat
.Binary
18 import Distribution
.Simple
.Utils
(withTempDirectory
)
19 import Distribution
.System
(OS
(Windows
), buildOS
)
20 import Distribution
.Verbosity
(silent
)
22 import Distribution
.Client
.FileMonitor
23 import Distribution
.Compat
.Time
24 import Distribution
.Utils
.Structured
(Structured
, structureHash
)
25 import GHC
.Fingerprint
(Fingerprint
(..))
28 import Test
.Tasty
.ExpectedFailure
29 import Test
.Tasty
.HUnit
31 tests
:: Int -> [TestTree
]
35 [ testCase
"MonitorStateFile" $ structureHash
(Proxy
:: Proxy MonitorStateFile
) @?
= Fingerprint
0xe4108804c34962f6 0x06e94f8fc9e48e13
36 , testCase
"MonitorStateGlob" $ structureHash
(Proxy
:: Proxy MonitorStateGlob
) @?
= Fingerprint fingerprintStateGlob1 fingerprintStateGlob2
37 , testCase
"MonitorStateFileSet" $ structureHash
(Proxy
:: Proxy MonitorStateFileSet
) @?
= Fingerprint fingerprintStateFileSet1 fingerprintStateFileSet2
39 , testCase
"sanity check mtimes" $ testFileMTimeSanity mtimeChange
40 , testCase
"sanity check dirs" $ testDirChangeSanity mtimeChange
41 , testCase
"no monitor cache" testNoMonitorCache
42 , testCaseSteps
"corrupt monitor cache" testCorruptMonitorCache
43 , testCase
"empty monitor" testEmptyMonitor
44 , testCase
"missing file" testMissingFile
45 , testCase
"change file" $ testChangedFile mtimeChange
46 , testCase
"file mtime vs content" $ testChangedFileMtimeVsContent mtimeChange
47 , testCase
"update during action" $ testUpdateDuringAction mtimeChange
48 , testCase
"remove file" testRemoveFile
49 , testCase
"non-existent file" testNonExistentFile
50 , testCase
"changed file type" $ testChangedFileType mtimeChange
51 , testCase
"several monitor kinds" $ testMultipleMonitorKinds mtimeChange
54 [ testCase
"no change" testGlobNoChange
55 , testCase
"add match" $ testGlobAddMatch mtimeChange
56 , testCase
"remove match" $ testGlobRemoveMatch mtimeChange
57 , testCase
"change match" $ testGlobChangeMatch mtimeChange
58 , testCase
"add match subdir" $ testGlobAddMatchSubdir mtimeChange
59 , testCase
"remove match subdir" $ testGlobRemoveMatchSubdir mtimeChange
60 , testCase
"change match subdir" $ testGlobChangeMatchSubdir mtimeChange
61 , testCase
"match toplevel dir" $ testGlobMatchTopDir mtimeChange
62 , testCase
"add non-match" $ testGlobAddNonMatch mtimeChange
63 , testCase
"remove non-match" $ testGlobRemoveNonMatch mtimeChange
64 , knownBrokenInWindows
"See issue #3126" $
65 testCase
"add non-match subdir" $
66 testGlobAddNonMatchSubdir mtimeChange
67 , testCase
"remove non-match subdir" $ testGlobRemoveNonMatchSubdir mtimeChange
68 , testCase
"invariant sorted 1" $
69 testInvariantMonitorStateGlobFiles
71 , testCase
"invariant sorted 2" $
72 testInvariantMonitorStateGlobDirs
74 , testCase
"match dirs" $ testGlobMatchDir mtimeChange
75 , knownBrokenInWindows
"See issue #3126" $
76 testCase
"match dirs only" $
77 testGlobMatchDirOnly mtimeChange
78 , testCase
"change file type" $ testGlobChangeFileType mtimeChange
79 , testCase
"absolute paths" $ testGlobAbsolutePath mtimeChange
81 , testCase
"value unchanged" testValueUnchanged
82 , testCase
"value changed" testValueChanged
83 , testCase
"value & file changed" $ testValueAndFileChanged mtimeChange
84 , testCase
"value updated" testValueUpdated
87 knownBrokenInWindows msg
= case buildOS
of
88 Windows
-> expectFailBecause msg
90 fingerprintStateGlob1
, fingerprintStateGlob2
, fingerprintStateFileSet1
, fingerprintStateFileSet2
:: Word64
91 #if MIN_VERSION_base
(4,19,0)
92 fingerprintStateGlob1
= 0xae70229aabb1ba1f
93 fingerprintStateGlob2
= 0xb53ed324c96f0d0d
94 fingerprintStateFileSet1
= 0x8e509e16f973e036
95 fingerprintStateFileSet2
= 0xa23f21d8dc8a2dee
97 fingerprintStateGlob1
= 0xfd8f6be0e8258fe7
98 fingerprintStateGlob2
= 0xdb5fac737139bca6
99 fingerprintStateFileSet1
= 0xb745f4ea498389a5
100 fingerprintStateFileSet2
= 0x70db6adb5078aa27
103 -- Check the file system behaves the way we expect it to
105 -- we rely on file mtimes having a reasonable resolution
106 testFileMTimeSanity
:: Int -> Assertion
107 testFileMTimeSanity mtimeChange
=
108 withTempDirectory silent
"." "file-status-" $ \dir
-> do
110 IO.writeFile (dir
</> "a") "content"
111 t1
<- getModTime
(dir
</> "a")
112 threadDelay mtimeChange
113 IO.writeFile (dir
</> "a") "content"
114 t2
<- getModTime
(dir
</> "a")
115 assertBool
"expected different file mtimes" (t2
> t1
)
117 -- We rely on directories changing mtime when entries are added or removed
118 testDirChangeSanity
:: Int -> Assertion
119 testDirChangeSanity mtimeChange
=
120 withTempDirectory silent
"." "dir-mtime-" $ \dir
-> do
121 expectMTimeChange dir
"file add" $
122 IO.writeFile (dir
</> "file") "content"
124 expectMTimeSame dir
"file content change" $
125 IO.writeFile (dir
</> "file") "new content"
127 expectMTimeChange dir
"file del" $
128 IO.removeFile (dir
</> "file")
130 expectMTimeChange dir
"subdir add" $
131 IO.createDirectory (dir
</> "dir")
133 expectMTimeSame dir
"subdir file add" $
134 IO.writeFile (dir
</> "dir" </> "file") "content"
136 expectMTimeChange dir
"subdir file move in" $
137 IO.renameFile (dir
</> "dir" </> "file") (dir
</> "file")
139 expectMTimeChange dir
"subdir file move out" $
140 IO.renameFile (dir
</> "file") (dir
</> "dir" </> "file")
142 expectMTimeSame dir
"subdir dir add" $
143 IO.createDirectory (dir
</> "dir" </> "subdir")
145 expectMTimeChange dir
"subdir dir move in" $
146 IO.renameDirectory (dir
</> "dir" </> "subdir") (dir
</> "subdir")
148 expectMTimeChange dir
"subdir dir move out" $
149 IO.renameDirectory (dir
</> "subdir") (dir
</> "dir" </> "subdir")
158 expectMTimeChange dir descr action
= do
160 threadDelay mtimeChange
163 assertBool
("expected dir mtime change on " ++ descr
) (t
' > t
)
165 expectMTimeSame dir descr action
= do
167 threadDelay mtimeChange
170 assertBool
("expected same dir mtime on " ++ descr
) (t
' == t
)
172 -- Now for the FileMonitor tests proper...
174 -- first run, where we don't even call updateMonitor
175 testNoMonitorCache
:: Assertion
177 withFileMonitor
$ \root monitor
-> do
178 reason
<- expectMonitorChanged root
(monitor
:: FileMonitor
() ()) ()
179 reason
@?
= MonitorFirstRun
181 -- write garbage into the binary cache file
182 testCorruptMonitorCache
:: (String -> IO ()) -> Assertion
183 testCorruptMonitorCache step
=
184 withFileMonitor
$ \root monitor
-> do
185 step
"Writing broken file"
186 IO.writeFile (fileMonitorCacheFile monitor
) "broken"
187 reason
<- expectMonitorChanged root monitor
()
188 reason
@?
= MonitorCorruptCache
190 step
"Updating file monitor"
191 updateMonitor root monitor
[] () ()
192 (res
, files
) <- expectMonitorUnchanged root monitor
()
196 step
"Writing broken file again"
197 IO.writeFile (fileMonitorCacheFile monitor
) "broken"
198 reason2
<- expectMonitorChanged root monitor
()
199 reason2
@?
= MonitorCorruptCache
201 -- no files to monitor
202 testEmptyMonitor
:: Assertion
204 withFileMonitor
$ \root monitor
-> do
206 updateMonitor root monitor
[] () ()
208 (res
, files
) <- expectMonitorUnchanged root monitor
()
212 -- monitor a file that is expected to exist
213 testMissingFile
:: Assertion
215 test monitorFile touchFile
"a"
216 test monitorFileHashed touchFile
"a"
217 test monitorFile touchFile
("dir" </> "a")
218 test monitorFileHashed touchFile
("dir" </> "a")
219 test monitorDirectory touchDir
"a"
220 test monitorDirectory touchDir
("dir" </> "a")
223 :: (FilePath -> MonitorFilePath
)
224 -> (RootPath
-> FilePath -> IO ())
227 test monitorKind touch file
=
228 withFileMonitor
$ \root monitor
-> do
229 -- a file that doesn't exist at snapshot time is considered to have
231 updateMonitor root monitor
[monitorKind file
] () ()
232 reason
<- expectMonitorChanged root monitor
()
233 reason
@?
= MonitoredFileChanged file
235 -- a file doesn't exist at snapshot time, but gets added afterwards is
236 -- also considered to have changed
237 updateMonitor root monitor
[monitorKind file
] () ()
239 reason2
<- expectMonitorChanged root monitor
()
240 reason2
@?
= MonitoredFileChanged file
242 testChangedFile
:: Int -> Assertion
243 testChangedFile mtimeChange
= do
244 test monitorFile touchFile touchFile
"a"
245 test monitorFileHashed touchFile touchFileContent
"a"
246 test monitorFile touchFile touchFile
("dir" </> "a")
247 test monitorFileHashed touchFile touchFileContent
("dir" </> "a")
248 test monitorDirectory touchDir touchDir
"a"
249 test monitorDirectory touchDir touchDir
("dir" </> "a")
252 :: (FilePath -> MonitorFilePath
)
253 -> (RootPath
-> FilePath -> IO ())
254 -> (RootPath
-> FilePath -> IO ())
257 test monitorKind touch touch
' file
=
258 withFileMonitor
$ \root monitor
-> do
260 updateMonitor root monitor
[monitorKind file
] () ()
261 threadDelay mtimeChange
263 reason
<- expectMonitorChanged root monitor
()
264 reason
@?
= MonitoredFileChanged file
266 testChangedFileMtimeVsContent
:: Int -> Assertion
267 testChangedFileMtimeVsContent mtimeChange
=
268 withFileMonitor
$ \root monitor
-> do
269 -- if we don't touch the file, it's unchanged
271 updateMonitor root monitor
[monitorFile
"a"] () ()
272 (res
, files
) <- expectMonitorUnchanged root monitor
()
274 files
@?
= [monitorFile
"a"]
276 -- if we do touch the file, it's changed if we only consider mtime
277 updateMonitor root monitor
[monitorFile
"a"] () ()
278 threadDelay mtimeChange
280 reason
<- expectMonitorChanged root monitor
()
281 reason
@?
= MonitoredFileChanged
"a"
283 -- but if we touch the file, it's unchanged if we consider content hash
284 updateMonitor root monitor
[monitorFileHashed
"a"] () ()
285 threadDelay mtimeChange
287 (res2
, files2
) <- expectMonitorUnchanged root monitor
()
289 files2
@?
= [monitorFileHashed
"a"]
291 -- finally if we change the content it's changed
292 updateMonitor root monitor
[monitorFileHashed
"a"] () ()
293 threadDelay mtimeChange
294 touchFileContent root
"a"
295 reason2
<- expectMonitorChanged root monitor
()
296 reason2
@?
= MonitoredFileChanged
"a"
298 testUpdateDuringAction
:: Int -> Assertion
299 testUpdateDuringAction mtimeChange
= do
300 test
(monitorFile
"a") touchFile
"a"
301 test
(monitorFileHashed
"a") touchFile
"a"
302 test
(monitorDirectory
"a") touchDir
"a"
303 test
(monitorFileGlobStr
"*") touchFile
"a"
305 (monitorFileGlobStr
"*"){monitorKindDir
= DirModTime
}
311 -> (RootPath
-> FilePath -> IO ())
314 test monitorSpec touch file
=
315 withFileMonitor
$ \root monitor
-> do
317 updateMonitor root monitor
[monitorSpec
] () ()
319 -- start doing an update action...
320 threadDelay mtimeChange
-- some time passes
321 touch root file
-- a file gets updates during the action
322 threadDelay mtimeChange
-- some time passes then we finish
323 updateMonitor root monitor
[monitorSpec
] () ()
324 -- we don't notice this change since we took the timestamp after the
326 (res
, files
) <- expectMonitorUnchanged root monitor
()
328 files
@?
= [monitorSpec
]
330 -- Let's try again, this time taking the timestamp before the action
331 timestamp
' <- beginUpdateFileMonitor
332 threadDelay mtimeChange
-- some time passes
333 touch root file
-- a file gets updates during the action
334 threadDelay mtimeChange
-- some time passes then we finish
335 updateMonitorWithTimestamp root monitor timestamp
' [monitorSpec
] () ()
336 -- now we do notice the change since we took the snapshot before the
338 reason
<- expectMonitorChanged root monitor
()
339 reason
@?
= MonitoredFileChanged file
341 testRemoveFile
:: Assertion
343 test monitorFile touchFile
removeFile "a"
344 test monitorFileHashed touchFile
removeFile "a"
345 test monitorFile touchFile
removeFile ("dir" </> "a")
346 test monitorFileHashed touchFile
removeFile ("dir" </> "a")
347 test monitorDirectory touchDir removeDir
"a"
348 test monitorDirectory touchDir removeDir
("dir" </> "a")
351 :: (FilePath -> MonitorFilePath
)
352 -> (RootPath
-> FilePath -> IO ())
353 -> (RootPath
-> FilePath -> IO ())
356 test monitorKind touch remove file
=
357 withFileMonitor
$ \root monitor
-> do
359 updateMonitor root monitor
[monitorKind file
] () ()
361 reason
<- expectMonitorChanged root monitor
()
362 reason
@?
= MonitoredFileChanged file
364 -- monitor a file that we expect not to exist
365 testNonExistentFile
:: Assertion
366 testNonExistentFile
=
367 withFileMonitor
$ \root monitor
-> do
368 -- a file that doesn't exist at snapshot time or check time is unchanged
369 updateMonitor root monitor
[monitorNonExistentFile
"a"] () ()
370 (res
, files
) <- expectMonitorUnchanged root monitor
()
372 files
@?
= [monitorNonExistentFile
"a"]
374 -- if the file then exists it has changed
376 reason
<- expectMonitorChanged root monitor
()
377 reason
@?
= MonitoredFileChanged
"a"
379 -- if the file then exists at snapshot and check time it has changed
380 updateMonitor root monitor
[monitorNonExistentFile
"a"] () ()
381 reason2
<- expectMonitorChanged root monitor
()
382 reason2
@?
= MonitoredFileChanged
"a"
384 -- but if the file existed at snapshot time and doesn't exist at check time
385 -- it is consider unchanged. This is unlike files we expect to exist, but
386 -- that's because files that exist can have different content and actions
387 -- can depend on that content, whereas if the action expected a file not to
388 -- exist and it now does not, it'll give the same result, irrespective of
389 -- the fact that the file might have existed in the meantime.
390 updateMonitor root monitor
[monitorNonExistentFile
"a"] () ()
392 (res2
, files2
) <- expectMonitorUnchanged root monitor
()
394 files2
@?
= [monitorNonExistentFile
"a"]
396 testChangedFileType
:: Int -> Assertion
397 testChangedFileType mtimeChange
= do
398 test
(monitorFile
"a") touchFile
removeFile createDir
399 test
(monitorFileHashed
"a") touchFile
removeFile createDir
401 test
(monitorDirectory
"a") createDir removeDir touchFile
402 test
(monitorFileOrDirectory
"a") createDir removeDir touchFile
405 (monitorFileGlobStr
"*"){monitorKindDir
= DirModTime
}
410 (monitorFileGlobStr
"*"){monitorKindDir
= DirModTime
}
417 -> (RootPath
-> String -> IO ())
418 -> (RootPath
-> String -> IO ())
419 -> (RootPath
-> String -> IO ())
421 test monitorKind touch remove touch
' =
422 withFileMonitor
$ \root monitor
-> do
424 updateMonitor root monitor
[monitorKind
] () ()
425 threadDelay mtimeChange
428 reason
<- expectMonitorChanged root monitor
()
429 reason
@?
= MonitoredFileChanged
"a"
431 -- Monitoring the same file with two different kinds of monitor should work
432 -- both should be kept, and both checked for changes.
433 -- We had a bug where only one monitor kind was kept per file.
434 -- https://github.com/haskell/cabal/pull/3863#issuecomment-248495178
435 testMultipleMonitorKinds
:: Int -> Assertion
436 testMultipleMonitorKinds mtimeChange
=
437 withFileMonitor
$ \root monitor
-> do
439 updateMonitor root monitor
[monitorFile
"a", monitorFileHashed
"a"] () ()
440 (res
, files
) <- expectMonitorUnchanged root monitor
()
442 files
@?
= [monitorFile
"a", monitorFileHashed
"a"]
443 threadDelay mtimeChange
444 touchFile root
"a" -- not changing content, just mtime
445 reason
<- expectMonitorChanged root monitor
()
446 reason
@?
= MonitoredFileChanged
"a"
452 [ monitorDirectory
"dir"
453 , monitorDirectoryExistence
"dir"
457 (res2
, files2
) <- expectMonitorUnchanged root monitor
()
459 files2
@?
= [monitorDirectory
"dir", monitorDirectoryExistence
"dir"]
460 threadDelay mtimeChange
461 touchFile root
("dir" </> "a") -- changing dir mtime, not existence
462 reason2
<- expectMonitorChanged root monitor
()
463 reason2
@?
= MonitoredFileChanged
"dir"
469 testGlobNoChange
:: Assertion
471 withFileMonitor
$ \root monitor
-> do
472 touchFile root
("dir" </> "good-a")
473 touchFile root
("dir" </> "good-b")
474 updateMonitor root monitor
[monitorFileGlobStr
"dir/good-*"] () ()
475 (res
, files
) <- expectMonitorUnchanged root monitor
()
477 files
@?
= [monitorFileGlobStr
"dir/good-*"]
479 testGlobAddMatch
:: Int -> Assertion
480 testGlobAddMatch mtimeChange
=
481 withFileMonitor
$ \root monitor
-> do
482 touchFile root
("dir" </> "good-a")
483 updateMonitor root monitor
[monitorFileGlobStr
"dir/good-*"] () ()
484 (res
, files
) <- expectMonitorUnchanged root monitor
()
486 files
@?
= [monitorFileGlobStr
"dir/good-*"]
487 threadDelay mtimeChange
488 touchFile root
("dir" </> "good-b")
489 reason
<- expectMonitorChanged root monitor
()
490 reason
@?
= MonitoredFileChanged
("dir" </> "good-b")
492 testGlobRemoveMatch
:: Int -> Assertion
493 testGlobRemoveMatch mtimeChange
=
494 withFileMonitor
$ \root monitor
-> do
495 touchFile root
("dir" </> "good-a")
496 touchFile root
("dir" </> "good-b")
497 updateMonitor root monitor
[monitorFileGlobStr
"dir/good-*"] () ()
498 threadDelay mtimeChange
499 removeFile root
"dir/good-a"
500 reason
<- expectMonitorChanged root monitor
()
501 reason
@?
= MonitoredFileChanged
("dir" </> "good-a")
503 testGlobChangeMatch
:: Int -> Assertion
504 testGlobChangeMatch mtimeChange
=
505 withFileMonitor
$ \root monitor
-> do
506 touchFile root
("dir" </> "good-a")
507 touchFile root
("dir" </> "good-b")
508 updateMonitor root monitor
[monitorFileGlobStr
"dir/good-*"] () ()
509 threadDelay mtimeChange
510 touchFile root
("dir" </> "good-b")
511 (res
, files
) <- expectMonitorUnchanged root monitor
()
513 files
@?
= [monitorFileGlobStr
"dir/good-*"]
515 touchFileContent root
("dir" </> "good-b")
516 reason
<- expectMonitorChanged root monitor
()
517 reason
@?
= MonitoredFileChanged
("dir" </> "good-b")
519 testGlobAddMatchSubdir
:: Int -> Assertion
520 testGlobAddMatchSubdir mtimeChange
=
521 withFileMonitor
$ \root monitor
-> do
522 touchFile root
("dir" </> "a" </> "good-a")
523 updateMonitor root monitor
[monitorFileGlobStr
"dir/*/good-*"] () ()
524 threadDelay mtimeChange
525 touchFile root
("dir" </> "b" </> "good-b")
526 reason
<- expectMonitorChanged root monitor
()
527 reason
@?
= MonitoredFileChanged
("dir" </> "b" </> "good-b")
529 testGlobRemoveMatchSubdir
:: Int -> Assertion
530 testGlobRemoveMatchSubdir mtimeChange
=
531 withFileMonitor
$ \root monitor
-> do
532 touchFile root
("dir" </> "a" </> "good-a")
533 touchFile root
("dir" </> "b" </> "good-b")
534 updateMonitor root monitor
[monitorFileGlobStr
"dir/*/good-*"] () ()
535 threadDelay mtimeChange
536 removeDir root
("dir" </> "a")
537 reason
<- expectMonitorChanged root monitor
()
538 reason
@?
= MonitoredFileChanged
("dir" </> "a" </> "good-a")
540 testGlobChangeMatchSubdir
:: Int -> Assertion
541 testGlobChangeMatchSubdir mtimeChange
=
542 withFileMonitor
$ \root monitor
-> do
543 touchFile root
("dir" </> "a" </> "good-a")
544 touchFile root
("dir" </> "b" </> "good-b")
545 updateMonitor root monitor
[monitorFileGlobStr
"dir/*/good-*"] () ()
546 threadDelay mtimeChange
547 touchFile root
("dir" </> "b" </> "good-b")
548 (res
, files
) <- expectMonitorUnchanged root monitor
()
550 files
@?
= [monitorFileGlobStr
"dir/*/good-*"]
552 touchFileContent root
"dir/b/good-b"
553 reason
<- expectMonitorChanged root monitor
()
554 reason
@?
= MonitoredFileChanged
("dir" </> "b" </> "good-b")
556 -- check nothing goes squiffy with matching in the top dir
557 testGlobMatchTopDir
:: Int -> Assertion
558 testGlobMatchTopDir mtimeChange
=
559 withFileMonitor
$ \root monitor
-> do
560 updateMonitor root monitor
[monitorFileGlobStr
"*"] () ()
561 threadDelay mtimeChange
563 reason
<- expectMonitorChanged root monitor
()
564 reason
@?
= MonitoredFileChanged
"a"
566 testGlobAddNonMatch
:: Int -> Assertion
567 testGlobAddNonMatch mtimeChange
=
568 withFileMonitor
$ \root monitor
-> do
569 touchFile root
("dir" </> "good-a")
570 updateMonitor root monitor
[monitorFileGlobStr
"dir/good-*"] () ()
571 threadDelay mtimeChange
572 touchFile root
("dir" </> "bad")
573 (res
, files
) <- expectMonitorUnchanged root monitor
()
575 files
@?
= [monitorFileGlobStr
"dir/good-*"]
577 testGlobRemoveNonMatch
:: Int -> Assertion
578 testGlobRemoveNonMatch mtimeChange
=
579 withFileMonitor
$ \root monitor
-> do
580 touchFile root
("dir" </> "good-a")
581 touchFile root
("dir" </> "bad")
582 updateMonitor root monitor
[monitorFileGlobStr
"dir/good-*"] () ()
583 threadDelay mtimeChange
584 removeFile root
"dir/bad"
585 (res
, files
) <- expectMonitorUnchanged root monitor
()
587 files
@?
= [monitorFileGlobStr
"dir/good-*"]
589 testGlobAddNonMatchSubdir
:: Int -> Assertion
590 testGlobAddNonMatchSubdir mtimeChange
=
591 withFileMonitor
$ \root monitor
-> do
592 touchFile root
("dir" </> "a" </> "good-a")
593 updateMonitor root monitor
[monitorFileGlobStr
"dir/*/good-*"] () ()
594 threadDelay mtimeChange
595 touchFile root
("dir" </> "b" </> "bad")
596 (res
, files
) <- expectMonitorUnchanged root monitor
()
598 files
@?
= [monitorFileGlobStr
"dir/*/good-*"]
600 testGlobRemoveNonMatchSubdir
:: Int -> Assertion
601 testGlobRemoveNonMatchSubdir mtimeChange
=
602 withFileMonitor
$ \root monitor
-> do
603 touchFile root
("dir" </> "a" </> "good-a")
604 touchFile root
("dir" </> "b" </> "bad")
605 updateMonitor root monitor
[monitorFileGlobStr
"dir/*/good-*"] () ()
606 threadDelay mtimeChange
607 removeDir root
("dir" </> "b")
608 (res
, files
) <- expectMonitorUnchanged root monitor
()
610 files
@?
= [monitorFileGlobStr
"dir/*/good-*"]
612 -- try and tickle a bug that happens if we don't maintain the invariant that
613 -- MonitorStateGlobFiles entries are sorted
614 testInvariantMonitorStateGlobFiles
:: Int -> Assertion
615 testInvariantMonitorStateGlobFiles mtimeChange
=
616 withFileMonitor
$ \root monitor
-> do
617 touchFile root
("dir" </> "a")
618 touchFile root
("dir" </> "b")
619 touchFile root
("dir" </> "c")
620 touchFile root
("dir" </> "d")
621 updateMonitor root monitor
[monitorFileGlobStr
"dir/*"] () ()
622 threadDelay mtimeChange
623 -- so there should be no change (since we're doing content checks)
624 -- but if we can get the dir entries to appear in the wrong order
625 -- then if the sorted invariant is not maintained then we can fool
626 -- the 'probeGlobStatus' into thinking there's changes
627 removeFile root
("dir" </> "a")
628 removeFile root
("dir" </> "b")
629 removeFile root
("dir" </> "c")
630 removeFile root
("dir" </> "d")
631 touchFile root
("dir" </> "d")
632 touchFile root
("dir" </> "c")
633 touchFile root
("dir" </> "b")
634 touchFile root
("dir" </> "a")
635 (res
, files
) <- expectMonitorUnchanged root monitor
()
637 files
@?
= [monitorFileGlobStr
"dir/*"]
639 -- same thing for the subdirs case
640 testInvariantMonitorStateGlobDirs
:: Int -> Assertion
641 testInvariantMonitorStateGlobDirs mtimeChange
=
642 withFileMonitor
$ \root monitor
-> do
643 touchFile root
("dir" </> "a" </> "file")
644 touchFile root
("dir" </> "b" </> "file")
645 touchFile root
("dir" </> "c" </> "file")
646 touchFile root
("dir" </> "d" </> "file")
647 updateMonitor root monitor
[monitorFileGlobStr
"dir/*/file"] () ()
648 threadDelay mtimeChange
649 removeDir root
("dir" </> "a")
650 removeDir root
("dir" </> "b")
651 removeDir root
("dir" </> "c")
652 removeDir root
("dir" </> "d")
653 touchFile root
("dir" </> "d" </> "file")
654 touchFile root
("dir" </> "c" </> "file")
655 touchFile root
("dir" </> "b" </> "file")
656 touchFile root
("dir" </> "a" </> "file")
657 (res
, files
) <- expectMonitorUnchanged root monitor
()
659 files
@?
= [monitorFileGlobStr
"dir/*/file"]
661 -- ensure that a glob can match a directory as well as a file
662 testGlobMatchDir
:: Int -> Assertion
663 testGlobMatchDir mtimeChange
=
664 withFileMonitor
$ \root monitor
-> do
665 createDir root
("dir" </> "a")
666 updateMonitor root monitor
[monitorFileGlobStr
"dir/*"] () ()
667 threadDelay mtimeChange
668 -- nothing changed yet
669 (res
, files
) <- expectMonitorUnchanged root monitor
()
671 files
@?
= [monitorFileGlobStr
"dir/*"]
672 -- expect dir/b to match and be detected as changed
673 createDir root
("dir" </> "b")
674 reason
<- expectMonitorChanged root monitor
()
675 reason
@?
= MonitoredFileChanged
("dir" </> "b")
676 -- now remove dir/a and expect it to be detected as changed
677 updateMonitor root monitor
[monitorFileGlobStr
"dir/*"] () ()
678 threadDelay mtimeChange
679 removeDir root
("dir" </> "a")
680 reason2
<- expectMonitorChanged root monitor
()
681 reason2
@?
= MonitoredFileChanged
("dir" </> "a")
683 testGlobMatchDirOnly
:: Int -> Assertion
684 testGlobMatchDirOnly mtimeChange
=
685 withFileMonitor
$ \root monitor
-> do
686 updateMonitor root monitor
[monitorFileGlobStr
"dir/*/"] () ()
687 threadDelay mtimeChange
688 -- expect file dir/a to not match, so not detected as changed
689 touchFile root
("dir" </> "a")
690 (res
, files
) <- expectMonitorUnchanged root monitor
()
692 files
@?
= [monitorFileGlobStr
"dir/*/"]
693 -- note that checking the file monitor for changes can updates the
694 -- cached dir mtimes (when it has to record that there's new matches)
695 -- so we need an extra mtime delay
696 threadDelay mtimeChange
697 -- but expect dir/b to match
698 createDir root
("dir" </> "b")
699 reason
<- expectMonitorChanged root monitor
()
700 reason
@?
= MonitoredFileChanged
("dir" </> "b")
702 testGlobChangeFileType
:: Int -> Assertion
703 testGlobChangeFileType mtimeChange
=
704 withFileMonitor
$ \root monitor
-> do
705 -- change file to dir
706 touchFile root
("dir" </> "a")
707 updateMonitor root monitor
[monitorFileGlobStr
"dir/*"] () ()
708 threadDelay mtimeChange
709 removeFile root
("dir" </> "a")
710 createDir root
("dir" </> "a")
711 reason
<- expectMonitorChanged root monitor
()
712 reason
@?
= MonitoredFileChanged
("dir" </> "a")
713 -- change dir to file
714 updateMonitor root monitor
[monitorFileGlobStr
"dir/*"] () ()
715 threadDelay mtimeChange
716 removeDir root
("dir" </> "a")
717 touchFile root
("dir" </> "a")
718 reason2
<- expectMonitorChanged root monitor
()
719 reason2
@?
= MonitoredFileChanged
("dir" </> "a")
721 testGlobAbsolutePath
:: Int -> Assertion
722 testGlobAbsolutePath mtimeChange
=
723 withFileMonitor
$ \root monitor
-> do
724 root
' <- absoluteRoot root
725 -- absolute glob, removing a file
726 touchFile root
("dir/good-a")
727 touchFile root
("dir/good-b")
728 updateMonitor root monitor
[monitorFileGlobStr
(root
' </> "dir/good-*")] () ()
729 threadDelay mtimeChange
730 removeFile root
"dir/good-a"
731 reason
<- expectMonitorChanged root monitor
()
732 reason
@?
= MonitoredFileChanged
(root
' </> "dir" </> "good-a")
733 -- absolute glob, adding a file
734 updateMonitor root monitor
[monitorFileGlobStr
(root
' </> "dir/good-*")] () ()
735 threadDelay mtimeChange
736 touchFile root
("dir/good-a")
737 reason2
<- expectMonitorChanged root monitor
()
738 reason2
@?
= MonitoredFileChanged
(root
' </> "dir" </> "good-a")
739 -- absolute glob, changing a file
740 updateMonitor root monitor
[monitorFileGlobStr
(root
' </> "dir/good-*")] () ()
741 threadDelay mtimeChange
742 touchFileContent root
"dir/good-b"
743 reason3
<- expectMonitorChanged root monitor
()
744 reason3
@?
= MonitoredFileChanged
(root
' </> "dir" </> "good-b")
750 testValueUnchanged
:: Assertion
752 withFileMonitor
$ \root monitor
-> do
754 updateMonitor root monitor
[monitorFile
"a"] (42 :: Int) "ok"
755 (res
, files
) <- expectMonitorUnchanged root monitor
42
757 files
@?
= [monitorFile
"a"]
759 testValueChanged
:: Assertion
761 withFileMonitor
$ \root monitor
-> do
763 updateMonitor root monitor
[monitorFile
"a"] (42 :: Int) "ok"
764 reason
<- expectMonitorChanged root monitor
43
765 reason
@?
= MonitoredValueChanged
42
767 testValueAndFileChanged
:: Int -> Assertion
768 testValueAndFileChanged mtimeChange
=
769 withFileMonitor
$ \root monitor
-> do
772 -- we change the value and the file, and the value change is reported
773 updateMonitor root monitor
[monitorFile
"a"] (42 :: Int) "ok"
774 threadDelay mtimeChange
776 reason
<- expectMonitorChanged root monitor
43
777 reason
@?
= MonitoredValueChanged
42
779 -- if fileMonitorCheckIfOnlyValueChanged then if only the value changed
780 -- then it's reported as MonitoredValueChanged
781 let monitor
' :: FileMonitor
Int String
782 monitor
' = monitor
{fileMonitorCheckIfOnlyValueChanged
= True}
783 updateMonitor root monitor
' [monitorFile
"a"] 42 "ok"
784 reason2
<- expectMonitorChanged root monitor
' 43
785 reason2
@?
= MonitoredValueChanged
42
787 -- but if a file changed too then we don't report MonitoredValueChanged
788 updateMonitor root monitor
' [monitorFile
"a"] 42 "ok"
789 threadDelay mtimeChange
791 reason3
<- expectMonitorChanged root monitor
' 43
792 reason3
@?
= MonitoredFileChanged
"a"
794 testValueUpdated
:: Assertion
796 withFileMonitor
$ \root monitor
-> do
799 let monitor
' :: FileMonitor
(Set
.Set
Int) String
801 (monitor
:: FileMonitor
(Set
.Set
Int) String)
802 { fileMonitorCheckIfOnlyValueChanged
= True
803 , fileMonitorKeyValid
= Set
.isSubsetOf
806 updateMonitor root monitor
' [monitorFile
"a"] (Set
.fromList
[42, 43]) "ok"
807 (res
, _files
) <- expectMonitorUnchanged root monitor
' (Set
.fromList
[42])
810 reason
<- expectMonitorChanged root monitor
' (Set
.fromList
[42, 44])
811 reason
@?
= MonitoredValueChanged
(Set
.fromList
[42, 43])
816 newtype RootPath
= RootPath
FilePath
818 touchFile
:: RootPath
-> FilePath -> IO ()
819 touchFile
(RootPath root
) fname
= do
820 let path
= root
</> fname
821 IO.createDirectoryIfMissing
True (takeDirectory path
)
822 IO.writeFile path
"touched"
824 touchFileContent
:: RootPath
-> FilePath -> IO ()
825 touchFileContent
(RootPath root
) fname
= do
826 let path
= root
</> fname
827 IO.createDirectoryIfMissing
True (takeDirectory path
)
828 IO.writeFile path
"different"
830 removeFile :: RootPath
-> FilePath -> IO ()
831 removeFile (RootPath root
) fname
= IO.removeFile (root
</> fname
)
833 touchDir
:: RootPath
-> FilePath -> IO ()
834 touchDir root
@(RootPath rootdir
) dname
= do
835 IO.createDirectoryIfMissing
True (rootdir
</> dname
)
836 touchFile root
(dname
</> "touch")
837 removeFile root
(dname
</> "touch")
839 createDir
:: RootPath
-> FilePath -> IO ()
840 createDir
(RootPath root
) dname
= do
841 let path
= root
</> dname
842 IO.createDirectoryIfMissing
True (takeDirectory path
)
843 IO.createDirectory path
845 removeDir
:: RootPath
-> FilePath -> IO ()
846 removeDir
(RootPath root
) dname
= IO.removeDirectoryRecursive
(root
</> dname
)
848 absoluteRoot
:: RootPath
-> IO FilePath
849 absoluteRoot
(RootPath root
) = IO.canonicalizePath root
851 monitorFileGlobStr
:: String -> MonitorFilePath
852 monitorFileGlobStr globstr
853 | Just glob
<- simpleParsec globstr
= monitorFileGlob glob
854 |
otherwise = error $ "Failed to parse " ++ globstr
857 :: (Binary a
, Structured a
, Binary b
, Structured b
)
861 -> IO (MonitorChangedReason a
)
862 expectMonitorChanged root monitor key
= do
863 res
<- checkChanged root monitor key
865 MonitorChanged reason
-> return reason
866 MonitorUnchanged _ _
-> throwIO
$ HUnitFailure Nothing
"expected change"
868 expectMonitorUnchanged
869 :: (Binary a
, Structured a
, Binary b
, Structured b
)
873 -> IO (b
, [MonitorFilePath
])
874 expectMonitorUnchanged root monitor key
= do
875 res
<- checkChanged root monitor key
877 MonitorChanged _reason
-> throwIO
$ HUnitFailure Nothing
"expected no change"
878 MonitorUnchanged b files
-> return (b
, files
)
881 :: (Binary a
, Structured a
, Binary b
, Structured b
)
885 -> IO (MonitorChanged a b
)
886 checkChanged
(RootPath root
) monitor key
=
887 checkFileMonitorChanged monitor root key
890 :: (Binary a
, Structured a
, Binary b
, Structured b
)
897 updateMonitor
(RootPath root
) monitor files key result
=
898 updateFileMonitor monitor root Nothing files key result
900 updateMonitorWithTimestamp
901 :: (Binary a
, Structured a
, Binary b
, Structured b
)
909 updateMonitorWithTimestamp
(RootPath root
) monitor timestamp files key result
=
910 updateFileMonitor monitor root
(Just timestamp
) files key result
912 withFileMonitor
:: Eq a
=> (RootPath
-> FileMonitor a b
-> IO c
) -> IO c
913 withFileMonitor action
= do
914 withTempDirectory silent
"." "file-status-" $ \root
-> do
915 let file
= root
<.> "monitor"
916 monitor
= newFileMonitor file
917 finally
(action
(RootPath root
) monitor
) $ do
918 exists
<- IO.doesFileExist file
919 when exists
$ IO.removeFile file