diff --git a/src/Lib/Types/Emver.hs b/src/Lib/Types/Emver.hs index 9353557..6915ce0 100644 --- a/src/Lib/Types/Emver.hs +++ b/src/Lib/Types/Emver.hs @@ -1,113 +1,126 @@ -{- | -Module : Lib.Types.Emver -Description : Semver with 4th digit extension for Embassy -License : Start9 Non-Commercial -Maintainer : keagan@start9labs.com -Stability : experimental -Portability : portable - -This module was designed to address the problem of releasing updates to Embassy Packages where the upstream project was -either unaware of or apathetic towards supporting their application on the Embassy platform. In most cases, the original -package will support . This leaves us with the problem where we would like -to preserve the original package's version, since one of the goals of the Embassy platform is transparency. However, on -occasion, we have screwed up and published a version of a package that needed to have its metadata updated. In this -scenario we were left with the conundrum of either unilaterally claiming a version number of a package we did not author -or let the issue persist until the next update. Neither of these promote good user experiences, for different reasons. -This module extends the semver standard linked above with a 4th digit, which is given PATCH semantics. --} {-# LANGUAGE BangPatterns #-} +{-# LANGUAGE RankNTypes #-} -module Lib.Types.Emver - ( major - , minor - , patch - , revision - , satisfies - , (<||) - , (||>) +-- | +--Module : Lib.Types.Emver +--Description : Semver with 4th digit extension for Embassy +--License : Start9 Non-Commercial +--Maintainer : keagan@start9labs.com +--Stability : experimental +--Portability : portable +-- +--This module was designed to address the problem of releasing updates to Embassy Packages where the upstream project was +--either unaware of or apathetic towards supporting their application on the Embassy platform. In most cases, the original +--package will support . This leaves us with the problem where we would like +--to preserve the original package's version, since one of the goals of the Embassy platform is transparency. However, on +--occasion, we have screwed up and published a version of a package that needed to have its metadata updated. In this +--scenario we were left with the conundrum of either unilaterally claiming a version number of a package we did not author +--or let the issue persist until the next update. Neither of these promote good user experiences, for different reasons. +--This module extends the semver standard linked above with a 4th digit, which is given PATCH semantics. +module Lib.Types.Emver ( + major, + minor, + patch, + revision, + satisfies, + (<||), + (||>), -- we do not export 'None' because it is useful for its internal algebraic properties only - , VersionRange(Anchor, Any, None) - , Version(..) - , AnyRange(..) - , AllRange(..) - , conj - , disj - , exactly - , parseVersion - , parseRange - ) where + VersionRange (..), + Version (..), + AnyRange (..), + AllRange (..), + conj, + disj, + exactly, + parseVersion, + parseRange, + reduce, + nodes, +) where -import Startlude ( ($) - , ($>) - , (&&) - , (.) - , (<$>) - , (<&>) - , (<<$>>) - , Alternative((<|>)) - , Applicative((*>), (<*), liftA2, pure) - , Bool(..) - , Either(..) - , Eq(..) - , Foldable(foldMap, length) - , Hashable - , IsString(..) - , Monad((>>=)) - , Monoid(mappend, mempty) - , NFData(..) - , Num((+)) - , Ord(compare) - , Ordering(..) - , Read - , Semigroup((<>)) - , Show - , String - , Word - , either - , flip - , id - , on - , seq - , show - , (||) - ) +import Startlude ( + Alternative ((<|>)), + Applicative (liftA2, pure, (*>), (<*)), + Bool (..), + Either (..), + Eq (..), + Foldable (foldMap, length), + Hashable, + IsString (..), + Monad ((>>=)), + Monoid (mappend, mempty), + NFData (..), + Num ((+)), + Ord (compare), + Ordering (..), + Read, + Semigroup ((<>)), + Show, + String, + Word, + either, + flip, + id, + on, + seq, + show, + ($), + ($>), + (&&), + (.), + (<$>), + (<&>), + (<<$>>), + (||), + ) + +import Control.Monad.Fail (fail) +import Data.Aeson (ToJSONKey) +import qualified Data.Attoparsec.Text as Atto +import qualified Data.Text as T +import GHC.Base (error) +import qualified GHC.Read as GHC ( + readsPrec, + ) +import qualified GHC.Show as GHC ( + show, + ) -import Control.Monad.Fail ( fail ) -import Data.Aeson ( ToJSONKey ) -import qualified Data.Attoparsec.Text as Atto -import qualified Data.Text as T -import GHC.Base ( error ) -import qualified GHC.Read as GHC - ( readsPrec ) -import qualified GHC.Show as GHC - ( show ) -- | AppVersion is the core representation of the SemverQuad type. -newtype Version = Version { unVersion :: (Word, Word, Word, Word) } deriving (Eq, Ord, ToJSONKey, Hashable) +newtype Version = Version {unVersion :: (Word, Word, Word, Word)} deriving (Eq, Ord, ToJSONKey, Hashable) + + instance Show Version where show (Version (x, y, z, q)) = let postfix = if q == 0 then "" else '.' : GHC.show q - in GHC.show x <> "." <> GHC.show y <> "." <> GHC.show z <> postfix + in GHC.show x <> "." <> GHC.show y <> "." <> GHC.show z <> postfix instance IsString Version where fromString s = either error id $ Atto.parseOnly parseVersion (T.pack s) instance Read Version where readsPrec _ s = case Atto.parseOnly parseVersion (T.pack s) of - Left _ -> [] + Left _ -> [] Right a -> [(a, "")] + + -- | A change in the value found at 'major' implies a breaking change in the API that this version number describes major :: Version -> Word major (Version (x, _, _, _)) = x + -- | A change in the value found at 'minor' implies a backwards compatible addition to the API that this version number -- describes minor :: Version -> Word minor (Version (_, y, _, _)) = y + -- | A change in the value found at 'patch' implies that the implementation of the API has changed without changing the -- invariants promised by the API. In many cases this will be incremented when repairing broken functionality patch :: Version -> Word patch (Version (_, _, z, _)) = z + -- | This is the fundamentally new value in comparison to the original semver 2.0 specification. It is given the same -- semantics as 'patch' above, which begs the question, when should you update this value instead of that one. Generally -- speaking, if you are both the package author and maintainer, you should not ever increment this number, as it is @@ -121,6 +134,19 @@ revision (Version (_, _, _, q)) = q -- Left negates it type Operator = Either Ordering Ordering + +primOrd :: Operator -> Ordering +primOrd = either id id + + +complement :: Ordering -> Ordering -> Ordering +complement LT EQ = GT +complement LT GT = EQ +complement EQ GT = LT +complement a b | a == b = a +complement a b = complement b a + + -- | 'VersionRange' is the algebra of sets of versions. They can be constructed by having an 'Anchor' term which -- compares against the target version, or can be described with 'Conj' which is a conjunction, or 'Disj', which is a -- disjunction. The 'Any' and 'All' terms are primarily there to round out the algebra, but 'Any' is also exposed due to @@ -146,80 +172,271 @@ data VersionRange | Any | None deriving (Eq) + + instance NFData VersionRange where rnf (Conj a b) = rnf a `seq` rnf b rnf (Disj a b) = rnf a `seq` rnf b - rnf other = other `seq` () + rnf other = other `seq` () + + +nodes :: VersionRange -> Word64 +nodes Any = 1 +nodes None = 1 +nodes (Anchor _ _) = 1 +nodes (Conj a b) = 1 + nodes a + nodes b +nodes (Disj a b) = 1 + nodes a + nodes b + -- | Smart constructor for conjunctions. Eagerly evaluates zeros and identities conj :: VersionRange -> VersionRange -> VersionRange -conj Any b = b -conj a Any = a -conj None _ = None -conj _ None = None -conj !a !b = Conj a b +conj a b = reduce $ Conj a b + -- | Smart constructor for disjunctions. Eagerly evaluates zeros and identities disj :: VersionRange -> VersionRange -> VersionRange -disj Any _ = Any -disj _ Any = Any -disj None b = b -disj a None = a -disj !a !b = Disj a b +disj a b = reduce $ Disj a b + + +-- conj forms +data Diamond = Diamond + { lowerInclusive :: Bool + , lowerVersion :: Version + , upperVersion :: Version + , upperInclusive :: Bool + } + + +data Fish = Fish + { noseInclusive :: Bool + , noseVersion :: Version + , tailVersion :: Version + } + + +data DeadEyes = DeadEyes + { lowerEye :: Version + , upperEye :: Version + } + + +diamond :: VersionRange -> Maybe Diamond +diamond (Conj (Anchor opA vA) (Anchor opB vB)) = _ +diamond _ = Nothing + + +fish :: VersionRange -> Maybe Fish +fish = _ + + +deadEyes :: VersionRange -> Maybe DeadEyes +deadEyes = _ + + +reduce :: VersionRange -> VersionRange +reduce Any = Any +reduce None = None +reduce vr@(Anchor _ _) = vr +reduce (Conj Any vr) = vr +reduce (Conj vr Any) = vr +reduce (Conj None _) = None +reduce (Conj _ None) = None +reduce (Disj Any _) = Any +reduce (Disj _ Any) = Any +reduce (Disj None vr) = vr +reduce (Disj vr None) = vr +reduce x@(Conj a@(Anchor op pt) b@(Anchor op' pt')) = case compare pt pt' of + GT -> reduce (Conj b a) -- conj commutes so we can make normalization order the points + EQ -> case (isRight op, isRight op', isRight op == isRight op', primOrd op == primOrd op') of + -- the theorems found here will elucidate what is going on + -- https://faculty.uml.edu/klevasseur/ads/s-laws-of-set-theory.html + -- conj idempodent law: these sets are identical + (_, _, True, True) -> a + -- conj complement law: these sets are opposites + (_, _, False, True) -> None + -- inequality incompatibility: these sets do not overlap + (True, True, _, False) -> None + -- conj absorption law (right): the right set is more specific + (False, True, _, False) -> b + -- conj absorption law (left): the left set is more specific + (True, False, _, False) -> a + -- all that is left is to intersect these sets. In every one of these cases the intersection can be expressed + -- as exactly the ordering that is not mentioned by the other two. + (False, False, _, False) -> Anchor (Right $ complement (primOrd op) (primOrd op')) pt + LT -> case (op, op') of -- at this point the left post is is guaranteed to be a lower version than the right + (Left LT, Left LT) -> b + (Left LT, Left EQ) -> x + (Left LT, Left GT) -> x + (Left LT, Right LT) -> x + (Left LT, Right EQ) -> b + (Left LT, Right GT) -> b + (Left EQ, Left LT) -> b + (Left EQ, Left EQ) -> x + (Left EQ, Left GT) -> x + (Left EQ, Right LT) -> x + (Left EQ, Right EQ) -> b + (Left EQ, Right GT) -> b + (Left GT, Left LT) -> None + (Left GT, Left EQ) -> a + (Left GT, Left GT) -> a + (Left GT, Right LT) -> a + (Left GT, Right EQ) -> None + (Left GT, Right GT) -> None + (Right LT, Left LT) -> None + (Right LT, Left EQ) -> a + (Right LT, Left GT) -> a + (Right LT, Right LT) -> a + (Right LT, Right EQ) -> None + (Right LT, Right GT) -> None + (Right EQ, Left LT) -> None + (Right EQ, Left EQ) -> a + (Right EQ, Left GT) -> a + (Right EQ, Right LT) -> a + (Right EQ, Right EQ) -> None + (Right EQ, Right GT) -> None + (Right GT, Left LT) -> b + (Right GT, Left EQ) -> x + (Right GT, Left GT) -> x + (Right GT, Right LT) -> x + (Right GT, Right EQ) -> b + (Right GT, Right GT) -> b +reduce x@(Disj a@(Anchor op pt) b@(Anchor op' pt')) = case compare pt pt' of + GT -> reduce (Disj b a) + EQ -> case (isRight op, isRight op', isRight op == isRight op', primOrd op == primOrd op') of + (_, _, True, True) -> a + (_, _, False, True) -> Any + (True, True, _, False) -> Anchor (Left $ complement (primOrd op) (primOrd op')) pt + (False, True, _, False) -> a + (True, False, _, False) -> b + (False, False, _, False) -> Any + LT -> case (op, op') of + (Left LT, Left LT) -> a + (Left LT, Left EQ) -> Any + (Left LT, Left GT) -> Any + (Left LT, Right LT) -> Any + (Left LT, Right EQ) -> a + (Left LT, Right GT) -> a + (Left EQ, Left LT) -> a + (Left EQ, Left EQ) -> Any + (Left EQ, Left GT) -> Any + (Left EQ, Right LT) -> Any + (Left EQ, Right EQ) -> a + (Left EQ, Right GT) -> a + (Left GT, Left LT) -> x + (Left GT, Left EQ) -> b + (Left GT, Left GT) -> b + (Left GT, Right LT) -> b + (Left GT, Right EQ) -> x + (Left GT, Right GT) -> x + (Right LT, Left LT) -> x + (Right LT, Left EQ) -> b + (Right LT, Left GT) -> b + (Right LT, Right LT) -> b + (Right LT, Right EQ) -> x + (Right LT, Right GT) -> x + (Right EQ, Left LT) -> x + (Right EQ, Left EQ) -> b + (Right EQ, Left GT) -> b + (Right EQ, Right LT) -> b + (Right EQ, Right EQ) -> x + (Right EQ, Right GT) -> x + (Right GT, Left LT) -> a + (Right GT, Left EQ) -> Any + (Right GT, Left GT) -> Any + (Right GT, Right LT) -> Any + (Right GT, Right EQ) -> a + (Right GT, Right GT) -> a +reduce (Conj a@(Conj _ _) b@(Anchor _ _)) = reduce (Conj b a) +reduce x@(Conj a@(Anchor op pt) b@(Conj p q)) = case (p, q) of + ((Anchor opP ptP), (Anchor opQ ptQ)) -> case compare ptP ptQ of + LT -> case (opP, opQ) of + -- diamonds <> + (Right GT, Right LT) -> case op of + Right GT | pt <= ptP -> b + Right GT | pt >= ptQ -> None + Right GT | pt > ptP && pt < ptQ -> Conj a q + Left LT | pt <= ptP -> b + Left LT | pt > ptQ -> None + _ -> x + (Right GT, Left GT) -> x + (Left LT, Right LT) -> x + (Left LT, Left GT) -> x + -- fish x> + (Left EQ, Right LT) -> x + (Left EQ, Left GT) -> x + -- fish x + (Left LT, Left EQ) -> x + -- dead xx + (Left EQ, Left EQ) -> x + -- all other states are unstable for conj + _ -> reduce (Conj a (reduce b)) + _ -> reduce (Conj a (reduce b)) + _ -> x +reduce rest = rest + exactly :: Version -> VersionRange exactly = Anchor (Right EQ) + instance Show VersionRange where - show (Anchor ( Left EQ) v ) = '!' : '=' : GHC.show v - show (Anchor ( Right EQ) v ) = '=' : GHC.show v - show (Anchor ( Left LT) v ) = '>' : '=' : GHC.show v - show (Anchor ( Right LT) v ) = '<' : GHC.show v - show (Anchor ( Left GT) v ) = '<' : '=' : GHC.show v - show (Anchor ( Right GT) v ) = '>' : GHC.show v - show (Conj a@(Disj _ _) b@(Disj _ _)) = paren (GHC.show a) <> (' ' : paren (GHC.show b)) - show (Conj a@(Disj _ _) b ) = paren (GHC.show a) <> (' ' : GHC.show b) - show (Conj a b@(Disj _ _)) = GHC.show a <> (' ' : paren (GHC.show b)) - show (Conj a b ) = GHC.show a <> (' ' : GHC.show b) - show (Disj a b ) = GHC.show a <> " || " <> GHC.show b - show Any = "*" - show None = "!" + show (Anchor (Left EQ) v) = '!' : '=' : GHC.show v + show (Anchor (Right EQ) v) = '=' : GHC.show v + show (Anchor (Left LT) v) = '>' : '=' : GHC.show v + show (Anchor (Right LT) v) = '<' : GHC.show v + show (Anchor (Left GT) v) = '<' : '=' : GHC.show v + show (Anchor (Right GT) v) = '>' : GHC.show v + show (Conj a@(Disj _ _) b@(Disj _ _)) = paren (GHC.show a) <> (' ' : paren (GHC.show b)) + show (Conj a@(Disj _ _) b) = paren (GHC.show a) <> (' ' : GHC.show b) + show (Conj a b@(Disj _ _)) = GHC.show a <> (' ' : paren (GHC.show b)) + show (Conj a b) = GHC.show a <> (' ' : GHC.show b) + show (Disj a b) = GHC.show a <> " || " <> GHC.show b + show Any = "*" + show None = "!" instance Read VersionRange where readsPrec _ s = case Atto.parseOnly parseRange (T.pack s) of - Left _ -> [] + Left _ -> [] Right a -> [(a, "")] + paren :: String -> String paren = mappend "(" . flip mappend ")" -newtype AnyRange = AnyRange { unAnyRange :: VersionRange } + +newtype AnyRange = AnyRange {unAnyRange :: VersionRange} instance Semigroup AnyRange where (<>) = AnyRange <<$>> disj `on` unAnyRange instance Monoid AnyRange where mempty = AnyRange None -newtype AllRange = AllRange { unAllRange :: VersionRange } + +newtype AllRange = AllRange {unAllRange :: VersionRange} instance Semigroup AllRange where (<>) = AllRange <<$>> conj `on` unAllRange instance Monoid AllRange where mempty = AllRange Any + -- | Predicate for deciding whether the 'Version' is in the 'VersionRange' satisfies :: Version -> VersionRange -> Bool satisfies v (Anchor op v') = either (\c x y -> compare x y /= c) (\c x y -> compare x y == c) op v v' -satisfies v (Conj a b ) = v `satisfies` a && v `satisfies` b -satisfies v (Disj a b ) = v `satisfies` a || v `satisfies` b -satisfies _ Any = True -satisfies _ None = False +satisfies v (Conj a b) = v `satisfies` a && v `satisfies` b +satisfies v (Disj a b) = v `satisfies` a || v `satisfies` b +satisfies _ Any = True +satisfies _ None = False + (<||) :: Version -> VersionRange -> Bool (<||) = satisfies {-# INLINE (<||) #-} + (||>) :: VersionRange -> Version -> Bool (||>) = flip satisfies {-# INLINE (||>) #-} + parseOperator :: Atto.Parser Operator parseOperator = (Atto.char '=' $> Right EQ) @@ -229,14 +446,16 @@ parseOperator = <|> (Atto.char '>' $> Right GT) <|> (Atto.char '<' $> Right LT) + parseVersion :: Atto.Parser Version parseVersion = do major' <- Atto.decimal <* Atto.char '.' minor' <- Atto.decimal <* Atto.char '.' patch' <- Atto.decimal - quad' <- Atto.option 0 $ Atto.char '.' *> Atto.decimal + quad' <- Atto.option 0 $ Atto.char '.' *> Atto.decimal pure $ Version (major', minor', patch', quad') + -- >>> Atto.parseOnly parseRange "=2.3.4 1.2.3.4 - 2.3.4.5 (>3.0.0 || <3.4.5)" -- Right =2.3.4 >=1.2.3.4 <=2.3.4.5 ((>3.0.0 || <3.4.5)) -- >>> Atto.parseOnly parseRange "0.2.6" @@ -248,47 +467,56 @@ parseRange = s <|> (Atto.char '*' $> Any) <|> (Anchor (Right EQ) <$> parseVersio sub = Atto.char '(' *> Atto.skipSpace *> parseRange <* Atto.skipSpace <* Atto.char ')' s = unAnyRange - . foldMap AnyRange + . foldMap AnyRange <$> ((p <|> sub) `Atto.sepBy1` (Atto.skipSpace *> Atto.string "||" <* Atto.skipSpace)) p = unAllRange . foldMap AllRange <$> ((a <|> sub) `Atto.sepBy1` Atto.space) a = liftA2 Anchor parseOperator parseVersion <|> caret <|> tilde <|> wildcard <|> hyphen + -- >>> liftA2 satisfies (Atto.parseOnly parseVersion "0.20.1.1") (Atto.parseOnly parseRange "^0.20.1") -- Right True caret :: Atto.Parser VersionRange -caret = (Atto.char '^' *> parseVersion) <&> \case - v@(Version (0, 0, 0, _)) -> Anchor (Right EQ) v - v@(Version (0, 0, z, _)) -> rangeIE v (Version (0, 0, z + 1, 0)) - v@(Version (0, y, _, _)) -> rangeIE v (Version (0, y + 1, 0, 0)) - v@(Version (x, _, _, _)) -> rangeIE v (Version (x + 1, 0, 0, 0)) +caret = + (Atto.char '^' *> parseVersion) <&> \case + v@(Version (0, 0, 0, _)) -> Anchor (Right EQ) v + v@(Version (0, 0, z, _)) -> rangeIE v (Version (0, 0, z + 1, 0)) + v@(Version (0, y, _, _)) -> rangeIE v (Version (0, y + 1, 0, 0)) + v@(Version (x, _, _, _)) -> rangeIE v (Version (x + 1, 0, 0, 0)) + -- >>> Atto.parseOnly tilde "~1.2.3.4" -- Right >=1.2.3.4 <1.2.4 tilde :: Atto.Parser VersionRange -tilde = (Atto.char '~' *> (Atto.decimal `Atto.sepBy1` Atto.char '.')) >>= \case - [x, y, z, q] -> pure $ rangeIE (Version (x, y, z, q)) (Version (x, y, z + 1, 0)) - [x, y, z] -> pure $ rangeIE (Version (x, y, z, 0)) (Version (x, y + 1, 0, 0)) - [x, y] -> pure $ rangeIE (Version (x, y, 0, 0)) (Version (x, y + 1, 0, 0)) - [x] -> pure $ rangeIE (Version (x, 0, 0, 0)) (Version (x + 1, 0, 0, 0)) - o -> fail $ "Invalid number of version numbers: " <> show (length o) +tilde = + (Atto.char '~' *> (Atto.decimal `Atto.sepBy1` Atto.char '.')) >>= \case + [x, y, z, q] -> pure $ rangeIE (Version (x, y, z, q)) (Version (x, y, z + 1, 0)) + [x, y, z] -> pure $ rangeIE (Version (x, y, z, 0)) (Version (x, y + 1, 0, 0)) + [x, y] -> pure $ rangeIE (Version (x, y, 0, 0)) (Version (x, y + 1, 0, 0)) + [x] -> pure $ rangeIE (Version (x, 0, 0, 0)) (Version (x + 1, 0, 0, 0)) + o -> fail $ "Invalid number of version numbers: " <> show (length o) + range :: Bool -> Bool -> Version -> Version -> VersionRange range inc0 inc1 v0 v1 = let lo = if inc0 then Left LT else Right GT hi = if inc1 then Left GT else Right LT - in Conj (Anchor lo v0) (Anchor hi v1) + in Conj (Anchor lo v0) (Anchor hi v1) + rangeIE :: Version -> Version -> VersionRange rangeIE = range True False + -- >>> Atto.parseOnly wildcard "1.2.3.x" -- Right >=1.2.3 <1.2.4 wildcard :: Atto.Parser VersionRange -wildcard = (Atto.many1 (Atto.decimal <* Atto.char '.') <* Atto.char 'x') >>= \case - [x, y, z] -> pure $ rangeIE (Version (x, y, z, 0)) (Version (x, y, z + 1, 0)) - [x, y] -> pure $ rangeIE (Version (x, y, 0, 0)) (Version (x, y + 1, 0, 0)) - [x] -> pure $ rangeIE (Version (x, 0, 0, 0)) (Version (x + 1, 0, 0, 0)) - o -> fail $ "Invalid number of version numbers: " <> show (length o) +wildcard = + (Atto.many1 (Atto.decimal <* Atto.char '.') <* Atto.char 'x') >>= \case + [x, y, z] -> pure $ rangeIE (Version (x, y, z, 0)) (Version (x, y, z + 1, 0)) + [x, y] -> pure $ rangeIE (Version (x, y, 0, 0)) (Version (x, y + 1, 0, 0)) + [x] -> pure $ rangeIE (Version (x, 0, 0, 0)) (Version (x + 1, 0, 0, 0)) + o -> fail $ "Invalid number of version numbers: " <> show (length o) + -- >>> Atto.parseOnly hyphen "0.1.2.3 - 1.2.3.4" -- Right >=0.1.2.3 <=1.2.3.4 diff --git a/test/Main.hs b/test/Main.hs index ff3522c..f29a8e1 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -1,13 +1,15 @@ module Main where -import Test.Hspec.Runner -import qualified Spec -import Test.Hspec.Formatters -import Startlude import GHC.IO.Encoding +import qualified Lib.Types.EmverProp as EmverProp +import qualified Spec +import Startlude +import Test.Hspec.Formatters +import Test.Hspec.Runner main :: IO () main = do setLocaleEncoding utf8 + EmverProp.tests hspecWith defaultConfig { configFormatter = Just progress } Spec.spec