skeletest 0.2.0 → 0.2.1
raw patch · 11 files changed
+200/−12 lines, 11 files
Files
- CHANGELOG.md +4/−0
- README.md +1/−1
- skeletest.cabal +1/−1
- src/Skeletest/Assertions.hs +7/−0
- src/Skeletest/Internal/Predicate.hs +124/−8
- src/Skeletest/Internal/Spec.hs +20/−0
- src/Skeletest/Internal/TestRunner.hs +6/−0
- src/Skeletest/Predicate.hs +1/−0
- src/Skeletest/Prop/Internal.hs +3/−2
- test/Skeletest/PredicateSpec.hs +11/−0
- test/Skeletest/__snapshots__/PredicateSpec.snap.md +22/−0
CHANGELOG.md view
@@ -1,3 +1,7 @@+## v0.2.1++* Add `P.list`+ ## v0.2.0 * Move setting properties to `Skeletest.Prop`
README.md view
@@ -219,7 +219,7 @@ * `P.returns (P.gt 10)` * Satisfied when the left hand side is an `IO` action that returns a value greater than `10`. -* `P.throws MyException`+* `P.throws (P.eq MyException)` * Satisfied when the left hand side is an `IO` action that throws the given exception. ### Unit and Integration tests
skeletest.cabal view
@@ -1,7 +1,7 @@ cabal-version: 3.0 name: skeletest-version: 0.2.0+version: 0.2.1 synopsis: Batteries-included, opinionated test framework description: Batteries-included, opinionated test framework. See README.md for more details. homepage: https://github.com/brandonchinn178/skeletest#readme
src/Skeletest/Assertions.hs view
@@ -45,12 +45,17 @@ infix 1 `shouldBe`, `shouldNotBe`, `shouldSatisfy`, `shouldNotSatisfy` +-- | Assert that the given input should match the given value.+-- Equivalent to @actual `shouldSatisfy` P.eq expected@ shouldBe :: (HasCallStack, Testable m, Eq a) => a -> a -> m () actual `shouldBe` expected = GHC.withFrozenCallStack $ actual `shouldSatisfy` P.eq expected +-- | Assert that the given input should not match the given value.+-- Equivalent to @actual `shouldNotSatisfy` P.eq expected@ shouldNotBe :: (HasCallStack, Testable m, Eq a) => a -> a -> m () actual `shouldNotBe` expected = GHC.withFrozenCallStack $ actual `shouldNotSatisfy` P.eq expected +-- | Assert that the given input should satisfy the given predicate. shouldSatisfy :: (HasCallStack, Testable m) => a -> Predicate m a -> m () actual `shouldSatisfy` p = GHC.withFrozenCallStack $@@ -58,6 +63,7 @@ PredicateSuccess -> pure () PredicateFail msg -> failTest' msg +-- | Assert that the given input should not satisfy the given predicate. shouldNotSatisfy :: (HasCallStack, Testable m) => a -> Predicate m a -> m () actual `shouldNotSatisfy` p = GHC.withFrozenCallStack $ actual `shouldSatisfy` P.not p @@ -67,6 +73,7 @@ (modifyIORef failContextRef (Text.pack msg :)) (modifyIORef failContextRef (drop 1)) +-- | Unconditionally fail the test with the given message. failTest :: (HasCallStack, Testable m) => String -> m a failTest = GHC.withFrozenCallStack $ failTest' . Text.pack
src/Skeletest/Internal/Predicate.hs view
@@ -27,6 +27,7 @@ nothing, left, right,+ list, IsPredTuple (..), tup, con,@@ -203,6 +204,7 @@ {----- General -----} +-- | A predicate that matches any value anything :: (Monad m) => Predicate m a anything = Predicate@@ -219,23 +221,41 @@ {----- Ord -----} +-- | A predicate checking if the input is equal to the given value+--+-- >>> 1 `shouldSatisfy` P.eq 1 eq :: (Eq a, Monad m) => a -> Predicate m a eq = mkPredicateOp "=" "≠" $ \actual expected -> actual == expected +-- | A predicate checking if the input is greater than the given value+--+-- >>> 1 `shouldSatisfy` P.gt 0 gt :: (Ord a, Monad m) => a -> Predicate m a gt = mkPredicateOp ">" "≯" $ \actual expected -> actual > expected +-- | A predicate checking if the input is greater than or equal to the given value+--+-- >>> 1 `shouldSatisfy` P.gte 0 gte :: (Ord a, Monad m) => a -> Predicate m a gte = mkPredicateOp "≥" "≱" $ \actual expected -> actual > expected Prelude.|| actual == expected +-- | A predicate checking if the input is less than the given value+--+-- >>> 1 `shouldSatisfy` P.lt 10 lt :: (Ord a, Monad m) => a -> Predicate m a lt = mkPredicateOp "<" "≮" $ \actual expected -> actual < expected +-- | A predicate checking if the input is less than or equal to the given value+--+-- >>> 1 `shouldSatisfy` P.lte 10 lte :: (Ord a, Monad m) => a -> Predicate m a lte = mkPredicateOp "≤" "≰" $ \actual expected -> actual < expected Prelude.|| actual == expected {----- Data types -----} +-- | A predicate checking if the input is Just, wrapping a value matching the given predicate.+--+-- >>> Just 1 `shouldSatisfy` P.just (P.gt 0) just :: (Monad m) => Predicate m a -> Predicate m (Maybe a) just p = conMatches "Just" fieldNames toFields preds where@@ -245,6 +265,9 @@ _ -> Nothing preds = HCons p HNil +-- | A predicate checking if the input is Nothing+--+-- >>> Nothing `shouldSatisfy` P.nothing nothing :: (Monad m) => Predicate m (Maybe a) nothing = conMatches "Nothing" fieldNames toFields preds where@@ -254,6 +277,9 @@ _ -> Nothing preds = HNil +-- | A predicate checking if the input is Left, wrapping a value matching the given predicate.+--+-- >>> Left 1 `shouldSatisfy` P.left (P.gt 0) left :: (Monad m) => Predicate m a -> Predicate m (Either a b) left p = conMatches "Left" fieldNames toFields preds where@@ -263,6 +289,9 @@ _ -> Nothing preds = HCons p HNil +-- | A predicate checking if the input is Right, wrapping a value matching the given predicate.+--+-- >>> Right 1 `shouldSatisfy` P.right (P.gt 0) right :: (Monad m) => Predicate m b -> Predicate m (Either a b) right p = conMatches "Right" fieldNames toFields preds where@@ -272,6 +301,34 @@ _ -> Nothing preds = HCons p HNil +-- | A predicate checking if the input is a list matching exactly the given predicates.+--+-- >>> [1, 2, 3] `shouldSatisfy` P.list [P.eq 1, P.eq 2, P.eq 3]+-- >>> [1, 2, 3] `shouldNotSatisfy` P.list [P.eq 1, P.eq 2]+-- >>> [1, 2, 3] `shouldNotSatisfy` P.list [P.eq 1, P.eq 2, P.eq 3, P.eq 4]+--+-- @since 0.2.1+list :: (Monad m) => [Predicate m a] -> Predicate m [a]+list predList =+ Predicate+ { predicateFunc = \actual ->+ if length actual == length predList+ then verifyAll listify <$> sequence (zipWith predicateFunc predList actual)+ else+ pure+ PredicateFuncResult+ { predicateSuccess = False+ , predicateExplain = "Got different number of elements"+ , predicateShowFailCtx = ShowFailCtx+ }+ , predicateDisp = disp+ , predicateDispNeg = dispNeg+ }+ where+ listify vals = "[" <> Text.intercalate ", " vals <> "]"+ disp = listify $ map predicateDisp predList+ dispNeg = "not " <> disp+ class IsTuple a where type TupleArgs a :: [Type] toHList :: a -> HList Identity (TupleArgs a)@@ -310,11 +367,10 @@ type ToPredTuple m (a, b, c, d, e, f) = (Predicate m a, Predicate m b, Predicate m c, Predicate m d, Predicate m e, Predicate m f) toHListPred _ (a, b, c, d, e, f) = HCons a . HCons b . HCons c . HCons d . HCons e . HCons f $ HNil --- | Matches a tuple satisfying the given predicates. Works for tuples up to 6 elements.+-- | A predicate checking if the input matches a tuple matching the given predicates.+-- Works for tuples up to 6 elements. ----- @--- P.tup (P.eq 1, P.gt 2, P.hasPrefix "hello ")--- @+-- >>> (1, 10, "hello world") `shouldSatisfy` P.tup (P.eq 1, P.gt 2, P.hasPrefix "hello ") tup :: forall a m. (IsPredTuple m a, Monad m) => ToPredTuple m a -> Predicate m a tup predTup = Predicate@@ -329,13 +385,13 @@ disp = tupify $ HList.toListWith predicateDisp preds dispNeg = "not " <> disp --- | A predicate for checking that a value matches the given constructor.+-- | A predicate checking if the input matches the given constructor. -- -- It takes one argument, which is the constructor, except with all fields -- taking a Predicate instead of the normal value. Skeletest will rewrite -- the expression so it typechecks correctly. ----- >>> user `shouldSatisfy` P.con User{name = P.eq "user1", email = P.contains "@"}+-- >>> User "user1" "user1@example.com" `shouldSatisfy` P.con User{name = P.eq "user1", email = P.contains "@"} -- -- Record fields that are omitted are not checked at all; i.e. -- @P.con Foo{}@ and @P.con Foo{a = P.anything}@ are equivalent.@@ -386,7 +442,7 @@ {----- Numeric -----} --- | A predicate for checking that a value is equal within some tolerance.+-- | A predicate checking if the input is equal to the given value within some tolerance. -- -- Useful for checking equality with floats, which might not be exactly equal. -- For more information, see: https://jvns.ca/blog/2023/01/13/examples-of-floating-point-problems/.@@ -413,11 +469,18 @@ | x < 0 = error $ "tolerance can't be negative: " <> show x | otherwise = fromRational x +-- | The tolerance to use in 'approx'.+--+-- An input satisfies a tolerance if it's within the relative tolerance+-- (rel * input) or the absolute tolerance of the reference value. data Tolerance = Tolerance { rel :: Maybe Rational+ -- ^ If provided, the relative tolerance. Defaults to 1e-6. , abs :: Rational+ -- ^ The absolute tolerance. Defaults to 1e-12. } +-- | The default tolerance for 'approx'. tol :: Tolerance tol = Tolerance{rel = Just 1e-6, abs = 1e-12} @@ -425,6 +488,9 @@ infixr 1 <<<, >>> +-- | A predicate checking if the input matches the given predicate, after applying the given function.+--+-- >>> "hello" `shouldSatisfy` (P.eq 5 P.<<< length) (<<<) :: (Monad m) => Predicate m a -> (b -> a) -> Predicate m b Predicate{..} <<< f = Predicate@@ -433,9 +499,15 @@ , predicateDispNeg } +-- | Same as '<<<', except with the arguments flipped.+--+-- >>> "hello" `shouldSatisfy` (length P.>>> P.eq 5) (>>>) :: (Monad m) => (b -> a) -> Predicate m a -> Predicate m b (>>>) = flip (<<<) +-- | A predicate checking if the input does not match the given predicate+--+-- >>> Just 2 `shouldSatisfy` P.just (P.not (P.eq 1)) not :: (Monad m) => Predicate m a -> Predicate m a not Predicate{..} = Predicate@@ -446,12 +518,21 @@ , predicateDispNeg = predicateDisp } +-- | A predicate checking if the input matches both of the given predicates+--+-- >>> 1 `shouldSatisfy` P.gt 0 P.&& P.lt 2 (&&) :: (Monad m) => Predicate m a -> Predicate m a -> Predicate m a p1 && p2 = and [p1, p2] +-- | A predicate checking if the input matches one of the given predicates+--+-- >>> 1 `shouldSatisfy` P.lt 5 P.|| P.gt 10 (||) :: (Monad m) => Predicate m a -> Predicate m a -> Predicate m a p1 || p2 = or [p1, p2] +-- | A predicate checking if the input matches all of the given predicates+--+-- >>> 1 `shouldSatisfy` P.and [P.gt 0, P.lt 2] and :: (Monad m) => [Predicate m a] -> Predicate m a and preds = Predicate@@ -464,6 +545,9 @@ andify = Text.intercalate "\nand " predList = map (parens . predicateDisp) preds +-- | A predicate checking if the input matches any of the given predicates+--+-- >>> 1 `shouldSatisfy` P.or [P.lt 5, P.gt 10] or :: (Monad m) => [Predicate m a] -> Predicate m a or preds = Predicate@@ -478,6 +562,9 @@ {----- Containers -----} +-- | A predicate checking if the input is a list-like type where some element matches the given predicate.+--+-- >>> [1, 2, 3] `shouldSatisfy` P.any (P.eq 1) any :: (Foldable t, Monad m) => Predicate m a -> Predicate m (t a) any Predicate{..} = Predicate@@ -487,6 +574,9 @@ , predicateDispNeg = "no elements matching " <> parens predicateDisp } +-- | A predicate checking if the input is a list-like type where all elements match the given predicate.+--+-- >>> [1, 2, 3] `shouldSatisfy` P.all (P.gt 0) all :: (Foldable t, Monad m) => Predicate m a -> Predicate m (t a) all Predicate{..} = Predicate@@ -496,6 +586,10 @@ , predicateDispNeg = "some elements not matching " <> parens predicateDisp } +-- | A predicate checking if the input is a list-like type where the given element is present.+-- Equivalent to @P.any . P.eq@.+--+-- >>> [1, 2, 3] `shouldSatisfy` P.elem 1 elem :: (Eq a, Foldable t, Monad m) => a -> Predicate m (t a) elem = any . eq @@ -514,6 +608,10 @@ isInfixOf = Text.isInfixOf isSuffixOf = Text.isSuffixOf +-- | A predicate checking if the input has the given prefix+--+-- >>> [1, 2, 3] `shouldSatisfy` P.hasPrefix [1, 2]+-- >>> "hello world" `shouldSatisfy` P.hasPrefix "hello " hasPrefix :: (HasSubsequences a, Monad m) => a -> Predicate m a hasPrefix prefix = Predicate@@ -535,6 +633,10 @@ disp = "has prefix " <> render prefix dispNeg = "does not have prefix " <> render prefix +-- | A predicate checking if the input contains the given subsequence+--+-- >>> [1, 2, 3] `shouldSatisfy` P.hasInfix [2]+-- >>> "hello world" `shouldSatisfy` P.hasInfix "ello" hasInfix :: (HasSubsequences a, Monad m) => a -> Predicate m a hasInfix elems = Predicate@@ -556,6 +658,10 @@ disp = "has infix " <> render elems dispNeg = "does not have infix " <> render elems +-- | A predicate checking if the input has the given suffix+--+-- >>> [1, 2, 3] `shouldSatisfy` P.hasSuffix [2, 3]+-- >>> "hello world" `shouldSatisfy` P.hasSuffix " world" hasSuffix :: (HasSubsequences a, Monad m) => a -> Predicate m a hasSuffix suffix = Predicate@@ -579,6 +685,9 @@ {----- IO -----} +-- | A predicate checking if the input is an IO action that returns a value matching the given predicate.+--+-- >>> pure 1 `shouldSatisfy` P.returns (P.eq 1) returns :: (MonadIO m) => Predicate m a -> Predicate m (m a) returns Predicate{..} = Predicate@@ -604,6 +713,9 @@ , predicateDispNeg = predicateDispNeg } +-- | A predicate checking if the input is an IO action that throws an exception matching the given predicate.+--+-- >>> throwIO MyException `shouldSatisfy` P.throws (P.eq MyException) throws :: (Exception e, MonadUnliftIO m) => Predicate m e -> Predicate m (m a) throws Predicate{..} = Predicate@@ -655,7 +767,7 @@ -- @ -- prop "reverse . reverse === id" $ do -- let genList = Gen.list (Gen.linear 0 10) $ Gen.int (Gen.linear 0 1000)--- (reverse . reverse) === id \`shouldSatisfy\` P.isoWith genList+-- (reverse . reverse) P.=== id \`shouldSatisfy\` P.isoWith genList -- @ (===) :: (a -> b) -> (a -> b) -> IsoChecker a b f === g = IsoChecker (Fun "lhs" f) (Fun "rhs" g)@@ -697,6 +809,10 @@ {----- Snapshot -----} +-- | A predicate checking if the input matches the snapshot.+-- See the "Snapshot tests" section in the README.+--+-- >>> user `shouldSatisfy` P.matchesSnapshot matchesSnapshot :: (Typeable a, MonadIO m) => Predicate m a matchesSnapshot = Predicate
src/Skeletest/Internal/Spec.hs view
@@ -256,6 +256,7 @@ {----- Defining a Spec -----} +-- | The entity or concept being tested. describe :: String -> Spec -> Spec describe name = runIdentity . withSpecTrees (pure . (: []) . mkGroup) where@@ -275,9 +276,27 @@ , testAction = runTestable t } +-- | Define an IO-based test.+--+-- Should typically be written to be read as full sentences in traditional BDD style:+-- https://en.wikipedia.org/wiki/Behavior-driven_development.+--+-- @+-- describe \"User\" $ do+-- it "can be checked for equality" $ do+-- user1 `shouldBe` user1+-- @ it :: String -> IO () -> Spec it = test +-- | Define a property test.+--+-- @+-- describe \"User\" $ do+-- prop "decode . encode === Just" $ do+-- let genUser = ...+-- (decode . encode) P.=== Just \`shouldSatisfy\` P.isoWith genUser+-- @ prop :: String -> Property -> Spec prop = test @@ -335,6 +354,7 @@ Nothing -> runTest } +-- | Mark tests as tests that should only be run when explicitly specified on the command line. markManual :: Spec -> Spec markManual = withMarker MarkerManual
src/Skeletest/Internal/TestRunner.hs view
@@ -41,7 +41,13 @@ class (MonadIO m) => Testable m where runTestable :: m () -> IO TestResult++ -- | Add any context to display if the test fails.+ --+ -- >>> (code, stdout) <- runCommand ...+ -- >>> context stdout $ code `shouldBe` ExitSuccess context :: String -> m a -> m a+ throwFailure :: AssertionFail -> m a {----- TestResult -----}
src/Skeletest/Predicate.hs view
@@ -16,6 +16,7 @@ nothing, left, right,+ list, tup, con,
src/Skeletest/Prop/Internal.hs view
@@ -358,10 +358,11 @@ -- -- In the following example, if the condition does not have at least 30% -- coverage, the test will fail.+-- -- @ -- match <- forAll Gen.bool--- cover 30 "True" $ match--- cover 30 "False" $ not match+-- cover 30 "true" $ match+-- cover 30 "false" $ not match -- @ cover :: (GHC.HasCallStack) => Double -> String -> Bool -> Property cover p l cond = GHC.withFrozenCallStack $ propM $ Hedgehog.cover (Hedgehog.CoverPercentage p) (Hedgehog.LabelName l) cond
test/Skeletest/PredicateSpec.hs view
@@ -89,6 +89,17 @@ Right 1 `shouldNotSatisfy` P.right (P.gt 2) Left 1 `shouldNotSatisfy` P.right P.anything + describe "list" $ do+ it "checks list" $ do+ [1, 2, 3] `shouldSatisfy` P.list [P.eq 1, P.eq 2, P.eq 3]+ [1, 2, 3] `shouldNotSatisfy` P.list [P.eq 1, P.eq 2, P.lt 0]+ [1, 2, 3] `shouldNotSatisfy` P.list [P.eq 1, P.eq 2]+ [1, 2, 3] `shouldNotSatisfy` P.list [P.eq 1, P.eq 2, P.eq 3, P.eq 4]++ it "shows helpful failure messages" $ do+ snapshotFailure (P.list [P.eq 0, P.eq 1]) [0, 10]+ snapshotFailure (P.list [P.eq 0, P.eq 1]) [0]+ describe "tup" $ do it "checks all predicates" $ do (1, "hello") `shouldSatisfy` P.tup (P.eq 1, P.hasPrefix "he")
test/Skeletest/__snapshots__/PredicateSpec.snap.md view
@@ -268,6 +268,28 @@ -------------------------------------------------------------------------------- ``` +## Data types / list / shows helpful failure messages++```+10 ≠ 1++Expected:+ [= 0, = 1]++Got:+ [0,10]+```++```+Got different number of elements++Expected:+ [= 0, = 1]++Got:+ [0]+```+ ## Data types / tup / shows helpful failure messages ```