packages feed

filestore 0.4.2 → 0.5

raw patch · 9 files changed

+497/−465 lines, 9 filesdep +filestoredep ~directoryPVP ok

version bump matches the API change (PVP)

Dependencies added: filestore

Dependency ranges changed: directory

API changes (from Hackage documentation)

- Data.FileStore.Types: FileStore :: IO () -> (forall a. Contents a => FilePath -> Author -> Description -> a -> IO ()) -> (forall a. Contents a => FilePath -> Maybe RevisionId -> IO a) -> (FilePath -> Author -> Description -> IO ()) -> (FilePath -> FilePath -> Author -> Description -> IO ()) -> ([FilePath] -> TimeRange -> IO [Revision]) -> (FilePath -> IO RevisionId) -> (RevisionId -> IO Revision) -> IO [FilePath] -> (FilePath -> IO [Resource]) -> (RevisionId -> RevisionId -> Bool) -> (SearchQuery -> IO [SearchMatch]) -> FileStore
+ Data.FileStore.Types: FileStore :: IO () -> (forall a. Contents a => FilePath -> Author -> Description -> a -> IO ()) -> (forall a. Contents a => FilePath -> Maybe RevisionId -> IO a) -> (FilePath -> Author -> Description -> IO ()) -> (FilePath -> FilePath -> Author -> Description -> IO ()) -> ([FilePath] -> TimeRange -> Maybe Int -> IO [Revision]) -> (FilePath -> IO RevisionId) -> (RevisionId -> IO Revision) -> IO [FilePath] -> (FilePath -> IO [Resource]) -> (RevisionId -> RevisionId -> Bool) -> (SearchQuery -> IO [SearchMatch]) -> FileStore
- Data.FileStore.Types: history :: FileStore -> [FilePath] -> TimeRange -> IO [Revision]
+ Data.FileStore.Types: history :: FileStore -> [FilePath] -> TimeRange -> Maybe Int -> IO [Revision]

Files

