packages feed

horizontal-rule 0.6.0.0 → 0.7.0.0

raw patch · 25 files changed

+4548/−43 lines, 25 filesdep +arraydep +constraintsdep +containersdep −HMockdep ~ansi-wl-pprintdep ~basedep ~optparse-applicativePVP ok

version bump matches the API change (PVP)

Dependencies added: array, constraints, containers, data-default, exceptions, extra, monad-control, mtl, stm, syb, template-haskell, transformers-base, unliftio

Dependencies removed: HMock

Dependency ranges changed: ansi-wl-pprint, base, optparse-applicative, tasty, tasty-hunit, terminal-size, text, time

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -4,7 +4,7 @@ versions in `A.B.C.D` format.  `A` may be incremented arbitrarily for non-technical reasons, but [semantic versioning][SemVer] is otherwise followed, where `A.B` is the major version, `C` is the minor version, and `D`-is the patch version.  Initial development uses versions `0.0.0.D`, for which+is the patch version.  Initial development uses versions `0.0.C.D`, for which every version is considered breaking.  [PVP]: <https://pvp.haskell.org/>@@ -23,6 +23,23 @@ * Changes are listed in arbitrary order and present tense.  [KaC]: <https://keepachangelog.com/en/1.0.0/>++## 0.7.0.0 (2024-12-04)++### Breaking++* Remove support for GHC 8.6, constraining lower bounds+* Remove support for GHC 8.4, constraining lower bounds+* Remove support for GHC 8.2, constraining lower bounds+* Change minimal Cabal from 1.24 to 3.0++### Non-Breaking++* Bump `base` dependency version upper bound+* Bump `tasty` dependency version upper bound+* Bump `text` dependency version upper bound+* Bump `time` dependency version upper bound+* Vendor `HMock` and `explainable-predicates`  ## 0.6.0.0 (2023-05-28) 
LICENSE view
@@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2019-2023 Travis Cardwell+Copyright (c) 2019-2024 Travis Cardwell  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal
app/LibOA.hs view
@@ -2,7 +2,7 @@ -- | -- Module      : LibOA -- Description : supplementary functions for optparse-applicative--- Copyright   : Copyright (c) 2019-2023 Travis Cardwell+-- Copyright   : Copyright (c) 2019-2024 Travis Cardwell -- License     : MIT -- -- This is a collection of functions that I often use with
app/Main.hs view
@@ -2,7 +2,7 @@ -- | -- Module      : Main -- Description : hr: a horizontal rule for terminals--- Copyright   : Copyright (c) 2019-2023 Travis Cardwell+-- Copyright   : Copyright (c) 2019-2024 Travis Cardwell -- License     : MIT -- -- See the README for details.
horizontal-rule.cabal view
@@ -1,36 +1,34 @@-name:           horizontal-rule-version:        0.6.0.0-category:       Utils-synopsis:       horizontal rule for the terminal+cabal-version:      3.0+name:               horizontal-rule+version:            0.7.0.0+synopsis:           horizontal rule for the terminal description:   This package provides a utility for displaying a horizontal rule in a   terminal.  Please see the README on GitHub at   <https://github.com/ExtremaIS/hr-haskell#readme>.+homepage:           https://github.com/ExtremaIS/hr-haskell#readme+bug-reports:        https://github.com/ExtremaIS/hr-haskell/issues+license:            MIT+license-file:       LICENSE+author:             Travis Cardwell <travis.cardwell@extrema.is>+maintainer:         Travis Cardwell <travis.cardwell@extrema.is>+copyright:          Copyright (c) 2019-2024 Travis Cardwell+category:           Utils+build-type:         Simple -homepage:       https://github.com/ExtremaIS/hr-haskell#readme-bug-reports:    https://github.com/ExtremaIS/hr-haskell/issues-author:         Travis Cardwell <travis.cardwell@extrema.is>-maintainer:     Travis Cardwell <travis.cardwell@extrema.is>-copyright:      Copyright (c) 2019-2023 Travis Cardwell-license:        MIT-license-file:   LICENSE+extra-doc-files:+  CHANGELOG.md+  README.md -cabal-version:  1.24-build-type:     Simple tested-with:-  GHC ==8.2.2-   || ==8.4.4-   || ==8.6.5-   || ==8.8.4+  GHC ==8.8.4    || ==8.10.7    || ==9.0.2    || ==9.2.8-   || ==9.4.5-   || ==9.6.2--extra-source-files:-  CHANGELOG.md-  README.md+   || ==9.4.8+   || ==9.6.6+   || ==9.8.4+   || ==9.10.1  source-repository head   type: git@@ -49,10 +47,12 @@     , HR.Monad.Terminal   other-modules:       Paths_horizontal_rule+  autogen-modules:+      Paths_horizontal_rule   build-depends:-      base >=4.10.1 && <4.19-    , terminal-size >=0.2 && <0.4-    , text >=1.2.3 && <2.1+      base >=4.13.0.0 && <4.21+    , terminal-size >=0.3.2.1 && <0.4+    , text >=1.2.4.0 && <2.2   default-language: Haskell2010   default-extensions:       OverloadedStrings@@ -67,15 +67,15 @@       base     , horizontal-rule     , text-    , time >=1.8.0.2 && <1.13+    , time >=1.9.3 && <1.15   if flag(optparse-applicative_ge_0_18)     build-depends:         optparse-applicative >=0.18 && <0.19       , prettyprinter >=1.7.1 && <1.8   else     build-depends:-        ansi-wl-pprint >=0.6.8 && <1.1-      , optparse-applicative >=0.13 && <0.18+        ansi-wl-pprint >=0.6.9 && <1.1+      , optparse-applicative >=0.15.1.0 && <0.18   default-language: Haskell2010   ghc-options: -Wall @@ -84,16 +84,48 @@   hs-source-dirs: test   main-is: Spec.hs   other-modules:-      HR.Test+      HR.Mock+    , HR.Test+  other-modules:+      -- vendored explainable-preducates+      Test.Predicates+    , Test.Predicates.Internal.FlowMatcher+    , Test.Predicates.Internal.Util+  other-modules:+      -- vendored HMock+      Test.HMock+    , Test.HMock.ExpectContext+    , Test.HMock.Internal.ExpectSet+    , Test.HMock.Internal.Rule+    , Test.HMock.Internal.State+    , Test.HMock.Internal.Step+    , Test.HMock.Internal.TH+    , Test.HMock.Internal.Util+    , Test.HMock.MockMethod+    , Test.HMock.MockT+    , Test.HMock.Mockable+    , Test.HMock.Multiplicity+    , Test.HMock.Rule+    , Test.HMock.TH   build-depends:       base     , horizontal-rule-    , tasty >=0.12 && <1.5-    , tasty-hunit >=0.8 && <0.11-  if impl(ghc >= 8.6.1)-    other-modules:-        HR.Mock-    build-depends:-        HMock >=0.5.1 && <0.6+    , tasty >=1.2.3 && <1.6+    , tasty-hunit >=0.10.0.3 && <0.11+  build-depends:+      -- vendored dependencies+      array >=0.5.2 && <0.6+    , constraints >=0.13 && <0.15+    , containers >=0.6.2 && <0.8+    , data-default >=0.7.1 && <0.9+    , exceptions >=0.10.4 && <0.11+    , extra >=1.7.9 && <1.9+    , monad-control >=1.0.2 && <1.1+    , mtl >=2.2.2 && <2.4+    , stm >=2.5.0 && <2.6+    , syb >=0.7.2 && <0.8+    , template-haskell >=2.14 && <2.23+    , transformers-base >=0.4.5 && <0.5+    , unliftio >=0.2.18 && <0.3   default-language: Haskell2010   ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
src/HR.hs view
@@ -2,7 +2,7 @@ -- | -- Module      : HR -- Description : horizontal rule for terminals--- Copyright   : Copyright (c) 2019-2023 Travis Cardwell+-- Copyright   : Copyright (c) 2019-2024 Travis Cardwell -- License     : MIT -- -- This library is meant to be imported qualified, as follows:
src/HR/Monad/Terminal.hs view
@@ -2,7 +2,7 @@ -- | -- Module      : HR.Monad.Terminal -- Description : terminal output--- Copyright   : Copyright (c) 2019-2023 Travis Cardwell+-- Copyright   : Copyright (c) 2019-2024 Travis Cardwell -- License     : MIT ------------------------------------------------------------------------------ 
+ test/Test/HMock.hs view
@@ -0,0 +1,116 @@+{-# LANGUAGE FlexibleInstances #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- |+--+-- This module provides a monad transformer, 'MockT', which can be used to test+-- with mocks of Haskell @mtl@-style type classes.  To use a mock, you define+-- the expected actions and their results, and then run the code you are+-- testing.  The framework verifies that the behavior of the code matched your+-- expectations.+--+-- For an introduction to the idea of mocks, see+-- <https://martinfowler.com/articles/mocksArentStubs.html Mocks Aren't Stubs>,+-- by Martin Fowler.+--+-- WARNING: Hmock's API is likely to change soon.  Please ensure you use an+-- upper bound on the version number.  The current API works fine for mocking+-- with MTL-style classes.  I want HMock to also work with effect systems,+-- servant, haxl, and more.  To accomplish this, I'll need to make breaking+-- changes to the API.+--+-- Suppose you have a @MonadFilesystem@ typeclass, which is instantiated by+-- monads that implement filesystem operations:+--+-- @+-- class 'Monad' m => MonadFilesystem m where+--   readFile :: 'FilePath' -> m 'String'+--   writeFile :: 'FilePath' -> 'String' -> m ()+-- @+--+-- You can use HMock to test code using @MonadFilesystem@ like this:+--+-- @+-- copyFile :: MonadFilesystem m => 'FilePath' -> 'FilePath' -> m ()+-- copyFile a b = readFile a >>= writeFile b+--+-- 'Test.HMock.TH.makeMockable' [t|MonadFilesystem|]+--+-- spec = describe "copyFile" '$'+--   it "reads a file and writes its contents to another file" '$'+--     'runMockT' '$' do+--       'expect' '$' ReadFile "foo.txt" '|->' "contents"+--       'expect' '$' WriteFile "bar.txt" "contents" '|->' ()+--       copyFile "foo.txt" "bar.txt"+-- @+--+-- The Template Haskell splice, 'Test.HMock.TH.makeMockable', generates the+-- boilerplate needed to use @MonadFilesystem@ with HMock.  You then use+-- 'runMockT' to begin a test with mocks, 'expect' to set up your expected+-- actions and responses, and finally execute your code.+module Test.HMock+  ( +    -- * The 'Mockable' class++    -- | HMock starts with the 'Mockable' class (most of which is actually in+    -- its superclass, 'MockableBase').  This class is implemented for each+    -- interface you want to mock, and describes which actions are possible,+    -- and how to match and compare them.  It's a lot of boilerplate, so you'll+    -- usually derive it with Template Haskell, but the instance must exist.+    module Test.HMock.Mockable,++    -- * Running mocks++    -- | Tests with mocks run in the 'MockT' monad transformer, which wraps a+    -- base monad and adds the ability to delegate methods to HMock for+    -- matching.  'runMockT' is the entry point for 'MockT'.+    --+    -- This module also defines the more restricted 'MockSetup;' monad, which+    -- is used to set up defaults for a type.+    module Test.HMock.MockT,++    -- * Rules for actions and responses++    -- | The bread and butter of mocks is matching actions and specifying+    -- responses.  Matchers and corresponding responses are combined into a+    -- 'Rule'+    module Test.HMock.Rule,++    -- * Combinators for building test plans++    -- | A complete execution plans consists of a collection of individual rules+    -- combined in various ways.  HMock defines a set of composable combinators+    -- for the execution plan.+    module Test.HMock.ExpectContext,++    -- * Multiplicity++    -- | For repeated actions in your execution plan, you often want to control+    -- the number of times somrthing is allowed to happen.  This is called a+    -- 'Multiplicity'.+    module Test.HMock.Multiplicity,++    -- * Delegating mocks++    -- | In order to run your test code with the 'MockT', you need instances of+    -- your effect classes for the 'MockT' type.  If you mock all methods of the+    -- class, this can be derived using Template Haskell.  For partial mocks,+    -- you'll need to write the instances yourself, using 'mockMethod' and its+    -- cousin 'mockDefaultlessMethod'.+    module Test.HMock.MockMethod,++    -- * Template Haskell generator++    -- | These are the Template Haskell splices which generate boilerplate for+    -- your classes to be used with HMock.+    module Test.HMock.TH,+  )+where++import Test.HMock.ExpectContext+import Test.HMock.MockT+import Test.HMock.MockMethod+import Test.HMock.Mockable+import Test.HMock.Multiplicity+import Test.HMock.Rule+import Test.HMock.TH
+ test/Test/HMock/ExpectContext.hs view
@@ -0,0 +1,192 @@+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE RankNTypes #-}++-- | This module defines the 'ExpectContext' class, whose members provide the+-- combinators for building the execution plan for your mocks.  Notably, there+-- is a 'Test.HMock.MockT.MockT' instance for 'ExpectContext', so you can use+-- these combinators to add expectations inside your tests that run in+-- 'Test.HMock.MockT.MockT', as well as nesting them in other combinators.+module Test.HMock.ExpectContext+  ( MockableMethod,+    ExpectContext (..),+  )+where++import Control.Monad.IO.Class (MonadIO)+import Data.Kind (Constraint, Type)+import Data.Typeable (Typeable)+import GHC.Stack (HasCallStack)+import GHC.TypeLits (KnownSymbol, Symbol)+import Test.HMock.Mockable (Mockable)+import Test.HMock.Multiplicity (Multiplicity)+import Test.HMock.Rule (Expectable)++-- | All constraints needed to mock a method with the given class, name, base+-- monad, and return type.+type MockableMethod+  (cls :: (Type -> Type) -> Constraint)+  (name :: Symbol)+  (m :: Type -> Type)+  (r :: Type) =+  (Mockable cls, Typeable m, KnownSymbol name, Typeable r)++-- | Type class for contexts in which one can build expectations.  Notably, this+-- includes `Test.HMock.MockT.MockT`, which expects actions to be performed+-- during a test.+--+-- The methods of this class represent the user-facing API for build your+-- execution plan for mocks.+class ExpectContext (ctx :: (Type -> Type) -> Type -> Type) where+  -- | Creates an expectation that an action is performed once per given+  -- response (or exactly once if there is no response).+  --+  -- @+  -- 'Test.HMock.MockT.runMockT' '$' do+  --   'expect' '$'+  --     ReadFile "foo.txt"+  --       'Test.HMock.Rule.|->' "lorem ipsum"+  --       'Test.HMock.Rule.|->' "oops, the file changed out from under me!"+  --   callCodeUnderTest+  -- @+  --+  -- In this example, `readFile` must be called exactly twice by the tested+  -- code, and will return "lorem ipsum" the first time, but something different+  -- the second time.+  expect ::+    ( HasCallStack,+      MonadIO m,+      MockableMethod cls name m r,+      Expectable cls name m r expectable+    ) =>+    expectable ->+    ctx m ()++  -- | Creates an expectation that an action is performed some number of times.+  --+  -- @+  --   'Test.HMock.MockT.runMockT' '$' do+  --     'expect' '$' MakeList+  --     'expectN' ('Test.HMock.atLeast' 2) '$'+  --       CheckList "Cindy Lou Who" 'Test.HMock.Rule.|->' Nice+  --+  --     callCodeUnderTest+  -- @+  expectN ::+    ( HasCallStack,+      MonadIO m,+      MockableMethod cls name m r,+      Expectable cls name m r expectable+    ) =>+    -- | The number of times the action should be performed.+    Multiplicity ->+    -- | The action and its response.+    expectable ->+    ctx m ()++  -- | Specifies a response if a matching action is performed, but doesn't+  -- expect anything.  This is equivalent to @'expectN'+  -- 'Test.HMock.Multiplicity.anyMultiplicity'@, but shorter.+  --+  -- In this example, the later use of 'expectAny' overrides earlier uses, but+  -- only for calls that match its conditions.+  --+  -- @+  --   'Test.HMock.MockT.runMockT' '$' do+  --     'expectAny' '$'+  --       ReadFile_ anything 'Test.HMock.Rule.|->' "tlhIngan maH!"+  --     'expectAny' '$'+  --       ReadFile "config.txt" 'Test.HMock.Rule.|->' "lang: klingon"+  --+  --     callCodeUnderTest+  -- @+  expectAny ::+    ( HasCallStack,+      MonadIO m,+      MockableMethod cls name m r,+      Expectable cls name m r expectable+    ) =>+    expectable ->+    ctx m ()++  -- | Creates a sequential expectation.  Other actions can still happen during+  -- the sequence, but these specific expectations must be met in this order.+  --+  -- @+  --   'inSequence'+  --     [ 'expect' '$' MoveForward,+  --       'expect' '$' TurnRight,+  --       'expect' '$' MoveForward+  --     ]+  -- @+  --+  -- Beware of using 'inSequence' too often.  It is appropriate when the+  -- property you are testing is that the order of effects is correct.  If+  -- that's not the purpose of the test, consider adding several independent+  -- expectations, instead.  This avoids over-asserting, and keeps your tests+  -- less brittle.+  inSequence ::+    MonadIO m => (forall ctx'. ExpectContext ctx' => [ctx' m ()]) -> ctx m ()++  -- | Combines multiple expectations, which can occur in any order.  Most of+  -- the time, you can achieve the same thing by expecting each separately, but+  -- this can be combined in compound expectations to describe more complex+  -- ordering constraints.+  --+  -- If ambiguity checking is disabled, the choice is left-biased, so earlier+  -- options are preferred over ambiguous later options.+  --+  -- @+  --   'inSequence'+  --     [ 'inAnyOrder'+  --         [ 'expect' '$' AdjustMirrors,+  --           'expect' '$' FastenSeatBelt+  --         ],+  --       'expect' '$' StartCar+  --     ]+  -- @+  inAnyOrder ::+    MonadIO m => (forall ctx'. ExpectContext ctx' => [ctx' m ()]) -> ctx m ()++  -- | Combines multiple expectations, requiring exactly one of them to occur.+  -- If ambiguity checking is disabled, the choice is left-biased, so earlier+  -- options are preferred over ambiguous later options.+  --+  -- @+  --   'anyOf'+  --     [ 'expect' $ ApplyForJob,+  --       'expect' $ ApplyForUniversity+  --     ]+  -- @+  anyOf ::+    MonadIO m => (forall ctx'. ExpectContext ctx' => [ctx' m ()]) -> ctx m ()++  -- | Creates a parent expectation that the child expectation will happen a+  -- certain number of times.  Unlike `expectN`, the child expectation can be+  -- arbitrarily complex and span multiple actions.  Also unlike 'expectN', each+  -- new execution will restart response sequences for rules with more than one+  -- response.+  --+  -- Different occurrences of the child can be interleaved.  If ambiguity+  -- checking is disabled, progressing on an existing occurrence is preferred+  -- over starting a new occurrence when it's ambiguous.+  times ::+    MonadIO m =>+    Multiplicity ->+    (forall ctx'. ExpectContext ctx' => ctx' m ()) ->+    ctx m ()++  -- | Creates a parent expectation that the child expectation will happen a+  -- certain number of times.  Unlike `expectN`, the child expectation can be+  -- arbitrarily complex and span multiple actions.  Also unlike 'expectN', each+  -- new execution will restart response sequences for rules with more than one+  -- response.+  --+  -- Different occurrences of the child must happen consecutively, with one+  -- finishing before the next begins.+  consecutiveTimes ::+    MonadIO m =>+    Multiplicity ->+    (forall ctx'. ExpectContext ctx' => ctx' m ()) ->+    ctx m ()
+ test/Test/HMock/Internal/ExpectSet.hs view
@@ -0,0 +1,203 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE GADTSyntax #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}++-- | The internal core language of expectations in HMock.+module Test.HMock.Internal.ExpectSet where++import Test.HMock.Multiplicity+  ( Multiplicity,+    between,+    feasible, meetsMultiplicity+  )++-- | A set of expected steps and their responses.  This is the "core" language+-- of expectations for HMock.  It's based roughly on Svenningsson, Svensson,+-- Smallbone, Arts, Norell, and Hughes' Expressive Semantics of Mocking.+-- However, there are a few small adjustments.  We have two repetition operators+-- which respectively represent general repetition with interleaving, and+-- consecutive repetition.  We also attach arbitrary multiplicities to+-- repetition.+data ExpectSet step where+  ExpectStep :: step -> ExpectSet step+  ExpectNothing :: ExpectSet step+  ExpectSequence :: ExpectSet step -> ExpectSet step -> ExpectSet step+  ExpectInterleave :: ExpectSet step -> ExpectSet step -> ExpectSet step+  ExpectEither :: ExpectSet step -> ExpectSet step -> ExpectSet step+  ExpectMulti :: Multiplicity -> ExpectSet step -> ExpectSet step+  ExpectConsecutive :: Multiplicity -> ExpectSet step -> ExpectSet step+  deriving (Show, Eq)++-- | Checks whether an ExpectSet is in an "accepting" state.  In other words, is+-- it okay for the test to end here?  If False, then there are still+-- expectations that must be satisfied before the test can succeed.+satisfied :: ExpectSet step -> Bool+satisfied (ExpectStep _) = False+satisfied ExpectNothing = True+satisfied (ExpectSequence e f) = satisfied e && satisfied f+satisfied (ExpectInterleave e f) = satisfied e && satisfied f+satisfied (ExpectEither e f) = satisfied e || satisfied f+satisfied (ExpectMulti mult e) =+  feasible mult && (meetsMultiplicity mult 0 || satisfied e)+satisfied (ExpectConsecutive mult e) =+  feasible mult && (meetsMultiplicity mult 0 || satisfied e)++-- | Computes the live steps of the ExpectSet.  In other words: which individual+-- steps can be matched right now, and what are the remaining expectations in+-- each case?+liveSteps :: ExpectSet step -> [(step, ExpectSet step)]+liveSteps (ExpectStep step) = [(step, ExpectNothing)]+liveSteps ExpectNothing = []+liveSteps (ExpectSequence e f) =+  (fmap (`ExpectSequence` f) <$> liveSteps e)+    ++ if satisfied e then liveSteps f else []+liveSteps (ExpectInterleave e f) =+  (fmap (`ExpectInterleave` f) <$> liveSteps e)+    ++ (fmap (ExpectInterleave e) <$> liveSteps f)+liveSteps (ExpectEither e f) = liveSteps e ++ liveSteps f+liveSteps (ExpectMulti mult e)+  | feasible (mult - 1) =+    [ (step, ExpectInterleave f (ExpectMulti (mult - 1) e))+      | (step, f) <- liveSteps e+    ]+  | otherwise = []+liveSteps (ExpectConsecutive mult e)+  | feasible (mult - 1) =+    [ (step, ExpectSequence f (ExpectConsecutive (mult - 1) e))+      | (step, f) <- liveSteps e+    ]+  | otherwise = []++-- | Performs a complete simplification of the ExpectSet.  This could be slow,+-- but we intend to do it only for error messages, so it need not be very fast.+simplify :: ExpectSet step -> ExpectSet step+simplify (ExpectSequence e f)+  | ExpectNothing <- e' = f'+  | ExpectNothing <- f' = e'+  | ExpectSequence e1 e2 <- e' =+    simplify (ExpectSequence e1 (ExpectSequence e2 f'))+  | otherwise = ExpectSequence e' f'+  where+    e' = simplify e+    f' = simplify f+simplify (ExpectInterleave e f)+  | ExpectNothing <- e' = f'+  | ExpectNothing <- f' = e'+  | ExpectInterleave e1 e2 <- e' =+    simplify (ExpectInterleave e1 (ExpectInterleave e2 f'))+  | otherwise = ExpectInterleave e' f'+  where+    e' = simplify e+    f' = simplify f+simplify (ExpectEither e f)+  | ExpectNothing <- e', ExpectNothing <- f' = ExpectNothing+  | ExpectNothing <- e' = simplify (ExpectEither f' ExpectNothing)+  | ExpectEither e1 e2 <- e' =+    simplify (ExpectEither e1 (ExpectEither e2 f'))+  | ExpectNothing <- f', satisfied e' = e'+  | ExpectNothing <- f' = simplify (ExpectMulti (between 0 1) e')+  | otherwise = ExpectEither e' f'+  where+    e' = simplify e+    f' = simplify f+simplify (ExpectMulti m e)+  | not (feasible m) = ExpectMulti m ExpectNothing +  | ExpectNothing <- e' = ExpectNothing+  | m == 0 = ExpectNothing+  | m == 1 = e'+  | otherwise = ExpectMulti m e'+  where+    e' = simplify e+simplify (ExpectConsecutive m e)+  | not (feasible m) = ExpectConsecutive m ExpectNothing +  | ExpectNothing <- e' = ExpectNothing+  | m == 0 = ExpectNothing+  | m == 1 = e'+  | otherwise = ExpectConsecutive m e'+  where+    e' = simplify e+simplify other = other++-- | Get a list of all steps mentioned by an 'ExpectSet'.  This is used to+-- determine which classes need to be initialized before adding an expectation.+getSteps :: ExpectSet step -> [step]+getSteps ExpectNothing = []+getSteps (ExpectStep step) = [step]+getSteps (ExpectInterleave e f) = getSteps e ++ getSteps f+getSteps (ExpectSequence e f) = getSteps e ++ getSteps f+getSteps (ExpectEither e f) = getSteps e ++ getSteps f+getSteps (ExpectMulti _ e) = getSteps e+getSteps (ExpectConsecutive _ e) = getSteps e++-- | A higher-level intermediate form of an ExpectSet suitable for communication+-- with the user.  Chains of binary operators are collected into sequences to+-- be displayed in lists rather than arbitrary nesting.+data CollectedSet step where+  CollectedStep :: step -> CollectedSet step+  CollectedNothing :: CollectedSet step+  CollectedSequence :: [CollectedSet step] -> CollectedSet step+  CollectedInterleave :: [CollectedSet step] -> CollectedSet step+  CollectedChoice :: [CollectedSet step] -> CollectedSet step+  CollectedMulti :: Multiplicity -> CollectedSet step -> CollectedSet step+  CollectedConsecutive :: Multiplicity -> CollectedSet step -> CollectedSet step++-- | Collects an ExpectSet into the intermediate form for display.  It's assumed+-- that the expression was simplified before this operation.+collect :: ExpectSet step -> CollectedSet step+collect (ExpectStep s) = CollectedStep s+collect ExpectNothing = CollectedNothing+collect (ExpectSequence e f) = CollectedSequence (collect e : fs)+  where+    fs = case collect f of+      CollectedSequence f' -> f'+      f' -> [f']+collect (ExpectInterleave e f) = CollectedInterleave (collect e : fs)+  where+    fs = case collect f of+      CollectedInterleave f' -> f'+      f' -> [f']+collect (ExpectEither e f) = CollectedChoice (collect e : fs)+  where+    fs = case collect f of+      CollectedChoice f' -> f'+      f' -> [f']+collect (ExpectMulti m e) = CollectedMulti m (collect e)+collect (ExpectConsecutive m e) = CollectedConsecutive m (collect e)++-- | Converts a set of expectations into a string that summarizes them, with+-- the given prefix (used to indent).+formatExpectSet :: (Show step) => ExpectSet step -> String+formatExpectSet = go "" . collect . simplify+  where+    go prefix CollectedNothing = prefix ++ "* nothing"+    go prefix (CollectedStep step) = prefix ++ "* " ++ show step+    go prefix (CollectedSequence cs) =+      prefix ++ "* in sequence:\n" ++ unlines (map (go ("    " ++ prefix)) cs)+    go prefix (CollectedInterleave cs) =+      prefix ++ "* in any order:\n" ++ unlines (map (go ("    " ++ prefix)) cs)+    go prefix (CollectedChoice cs) =+      prefix ++ "* any of:\n" ++ unlines (map (go ("    " ++ prefix)) cs)+    go prefix (CollectedMulti m e) =+      prefix ++ "* " ++ show m ++ ":\n" ++ go ("    " ++ prefix) e+    go prefix (CollectedConsecutive m e) =+      prefix ++ "* " ++ show m ++ " consecutively:\n" ++ go ("    " ++ prefix) e++-- | Reduces a set of expectations to the minimum steps that would be required+-- to satisfy the entire set.  This weeds out unnecessary information before+-- reporting that there were unmet expectations at the end of the test.+excess :: ExpectSet step -> ExpectSet step+excess = simplify . go+  where+    go (ExpectSequence e f) = ExpectSequence (go e) (go f)+    go (ExpectInterleave e f) = ExpectInterleave (go e) (go f)+    go (ExpectEither e f)+      | satisfied e || satisfied f = ExpectNothing+      | otherwise = ExpectEither (go e) (go f)+    go (ExpectMulti m e)+      | meetsMultiplicity m 0 = ExpectNothing+      | otherwise = ExpectMulti m (go e)+    go (ExpectConsecutive m e)+      | meetsMultiplicity m 0 = ExpectNothing+      | otherwise = ExpectConsecutive m (go e)+    go other = other
+ test/Test/HMock/Internal/Rule.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE KindSignatures #-}++-- | Internal module to define 'Rule', so that its constructor can be visible+-- to other implementation code.+module Test.HMock.Internal.Rule where++import Data.Kind (Constraint, Type)+import GHC.TypeLits (Symbol)+import {-# SOURCE #-} Test.HMock.Internal.State (MockT)+import Test.HMock.Mockable (MockableBase (..))++-- | A way to match an entire action, using conditions that might depend on the+-- relationship between arguments.+data WholeMethodMatcher cls name m r where+  JustMatcher :: Matcher cls name m r -> WholeMethodMatcher cls name m r+  SuchThat ::+    Matcher cls name m r ->+    (Action cls name m r -> Bool) ->+    WholeMethodMatcher cls name m r++-- | Displays a WholeMethodMatcher.  The predicate isn't showable, but we can at+-- least indicate whether there is one present.+showWholeMatcher ::+  MockableBase cls =>+  Maybe (Action cls name m a) ->+  WholeMethodMatcher cls name m b ->+  String+showWholeMatcher a (JustMatcher m) = showMatcher a m+showWholeMatcher a (m `SuchThat` _) =+  showMatcher a m ++ " (with whole method matcher)"++-- | A rule for matching a method and responding to it when it matches.+--+-- The method may be matched by providing either an 'Action' to match exactly,+-- or a 'Matcher'.  Exact matching is only available when all method arguments+--+-- A 'Rule' may have zero or more responses, which are attached using+-- 'Test.HMock.Rule.|->' and 'Test.HMock.Rule.|=>'.  If there are no responses+-- for a 'Rule', then there must be a default response for that action, and it+-- is used.  If more than one response is added, the rule will perform the+-- responses in order, repeating the last response if there are additional+-- matches.+--+-- Example:+--+-- @+-- 'Test.HMock.ExpectContext.expect' $+--   GetLine_ 'Test.HMock.anything'+--     'Test.HMock.Rule.|->' "hello"+--     'Test.HMock.Rule.|=>' \(GetLine prompt) -> "The prompt was " ++ prompt+--     'Test.HMock.Rule.|->' "quit"+-- @+data+  Rule+    (cls :: (Type -> Type) -> Constraint)+    (name :: Symbol)+    (m :: Type -> Type)+    (r :: Type)+  where+  (:=>) ::+    WholeMethodMatcher cls name m r ->+    [Action cls name m r -> MockT m r] ->+    Rule cls name m r
+ test/Test/HMock/Internal/State.hs view
@@ -0,0 +1,296 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE KindSignatures #-}++-- | This module contains MockT and SetupMockT state functions.+module Test.HMock.Internal.State where++import Control.Monad (forM_, unless, (<=<))+import Control.Monad.Base (MonadBase)+import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)+import Control.Monad.Cont (MonadCont)+import Control.Monad.Except (MonadError)+import Control.Monad.Extra (maybeM)+import Control.Monad.IO.Class (liftIO)+import Control.Monad.RWS (MonadRWS)+import Control.Monad.Reader (MonadReader (..), ReaderT, mapReaderT, runReaderT)+import Control.Monad.State (MonadState)+import Control.Monad.Trans (MonadTrans, lift)+import Control.Monad.Writer (MonadWriter)+import Data.Proxy (Proxy (Proxy))+import Data.Set (Set)+import qualified Data.Set as Set+import Data.Typeable (TypeRep, Typeable, typeRep)+import GHC.Stack (withFrozenCallStack, HasCallStack)+import GHC.TypeLits (KnownSymbol, symbolVal)+import System.IO (hPutStrLn, stderr)+import Test.HMock.ExpectContext (ExpectContext (..))+import Test.HMock.Internal.ExpectSet (ExpectSet (..), getSteps)+import Test.HMock.Internal.Step (SingleRule, Step (..), unwrapExpected)+import Test.HMock.Internal.Util (Located)+import Test.HMock.Mockable (Mockable (..))+import UnliftIO+  ( MonadIO,+    MonadUnliftIO(withRunInIO),+    STM,+    TVar,+    atomically,+    modifyTVar,+    newTVarIO,+    readTVar,+    readTVarIO,+  )+import Data.Kind (Type, Constraint)++#if !MIN_VERSION_base(4, 13, 0)+import Control.Monad.Fail (MonadFail)+#endif++-- | The severity for a possible problem.+data Severity+  = -- | Fail the test.+    Error+  | -- | Print a message, but continue the test.+    Warning+  | -- | Don't do anything.+    Ignore++-- | Full state of a mock.+data MockState m = MockState+  { mockExpectSet :: TVar (ExpectSet (Step m)),+    mockDefaults :: TVar [Step m],+    mockAllowUnexpected :: TVar [Step m],+    mockSideEffects :: TVar [Step m],+    mockAmbiguitySeverity :: TVar Severity,+    mockUnexpectedSeverity :: TVar Severity,+    mockUninterestingSeverity :: TVar Severity,+    mockUnmetSeverity :: TVar Severity,+    mockClasses :: TVar (Set TypeRep),+    mockInterestingMethods :: TVar (Set (TypeRep, String)),+    mockParent :: Maybe (MockState m)+  }++-- | Initializes a new 'MockState' with the given parent.  If the parent is+-- 'Nothing', then a new root state is made.+initMockState :: MonadIO m => Maybe (MockState m) -> m (MockState m)+initMockState parent =+  MockState+    <$> newTVarIO ExpectNothing+    <*> newTVarIO []+    <*> newTVarIO []+    <*> newTVarIO []+    <*> maybeM+      (newTVarIO Ignore)+      (newTVarIO <=< readTVarIO . mockAmbiguitySeverity)+      (return parent)+    <*> maybeM+      (newTVarIO Error)+      (newTVarIO <=< readTVarIO . mockUnexpectedSeverity)+      (return parent)+    <*> maybeM+      (newTVarIO Error)+      (newTVarIO <=< readTVarIO . mockUninterestingSeverity)+      (return parent)+    <*> maybeM+      (newTVarIO Error)+      (newTVarIO <=< readTVarIO . mockUnmetSeverity)+      (return parent)+    <*> maybe (newTVarIO Set.empty) (return . mockClasses) parent+    <*> maybe (newTVarIO Set.empty) (return . mockInterestingMethods) parent+    <*> pure parent++-- | Gets a list of all states, starting with the innermost.+allStates :: MockState m -> [MockState m]+allStates s+  | Just s' <- mockParent s = s : allStates s'+  | otherwise = [s]++-- | Gets the root state.+rootState :: MockState m -> MockState m+rootState = last . allStates++-- | Monad for setting up a mockable class.  Note that even though the type+-- looks that way, this is *not* a monad transformer.  It's a very restricted+-- environment that can only be used to set up defaults for a class.+newtype MockSetup m a where+  MockSetup :: {unMockSetup :: ReaderT (MockState m) STM a} -> MockSetup m a+  deriving (Functor, Applicative, Monad)++-- | Runs a setup action with the root state, rather than the current one.+runInRootState :: MockSetup m a -> MockSetup m a+runInRootState = MockSetup . local rootState . unMockSetup++-- | Run an STM action in 'MockSetup'+mockSetupSTM :: STM a -> MockSetup m a+mockSetupSTM m = MockSetup (lift m)++-- | Runs class initialization for a 'Mockable' class, if it hasn't been run+-- yet.+initClassIfNeeded ::+  forall cls m proxy.+  (Mockable cls, Typeable m, MonadIO m) =>+  proxy cls ->+  MockSetup m ()+initClassIfNeeded proxy = runInRootState $ do+  state <- MockSetup ask+  classes <- mockSetupSTM $ readTVar (mockClasses state)+  unless (Set.member t classes) $ do+    mockSetupSTM $ modifyTVar (mockClasses state) (Set.insert t)+    setupMockable (Proxy :: Proxy cls)+  where+    t = typeRep proxy++-- | Marks a method as "interesting".  This can have implications for what+-- happens to calls to that method.+markInteresting ::+  forall (cls :: (Type -> Type) -> Constraint) name m proxy1 proxy2.+  (Typeable cls, KnownSymbol name) =>+  proxy1 cls ->+  proxy2 name ->+  MockSetup m ()+markInteresting proxyCls proxyName = runInRootState $ do+  state <- MockSetup ask+  mockSetupSTM $+    modifyTVar+      (mockInterestingMethods state)+      (Set.insert (typeRep proxyCls, symbolVal proxyName))++-- | Determines whether a method is "interesting".+isInteresting :: +  forall (cls :: (Type -> Type) -> Constraint) name m proxy1 proxy2.+  (Typeable cls, KnownSymbol name) =>+  proxy1 cls ->+  proxy2 name ->+  MockSetup m Bool+isInteresting proxyCls proxyName = runInRootState $ do+  state <- MockSetup ask+  interesting <- mockSetupSTM $ readTVar (mockInterestingMethods state)+  return ((typeRep proxyCls, symbolVal proxyName) `Set.member` interesting)++-- | Runs class initialization for all uninitialized 'Mockable' classes in the+-- given 'ExpectSet'.+initClassesAsNeeded :: MonadIO m => ExpectSet (Step m) -> MockSetup m ()+initClassesAsNeeded es = runInRootState $+  forM_ (getSteps es) $+    \(Step (_ :: Located (SingleRule cls name m r))) -> do+      initClassIfNeeded (Proxy :: Proxy cls)+      markInteresting (Proxy :: Proxy cls) (Proxy :: Proxy name)++-- | Monad transformer for running mocks.+newtype MockT m a where+  MockT :: {unMockT :: ReaderT (MockState m) m a} -> MockT m a+  deriving+    ( Functor,+      Applicative,+      Monad,+      MonadFail,+      MonadIO,+      MonadState s,+      MonadWriter w,+      MonadRWS r w s,+      MonadError e,+      MonadCont,+      MonadBase b,+      MonadCatch,+      MonadMask,+      MonadThrow+    )++instance MonadTrans MockT where+  lift = MockT . lift++-- Note: The 'MonadUnliftIO' instance is implemented manually because deriving+-- it causes compilation failure in GHC 8.6 and 8.8.  (See issue #23.)+instance MonadUnliftIO m => MonadUnliftIO (MockT m) where+  withRunInIO inner = MockT $ withRunInIO $ \run -> inner (run . unMockT)++-- | Applies a function to the base monad of 'MockT'.+mapMockT :: (m a -> m b) -> MockT m a -> MockT m b+mapMockT f = MockT . mapReaderT f . unMockT++instance MonadReader r m => MonadReader r (MockT m) where+  ask = lift ask+  local = mapMockT . local+  reader = lift . reader++-- | This type class defines a shared API between the 'MockT' and 'MockSetup'+-- monads.+class MockContext ctx where+  -- | Runs a 'MockSetup' action in this monad.+  fromMockSetup :: MonadIO m => MockSetup m a -> ctx m a++instance MockContext MockSetup where+  fromMockSetup = id++instance MockContext MockT where+  fromMockSetup m = do+    state <- MockT ask+    atomically $ runReaderT (unMockSetup m) state++-- | Adds an expectation to the 'MockState' for the given 'ExpectSet',+-- interleaved with any existing expectations.+expectThisSet :: MonadIO m => ExpectSet (Step m) -> MockSetup m ()+expectThisSet e = do+  initClassesAsNeeded e+  state <- MockSetup ask+  mockSetupSTM $ modifyTVar (mockExpectSet state) (e `ExpectInterleave`)++-- | This instance allows you to add expectations from 'MockSetup' actions.+-- This is an unusual thing to do.  Consider using+-- 'Test.HMock.MockT.allowUnexpected', instead.+instance ExpectContext MockSetup where+  expect e =+    withFrozenCallStack $ expectThisSet $ unwrapExpected $ expect e+  expectN mult e =+    withFrozenCallStack $ expectThisSet $ unwrapExpected $ expectN mult e+  expectAny e =+    withFrozenCallStack $ expectThisSet $ unwrapExpected $ expectAny e+  inSequence es =+    withFrozenCallStack $ expectThisSet $ unwrapExpected $ inSequence es+  inAnyOrder es =+    withFrozenCallStack $ expectThisSet $ unwrapExpected $ inAnyOrder es+  anyOf es =+    withFrozenCallStack $ expectThisSet $ unwrapExpected $ anyOf es+  times mult es =+    withFrozenCallStack $ expectThisSet $ unwrapExpected $ times mult es+  consecutiveTimes mult es =+    withFrozenCallStack $+      expectThisSet $ unwrapExpected $ consecutiveTimes mult es++instance ExpectContext MockT where+  expect e =+    withFrozenCallStack $+      fromMockSetup $ expectThisSet $ unwrapExpected $ expect e+  expectN mult e =+    withFrozenCallStack $+      fromMockSetup $ expectThisSet $ unwrapExpected $ expectN mult e+  expectAny e =+    withFrozenCallStack $+      fromMockSetup $ expectThisSet $ unwrapExpected $ expectAny e+  inSequence es =+    withFrozenCallStack $+      fromMockSetup $ expectThisSet $ unwrapExpected $ inSequence es+  inAnyOrder es =+    withFrozenCallStack $+      fromMockSetup $ expectThisSet $ unwrapExpected $ inAnyOrder es+  anyOf es =+    withFrozenCallStack $+      fromMockSetup $ expectThisSet $ unwrapExpected $ anyOf es+  times mult es =+    withFrozenCallStack $+      fromMockSetup $ expectThisSet $ unwrapExpected $ times mult es+  consecutiveTimes mult es =+    withFrozenCallStack $+      fromMockSetup $ expectThisSet $ unwrapExpected $ consecutiveTimes mult es++-- | Reports a potential problem with the given 'Severity'.+reportFault :: (HasCallStack, MonadIO m) => Severity -> String -> MockT m ()+reportFault severity msg = case severity of+  Ignore -> return ()+  Warning -> liftIO $ hPutStrLn stderr msg+  Error -> error msg
+ test/Test/HMock/Internal/State.hs-boot view
@@ -0,0 +1,18 @@+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE RoleAnnotations #-}++module Test.HMock.Internal.State where++import Data.Kind (Type)++type role MockT nominal nominal++data MockT (m :: Type -> Type) (a :: Type)++instance Monad m => Monad (MockT m)++type role MockSetup nominal nominal++data MockSetup (m :: Type -> Type) (a :: Type)++instance Monad (MockSetup m)
+ test/Test/HMock/Internal/Step.hs view
@@ -0,0 +1,112 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE KindSignatures #-}++-- | This module defines the desugaring from multi-response 'Rule's into+-- multiple steps.+module Test.HMock.Internal.Step where++import Data.Kind (Constraint, Type)+import Data.Maybe (listToMaybe)+import GHC.Stack (CallStack, callStack)+import GHC.TypeLits (Symbol)+import Test.HMock.ExpectContext (ExpectContext (..), MockableMethod)+import Test.HMock.Internal.ExpectSet (ExpectSet (..))+import Test.HMock.Internal.Rule+  ( Rule (..),+    WholeMethodMatcher (..),+    showWholeMatcher,+  )+import {-# SOURCE #-} Test.HMock.Internal.State (MockT)+import Test.HMock.Internal.Util (Located (..), locate, withLoc)+import Test.HMock.Mockable (MockableBase (..))+import Test.HMock.Multiplicity+  ( Multiplicity,+    anyMultiplicity,+    feasible,+    meetsMultiplicity,+  )+import Test.HMock.Rule (Expectable (toRule))++-- | A Rule that contains only a single response.  This is the target for+-- desugaring the multi-response rule format.+data+  SingleRule+    (cls :: (Type -> Type) -> Constraint)+    (name :: Symbol)+    (m :: Type -> Type)+    (r :: Type)+  where+  (:->) ::+    WholeMethodMatcher cls name m r ->+    Maybe (Action cls name m r -> MockT m r) ->+    SingleRule cls name m r++-- | A single step of an expectation.+data Step m where+  Step ::+    MockableMethod cls name m r =>+    Located (SingleRule cls name m r) ->+    Step m++instance Show (Step m) where+  show (Step l@(Loc _ (m :-> _))) =+    withLoc (showWholeMatcher Nothing m <$ l)++-- | Expands a Rule into an expectation.  The expected multiplicity will be one+-- if there are no responses; otherwise one call is expected per response.+expandRule ::+  MockableMethod cls name m r =>+  CallStack ->+  Rule cls name m r ->+  ExpectSet (Step m)+expandRule callstack (m :=> []) =+  ExpectStep (Step (locate callstack (m :-> Nothing)))+expandRule callstack (m :=> rs) =+  foldr1+    ExpectSequence+    (map (ExpectStep . Step . locate callstack . (m :->) . Just) rs)++-- | Expands a Rule into an expectation, given a target multiplicity.  It is an+-- error if there are too many responses for the multiplicity.  If there are+-- too few responses, the last response will be repeated.+expandRepeatRule ::+  MockableMethod cls name m r =>+  Multiplicity ->+  CallStack ->+  Rule cls name m r ->+  ExpectSet (Step m)+expandRepeatRule mult _ (_ :=> rs)+  | not (feasible (mult - fromIntegral (length rs))) =+    error $+      show (length rs)+        ++ " responses is too many for multiplicity "+        ++ show mult+expandRepeatRule mult callstack (m :=> (r1 : r2 : rs))+  | meetsMultiplicity mult 0 = ExpectEither ExpectNothing body+  | otherwise = body+  where+    body =+      ExpectSequence+        (ExpectStep (Step (locate callstack (m :-> Just r1))))+        (expandRepeatRule (mult - 1) callstack (m :=> (r2 : rs)))+expandRepeatRule mult callstack (m :=> rs) =+  ExpectConsecutive+    mult+    (ExpectStep (Step (locate callstack (m :-> listToMaybe rs))))++-- | Newtype wrapper to make the type of ExpectSet conform to the ExpectContext+-- class.  The "return type" a is a phantom.+newtype Expected m a = Expected {unwrapExpected :: ExpectSet (Step m)}++instance ExpectContext Expected where+  expect e = Expected (expandRule callStack (toRule e))+  expectN mult e = Expected (expandRepeatRule mult callStack (toRule e))+  expectAny e =+    Expected (expandRepeatRule anyMultiplicity callStack (toRule e))+  inSequence es = Expected (foldr1 ExpectSequence (map unwrapExpected es))+  inAnyOrder es = Expected (foldr1 ExpectInterleave (map unwrapExpected es))+  anyOf es = Expected (foldr1 ExpectEither (map unwrapExpected es))+  times mult e = Expected (ExpectMulti mult (unwrapExpected e))+  consecutiveTimes mult e =+    Expected (ExpectConsecutive mult (unwrapExpected e))
+ test/Test/HMock/Internal/TH.hs view
@@ -0,0 +1,262 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE TupleSections #-}++-- | Template Haskell utilities used to implement HMock.+module Test.HMock.Internal.TH+  ( unappliedName,+    tvName,+    bindVar,+    substTypeVar,+    substTypeVars,+    splitType,+    freeTypeVars,+    relevantContext,+    constrainVars,+    unifyTypes,+    removeModNames,+    hasPolyType,+    hasNestedPolyType,+    resolveInstance,+    resolveInstanceType,+    simplifyContext,+    localizeMember,+  )+where++import Control.Monad.Extra (mapMaybeM, concatMapM)+import Data.Generics+import Data.List ((\\), nub)+import Data.Maybe (catMaybes, fromMaybe)+import Language.Haskell.TH+import Language.Haskell.TH.Syntax (NameFlavour (..))+import Test.HMock.Internal.Util (choices)++#if MIN_VERSION_template_haskell(2,17,0)++-- | Fetches the 'Name' of a 'TyVarBndr'.+tvName :: TyVarBndr flag -> Name+tvName (PlainTV name _) = name+tvName (KindedTV name _ _) = name++-- | Creates a 'TyVarBndr' for a plain variable without a kind annotation.+bindVar :: Name -> TyVarBndr Specificity+bindVar n = PlainTV n SpecifiedSpec++#else++-- | Fetches the 'Name' of a 'TyVarBndr'.+tvName :: TyVarBndr -> Name+tvName (PlainTV name) = name+tvName (KindedTV name _) = name++-- | Creates a 'TyVarBndr' for a plain variable without a kind annotation.+bindVar :: Name -> TyVarBndr+bindVar = PlainTV++#endif++-- | Gets the unapplied top-level name from a type application.+unappliedName :: Type -> Maybe Name+unappliedName (AppT a _) = unappliedName a+unappliedName (ConT a) = Just a+unappliedName _ = Nothing++-- | Substitutes a 'Type' for all occurrences of the given 'Name'.+substTypeVar :: Name -> Type -> Type -> Type+substTypeVar n t = substTypeVars [(n, t)]++-- | Makes variable substitutions from the given table.+substTypeVars :: [(Name, Type)] -> Type -> Type+substTypeVars classVars = everywhere (mkT subst)+  where+    subst (VarT x) | Just t <- lookup x classVars = t+    subst t = t++-- | Splits a type application into a top-level constructor and a list of its+-- type arguments.+splitTypeApp :: Type -> Maybe (Name, [Type])+splitTypeApp (ConT name) = Just (name, [])+splitTypeApp (AppT a b) = fmap (++ [b]) <$> splitTypeApp a+splitTypeApp _ = Nothing++-- | Splits a function type into a list of bound type vars, context, parameter+-- types, and return value type.+splitType :: Type -> ([Name], Cxt, [Type], Type)+splitType (ForallT tv cx b) =+  let (tvs, cxs, params, retval) = splitType b+   in (map tvName tv ++ tvs, cx ++ cxs, params, retval)+splitType (AppT (AppT ArrowT a) b) =+  let (tvs, cx, params, retval) = splitType b in (tvs, cx, a : params, retval)+splitType r = ([], [], [], r)++-- | Gets all free type variable 'Name's in the given 'Type'.+freeTypeVars :: Type -> [Name]+freeTypeVars = everythingWithContext [] (++) (mkQ ([],) go)+  where+    go (VarT v) bound+      | v `elem` bound = ([], bound)+      | otherwise = ([v], bound)+    go (ForallT vs _ _) bound = ([], map tvName vs ++ bound)+    go _ bound = ([], bound)++-- | Produces a 'CxtQ' that gives all given variable 'Name's all of the given+-- class 'Type's.+constrainVars :: [TypeQ] -> [Name] -> CxtQ+constrainVars cs vs = sequence [appT c (varT v) | c <- cs, v <- vs]++-- | Culls the given binders and constraints to choose only those that apply to+-- free variables in the given type.+relevantContext :: Type -> ([Name], Cxt) -> ([Name], Cxt)+relevantContext ty (tvs, cx) = (filter needsTv tvs, filteredCx)+  where+    filteredCx = filter (any (`elem` freeTypeVars ty) . freeTypeVars) cx+    needsTv v = any ((v `elem`) . freeTypeVars) (ty : filteredCx)++-- | Attempts to unify the given types by constructing a table of substitutions+-- for the variables of the left type that obtain the right one.+unifyTypes :: Type -> Type -> Q (Maybe [(Name, Type)])+unifyTypes = unifyTypesWith []++-- | Unify types, but starting with a table of substitutions.+unifyTypesWith :: [(Name, Type)] -> Type -> Type -> Q (Maybe [(Name, Type)])+unifyTypesWith tbl (VarT v) t2+  | Just t1 <- lookup v tbl = unifyTypesWith tbl t1 t2+  | otherwise = return (Just ((v, t2) : tbl))+unifyTypesWith tbl (ConT a) (ConT b) | a == b = return (Just tbl)+unifyTypesWith tbl a b = do+  mbA <- replaceSyn a+  mbB <- replaceSyn b+  case (mbA, mbB) of+    (Nothing, Nothing) -> unifyWithin tbl a b+    _ -> unifyTypesWith tbl (fromMaybe a mbA) (fromMaybe b mbB)+  where+    replaceSyn :: Type -> Q (Maybe Type)+    replaceSyn (ConT n) = do+      info <- reify n+      case info of+        TyConI (TySynD _ [] t) -> return (Just t)+        _ -> return Nothing+    replaceSyn _ = return Nothing++-- Unifies the types that occur within the arguments, starting with a table of+-- substitutions.+unifyWithin ::+  (Data a, Data b) => [(Name, Type)] -> a -> b -> Q (Maybe [(Name, Type)])+unifyWithin tbl a b+  | toConstr a == toConstr b =+    compose (gzipWithQ (\a' b' tbl' -> unify tbl' a' b') a b) tbl+  | otherwise = return Nothing+  where+    unify ::+      (Data a, Data b) => [(Name, Type)] -> a -> b -> Q (Maybe [(Name, Type)])+    unify tbl' a' b' = do+      case (cast a', cast b') of+        (Just a'', Just b'') -> unifyTypesWith tbl' a'' b''+        _ -> unifyWithin tbl' a' b'++    compose :: Monad m => [t -> m (Maybe t)] -> t -> m (Maybe t)+    compose [] x = return (Just x)+    compose (f : fs) x = do+      y <- f x+      case y of+        Just y' -> compose fs y'+        _ -> return Nothing++-- | Removes all module names from 'Name's in the given value, so that it will+-- pretty-print more cleanly.+removeModNames :: Data a => a -> a+removeModNames = everywhere (mkT unMod)+  where+    unMod NameG {} = NameS+    unMod other = other++-- | Determines if there is a polytype nested anywhere in the given type.+-- Top-level quantification doesn't count.+hasNestedPolyType :: Type -> Bool+hasNestedPolyType (ForallT _ _ t) = hasPolyType t+hasNestedPolyType t = hasPolyType t++-- | Determines if this is a polytype, including top-level quantification.+hasPolyType :: Type -> Bool+hasPolyType = everything (||) (mkQ False isPolyType)+  where+    isPolyType (ForallT tvs _ _) = not (null tvs)+    isPolyType _ = False++-- | Attempts to produce sufficient constraints for the given 'Type' to be an+-- instance of the given class 'Name'.+resolveInstance :: Name -> [Type] -> Q (Maybe Cxt)+resolveInstance cls args = do+  decs <- reifyInstances cls args+  results <- catMaybes <$> traverse (tryInstance args) decs+  case results of+    [cx] -> pure (Just cx)+    _ -> return Nothing+  where+    tryInstance :: [Type] -> InstanceDec -> Q (Maybe Cxt)+    tryInstance actualArgs (InstanceD _ cx instType _) =+      case splitTypeApp instType of+        Just (cls', instArgs)+          | cls' == cls ->+            unifyWithin [] instArgs actualArgs >>= \case+              Just tbl -> simplifyContext (substTypeVars tbl <$> cx)+              Nothing -> return Nothing+        _ -> return Nothing+    tryInstance _ _ = return Nothing++-- | Attempts to produce sufficient constraints for the given 'Type' to be a+-- satisfied constraint.  The type should be a class applied to its type+-- parameters.+--+-- Unlike 'simplifyContext', this function always resolves the top-level+-- constraint, and returns 'Nothing' if it cannot do so.+resolveInstanceType :: Type -> Q (Maybe Cxt)+resolveInstanceType t =+  maybe (pure Nothing) (uncurry resolveInstance) (splitTypeApp t)++-- | Simplifies a context with complex types (requiring FlexibleContexts) to try+-- to obtain one with all constraints applied to variables.+--+-- Should return Nothing if and only if the simplified contraint is+-- unsatisfiable, which is the case if and only if it contains a component with+-- no type variables.+simplifyContext :: Cxt -> Q (Maybe Cxt)+simplifyContext preds+  | all isVarApp preds = return (Just preds)+  | otherwise = do+    let simplifyPred t = fromMaybe [t] <$> resolveInstanceType t+    components <- concatMapM simplifyPred preds+    if any (null . freeTypeVars) components+      then return Nothing+      else return (Just (nub components))+  where+    isVarApp (ConT _) = True+    isVarApp (AppT t (VarT _)) | isVarApp t = True+    isVarApp _ = False++-- | Remove instance context from a method.+--+-- Some GHC versions report class members including the instance context (for+-- example, @show :: Show a => a -> String@, instead of @show :: a -> String@).+-- This looks for the instance context, and substitutes if needed to eliminate+-- it.+localizeMember :: Type -> Name -> Type -> Q Type+localizeMember instTy m t@(ForallT tvs cx ty) = do+  let fullConstraint = AppT instTy (VarT m)+  let unifyLeft (c, cs) = fmap (,cs) <$> unifyTypes c fullConstraint+  results <- mapMaybeM unifyLeft (choices cx)+  case results of+    ((tbl, remainingCx) : _) -> do+      let cx' = substTypeVars tbl <$> remainingCx+          ty' = substTypeVars tbl ty+          (tvs', cx'') =+            relevantContext+              ty'+              ((tvName <$> tvs) \\ (fst <$> tbl), cx')+          t'+            | null tvs' && null cx'' = ty'+            | otherwise = ForallT (bindVar <$> tvs') cx'' ty'+      return t'+    _ -> return t+localizeMember _ _ t = return t
+ test/Test/HMock/Internal/Util.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE FlexibleContexts #-}++-- | Internal utilities used for HMock implementation.+module Test.HMock.Internal.Util where++import GHC.Stack (CallStack, getCallStack, prettySrcLoc)++-- | A value together with its source location.+data Located a = Loc (Maybe String) a deriving (Functor)++-- | Annotates a value with its source location from the call stack.+locate :: CallStack -> a -> Located a+locate stack = case map snd (getCallStack stack) of+  (loc : _) -> Loc (Just (prettySrcLoc loc))+  _ -> Loc Nothing++-- | Formats a 'Located' 'String' to include its source location.+withLoc :: Located String -> String+withLoc (Loc Nothing s) = s+withLoc (Loc (Just loc) s) = s ++ " at " ++ loc++-- | Returns all ways to choose one element from a list, and the corresponding+-- remaining list.+choices :: [a] -> [(a, [a])]+choices [] = []+choices (x : xs) = (x, xs) : (fmap (x :) <$> choices xs)
+ test/Test/HMock/MockMethod.hs view
@@ -0,0 +1,259 @@+{-# LANGUAGE ScopedTypeVariables #-}++-- | Functions to delegate 'Action's to HMock to match expectations.  There is+-- one delegation function that works if the return type has a 'Default'+-- instance, and another that doesn't require the 'Default' instance, but causes+-- the method to return 'undefined' by default.+module Test.HMock.MockMethod+  ( mockMethod,+    mockDefaultlessMethod,+  )+where++import Control.Concurrent.STM (TVar, readTVar, writeTVar)+import Control.Monad (forM, forM_, join, unless, void)+import Control.Monad.Extra (concatMapM)+import Control.Monad.IO.Class (MonadIO)+import Control.Monad.Reader (ask)+import Data.Bifunctor (bimap)+import Data.Default (Default (def))+import Data.Either (partitionEithers)+import Data.Function (on)+import Data.Functor (($>))+import Data.List (intercalate, sortBy)+import Data.Maybe (catMaybes, fromMaybe)+import Data.Proxy (Proxy (Proxy))+import Data.Typeable (cast)+import GHC.Stack (HasCallStack, withFrozenCallStack)+import Test.HMock.ExpectContext (MockableMethod)+import Test.HMock.Internal.ExpectSet (ExpectSet, liveSteps)+import Test.HMock.Internal.Rule (WholeMethodMatcher (..), showWholeMatcher)+import Test.HMock.Internal.State+  ( MockContext (..),+    MockSetup (..),+    MockState (..),+    MockT,+    Severity (..),+    allStates,+    initClassIfNeeded,+    isInteresting,+    mockSetupSTM,+    reportFault,+  )+import Test.HMock.Internal.Step (SingleRule ((:->)), Step (Step))+import Test.HMock.Internal.Util (Located (Loc), withLoc)+import Test.HMock.MockT (describeExpectations)+import Test.HMock.Mockable+  ( MatchResult (..),+    Mockable (..),+    MockableBase (..),+  )++matchWholeAction ::+  MockableBase cls =>+  WholeMethodMatcher cls name m a ->+  Action cls name m a ->+  MatchResult+matchWholeAction (JustMatcher m) a = matchAction m a+matchWholeAction (m `SuchThat` p) a = case matchAction m a of+  NoMatch n -> NoMatch n+  Match+    | p a -> Match+    | otherwise -> NoMatch []++-- | Implements mock delegation for actions.+mockMethodImpl ::+  forall cls name m r.+  (HasCallStack, MonadIO m, MockableMethod cls name m r) =>+  r ->+  Action cls name m r ->+  MockT m r+mockMethodImpl surrogate action = join $+  fromMockSetup $ do+    initClassIfNeeded (Proxy :: Proxy cls)+    states <- allStates <$> MockSetup ask+    (partial, full) <- fmap (bimap concat concat . unzip) $+      forM states $ \state -> do+        expectSet <- mockSetupSTM $ readTVar (mockExpectSet state)+        return $+          partitionEithers+            (tryMatch (mockExpectSet state) <$> liveSteps expectSet)+    let orderedPartial = sortBy (compare `on` (length . fst)) (catMaybes partial)+    defaults <- concatMapM (mockSetupSTM . readTVar . mockDefaults) states+    unexpected <-+      concatMapM+        (mockSetupSTM . readTVar . mockAllowUnexpected)+        states+    sideEffect <-+      getSideEffect+        <$> concatMapM (mockSetupSTM . readTVar . mockSideEffects) states+    ambigSev <- mockSetupSTM $ readTVar . mockAmbiguitySeverity . head $ states+    unintSev <-+      mockSetupSTM $ readTVar . mockUninterestingSeverity . head $ states+    unexpSev <- mockSetupSTM $ readTVar . mockUnexpectedSeverity . head $ states+    case ( full,+           orderedPartial,+           allowedUnexpected unexpected,+           findDefault defaults+         ) of+      (opts@((_, choose, response) : rest), _, _, d) -> do+        choose+        return $ do+          unless (null rest) $+            ambiguityError ambigSev action ((\(s, _, _) -> s) <$> opts)+          sideEffect+          fromMaybe d response+      ([], _, Just response, d) -> return (sideEffect >> fromMaybe d response)+      ([], [], _, d) -> do+        interesting <- isInteresting (Proxy :: Proxy cls) (Proxy :: Proxy name)+        case (interesting, unintSev) of+          (True, _) -> return (noMatchError unexpSev action >> d)+          (False, Error) -> return (noMatchError unexpSev action >> d)+          _ -> return (uninterestingError unintSev action >> d)+      ([], _, _, d) ->+        return (partialMatchError unexpSev action orderedPartial >> d)+  where+    tryMatch ::+      TVar (ExpectSet (Step m)) ->+      (Step m, ExpectSet (Step m)) ->+      Either+        (Maybe ([(Int, String)], String))+        (String, MockSetup m (), Maybe (MockT m r))+    tryMatch tvar (Step expected, e)+      | Just lrule@(Loc _ (m :-> impl)) <- cast expected =+        case matchWholeAction m action of+          NoMatch n ->+            Left (Just (n, withLoc (showWholeMatcher (Just action) m <$ lrule)))+          Match ->+            Right+              ( withLoc (lrule $> showWholeMatcher (Just action) m),+                mockSetupSTM $ writeTVar tvar e,+                ($ action) <$> impl+              )+      | otherwise = Left Nothing++    allowedUnexpected :: [Step m] -> Maybe (Maybe (MockT m r))+    allowedUnexpected [] = Nothing+    allowedUnexpected (Step unexpected : steps)+      | Just (Loc _ (m :-> impl)) <- cast unexpected,+        Match <- matchWholeAction m action =+        Just (($ action) <$> impl)+      | otherwise = allowedUnexpected steps++    findDefault :: [Step m] -> MockT m r+    findDefault [] = return surrogate+    findDefault (Step expected : steps)+      | Just (Loc _ (m :-> impl)) <- cast expected,+        Match <- matchWholeAction m action =+        maybe (findDefault steps) ($ action) impl+      | otherwise = findDefault steps++    getSideEffect :: [Step m] -> MockT m ()+    getSideEffect effects =+      forM_ effects $ \(Step expected) -> case cast expected of+        Just (Loc _ (m :-> Just impl))+          | Match <- matchWholeAction m action -> void (impl action)+        _ -> return ()++-- | Implements a method in a 'Mockable' monad by delegating to the mock+-- framework.  If the method is called unexpectedly, an exception will be+-- thrown.  However, an expected invocation without a specified response will+-- return the default value.+mockMethod ::+  ( HasCallStack,+    MonadIO m,+    MockableMethod cls name m r,+    Default r+  ) =>+  Action cls name m r ->+  MockT m r+mockMethod action =+  withFrozenCallStack $ mockMethodImpl def action++-- | Implements a method in a 'Mockable' monad by delegating to the mock+-- framework.  If the method is called unexpectedly, an exception will be+-- thrown.  However, an expected invocation without a specified response will+-- return undefined.  This can be used in place of 'mockMethod' when the return+-- type has no default.+mockDefaultlessMethod ::+  ( HasCallStack,+    MonadIO m,+    MockableMethod cls name m r+  ) =>+  Action cls name m r ->+  MockT m r+mockDefaultlessMethod action =+  withFrozenCallStack $ mockMethodImpl undefined action++-- | An error for an action that matches no expectations at all.  This is only+-- used if severity is Ignore or Warning.+uninterestingError ::+  (HasCallStack, Mockable cls, MonadIO m) =>+  Severity ->+  Action cls name m r ->+  MockT m ()+uninterestingError severity a =+  reportFault severity $ "Uninteresting action: " ++ showAction a++-- | An error for an action that matches no expectations at all.+noMatchError ::+  (HasCallStack, Mockable cls, MonadIO m) =>+  Severity ->+  Action cls name m r ->+  MockT m ()+noMatchError severity a = do+  fullExpectations <- describeExpectations+  reportFault severity $+    "Unexpected action: " ++ showAction a+      ++ "\n\nFull expectations:\n"+      ++ fullExpectations++-- | An error for an action that doesn't match the argument predicates for any+-- of the method's expectations.+partialMatchError ::+  (HasCallStack, Mockable cls, MonadIO m) =>+  Severity ->+  Action cls name m r ->+  [([(Int, String)], String)] ->+  MockT m ()+partialMatchError severity a partials = do+  fullExpectations <- describeExpectations+  reportFault severity $+    "Wrong arguments: "+      ++ showAction a+      ++ "\n\nClosest matches:\n - "+      ++ intercalate "\n - " (map formatPartial $ take 5 partials)+      ++ "\n\nFull expectations:\n"+      ++ fullExpectations+  where+    formatPartial :: ([(Int, String)], String) -> String+    formatPartial (mismatches, matcher)+      | null mismatches = matcher ++ "\n   * Failed whole-method matcher"+      | otherwise =+        matcher ++ "\n   * "+          ++ intercalate+            "\n   * "+            ( map+                ( \(i, mm) ->+                    "Arg #" ++ show i ++ ": " ++ mm+                )+                mismatches+            )++-- | An error for an 'Action' that matches more than one 'Matcher'.  This only+-- triggers an error if ambiguity checks are on.+ambiguityError ::+  (HasCallStack, Mockable cls, MonadIO m) =>+  Severity ->+  Action cls name m r ->+  [String] ->+  MockT m ()+ambiguityError severity a choices = do+  fullExpectations <- describeExpectations+  reportFault severity $+    "Ambiguous action matched multiple expectations: "+      ++ showAction a+      ++ "\n\nMatches:\n - "+      ++ intercalate "\n - " choices+      ++ "\n\nFull expectations:\n"+      ++ fullExpectations
+ test/Test/HMock/MockT.hs view
@@ -0,0 +1,280 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UndecidableInstances #-}++-- | This module defines monads for working with mocks.  HMock tests run in the+-- 'MockT' monad transformer.  A more limited monad, 'MockSetup', is used for+-- setting up defaults for each class.  Both are instances of the 'MockContext'+-- monad, which defines a shared API.+module Test.HMock.MockT+  ( MockT,+    runMockT,+    withMockT,+    nestMockT,+    withNestedMockT,+    Severity (..),+    setAmbiguityCheck,+    setUninterestingActionCheck,+    setUnexpectedActionCheck,+    setUnmetExpectationCheck,+    describeExpectations,+    verifyExpectations,+    MockSetup,+    MockContext,+    allowUnexpected,+    byDefault,+    whenever,+  )+where++import Control.Monad (join)+import Control.Monad.Reader+  ( MonadReader (..),+    runReaderT,+  )+import Control.Monad.Trans (lift)+import Data.List (intercalate)+import Data.Maybe (listToMaybe)+import Data.Proxy (Proxy (Proxy))+import GHC.Stack (callStack)+import Test.HMock.ExpectContext (MockableMethod)+import Test.HMock.Internal.ExpectSet+import Test.HMock.Internal.Rule (Rule ((:=>)))+import Test.HMock.Internal.State+import Test.HMock.Internal.Step (SingleRule ((:->)), Step (Step))+import Test.HMock.Internal.Util (locate)+import Test.HMock.Rule (Expectable (toRule))+import UnliftIO++-- | Runs a test in the 'MockT' monad, handling all of the mocks.+runMockT :: forall m a. MonadIO m => MockT m a -> m a+runMockT test = withMockT constTest+  where+    constTest :: (forall b. MockT m b -> m b) -> MockT m a+    constTest _inMockT = test++-- | Runs a test in the 'MockT' monad.  The test can unlift other MockT pieces+-- to the base monad while still acting on the same set of expectations.  This+-- can be useful for testing concurrency or similar mechanisms.+--+-- @+-- test = 'withMockT' '$' \inMockT -> do+--    'Test.HMock.Expectable.expect' '$' ...+--+--    'liftIO' '$' 'Control.Concurrent.forkIO' '$' inMockT firstThread+--    'liftIO' '$' 'Control.Concurrent.forkIO' '$' inMockT secondThread+-- @+--+-- This is a low-level primitive.  Consider using the @unliftio@ package for+-- higher level implementations of multithreading and other primitives.+withMockT ::+  forall m b. MonadIO m => ((forall a. MockT m a -> m a) -> MockT m b) -> m b+withMockT test = do+  state <- initMockState Nothing+  let inMockT :: forall a. MockT m a -> m a+      inMockT m = runReaderT (unMockT m) state+  flip runReaderT state $+    unMockT $ do+      a <- test inMockT+      verifyExpectations+      return a++-- | Starts a nested block within 'MockT'.  The nested block has its own set of+-- expectations, which must be fulfilled before the end of the block.+--+-- Beware: use of 'nestMockT' might signify that you are doing too much in a+-- single test.  Consider splitting large tests into a separate test for each+-- case.+nestMockT :: forall m a. MonadIO m => MockT m a -> MockT m a+nestMockT nest = withNestedMockT constNest+  where+    constNest :: (forall b. MockT m b -> m b) -> MockT m a+    constNest _inMockT = nest++-- | Starts a nested block within 'MockT'.  The nested block has its own set of+-- expectations, which must be fulfilled before the end of the block.  It can+-- unlift other MockT pieces to the base monad while still acting on the same+-- set of expectations.  This can be useful for testing concurrency or similar+-- mechanisms.+--+-- Beware: use of 'nestMockT' might signify that you are doing too much in a+-- single test.  Consider splitting large tests into a separate test for each+-- case.+withNestedMockT ::+  forall m b.+  MonadIO m =>+  ((forall a. MockT m a -> m a) -> MockT m b) ->+  MockT m b+withNestedMockT nest = do+  parent <- MockT ask+  state <- lift $ initMockState (Just parent)+  withState state $ do+    a <- nest (flip runReaderT state . unMockT)+    verifyExpectations+    return a+  where+    withState state = MockT . local (const state) . unMockT++-- | Sets the severity for ambiguous actions.  An ambiguous action is one that+-- matches expectations in more than one way.  If this is not set to `Error`,+-- the most recently added expectation will take precedence.+--+-- This defaults to 'Ignore'.+setAmbiguityCheck :: MonadIO m => Severity -> MockT m ()+setAmbiguityCheck severity = fromMockSetup $ do+  state <- MockSetup ask+  mockSetupSTM $ writeTVar (mockAmbiguitySeverity state) severity++-- | Sets the severity for uninteresting actions.  An uninteresting action is+-- one for which no expectations or other configuration have been added that+-- mention the method at all.  If this is not set to `Error`, then uninteresting+-- methods are treated just like unexpected methods.+--+-- Before you weaken this check, consider that the labeling of methods as+-- "uninteresting" is non-compositional.  A change in one part of your test can+-- result in a formerly uninteresting action being considered interesting in a+-- different part of the test.+--+-- This defaults to 'Error'.+setUninterestingActionCheck :: MonadIO m => Severity -> MockT m ()+setUninterestingActionCheck severity = fromMockSetup $ do+  state <- MockSetup ask+  mockSetupSTM $ writeTVar (mockUninterestingSeverity state) severity++-- | Sets the severity for unexpected actions.  An unexpected action is one that+-- doesn't match any expectations *and* isn't explicitly allowed by+-- `allowUnexpected`.  If this is not set to `Error`, the action returns its+-- default response.+--+-- This defaults to 'Error'.+setUnexpectedActionCheck :: MonadIO m => Severity -> MockT m ()+setUnexpectedActionCheck severity = fromMockSetup $ do+  state <- MockSetup ask+  mockSetupSTM $ writeTVar (mockUnexpectedSeverity state) severity++-- | Sets the severity for unmet expectations.  An unmet expectation happens+-- when an expectation is added, but either the test (or nesting level) ends or+-- 'verifyExpectations' is used before a matching action takes place.+--+-- This defaults to 'Error'.+setUnmetExpectationCheck :: MonadIO m => Severity -> MockT m ()+setUnmetExpectationCheck severity = fromMockSetup $ do+  state <- MockSetup ask+  mockSetupSTM $ writeTVar (mockUnmetSeverity state) severity++-- | Fetches a 'String' that describes the current set of outstanding+-- expectations.  This is sometimes useful for debugging test code.  The exact+-- format is not specified.+describeExpectations :: MonadIO m => MockT m String+describeExpectations = fromMockSetup $ do+  states <- allStates <$> MockSetup ask+  expectSets <- mapM (mockSetupSTM . readTVar . mockExpectSet) states+  return $+    intercalate "\n----- (next layer) -----\n" $+      formatExpectSet <$> expectSets++-- | Verifies that all mock expectations are satisfied.  If there is a nested+-- block in effect, only the expectations of that nested block are verified+-- You normally don't need to do this, because it happens automatically at the+-- end of your test or nested block.  However, it's occasionally useful to check+-- expectations early.+--+-- Beware: use of 'verifyExpectations' might signify that you are doing too much+-- in a single test.  Consider splitting large tests into a separate test for+-- each case.+verifyExpectations :: MonadIO m => MockT m ()+verifyExpectations = join $ do+  fromMockSetup $ do+    states <- MockSetup ask+    expectSet <- mockSetupSTM $ readTVar $ mockExpectSet states+    missingSev <- mockSetupSTM $ readTVar $ mockUnmetSeverity states+    case excess expectSet of+      ExpectNothing -> return (return ())+      missing ->+        return $+          reportFault missingSev $+            "Unmet expectations:\n" ++ formatExpectSet missing++-- | Adds a handler for unexpected actions.  Matching calls will not fail, but+-- will use a default response instead.  The rule passed in must have zero or+-- one responses: if there is a response, @'allowUnexpected' (m+-- 'Test.HMock.Rule.|=>' r)@ is equivalent to @'allowUnexpected' m >>+-- 'byDefault' (m 'Test.HMock.Rule.|=>' r)@.+--+-- The difference between 'Test.HMock.Expectable.expectAny' and+-- 'allowUnexpected' is subtle, but comes down to ambiguity:+--+-- * 'allowUnexpected' is not an expectation, so it cannot be ambiguous.  It+--   only has an effect if no true expectation matches, regardless of when the+--   expectations were added.+-- * 'Test.HMock.Expectable.expectAny' adds an expectation, so if another+--   expectation is in effect at the same time, a call to the method is+--   ambiguous.  If ambiguity checking is enabled, the method will throw an+--   error; otherwise, the more recently added of the two expectations is used.+allowUnexpected ::+  forall cls name m r rule ctx.+  ( MonadIO m,+    MockableMethod cls name m r,+    Expectable cls name m r rule,+    MockContext ctx+  ) =>+  rule ->+  ctx m ()+allowUnexpected e = fromMockSetup $ case toRule e of+  _ :=> (_ : _ : _) -> error "allowUnexpected may not have multiple responses."+  m :=> r -> do+    initClassIfNeeded (Proxy :: Proxy cls)+    state <- MockSetup ask+    mockSetupSTM $+      modifyTVar'+        (mockAllowUnexpected state)+        (Step (locate callStack (m :-> listToMaybe r)) :)++-- | Sets a default action for *expected* matching calls.  The new default only+-- applies to calls for which an expectation exists, but it lacks an explicit+-- response.  The rule passed in must have exactly one response.+byDefault ::+  forall cls name m r ctx.+  ( MonadIO m,+    MockableMethod cls name m r,+    MockContext ctx+  ) =>+  Rule cls name m r ->+  ctx m ()+byDefault (m :=> [r]) = fromMockSetup $ do+  initClassIfNeeded (Proxy :: Proxy cls)+  state <- MockSetup ask+  mockSetupSTM $+    modifyTVar'+      (mockDefaults state)+      (Step (locate callStack (m :-> Just r)) :)+byDefault _ = error "Defaults must have exactly one response."++-- | Adds a side-effect, which happens whenever a matching call occurs, in+-- addition to the usual response.  The return value is entirely ignored.+--+-- Be warned: using side effects makes it easy to break abstraction boundaries.+-- Be aware that there may be other uses of a method besides the one which you+-- intend to intercept here.  If possible, add the desired behavior to the+-- response for the matching expectation instead.+whenever ::+  forall cls name m r ctx.+  ( MonadIO m,+    MockableMethod cls name m r,+    MockContext ctx+  ) =>+  Rule cls name m r ->+  ctx m ()+whenever (m :=> [r]) = fromMockSetup $ do+  initClassIfNeeded (Proxy :: Proxy cls)+  state <- MockSetup ask+  mockSetupSTM $+    modifyTVar'+      (mockSideEffects state)+      (Step (locate callStack (m :-> Just r)) :)+whenever _ = error "Side effects must have exactly one response."
+ test/Test/HMock/Mockable.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}++-- | This module defines the 'MockableBase' and 'Mockable' classes that are+-- needed to use an MTL-style type class with 'Test.HMock.MockT.MockT'.  You+-- will typically derive 'MockableBase' with Template Haskell, since it's mostly+-- boilerplate.  The 'Mockable' class adds a customizable setup method which you+-- can define yourself to add the right defaults for methods in the mocked+-- class.+module Test.HMock.Mockable+  ( Mockable (..),+    MockableBase (..),+    MatchResult (..),+  )+where++import Control.Monad.Trans (MonadIO)+import Data.Kind (Constraint, Type)+import Data.Typeable (Typeable)+import GHC.TypeLits (Symbol)+import {-# SOURCE #-} Test.HMock.Internal.State (MockSetup)++-- | The result of matching a @'Matcher' a@ with an @'Action' b@.  Because the+-- types should already guarantee that the methods match, all that's left is to+-- match arguments.+data MatchResult where+  -- | No match.  The arg is explanations of mismatch.+  NoMatch :: [(Int, String)] -> MatchResult+  -- | Match. Stores a witness to the equality of return types.+  Match :: MatchResult++-- | A base class for 'Monad' subclasses whose methods can be mocked.  You+-- usually want to generate this instance using 'Test.HMock.TH.makeMockable',+-- 'Test.HMock.TH.makeMockable', or 'Test.HMock.TH.makeMockableWithOptions',+-- since it's just boilerplate.+class (Typeable cls) => MockableBase (cls :: (Type -> Type) -> Constraint) where+  -- | An action that is performed.  This data type will have one constructor+  -- for each method.+  data Action cls :: Symbol -> (Type -> Type) -> Type -> Type++  -- | A specification for matching actions.  The actual arguments should be+  -- replaced with predicates.+  data Matcher cls :: Symbol -> (Type -> Type) -> Type -> Type++  -- | Gets a text description of an 'Action', for use in error messages.+  showAction :: Action cls name m a -> String++  -- | Gets a text description of a 'Matcher', for use in error messages.+  showMatcher :: Maybe (Action cls name m a) -> Matcher cls name m b -> String++  -- | Attempts to match an 'Action' with a 'Matcher'.+  matchAction :: Matcher cls name m a -> Action cls name m a -> MatchResult++-- | A class for 'Monad' subclasses whose methods can be mocked.  This class+-- augments 'MockableBase' with a setup method that is run before HMock touches+-- the 'Monad' subclass for the first time.  The default implementation does+-- nothing, but you can derive your own instances that add setup behavior.+class MockableBase cls => Mockable (cls :: (Type -> Type) -> Constraint) where+  -- | An action to run and set up defaults for this class.  The action will be+  -- run before HMock touches the class, either to add expectations or to+  -- delegate a method.+  --+  -- By default, unexpected actions throw errors, and actions with no explicit+  -- default always return the default value of their return type, or+  -- 'undefined' if there is none.  You can change this on a per-class or+  -- per-test basis.+  --+  -- * To change defaults on a per-class basis, you should use+  --   'Test.HMock.MockT.allowUnexpected' and/or 'Test.HMock.MockT.byDefault'+  --   to perform the setup you need here.+  -- * To change defaults on a per-test basis, you should use+  --   'Test.HMock.MockT.allowUnexpected' and/or 'Test.HMock.MockT.byDefault'+  --   directly from the test.+  setupMockable :: (MonadIO m, Typeable m) => proxy cls -> MockSetup m ()+  setupMockable _ = return ()
+ test/Test/HMock/Multiplicity.hs view
@@ -0,0 +1,155 @@+{-# LANGUAGE DeriveDataTypeable #-}++-- | This module provides the basic vocabulary for talking about multiplicity,+-- which is the number of times something is allowed to happen.  Multiplicities+-- can be any range of natural numbers, with or without an upper bound.+module Test.HMock.Multiplicity+  ( Multiplicity,+    meetsMultiplicity,+    feasible,+    once,+    anyMultiplicity,+    atLeast,+    atMost,+    between,+  )+where++-- | An acceptable range of number of times for something to happen.+--+-- A multiplicity can have a lower and an upper bound.+data Multiplicity = Multiplicity Int (Maybe Int) deriving (Eq)++instance Show Multiplicity where+  show mult = go (normalize mult)+    where+      go m | not (feasible m) = "infeasible"+      go (Multiplicity 0 (Just 0)) = "never"+      go (Multiplicity 1 (Just 1)) = "once"+      go (Multiplicity 2 (Just 2)) = "twice"+      go (Multiplicity 0 Nothing) = "any number of times"+      go (Multiplicity 1 Nothing) = "at least once"+      go (Multiplicity 2 Nothing) = "at least twice"+      go (Multiplicity n Nothing) = "at least " ++ show n ++ " times"+      go (Multiplicity 0 (Just 1)) = "at most once"+      go (Multiplicity 0 (Just 2)) = "at most twice"+      go (Multiplicity 0 (Just n)) = "at most " ++ show n ++ " times"+      go (Multiplicity m (Just n))+        | m == n = show n ++ " times"+        | m == n - 1 = show m ++ " or " ++ show n ++ " times"+        | otherwise = show m ++ " to " ++ show n ++ " times"++-- | A 'Multiplicity' value representing inconsistent expectations.+infeasible :: Multiplicity+infeasible = Multiplicity 0 (Just (-1))++-- | This is an incomplete instance, provided for convenience.+--+-- >>> meetsMultiplicity 5 4+-- False+-- >>> meetsMultiplicity 5 5+-- True+-- >>> between 4 6 - between 1 2+-- 2 to 5 times+instance Num Multiplicity where+  fromInteger n+    | n < 0 = infeasible+    | otherwise =+      normalize $+        Multiplicity (fromInteger n) (Just (fromInteger n))++  m1@(Multiplicity a b) + m2@(Multiplicity c d)+    | feasible m1 && feasible m2 =+      normalize $ Multiplicity (a + c) ((+) <$> b <*> d)+    | otherwise = infeasible++  m1@(Multiplicity a b) - m2@(Multiplicity c d)+    | feasible m1 && feasible m2 =+      normalize $ Multiplicity (maybe 0 (a -) d) (subtract c <$> b)+    | otherwise = infeasible++  (*) = error "Multiplicities are not closed under multiplication"++  abs = id+  signum x = if x == 0 then 0 else 1++normalize :: Multiplicity -> Multiplicity+normalize m@(Multiplicity a b)+  | not (feasible m) = infeasible+  | otherwise = Multiplicity (max a 0) b++-- | Checks whether a certain number satisfies the 'Multiplicity'.+meetsMultiplicity :: Multiplicity -> Int -> Bool+meetsMultiplicity (Multiplicity lo mbhi) n+  | n < lo = False+  | Just hi <- mbhi, n > hi = False+  | otherwise = True++-- | A 'Multiplicity' that means exactly once.+--+-- >>> meetsMultiplicity once 0+-- False+-- >>> meetsMultiplicity once 1+-- True+-- >>> meetsMultiplicity once 2+-- False+once :: Multiplicity+once = 1++-- | A 'Multiplicity' that means any number of times.+-- >>> meetsMultiplicity anyMultiplicity 0+-- True+-- >>> meetsMultiplicity anyMultiplicity 1+-- True+-- >>> meetsMultiplicity anyMultiplicity 10+-- True+anyMultiplicity :: Multiplicity+anyMultiplicity = atLeast 0++-- | A 'Multiplicity' that means at least this many times.+--+-- >>> meetsMultiplicity (atLeast 2) 1+-- False+-- >>> meetsMultiplicity (atLeast 2) 2+-- True+-- >>> meetsMultiplicity (atLeast 2) 3+-- True+atLeast :: Multiplicity -> Multiplicity+atLeast (Multiplicity n _) = normalize $ Multiplicity n Nothing++-- | A 'Multiplicity' that means at most this many times.+--+-- >>> meetsMultiplicity (atMost 2) 1+-- True+-- >>> meetsMultiplicity (atMost 2) 2+-- True+-- >>> meetsMultiplicity (atMost 2) 3+-- False+atMost :: Multiplicity -> Multiplicity+atMost (Multiplicity _ n) = normalize $ Multiplicity 0 n++-- | A 'Multiplicity' that means any number in this interval, endpoints+-- included.  For example, @'between' 2 3@ means 2 or 3 times, while+-- @'between' n n@ is equivalent to @n@.+--+-- >>> meetsMultiplicity (between 2 3) 1+-- False+-- >>> meetsMultiplicity (between 2 3) 2+-- True+-- >>> meetsMultiplicity (between 2 3) 3+-- True+-- >>> meetsMultiplicity (between 2 3) 4+-- False+between :: Multiplicity -> Multiplicity -> Multiplicity+between (Multiplicity m _) (Multiplicity _ n) = normalize $ Multiplicity m n++-- | Checks whether a 'Multiplicity' is capable of matching any number at all.+--+-- >>> feasible once+-- True+-- >>> feasible 0+-- True+-- >>> feasible (once - 2)+-- False+feasible :: Multiplicity -> Bool+feasible (Multiplicity a b) = maybe True (>= max 0 a) b
+ test/Test/HMock/Rule.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-}++-- | This module defines the 'Rule' type, which describes a matcher for methods+-- and a (possibly empty) list of responses to use for successive calls to+-- matching methods.  The 'Expectable' type class generalizes 'Rule', so that+-- you can specify a bare 'Matcher' or 'Action' in most situations where a+-- 'Rule' is needed but you don't want to provide a response.+module Test.HMock.Rule+  ( Rule,+    Expectable (..),+    (|->),+    (|=>),+    WholeMethodMatcher (SuchThat),+  )+where++import Test.HMock.Internal.Rule (Rule (..), WholeMethodMatcher (..))+import {-# SOURCE #-} Test.HMock.Internal.State (MockT)+import Test.HMock.Mockable (MockableBase (Action, Matcher))++-- | Class for things that can be expected.  This is includes 'Rule's, but also+-- bare 'Matcher's and 'Action's with no explicit response.+class Expectable cls name m r ex | ex -> cls name m r where+  -- | Converts an expectable to a Rule that means the same thing.+  toRule :: ex -> Rule cls name m r++-- | Attaches a response to an expectation.  This is a flexible response,+-- which can look at arguments, do things in the base monad, set up more+-- expectations, etc.  A matching 'Action' is passed to the response.+(|=>) ::+  Expectable cls name m r ex =>+  ex ->+  (Action cls name m r -> MockT m r) ->+  Rule cls name m r+e |=> r = m :=> (rs ++ [r]) where m :=> rs = toRule e++infixl 1 |=>++-- | Attaches a return value to an expectation.  This is more convenient than+-- '|=>' in the common case where you just want to return a known result.+-- @e '|->' r@ means the same thing as @e '|=>' 'const' ('return' r)@.+(|->) ::+  (Monad m, Expectable cls name m r ex) =>+  ex ->+  r ->+  Rule cls name m r+m |-> r = m |=> const (return r)++infixl 1 |->++instance Expectable cls name m r (Rule cls name m r) where+  toRule = id++instance Expectable cls name m r (Matcher cls name m r) where+  toRule m = JustMatcher m :=> []++instance Expectable cls name m r (WholeMethodMatcher cls name m r) where+  toRule m = m :=> []
+ test/Test/HMock/TH.hs view
@@ -0,0 +1,707 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeOperators #-}++-- | This module provides Template Haskell splices that can be used to derive+-- boilerplate instances for HMock.  'makeMockable' implements the common case+-- where you just want to generate everything you need to mock with a class.+-- The variant 'makeMockableWithOptions' is similar, but takes an options+-- parameter that can be used to customize the generation.+module Test.HMock.TH+  ( MakeMockableOptions (..),+    makeMockable,+    makeMockableWithOptions,+  )+where++import Control.Monad (replicateM, unless, when, zipWithM)+import Control.Monad.Extra (concatMapM)+import Control.Monad.Trans (MonadIO)+import Data.Bool (bool)+import Data.Char (toUpper)+import Data.Default (Default (..))+import Data.Either (partitionEithers)+import qualified Data.Kind+import Data.List (foldl', (\\))+import Data.Maybe (catMaybes, isNothing)+import Data.Proxy (Proxy)+import Data.Typeable (Typeable, typeRep)+import GHC.Stack (HasCallStack)+import GHC.TypeLits (ErrorMessage (Text, (:$$:), (:<>:)), Symbol, TypeError)+import Language.Haskell.TH hiding (Match, match)+import Language.Haskell.TH.Syntax (Lift (lift))+import Test.HMock.Internal.State (MockT)+import Test.HMock.Internal.TH+import Test.HMock.MockMethod (mockDefaultlessMethod, mockMethod)+import Test.HMock.Mockable (MatchResult (..), Mockable, MockableBase (..))+import Test.HMock.Rule (Expectable (..))+import Test.Predicates (Predicate (..), eq)++-- | Custom options for deriving 'MockableBase' and related instances.+data MakeMockableOptions = MakeMockableOptions+  { -- | Whether to generate a 'Mockable' instance with an empty setup.+    -- Defaults to 'True'.+    --+    -- If this is 'False', you are responsible for providing a 'Mockable'+    -- instance as follows:+    --+    -- @+    -- instance 'Mockable' MyClass where+    --   'Test.HMock.Mockable.setupMockable' _ = ...+    -- @+    mockEmptySetup :: Bool,+    -- | Whether to derive instances of the class for 'MockT' or not.  Defaults+    -- to 'True'.+    --+    -- This option will cause a build error if some members of the class are+    -- unmockable or are not methods.  In this case, you'll need to define this+    -- instance yourself, delegating the mockable methods as follows:+    --+    -- @+    -- instance MyClass ('MockT' m) where+    --   myMethod x y = 'mockMethod' (MyMethod x y)+    --   ...+    -- @+    mockDeriveForMockT :: Bool,+    -- | Suffix to add to 'Action' and 'Matcher' names.  Defaults to @""@.+    mockSuffix :: String,+    -- | Whether to warn about limitations of the generated mocks.  This is+    -- mostly useful temporarily for finding out why generated code doesn't+    -- match your expectations.  Defaults to @'False'@.+    mockVerbose :: Bool+  }++instance Default MakeMockableOptions where+  def =+    MakeMockableOptions+      { mockEmptySetup = True,+        mockDeriveForMockT = True,+        mockSuffix = "",+        mockVerbose = False+      }++-- | Defines all instances necessary to use HMock with the given type, using+-- default options.  The type should be a type class extending 'Monad', applied+-- to zero or more type arguments.+--+-- This defines all of the following instances, if necessary:+--+-- * 'MockableBase' and the associated 'Action' and 'Matcher' types.+-- * 'Expectable' instances for the 'Action' type.+-- * 'Mockable' with an empty setup.+-- * Instances of the provided application type class to allow unit tests to be+--   run with the 'MockT' monad transformer.+makeMockable :: Q Type -> Q [Dec]+makeMockable qtype = makeMockableWithOptions qtype def++-- | Defines all instances necessary to use HMock with the given type, using+-- the provided options.  The type should be a type class extending 'Monad',+-- applied to zero or more type arguments.+--+-- This defines the following instances, if necessary:+--+-- * 'MockableBase' and the associated 'Action' and 'Matcher' types.+-- * 'Expectable' instances for the 'Action' type.+-- * If 'mockEmptySetup' is 'True': 'Mockable' with an empty setup.+-- * If 'mockDeriveForMockT' is 'True': Instances of the provided application+--   type class to allow unit tests to be run with the 'MockT' monad+--   transformer.+makeMockableWithOptions :: Q Type -> MakeMockableOptions -> Q [Dec]+makeMockableWithOptions qtype options = makeMockableImpl options qtype++data Instance = Instance+  { instType :: Type,+    instRequiredContext :: Cxt,+    instGeneralParams :: [Name],+    instMonadVar :: Name,+    instMethods :: [Method],+    instExtraMembers :: [Dec]+  }+  deriving (Show)++data Method = Method+  { methodName :: Name,+    methodTyVars :: [Name],+    methodCxt :: Cxt,+    methodArgs :: [Type],+    methodResult :: Type+  }+  deriving (Show)++withClass :: Type -> (Dec -> Q a) -> Q a+withClass t f = do+  case unappliedName t of+    Just cls -> do+      info <- reify cls+      case info of+        ClassI dec@ClassD {} _ -> f dec+        _ -> fail $ "Expected " ++ show cls ++ " to be a class, but it wasn't."+    _ -> fail "Expected a class, but got something else."++getInstance :: MakeMockableOptions -> Type -> Q Instance+getInstance options ty = withClass ty go+  where+    go (ClassD _ className [] _ _) =+      fail $ "Class " ++ nameBase className ++ " has no type parameters."+    go (ClassD cx _ params _ members) =+      matchVars ty [] (tvName <$> params)+      where+        matchVars :: Type -> [Type] -> [Name] -> Q Instance+        matchVars _ _ [] = internalError+        matchVars (AppT _ _) _ [_] =+          fail $ pprint ty ++ " is applied to too many arguments."+        matchVars (AppT a b) ts (_ : ps) =+          checkExt FlexibleInstances >> matchVars a (b : ts) ps+        matchVars _ ts ps = do+          let genVars = init ps+          let mVar = last ps+          let t = foldl' (\t' v -> AppT t' (VarT v)) ty genVars+          let tbl = zip (tvName <$> params) ts+          let cx' = substTypeVars tbl <$> cx+          makeInstance options t cx' tbl genVars mVar members+    go _ = internalError++makeInstance ::+  MakeMockableOptions ->+  Type ->+  Cxt ->+  [(Name, Type)] ->+  [Name] ->+  Name ->+  [Dec] ->+  Q Instance+makeInstance options ty cx tbl ps m members = do+  processedMembers <- mapM (getMethod ty m tbl) $ filter isRelevantMember members+  (extraMembers, methods) <-+    partitionEithers <$> zipWithM memberOrMethod members processedMembers+  return $+    Instance+      { instType = ty,+        instRequiredContext = cx,+        instGeneralParams = ps,+        instMonadVar = m,+        instMethods = methods,+        instExtraMembers = extraMembers+      }+  where+    isRelevantMember :: Dec -> Bool+    isRelevantMember DefaultSigD {} = False+    isRelevantMember _ = True++    memberOrMethod :: Dec -> Either [String] Method -> Q (Either Dec Method)+    memberOrMethod dec (Left warnings) = do+      when (mockVerbose options) $ mapM_ reportWarning warnings+      return (Left dec)+    memberOrMethod _ (Right method) = return (Right method)++getMethod :: Type -> Name -> [(Name, Type)] -> Dec -> Q (Either [String] Method)+getMethod instTy m tbl (SigD name ty) = do+  simpleTy <- localizeMember instTy m (substTypeVars tbl ty)+  let (tvs, cx, args, mretval) = splitType simpleTy+  return $ do+    retval <- case mretval of+      AppT (VarT m') retval | m' == m -> return retval+      _ ->+        Left+          [ nameBase name+              ++ " can't be mocked: return value not in the expected monad."+          ]+    unless+      ( all+          (isVarTypeable cx)+          (filter (`elem` tvs) (freeTypeVars retval))+      )+      $ Left+        [ nameBase name+            ++ " can't be mocked: return value not Typeable."+        ]+    let argTypes = map (substTypeVar m (AppT (ConT ''MockT) (VarT m))) args+    when (any hasNestedPolyType argTypes) $+      Left+        [ nameBase name+            ++ " can't be mocked: rank-n types nested in arguments."+        ]++    return $+      Method+        { methodName = name,+          methodTyVars = tvs,+          methodCxt = cx,+          methodArgs = argTypes,+          methodResult = retval+        }+  where+    isVarTypeable :: Cxt -> Name -> Bool+    isVarTypeable cx v = AppT (ConT ''Typeable) (VarT v) `elem` cx+getMethod _ _ _ (DataD _ name _ _ _ _) =+  return $+    Left [nameBase name ++ " must be defined manually in MockT instance."]+getMethod _ _ _ (NewtypeD _ name _ _ _ _) =+  return $+    Left [nameBase name ++ " must be defined manually in MockT instance."]+getMethod _ _ _ (TySynD name _ _) =+  return $+    Left [nameBase name ++ " must be defined manually in MockT instance."]+getMethod _ _ _ (DataFamilyD name _ _) =+  return $+    Left [nameBase name ++ " must be defined manually in MockT instance."]+getMethod _ _ _ (OpenTypeFamilyD (TypeFamilyHead name _ _ _)) =+  return $+    Left [nameBase name ++ " must be defined manually in MockT instance."]+getMethod _ _ _ (ClosedTypeFamilyD (TypeFamilyHead name _ _ _) _) =+  return $+    Left [nameBase name ++ " must be defined manually in MockT instance."]+getMethod _ _ _ _ = return (Left [])++isKnownType :: Method -> Type -> Bool+isKnownType method ty = null tyVars && null cx+  where+    (tyVars, cx) =+      relevantContext ty (methodTyVars method, methodCxt method)++withMethodParams :: Instance -> Method -> TypeQ -> TypeQ+withMethodParams inst method t =+  [t|+    $t+      $(pure (instType inst))+      $(litT (strTyLit (nameBase (methodName method))))+      $(varT (instMonadVar inst))+      $(pure (methodResult method))+    |]++makeMockableImpl :: MakeMockableOptions -> Q Type -> Q [Dec]+makeMockableImpl options qtype = do+  checkExt DataKinds+  checkExt FlexibleInstances+  checkExt GADTs+  checkExt MultiParamTypeClasses+  checkExt ScopedTypeVariables+  checkExt TypeFamilies++  ty <- qtype+  let generalizedTy = case unappliedName ty of+        Just cls -> ConT cls+        _ -> ty+  inst <- getInstance options generalizedTy++  when (null (instMethods inst)) $ do+    fail $+      "Cannot derive Mockable because " ++ pprint (instType inst)+        ++ " has no mockable methods."++  typeableCxt <- constrainVars [conT ''Typeable] (instGeneralParams inst)++  needsMockableBase <-+    isNothing <$> resolveInstance ''MockableBase [instType inst]+  mockableBase <-+    if needsMockableBase+      then do+        mockableBase <-+          instanceD+            (pure typeableCxt)+            [t|MockableBase $(pure (instType inst))|]+            [ defineActionType options inst,+              defineMatcherType options inst,+              defineShowAction options (instMethods inst),+              defineShowMatcher options (instMethods inst),+              defineMatchAction options (instMethods inst)+            ]+        expectables <- defineExpectableActions options inst+        return (mockableBase : expectables)+      else return []++  needsMockable <-+    if mockEmptySetup options+      then isNothing <$> resolveInstance ''Mockable [instType inst]+      else return False+  mockable <-+    if needsMockable+      then do+        t <- [t|Mockable $(pure (instType inst))|]+        return [InstanceD (Just Overlappable) typeableCxt t []]+      else return []++  mockt <- deriveForMockT options ty++  return $ mockableBase ++ mockable ++ mockt++defineActionType :: MakeMockableOptions -> Instance -> DecQ+defineActionType options inst = do+  kind <-+    [t|+      Symbol ->+      (Data.Kind.Type -> Data.Kind.Type) ->+      Data.Kind.Type ->+      Data.Kind.Type+      |]+  let cons = actionConstructor options inst <$> instMethods inst+  dataInstD+    (pure [])+    ''Action+    [pure (instType inst)]+    (Just kind)+    cons+    []++actionConstructor :: MakeMockableOptions -> Instance -> Method -> ConQ+actionConstructor options inst method = do+  forallC [] (return (methodCxt method)) $+    gadtC+      [getActionName options method]+      [ return (Bang NoSourceUnpackedness NoSourceStrictness, argTy)+        | argTy <- methodArgs method+      ]+      (withMethodParams inst method [t|Action|])++getActionName :: MakeMockableOptions -> Method -> Name+getActionName options method =+  mkName (map toUpper (take 1 name) ++ drop 1 name ++ mockSuffix options)+  where+    name = nameBase (methodName method)++defineMatcherType :: MakeMockableOptions -> Instance -> Q Dec+defineMatcherType options inst = do+  kind <-+    [t|+      Symbol ->+      (Data.Kind.Type -> Data.Kind.Type) ->+      Data.Kind.Type ->+      Data.Kind.Type+      |]+  let cons = matcherConstructor options inst <$> instMethods inst+  dataInstD+    (pure [])+    ''Matcher+    [pure (instType inst)]+    (Just kind)+    cons+    []++matcherConstructor :: MakeMockableOptions -> Instance -> Method -> ConQ+matcherConstructor options inst method = do+  gadtC+    [getMatcherName options method]+    [ (Bang NoSourceUnpackedness NoSourceStrictness,) <$> mkPredicate argTy+      | argTy <- methodArgs method+    ]+    (withMethodParams inst method [t|Matcher|])+  where+    mkPredicate argTy+      | hasPolyType argTy = do+        checkExt RankNTypes+        v <- newName "t"+        forallT [bindVar v] (pure []) [t|Predicate $(varT v)|]+      | null tyVars && null cx = [t|Predicate $(pure argTy)|]+      | otherwise = do+        checkExt RankNTypes+        forallT (bindVar <$> tyVars) (pure cx) [t|Predicate $(pure argTy)|]+      where+        (tyVars, cx) =+          relevantContext argTy (methodTyVars method, methodCxt method)++getMatcherName :: MakeMockableOptions -> Method -> Name+getMatcherName options method =+  mkName (map toUpper (take 1 name) ++ drop 1 name ++ mockSuffix options ++ "_")+  where+    name = nameBase (methodName method)++defineShowAction :: MakeMockableOptions -> [Method] -> Q Dec+defineShowAction options methods =+  funD 'showAction (showActionClause options <$> methods)++showActionClause :: MakeMockableOptions -> Method -> Q Clause+showActionClause options method = do+  argVars <- replicateM (length (methodArgs method)) (newName "a")+  clause+    [ conP+        (getActionName options method)+        (zipWith argPattern (methodArgs method) argVars)+    ]+    ( normalB+        [|+          unwords+            ( $(lift (nameBase (methodName method))) :+              $(listE (zipWith showArg (methodArgs method) argVars))+            )+          |]+    )+    []+  where+    isLocalPoly ty =+      not . null . fst $+        relevantContext ty (methodTyVars method, methodCxt method)++    canShow ty+      | hasPolyType ty = return False+      | isLocalPoly ty = (`elem` methodCxt method) <$> [t|Show $(pure ty)|]+      | null (freeTypeVars ty) = isInstance ''Show [ty]+      | otherwise = return False++    canType ty+      | hasPolyType ty = return False+      | isLocalPoly ty =+        (`elem` methodCxt method)+          <$> [t|Typeable $(pure ty)|]+      | otherwise = return (null (freeTypeVars ty))++    argPattern ty v = canShow ty >>= flip sigP (pure ty) . bool wildP (varP v)++    showArg ty var = do+      showable <- canShow ty+      typeable <- canType ty+      case (showable, typeable) of+        (True, _) -> [|showsPrec 11 $(varE var) ""|]+        (_, True) ->+          [|+            "(_ :: "+              ++ show (typeRep (undefined :: Proxy $(return ty)))+              ++ ")"+            |]+        _ -> lift ("(_  :: " ++ pprint (removeModNames ty) ++ ")")++defineShowMatcher :: MakeMockableOptions -> [Method] -> Q Dec+defineShowMatcher options methods = do+  clauses <- concatMapM (showMatcherClauses options) methods+  funD 'showMatcher clauses++showMatcherClauses :: MakeMockableOptions -> Method -> Q [ClauseQ]+showMatcherClauses options method = do+  argTVars <- replicateM (length (methodArgs method)) (newName "t")+  predVars <- replicateM (length (methodArgs method)) (newName "p")+  let actionArgs = zipWith actionArg argTVars (methodArgs method)+  let matcherArgs = varP <$> predVars+  let printedArgs = zipWith3 printedArg predVars argTVars (methodArgs method)+  let polyMatcherArgs = zipWith matcherArg predVars (methodArgs method)+  let printedPolyArgs = zipWith printedPolyArg predVars (methodArgs method)+  let body name args = normalB [|unwords ($(lift name) : $(listE args))|]+  return+    [ clause+        [ conP 'Just [conP (getActionName options method) actionArgs],+          conP (getMatcherName options method) matcherArgs+        ]+        (body (nameBase (methodName method)) printedArgs)+        [],+      clause+        [ conP 'Nothing [],+          conP (getMatcherName options method) polyMatcherArgs+        ]+        (body (nameBase (methodName method)) printedPolyArgs)+        []+    ]+  where+    actionArg t ty+      | isKnownType method ty = wildP+      | otherwise = sigP wildP (varT t)++    matcherArg p ty+      | isKnownType method ty = varP p+      | otherwise = wildP++    printedArg p t ty+      | isKnownType method ty = [|"«" ++ show $(varE p) ++ "»"|]+      | otherwise =+        [|"«" ++ show ($(varE p) :: Predicate $(varT t)) ++ "»"|]++    printedPolyArg p ty+      | isKnownType method ty = [|"«" ++ show $(varE p) ++ "»"|]+      | otherwise = [|"«polymorphic»"|]++defineMatchAction :: MakeMockableOptions -> [Method] -> Q Dec+defineMatchAction options methods =+  funD 'matchAction (matchActionClause options <$> methods)++matchActionClause :: MakeMockableOptions -> Method -> Q Clause+matchActionClause options method = do+  argVars <-+    replicateM+      (length (methodArgs method))+      ((,) <$> newName "p" <*> newName "a")+  mmVar <- newName "mismatches"+  clause+    [ conP+        (getMatcherName options method)+        (varP . fst <$> argVars),+      conP (getActionName options method) (varP . snd <$> argVars)+    ]+    ( guardedB+        [ (,) <$> normalG [|null $(varE mmVar)|] <*> [|Match|],+          (,) <$> normalG [|otherwise|] <*> [|NoMatch $(varE mmVar)|]+        ]+    )+    [ valD+        (varP mmVar)+        ( normalB+            [|+              catMaybes $+                zipWith+                  (fmap . (,))+                  [1 ..]+                  $(listE (mkAccept <$> argVars))+              |]+        )+        []+    ]+  where+    mkAccept (p, a) =+      [|+        if accept $(return (VarE p)) $(return (VarE a))+          then Nothing+          else Just $ explain $(return (VarE p)) $(return (VarE a))+        |]++defineExpectableActions :: MakeMockableOptions -> Instance -> Q [Dec]+defineExpectableActions options inst =+  mapM (defineExpectableAction options inst) (instMethods inst)++type ComplexExpectableMessage name =+  ( 'Text "Method " ':<>: 'Text name+      ':<>: 'Text " is too complex to expect with an Action."+  )+    ':$$: 'Text "Suggested fix: Use a Matcher instead of an Action."++defineExpectableAction :: MakeMockableOptions -> Instance -> Method -> Q Dec+defineExpectableAction options inst method = do+  maybeCxt <- wholeCxt (methodArgs method)+  argVars <- replicateM (length (methodArgs method)) (newName "a")+  case maybeCxt of+    Just cx -> do+      instanceD+        (pure (methodCxt method ++ cx))+        ( appT+            (withMethodParams inst method [t|Expectable|])+            (withMethodParams inst method [t|Action|])+        )+        [ funD+            'toRule+            [ clause+                [conP (getActionName options method) (map varP argVars)]+                ( normalB $+                    let matcherCon = conE (getMatcherName options method)+                     in appE (varE 'toRule) (makeBody argVars matcherCon)+                )+                []+            ]+        ]+    _ -> do+      checkExt UndecidableInstances+      instanceD+        ( (: [])+            <$> [t|+              TypeError+                ( ComplexExpectableMessage+                    $(litT $ strTyLit $ nameBase $ methodName method)+                )+              |]+        )+        ( appT+            (withMethodParams inst method [t|Expectable|])+            (withMethodParams inst method [t|Action|])+        )+        [ funD+            'toRule+            [clause [] (normalB [|undefined|]) []]+        ]+  where+    makeBody [] e = e+    makeBody (v : vs) e = makeBody vs [|$e (eq $(varE v))|]++    wholeCxt :: [Type] -> Q (Maybe Cxt)+    wholeCxt (ty : ts) = do+      thisCxt <- argCxt ty+      otherCxt <- wholeCxt ts+      return ((++) <$> thisCxt <*> otherCxt)+    wholeCxt [] = return (Just [])++    argCxt :: Type -> Q (Maybe Cxt)+    argCxt argTy+      | not (isKnownType method argTy) = return Nothing+      | otherwise =+        simplifyContext [AppT (ConT ''Eq) argTy, AppT (ConT ''Show) argTy]++deriveForMockT :: MakeMockableOptions -> Type -> Q [Dec]+deriveForMockT options ty = do+  inst <- getInstance options {mockVerbose = False} ty+  needsMockT <-+    if mockDeriveForMockT options+      then+        isNothing+          <$> resolveInstanceType+            ( AppT+                (instType inst)+                (AppT (ConT ''MockT) (VarT (instMonadVar inst)))+            )+      else return False++  if needsMockT+    then do+      unless (null (instExtraMembers inst)) $+        fail $+          "Cannot derive MockT because " ++ pprint (instType inst)+            ++ " has unmockable methods."++      m <- newName "m"+      let decs = map (implementMethod options) (instMethods inst)++      let cx =+            instRequiredContext inst+              \\ [ AppT (ConT ''Typeable) (VarT (instMonadVar inst)),+                   AppT (ConT ''Functor) (VarT (instMonadVar inst)),+                   AppT (ConT ''Applicative) (VarT (instMonadVar inst)),+                   AppT (ConT ''Monad) (VarT (instMonadVar inst)),+                   AppT (ConT ''MonadIO) (VarT (instMonadVar inst))+                 ]++      let mockTConstraints =+            substTypeVar+              (instMonadVar inst)+              (AppT (ConT ''MockT) (VarT m))+              <$> cx+      simplifyContext mockTConstraints+        >>= \case+          Just cxMockT ->+            (: [])+              <$> instanceD+                ( concat+                    <$> sequence+                      [ return cxMockT,+                        constrainVars [[t|Typeable|]] (instGeneralParams inst),+                        constrainVars [[t|Typeable|], [t|MonadIO|]] [m]+                      ]+                )+                [t|$(pure (instType inst)) (MockT $(varT m))|]+                decs+          Nothing -> fail "Missing MockT instance for a superclass."+    else return []++implementMethod :: MakeMockableOptions -> Method -> Q Dec+implementMethod options method = do+  argVars <- replicateM (length (methodArgs method)) (newName "a")+  funD+    (methodName method)+    [clause (varP <$> argVars) (normalB (body argVars)) []]+  where+    actionExp [] e = e+    actionExp (v : vs) e = actionExp vs [|$e $(varE v)|]++    body argVars = do+      defaultCxt <- simplifyContext [AppT (ConT ''Default) (methodResult method)]+      let someMockMethod = case defaultCxt of+            Just [] -> [|mockMethod|]+            _ -> [|mockDefaultlessMethod|]+      [|+        $someMockMethod+          $(actionExp argVars (conE (getActionName options method)))+        |]++checkExt :: Extension -> Q ()+checkExt e = do+  enabled <- isExtEnabled e+  unless enabled $+    fail $ "Please enable " ++ show e ++ " to generate this mock."++internalError :: HasCallStack => Q a+internalError = error "Internal error in HMock.  Please report this as a bug."
+ test/Test/Predicates.hs view
@@ -0,0 +1,1477 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE ParallelListComp #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}++-- | Explainable 'Predicate's are essentially functions from types to `Bool`+-- which can additionally describe themselves and explain why an argument does+-- or doesn't match.  They are intended to be used during unit tests to provide+-- better error messages when tests fail.+module Test.Predicates+  ( -- * The Predicate type+    Predicate (..),+    (==~),+    PredicateFailure (..),+    acceptIO,++    -- * Predicate combinators++    -- ** Basic predicates+    anything,+    eq,+    neq,+    gt,+    geq,+    lt,+    leq,+    just,+    nothing,+    left,+    right,++    -- ** Zips+    zipP,+    zip3P,+    zip4P,+    zip5P,++    -- ** Logic+    andP,+    orP,+    notP,+#ifdef REGEX+    -- ** Regular expressions+    matchesRegex,+    matchesCaseInsensitiveRegex,+    containsRegex,+    containsCaseInsensitiveRegex,+#endif++#ifdef CONTAINERS+    -- ** Strings and sequences+    startsWith,+    endsWith,+    hasSubstr,+    hasSubsequence,+    caseInsensitive,++    -- ** Containers+    isEmpty,+    nonEmpty,+    sizeIs,+    elemsAre,+    unorderedElemsAre,+    each,+    contains,+    containsAll,+    containsOnly,+    keys,+    values,+#endif++    -- ** Numerics+    approxEq,+    positive,+    negative,+    nonPositive,+    nonNegative,+    finite,+    infinite,+    nAn,++    -- ** Miscellaneous+    is,+    qIs,+    with,+    qWith,+    inBranch,+    qADT,+    qMatch,+    typed,+  )+where++import Control.Exception (Exception, throwIO)+import Control.Monad (replicateM, unless)+import Data.Functor.Contravariant (Contravariant (..))+import Data.List (intercalate)+import Data.Maybe (isNothing)+import Data.Typeable (Proxy (..), Typeable, cast, typeRep)+import GHC.Stack (CallStack, HasCallStack, callStack, prettyCallStack)+import Language.Haskell.TH+import Language.Haskell.TH.Syntax (lift)+import Test.Predicates.Internal.Util (locate, removeModNames, withLoc)++#ifdef REGEX+import Data.Maybe (isJust)+import Text.Regex.TDFA+  ( CompOption (caseSensitive, lastStarGreedy, newSyntax),+    ExecOption (captureGroups),+    Extract (empty),+    Regex,+    RegexLike (matchOnce, matchOnceText),+    RegexMaker (makeRegexOpts),+    RegexOptions (defaultCompOpt, defaultExecOpt),+  )+#endif++#ifdef CONTAINERS+import Data.Char (toUpper)+import Data.Maybe (catMaybes)+import Data.MonoTraversable (Element, MonoFoldable (..), MonoFunctor (..))+import qualified Data.Sequences as Seq+import GHC.Exts (IsList (Item, toList))+import Test.Predicates.Internal.FlowMatcher (bipartiteMatching)+import Test.Predicates.Internal.Util (isSubsequenceOf)+#endif++-- $setup+-- >>> :set -XLambdaCase+-- >>> :set -XTemplateHaskell+-- >>> :set -XTypeApplications+-- >>> :set -Wno-type-defaults++-- | A predicate, which tests values and either accepts or rejects them.  This+-- is similar to @a -> 'Bool'@, but also can describe itself and explain why an+-- argument does or doesn't match.+data Predicate a = Predicate+  { showPredicate :: String,+    showNegation :: String,+    accept :: a -> Bool,+    explain :: a -> String+  }++instance Show (Predicate a) where show = showPredicate++data PredicateFailure = PredicateFailure String CallStack++instance Show PredicateFailure where+  show (PredicateFailure message cs) = message ++ "\n" ++ prettyCallStack cs+instance Exception PredicateFailure++-- | Same as 'accept', except throws a 'PredicateFailure' instead of returning a 'Bool'.+acceptIO :: HasCallStack => Predicate a -> a -> IO ()+acceptIO p x =+  unless (accept p x) $+    throwIO $ PredicateFailure (explain p x) callStack++-- | An infix synonym for 'accept'.+--+-- >>> eq 1 ==~ 1+-- True+-- >>> eq 2 ==~ 1+-- False+(==~) :: Predicate a -> a -> Bool+(==~) = accept++withDefaultExplain ::+  (a -> String) -> String -> ((a -> String) -> Predicate a) -> Predicate a+withDefaultExplain format connector mk = p+  where+    p = mk $ \x ->+      if accept p x+        then format x ++ connector ++ showPredicate p+        else format x ++ connector ++ showNegation p++-- | A 'Predicate' that accepts anything at all.+--+-- >>> accept anything "foo"+-- True+-- >>> accept anything undefined+-- True+anything :: Predicate a+anything =+  Predicate+    { showPredicate = "anything",+      showNegation = "nothing",+      accept = const True,+      explain = const "always matches"+    }++-- | A 'Predicate' that accepts only the given value.+--+-- >>> accept (eq "foo") "foo"+-- True+-- >>> accept (eq "foo") "bar"+-- False+eq :: (Show a, Eq a) => a -> Predicate a+eq x =+  Predicate+    { showPredicate = show x,+      showNegation = "≠ " ++ show x,+      accept = (== x),+      explain = \y ->+        if y == x+          then show y ++ " = " ++ show x+          else show y ++ " ≠ " ++ show x+    }++-- | A 'Predicate' that accepts anything but the given value.+--+-- >>> accept (neq "foo") "foo"+-- False+-- >>> accept (neq "foo") "bar"+-- True+neq :: (Show a, Eq a) => a -> Predicate a+neq = notP . eq++-- | A 'Predicate' that accepts anything greater than the given value.+--+-- >>> accept (gt 5) 4+-- False+-- >>> accept (gt 5) 5+-- False+-- >>> accept (gt 5) 6+-- True+gt :: (Show a, Ord a) => a -> Predicate a+gt x = withDefaultExplain show " " $ \explainImpl ->+  Predicate+    { showPredicate = "> " ++ show x,+      showNegation = "≤ " ++ show x,+      accept = (> x),+      explain = explainImpl+    }++-- | A 'Predicate' that accepts anything greater than or equal to the given+-- value.+--+-- >>> accept (geq 5) 4+-- False+-- >>> accept (geq 5) 5+-- True+-- >>> accept (geq 5) 6+-- True+geq :: (Show a, Ord a) => a -> Predicate a+geq x = withDefaultExplain show " " $ \explainImpl ->+  Predicate+    { showPredicate = "≥ " ++ show x,+      showNegation = "< " ++ show x,+      accept = (>= x),+      explain = explainImpl+    }++-- | A 'Predicate' that accepts anything less than the given value.+--+-- >>> accept (lt 5) 4+-- True+-- >>> accept (lt 5) 5+-- False+-- >>> accept (lt 5) 6+-- False+lt :: (Show a, Ord a) => a -> Predicate a+lt = notP . geq++-- | A 'Predicate' that accepts anything less than or equal to the given value.+--+-- >>> accept (leq 5) 4+-- True+-- >>> accept (leq 5) 5+-- True+-- >>> accept (leq 5) 6+-- False+leq :: (Show a, Ord a) => a -> Predicate a+leq = notP . gt++-- | A 'Predicate' that accepts 'Maybe' values of @'Just' x@, where @x@ matches+-- the given child 'Predicate'.+--+-- >>> accept (just (eq "value")) Nothing+-- False+-- >>> accept (just (eq "value")) (Just "value")+-- True+-- >>> accept (just (eq "value")) (Just "wrong value")+-- False+just :: Predicate a -> Predicate (Maybe a)+just p =+  Predicate+    { showPredicate = "Just (" ++ showPredicate p ++ ")",+      showNegation = "not Just (" ++ showPredicate p ++ ")",+      accept = \case Just x -> accept p x; _ -> False,+      explain = \case Just x -> explain p x; _ -> "Nothing ≠ Just _"+    }++-- | A Predicate that accepts 'Maybe' values of @'Nothing'@.  Unlike 'eq', this+-- doesn't require 'Eq' or 'Show' instances.+--+-- >>> accept nothing Nothing+-- True+--+-- >>> accept nothing (Just "something")+-- False+nothing :: Predicate (Maybe a)+nothing =+  Predicate+    { showPredicate = "Nothing",+      showNegation = "Just anything",+      accept = isNothing,+      explain = \case Nothing -> "Nothing = Nothing"; _ -> "Just _ ≠ Nothing"+    }++-- | A 'Predicate' that accepts an 'Either' value of @'Left' x@, where @x@+-- matches the given child 'Predicate'.+--+-- >>> accept (left (eq "value")) (Left "value")+-- True+-- >>> accept (left (eq "value")) (Right "value")+-- False+-- >>> accept (left (eq "value")) (Left "wrong value")+-- False+left :: Predicate a -> Predicate (Either a b)+left p =+  Predicate+    { showPredicate = "Left (" ++ showPredicate p ++ ")",+      showNegation = "not Left (" ++ showPredicate p ++ ")",+      accept = \case Left x -> accept p x; _ -> False,+      explain = \case Left x -> explain p x; _ -> "Right _ ≠ Left _"+    }++-- | A 'Predicate' that accepts an 'Either' value of @'Right' x@, where @x@+-- matches the given child 'Predicate'.+--+-- >>> accept (right (eq "value")) (Right "value")+-- True+-- >>> accept (right (eq "value")) (Right "wrong value")+-- False+-- >>> accept (right (eq "value")) (Left "value")+-- False+right :: Predicate b -> Predicate (Either a b)+right p =+  Predicate+    { showPredicate = "Right (" ++ showPredicate p ++ ")",+      showNegation = "not Right (" ++ showPredicate p ++ ")",+      accept = \case Right x -> accept p x; _ -> False,+      explain = \case Right x -> explain p x; _ -> "Left _ ≠ Right _"+    }++-- | A 'Predicate' that accepts pairs whose elements satisfy the corresponding+-- child 'Predicate's.+--+-- >>> accept (zipP (eq "foo") (eq "bar")) ("foo", "bar")+-- True+-- >>> accept (zipP (eq "foo") (eq "bar")) ("bar", "foo")+-- False+zipP :: Predicate a -> Predicate b -> Predicate (a, b)+zipP p1 p2 =+  Predicate+    { showPredicate = show (p1, p2),+      showNegation = "not " ++ show (p1, p2),+      accept = all fst . acceptAndExplain,+      explain = \xs ->+        let results = acceptAndExplain xs+            significant+              | all fst results = results+              | otherwise = filter (not . fst) results+         in intercalate " and " $ map snd significant+    }+  where+    acceptAndExplain = \(x1, x2) ->+      [ (accept p1 x1, explain p1 x1),+        (accept p2 x2, explain p2 x2)+      ]++-- | A 'Predicate' that accepts 3-tuples whose elements satisfy the+-- corresponding child 'Predicate's.+--+-- >>> accept (zip3P (eq "foo") (eq "bar") (eq "qux")) ("foo", "bar", "qux")+-- True+-- >>> accept (zip3P (eq "foo") (eq "bar") (eq "qux")) ("qux", "bar", "foo")+-- False+zip3P :: Predicate a -> Predicate b -> Predicate c -> Predicate (a, b, c)+zip3P p1 p2 p3 =+  Predicate+    { showPredicate = show (p1, p2, p3),+      showNegation = "not " ++ show (p1, p2, p3),+      accept = all fst . acceptAndExplain,+      explain = \xs ->+        let results = acceptAndExplain xs+            significant+              | all fst results = results+              | otherwise = filter (not . fst) results+         in intercalate " and " $ map snd significant+    }+  where+    acceptAndExplain = \(x1, x2, x3) ->+      [ (accept p1 x1, explain p1 x1),+        (accept p2 x2, explain p2 x2),+        (accept p3 x3, explain p3 x3)+      ]++-- | A 'Predicate' that accepts 3-tuples whose elements satisfy the+-- corresponding child 'Predicate's.+--+-- >>> accept (zip4P (eq 1) (eq 2) (eq 3) (eq 4)) (1, 2, 3, 4)+-- True+-- >>> accept (zip4P (eq 1) (eq 2) (eq 3) (eq 4)) (4, 3, 2, 1)+-- False+zip4P ::+  Predicate a ->+  Predicate b ->+  Predicate c ->+  Predicate d ->+  Predicate (a, b, c, d)+zip4P p1 p2 p3 p4 =+  Predicate+    { showPredicate = show (p1, p2, p3, p4),+      showNegation = "not " ++ show (p1, p2, p3, p4),+      accept = all fst . acceptAndExplain,+      explain = \xs ->+        let results = acceptAndExplain xs+            significant+              | all fst results = results+              | otherwise = filter (not . fst) results+         in intercalate " and " $ map snd significant+    }+  where+    acceptAndExplain = \(x1, x2, x3, x4) ->+      [ (accept p1 x1, explain p1 x1),+        (accept p2 x2, explain p2 x2),+        (accept p3 x3, explain p3 x3),+        (accept p4 x4, explain p4 x4)+      ]++-- | A 'Predicate' that accepts 3-tuples whose elements satisfy the+-- corresponding child 'Predicate's.+--+-- >>> accept (zip5P (eq 1) (eq 2) (eq 3) (eq 4) (eq 5)) (1, 2, 3, 4, 5)+-- True+-- >>> accept (zip5P (eq 1) (eq 2) (eq 3) (eq 4) (eq 5)) (5, 4, 3, 2, 1)+-- False+zip5P ::+  Predicate a ->+  Predicate b ->+  Predicate c ->+  Predicate d ->+  Predicate e ->+  Predicate (a, b, c, d, e)+zip5P p1 p2 p3 p4 p5 =+  Predicate+    { showPredicate = show (p1, p2, p3, p4, p5),+      showNegation = "not " ++ show (p1, p2, p3, p4, p5),+      accept = all fst . acceptAndExplain,+      explain = \xs ->+        let results = acceptAndExplain xs+            significant+              | all fst results = results+              | otherwise = filter (not . fst) results+         in intercalate " and " $ map snd significant+    }+  where+    acceptAndExplain = \(x1, x2, x3, x4, x5) ->+      [ (accept p1 x1, explain p1 x1),+        (accept p2 x2, explain p2 x2),+        (accept p3 x3, explain p3 x3),+        (accept p4 x4, explain p4 x4),+        (accept p5 x5, explain p5 x5)+      ]++-- | A 'Predicate' that accepts anything accepted by both of its children.+--+-- >>> accept (lt "foo" `andP` gt "bar") "eta"+-- True+-- >>> accept (lt "foo" `andP` gt "bar") "quz"+-- False+-- >>> accept (lt "foo" `andP` gt "bar") "alpha"+-- False+andP :: Predicate a -> Predicate a -> Predicate a+p `andP` q =+  Predicate+    { showPredicate = showPredicate p ++ " and " ++ showPredicate q,+      showNegation = showNegation p ++ " or " ++ showNegation q,+      accept = \x -> accept p x && accept q x,+      explain = \x ->+        if+            | not (accept p x) -> explain p x+            | not (accept q x) -> explain q x+            | otherwise -> explain p x ++ " and " ++ explain q x+    }++-- | A 'Predicate' that accepts anything accepted by either of its children.+--+-- >>> accept (lt "bar" `orP` gt "foo") "eta"+-- False+-- >>> accept (lt "bar" `orP` gt "foo") "quz"+-- True+-- >>> accept (lt "bar" `orP` gt "foo") "alpha"+-- True+orP :: Predicate a -> Predicate a -> Predicate a+p `orP` q = notP (notP p `andP` notP q)++-- | A 'Predicate' that inverts another 'Predicate', accepting whatever its+-- child rejects, and rejecting whatever its child accepts.+--+-- >>> accept (notP (eq "negative")) "positive"+-- True+-- >>> accept (notP (eq "negative")) "negative"+-- False+notP :: Predicate a -> Predicate a+notP p =+  Predicate+    { showPredicate = showNegation p,+      showNegation = showPredicate p,+      accept = not . accept p,+      explain = explain p+    }++#ifdef REGEX++-- | A 'Predicate' that accepts 'String's or string-like values matching a+-- regular expression.  The expression must match the entire argument.+--+-- You should not use @'caseInsensitive' 'matchesRegex'@, because regular+-- expression syntax itself is still case-sensitive even when the text you are+-- matching is not.  Instead, use 'matchesCaseInsensitiveRegex'.+--+-- >>> accept (matchesRegex "x{2,5}y?") "xxxy"+-- True+-- >>> accept (matchesRegex "x{2,5}y?") "xyy"+-- False+-- >>> accept (matchesRegex "x{2,5}y?") "wxxxyz"+-- False+matchesRegex :: (RegexLike Regex a, Eq a, Show a) => String -> Predicate a+matchesRegex s =+  Predicate+    { showPredicate = pat,+      showNegation = "not " ++ pat,+      accept = accepts,+      explain = \x ->+        if accepts x+          then show x ++ " matches " ++ pat+          else show x ++ " doesn't match " ++ pat+    }+  where+    pat = "/" ++ init (tail $ show s) ++ "/"+    accepts x = case matchOnceText r x of+      Just (a, _, b) -> a == empty && b == empty+      Nothing -> False+    r = makeRegexOpts comp exec s :: Regex+    comp = defaultCompOpt {newSyntax = True, lastStarGreedy = True}+    exec = defaultExecOpt {captureGroups = False}++-- | A 'Predicate' that accepts 'String's or string-like values matching a+-- regular expression in a case-insensitive way.  The expression must match the+-- entire argument.+--+-- You should use this instead of @'caseInsensitive' 'matchesRegex'@, because+-- regular expression syntax itself is still case-sensitive even when the text+-- you are matching is not.+--+-- >>> accept (matchesCaseInsensitiveRegex "x{2,5}y?") "XXXY"+-- True+-- >>> accept (matchesCaseInsensitiveRegex "x{2,5}y?") "XYY"+-- False+-- >>> accept (matchesCaseInsensitiveRegex "x{2,5}y?") "WXXXYZ"+-- False+matchesCaseInsensitiveRegex ::+  (RegexLike Regex a, Eq a, Show a) => String -> Predicate a+matchesCaseInsensitiveRegex s =+  Predicate+    { showPredicate = pat,+      showNegation = "not " ++ pat,+      accept = accepts,+      explain = \x ->+        if accepts x+          then show x ++ " matches " ++ pat+          else show x ++ " doesn't match " ++ pat+    }+  where+    pat = "/" ++ init (tail $ show s) ++ "/i"+    accepts x = case matchOnceText r x of+      Just (a, _, b) -> a == empty && b == empty+      Nothing -> False+    r = makeRegexOpts comp exec s :: Regex+    comp =+      defaultCompOpt+        { newSyntax = True,+          lastStarGreedy = True,+          caseSensitive = False+        }+    exec = defaultExecOpt {captureGroups = False}++-- | A 'Predicate' that accepts 'String's or string-like values containing a+-- match for a regular expression.  The expression need not match the entire+-- argument.+--+-- You should not use @'caseInsensitive' 'containsRegex'@, because regular+-- expression syntax itself is still case-sensitive even when the text you are+-- matching is not.  Instead, use 'containsCaseInsensitiveRegex'.+--+-- >>> accept (containsRegex "x{2,5}y?") "xxxy"+-- True+-- >>> accept (containsRegex "x{2,5}y?") "xyy"+-- False+-- >>> accept (containsRegex "x{2,5}y?") "wxxxyz"+-- True+containsRegex :: (RegexLike Regex a, Eq a, Show a) => String -> Predicate a+containsRegex s = withDefaultExplain show " " $ \explainImpl ->+  Predicate+    { showPredicate = "contains " ++ pat,+      showNegation = "doesn't contain " ++ pat,+      accept = isJust . matchOnce r,+      explain = explainImpl+    }+  where+    pat = "/" ++ init (tail $ show s) ++ "/"+    r = makeRegexOpts comp exec s :: Regex+    comp = defaultCompOpt {newSyntax = True, lastStarGreedy = True}+    exec = defaultExecOpt {captureGroups = False}++-- | A 'Predicate' that accepts 'String's or string-like values containing a+-- match for a regular expression in a case-insensitive way.  The expression+-- need match the entire argument.+--+-- You should use this instead of @'caseInsensitive' 'containsRegex'@, because+-- regular expression syntax itself is still case-sensitive even when the text+-- you are matching is not.+--+-- >>> accept (containsCaseInsensitiveRegex "x{2,5}y?") "XXXY"+-- True+-- >>> accept (containsCaseInsensitiveRegex "x{2,5}y?") "XYY"+-- False+-- >>> accept (containsCaseInsensitiveRegex "x{2,5}y?") "WXXXYZ"+-- True+containsCaseInsensitiveRegex ::+  (RegexLike Regex a, Eq a, Show a) => String -> Predicate a+containsCaseInsensitiveRegex s = withDefaultExplain show " " $ \explainImpl ->+  Predicate+    { showPredicate = "contains " ++ pat,+      showNegation = "doesn't contain " ++ pat,+      accept = isJust . matchOnce r,+      explain = explainImpl+    }+  where+    pat = "/" ++ init (tail $ show s) ++ "/i"+    r = makeRegexOpts comp exec s :: Regex+    comp =+      defaultCompOpt+        { newSyntax = True,+          lastStarGreedy = True,+          caseSensitive = False+        }+    exec = defaultExecOpt {captureGroups = False}++#endif++#ifdef CONTAINERS++-- | A 'Predicate' that accepts sequences that start with the given prefix.+--+-- >>> accept (startsWith "fun") "fungible"+-- True+-- >>> accept (startsWith "gib") "fungible"+-- False+startsWith :: (Show t, Seq.IsSequence t, Eq (Element t)) => t -> Predicate t+startsWith pfx = withDefaultExplain show " " $ \explainImpl ->+  Predicate+    { showPredicate = "starts with " ++ show pfx,+      showNegation = "doesn't start with " ++ show pfx,+      accept = (pfx `Seq.isPrefixOf`),+      explain = explainImpl+    }++-- | A 'Predicate' that accepts sequences that end with the given suffix.+--+-- >>> accept (endsWith "ow") "crossbow"+-- True+-- >>> accept (endsWith "ow") "trebuchet"+-- False+endsWith :: (Show t, Seq.IsSequence t, Eq (Element t)) => t -> Predicate t+endsWith sfx = withDefaultExplain show " " $ \explainImpl ->+  Predicate+    { showPredicate = "ends with " ++ show sfx,+      showNegation = "doesn't end with " ++ show sfx,+      accept = (sfx `Seq.isSuffixOf`),+      explain = explainImpl+    }++-- | A 'Predicate' that accepts sequences that contain the given (consecutive)+-- substring.+--+-- >>> accept (hasSubstr "i") "team"+-- False+-- >>> accept (hasSubstr "i") "partnership"+-- True+hasSubstr :: (Show t, Seq.IsSequence t, Eq (Element t)) => t -> Predicate t+hasSubstr s = withDefaultExplain show " " $ \explainImpl ->+  Predicate+    { showPredicate = "has substring " ++ show s,+      showNegation = "doesn't have substring " ++ show s,+      accept = (s `Seq.isInfixOf`),+      explain = explainImpl+    }++-- | A 'Predicate' that accepts sequences that contain the given (not+-- necessarily consecutive) subsequence.+--+-- >>> accept (hasSubsequence [1..5]) [1, 2, 3, 4, 5]+-- True+-- >>> accept (hasSubsequence [1..5]) [0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0]+-- True+-- >>> accept (hasSubsequence [1..5]) [2, 3, 5, 7, 11]+-- False+hasSubsequence :: (Show t, Seq.IsSequence t, Eq (Element t)) => t -> Predicate t+hasSubsequence s = withDefaultExplain show " " $ \explainImpl ->+  Predicate+    { showPredicate = "has subsequence " ++ show s,+      showNegation = "doesn't have subsequence " ++ show s,+      accept = (s `isSubsequenceOf`),+      explain = explainImpl+    }++-- | Transforms a 'Predicate' on 'String's or string-like types to match without+-- regard to case.+--+-- >>> accept (caseInsensitive startsWith "foo") "FOOTBALL!"+-- True+-- >>> accept (caseInsensitive endsWith "ball") "soccer"+-- False+-- >>> accept (caseInsensitive eq "time") "TIME"+-- True+-- >>> accept (caseInsensitive gt "NOTHING") "everything"+-- False+caseInsensitive ::+  ( MonoFunctor t,+    MonoFunctor a,+    Element t ~ Char,+    Element a ~ Char+  ) =>+  (t -> Predicate a) ->+  (t -> Predicate a)+caseInsensitive p s =+  Predicate+    { showPredicate = "(case insensitive) " ++ show (p s),+      showNegation = "(case insensitive) " ++ show (notP (p s)),+      accept = accept capP . omap toUpper,+      explain = explain capP . omap toUpper+    }+  where+    capP = p (omap toUpper s)++-- | A 'Predicate' that accepts empty data structures.+--+-- >>> accept isEmpty ([] :: [Int])+-- True+-- >>> accept isEmpty [1, 2, 3]+-- False+-- >>> accept isEmpty ""+-- True+-- >>> accept isEmpty "gas tank"+-- False+isEmpty :: (MonoFoldable t, Show t) => Predicate t+isEmpty = withDefaultExplain show " is " $ \explainImpl ->+  Predicate+    { showPredicate = "empty",+      showNegation = "non-empty",+      accept = onull,+      explain = explainImpl+    }++-- | A 'Predicate' that accepts non-empty data structures.+--+-- >>> accept nonEmpty ([] :: [Int])+-- False+-- >>> accept nonEmpty [1, 2, 3]+-- True+-- >>> accept nonEmpty ""+-- False+-- >>> accept nonEmpty "gas tank"+-- True+nonEmpty :: (MonoFoldable t, Show t) => Predicate t+nonEmpty = notP isEmpty++-- | A 'Predicate' that accepts data structures whose number of elements match+-- the child 'Predicate'.+--+-- >>> accept (sizeIs (lt 3)) ['a' .. 'f']+-- False+-- >>> accept (sizeIs (lt 3)) ['a' .. 'b']+-- True+sizeIs :: (MonoFoldable t, Show t) => Predicate Int -> Predicate t+sizeIs p =+  Predicate+    { showPredicate = "size " ++ showPredicate p,+      showNegation = "size " ++ showNegation p,+      accept = accept p . olength,+      explain = \y ->+        let detail+              | accept p (olength y) = showPredicate p+              | otherwise = showNegation p+            detailStr+              | show (olength y) == detail = ""+              | otherwise = ", which is " ++ detail+         in show y ++ " has size " ++ show (olength y) ++ detailStr+    }++-- | A 'Predicate' that accepts data structures whose contents each match the+-- corresponding 'Predicate' in the given list, in the same order.+--+-- >>> accept (elemsAre [lt 3, lt 4, lt 5]) [2, 3, 4]+-- True+-- >>> accept (elemsAre [lt 3, lt 4, lt 5]) [2, 3, 4, 5]+-- False+-- >>> accept (elemsAre [lt 3, lt 4, lt 5]) [2, 10, 4]+-- False+elemsAre :: MonoFoldable t => [Predicate (Element t)] -> Predicate t+elemsAre ps =+  Predicate+    { showPredicate = show ps,+      showNegation = "not " ++ show ps,+      accept = \xs ->+        olength xs == olength ps+          && and (zipWith accept ps (otoList xs)),+      explain = \xs ->+        let results = acceptAndExplain (otoList xs)+         in if+                | olength xs /= length ps ->+                  "wrong size (got "+                    ++ show (olength xs)+                    ++ "; expected "+                    ++ show (length ps)+                    ++ ")"+                | all fst results -> "elements are " ++ show ps+                | otherwise ->+                  intercalate "; and " $+                    snd <$> filter (not . fst) results+    }+  where+    acceptAndExplain xs = zipWith3 matchAndExplain [1 :: Int ..] ps xs+    matchAndExplain i p x =+      (accept p x, "in element #" ++ show i ++ ": " ++ explain p x)++-- | A 'Predicate' that accepts data structures whose contents each match the+-- corresponding 'Predicate' in the given list, in any order.+--+-- >>> accept (unorderedElemsAre [eq 1, eq 2, eq 3]) [1, 2, 3]+-- True+-- >>> accept (unorderedElemsAre [eq 1, eq 2, eq 3]) [2, 3, 1]+-- True+-- >>> accept (unorderedElemsAre [eq 1, eq 2, eq 3]) [1, 2, 3, 4]+-- False+-- >>> accept (unorderedElemsAre [eq 1, eq 2, eq 3]) [1, 3]+-- False+unorderedElemsAre :: MonoFoldable t => [Predicate (Element t)] -> Predicate t+unorderedElemsAre ps =+  Predicate+    { showPredicate =+        "(any order) " ++ show ps,+      showNegation =+        "not (in any order) " ++ show ps,+      accept = \xs ->+        let (_, orphanPs, orphanXs) = matchAll xs+         in null orphanPs && null orphanXs,+      explain = \xs ->+        let (matches, orphanPs, orphanXs) = matchAll xs+         in if null orphanPs && null orphanXs+              then intercalate "; and " (explainMatch <$> matches)+              else+                let missingExplanation =+                      if null orphanPs+                        then Nothing+                        else+                          Just+                            ( "Missing: "+                                ++ intercalate ", " (showPredicate <$> orphanPs)+                            )+                    extraExplanation =+                      if null orphanXs+                        then Nothing+                        else+                          Just+                            ( "Extra elements: "+                                ++ intercalate+                                  ", "+                                  (("#" ++) . show . fst <$> orphanXs)+                            )+                 in intercalate+                      "; "+                      (catMaybes [missingExplanation, extraExplanation])+    }+  where+    matchOne p (_, x) = accept p x+    matchAll xs = bipartiteMatching matchOne ps (zip [1 :: Int ..] (otoList xs))+    explainMatch (p, (j, x)) = "element #" ++ show j ++ ": " ++ explain p x++-- | A 'Predicate' that accepts data structures whose elements each match the+-- child 'Predicate'.+--+-- >>> accept (each (gt 5)) [4, 5, 6]+-- False+-- >>> accept (each (gt 5)) [6, 7, 8]+-- True+-- >>> accept (each (gt 5)) []+-- True+each :: MonoFoldable t => Predicate (Element t) -> Predicate t+each p =+  Predicate+    { showPredicate = "each (" ++ showPredicate p ++ ")",+      showNegation = "contains (" ++ showNegation p ++ ")",+      accept = all fst . acceptAndExplain,+      explain = \xs ->+        let results = acceptAndExplain xs+            format (i, explanation) =+              "element #" ++ show i ++ ": " ++ explanation+         in if all fst results+              then "all elements " ++ showPredicate p+              else+                intercalate "; and " $+                  format . snd <$> filter (not . fst) results+    }+  where+    acceptAndExplain xs =+      [(accept p x, (i, explain p x)) | i <- [1 :: Int ..] | x <- otoList xs]++-- | A 'Predicate' that accepts data structures which contain at least one+-- element matching the child 'Predicate'.+--+-- >>> accept (contains (gt 5)) [3, 4, 5]+-- False+-- >>> accept (contains (gt 5)) [4, 5, 6]+-- True+-- >>> accept (contains (gt 5)) []+-- False+contains :: MonoFoldable t => Predicate (Element t) -> Predicate t+contains = notP . each . notP++-- | A 'Predicate' that accepts data structures whose elements all satisfy the+-- given child 'Predicate's.+--+-- >>> accept (containsAll [eq "foo", eq "bar"]) ["bar", "foo"]+-- True+-- >>> accept (containsAll [eq "foo", eq "bar"]) ["foo"]+-- False+-- >>> accept (containsAll [eq "foo", eq "bar"]) ["foo", "bar", "qux"]+-- True+--+-- Each child 'Predicate' must be satisfied by a different element, so repeating+-- a 'Predicate' requires that two different matching elements exist.  If you+-- want a 'Predicate' to match multiple elements, instead, you can accomplish+-- this with @'contains' p1 `'andP'` 'contains' p2 `'andP'` ...@.+--+-- >>> accept (containsAll [startsWith "f", endsWith "o"]) ["foo"]+-- False+-- >>> accept (contains (startsWith "f") `andP` contains (endsWith "o")) ["foo"]+-- True+containsAll :: MonoFoldable t => [Predicate (Element t)] -> Predicate t+containsAll ps =+  Predicate+    { showPredicate = "contains all of " ++ show ps,+      showNegation = "not all of " ++ show ps,+      accept = \xs -> let (_, orphanPs, _) = matchAll xs in null orphanPs,+      explain = \xs ->+        let (matches, orphanPs, _) = matchAll xs+         in if null orphanPs+              then intercalate "; and " (explainMatch <$> matches)+              else "Missing: " ++ intercalate ", " (showPredicate <$> orphanPs)+    }+  where+    matchOne p (_, x) = accept p x+    matchAll xs = bipartiteMatching matchOne ps (zip [1 :: Int ..] (otoList xs))+    explainMatch (p, (j, x)) = "element #" ++ show j ++ ": " ++ explain p x++-- | A 'Predicate' that accepts data structures whose elements all satisfy one+-- of the child 'Predicate's.+--+-- >>> accept (containsOnly [eq "foo", eq "bar"]) ["foo"]+-- True+-- >>> accept (containsOnly [eq "foo", eq "bar"]) ["foo", "bar"]+-- True+-- >>> accept (containsOnly [eq "foo", eq "bar"]) ["foo", "qux"]+-- False+--+-- Each element must satisfy a different child 'Predicate'.  If you want+-- multiple elements to match the same 'Predicate', instead, you can accomplish+-- this with @'each' (p1 `'orP'` p2 `'orP'` ...)@.+--+-- >>> accept (containsOnly [eq "foo", eq "bar"]) ["foo", "foo"]+-- False+-- >>> accept (each (eq "foo" `orP` eq "bar")) ["foo", "foo"]+-- True+containsOnly :: MonoFoldable t => [Predicate (Element t)] -> Predicate t+containsOnly ps =+  Predicate+    { showPredicate = "contains only " ++ show ps,+      showNegation = "not only " ++ show ps,+      accept = \xs -> let (_, _, orphanXs) = matchAll xs in null orphanXs,+      explain = \xs ->+        let (matches, _, orphanXs) = matchAll xs+         in if null orphanXs+              then intercalate "; and " (explainMatch <$> matches)+              else+                "Extra elements: "+                  ++ intercalate ", " (("#" ++) . show . fst <$> orphanXs)+    }+  where+    matchOne p (_, x) = accept p x+    matchAll xs = bipartiteMatching matchOne ps (zip [1 :: Int ..] (otoList xs))+    explainMatch (p, (j, x)) = "element #" ++ show j ++ ": " ++ explain p x++-- | Transforms a 'Predicate' on a list of keys into a 'Predicate' on map-like+-- data structures.+--+-- This is equivalent to @'with' ('map' 'fst' '.' 'toList')@, but more readable.+--+-- >>> accept (keys (each (eq "foo"))) [("foo", 5)]+-- True+--+-- >>> accept (keys (each (eq "foo"))) [("foo", 5), ("bar", 6)]+-- False+keys :: (IsList t, Item t ~ (k, v)) => Predicate [k] -> Predicate t+keys p =+  Predicate+    { showPredicate = "keys (" ++ showPredicate p ++ ")",+      showNegation = "keys (" ++ showNegation p ++ ")",+      accept = accept p . map fst . toList,+      explain = ("in keys, " ++) . explain p . map fst . toList+    }++-- | Transforms a 'Predicate' on a list of values into a 'Predicate' on map-like+-- data structures.+--+-- This is equivalent to @'with' ('map' 'snd' '.' 'toList')@, but more readable.+--+-- >>> accept (values (each (eq 5))) [("foo", 5), ("bar", 5)]+-- True+--+-- >>> accept (values (each (eq 5))) [("foo", 5), ("bar", 6)]+-- False+values :: (IsList t, Item t ~ (k, v)) => Predicate [v] -> Predicate t+values p =+  Predicate+    { showPredicate = "values (" ++ showPredicate p ++ ")",+      showNegation = "values (" ++ showNegation p ++ ")",+      accept = accept p . map snd . toList,+      explain = ("in values, " ++) . explain p . map snd . toList+    }++#endif++-- | A 'Predicate' that accepts values of 'RealFloat' types that are close to+-- the given number.  The expected precision is scaled based on the target+-- value, so that reasonable rounding error is accepted but grossly inaccurate+-- results are not.+--+-- The following naive use of 'eq' fails due to rounding:+--+-- >>> accept (eq 1.0) (sum (replicate 100 0.01))+-- False+--+-- The solution is to use 'approxEq', which accounts for rounding error.+-- However, 'approxEq' doesn't accept results that are far enough off that they+-- likely arise from incorrect calculations instead of rounding error.+--+-- >>> accept (approxEq 1.0) (sum (replicate 100 0.01))+-- True+-- >>> accept (approxEq 1.0) (sum (replicate 100 0.009999))+-- False+approxEq :: (RealFloat a, Show a) => a -> Predicate a+approxEq x = withDefaultExplain show " " $ \explainImpl ->+  Predicate+    { showPredicate = "≈ " ++ show x,+      showNegation = "≇" ++ show x,+      accept = \y -> abs (x - y) < diff,+      explain = explainImpl+    }+  where+    diff = encodeFloat 1 (snd (decodeFloat x) + floatDigits x `div` 2)++-- | A 'Predicate' that accepts positive numbers of any 'Ord'ered 'Num' type.+--+-- >>> accept positive 1+-- True+--+-- >>> accept positive 0+-- False+--+-- >>> accept positive (-1)+-- False+positive :: (Ord a, Num a) => Predicate a+positive =+  Predicate+    { showPredicate = "positive",+      showNegation = "non-positive",+      accept = \x -> signum x > 0,+      explain = \x ->+        if+            | signum x > 0 -> "value is positive"+            | x == 0 -> "value is zero"+            | signum x < 0 -> "value is negative"+            | otherwise -> "value has unknown sign"+    }++-- | A 'Predicate' that accepts negative numbers of any 'Ord'ered 'Num' type.+--+-- >>> accept negative 1+-- False+--+-- >>> accept negative 0+-- False+--+-- >>> accept negative (-1)+-- True+negative :: (Ord a, Num a) => Predicate a+negative =+  Predicate+    { showPredicate = "negative",+      showNegation = "non-negative",+      accept = \x -> signum x < 0,+      explain = \x ->+        if+            | signum x < 0 -> "value is negative"+            | x == 0 -> "value is zero"+            | signum x < 0 -> "value is positive"+            | otherwise -> "value has unknown sign"+    }++-- | A 'Predicate' that accepts non-positive numbers of any 'Ord'ered 'Num'+-- type.+--+-- >>> accept nonPositive 1+-- False+--+-- >>> accept nonPositive 0+-- True+--+-- >>> accept nonPositive (-1)+-- True+nonPositive :: (Ord a, Num a) => Predicate a+nonPositive = notP positive++-- | A 'Predicate' that accepts non-negative numbers of any 'Ord'ered 'Num'+-- type.+--+-- >>> accept nonNegative 1+-- True+--+-- >>> accept nonNegative 0+-- True+--+-- >>> accept nonNegative (-1)+-- False+nonNegative :: (Ord a, Num a) => Predicate a+nonNegative = notP negative++-- | A 'Predicate' that accepts finite numbers of any 'RealFloat' type.+--+-- >>> accept finite 1.0+-- True+-- >>> accept finite (0 / 0)+-- False+-- >>> accept finite (1 / 0)+-- False+finite :: RealFloat a => Predicate a+finite =+  Predicate+    { showPredicate = "finite",+      showNegation = "non-finite",+      accept = isFinite,+      explain = \x ->+        if isFinite x+          then "value is finite"+          else "value is not finite"+    }+  where+    isFinite x = not (isInfinite x) && not (isNaN x)++-- | A 'Predicate' that accepts infinite numbers of any 'RealFloat' type.+--+-- >>> accept infinite 1.0+-- False+-- >>> accept infinite (0 / 0)+-- False+-- >>> accept infinite (1 / 0)+-- True+infinite :: RealFloat a => Predicate a+infinite =+  Predicate+    { showPredicate = "infinite",+      showNegation = "non-infinite",+      accept = isInfinite,+      explain = \x ->+        if isInfinite x+          then "value is infinite"+          else "value is not infinite"+    }++-- | A 'Predicate' that accepts NaN values of any 'RealFloat' type.+--+-- >>> accept nAn 1.0+-- False+-- >>> accept nAn (0 / 0)+-- True+-- >>> accept nAn (1 / 0)+-- False+nAn :: RealFloat a => Predicate a+nAn =+  Predicate+    { showPredicate = "NaN",+      showNegation = "non-NaN",+      accept = isNaN,+      explain = \x ->+        if isNaN x+          then "value is NaN"+          else "value is not NaN"+    }++-- | A conversion from @a -> 'Bool'@ to 'Predicate'.  This is a fallback that+-- can be used to build a 'Predicate' that checks anything at all.  However, its+-- description will be less helpful than standard 'Predicate's.  You can use+-- 'qIs' instead to get better descriptions using Template Haskell.+--+-- >>> accept (is even) 3+-- False+-- >>> accept (is even) 4+-- True+is :: HasCallStack => (a -> Bool) -> Predicate a+is p =+  Predicate+    { showPredicate = withLoc (locate callStack "custom predicate"),+      showNegation = withLoc (locate callStack "negated custom predicate"),+      accept = p,+      explain = \x ->+        if p x+          then "value matched custom predicate"+          else "value did not match custom predicate"+    }++-- | A Template Haskell splice that acts like 'is', but receives a quoted+-- expression at compile time and has a more helpful explanation.+--+-- >>> accept $(qIs [| even |]) 3+-- False+-- >>> accept $(qIs [| even |]) 4+-- True+--+-- >>> show $(qIs [| even |])+-- "even"+qIs :: HasCallStack => ExpQ -> ExpQ+qIs p =+  [|+    Predicate+      { showPredicate = $description,+        showNegation = "not " ++ $description,+        accept = $p,+        explain = \x -> if $p x then $description else "not " ++ $description+      }+    |]+  where+    description = lift . pprint . removeModNames =<< p++-- | A combinator to lift a 'Predicate' to work on a property or computed value+-- of the original value.  The explanations are less helpful that standard+-- predicates like 'sizeIs'.  You can use 'qWith' instead to get better+-- explanations using Template Haskell.+--+-- >>> accept (with abs (gt 5)) (-6)+-- True+-- >>> accept (with abs (gt 5)) (-5)+-- False+-- >>> accept (with reverse (eq "olleh")) "hello"+-- True+-- >>> accept (with reverse (eq "olleh")) "goodbye"+-- False+with :: HasCallStack => (a -> b) -> Predicate b -> Predicate a+with f p =+  Predicate+    { showPredicate = prop ++ ": " ++ show p,+      showNegation = prop ++ ": " ++ showNegation p,+      accept = accept p . f,+      explain = ((prop ++ ": ") ++) . explain p . f+    }+  where+    prop = withLoc (locate callStack "property")++-- | Use 'with' or 'qWith' instead of 'contramap' to get better explanations.+instance Contravariant Predicate where+  contramap f p =+    Predicate+      { showPredicate = "in a property: " ++ show p,+        showNegation = "in a property: " ++ showNegation p,+        accept = accept p . f,+        explain = ("in a property: " ++) . explain p . f+      }++-- | A Template Haskell splice that acts like 'with', but receives a quoted+-- typed expression at compile time and has a more helpful explanation.+--+-- >>> accept ($(qWith [| abs |]) (gt 5)) (-6)+-- True+-- >>> accept ($(qWith [| abs |]) (gt 5)) (-5)+-- False+-- >>> accept ($(qWith [| reverse |]) (eq "olleh")) "hello"+-- True+-- >>> accept ($(qWith [| reverse |]) (eq "olleh")) "goodbye"+-- False+--+-- >>> show ($(qWith [| abs |]) (gt 5))+-- "abs: > 5"+qWith :: ExpQ -> ExpQ+qWith f =+  [|+    \p ->+      Predicate+        { showPredicate = $prop ++ ": " ++ show p,+          showNegation = $prop ++ ": " ++ showNegation p,+          accept = accept p . $f,+          explain = (($prop ++ ": ") ++) . explain p . $f+        }+    |]+  where+    prop = lift . pprint . removeModNames =<< f++-- | A 'Predicate' that accepts values with a given nested value.  This is+-- intended to match constructors with arguments.  You can use 'qADT' instead+-- to get better explanations using Template Haskell.+--+-- >>> accept (inBranch "Left" (\case {Left x -> Just x; _ -> Nothing}) positive) (Left 1)+-- True+-- >>> accept (inBranch "Left" (\case {Left x -> Just x; _ -> Nothing}) positive) (Left 0)+-- False+-- >>> accept (inBranch "Left" (\case {Left x -> Just x; _ -> Nothing}) positive) (Right 1)+-- False+inBranch :: String -> (a -> Maybe b) -> Predicate b -> Predicate a+inBranch name f p =+  Predicate+    { showPredicate = "(" ++ name ++ " _)",+      showNegation = "not (" ++ name ++ " _)",+      accept = \x -> case f x of Just y -> accept p y; _ -> False,+      explain = \x -> case f x of+        Just y -> "In " ++ name ++ ": " ++ explain p y+        _ -> "Branch didn't match"+    }++-- | A Template Haskell splice which, given a constructor for an abstract data+-- type, writes a 'Predicate' that matches on that constructor and applies other+-- 'Predicate's to its fields.+--+-- >>> accept $(qADT 'Nothing) Nothing+-- True+-- >>> accept $(qADT 'Nothing) (Just 5)+-- False+-- >>> accept ($(qADT 'Just) positive) (Just 5)+-- True+-- >>> accept ($(qADT 'Just) positive) Nothing+-- False+-- >>> accept ($(qADT 'Just) positive) (Just 0)+-- False+qADT :: Name -> ExpQ+qADT conName =+  do+    let prettyConName = lift (pprint (removeModNames conName))+    t <- reify conName >>= (\case+       DataConI _ ty _ -> pure ty+       PatSynI _ ty -> pure ty+       _ -> fail $ "qADT: " ++ show conName ++ " is not a data constructor")++    let n = countArguments t+    subpreds <- replicateM n (newName "p")+    let subdescs =+          map+            (\p -> [|"(" ++ showPredicate $p ++ ")"|])+            (varE <$> subpreds)+    let desc = [|unwords ($prettyConName : $(listE subdescs))|]+    let negDesc+          | n == 0 = [|"≠ " ++ $desc|]+          | otherwise = [|"not (" ++ $desc ++ ")"|]+    args <- replicateM n (newName "x")+    let pattern = conP conName (varP <$> args)+    let acceptExplainFields =+          listE $+            zipWith+              (\p x -> [|(accept $p $x, explain $p $x)|])+              (varE <$> subpreds)+              (varE <$> args)+    y <- newName "y"+    lamE+      (varP <$> subpreds)+      [|+        let acceptAndExplain $(varP y) = case $(varE y) of+              $pattern -> Just $acceptExplainFields+              _ -> Nothing+         in Predicate+              { showPredicate = $desc,+                showNegation = $negDesc,+                accept = maybe False (all fst) . acceptAndExplain,+                explain = \x -> case acceptAndExplain x of+                  Nothing -> "Not a " ++ $prettyConName+                  Just results ->+                    let significant+                          | all fst results = results+                          | otherwise = filter (not . fst) results+                     in "In " ++ $prettyConName ++ ": "+                          ++ intercalate " and " (map snd significant)+              }+        |]+  where+    countArguments (ForallT _ _ t) = countArguments t+    countArguments (AppT (AppT ArrowT _) t) = countArguments t + 1+#if MIN_VERSION_template_haskell(2,17,0)+    countArguments (AppT (AppT (AppT MulArrowT _) _) t) = countArguments t + 1+#endif+    countArguments _ = 0++-- | A Template Haskell splice that turns a quoted pattern into a predicate that+-- accepts values that match the pattern.+--+-- >>> accept $(qMatch [p| Just (Left _) |]) Nothing+-- False+-- >>> accept $(qMatch [p| Just (Left _) |]) (Just (Left 5))+-- True+-- >>> accept $(qMatch [p| Just (Left _) |]) (Just (Right 5))+-- False+--+-- >>> show $(qMatch [p| Just (Left _) |])+-- "Just (Left _)"+qMatch :: PatQ -> ExpQ+qMatch qpat =+  [|+    Predicate+      { showPredicate = $patString,+        showNegation = "not " ++ $patString,+        accept = \case+          $qpat -> True+          _ -> False,+        explain = \case+          $qpat -> "value matched " ++ $patString+          _ -> "value didn't match " ++ $patString+      }+    |]+  where+    patString = lift . pprint . removeModNames =<< qpat++-- | Converts a 'Predicate' to a new type.  Typically used with visible type+-- application, as in the examples below.+--+-- >>> accept (typed @String anything) "foo"+-- True+-- >>> accept (typed @String (sizeIs (gt 5))) "foo"+-- False+-- >>> accept (typed @String anything) (42 :: Int)+-- False+typed :: forall a b. (Typeable a, Typeable b) => Predicate a -> Predicate b+typed p =+  Predicate+    { showPredicate =+        showPredicate p ++ " :: " ++ show (typeRep (Proxy :: Proxy a)),+      showNegation =+        "not " ++ showPredicate p ++ " :: "+          ++ show (typeRep (Proxy :: Proxy a)),+      accept = \x -> case cast x of+        Nothing -> False+        Just y -> accept p y,+      explain = \x -> case cast x of+        Nothing ->+          "wrong type ("+            ++ show (typeRep (undefined :: Proxy b))+            ++ " vs. "+            ++ show (typeRep (undefined :: Proxy a))+            ++ ")"+        Just y -> explain p y+    }
+ test/Test/Predicates/Internal/FlowMatcher.hs view
@@ -0,0 +1,95 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | An implementation of bipartite matching using the Ford-Fulkerson algorithm.+module Test.Predicates.Internal.FlowMatcher where++import Control.Monad (forM_, when)+import Control.Monad.ST (ST)+import Data.Array.IArray (Array, assocs, elems)+import Data.Array.ST+  ( MArray (newArray),+    STArray,+    newListArray,+    readArray,+    runSTArray,+    writeArray,+  )+import Data.List ((\\))+import Data.Maybe (catMaybes)++-- $setup+-- >>> :set -Wno-type-defaults++-- | Computes the best bipartite matching of the elements in the two lists,+-- given the compatibility function.+--+-- Returns matched pairs, then unmatched lhs elements, then unmatched rhs+-- elements.+--+-- >>> bipartiteMatching (==) [1 .. 5] [6, 5 .. 2]+-- ([(2,2),(3,3),(4,4),(5,5)],[1],[6])+bipartiteMatching ::+  forall a b. (a -> b -> Bool) -> [a] -> [b] -> ([(a, b)], [a], [b])+bipartiteMatching compatible xs ys = (matchedPairs, unmatchedX, unmatchedY)+  where+    matchedPairs :: [(a, b)]+    matchedPairs = [(xs !! i, ys !! j) | (i, Just j) <- assocs matches]++    unmatchedX :: [a]+    unmatchedX = [xs !! i | (i, Nothing) <- assocs matches]++    unmatchedY :: [b]+    unmatchedY = [ys !! j | j <- [0 .. numYs - 1] \\ catMaybes (elems matches)]++    matches :: Array Int (Maybe Int)+    matches = runSTArray st++    st :: forall s. ST s (STArray s Int (Maybe Int))+    st = do+      compatArray <-+        newListArray+          ((0, 0), (numXs - 1, numYs - 1))+          [compatible x y | x <- xs, y <- ys] ::+          ST s (STArray s (Int, Int) Bool)+      matchArray <-+        newArray (0, numXs - 1) Nothing ::+          ST s (STArray s Int (Maybe Int))+      forM_ [0 .. numYs - 1] $ \j -> do+        seen <-+          newArray (0, numXs - 1) False :: ST s (STArray s Int Bool)+        _ <- go compatArray j matchArray seen+        return ()++      return matchArray++    numXs, numYs :: Int+    numXs = length xs+    numYs = length ys++    go ::+      forall s.+      STArray s (Int, Int) Bool ->+      Int ->+      STArray s Int (Maybe Int) ->+      STArray s Int Bool ->+      ST s Bool+    go compatArray j matchArray seen = loop False 0+      where+        loop True _ = return True+        loop _ i+          | i == numXs = return False+          | otherwise = do+            compat <- readArray compatArray (i, j)+            isSeen <- readArray seen i+            replace <-+              if isSeen || not compat+                then return False+                else do+                  writeArray seen i True+                  matchNum <- readArray matchArray i+                  case matchNum of+                    Nothing -> return True+                    Just n -> go compatArray n matchArray seen+            when replace $ writeArray matchArray i (Just j)+            loop replace (i + 1)
+ test/Test/Predicates/Internal/Util.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE FlexibleContexts #-}++-- | Internal utilities used for HMock implementation.+module Test.Predicates.Internal.Util where++import Data.Generics (Data, everywhere, mkT)+import GHC.Stack (CallStack, getCallStack, prettySrcLoc)+import Language.Haskell.TH.Syntax (NameFlavour (..))++#ifdef CONTAINERS+import Data.MonoTraversable (Element)+import qualified Data.Sequences as Seq+#endif++-- | A value together with its source location.+data Located a = Loc (Maybe String) a deriving (Functor)++-- | Annotates a value with its source location from the call stack.+locate :: CallStack -> a -> Located a+locate stack = case map snd (getCallStack stack) of+  (loc : _) -> Loc (Just (prettySrcLoc loc))+  _ -> Loc Nothing++-- | Formats a 'Located' 'String' to include its source location.+withLoc :: Located String -> String+withLoc (Loc Nothing s) = s+withLoc (Loc (Just loc) s) = s ++ " at " ++ loc++-- | Returns all ways to choose one element from a list, and the corresponding+-- remaining list.+choices :: [a] -> [(a, [a])]+choices [] = []+choices (x : xs) = (x, xs) : (fmap (x :) <$> choices xs)++#ifdef CONTAINERS++-- | Checks if one sequence is a subsequence of another.+isSubsequenceOf :: (Seq.IsSequence t, Eq (Element t)) => t -> t -> Bool+xs `isSubsequenceOf` ys = case Seq.uncons xs of+  Nothing -> True+  Just (x, xs') -> case Seq.uncons (snd (Seq.break (== x) ys)) of+    Nothing -> False+    Just (_, ys') -> xs' `isSubsequenceOf` ys'++#endif++-- | Removes all module names from Template Haskell names in the given value, so+-- that it will pretty-print more cleanly.+removeModNames :: Data a => a -> a+removeModNames = everywhere (mkT unMod)+  where+    unMod NameG {} = NameS+    unMod other = other