packages feed

fs-sim 0.3.1.0 → 0.4.0.0

raw patch · 9 files changed

+568/−109 lines, 9 filesdep +deepseqdep ~QuickCheckdep ~basedep ~fs-apinew-uploaderPVP ok

version bump matches the API change (PVP)

Dependencies added: deepseq

Dependency ranges changed: QuickCheck, base, fs-api, io-classes

API changes (from Hackage documentation)

- System.FS.Sim.Stream: genMaybe' :: Int -> Gen a -> Gen (Maybe a)
- System.FS.Sim.Stream: mkInfinite :: [Maybe a] -> Stream a
+ System.FS.Sim.Stream: Finite :: InternalInfo
+ System.FS.Sim.Stream: Infinite :: InternalInfo
+ System.FS.Sim.Stream: UnsafeStream :: InternalInfo -> [Maybe a] -> Stream a
+ System.FS.Sim.Stream: [unsafeStreamInternalInfo] :: Stream a -> InternalInfo
+ System.FS.Sim.Stream: [unsafeStreamList] :: Stream a -> [Maybe a]
+ System.FS.Sim.Stream: data InternalInfo
+ System.FS.Sim.Stream: filter :: (Maybe a -> Bool) -> Stream a -> Stream a
+ System.FS.Sim.Stream: genFiniteN :: Int -> Gen (Maybe a) -> Gen (Stream a)
+ System.FS.Sim.Stream: isFinite :: Stream a -> Bool
+ System.FS.Sim.Stream: isInfinite :: Stream a -> Bool
+ System.FS.Sim.Stream: liftShrinkStream :: (Maybe a -> [Maybe a]) -> Stream a -> [Stream a]
+ System.FS.Sim.Stream: runStreamIndefinitely :: Stream a -> [Maybe a]
+ System.FS.Sim.Stream: runStreamN :: Int -> Stream a -> ([Maybe a], Stream a)
+ System.FS.Sim.Stream: unsafeMkInfinite :: [Maybe a] -> Stream a
- System.FS.Sim.Error: withErrors :: MonadSTM m => StrictTVar m Errors -> Errors -> m a -> m a
+ System.FS.Sim.Error: withErrors :: (MonadSTM m, MonadThrow m) => StrictTVar m Errors -> Errors -> m a -> m a
- System.FS.Sim.Stream: genFinite :: Int -> Gen (Maybe a) -> Gen (Stream a)
+ System.FS.Sim.Stream: genFinite :: Gen (Maybe a) -> Gen (Stream a)

Files

