{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Test.Tasty.Checklist
(
withChecklist
, CanCheck
, check
, discardCheck
, checkValues
, DerivedVal(Val, Got, Observe)
, CheckResult
, ChecklistFailures
, TestShow(testShow)
, testShowList
, multiLineDiff
)
where
import Control.Exception ( evaluate )
import Control.Monad ( join, unless )
import Control.Monad.Catch
import Control.Monad.IO.Class ( MonadIO, liftIO )
import Data.IORef
import qualified Data.List as List
import qualified Data.Parameterized.Context as Ctx
import Data.Text ( Text )
import qualified Data.Text as T
import System.IO ( hFlush, hPutStrLn, stdout, stderr )
data ChecklistFailures = ChecklistFailures Text [CheckResult]
data CheckResult = CheckFailed Text Text
instance Exception ChecklistFailures
instance Show CheckResult where
show :: CheckResult -> String
show (CheckFailed Text
what Text
msg) =
String
"Failed check of " forall a. Semigroup a => a -> a -> a
<> Text -> String
T.unpack Text
what forall a. Semigroup a => a -> a -> a
<> String
" with: " forall a. Semigroup a => a -> a -> a
<> Text -> String
T.unpack Text
msg
instance Show ChecklistFailures where
show :: ChecklistFailures -> String
show (ChecklistFailures Text
topMsg [CheckResult]
fails) =
String
"ERROR: " forall a. Semigroup a => a -> a -> a
<> Text -> String
T.unpack Text
topMsg forall a. Semigroup a => a -> a -> a
<> String
"\n " forall a. Semigroup a => a -> a -> a
<>
forall a. Show a => a -> String
show (forall (t :: * -> *) a. Foldable t => t a -> Int
length [CheckResult]
fails) forall a. Semigroup a => a -> a -> a
<> String
" checks failed in this checklist:\n -" forall a. Semigroup a => a -> a -> a
<>
forall a. [a] -> [[a]] -> [a]
List.intercalate String
"\n -" (forall a. Show a => a -> String
show forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [CheckResult]
fails)
type CanCheck = (?checker :: IORef [CheckResult])
withChecklist :: (MonadIO m, MonadMask m)
=> Text -> (CanCheck => m a) -> m a
withChecklist :: forall (m :: * -> *) a.
(MonadIO m, MonadMask m) =>
Text -> (CanCheck => m a) -> m a
withChecklist Text
topMsg CanCheck => m a
t = do
IORef [CheckResult]
checks <- forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$ forall a. a -> IO (IORef a)
newIORef forall a. Monoid a => a
mempty
a
r <- (let ?checker = IORef [CheckResult]
checks in CanCheck => m a
t)
forall (m :: * -> *) a b. MonadCatch m => m a -> m b -> m a
`onException` (forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$
do [CheckResult]
cs <- forall a. [a] -> [a]
List.reverse forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> forall a. IORef a -> IO a
readIORef IORef [CheckResult]
checks
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
unless (forall (t :: * -> *) a. Foldable t => t a -> Bool
null [CheckResult]
cs) forall a b. (a -> b) -> a -> b
$ do
Handle -> IO ()
hFlush Handle
stdout
Handle -> String -> IO ()
hPutStrLn Handle
stderr String
""
let pfx :: String
pfx = String
" WARN "
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ (Handle -> String -> IO ()
hPutStrLn Handle
stderr forall b c a. (b -> c) -> (a -> b) -> a -> c
. (String
pfx forall a. Semigroup a => a -> a -> a
<>) forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a. Show a => a -> String
show) [CheckResult]
cs
Handle -> IO ()
hFlush Handle
stderr
)
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$ do
[CheckResult]
collected <- forall a. [a] -> [a]
List.reverse forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> forall a. IORef a -> IO a
readIORef IORef [CheckResult]
checks
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
unless (forall (t :: * -> *) a. Foldable t => t a -> Bool
null [CheckResult]
collected) forall a b. (a -> b) -> a -> b
$
forall (m :: * -> *) e a. (MonadThrow m, Exception e) => e -> m a
throwM (Text -> [CheckResult] -> ChecklistFailures
ChecklistFailures Text
topMsg [CheckResult]
collected)
forall (m :: * -> *) a. Monad m => a -> m a
return a
r
check :: (CanCheck, TestShow a, MonadIO m)
=> Text -> (a -> Bool) -> a -> m ()
check :: forall a (m :: * -> *).
(CanCheck, TestShow a, MonadIO m) =>
Text -> (a -> Bool) -> a -> m ()
check = forall (m :: * -> *) a.
(CanCheck, MonadIO m) =>
(a -> String) -> Text -> (a -> Bool) -> a -> m ()
checkShow forall v. TestShow v => v -> String
testShow
checkShow :: (CanCheck, MonadIO m)
=> (a -> String) -> Text -> (a -> Bool) -> a -> m ()
checkShow :: forall (m :: * -> *) a.
(CanCheck, MonadIO m) =>
(a -> String) -> Text -> (a -> Bool) -> a -> m ()
checkShow a -> String
showit Text
what a -> Bool
eval a
val = do
Bool
r <- forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$ forall a. a -> IO a
evaluate (a -> Bool
eval a
val)
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
unless Bool
r forall a b. (a -> b) -> a -> b
$ do
let chk :: CheckResult
chk = Text -> Text -> CheckResult
CheckFailed Text
what forall a b. (a -> b) -> a -> b
$ String -> Text
T.pack forall a b. (a -> b) -> a -> b
$ a -> String
showit a
val
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$ forall a. IORef a -> (a -> a) -> IO ()
modifyIORef CanCheck
?checker (CheckResult
chkforall a. a -> [a] -> [a]
:)
discardCheck :: (CanCheck, MonadIO m) => Text -> m ()
discardCheck :: forall (m :: * -> *). (CanCheck, MonadIO m) => Text -> m ()
discardCheck Text
what = do
let isCheck :: Text -> CheckResult -> Bool
isCheck Text
n (CheckFailed Text
n' Text
_) = Text
n forall a. Eq a => a -> a -> Bool
== Text
n'
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$ forall a. IORef a -> (a -> a) -> IO ()
modifyIORef CanCheck
?checker (forall a. (a -> Bool) -> [a] -> [a]
filter (Bool -> Bool
not forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> CheckResult -> Bool
isCheck Text
what))
checkValues :: CanCheck
=> TestShow dType
=> dType -> Ctx.Assignment (DerivedVal dType) idx -> IO ()
checkValues :: forall dType (idx :: Ctx (*)).
(CanCheck, TestShow dType) =>
dType -> Assignment (DerivedVal dType) idx -> IO ()
checkValues dType
got Assignment (DerivedVal dType) idx
expF =
forall (m :: * -> *) a. Monad m => m (m a) -> m a
join forall a b. (a -> b) -> a -> b
$ forall a. a -> IO a
evaluate forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> forall {k} (m :: * -> *) (ctx :: Ctx k) (f :: k -> *).
Applicative m =>
(forall (tp :: k). Index ctx tp -> f tp -> m ())
-> Assignment f ctx -> m ()
Ctx.traverseWithIndex_ (forall dType (idx :: Ctx (*)) valType.
(CanCheck, TestShow dType) =>
dType -> Index idx valType -> DerivedVal dType valType -> IO ()
chkValue dType
got) Assignment (DerivedVal dType) idx
expF
chkValue :: CanCheck
=> TestShow dType
=> dType -> Ctx.Index idx valType -> DerivedVal dType valType -> IO ()
chkValue :: forall dType (idx :: Ctx (*)) valType.
(CanCheck, TestShow dType) =>
dType -> Index idx valType -> DerivedVal dType valType -> IO ()
chkValue dType
got Index idx valType
_idx = \case
(Val Text
txt dType -> valType
fld valType
v) ->
let r :: valType
r = dType -> valType
fld dType
got
msg :: Text
msg = Text
txt forall a. Semigroup a => a -> a -> a
<> Text
" on input <<" forall a. Semigroup a => a -> a -> a
<> Text
ti forall a. Semigroup a => a -> a -> a
<> Text
">>\n"
forall a. Semigroup a => a -> a -> a
<> Text
" " forall a. Semigroup a => a -> a -> a
<> Text
"expected: " forall a. Semigroup a => a -> a -> a
<> Text
tv forall a. Semigroup a => a -> a -> a
<> Text
"\n"
forall a. Semigroup a => a -> a -> a
<> Text
" " forall a. Semigroup a => a -> a -> a
<> Text
"failed"
ti :: Text
ti = String -> Text
T.pack (forall v. TestShow v => v -> String
testShow dType
got)
tv :: Text
tv = String -> Text
T.pack (forall v. TestShow v => v -> String
testShow valType
v)
in forall a (m :: * -> *).
(CanCheck, TestShow a, MonadIO m) =>
Text -> (a -> Bool) -> a -> m ()
check Text
msg (valType
v forall a. Eq a => a -> a -> Bool
==) valType
r
(Observe Text
txt dType -> valType
fld valType
v valType -> valType -> String
observationReport) ->
let r :: valType
r = dType -> valType
fld dType
got
msg :: Text
msg = Text
txt forall a. Semigroup a => a -> a -> a
<> Text
" observation failure"
in forall (m :: * -> *) a.
(CanCheck, MonadIO m) =>
(a -> String) -> Text -> (a -> Bool) -> a -> m ()
checkShow (valType -> valType -> String
observationReport valType
v) Text
msg (valType
v forall a. Eq a => a -> a -> Bool
==) valType
r
(Got Text
txt dType -> Bool
fld) ->
let r :: Bool
r = dType -> Bool
fld dType
got
msg :: Text
msg = Text
txt forall a. Semigroup a => a -> a -> a
<> Text
" on input <<" forall a. Semigroup a => a -> a -> a
<> Text
ti forall a. Semigroup a => a -> a -> a
<> Text
">>"
ti :: Text
ti = String -> Text
T.pack (forall v. TestShow v => v -> String
testShow dType
got)
in forall a (m :: * -> *).
(CanCheck, TestShow a, MonadIO m) =>
Text -> (a -> Bool) -> a -> m ()
check Text
msg (Bool
True forall a. Eq a => a -> a -> Bool
==) Bool
r
data DerivedVal i d where
Val :: (TestShow d, Eq d) => Text -> (i -> d) -> d -> DerivedVal i d
Got :: Text -> (i -> Bool) -> DerivedVal i Bool
Observe :: (Eq d) => Text -> (i -> d) -> d -> (d -> d -> String) -> DerivedVal i d
class TestShow v where
testShow :: v -> String
default testShow :: Show v => v -> String
testShow = forall a. Show a => a -> String
show
instance TestShow ()
instance TestShow Bool
instance TestShow Int
instance TestShow Integer
instance TestShow Float
instance TestShow Char
instance TestShow String
instance (TestShow a, TestShow b) => TestShow (a,b) where
testShow :: (a, b) -> String
testShow (a
a,b
b) = String
"(" forall a. Semigroup a => a -> a -> a
<> forall v. TestShow v => v -> String
testShow a
a forall a. Semigroup a => a -> a -> a
<> String
", " forall a. Semigroup a => a -> a -> a
<> forall v. TestShow v => v -> String
testShow b
b forall a. Semigroup a => a -> a -> a
<> String
")"
instance (TestShow a, TestShow b, TestShow c) => TestShow (a,b,c) where
testShow :: (a, b, c) -> String
testShow (a
a,b
b,c
c) = String
"(" forall a. Semigroup a => a -> a -> a
<> forall v. TestShow v => v -> String
testShow a
a forall a. Semigroup a => a -> a -> a
<> String
", " forall a. Semigroup a => a -> a -> a
<> forall v. TestShow v => v -> String
testShow b
b forall a. Semigroup a => a -> a -> a
<> String
", " forall a. Semigroup a => a -> a -> a
<> forall v. TestShow v => v -> String
testShow c
c forall a. Semigroup a => a -> a -> a
<> String
")"
testShowList :: TestShow v => [v] -> String
testShowList :: forall v. TestShow v => [v] -> String
testShowList [v]
l = String
"[ " forall a. Semigroup a => a -> a -> a
<> (forall a. [a] -> [[a]] -> [a]
List.intercalate String
", " (forall v. TestShow v => v -> String
testShow forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [v]
l)) forall a. Semigroup a => a -> a -> a
<> String
" ]"
multiLineDiff :: T.Text -> T.Text -> String
multiLineDiff :: Text -> Text -> String
multiLineDiff Text
expected Text
actual =
let dl :: (a, a) -> a
dl (a
e,a
a) = if a
e forall a. Eq a => a -> a -> Bool
== a
a then forall {a}. (Semigroup a, IsString a) => a -> a
db a
e else forall {a}. (Semigroup a, IsString a) => a -> a -> a
de a
" ↱" a
e forall a. Semigroup a => a -> a -> a
<> a
"\n " forall a. Semigroup a => a -> a -> a
<> forall {a}. (Semigroup a, IsString a) => a -> a -> a
da a
" ↳" a
a
db :: a -> a
db a
b = a
"| > " forall a. Semigroup a => a -> a -> a
<> a
b
de :: a -> a -> a
de a
m a
e = a
"|" forall a. Semigroup a => a -> a -> a
<> a
m forall a. Semigroup a => a -> a -> a
<> a
"expect> " forall a. Semigroup a => a -> a -> a
<> a
e
da :: a -> a -> a
da a
m a
a = a
"|" forall a. Semigroup a => a -> a -> a
<> a
m forall a. Semigroup a => a -> a -> a
<> a
"actual> " forall a. Semigroup a => a -> a -> a
<> a
a
el :: [Text]
el = Text -> Text
visible forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Text -> [Text]
T.lines Text
expected
al :: [Text]
al = Text -> Text
visible forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Text -> [Text]
T.lines Text
actual
visible :: Text -> Text
visible = Text -> Text -> Text -> Text
T.replace Text
" " Text
"␠"
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text -> Text -> Text
T.replace Text
"\n" Text
""
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text -> Text -> Text
T.replace Text
"\t" Text
"␉"
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text -> Text -> Text
T.replace Text
"\012" Text
"␍"
addnum :: Int -> T.Text -> T.Text
addnum :: Int -> Text -> Text
addnum Int
n Text
l = let nt :: Text
nt = String -> Text
T.pack (forall a. Show a => a -> String
show Int
n)
nl :: Int
nl = Text -> Int
T.length Text
nt
in Int -> Text -> Text
T.take (Int
4 forall a. Num a => a -> a -> a
- Int
nl) Text
" " forall a. Semigroup a => a -> a -> a
<> Text
nt forall a. Semigroup a => a -> a -> a
<> Text
l
ll :: [a] -> Text
ll = String -> Text
T.pack forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a. Show a => a -> String
show forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall (t :: * -> *) a. Foldable t => t a -> Int
length
tl :: Text -> Text
tl = String -> Text
T.pack forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a. Show a => a -> String
show forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Int
T.length
banner :: Text
banner = Text
"MISMATCH between "
forall a. Semigroup a => a -> a -> a
<> forall {a}. [a] -> Text
ll [Text]
el forall a. Semigroup a => a -> a -> a
<> Text
"l/" forall a. Semigroup a => a -> a -> a
<> Text -> Text
tl Text
expected forall a. Semigroup a => a -> a -> a
<> Text
"c expected and "
forall a. Semigroup a => a -> a -> a
<> forall {a}. [a] -> Text
ll [Text]
al forall a. Semigroup a => a -> a -> a
<> Text
"l/" forall a. Semigroup a => a -> a -> a
<> Text -> Text
tl Text
actual forall a. Semigroup a => a -> a -> a
<> Text
"c actual"
diffReport :: [Text]
diffReport = forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (forall a b c. (a -> b -> c) -> (a, b) -> c
uncurry Int -> Text -> Text
addnum) forall a b. (a -> b) -> a -> b
$
forall a b. [a] -> [b] -> [(a, b)]
zip [Int
1..] forall a b. (a -> b) -> a -> b
$ forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat forall a b. (a -> b) -> a -> b
$
[ forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap forall {a}. (Eq a, Semigroup a, IsString a) => (a, a) -> a
dl forall a b. (a -> b) -> a -> b
$ forall a b. [a] -> [b] -> [(a, b)]
zip [Text]
el [Text]
al
, forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (forall {a}. (Semigroup a, IsString a) => a -> a -> a
de Text
"∌ ") forall a b. (a -> b) -> a -> b
$ forall a. Int -> [a] -> [a]
drop (forall (t :: * -> *) a. Foldable t => t a -> Int
length [Text]
al) [Text]
el
, forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (forall {a}. (Semigroup a, IsString a) => a -> a -> a
da Text
"∹ ") forall a b. (a -> b) -> a -> b
$ forall a. Int -> [a] -> [a]
drop (forall (t :: * -> *) a. Foldable t => t a -> Int
length [Text]
el) [Text]
al
]
forall a. Semigroup a => a -> a -> a
<> if [Text]
el forall a. Eq a => a -> a -> Bool
== [Text]
al
then let maxlen :: Int
maxlen = forall a. Ord a => a -> a -> a
max (Text -> Int
T.length Text
expected) (Text -> Int
T.length Text
actual)
end :: Text -> Text
end Text
x = Int -> Text -> Text
T.drop (Int
maxlen forall a. Num a => a -> a -> a
- Int
5) Text
x
in [ [ forall {a}. (Semigroup a, IsString a) => a -> a -> a
de Text
"∌ ending " forall a b. (a -> b) -> a -> b
$ Text -> Text
visible forall a b. (a -> b) -> a -> b
$ Text -> Text
end Text
expected ]
, [ forall {a}. (Semigroup a, IsString a) => a -> a -> a
da Text
"∹ ending " forall a b. (a -> b) -> a -> b
$ Text -> Text
visible forall a b. (a -> b) -> a -> b
$ Text -> Text
end Text
actual ]
]
else forall a. Monoid a => a
mempty
details :: [Text]
details = Text
banner forall a. a -> [a] -> [a]
: [Text]
diffReport
in if Text
expected forall a. Eq a => a -> a -> Bool
== Text
actual then String
"<no difference>" else Text -> String
T.unpack ([Text] -> Text
T.unlines [Text]
details)