{-# LANGUAGE QuasiQuotes #-} import Codec.Archive.ZTar import Control.Monad (forM, forM_) import Control.Monad.Extra (unlessM) import qualified Data.ByteString as BS import Data.ByteString.Arbitrary (ArbByteString(..)) import Data.List (dropWhileEnd, intercalate, nub) import Data.Maybe (fromJust) import Path ( Dir , File , Rel , Path , absdir , fromAbsFile , parent , parseRelDir , parseRelFile , () ) import Path.IO (doesFileExist, ensureDir, isLocationOccupied, withTempDir) import Test.QuickCheck import Test.QuickCheck.Monadic import Test.Tasty (defaultMain, testGroup) import Test.Tasty.QuickCheck (testProperty) main :: IO () main = defaultMain $ testGroup "ztar" [ testProperty "Create/extract uncompressed tar archives" $ testZTar NoCompression , testProperty "Create/extract GZip tar archives" $ testZTar GZip , testProperty "Create/extract Zip archives" $ testZTar Zip ] testZTar :: Compression -> Property testZTar compression = monadicIO $ do archive:src:dest:paths <- pick $ uniqueListOf 4 arbitrary files <- forM paths $ \path -> do Blind (ABS contents) <- pick arbitrary return (toRelFile path, contents) run $ withTempDir [absdir|/tmp|] "" $ \dir -> do let archive' = dir toRelFile archive src' = dir toRelDir src dest' = dir toRelDir dest -- write files to be bundled forM_ files $ \(path, contents) -> do let path' = src' path -- case writing `a` when `a/b` already exists unlessM (isLocationOccupied path') $ do ensureDir $ parent path' BS.writeFile (fromAbsFile path') contents -- create and extract archive ensureDir $ parent archive' create' compression archive' src' extract' archive' dest' -- check files fmap and $ forM files $ \(path, contents) -> do let path' = dest' path isExist <- isLocationOccupied path' isFile <- doesFileExist path' case (isExist, isFile) of (False, _) -> return False (True, True) -> do contents' <- BS.readFile $ fromAbsFile path' return $ contents' == contents (True, False) -> return True {- Helpers -} -- | Generate a unique list with length at least N. uniqueListOf :: Eq a => Int -> Gen a -> Gen [a] uniqueListOf 0 gen = nub <$> listOf gen uniqueListOf n gen = do rest <- uniqueListOf (n - 1) gen x <- gen `suchThat` (`notElem` rest) return $ x : rest -- | A valid relative file path. newtype ValidPath = ValidPath { unPath :: FilePath } deriving (Show,Eq) instance Arbitrary ValidPath where arbitrary = ValidPath . truncatePath . intercalate "/" . map unName <$> listOf1 arbitrary where truncatePath = dropWhileEnd (`elem` ['/', '.']) . take 50 toRelFile :: ValidPath -> Path Rel File toRelFile = fromJust . parseRelFile . unPath toRelDir :: ValidPath -> Path Rel Dir toRelDir = fromJust . parseRelDir . unPath -- | A valid file or directory name. newtype ValidName = ValidName { unName :: String } deriving (Show) instance Arbitrary ValidName where arbitrary = do -- https://stackoverflow.com/a/2306003/8565175 name <- take 14 <$> listOf1 (elements validChars) if or [ last name == '.' , name `elem` ["nul"] -- https://superuser.com/questions/259703/get-mac-tar-to-stop-putting-filenames-in-tar-archives , name !! 0 == '.' && name !! 1 == '_' ] then arbitrary else return $ ValidName name where validChars = ['a'..'z'] ++ ['0'..'9'] ++ ['.', '_', '-']