packages feed

skeletest 0.2.0 → 0.2.1

raw patch · 11 files changed

+200/−12 lines, 11 files

Files

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  ```