{-# LANGUAGE LambdaCase #-} module Hydra.PersistenceSpec where import Hydra.Prelude hiding (label) import Test.Hydra.Prelude import Data.Aeson (Value (..)) import Data.Aeson qualified as Aeson import Data.Text qualified as Text import Hydra.Persistence (Persistence (..), PersistenceException (..), PersistenceIncremental (..), createPersistence, createPersistenceIncremental) import Test.QuickCheck (checkCoverage, cover, elements, oneof, suchThat, (===)) import Test.QuickCheck.Gen (listOf) import Test.QuickCheck.Monadic (monadicIO, monitor, pick, run) spec :: Spec spec :: Spec spec = do FilePath -> Spec -> Spec forall a. HasCallStack => FilePath -> SpecWith a -> SpecWith a describe FilePath "Persistence" (Spec -> Spec) -> Spec -> Spec forall a b. (a -> b) -> a -> b $ do FilePath -> IO () -> SpecWith (Arg (IO ())) forall a. (HasCallStack, Example a) => FilePath -> a -> SpecWith (Arg a) it FilePath "can handle empty files" (IO () -> SpecWith (Arg (IO ()))) -> IO () -> SpecWith (Arg (IO ())) forall a b. (a -> b) -> a -> b $ do FilePath -> (FilePath -> IO ()) -> IO () forall (m :: * -> *) r. MonadIO m => FilePath -> (FilePath -> m r) -> m r withTempDir FilePath "hydra-persistence" ((FilePath -> IO ()) -> IO ()) -> (FilePath -> IO ()) -> IO () forall a b. (a -> b) -> a -> b $ \FilePath tmpDir -> do let fp :: FilePath fp = FilePath tmpDir FilePath -> FilePath -> FilePath forall a. Semigroup a => a -> a -> a <> FilePath "/data" FilePath -> ByteString -> IO () forall (m :: * -> *). MonadIO m => FilePath -> ByteString -> m () writeFileBS FilePath fp ByteString "" Persistence{FromJSON Value => IO (Maybe Value) load :: FromJSON Value => IO (Maybe Value) $sel:load:Persistence :: forall a (m :: * -> *). Persistence a m -> FromJSON a => m (Maybe a) load} <- FilePath -> IO (Persistence Value IO) forall (m :: * -> *) a. (MonadIO m, MonadThrow m) => FilePath -> m (Persistence a m) createPersistence FilePath fp IO (Maybe Value) FromJSON Value => IO (Maybe Value) load IO (Maybe Value) -> Maybe Value -> IO () forall a. (HasCallStack, Show a, Eq a) => IO a -> a -> IO () `shouldReturn` (Maybe Value forall a. Maybe a Nothing :: Maybe Aeson.Value) FilePath -> Property -> SpecWith (Arg Property) forall a. (HasCallStack, Example a) => FilePath -> a -> SpecWith (Arg a) it FilePath "is consistent after save/load roundtrip" (Property -> SpecWith (Arg Property)) -> Property -> SpecWith (Arg Property) forall a b. (a -> b) -> a -> b $ Property -> Property forall prop. Testable prop => prop -> Property checkCoverage (Property -> Property) -> Property -> Property forall a b. (a -> b) -> a -> b $ PropertyM IO Property -> Property forall a. Testable a => PropertyM IO a -> Property monadicIO (PropertyM IO Property -> Property) -> PropertyM IO Property -> Property forall a b. (a -> b) -> a -> b $ do Value item <- Gen Value -> PropertyM IO Value forall (m :: * -> *) a. (Monad m, Show a) => Gen a -> PropertyM m a pick Gen Value genPersistenceItem Maybe Value actualResult <- IO (Maybe Value) -> PropertyM IO (Maybe Value) forall (m :: * -> *) a. Monad m => m a -> PropertyM m a run (IO (Maybe Value) -> PropertyM IO (Maybe Value)) -> IO (Maybe Value) -> PropertyM IO (Maybe Value) forall a b. (a -> b) -> a -> b $ FilePath -> (FilePath -> IO (Maybe Value)) -> IO (Maybe Value) forall (m :: * -> *) r. MonadIO m => FilePath -> (FilePath -> m r) -> m r withTempDir FilePath "hydra-persistence" ((FilePath -> IO (Maybe Value)) -> IO (Maybe Value)) -> (FilePath -> IO (Maybe Value)) -> IO (Maybe Value) forall a b. (a -> b) -> a -> b $ \FilePath tmpDir -> do Persistence{ToJSON Value => Value -> IO () save :: ToJSON Value => Value -> IO () $sel:save:Persistence :: forall a (m :: * -> *). Persistence a m -> ToJSON a => a -> m () save, FromJSON Value => IO (Maybe Value) $sel:load:Persistence :: forall a (m :: * -> *). Persistence a m -> FromJSON a => m (Maybe a) load :: FromJSON Value => IO (Maybe Value) load} <- FilePath -> IO (Persistence Value IO) forall (m :: * -> *) a. (MonadIO m, MonadThrow m) => FilePath -> m (Persistence a m) createPersistence (FilePath -> IO (Persistence Value IO)) -> FilePath -> IO (Persistence Value IO) forall a b. (a -> b) -> a -> b $ FilePath tmpDir FilePath -> FilePath -> FilePath forall a. Semigroup a => a -> a -> a <> FilePath "/data" ToJSON Value => Value -> IO () Value -> IO () save Value item IO (Maybe Value) FromJSON Value => IO (Maybe Value) load Property -> PropertyM IO Property forall a. a -> PropertyM IO a forall (f :: * -> *) a. Applicative f => a -> f a pure (Property -> PropertyM IO Property) -> Property -> PropertyM IO Property forall a b. (a -> b) -> a -> b $ Maybe Value actualResult Maybe Value -> Maybe Value -> Property forall a. (Eq a, Show a) => a -> a -> Property === Value -> Maybe Value forall a. a -> Maybe a Just Value item FilePath -> Spec -> Spec forall a. HasCallStack => FilePath -> SpecWith a -> SpecWith a describe FilePath "PersistenceIncremental" (Spec -> Spec) -> Spec -> Spec forall a b. (a -> b) -> a -> b $ do FilePath -> IO () -> SpecWith (Arg (IO ())) forall a. (HasCallStack, Example a) => FilePath -> a -> SpecWith (Arg a) it FilePath "can handle empty files" (IO () -> SpecWith (Arg (IO ()))) -> IO () -> SpecWith (Arg (IO ())) forall a b. (a -> b) -> a -> b $ do FilePath -> (FilePath -> IO ()) -> IO () forall (m :: * -> *) r. MonadIO m => FilePath -> (FilePath -> m r) -> m r withTempDir FilePath "hydra-persistence" ((FilePath -> IO ()) -> IO ()) -> (FilePath -> IO ()) -> IO () forall a b. (a -> b) -> a -> b $ \FilePath tmpDir -> do let fp :: FilePath fp = FilePath tmpDir FilePath -> FilePath -> FilePath forall a. Semigroup a => a -> a -> a <> FilePath "/data" FilePath -> ByteString -> IO () forall (m :: * -> *). MonadIO m => FilePath -> ByteString -> m () writeFileBS FilePath fp ByteString "" PersistenceIncremental{FromJSON Value => IO [Value] loadAll :: FromJSON Value => IO [Value] $sel:loadAll:PersistenceIncremental :: forall a (m :: * -> *). PersistenceIncremental a m -> FromJSON a => m [a] loadAll} <- FilePath -> IO (PersistenceIncremental Value IO) forall a (m :: * -> *). (MonadIO m, MonadThrow m, MonadSTM m, MonadThread m, MonadThrow (STM m)) => FilePath -> m (PersistenceIncremental a m) createPersistenceIncremental FilePath fp IO [Value] FromJSON Value => IO [Value] loadAll IO [Value] -> [Value] -> IO () forall a. (HasCallStack, Show a, Eq a) => IO a -> a -> IO () `shouldReturn` ([] :: [Aeson.Value]) FilePath -> Property -> SpecWith (Arg Property) forall a. (HasCallStack, Example a) => FilePath -> a -> SpecWith (Arg a) it FilePath "is consistent after multiple append calls in presence of new-lines" (Property -> SpecWith (Arg Property)) -> Property -> SpecWith (Arg Property) forall a b. (a -> b) -> a -> b $ Property -> Property forall prop. Testable prop => prop -> Property checkCoverage (Property -> Property) -> Property -> Property forall a b. (a -> b) -> a -> b $ PropertyM IO Property -> Property forall a. Testable a => PropertyM IO a -> Property monadicIO (PropertyM IO Property -> Property) -> PropertyM IO Property -> Property forall a b. (a -> b) -> a -> b $ do [Value] items <- Gen [Value] -> PropertyM IO [Value] forall (m :: * -> *) a. (Monad m, Show a) => Gen a -> PropertyM m a pick (Gen [Value] -> PropertyM IO [Value]) -> Gen [Value] -> PropertyM IO [Value] forall a b. (a -> b) -> a -> b $ Gen Value -> Gen [Value] forall a. Gen a -> Gen [a] listOf Gen Value genPersistenceItem (Property -> Property) -> PropertyM IO () forall (m :: * -> *). Monad m => (Property -> Property) -> PropertyM m () monitor (Double -> Bool -> FilePath -> Property -> Property forall prop. Testable prop => Double -> Bool -> FilePath -> prop -> Property cover Double 1 ([Value] -> Bool forall a. [a] -> Bool forall (t :: * -> *) a. Foldable t => t a -> Bool null [Value] items) FilePath "no items stored") (Property -> Property) -> PropertyM IO () forall (m :: * -> *). Monad m => (Property -> Property) -> PropertyM m () monitor (Double -> Bool -> FilePath -> Property -> Property forall prop. Testable prop => Double -> Bool -> FilePath -> prop -> Property cover Double 10 ([Value] -> Bool containsNewLine [Value] items) FilePath "some item contains a new line") [Value] actualResult <- IO [Value] -> PropertyM IO [Value] forall (m :: * -> *) a. Monad m => m a -> PropertyM m a run (IO [Value] -> PropertyM IO [Value]) -> IO [Value] -> PropertyM IO [Value] forall a b. (a -> b) -> a -> b $ FilePath -> (FilePath -> IO [Value]) -> IO [Value] forall (m :: * -> *) r. MonadIO m => FilePath -> (FilePath -> m r) -> m r withTempDir FilePath "hydra-persistence" ((FilePath -> IO [Value]) -> IO [Value]) -> (FilePath -> IO [Value]) -> IO [Value] forall a b. (a -> b) -> a -> b $ \FilePath tmpDir -> do PersistenceIncremental{FromJSON Value => IO [Value] $sel:loadAll:PersistenceIncremental :: forall a (m :: * -> *). PersistenceIncremental a m -> FromJSON a => m [a] loadAll :: FromJSON Value => IO [Value] loadAll, ToJSON Value => Value -> IO () append :: ToJSON Value => Value -> IO () $sel:append:PersistenceIncremental :: forall a (m :: * -> *). PersistenceIncremental a m -> ToJSON a => a -> m () append} <- FilePath -> IO (PersistenceIncremental Value IO) forall a (m :: * -> *). (MonadIO m, MonadThrow m, MonadSTM m, MonadThread m, MonadThrow (STM m)) => FilePath -> m (PersistenceIncremental a m) createPersistenceIncremental (FilePath -> IO (PersistenceIncremental Value IO)) -> FilePath -> IO (PersistenceIncremental Value IO) forall a b. (a -> b) -> a -> b $ FilePath tmpDir FilePath -> FilePath -> FilePath forall a. Semigroup a => a -> a -> a <> FilePath "/data" [Value] -> (Value -> IO ()) -> IO () forall (t :: * -> *) (m :: * -> *) a b. (Foldable t, Monad m) => t a -> (a -> m b) -> m () forM_ [Value] items ToJSON Value => Value -> IO () Value -> IO () append IO [Value] FromJSON Value => IO [Value] loadAll Property -> PropertyM IO Property forall a. a -> PropertyM IO a forall (f :: * -> *) a. Applicative f => a -> f a pure (Property -> PropertyM IO Property) -> Property -> PropertyM IO Property forall a b. (a -> b) -> a -> b $ [Value] actualResult [Value] -> [Value] -> Property forall a. (Eq a, Show a) => a -> a -> Property === [Value] items FilePath -> Property -> SpecWith (Arg Property) forall a. (HasCallStack, Example a) => FilePath -> a -> SpecWith (Arg a) it FilePath "it cannot load from a different thread once having started appending" (Property -> SpecWith (Arg Property)) -> Property -> SpecWith (Arg Property) forall a b. (a -> b) -> a -> b $ PropertyM IO (IO ()) -> Property forall a. Testable a => PropertyM IO a -> Property monadicIO (PropertyM IO (IO ()) -> Property) -> PropertyM IO (IO ()) -> Property forall a b. (a -> b) -> a -> b $ do [Value] items <- Gen [Value] -> PropertyM IO [Value] forall (m :: * -> *) a. (Monad m, Show a) => Gen a -> PropertyM m a pick (Gen [Value] -> PropertyM IO [Value]) -> Gen [Value] -> PropertyM IO [Value] forall a b. (a -> b) -> a -> b $ Gen Value -> Gen [Value] forall a. Gen a -> Gen [a] listOf Gen Value genPersistenceItem [Value] moreItems <- Gen [Value] -> PropertyM IO [Value] forall (m :: * -> *) a. (Monad m, Show a) => Gen a -> PropertyM m a pick (Gen [Value] -> PropertyM IO [Value]) -> Gen [Value] -> PropertyM IO [Value] forall a b. (a -> b) -> a -> b $ Gen Value -> Gen [Value] forall a. Gen a -> Gen [a] listOf Gen Value genPersistenceItem Gen [Value] -> ([Value] -> Bool) -> Gen [Value] forall a. Gen a -> (a -> Bool) -> Gen a `suchThat` ((Int -> Int -> Bool forall a. Ord a => a -> a -> Bool > Int 2) (Int -> Bool) -> ([Value] -> Int) -> [Value] -> Bool forall b c a. (b -> c) -> (a -> b) -> a -> c . [Value] -> Int forall a. [a] -> Int forall (t :: * -> *) a. Foldable t => t a -> Int length) IO () -> PropertyM IO (IO ()) forall a. a -> PropertyM IO a forall (f :: * -> *) a. Applicative f => a -> f a pure (IO () -> PropertyM IO (IO ())) -> IO () -> PropertyM IO (IO ()) forall a b. (a -> b) -> a -> b $ FilePath -> (FilePath -> IO ()) -> IO () forall (m :: * -> *) r. MonadIO m => FilePath -> (FilePath -> m r) -> m r withTempDir FilePath "hydra-persistence" ((FilePath -> IO ()) -> IO ()) -> (FilePath -> IO ()) -> IO () forall a b. (a -> b) -> a -> b $ \FilePath tmpDir -> do PersistenceIncremental{FromJSON Value => IO [Value] $sel:loadAll:PersistenceIncremental :: forall a (m :: * -> *). PersistenceIncremental a m -> FromJSON a => m [a] loadAll :: FromJSON Value => IO [Value] loadAll, ToJSON Value => Value -> IO () $sel:append:PersistenceIncremental :: forall a (m :: * -> *). PersistenceIncremental a m -> ToJSON a => a -> m () append :: ToJSON Value => Value -> IO () append} <- FilePath -> IO (PersistenceIncremental Value IO) forall a (m :: * -> *). (MonadIO m, MonadThrow m, MonadSTM m, MonadThread m, MonadThrow (STM m)) => FilePath -> m (PersistenceIncremental a m) createPersistenceIncremental (FilePath -> IO (PersistenceIncremental Value IO)) -> FilePath -> IO (PersistenceIncremental Value IO) forall a b. (a -> b) -> a -> b $ FilePath tmpDir FilePath -> FilePath -> FilePath forall a. Semigroup a => a -> a -> a <> FilePath "/data" [Value] -> (Value -> IO ()) -> IO () forall (t :: * -> *) (m :: * -> *) a b. (Foldable t, Monad m) => t a -> (a -> m b) -> m () forM_ [Value] items ToJSON Value => Value -> IO () Value -> IO () append IO [Value] FromJSON Value => IO [Value] loadAll IO [Value] -> [Value] -> IO () forall a. (HasCallStack, Show a, Eq a) => IO a -> a -> IO () `shouldReturn` [Value] items IO Any -> IO () -> IO () forall a b. IO a -> IO b -> IO () forall (m :: * -> *) a b. MonadAsync m => m a -> m b -> m () race_ (IO [Value] -> IO Any forall (f :: * -> *) a b. Applicative f => f a -> f b forever (IO [Value] -> IO Any) -> IO [Value] -> IO Any forall a b. (a -> b) -> a -> b $ DiffTime -> IO () forall (m :: * -> *). MonadDelay m => DiffTime -> m () threadDelay DiffTime 0.01 IO () -> IO [Value] -> IO [Value] forall a b. IO a -> IO b -> IO b forall (m :: * -> *) a b. Monad m => m a -> m b -> m b >> IO [Value] FromJSON Value => IO [Value] loadAll) ([Value] -> (Value -> IO ()) -> IO () forall (t :: * -> *) (m :: * -> *) a b. (Foldable t, Monad m) => t a -> (a -> m b) -> m () forM_ [Value] moreItems ((Value -> IO ()) -> IO ()) -> (Value -> IO ()) -> IO () forall a b. (a -> b) -> a -> b $ \Value item -> ToJSON Value => Value -> IO () Value -> IO () append Value item IO () -> IO () -> IO () forall a b. IO a -> IO b -> IO b forall (m :: * -> *) a b. Monad m => m a -> m b -> m b >> DiffTime -> IO () forall (m :: * -> *). MonadDelay m => DiffTime -> m () threadDelay DiffTime 0.01) IO () -> Selector PersistenceException -> IO () forall e a. (HasCallStack, Exception e) => IO a -> Selector e -> IO () `shouldThrow` \case IncorrectAccessException{} -> Bool True PersistenceException _ -> Bool False genPersistenceItem :: Gen Aeson.Value genPersistenceItem :: Gen Value genPersistenceItem = [Gen Value] -> Gen Value forall a. [Gen a] -> Gen a oneof [ Value -> Gen Value forall a. a -> Gen a forall (f :: * -> *) a. Applicative f => a -> f a pure Value Null , Text -> Value String (Text -> Value) -> Gen Text -> Gen Value forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b <$> Gen Text genSomeText ] genSomeText :: Gen Text genSomeText :: Gen Text genSomeText = do let t :: FilePath t = [Char 'A' .. Char 'z'] FilePath -> FilePath -> FilePath forall a. Semigroup a => a -> a -> a <> [Char '\n', Char '\t', Char '\r'] FilePath -> Text Text.pack (FilePath -> Text) -> Gen FilePath -> Gen Text forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b <$> Gen Char -> Gen FilePath forall a. Gen a -> Gen [a] listOf (FilePath -> Gen Char forall a. [a] -> Gen a elements FilePath t) containsNewLine :: [Aeson.Value] -> Bool containsNewLine :: [Value] -> Bool containsNewLine = \case [] -> Bool False (Value i : [Value] is) -> case Value i of String Text t | Text "\n" Text -> Text -> Bool `Text.isInfixOf` Text t -> Bool True Value _ -> [Value] -> Bool containsNewLine [Value] is