CHANGELOG.md view
@@ -1,5 +1,44 @@ # Revision history for fs-sim +## ?.?.?.? -- ????-??-??++## 0.4.0.0 -- 2025-05-30++### Breaking++* Fix a bug where `withErrors` would not put back the previous `Errors` when an+  exception is thrown during execution of the function. Though we fixed the bug,+  it is also a breaking change: the type signature now has an additional+  constraint.+* Change finiteness guarantees for `Stream`s. Where streams could previously be+  *definitely* finite or *possibly* infinite, they should now be *definitely*+  finite or *definitely* infinite. This is mostly a conceptual change: it was+  already guaranteed by most if not all of the `Stream` functions. Still, the+  conceptual change should make the use of `Streams` more ergonomic going+  forward.++  As a result of and in addition to the conceptual change, the `Stream`+  interface got an overhaul. The concrete changes are:++  * The internals of the `Stream` are now exposed, but with big warnings about+    unsafe usage related to finiteness.+  * Added new `runStreamN` and `runStreamIndefinitely` functions.+  * Renamed `mkInfinite` to `unsafeMkInfinite`.+  * Added new `isFinite` and `isInfinite` queries.+  * Added a new `genFiniteN` function.+  * Removed `genMaybe'`, as it was just a specific instantiation of `genMaybe`+    that has no clear benefit being its own top-level function.+  * Added a new `liftShrinkStream` function.+  * Updated documentation.++### Patch++* Make it build with `ghc-9.12`.+* Drop support for `ghc-8.10`.+* Support `io-classes-1.8.0.1`.+* Support the new `MustExist` option for `AllowExisting` that was added in+  `fs-api`.+ ## 0.3.1.0 -- 2024-12-10  ### Non-breaking
fs-sim.cabal view
@@ -1,6 +1,6 @@ cabal-version:   3.0 name:            fs-sim-version:         0.3.1.0+version:         0.4.0.0 synopsis:        Simulated file systems description:     Simulated file systems. license:         Apache-2.0@@ -19,13 +19,19 @@   CHANGELOG.md   README.md -tested-with:     GHC ==8.10 || ==9.2 || ==9.4 || ==9.6 || ==9.8 || ==9.10+tested-with:     GHC ==9.2 || ==9.4 || ==9.6 || ==9.8 || ==9.10 || ==9.12  source-repository head   type:     git   location: https://github.com/input-output-hk/fs-sim   subdir:   fs-sim +source-repository this+  type:     git+  location: https://github.com/input-output-hk/fs-sim+  subdir:   fs-sim+  tag:      fs-sim-0.4.0.0+ library   hs-source-dirs:   src   exposed-modules:@@ -38,12 +44,12 @@    default-language: Haskell2010   build-depends:-    , base                   >=4.14  && <4.21+    , base                   >=4.16  && <4.22     , base16-bytestring      ^>=0.1  || ^>=1.0     , bytestring             ^>=0.10 || ^>=0.11 || ^>=0.12     , containers             ^>=0.5  || ^>=0.6  || ^>=0.7-    , fs-api                 ^>=0.3-    , io-classes             ^>=1.6  || ^>=1.7+    , fs-api                 ^>=0.4+    , io-classes             ^>=1.6  || ^>=1.7  || ^>=1.8.0.1     , io-classes:strict-stm     , mtl                    ^>=2.2  || ^>=2.3     , primitive              ^>=0.9@@ -63,6 +69,7 @@   other-modules:     Test.System.FS.Sim.Error     Test.System.FS.Sim.FsTree+    Test.System.FS.Sim.Stream     Test.System.FS.StateMachine     Test.Util     Test.Util.RefEnv@@ -74,6 +81,7 @@     , bifunctors     , bytestring     , containers+    , deepseq     , fs-api     , fs-sim     , generics-sop
src/System/FS/Sim/Error.hs view
@@ -433,9 +433,11 @@     hPutBufSomeAtE <- commonPutErrors     return Errors {..}   where-    streamGen l = Stream.genInfinite . Stream.genMaybe' l . QC.elements-    streamGen' l = Stream.genInfinite . Stream.genMaybe' l . QC.frequency+    genMaybe' = Stream.genMaybe 2 +    streamGen l = Stream.genInfinite . genMaybe' l . QC.elements+    streamGen' l = Stream.genInfinite . genMaybe' l . QC.frequency+     commonGetErrors = streamGen' 20       [ (1, return $ Left FsReachedEOF)       , (3, Right <$> arbitrary) ]@@ -572,15 +574,12 @@  -- | Execute the next action using the given 'Errors'. After the action is -- finished, the previous 'Errors' are restored.-withErrors :: MonadSTM m => StrictTVar m Errors -> Errors -> m a -> m a-withErrors errorsVar tempErrors action = do-    originalErrors <- atomically $ do-      originalErrors <- readTVar errorsVar-      writeTVar errorsVar tempErrors-      return originalErrors-    res <- action-    atomically $ writeTVar errorsVar originalErrors-    return res+withErrors :: (MonadSTM m, MonadThrow m) => StrictTVar m Errors -> Errors -> m a -> m a+withErrors errorsVar tempErrors action =+    bracket+      (atomically $ swapTVar errorsVar tempErrors)+      (\originalErrors -> atomically $ swapTVar errorsVar originalErrors)+      $ \_ -> action  {-------------------------------------------------------------------------------   Utilities
src/System/FS/Sim/FsTree.hs view
@@ -231,14 +231,23 @@   Specific file system functions -------------------------------------------------------------------------------} --- | Open a file: create it if necessary or throw an error if it existed--- already wile we were supposed to create it from scratch (when passed--- 'MustBeNew').+-- | Open a file: create it if necessary or throw an error if either:+--    1. It existed already while we were supposed to create it from scratch+--        (when passed 'MustBeNew').+--    2. It did not already exists when we expected to (when passed 'MustExist'). openFile :: Monoid a          => FsPath -> AllowExisting -> FsTree a -> Either FsTreeError (FsTree a)-openFile fp ex = alterFile fp Left (Right mempty) $ \a -> case ex of-    AllowExisting -> Right a-    MustBeNew     -> Left (FsExists fp)+openFile fp ex = alterFile fp Left caseDoesNotExist caseAlreadyExist+  where+    caseAlreadyExist a = case ex of+      AllowExisting -> Right a+      MustBeNew     -> Left (FsExists fp)+      MustExist     -> Right a++    caseDoesNotExist = case ex of+      AllowExisting -> Right mempty+      MustBeNew     -> Right mempty+      MustExist     -> Left (FsMissing fp (pathLast fp :| []))  -- | Replace the contents of the specified file (which must exist) replace :: FsPath -> a -> FsTree a -> Either FsTreeError (FsTree a)
src/System/FS/Sim/MockFS.hs view
@@ -65,7 +65,7 @@   , hPutBufSomeAt   ) where -import           Control.Monad (forM, forM_, unless, void, when)+import           Control.Monad (forM, forM_, unless, when) import           Control.Monad.Except (MonadError, throwError) import           Control.Monad.Primitive (PrimMonad (..)) import           Control.Monad.State.Strict (MonadState, get, gets, put)@@ -491,8 +491,6 @@           , fsErrorStack  = prettyCallStack           , fsLimitation  = True           }-      when (openMode == ReadMode) $ void $-        checkFsTree $ FS.getFile fp (mockFiles fs)       files' <- checkFsTree $ FS.openFile fp ex (mockFiles fs)       return $ newHandle (fs { mockFiles = files' })                          (OpenHandle fp (filePtr openMode))
src/System/FS/Sim/Stream.hs view
@@ -1,30 +1,39 @@ {-# LANGUAGE DeriveFunctor       #-} {-# LANGUAGE ScopedTypeVariables #-} --- | Possibly infinite streams of @'Maybe' a@s.+-- | Finite and infinite streams of @'Maybe' a@s. module System.FS.Sim.Stream (     -- * Streams-    Stream+    Stream (..)+  , InternalInfo (..)     -- * Running   , runStream+  , runStreamN+  , runStreamIndefinitely     -- * Construction   , always   , empty-  , mkInfinite   , repeating+  , unsafeMkInfinite   , unsafeMkFinite+    -- * Modify+  , filter     -- * Query   , null+  , isFinite+  , isInfinite     -- * Generation and shrinking   , genFinite+  , genFiniteN   , genInfinite   , genMaybe-  , genMaybe'   , shrinkStream+  , liftShrinkStream   ) where  import           Control.Monad (replicateM)-import           Prelude hiding (null)+import           Prelude hiding (filter, isInfinite, null)+import qualified Prelude import qualified Test.QuickCheck as QC import           Test.QuickCheck (Gen) @@ -32,29 +41,45 @@   Streams -------------------------------------------------------------------------------} --- | A 'Stream' is a stream of @'Maybe' a@s, which is /possibly/ infinite or--- /definitely/ finite.------ Finiteness is tracked internally and used for 'QC.shrink'ing and the 'Show'--- instance.-data Stream a = Stream {-      -- | Info about the size of the stream.-      _streamInternalInfo :: InternalInfo-    , _getStream          :: [Maybe a]+-- | A stream of @'Maybe' a@s that can be infinite.+data Stream a =+  -- | UNSAFE: when constructing, modifying, or accessing the internals of a+  -- 'Stream', it is the responsibility of the user to preserve the following+  -- invariant:+  --+  -- INVARIANT: if the stream is marked as 'Infinite', then the internal list+  -- should be infinite. If the stream is marked as 'Finite', then the internal+  -- list should finite.+  --+  -- * If the internal list is infinite but marked as 'Finite', then 'QC.shrink'+  --   or 'show' on the corresponding stream will diverge.+  --+  -- * If the internal list is finite but marked as 'Infinite', then 'QC.shrink'+  --   on the corresponding stream will degrade to an infinite list of empty+  --   streams.+  UnsafeStream {+      -- | UNSAFE: see 'UnsafeStream' for more information.+      --+      -- Info about the finiteness of the stream. It is used for 'QC.shrink'ing+      -- and the 'Show' instance.+      unsafeStreamInternalInfo :: InternalInfo+      -- | UNSAFE: see 'UnsafeStream' for more information.+      --+      -- The internal list underlying the stream.+    , unsafeStreamList         :: [Maybe a]     }   deriving Functor --- | Tag for 'Stream's that describes whether it is either /definitely/ a finite--- stream, or /possibly/ an infinite stream.+-- | Tag for 'Stream's that describes whether it is finite or infinite. ----- Useful for the 'Show' instance of 'Stream': when a 'Stream' is /definitely/--- finite, we can safely print the full stream.+-- Useful for the 'Show' instance of 'Stream': when a 'Stream' is finite, we can+-- safely print the full stream. data InternalInfo = Infinite | Finite --- | Fully shows a 'Stream' if it is /definitely/ finite, or prints a--- placeholder string if it is /possibly/ infinite.+-- | Fully shows a 'Stream' if it is finite, or prints a placeholder string if+-- it is infinite. instance Show a => Show (Stream a) where-  showsPrec n (Stream info xs) = case info of+  showsPrec n (UnsafeStream info xs) = case info of       Infinite -> ("<infinite stream>" ++)       Finite   -> (if n > 10 then ('(':) else id)                 . shows xs@@ -65,81 +90,127 @@   Running -------------------------------------------------------------------------------} --- | Advance the 'Stream'. Return the @'Maybe' a@ and the remaining 'Stream'.+-- | \( O(1) \): advance the 'Stream'. Return the @'Maybe' a@ and the remaining+-- 'Stream'. -- -- Returns 'Nothing' by default if the 'Stream' is empty. runStream :: Stream a -> (Maybe a, Stream a)-runStream s@(Stream _    []    ) = (Nothing, s)-runStream   (Stream info (a:as)) = (a, Stream info as)+runStream s@(UnsafeStream _    []    ) = (Nothing, s)+runStream   (UnsafeStream info (a:as)) = (a, UnsafeStream info as) +-- | \( O(n) \): like 'runStream', but advancing the stream @n@ times.+--+-- If @n<=0@, then the stream is advanced @0@ times.+runStreamN :: Int -> Stream a -> ([Maybe a], Stream a)+runStreamN n s+  | n <= 0 = ([], s)+  | otherwise =+      let (x, s') = runStream s+          (xs, s'') = runStreamN (n-1) s'+      in  (x:xs, s'')++-- | \( O(\infty) \): like 'runStream', but advancing the stream indefinitely.+--+-- For infinite streams, this produces an infinite list. For finite streams,+-- this produces a finite list.+runStreamIndefinitely :: Stream a -> [Maybe a]+runStreamIndefinitely (UnsafeStream _ as) = as ++ repeat Nothing+ {-------------------------------------------------------------------------------   Construction -------------------------------------------------------------------------------}  -- | Make an empty 'Stream'. empty :: Stream a-empty = Stream Finite []+empty = UnsafeStream Finite []  -- | Make a 'Stream' that always generates the given @a@. always :: a -> Stream a-always x = Stream Infinite (repeat (Just x))+always x = UnsafeStream Infinite (repeat (Just x))  -- | Make a 'Stream' that infinitely repeats the given list. repeating :: [Maybe a] -> Stream a-repeating xs = Stream Infinite $ concat (repeat xs)+repeating xs = UnsafeStream Infinite $ cycle xs --- | UNSAFE: Make a 'Stream' that is marked as definitely finite.------ This is unsafe since a user can pass in any list, and evaluating--- 'Test.QuickCheck.shrink' or 'show' on the resulting 'Stream' will diverge. It--- is the user's responsibility to only pass in a finite list.+-- | UNSAFE: Make a 'Stream' that is marked as finite. It is the user's+-- responsibility to only pass in finite lists. See 'UnsafeStream' for more+-- information. unsafeMkFinite :: [Maybe a] -> Stream a-unsafeMkFinite = Stream Finite+unsafeMkFinite = UnsafeStream Finite --- | Make a 'Stream' that is marked as possibly infinite.-mkInfinite :: [Maybe a] -> Stream a-mkInfinite = Stream Infinite+-- | UNSAFE: Make a 'Stream' that is marked as infinite. It is the user's+-- responsibility to only pass in infinite lists. See 'UnsafeStream' for more+-- information.+unsafeMkInfinite :: [Maybe a] -> Stream a+unsafeMkInfinite = UnsafeStream Infinite  {-------------------------------------------------------------------------------+  Modify+-------------------------------------------------------------------------------}++-- | Filter a 'Stream', preserving finiteness.+filter :: (Maybe a -> Bool) -> Stream a -> Stream a+filter p (UnsafeStream info xs) = UnsafeStream info (Prelude.filter p xs)++{-------------------------------------------------------------------------------   Query -------------------------------------------------------------------------------} --- | Return 'True' if the stream is empty.+-- | Check that the stream is empty. ----- A stream consisting of only 'Nothing's (even if it is only one) is not--- considered to be empty.+-- In general, a stream is only empty if the stream is equivalent to 'empty'.+--+-- A finite\/infinite stream consisting of only 'Nothing's is not considered to+-- be empty. In particular, @'null' ('always' Nothing) /= True@. null :: Stream a -> Bool-null (Stream _ []) = True-null _             = False+null (UnsafeStream Finite []) = True+null _                        = False +-- | Check that the stream is finite+isFinite :: Stream a -> Bool+isFinite (UnsafeStream Finite _)   = True+isFinite (UnsafeStream Infinite _) = False++-- | Check that the stream is infinite+isInfinite :: Stream a -> Bool+isInfinite (UnsafeStream Finite _)   = False+isInfinite (UnsafeStream Infinite _) = True+ {-------------------------------------------------------------------------------   Generation and shrinking -------------------------------------------------------------------------------} --- | Shrink a stream like it is an 'Test.QuickCheck.InfiniteList'.+-- | Shrink a stream like it is an 'QC.InfiniteList'. ----- Possibly infinite streams are shrunk differently than lists that are--- definitely finite, which is to ensure that shrinking terminates.--- * Possibly infinite streams are shrunk by taking finite prefixes of the---  argument stream. As such, shrinking a possibly infinite stream creates---  definitely finite streams.--- * Definitely finite streams are shrunk like lists are shrunk normally,---   preserving that the created streams are still definitely finite.+-- Infinite streams are shrunk differently than lists that are finite, which is+-- to ensure that we shrink infinite lists towards finite lists.+--+-- * Infinite streams are shrunk by taking finite prefixes of the argument+--   stream. Note that there are an infinite number of finite prefixes, so even+--   though the *shrink list* is infinite, the individual *list elements* are+--   finite.+--+-- * Finite streams are shrunk like lists are shrunk normally, preserving+--   finiteness. shrinkStream :: Stream a -> [Stream a]-shrinkStream (Stream info xs0) = case info of-    Infinite -> Stream Finite <$> [take n xs0 | n <- map (2^) [0 :: Int ..]]-    Finite   -> Stream Finite <$> QC.shrinkList (const []) xs0+shrinkStream (UnsafeStream info xs0) = case info of+    Infinite -> UnsafeStream Finite <$> [take n xs0 | n <- map (2^) [0 :: Int ..]]+    Finite   -> UnsafeStream Finite <$> QC.shrinkList (const []) xs0 +-- | Like 'shrinkStream', but with a custom shrinker for elements of the stream.+liftShrinkStream :: (Maybe a -> [Maybe a]) -> Stream a -> [Stream a]+liftShrinkStream shrinkOne (UnsafeStream info xs0) = case info of+    Infinite -> UnsafeStream Finite <$> [take n xs0 | n <- map (2^) [0 :: Int ..]]+    Finite   -> UnsafeStream Finite <$> QC.shrinkList shrinkOne xs0+ -- | Make a @'Maybe' a@ generator based on an @a@ generator. -- -- Each element has a chance of being either 'Nothing' or an element generated--- with the given @a@ generator (wrapped in a 'Just').------ The first argument is the likelihood (as used by 'QC.frequency') of a--- 'Just' where 'Nothing' has likelihood 2.+-- with the given @a@ generator (wrapped in a 'Just'). These /likelihoods/ are+-- passed to 'QC.frequency'. genMaybe ::-     Int   -- ^ Likelihood of 'Nothing'-  -> Int   -- ^ Likelihood of @'Just' a@+     Int -- ^ Likelihood of 'Nothing'+  -> Int -- ^ Likelihood of @'Just' a@   -> Gen a   -> Gen (Maybe a) genMaybe nLi jLi genA = QC.frequency@@ -147,22 +218,21 @@     , (jLi, Just <$> genA)     ] --- | Like 'genMaybe', but with the likelihood of 'Nothing' fixed to @2@. 'QC.frequency'-genMaybe' ::-     Int   -- ^ Likelihood of @'Just' a@-  -> Gen a+-- | Generate a finite 'Stream' of length @n@.+genFiniteN ::+     Int -- ^ Requested size of finite stream.   -> Gen (Maybe a)-genMaybe' = genMaybe 2+  -> Gen (Stream a)+genFiniteN n gen = UnsafeStream Finite <$> replicateM n gen --- | Generate a finite 'Stream' of length @n@.+-- | Generate a sized, finite 'Stream'. genFinite ::-     Int           -- ^ Requested size of finite stream. Tip: use 'genMaybe'.-  -> Gen (Maybe a)+     Gen (Maybe a)   -> Gen (Stream a)-genFinite n gen = Stream Finite <$> replicateM n gen+genFinite gen = UnsafeStream Finite <$> QC.listOf gen  -- | Generate an infinite 'Stream'. genInfinite ::-     Gen (Maybe a)  -- ^ Tip: use 'genMaybe'.+     Gen (Maybe a)   -> Gen (Stream a)-genInfinite gen = Stream Infinite <$> QC.infiniteListOf gen+genInfinite gen = UnsafeStream Infinite <$> QC.infiniteListOf gen
test/Main.hs view
@@ -2,6 +2,7 @@  import qualified Test.System.FS.Sim.Error import qualified Test.System.FS.Sim.FsTree+import qualified Test.System.FS.Sim.Stream import qualified Test.System.FS.StateMachine import           Test.Tasty @@ -9,5 +10,6 @@ main = defaultMain $ testGroup "fs-sim-test" [       Test.System.FS.Sim.Error.tests     , Test.System.FS.Sim.FsTree.tests+    , Test.System.FS.Sim.Stream.tests     , Test.System.FS.StateMachine.tests     ]
+ test/Test/System/FS/Sim/Stream.hs view
@@ -0,0 +1,274 @@+{-# LANGUAGE BangPatterns        #-}+{-# LANGUAGE DerivingStrategies  #-}+{-# LANGUAGE FlexibleInstances   #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications    #-}++{-# OPTIONS_GHC -Wno-orphans #-}++-- The @base@ library throws warnings on uses of @head@ on later GHC versions.+-- This is a just a test module, so we ignore the warning.+{-# OPTIONS_GHC -Wno-unrecognised-warning-flags #-}+{-# OPTIONS_GHC -Wno-x-partial #-}++module Test.System.FS.Sim.Stream (tests) where++import           Control.DeepSeq+import           Data.Maybe (isJust, isNothing)+import           Prelude hiding (filter, isInfinite, null)+import qualified Prelude+import           System.FS.Sim.Stream+import           Test.Tasty+import           Test.Tasty.QuickCheck+import           Test.Util++tests :: TestTree+tests = testGroup "Test.System.FS.Sim.Stream" [+      testProperty "prop_runStream" $+        prop_runStream @Int+    , testProperty "prop_runStreamN" $+        prop_runStreamN @Int+    , testProperty "prop_null"+        prop_null+    , testGroup "Shrinkers" [+          testProperty "prop_shrinkStreamFail" $+            prop_shrinkStreamFail @Int+        , testProperty "prop_eventuallyJust @InfiniteStream" $+            \(InfiniteStream s) -> prop_eventuallyJust @Int s+        , testProperty "prop_eventuallyNothing @InfiniteStream" $+            \(InfiniteStream s) -> prop_eventuallyNothing @Int s+        , testProperty "prop_eventuallyNothing @FiniteStream" $+            \(FiniteStream s) -> prop_eventuallyNothing @Int s+        ]+    , testGroup "Generators and shrinkers" [+          testGroup "Stream" $+            prop_forAllArbitraryAndShrinkSatisfy+              @(Stream Int)+              arbitrary+              (\s -> if isFinite s then shrink s else take 20 (shrink s))+              prop_stream+              prop_finiteStream+        , testGroup "FiniteSteam" $+            prop_arbitraryAndShrinkSatisfy+              @(FiniteStream Int)+              (\(FiniteStream s) -> prop_finiteStream s)+              (\(FiniteStream s) -> prop_finiteStream s)+        , testGroup "InfiniteStream" $+            prop_forAllArbitraryAndShrinkSatisfy+              @(InfiniteStream Int)+              arbitrary+              (take 6 . shrink)+              (\(InfiniteStream s) -> prop_infiniteStream s)+              (\(InfiniteStream s) -> prop_finiteStream s)+        , testGroup "NonEmptyStream" $+            prop_forAllArbitraryAndShrinkSatisfy+              @(NonEmptyStream Int)+              arbitrary+              (\s@(NonEmptyStream s') -> if isFinite s' then shrink s else take 20 (shrink s))+              (\(NonEmptyStream s) -> prop_nonEmptyStream s)+              (\(NonEmptyStream s) -> prop_nonEmptyStream s)+        ]+    ]++{-------------------------------------------------------------------------------+  Properties+-------------------------------------------------------------------------------}++-- | Advancing a stream behaves like a head on an equivalent list.+prop_runStream :: (Eq a, Show a) => Stream a -> Property+prop_runStream s =+    x === head ys+  where+    (x, _s') = runStream s+    ys = runStreamIndefinitely s++-- | Advancing a stream @n@ times behaves likes @take n@ on an equivalent list.+prop_runStreamN :: (Eq a, Show a) => Int -> Stream a -> Property+prop_runStreamN n s =+    xs === take n ys+  where+    (xs, _s') = runStreamN n s+    ys = runStreamIndefinitely s++-- | Empty streams are null+prop_null :: Property+prop_null = once $ property $ null empty++{-------------------------------------------------------------------------------+  Shrinkers+-------------------------------------------------------------------------------}++-- | A simple property that is expected to fail, but should exercise the+-- shrinker a little bit.+prop_shrinkStreamFail :: Stream a -> Property+prop_shrinkStreamFail s = expectFailure $+    let xs = fst (runStreamN 10 s)+    in  property $ length (Prelude.filter isJust xs) /= length (Prelude.filter isNothing xs)++-- | A stream eventually produces a 'Just'+prop_eventuallyJust :: Stream a -> Property+prop_eventuallyJust = eventually isJust++-- | A stream eventually produces a 'Nothing'+prop_eventuallyNothing :: Stream a -> Property+prop_eventuallyNothing = eventually isNothing++eventually :: (Maybe a -> Bool) -> Stream a -> Property+eventually p = go 1+  where+    go !n s =+      let (x, s') = runStream s in+      if p x+        then tabulate "Number of elements inspected" [showPowersOf 2 n] $ property True+        else go (n+1) s'++{-------------------------------------------------------------------------------+  Generators and shrinkers+-------------------------------------------------------------------------------}++-- | A 'Stream' is either finite or infinite+prop_stream :: NFData a => Stream a -> Property+prop_stream s =+         prop_finiteStream s+    .||. prop_infiniteStream s++-- | A stream is finite+prop_finiteStream :: NFData a => Stream a -> Property+prop_finiteStream s =+    property (isFinite s) .&&. property (not (isInfinite s)) .&&.+    prop_deepseqStream s++-- | An stream is infinite+prop_infiniteStream :: Stream a -> Property+prop_infiniteStream s =+    property (isInfinite s) .&&. property (not (isFinite s))++-- | A stream is non-empty, and finite or infinite+prop_nonEmptyStream :: NFData a => Stream a -> Property+prop_nonEmptyStream s =+         property $ not (null s)+    .&&. prop_stream s++prop_deepseqStream :: NFData a => Stream a -> Property+prop_deepseqStream (UnsafeStream info xs) = property (rwhnf info `seq` rnf xs)++{-------------------------------------------------------------------------------+  Generators and shrinkers: utility properties+-------------------------------------------------------------------------------}++prop_arbitraryAndShrinkSatisfy ::+     forall a. (Arbitrary a, Show a)+  => (a -> Property) -- ^ Generator property+  -> (a -> Property) -- ^ Shrinker property+  -> [TestTree]+prop_arbitraryAndShrinkSatisfy =+    prop_forAllArbitraryAndShrinkSatisfy arbitrary shrink++prop_forAllArbitraryAndShrinkSatisfy ::+     forall a. Show a+  => Gen a+  -> (a -> [a])+  -> (a -> Property) -- ^ Generator property+  -> (a -> Property) -- ^ Shrinker property+  -> [TestTree]+prop_forAllArbitraryAndShrinkSatisfy gen shr pgen pshr =+    [ prop_forAllArbitrarySatisfies gen shr pgen+    , prop_forAllShrinkSatisfies gen shr pshr+    ]++prop_forAllArbitrarySatisfies ::+     forall a. Show a+  => Gen a+  -> (a -> [a])+  -> (a -> Property)+  -> TestTree+prop_forAllArbitrarySatisfies gen shr p =+    testProperty "Arbitrary satisfies property" $+      forAllShrink gen shr p++prop_forAllShrinkSatisfies ::+     forall a. Show a+  => Gen a+  -> (a -> [a])+  -> (a -> Property)+  -> TestTree+prop_forAllShrinkSatisfies gen shr p =+    testProperty "Shrinking satisfies property" $+      forAll gen $ \x ->+        case shr x of+          [] -> label "no shrinks" $ property True+          xs -> forAll (growingElements xs) p++{-------------------------------------------------------------------------------+  Arbitrary instances+-------------------------------------------------------------------------------}++--+-- Stream+--++instance Arbitrary a => Arbitrary (Stream a) where+  arbitrary = oneof [+        genFinite arbitrary+      , genInfinite arbitrary+      ]+  shrink = liftShrinkStream shrink++--+-- NonEmptyStream+--++newtype NonEmptyStream a = NonEmptyStream (Stream a)+  deriving stock Show++instance Arbitrary a => Arbitrary (NonEmptyStream a) where+  arbitrary = NonEmptyStream <$> oneof [+        genFiniteNonEmpty+      , genInfinite arbitrary+      ]+    where+      genFiniteNonEmpty = do+        x <- arbitrary+        xs <- arbitrary+        pure $ unsafeMkFinite (x : xs)+  shrink (NonEmptyStream s) =+      [ NonEmptyStream s'+      | s' <- shrink s+      , not (Prelude.null (unsafeStreamList s')) ]++--+-- FiniteStream+--++newtype FiniteStream a = FiniteStream {+    getFiniteStream :: Stream a+  }+  deriving stock Show++instance Arbitrary a => Arbitrary (FiniteStream a) where+  arbitrary = FiniteStream <$> genFinite arbitrary+  shrink (FiniteStream s) = FiniteStream <$> shrinkStream s++--+-- InfiniteStream+--++newtype InfiniteStream a = InfiniteStream {+    getInfiniteStream :: Stream a+  }+  deriving stock Show++instance Arbitrary a => Arbitrary (InfiniteStream a) where+  arbitrary = InfiniteStream <$> genInfinite arbitrary+  shrink (InfiniteStream s) = InfiniteStream <$> shrinkStream s++--+-- Tiny+--++newtype Tiny a = Tiny a+  deriving stock Show++instance Arbitrary (Tiny Int) where+  arbitrary = Tiny <$> choose (0, 5)+  shrink (Tiny x) = [ Tiny x' | x' <- shrink x, 0 <= x', x' <= 5]
test/Test/System/FS/StateMachine.hs view
@@ -676,7 +676,7 @@         (rf, wf) = if fileExists then (10,3) else (1,3)      genAllowExisting :: Gen AllowExisting-    genAllowExisting = elements [AllowExisting, MustBeNew]+    genAllowExisting = elements [AllowExisting, MustBeNew, MustExist]      genSeekMode :: Gen SeekMode     genSeekMode = elements [@@ -1004,84 +1004,107 @@   -- > Get ..   | TagPutTruncateGet -  -- Close a handle 2 times+  -- | Close a handle 2 times   --   -- > h <- Open ..   -- > close h   -- > close h   | TagClosedTwice -  -- Open an existing file with ReadMode and then with WriteMode+  -- | Open an existing file with ReadMode and then with WriteMode   --   -- > open fp ReadMode   -- > open fp Write   | TagOpenReadThenWrite -  -- Open 2 Readers of a file.+  -- | Open 2 Readers of a file.   --   -- > open fp ReadMode   -- > open fp ReadMode   | TagOpenReadThenRead -  -- ListDir on a non empty dirextory.+  -- | ListDir on a non empty dirextory.   --   -- > CreateDirIfMissing True a/b   -- > ListDirectory a   | TagCreateDirWithParentsThenListDirNotNull -  -- Read from an AppendMode file+  -- | Read from an AppendMode file   --   -- > h <- Open fp AppendMode   -- > Read h ..   | TagReadInvalid -  -- Write to a read only file+  -- | Write to a read only file   --   -- > h <- Open fp ReadMode   -- > Put h ..   | TagWriteInvalid -  -- Put Seek and Get+  -- | Put Seek and Get   --   -- > Put ..   -- > Seek ..   -- > Get ..   | TagPutSeekGet -  -- Put Seek (negative) and Get+  -- | Put Seek (negative) and Get   --   -- > Put ..   -- > Seek .. (negative)   -- > Get ..   | TagPutSeekNegGet -  -- Open with MustBeNew (O_EXCL flag), but the file already existed.+  -- | Open with MustBeNew (O_EXCL flag), but the file already existed.   --   -- > h <- Open fp (AppendMode _)   -- > Close h   -- > Open fp (AppendMode MustBeNew)   | TagExclusiveFail +  -- | Open a file in read mode successfully+  --+  -- > h <- Open fp (WriteMode _)+  -- > Close h+  -- > h <- Open fp ReadMode+  | TagReadModeMustExist -  -- Reading returns an empty bytestring when EOF+  -- | Open a file in read mode, but it fails because the file does not exist.   --+  -- > h <- Open fp ReadMode+  | TagReadModeMustExistFail++  -- | Open a file in non-read mode with 'MustExist' successfully.+  --+  -- > h <- Open fp (_ MustBeNew)+  -- > Close h+  -- > h <- Open fp (_ MustExist)+  | TagFileMustExist++  -- | Open a file in non-read mode with 'MustExist', but it fails because the+  -- files does not exist.+  --+  -- > h <- Open fp (_ MustExist)+  | TagFileMustExistFail++  -- | Reading returns an empty bytestring when EOF+  --   -- > h <- open fp ReadMode   -- > Get h 1 == ""   | TagReadEOF --  -- GetAt+  -- | GetAt   --   -- > GetAt ...   | TagPread -  -- Roundtrip for I/O with user-supplied buffers+  -- | Roundtrip for I/O with user-supplied buffers   --   -- > PutBuf h bs c   -- > GetBuf h c          (==bs)   | TagPutGetBuf -  -- Roundtrip for I/O with user-supplied buffers+  -- | Roundtrip for I/O with user-supplied buffers   --   -- > PutBufAt h bs c o   -- > GetBufAt h c o      (==bs)@@ -1136,6 +1159,10 @@     , tagPutSeekGet Set.empty Set.empty     , tagPutSeekNegGet Set.empty Set.empty     , tagExclusiveFail+    , tagReadModeMustExist+    , tagReadModeMustExistFail+    , tagFileMustExist+    , tagFileMustExistFail     , tagReadEOF     , tagPread     , tagPutGetBuf Set.empty@@ -1480,6 +1507,39 @@           , fsErrorType fsError == FsResourceAlreadyExist ->             Left TagExclusiveFail         _otherwise -> Right tagExclusiveFail++    tagReadModeMustExist :: EventPred+    tagReadModeMustExist = C.predicate $ \ev ->+      case (eventMockCmd ev, eventMockResp ev) of+        (Open _ ReadMode, Resp (Right (RHandle _))) -> Left TagReadModeMustExist+        _otherwise -> Right tagReadModeMustExist++    tagReadModeMustExistFail :: EventPred+    tagReadModeMustExistFail = C.predicate $ \ev ->+      case (eventMockCmd ev, eventMockResp ev) of+        (Open _ ReadMode, Resp (Left fsError))+          | fsErrorType fsError == FsResourceDoesNotExist ->+            Left TagReadModeMustExistFail+        _otherwise -> Right tagReadModeMustExistFail++    tagFileMustExist :: EventPred+    tagFileMustExist = C.predicate $ \ev ->+      case (eventMockCmd ev, eventMockResp ev) of+        (Open _ mode, Resp (Right (WHandle _ _)))+          | MustExist <- allowExisting mode+          , mode /= ReadMode+          -> Left TagFileMustExist+        _otherwise -> Right tagFileMustExist++    tagFileMustExistFail :: EventPred+    tagFileMustExistFail = C.predicate $ \ev ->+      case (eventMockCmd ev, eventMockResp ev) of+        (Open _ mode, Resp (Left fsError))+          | MustExist <- allowExisting mode+          , mode /= ReadMode+          , fsErrorType fsError == FsResourceDoesNotExist ->+            Left TagFileMustExistFail+        _otherwise -> Right tagFileMustExistFail      tagReadEOF :: EventPred     tagReadEOF = successful $ \ev suc ->