-- | Repository model module Darcs.Test.Patch.V1Model ( V1Model, repoTree , RepoItem, File, Dir, Content , makeRepo, emptyRepo , makeFile, emptyFile , emptyDir , nullRepo , isFile, isDir , fileContent, dirContent , isEmpty , root , filterFiles, filterDirs , find , list , ap2fp , aFilename, aDirname , aLine, aContent , aFile, aDir , aRepo ) where import Prelude () import Darcs.Prelude import Darcs.Test.Util.QuickCheck ( alpha, uniques, bSized ) import Darcs.Test.Patch.RepoModel import Darcs.Patch.Apply( applyToTree ) import Darcs.Patch.Witnesses.Sealed ( Sealed, seal ) import Darcs.Patch.Witnesses.Show import Darcs.Util.Path import Darcs.Util.Tree( Tree, TreeItem ) import Darcs.Util.Tree.Hashed ( darcsUpdateHashes ) import qualified Darcs.Util.Tree as T import Control.Arrow ( second ) import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as BC import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy.Char8 as BLC import qualified Data.Map as M import Test.QuickCheck ( Arbitrary(..) , Gen, choose, vectorOf, frequency ) -- import Text.Show.Pretty ( ppShow ) ---------------------------------------------------------------------- -- * Model definition -- | A repository is an abstraction build in top of a 'Tree'. -- NB: Repository preferences are not supported yet. newtype V1Model wX = V1Model { repoTree :: Tree Fail } -- | Repository items may be text files or directories. -- NB: Binary files are not supported yet. newtype RepoItem = RepoItem { treeItem :: TreeItem Fail } type File = RepoItem type Dir = RepoItem type Content = [B.ByteString] ---------------------------------------- -- Instances data FlatItem = Dir FilePath | File FilePath [String] deriving Show flattenTree :: Tree Fail -> [FlatItem] flattenTree = map flattenEntry . T.list where flattenEntry (fn, T.SubTree _) = Dir (BC.unpack (flatten fn)) flattenEntry (fn, T.File blob) = File (BC.unpack (flatten fn)) (map BLC.unpack $ BLC.lines $ unFail $ T.readBlob blob) flattenEntry (_, _) = impossible instance Show (V1Model wX) where show repo = "V1Model " ++ show (flattenTree (repoTree repo)) instance Show1 V1Model where showDict1 = ShowDictClass ---------------------------------------- -- Utils bs2lbs :: B.ByteString -> BL.ByteString bs2lbs bs = BL.fromChunks [bs] lbs2bs :: BL.ByteString -> B.ByteString lbs2bs = B.concat . BL.toChunks content2lbs :: Content -> BL.ByteString content2lbs = BLC.unlines . map bs2lbs lbs2content :: BL.ByteString -> Content lbs2content = map lbs2bs . BLC.lines ---------------------------------------------------------------------- -- ** Path conversion ap2fp :: AnchoredPath -> FilePath ap2fp = anchorPath "" ---------------------------------------------------------------------- -- * Constructors makeRepo :: [(Name, RepoItem)] -> V1Model wX makeRepo = V1Model . T.makeTree . map (second treeItem) emptyRepo :: V1Model wX emptyRepo = V1Model T.emptyTree makeFile :: Content -> File makeFile = RepoItem . T.File . T.makeBlob . content2lbs emptyFile :: File emptyFile = RepoItem $ T.File T.emptyBlob emptyDir :: Dir emptyDir = RepoItem $ T.SubTree T.emptyTree ---------------------------------------------------------------------- -- * Queries nullRepo :: V1Model wX -> Bool nullRepo = M.null . T.items . repoTree isFile :: RepoItem -> Bool isFile (RepoItem (T.File _)) = True isFile _other = False isDir :: RepoItem -> Bool isDir (RepoItem (T.SubTree _)) = True isDir _other = False fileContent :: File -> Content fileContent (RepoItem (T.File blob)) = lbs2content $ unFail $ T.readBlob blob fileContent _other = error "fileContent: Not a file." dirContent :: Dir -> [(Name, RepoItem)] dirContent (RepoItem (T.SubTree subtree)) = map (second RepoItem) $ M.toList $ T.items subtree dirContent _other = error "dirContent: Not a directory." -- | @isEmpty file@ <=> file content is empty -- @isEmpty dir@ <=> dir has no child isEmpty :: RepoItem -> Bool isEmpty item | isFile item = null $ fileContent item | isDir item = null $ dirContent item | otherwise = undefined -- | The root directory of a repository. root :: V1Model wX -> Dir root = RepoItem . T.SubTree . repoTree find :: V1Model wX -> AnchoredPath -> Maybe RepoItem find (V1Model tree) path = RepoItem <$> T.find tree path -- | List repository items. -- NB: It does not include the root directory. list :: V1Model wX -> [(AnchoredPath, RepoItem)] list (V1Model tree) = map (second RepoItem) $ T.list tree ---------------------------------------------------------------------- -- ** Filtering filterFiles :: [(n, RepoItem)] -> [(n, File)] filterFiles = filter (isFile . snd) filterDirs :: [(n, RepoItem)] -> [(n, Dir)] filterDirs = filter (isDir . snd) ---------------------------------------------------------------------- -- * Comparing repositories diffRepos :: V1Model wX -> V1Model wY -> (V1Model wU, V1Model wV) diffRepos repo1 repo2 = let (diff1,diff2) = unFail $ T.diffTrees hashedTree1 hashedTree2 in (V1Model diff1, V1Model diff2) where hashedTree1, hashedTree2 :: Tree Fail hashedTree1 = unFail $ darcsUpdateHashes $ repoTree repo1 hashedTree2 = unFail $ darcsUpdateHashes $ repoTree repo2 ---------------------------------------------------------------------- -- * Patch application ---------------------------------------------------------------------- -- * QuickCheck generators -- Testing code assumes that aFilename and aDirname generators -- will always be able to generate a unique name given a list of -- existing names. This should be OK as long as the number of possible -- file/dirnames is much bigger than the number of files/dirs per repository. -- 'Arbitrary' 'V1Model' instance is based on the 'aSmallRepo' generator. -- | Files are distinguish by ending their names with ".txt". aFilename :: Gen Name aFilename = do len <- choose (1,maxLength) name <- vectorOf len alpha return $ makeName (name ++ ".txt") where maxLength = 3 aDirname :: Gen Name aDirname = do len <- choose (1,maxLength) name <- vectorOf len alpha return $ makeName name where maxLength = 3 aWord :: Gen B.ByteString aWord = do c <- alpha return $ BC.pack[c] aLine :: Gen B.ByteString aLine = do wordsNo <- choose (1,2) ws <- vectorOf wordsNo aWord return $ BC.unwords ws aContent :: Gen Content aContent = bSized 0 0.5 80 $ \k -> do n <- choose (0,k) vectorOf n aLine aFile :: Gen File aFile = makeFile <$> aContent -- | See 'aRepo', the same applies for 'aDir'. aDir :: Int -- ^ Maximum number of files -> Int -- ^ Maximum number of directories -> Gen Dir aDir filesL dirL = root <$> aRepo filesL dirL -- | @aRepo filesNo dirsNo@ produces repositories with *at most* -- @filesNo@ files and @dirsNo@ directories. -- The structure of the repository is aleatory. aRepo :: Int -- ^ Maximum number of files -> Int -- ^ Maximum number of directories -> Gen (V1Model wX) aRepo maxFiles maxDirs = do let minFiles = if maxDirs == 0 && maxFiles > 0 then 1 else 0 filesNo <- choose (minFiles,maxFiles) let minDirs = if filesNo == 0 && maxDirs > 0 then 1 else 0 dirsNo <- choose (minDirs,maxDirs) -- NB: Thanks to laziness we don't need to care about division by zero -- since if dirsNo == 0 then neither filesPerDirL nor subdirsPerDirL will -- be evaluated. let filesPerDirL = (maxFiles-filesNo) `div` dirsNo subdirsPerDirL = (maxDirs-dirsNo) `div` dirsNo files <- vectorOf filesNo aFile filenames <- uniques filesNo aFilename dirs <- vectorOf dirsNo (aDir filesPerDirL subdirsPerDirL) dirnames <- uniques dirsNo aDirname return $ makeRepo (filenames `zip` files ++ dirnames `zip` dirs) -- | Generate small repositories. -- Small repositories help generating (potentially) conflicting patches. instance RepoModel V1Model where type RepoState V1Model = Tree showModel m = show {- ppShow -} m aSmallRepo = do filesNo <- frequency [(3, return 1), (1, return 2)] dirsNo <- frequency [(3, return 1), (1, return 0)] aRepo filesNo dirsNo repoApply (V1Model tree) patch = V1Model <$> applyToTree patch tree eqModel repo1 repo2 = let (diff1,diff2) = diffRepos repo1 repo2 in nullRepo diff1 && nullRepo diff2 instance Arbitrary (Sealed V1Model) where arbitrary = seal <$> aSmallRepo