packages feed

cached-0.1.0.0: test/Test/Util.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE AllowAmbiguousTypes #-}


module Test.Util where

import Protolude
import qualified Data.List as List
import qualified Data.Map as Map
import qualified Data.Set as Set
import Data.Text (unpack, pack)
import System.Directory
import System.FilePath
import Test.QuickCheck
import Test.QuickCheck.Assertions
import Test.QuickCheck.Test

import Data.Cached
import Data.Cached.Internal

checkOrExit :: (Testable prop) => Text -> prop -> IO ()
checkOrExit msg p = do
  success <- fmap isSuccess $ quickCheckResult
             $ label (unpack msg)
             $ counterexample ("Failing property: " ++ unpack msg)
             p
  when (not success) exitFailure

-- Make sure the given file does not exist, delete if necessary.
rmFile :: FilePath -> IO ()
rmFile f = do
    System.Directory.doesFileExist f
    >>= bool (return ()) (removeFile f)

-- Make sure the given directory does not exist, delete recursively if necessary.
rmRec:: FilePath -> IO ()
rmRec dir = removePathForcibly dir

-- Set a directory to contain only the given files and their content
setDir :: FilePath -> [(FilePath, Text)] -> IO ()
setDir rootDir content = do
  rmRec rootDir
  createDirectory rootDir
  foldMap (\(p, t) -> do
            createDirectoryIfMissing True (takeDirectory p)
            writeFile p t)
          content

-- Turn a cache into a value that can be inspected. This function
-- runs the IO actions that are contained in the cache (cachedValue
-- and Build), and expects that the IO actions only read and write files,
-- and that the IO actions in the build only writes to files that are
-- specified as targets (the Build keys).
materialize :: Cached a
            -> IO (Either Text a, Set FilePath,
                   Map FilePath (Either Text Text, Set FilePath))
materialize a = do
  let needs = cacheNeeds a
  let runAndReadBack f ioe = ioe >>= \e -> readFile f >>= \r -> return (r <$ e)
  build <- Map.traverseWithKey
             (\k (ac, n) -> runAndReadBack k (runExceptT ac)
                        >>= \r -> return (r,n))
             (getBuild $ cacheBuild a)
  val <- getValue a
  return (val, needs, build)

-- Test the value, needs, dependencies of a given cached value, and the content written to files.
testCached :: (Eq a, Show a)
          => Cached a
          -> Either Text a
          -> Set FilePath
          -> Map FilePath (Either Text Text, Set FilePath)
          -> IO Property
testCached c value needs build = do
  (val', needs', build') <- materialize c
  return $ value ==? val'
      .&&. needs ==? needs'
      .&&. build ==? build


-- Test equality of two cached values: values, needs, and builds. Runs all IO actions in the cache.
eqCached :: (Eq a, Show a) => Cached a -> Cached a -> IO Property
eqCached a b = property <$> ((==?) <$> materialize a <*> materialize b)

newtype Seed = Seed Int deriving (Show)

instance Arbitrary Seed where
  arbitrary = Seed <$> choose (-100000, 100000)

newtype SimpleFileName = SimpleFileName {getSimpleFileName :: FilePath}
  deriving (Eq, Show, Ord)

instance Arbitrary SimpleFileName where
  arbitrary = scale (min 10)
            $ SimpleFileName
          <$> genStr `suchThat` (not . flip elem [".", ".."])
    where genStr :: Gen FilePath
          genStr = listOf1 $ elements
                 $ (['a'..'z'] ++ ['A'..'Z'] ++ ['_', '-', '.'])

newtype SimplePath = SimplePath {getSimplePath :: FilePath}
  deriving (Show, Ord, Eq)

instance Arbitrary SimplePath where
  arbitrary = SimplePath . intercalate "/"
          <$> scale (min 10)
              (listOf1 $ fmap getSimpleFileName arbitrary )

newtype DirectoryTree = DT (Map SimpleFileName DirectoryTree)
  deriving (Eq, Show, Ord)

instance Arbitrary DirectoryTree where
  arbitrary = sized genDirectoryTree

genDirectoryTree :: Int -> Gen DirectoryTree
genDirectoryTree 0 = pure $ DT mempty
genDirectoryTree 1 = (\f -> DT $ Map.singleton f (DT mempty))
                 <$> arbitrary
genDirectoryTree n = do
  nTop <- choose (1, n)
  splits <- List.sort <$> vectorOf (nTop - 1) (choose (0,n))
  let nChildren = fmap (\(a, b) -> b - a)
                     (zip (0:splits) (splits ++ [n]))
  filenames <- vector nTop
  subTrees <- traverse genDirectoryTree nChildren
  return $ DT $ Map.fromList $ zip filenames subTrees

directoryTreePaths :: DirectoryTree -> [FilePath]
directoryTreePaths (DT m) = do
  (SimpleFileName f, DT d) <- Map.toList m
  if null d
    then [f]
    else fmap ((f <> "/") <>) (directoryTreePaths (DT d))

instance Arbitrary Text where
  arbitrary = pack <$> (arbitrary :: Gen [Char])

genCached :: [FilePath] -> Gen (Cached Int)
genCached [] = genCachedPure
genCached paths = sized $ \s ->
  if s == 0
    then genCachedPure
    else oneof
      [ genCachedPure
      , genCachedFile paths
      , genCachedFunctor paths
      , genCachedApplicative paths ]

-- | Generate a pure cached value that is not associated to a cache file
genCachedPure :: (Arbitrary a) => Gen (Cached a)
genCachedPure = fmap pure arbitrary

-- | Generate a cached value that is associated to a cache file
genCachedFile :: [FilePath] -> Gen (Cached Int)
genCachedFile (path:paths) = sized $ \s ->
  fmap (cache' path) (resize (s - 1) $ genCached paths)

-- | Generate a cached value that is the result of an fmap.
genCachedFunctor :: [FilePath] -> Gen (Cached Int)
genCachedFunctor paths = sized $ \s -> do
  f <- arbitrary
  c <- resize (s - 1) $ genCached paths
  return $ fmap f c
 
-- | Generate a cached value that is the result of combining two cached values
-- together with <*>.
genCachedApplicative :: [FilePath] -> Gen (Cached Int)
genCachedApplicative paths = sized $ \s -> do
  f <- arbitrary
  split <- choose (0, s - 1)
  c1 <- resize (split) $ genCached paths1
  c2 <- resize (s - 1 - split) $ genCached paths2
  return $ f <$> c1 <*> c2
  where (paths1, paths2) = splitRL paths ([],[])
        splitRL [] (p1, p2) = (p1, p2)
        splitRL (p:ps) (p1, p2) = splitRL ps (p:p2, p1)