CHANGES view
@@ -1,3 +1,13 @@+Version 0.5 released 30 Apr 2012++* Added 'limit' parameter to 'history', so that in large repositories+  you don't need to generate and parse the entire log.++* Revised the git log parser, so it is faster and lazy.  This is+  helpful for applications where we may not need to parse the whole+  history.  It would be good to make similar modifications to the hg+  and darcs log parsers in the future.+ Version 0.3.4.3 released 26 Sep 2010  * runShellCommand: reverted to older version with temp files.
Data/FileStore/Darcs.hs view
@@ -117,16 +117,30 @@  -- | Return list of log entries for the list of resources. -- If list of resources is empty, log entries for all resources are returned.-darcsLog :: FilePath -> [FilePath] -> TimeRange -> IO [Revision]-darcsLog repo names (TimeRange begin end) = do-    let opts = timeOpts begin end-    do (status, err, output) <- runDarcsCommand repo "changes" $ ["--xml-output", "--summary"] ++ names ++ opts+darcsLog :: FilePath -> [FilePath] -> TimeRange -> Maybe Int -> IO [Revision]+darcsLog repo names (TimeRange begin end) mblimit = do+       (status, err, output) <- runDarcsCommand repo "changes" $ ["--xml-output", "--summary"] ++ names ++ opts        if status == ExitSuccess         then case parseDarcsXML $ toString output of             Nothing      -> throwIO ResourceExists-            Just parsed -> return parsed+            Just parsed -> return $+#ifdef USE_MAXCOUNT+                              parsed+#else+                              case mblimit of+                                   Just lim -> take lim parsed+                                   Nothing  -> parsed+#endif         else throwIO $ UnknownError $ "darcs changes returned error status.\n" ++ err     where+        opts = timeOpts begin end ++ limit+        limit = case mblimit of+#ifdef USE_MAXCOUNT+                    Just lim  -> ["--max-count",show lim]+#else+                    Just _    -> []+#endif+                    Nothing   -> []         timeOpts :: Maybe UTCTime -> Maybe UTCTime ->[String]         timeOpts b e = case (b,e) of                 (Nothing,Nothing) -> []@@ -160,7 +174,8 @@   let patchs = parseDarcsXML $ toString output   case patchs of       Nothing -> throwIO NotFound-      Just as -> if null as then throwIO NotFound else return $ revId $ head as+      Just [] -> throwIO NotFound+      Just (x:_) -> return $ revId x  -- | Retrieve the contents of a resource. darcsRetrieve :: Contents a
Data/FileStore/Generic.hs view
@@ -104,7 +104,7 @@   let matcher = if exact                 then (== desc)                 else (desc `isInfixOf`)-  revs <- history repo [name] (TimeRange Nothing Nothing)+  revs <- history repo [name] (TimeRange Nothing Nothing) Nothing   return $ Prelude.filter (matcher . revDescription) revs  -- | Try to retrieve a resource from the repository by name and possibly a
Data/FileStore/Git.hs view
@@ -23,8 +23,7 @@ import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import Data.FileStore.Utils (withSanityCheck, hashsMatch, runShellCommand, escapeRegexSpecialChars, withVerifyDir, encodeArg) import Data.ByteString.Lazy.UTF8 (toString)-import qualified Data.ByteString.Lazy as B-import qualified Text.ParserCombinators.Parsec as P+import qualified Data.ByteString.Lazy.Char8 as B import Control.Monad (when) import System.FilePath ((</>)) import System.Directory (createDirectoryIfMissing, doesDirectoryExist, executable, getPermissions, setPermissions)@@ -157,11 +156,7 @@ gitGetRevision repo revid = do   (status, _, output) <- runGitCommand repo "whatchanged" ["-z","--pretty=format:" ++ gitLogFormat, "--max-count=1", revid]   if status == ExitSuccess-     then case P.parse parseGitLog "" (toString output) of-                 Left err'   -> throwIO $ UnknownError $ "error parsing git log: " ++ show err'-                 Right [r]   -> return r-                 Right []    -> throwIO NotFound-                 Right xs    -> throwIO $ UnknownError $ "git rev-list returned more than one result: " ++ show xs+     then parseLogEntry $ B.drop 1 output  -- drop initial \1      else throwIO NotFound  -- | Get a list of all known files inside and managed by a repository.@@ -233,12 +228,12 @@ -}  gitLogFormat :: String-gitLogFormat = "%H%n%ct%n%an%n%ae%n%B%n%x00"+gitLogFormat = "%x01%H%x00%ct%x00%an%x00%ae%x00%B%n%x00"  -- | Return list of log entries for the given time frame and list of resources. -- If list of resources is empty, log entries for all resources are returned.-gitLog :: FilePath -> [FilePath] -> TimeRange -> IO [Revision]-gitLog repo names (TimeRange mbSince mbUntil) = do+gitLog :: FilePath -> [FilePath] -> TimeRange -> Maybe Int -> IO [Revision]+gitLog repo names (TimeRange mbSince mbUntil) mblimit = do   (status, err, output) <- runGitCommand repo "whatchanged" $                            ["-z","--pretty=format:" ++ gitLogFormat] ++                            (case mbSince of@@ -247,54 +242,56 @@                            (case mbUntil of                                  Just til   -> ["--until='" ++ show til ++ "'"]                                  Nothing      -> []) ++-                           ["--"] ++ names +                           (case mblimit of+                                 Just lim   -> ["-n", show lim]+                                 Nothing    -> []) +++                           ["--"] ++ names   if status == ExitSuccess-     then case P.parse parseGitLog "" (toString output) of-                Left err'    -> throwIO $ UnknownError $ "Error parsing git log.\n" ++ show err'-                Right parsed -> return parsed+     then parseGitLog output      else throwIO $ UnknownError $ "git whatchanged returned error status.\n" ++ err  -- -- Parsers to parse git log into Revisions. -- -parseGitLog :: P.Parser [Revision]-parseGitLog = P.manyTill gitLogEntry P.eof--wholeLine :: P.GenParser Char st String-wholeLine = P.manyTill P.anyChar P.newline--nonblankLine :: P.GenParser Char st String-nonblankLine = P.notFollowedBy P.newline >> wholeLine+parseGitLog :: B.ByteString -> IO [Revision]+parseGitLog = mapM parseLogEntry . splitEntries -nullChar :: P.GenParser Char st ()-nullChar = P.satisfy (=='\0') >> return ()+splitEntries :: B.ByteString -> [B.ByteString]+splitEntries = dropWhile B.null . B.split '\1' -- occurs just before each hash -gitLogEntry :: P.Parser Revision-gitLogEntry = do-  rev <- nonblankLine-  date <- nonblankLine-  author <- wholeLine-  email <- wholeLine-  subject <- P.manyTill P.anyChar nullChar-  P.spaces-  changes <- P.manyTill gitLogChange (P.eof P.<|> nullChar)-  let stripTrailingNewlines = reverse . dropWhile (=='\n') . reverse+parseLogEntry :: B.ByteString -> IO Revision+parseLogEntry entry = do+  let (rev : date' : author : email : subject : rest) = B.split '\0' entry+  date <- case B.readInteger date' of+               Just (x,_) -> return x+               Nothing    -> throwIO $ UnknownError $ "Could not read date"+  changes <- parseChanges $ takeWhile (not . B.null) rest   return Revision {-              revId          = rev-            , revDateTime    = posixSecondsToUTCTime $ realToFrac (read date :: Integer)-            , revAuthor      = Author { authorName = author, authorEmail = email }-            , revDescription = stripTrailingNewlines subject+              revId          = toString rev+            , revDateTime    = posixSecondsToUTCTime $ realToFrac date+            , revAuthor      = Author{ authorName = toString author+                                     , authorEmail = toString email }+            , revDescription = toString $ stripTrailingNewlines subject             , revChanges     = changes } -gitLogChange :: P.Parser Change-gitLogChange = do-  line <- P.manyTill P.anyChar nullChar-  let changeType = take 1 $ reverse line-  file' <- P.manyTill P.anyChar nullChar-  case changeType of-         "A"  -> return $ Added file'-         "M"  -> return $ Modified file'-         "D"  -> return $ Deleted file'-         _    -> return $ Modified file'+stripTrailingNewlines :: B.ByteString -> B.ByteString+stripTrailingNewlines = B.reverse . B.dropWhile (=='\n') . B.reverse++parseChanges :: [B.ByteString] -> IO [Change]+parseChanges (x:y:zs) = do+  when (B.null x) $+     throwIO $ UnknownError "parseChanges found empty change description"+  let changeType = B.last x+  let file' = toString y+  let next = case changeType of+           'A'  -> Added file'+           'M'  -> Modified file'+           'D'  -> Deleted file'+           _    -> Modified file'+  rest <- parseChanges zs+  return (next:rest)+parseChanges [_] =+  throwIO $ UnknownError $ "parseChanges encountered odd number of fields"+parseChanges [] = return [] 
Data/FileStore/Mercurial.hs view
@@ -200,9 +200,9 @@  -- | Return list of log entries for the given time frame and list of resources. -- If list of resources is empty, log entries for all resources are returned.-mercurialLog :: FilePath -> [FilePath] -> TimeRange -> IO [Revision]-mercurialLog repo names (TimeRange mbSince mbUntil) = do-  (status, err, output) <- runMercurialCommand repo "log" $ ["--template", mercurialLogFormat] ++ revOpts mbSince mbUntil ++ names+mercurialLog :: FilePath -> [FilePath] -> TimeRange -> Maybe Int -> IO [Revision]+mercurialLog repo names (TimeRange mbSince mbUntil) mblimit = do+  (status, err, output) <- runMercurialCommand repo "log" $ ["--template", mercurialLogFormat] ++ revOpts mbSince mbUntil ++ limit ++ names   if status == ExitSuccess      then case P.parse parseMercurialLog "" (toString output) of                 Left err'    -> throwIO $ UnknownError $ "Error parsing mercurial log.\n" ++ show err'@@ -213,6 +213,9 @@        revOpts (Just s) Nothing  = ["-d", ">" ++ showTime s]        revOpts (Just s) (Just u) = ["-d", showTime s ++ " to " ++ showTime u]        showTime = formatTime defaultTimeLocale "%F %X"+       limit = case mblimit of+                    Just lim  -> ["--limit", show lim]+                    Nothing   -> []   --
Data/FileStore/Types.hs view
@@ -184,6 +184,7 @@   , history        :: [FilePath]        -- List of resources to get history for                                         -- or @[]@ for all.                    -> TimeRange         -- Time range in which to get history.+                   -> Maybe Int         -- Maybe max number of entries.                    -> IO [Revision]      -- | Return the revision ID of the latest change for a resource.
− Tests.lhs
@@ -1,394 +0,0 @@-#!/usr/bin/env runghc--This program runs tests for the filestore modules.--> import Data.FileStore-> import Data.List (sort, isInfixOf)-> import Test.HUnit-> import System.Directory (doesFileExist, removeDirectoryRecursive)-> import Control.Monad (forM)-> import Prelude hiding (catch)-> import Control.Exception (catch)-> import Data.Time-> import Data.Maybe (mapMaybe)-> import System.FilePath-> import System.Process-> import Data.Algorithm.Diff (DI(..))--> main = do->   testFileStore (gitFileStore "tmp/gitfs") "Data.FileStore.Git"->   testFileStore (darcsFileStore "tmp/darcsfs") "Data.FileStore.Darcs"->   testFileStore (mercurialFileStore "tmp/mercurialfs") "Data.FileStore.Mercurial"->   removeDirectoryRecursive "tmp"---> testFileStore :: FileStore -> String -> IO Counts -> testFileStore fs fsName = do->   putStrLn   "**********************************"->   putStrLn $ "Testing " ++ fsName->   putStrLn   "**********************************"->   runTestTT $ TestList $ map (\(label, testFn) -> TestLabel label $ testFn fs) ->     [ ("pre initialize", preInitializeTest)->     , ("initialize", initializeTest)->     , ("create resource", createTest1)->     , ("create resource in subdirectory", createTest2)->     , ("create resource with non-ascii name", createTest3)->     , ("create resource with non-ascii subdirectory", createTest3a)->     , ("try to create resource outside repo", createTest4)->     , ("try to create resource in special directory", createTest5 fsName)->     , ("directory", directoryTest)->     , ("retrieve resource", retrieveTest1)->     , ("retrieve resource in a subdirectory", retrieveTest2)->     , ("retrieve resource with non-ascii name", retrieveTest3)->     , ("retrieve subdirectory (should raise error)", retrieveTest4)->     , ("modify resource", modifyTest)->     , ("delete resource", deleteTest fsName)->     , ("retrieve deleted file", retrieveTest5)->     , ("rename resource", renameTest)->     , ("test for matching IDs", matchTest)->     , ("history and revision", historyTest)->     , ("diff", diffTest)->     , ("search", searchTest)->     ]--> testAuthor :: Author-> testAuthor = Author "Test Suite" "suite@test.org"--> testContents :: String-> testContents = "Test contents.\nSecond line.\nThird test line with some Greek αβ."--> testTitle :: String-> testTitle = "New resource.txt"--> subdirTestTitle :: String-> subdirTestTitle = "subdir/Subdir title.txt"--> nonasciiTestTitle :: String-> nonasciiTestTitle = "αβγ"--> subdirNonasciiTestTitle :: String-> subdirNonasciiTestTitle = "Fooé/bar"--*** index and directory for noexisting repository should raise error:--> preInitializeTest fs = TestCase $ do->   catch (do index fs; assertFailure "preInitialize, uncaught error") $ ->                \e -> assertEqual "error status from attempt to get index of nonexistent repo" e NotFound->   catch (do directory fs "foo"; assertFailure "preInitialize, uncaught error") $ ->                \e -> assertEqual "error status from attempt to get directory of nonexistent repo" e NotFound--*** Initialize a repository, check for empty index, and then try to initialize again-*** in the same directory (should raise an error):--> initializeTest fs = TestCase $ do->   initialize fs->   ind <- index fs->   assertEqual "index of just-initialized repository" ind []->   catch (initialize fs >> assertFailure "did not return error for existing repository") $->     \e -> assertEqual "error status from existing repository" e RepositoryExists--*** Create a resource, and check to see that latest returns a revision ID for it:--> createTest1 fs = TestCase $ do->   create fs testTitle testAuthor "description of change" testContents->   revid <- latest fs testTitle->   assertBool "revision returns a revision after create" (not (null revid))--*** Create a resource in a subdirectory, and check to see that revision returns a revision for it:--> createTest2 fs = TestCase $ do->   create fs subdirTestTitle testAuthor "description of change" testContents->   revid <- latest fs testTitle->   assertBool "revision returns a revision after create" (not (null revid))->   create fs (subdirTestTitle ++ "2") testAuthor "+Second file" testContents--*** Create a resource with a non-ascii title, and check to see that revision returns a revision for it:--> createTest3 fs = TestCase $ do->   create fs nonasciiTestTitle testAuthor "description of change" testContents->   revid <- latest fs nonasciiTestTitle->   assertBool "revision returns a revision after create" (not (null revid))->   allfiles <- index fs->   assertBool "index contains file with nonascii name" (nonasciiTestTitle `elem` allfiles)--> createTest3a fs = TestCase $ do->   create fs subdirNonasciiTestTitle testAuthor "description of change" testContents->   revid <- latest fs subdirNonasciiTestTitle->   assertBool "revision returns a revision after create" (not (null revid))->   allfiles <- index fs->   assertBool "index contains file with nonascii subdir" (subdirNonasciiTestTitle `elem` allfiles)--*** Try to create a resource outside the repository (should fail with an error and NOT write the file):--> createTest4 fs = TestCase $ do->   catch (create fs "../oops" testAuthor "description of change" testContents >>->          assertFailure "did not return error from create ../oops") $->          \e -> assertEqual "error from create ../oops" IllegalResourceName e ->   exists <- doesFileExist "tmp/oops"->   assertBool "file ../oops was created outside repository" (not exists)--*** Try to create a resource in special directory (should fail with an error and NOT write the file):--> createTest5 fsName fs = TestCase $ do->   let (realpath, special) = case fsName of->                             "Data.FileStore.Git"       -> ("tmp" </> "gitfs" </> ".git" </> "newfile", ".git/newfile")->                             "Data.FileStore.Darcs"     -> ("tmp" </> "darcsfs" </> "_darcs" </> "newfile", "_darcs/newfile")->                             "Data.FileStore.Mercurial" -> ("tmp" </> "mercurialfs" </> ".hg" </> "newfile", ".hg/newfile")->                             _                          -> error "Unknown filestore type!  Add a test case."->   catch (create fs special testAuthor "description of change" testContents >>->          (assertFailure  $ "did not return error from create " ++ special)) $->          \e -> assertEqual ("error from create " ++ special) IllegalResourceName e ->   exists <- doesFileExist realpath->   assertBool ("file " ++ realpath ++ " was created outside repository") (not exists)--*** Test directory--> directoryTest fs = TestCase $ do--    Get directory for top-level-->   files <- directory fs ""->   assertEqual "result of directory on top-level" (sort [FSDirectory (takeDirectory subdirTestTitle), FSFile testTitle, FSFile nonasciiTestTitle, FSDirectory (takeDirectory subdirNonasciiTestTitle)]) (sort files)--    Get contents of subdirectory-->   subdirFiles <- directory fs "subdir"->   assertEqual "result of directory on subdir" [FSFile (takeFileName subdirTestTitle), FSFile (takeFileName (subdirTestTitle ++ "2"))] subdirFiles--    Try to get contents of nonexistent subdirectory-->   catch (do directory fs "foo"; assertFailure "nonexistent subdirectory, uncaught error") $ ->                \e -> assertEqual "error status from attempt to get directory listing of nonexistent directory" e NotFound--*** Retrieve latest version of a resource:--> retrieveTest1 fs = TestCase $->   retrieve fs testTitle Nothing >>=->     assertEqual "contents returned by retrieve" testContents--*** Retrieve latest version of a resource (in a subdirectory):--> retrieveTest2 fs = TestCase $->   retrieve fs subdirTestTitle Nothing >>=->     assertEqual "contents returned by retrieve" testContents--*** Retrieve latest version of a resource with a nonascii name:--> retrieveTest3 fs = TestCase $->   retrieve fs nonasciiTestTitle Nothing >>=->     assertEqual "contents returned by retrieve" testContents--*** Retrieve a directory (should fail):--> retrieveTest4 fs = TestCase $->   catch ((retrieve fs "subdir" Nothing :: IO String) >>->          assertFailure "did not return error from retrieve from subdir") $->      \e -> assertEqual "error from retrieve from subdir" NotFound e--*** Modify a resource:--> modifyTest fs = TestCase $ do--    Modify a resource.  Should return Right ().-->   revid <- latest fs testTitle->   let modifiedContents = unlines $ take 2 $ lines testContents->   modResult <- modify fs testTitle revid testAuthor "removed third line" modifiedContents->   assertEqual "results of modify" (Right ()) modResult--    Now retrieve the contents and make sure they were changed.-->   modifiedContents' <- retrieve fs testTitle Nothing->   newRevId <- latest fs testTitle->   newRev <- revision fs newRevId->   assertEqual "retrieved contents after modify" modifiedContents' modifiedContents--    Now try to modify again, using the old revision as base.  This should result in a merge with conflicts.-->   modResult2 <- modify fs testTitle revid testAuthor "modified from old version" (testContents ++ "\nFourth line")->   let normModResult2 = Left MergeInfo {mergeRevision = newRev, mergeConflicts = True, mergeText =->                         "Test contents.\nSecond line.\n<<<<<<< edited\nThird test line with some Greek \945\946.\nFourth line\n=======\n>>>>>>> " ++->                         newRevId ++ "\n"}->   assertEqual "results of modify from old version" normModResult2 modResult2--    Now try it again, still using the old version as base, but with contents of the new version.-    This should result in a merge without conflicts.-->   modResult3 <- modify fs testTitle revid testAuthor "modified from old version" modifiedContents->   let normModResult3 = Left MergeInfo {mergeRevision = newRev, mergeConflicts = False, mergeText = modifiedContents}->   assertEqual "results of modify from old version with new version's contents" normModResult3 modResult3--    Now try modifying again, this time using the new version as base. Should succeed with Right ().-->   modResult4 <- modify fs testTitle newRevId testAuthor "modified from new version" (modifiedContents ++ "\nThird line")->   assertEqual "results of modify from new version" (Right ()) modResult4 --*** Delete a resource:--> deleteTest fsName fs = TestCase $ do--    Create a file and verify that it's there.-->   let toBeDeleted = "Aaack!"->   create fs toBeDeleted testAuthor "description of change" testContents->   ind <- index fs->   assertBool "index contains resource to be deleted" (toBeDeleted `elem` ind) --    Now delete it and verify that it's gone.-->   delete fs toBeDeleted testAuthor "goodbye"->   ind <- index fs->   assertBool "index does not contain resource that was deleted" (toBeDeleted `notElem` ind)--    Now make sure you can create and delete it again.-->   create fs toBeDeleted testAuthor "description of change" testContents->   ind <- index fs->   assertBool "index contains re-created resource" (toBeDeleted `elem` ind) ->   delete fs toBeDeleted testAuthor "goodbye"->   ind <- index fs->   assertBool "index does not contain resource that was deleted" (toBeDeleted `notElem` ind)--    Try to delete a file somewhere we shouldn't be able to delete-->   let (realpath, special) = case fsName of->                             "Data.FileStore.Git"       -> ("tmp" </> "gitfs" </> ".git" </> "newfile", ".git/newfile")->                             "Data.FileStore.Darcs"     -> ("tmp" </> "darcsfs" </> "_darcs" </> "newfile", "_darcs/newfile")->                             "Data.FileStore.Mercurial" -> ("tmp" </> "mercurialfs" </> ".hg" </> "newfile", ".hg/newfile")->                             _                          -> error "Unknown filestore type!  Add a test case."->   catch (delete fs special testAuthor "description of change" >>->          (assertFailure  $ "did not return error from delete " ++ special)) $->          \e -> assertEqual ("error from delete " ++ special) IllegalResourceName e --*** Retrieve earlier version of deleted file:--> retrieveTest5 fs = TestCase $ do->   hist <- history fs ["Aaack!"] (TimeRange Nothing Nothing)->   assertBool "history is nonempty" (not (null hist))->   let deletedId = revId $ last hist->   contents <- retrieve fs "Aaack!" (Just deletedId) :: IO String->   assertEqual "contents returned by retrieve" testContents contents--*** Rename a resource:--> renameTest fs = TestCase $ do--    Create a file and verify that it's there.-->   let oldName = "Old Name"->   let newName = "newdir/New Name.txt"->   create fs oldName testAuthor "description of change" testContents->   ind <- index fs->   assertBool "index contains old name" (oldName `elem` ind) ->   assertBool "index does not contain new name" (newName `notElem` ind)--    Now rename it and verify that it changed names.-->   rename fs oldName newName testAuthor "rename"->   ind <- index fs->   assertBool "index does not contain old name" (oldName `notElem` ind) ->   assertBool "index contains new name" (newName `elem` ind)--    Try renaming a file that doesn't exist.-->   catch (rename fs "nonexistent file" "other name" testAuthor "rename" >>->       assertFailure "rename of nonexistent file did not throw error") $->       \e -> assertEqual "error status from rename of nonexistent file" NotFound e--    Try to rename a file to a location we shouldn't be able to write in.-->   let badName = "../eek"->   let cmd = "rename " ++ newName ++ " " ++ badName->   catch (rename fs newName badName testAuthor "description of change" >>->          (assertFailure  $ "did not return error from " ++ cmd)) $->          \e -> assertEqual ("error from " ++ cmd) IllegalResourceName e ---*** Test history and revision--> historyTest fs = TestCase $ do--    Get history for three files-->   hist <- history fs [testTitle, subdirTestTitle, nonasciiTestTitle] (TimeRange Nothing Nothing)->   assertBool "history is nonempty" (not (null hist))->   now <- getCurrentTime->   rev <- latest fs testTitle >>= revision fs  -- get latest revision->   assertBool "history contains latest revision" (rev `elem` hist)->   assertEqual "revAuthor" testAuthor (revAuthor rev)->   assertBool "revId non-null" (not (null (revId rev)))->   assertBool "revDescription non-null" (not (null (revDescription rev)))->   assertEqual "revChanges" [Modified testTitle] (revChanges rev)->   let revtime = revDateTime rev->   histNow <- history fs [testTitle] (TimeRange (Just $ addUTCTime (60 * 60 * 24) now) Nothing)->   assertBool "history from now + 1 day onwards is empty" (null histNow)--*** Test diff--> diffTest fs = TestCase $ do--  Create a file and modiy it.-->   let diffTitle = "difftest.txt"->   create fs diffTitle testAuthor "description of change" testContents->   save fs diffTitle testAuthor "removed a line" (unlines . init . lines $ testContents)-->   [secondrev, firstrev] <- history fs [diffTitle] (TimeRange Nothing Nothing)->   diff' <- diff fs diffTitle (Just $ revId firstrev) (Just $ revId secondrev)->   let subtracted' = mapMaybe (\(d,s) -> if d == F then Just s else Nothing) diff'->   assertEqual "subtracted lines" [[last (lines testContents)]] subtracted'--    Diff from Nothing should be diff from empty document.-->   diff'' <- diff fs diffTitle Nothing (Just $ revId firstrev)->   let added'' = mapMaybe (\(d,s) -> if d == S then Just s else Nothing) diff''->   assertEqual "added lines from empty document to first revision" [lines testContents] added''--    Diff to Nothing should be diff to latest.-->   diff''' <- diff fs diffTitle (Just $ revId firstrev) Nothing->   assertEqual "diff from first revision to latest" diff' diff'''--*** Test search--> searchTest fs = TestCase $ do--    Search for "bing"-->   create fs "foo" testAuthor "my 1st search test doc" "bing\nbong\nbang\nφ"->   create fs "bar" testAuthor "my 2nd search test doc" "bing BONG" ->   create fs "baz" testAuthor "my 3nd search test doc" "bingbang\nbong"--    Search for "bing" with whole-word matches.-->   res1 <- search fs SearchQuery{queryPatterns = ["bing"], queryWholeWords = True, queryMatchAll = True, queryIgnoreCase = True}->   assertEqual "search results 1" [SearchMatch "bar" 1 "bing BONG", SearchMatch "foo" 1 "bing"] res1--    Search for regex "BONG" case-sensitive.-->   res2 <- search fs SearchQuery{queryPatterns = ["BONG"], queryWholeWords = True, queryMatchAll = True, queryIgnoreCase = False}->   assertEqual "search results 2" [SearchMatch "bar" 1 "bing BONG"] res2--    Search for "bong" and "φ"-->   res3 <- search fs SearchQuery{queryPatterns = ["bong", "φ"], queryWholeWords = True, queryMatchAll = True, queryIgnoreCase = True}->   assertEqual "search results 3" [SearchMatch "foo" 2 "bong", SearchMatch "foo" 4 "φ"] res3--    Search for "bong" and "φ" but without match-all set-->   res4 <- search fs SearchQuery{queryPatterns = ["bong", "φ"], queryWholeWords = True, queryMatchAll = False, queryIgnoreCase = True}->   assertEqual "search results 4" [SearchMatch "bar" 1 "bing BONG", SearchMatch "baz" 2 "bong", SearchMatch "foo" 2 "bong", SearchMatch "foo" 4 "φ"] res4--    Search for "bing" but without whole-words set-->   res5 <- search fs SearchQuery{queryPatterns = ["bing"], queryWholeWords = False, queryMatchAll = True, queryIgnoreCase = True}->   assertEqual "search results 5" [SearchMatch "bar" 1 "bing BONG", SearchMatch "baz" 1 "bingbang", SearchMatch "foo" 1 "bing"] res5--*** Test IDs match--> matchTest fs = TestCase $ do-->   assertBool "match with two identical IDs" (idsMatch fs "abcde" "abcde")->   assertBool "match with nonidentical but matching IDs" (idsMatch fs "abcde" "abcde5553")->   assertBool "non-match" (not (idsMatch fs "abcde" "abedc"))-
filestore.cabal view
@@ -1,6 +1,6 @@ Name:                filestore-Version:             0.4.2-Cabal-version:       >= 1.2+Version:             0.5+Cabal-version:       >= 1.8 Build-type:          Custom Synopsis:            Interface for versioning file stores. Description:         The filestore library provides an abstract interface for a versioning@@ -13,15 +13,13 @@ License-File:        LICENSE Author:              John MacFarlane, Gwern Branwen, Sebastiaan Visser Maintainer:          jgm@berkeley.edu-Homepage:            http://johnmacfarlane.net/repos/filestore+Bug-Reports:         https://github.com/jgm/filestore/issues  Data-Files:          extra/post-update, CHANGES-Extra-source-files:  Tests.lhs --- Commented out until Cabal 1.6 becomes common.--- Source-repository head---    Type:            darcs---    Location:        http://johnmacfarlane.net/repos/filestore+Source-repository head+  type:          git+  location:      git://github.com/jgm/filestore.git  Flag tests     default:    False@@ -67,10 +65,17 @@  Executable test-filestore     if flag(tests)-       Buildable: True+       Buildable:   True     else-       Buildable: False-    Main-is: Tests.lhs-    Build-depends:  base >= 4 && < 5, HUnit >= 1.2 && < 1.3, mtl,-                    time, Diff+       Buildable:   False+    Hs-source-dirs: tests+    Main-is:        Tests.lhs+    Build-depends:  base >= 4 && < 5,+                    HUnit >= 1.2 && < 1.3,+                    mtl,+                    time,+                    Diff,+                    filepath >= 1.1 && < 1.4,+                    directory >= 1.1 && < 1.2,+                    filestore 
+ tests/Tests.lhs view
@@ -0,0 +1,395 @@+#!/usr/bin/env runghc++This program runs tests for the filestore modules.++> import Data.FileStore+> import Data.List (sort, isInfixOf)+> import Test.HUnit+> import System.Directory (doesFileExist, removeDirectoryRecursive)+> import Control.Monad (forM)+> import Prelude hiding (catch)+> import Control.Exception (catch)+> import Data.Time+> import Data.Maybe (mapMaybe)+> import System.FilePath+> import Data.Algorithm.Diff (DI(..))++> main = do+>   testFileStore (gitFileStore "tmp/gitfs") "Data.FileStore.Git"+>   testFileStore (darcsFileStore "tmp/darcsfs") "Data.FileStore.Darcs"+>   testFileStore (mercurialFileStore "tmp/mercurialfs") "Data.FileStore.Mercurial"+>   removeDirectoryRecursive "tmp"+++> testFileStore :: FileStore -> String -> IO Counts +> testFileStore fs fsName = do+>   putStrLn   "**********************************"+>   putStrLn $ "Testing " ++ fsName+>   putStrLn   "**********************************"+>   runTestTT $ TestList $ map (\(label, testFn) -> TestLabel label $ testFn fs) +>     [ ("pre initialize", preInitializeTest)+>     , ("initialize", initializeTest)+>     , ("create resource", createTest1)+>     , ("create resource in subdirectory", createTest2)+>     , ("create resource with non-ascii name", createTest3)+>     , ("create resource with non-ascii subdirectory", createTest3a)+>     , ("try to create resource outside repo", createTest4)+>     , ("try to create resource in special directory", createTest5 fsName)+>     , ("directory", directoryTest)+>     , ("retrieve resource", retrieveTest1)+>     , ("retrieve resource in a subdirectory", retrieveTest2)+>     , ("retrieve resource with non-ascii name", retrieveTest3)+>     , ("retrieve subdirectory (should raise error)", retrieveTest4)+>     , ("modify resource", modifyTest)+>     , ("delete resource", deleteTest fsName)+>     , ("retrieve deleted file", retrieveTest5)+>     , ("rename resource", renameTest)+>     , ("test for matching IDs", matchTest)+>     , ("history and revision", historyTest)+>     , ("diff", diffTest)+>     , ("search", searchTest)+>     ]++> testAuthor :: Author+> testAuthor = Author "Test Suite" "suite@test.org"++> testContents :: String+> testContents = "Test contents.\nSecond line.\nThird test line with some Greek αβ."++> testTitle :: String+> testTitle = "New resource.txt"++> subdirTestTitle :: String+> subdirTestTitle = "subdir/Subdir title.txt"++> nonasciiTestTitle :: String+> nonasciiTestTitle = "αβγ"++> subdirNonasciiTestTitle :: String+> subdirNonasciiTestTitle = "Fooé/bar"++*** index and directory for noexisting repository should raise error:++> preInitializeTest fs = TestCase $ do+>   catch (do index fs; assertFailure "preInitialize, uncaught error") $ +>                \e -> assertEqual "error status from attempt to get index of nonexistent repo" e NotFound+>   catch (do directory fs "foo"; assertFailure "preInitialize, uncaught error") $ +>                \e -> assertEqual "error status from attempt to get directory of nonexistent repo" e NotFound++*** Initialize a repository, check for empty index, and then try to initialize again+*** in the same directory (should raise an error):++> initializeTest fs = TestCase $ do+>   initialize fs+>   ind <- index fs+>   assertEqual "index of just-initialized repository" ind []+>   catch (initialize fs >> assertFailure "did not return error for existing repository") $+>     \e -> assertEqual "error status from existing repository" e RepositoryExists++*** Create a resource, and check to see that latest returns a revision ID for it:++> createTest1 fs = TestCase $ do+>   create fs testTitle testAuthor "description of change" testContents+>   revid <- latest fs testTitle+>   assertBool "revision returns a revision after create" (not (null revid))++*** Create a resource in a subdirectory, and check to see that revision returns a revision for it:++> createTest2 fs = TestCase $ do+>   create fs subdirTestTitle testAuthor "description of change" testContents+>   revid <- latest fs testTitle+>   assertBool "revision returns a revision after create" (not (null revid))+>   create fs (subdirTestTitle ++ "2") testAuthor "+Second file" testContents++*** Create a resource with a non-ascii title, and check to see that revision returns a revision for it:++> createTest3 fs = TestCase $ do+>   create fs nonasciiTestTitle testAuthor "description of change" testContents+>   revid <- latest fs nonasciiTestTitle+>   assertBool "revision returns a revision after create" (not (null revid))+>   allfiles <- index fs+>   assertBool "index contains file with nonascii name" (nonasciiTestTitle `elem` allfiles)++> createTest3a fs = TestCase $ do+>   create fs subdirNonasciiTestTitle testAuthor "description of change" testContents+>   revid <- latest fs subdirNonasciiTestTitle+>   assertBool "revision returns a revision after create" (not (null revid))+>   allfiles <- index fs+>   assertBool "index contains file with nonascii subdir" (subdirNonasciiTestTitle `elem` allfiles)++*** Try to create a resource outside the repository (should fail with an error and NOT write the file):++> createTest4 fs = TestCase $ do+>   catch (create fs "../oops" testAuthor "description of change" testContents >>+>          assertFailure "did not return error from create ../oops") $+>          \e -> assertEqual "error from create ../oops" IllegalResourceName e +>   exists <- doesFileExist "tmp/oops"+>   assertBool "file ../oops was created outside repository" (not exists)++*** Try to create a resource in special directory (should fail with an error and NOT write the file):++> createTest5 fsName fs = TestCase $ do+>   let (realpath, special) = case fsName of+>                             "Data.FileStore.Git"       -> ("tmp" </> "gitfs" </> ".git" </> "newfile", ".git/newfile")+>                             "Data.FileStore.Darcs"     -> ("tmp" </> "darcsfs" </> "_darcs" </> "newfile", "_darcs/newfile")+>                             "Data.FileStore.Mercurial" -> ("tmp" </> "mercurialfs" </> ".hg" </> "newfile", ".hg/newfile")+>                             _                          -> error "Unknown filestore type!  Add a test case."+>   catch (create fs special testAuthor "description of change" testContents >>+>          (assertFailure  $ "did not return error from create " ++ special)) $+>          \e -> assertEqual ("error from create " ++ special) IllegalResourceName e +>   exists <- doesFileExist realpath+>   assertBool ("file " ++ realpath ++ " was created outside repository") (not exists)++*** Test directory++> directoryTest fs = TestCase $ do++    Get directory for top-level++>   files <- directory fs ""+>   assertEqual "result of directory on top-level" (sort [FSDirectory (takeDirectory subdirTestTitle), FSFile testTitle, FSFile nonasciiTestTitle, FSDirectory (takeDirectory subdirNonasciiTestTitle)]) (sort files)++    Get contents of subdirectory++>   subdirFiles <- directory fs "subdir"+>   assertEqual "result of directory on subdir" [FSFile (takeFileName subdirTestTitle), FSFile (takeFileName (subdirTestTitle ++ "2"))] subdirFiles++    Try to get contents of nonexistent subdirectory++>   catch (do directory fs "foo"; assertFailure "nonexistent subdirectory, uncaught error") $ +>                \e -> assertEqual "error status from attempt to get directory listing of nonexistent directory" e NotFound++*** Retrieve latest version of a resource:++> retrieveTest1 fs = TestCase $+>   retrieve fs testTitle Nothing >>=+>     assertEqual "contents returned by retrieve" testContents++*** Retrieve latest version of a resource (in a subdirectory):++> retrieveTest2 fs = TestCase $+>   retrieve fs subdirTestTitle Nothing >>=+>     assertEqual "contents returned by retrieve" testContents++*** Retrieve latest version of a resource with a nonascii name:++> retrieveTest3 fs = TestCase $+>   retrieve fs nonasciiTestTitle Nothing >>=+>     assertEqual "contents returned by retrieve" testContents++*** Retrieve a directory (should fail):++> retrieveTest4 fs = TestCase $+>   catch ((retrieve fs "subdir" Nothing :: IO String) >>+>          assertFailure "did not return error from retrieve from subdir") $+>      \e -> assertEqual "error from retrieve from subdir" NotFound e++*** Modify a resource:++> modifyTest fs = TestCase $ do++    Modify a resource.  Should return Right ().++>   revid <- latest fs testTitle+>   let modifiedContents = unlines $ take 2 $ lines testContents+>   modResult <- modify fs testTitle revid testAuthor "removed third line" modifiedContents+>   assertEqual "results of modify" (Right ()) modResult++    Now retrieve the contents and make sure they were changed.++>   modifiedContents' <- retrieve fs testTitle Nothing+>   newRevId <- latest fs testTitle+>   newRev <- revision fs newRevId+>   assertEqual "retrieved contents after modify" modifiedContents' modifiedContents++    Now try to modify again, using the old revision as base.  This should result in a merge with conflicts.++>   modResult2 <- modify fs testTitle revid testAuthor "modified from old version" (testContents ++ "\nFourth line")+>   let normModResult2 = Left MergeInfo {mergeRevision = newRev, mergeConflicts = True, mergeText =+>                         "Test contents.\nSecond line.\n<<<<<<< edited\nThird test line with some Greek \945\946.\nFourth line\n=======\n>>>>>>> " +++>                         newRevId ++ "\n"}+>   assertEqual "results of modify from old version" normModResult2 modResult2++    Now try it again, still using the old version as base, but with contents of the new version.+    This should result in a merge without conflicts.++>   modResult3 <- modify fs testTitle revid testAuthor "modified from old version" modifiedContents+>   let normModResult3 = Left MergeInfo {mergeRevision = newRev, mergeConflicts = False, mergeText = modifiedContents}+>   assertEqual "results of modify from old version with new version's contents" normModResult3 modResult3++    Now try modifying again, this time using the new version as base. Should succeed with Right ().++>   modResult4 <- modify fs testTitle newRevId testAuthor "modified from new version" (modifiedContents ++ "\nThird line")+>   assertEqual "results of modify from new version" (Right ()) modResult4 ++*** Delete a resource:++> deleteTest fsName fs = TestCase $ do++    Create a file and verify that it's there.++>   let toBeDeleted = "Aaack!"+>   create fs toBeDeleted testAuthor "description of change" testContents+>   ind <- index fs+>   assertBool "index contains resource to be deleted" (toBeDeleted `elem` ind) ++    Now delete it and verify that it's gone.++>   delete fs toBeDeleted testAuthor "goodbye"+>   ind <- index fs+>   assertBool "index does not contain resource that was deleted" (toBeDeleted `notElem` ind)++    Now make sure you can create and delete it again.++>   create fs toBeDeleted testAuthor "description of change" testContents+>   ind <- index fs+>   assertBool "index contains re-created resource" (toBeDeleted `elem` ind) +>   delete fs toBeDeleted testAuthor "goodbye"+>   ind <- index fs+>   assertBool "index does not contain resource that was deleted" (toBeDeleted `notElem` ind)++    Try to delete a file somewhere we shouldn't be able to delete++>   let (realpath, special) = case fsName of+>                             "Data.FileStore.Git"       -> ("tmp" </> "gitfs" </> ".git" </> "newfile", ".git/newfile")+>                             "Data.FileStore.Darcs"     -> ("tmp" </> "darcsfs" </> "_darcs" </> "newfile", "_darcs/newfile")+>                             "Data.FileStore.Mercurial" -> ("tmp" </> "mercurialfs" </> ".hg" </> "newfile", ".hg/newfile")+>                             _                          -> error "Unknown filestore type!  Add a test case."+>   catch (delete fs special testAuthor "description of change" >>+>          (assertFailure  $ "did not return error from delete " ++ special)) $+>          \e -> assertEqual ("error from delete " ++ special) IllegalResourceName e ++*** Retrieve earlier version of deleted file:++> retrieveTest5 fs = TestCase $ do+>   hist <- history fs ["Aaack!"] (TimeRange Nothing Nothing) Nothing+>   assertBool "history is nonempty" (not (null hist))+>   let deletedId = revId $ last hist+>   contents <- retrieve fs "Aaack!" (Just deletedId) :: IO String+>   assertEqual "contents returned by retrieve" testContents contents++*** Rename a resource:++> renameTest fs = TestCase $ do++    Create a file and verify that it's there.++>   let oldName = "Old Name"+>   let newName = "newdir/New Name.txt"+>   create fs oldName testAuthor "description of change" testContents+>   ind <- index fs+>   assertBool "index contains old name" (oldName `elem` ind) +>   assertBool "index does not contain new name" (newName `notElem` ind)++    Now rename it and verify that it changed names.++>   rename fs oldName newName testAuthor "rename"+>   ind <- index fs+>   assertBool "index does not contain old name" (oldName `notElem` ind) +>   assertBool "index contains new name" (newName `elem` ind)++    Try renaming a file that doesn't exist.++>   catch (rename fs "nonexistent file" "other name" testAuthor "rename" >>+>       assertFailure "rename of nonexistent file did not throw error") $+>       \e -> assertEqual "error status from rename of nonexistent file" NotFound e++    Try to rename a file to a location we shouldn't be able to write in.++>   let badName = "../eek"+>   let cmd = "rename " ++ newName ++ " " ++ badName+>   catch (rename fs newName badName testAuthor "description of change" >>+>          (assertFailure  $ "did not return error from " ++ cmd)) $+>          \e -> assertEqual ("error from " ++ cmd) IllegalResourceName e +++*** Test history and revision++> historyTest fs = TestCase $ do++    Get history for three files++>   hist <- history fs [testTitle, subdirTestTitle, nonasciiTestTitle] (TimeRange Nothing Nothing) Nothing+>   assertBool "history is nonempty" (not (null hist))+>   now <- getCurrentTime+>   rev <- latest fs testTitle >>= revision fs  -- get latest revision+>   assertBool "history contains latest revision" (rev `elem` hist)+>   assertEqual "revAuthor" testAuthor (revAuthor rev)+>   assertBool "revId non-null" (not (null (revId rev)))+>   assertBool "revDescription non-null" (not (null (revDescription rev)))+>   assertEqual "revChanges" [Modified testTitle] (revChanges rev)+>   let revtime = revDateTime rev+>   histNow <- history fs [testTitle] (TimeRange (Just $ addUTCTime (60 * 60 * 24) now) Nothing) Nothing+>   assertBool "history from now + 1 day onwards is empty" (null histNow)+>   histOne <- history fs [testTitle] (TimeRange Nothing Nothing) (Just 1)+>   assertBool "history with limit = 1 contains one item" (length histOne == 1)++*** Test diff++> diffTest fs = TestCase $ do++  Create a file and modiy it.++>   let diffTitle = "difftest.txt"+>   create fs diffTitle testAuthor "description of change" testContents+>   save fs diffTitle testAuthor "removed a line" (unlines . init . lines $ testContents)++>   [secondrev, firstrev] <- history fs [diffTitle] (TimeRange Nothing Nothing) Nothing+>   diff' <- diff fs diffTitle (Just $ revId firstrev) (Just $ revId secondrev)+>   let subtracted' = mapMaybe (\(d,s) -> if d == F then Just s else Nothing) diff'+>   assertEqual "subtracted lines" [[last (lines testContents)]] subtracted'++    Diff from Nothing should be diff from empty document.++>   diff'' <- diff fs diffTitle Nothing (Just $ revId firstrev)+>   let added'' = mapMaybe (\(d,s) -> if d == S then Just s else Nothing) diff''+>   assertEqual "added lines from empty document to first revision" [lines testContents] added''++    Diff to Nothing should be diff to latest.++>   diff''' <- diff fs diffTitle (Just $ revId firstrev) Nothing+>   assertEqual "diff from first revision to latest" diff' diff'''++*** Test search++> searchTest fs = TestCase $ do++    Search for "bing"++>   create fs "foo" testAuthor "my 1st search test doc" "bing\nbong\nbang\nφ"+>   create fs "bar" testAuthor "my 2nd search test doc" "bing BONG" +>   create fs "baz" testAuthor "my 3nd search test doc" "bingbang\nbong"++    Search for "bing" with whole-word matches.++>   res1 <- search fs SearchQuery{queryPatterns = ["bing"], queryWholeWords = True, queryMatchAll = True, queryIgnoreCase = True}+>   assertEqual "search results 1" [SearchMatch "bar" 1 "bing BONG", SearchMatch "foo" 1 "bing"] res1++    Search for regex "BONG" case-sensitive.++>   res2 <- search fs SearchQuery{queryPatterns = ["BONG"], queryWholeWords = True, queryMatchAll = True, queryIgnoreCase = False}+>   assertEqual "search results 2" [SearchMatch "bar" 1 "bing BONG"] res2++    Search for "bong" and "φ"++>   res3 <- search fs SearchQuery{queryPatterns = ["bong", "φ"], queryWholeWords = True, queryMatchAll = True, queryIgnoreCase = True}+>   assertEqual "search results 3" [SearchMatch "foo" 2 "bong", SearchMatch "foo" 4 "φ"] res3++    Search for "bong" and "φ" but without match-all set++>   res4 <- search fs SearchQuery{queryPatterns = ["bong", "φ"], queryWholeWords = True, queryMatchAll = False, queryIgnoreCase = True}+>   assertEqual "search results 4" [SearchMatch "bar" 1 "bing BONG", SearchMatch "baz" 2 "bong", SearchMatch "foo" 2 "bong", SearchMatch "foo" 4 "φ"] res4++    Search for "bing" but without whole-words set++>   res5 <- search fs SearchQuery{queryPatterns = ["bing"], queryWholeWords = False, queryMatchAll = True, queryIgnoreCase = True}+>   assertEqual "search results 5" [SearchMatch "bar" 1 "bing BONG", SearchMatch "baz" 1 "bingbang", SearchMatch "foo" 1 "bing"] res5++*** Test IDs match++> matchTest fs = TestCase $ do++>   assertBool "match with two identical IDs" (idsMatch fs "abcde" "abcde")+>   assertBool "match with nonidentical but matching IDs" (idsMatch fs "abcde" "abcde5553")+>   assertBool "non-match" (not (idsMatch fs "abcde" "abedc"))+