2018-08-18 21:26:07 +03:00
|
|
|
{-# LANGUAGE CPP #-}
|
2018-08-15 21:43:29 +03:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
|
|
|
2018-08-18 09:33:41 +03:00
|
|
|
module Hledger.Utils.Test (
|
|
|
|
-- * easytest
|
2018-08-18 21:47:52 +03:00
|
|
|
module EasyTest
|
2018-08-18 09:33:41 +03:00
|
|
|
,runEasyTests
|
2018-08-18 21:47:52 +03:00
|
|
|
,tests
|
2018-08-18 09:33:41 +03:00
|
|
|
,_tests
|
|
|
|
,test
|
|
|
|
,_test
|
|
|
|
,it
|
|
|
|
,_it
|
|
|
|
,expectParseEq
|
2018-08-19 21:01:20 +03:00
|
|
|
,expectParseEqOn
|
2018-08-18 09:33:41 +03:00
|
|
|
-- * HUnit
|
2018-08-18 21:47:52 +03:00
|
|
|
,module Test.HUnit
|
2018-08-18 09:33:41 +03:00
|
|
|
,runHunitTests
|
|
|
|
,assertParse
|
|
|
|
,assertParseFailure
|
|
|
|
,assertParseEqual
|
|
|
|
,assertParseEqual'
|
|
|
|
,is
|
|
|
|
|
|
|
|
) where
|
2015-08-19 23:47:26 +03:00
|
|
|
|
2018-08-15 21:43:29 +03:00
|
|
|
import Control.Exception
|
|
|
|
import Control.Monad
|
2018-08-17 14:42:05 +03:00
|
|
|
import Control.Monad.State.Strict (StateT, evalStateT)
|
2018-08-18 21:26:07 +03:00
|
|
|
#if !(MIN_VERSION_base(4,11,0))
|
|
|
|
import Data.Monoid ((<>))
|
|
|
|
#endif
|
2018-08-16 08:16:09 +03:00
|
|
|
import Data.CallStack
|
2018-08-04 18:34:42 +03:00
|
|
|
import Data.Functor.Identity
|
2018-08-15 21:43:29 +03:00
|
|
|
import Data.List
|
|
|
|
import qualified Data.Text as T
|
|
|
|
import Safe
|
|
|
|
import System.Exit
|
|
|
|
import System.IO
|
2016-07-29 18:57:10 +03:00
|
|
|
import Text.Megaparsec
|
2018-08-16 07:57:43 +03:00
|
|
|
import Text.Megaparsec.Custom
|
2018-08-15 21:43:29 +03:00
|
|
|
|
2018-08-18 21:47:52 +03:00
|
|
|
import EasyTest hiding (char, char', tests) -- reexported
|
|
|
|
import qualified EasyTest as E -- used here
|
|
|
|
import Test.HUnit hiding (Test, test) -- reexported
|
|
|
|
import qualified Test.HUnit as U -- used here
|
2018-08-18 09:33:41 +03:00
|
|
|
|
2018-08-15 13:11:38 +03:00
|
|
|
import Hledger.Utils.Debug (pshow)
|
2018-08-15 21:43:29 +03:00
|
|
|
import Hledger.Utils.UTF8IOCompat (error')
|
2015-08-19 23:47:26 +03:00
|
|
|
|
2018-08-18 09:33:41 +03:00
|
|
|
-- * easytest helpers
|
|
|
|
|
|
|
|
-- | Name the given test(s). A readability synonym for easytest's "scope".
|
|
|
|
test :: T.Text -> E.Test a -> E.Test a
|
|
|
|
test = E.scope
|
|
|
|
|
|
|
|
-- | Skip the given test(s), with the same type signature as "test".
|
|
|
|
_test :: T.Text -> E.Test a -> E.Test a
|
|
|
|
_test _name = (E.skip >>)
|
|
|
|
|
|
|
|
-- | Name the given test(s). A synonym for "test".
|
|
|
|
it :: T.Text -> E.Test a -> E.Test a
|
|
|
|
it = test
|
|
|
|
|
|
|
|
-- | Skip the given test(s). A synonym for "_test".
|
|
|
|
_it :: T.Text -> E.Test a -> E.Test a
|
|
|
|
_it = _test
|
|
|
|
|
|
|
|
-- | Name and group a list of tests. Combines easytest's "scope" and "tests".
|
|
|
|
tests :: T.Text -> [E.Test ()] -> E.Test ()
|
2018-08-18 21:47:52 +03:00
|
|
|
tests name = E.scope name . E.tests
|
2018-08-18 09:33:41 +03:00
|
|
|
|
|
|
|
-- | Skip the given list of tests, with the same type signature as "group".
|
|
|
|
_tests :: T.Text -> [E.Test ()] -> E.Test ()
|
2018-08-18 21:47:52 +03:00
|
|
|
_tests _name = (E.skip >>) . E.tests
|
2018-08-18 09:33:41 +03:00
|
|
|
|
|
|
|
-- | Run some easytests, returning True if there was a problem. Catches ExitCode.
|
|
|
|
-- With arguments, runs only tests in the scope named by the first argument
|
|
|
|
-- (case sensitive).
|
|
|
|
-- If there is a second argument, it should be an integer and will be used
|
|
|
|
-- as the seed for randomness.
|
|
|
|
runEasyTests :: [String] -> E.Test () -> IO Bool
|
|
|
|
runEasyTests args easytests = (do
|
|
|
|
case args of
|
|
|
|
[] -> E.run easytests
|
|
|
|
[a] -> E.runOnly (T.pack a) easytests
|
|
|
|
a:b:_ -> do
|
|
|
|
case readMay b :: Maybe Int of
|
|
|
|
Nothing -> error' "the second argument should be an integer (a seed for easytest)"
|
|
|
|
Just seed -> E.rerunOnly seed (T.pack a) easytests
|
|
|
|
return False
|
|
|
|
)
|
|
|
|
`catch` (\(_::ExitCode) -> return True)
|
|
|
|
|
2018-08-19 21:01:20 +03:00
|
|
|
-- | Like easytest's expectEq, but pretty-prints the values in the failure output.
|
2018-08-18 09:33:41 +03:00
|
|
|
expectEq' :: (Eq a, Show a, HasCallStack) => a -> a -> E.Test ()
|
|
|
|
expectEq' x y = if x == y then E.ok else E.crash $
|
|
|
|
"expected:\n" <> T.pack (pshow x) <> "\nbut got:\n" <> T.pack (pshow y) <> "\n"
|
|
|
|
|
2018-08-20 10:25:03 +03:00
|
|
|
-- | Given a stateful parser runnable in IO, input text, and an
|
2018-08-19 21:01:20 +03:00
|
|
|
-- expected parse result, make a Test that parses the text and compares
|
|
|
|
-- the result, showing a nice failure message if either step fails.
|
|
|
|
expectParseEq :: (Monoid st, Eq a, Show a, HasCallStack) =>
|
|
|
|
StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> a -> E.Test ()
|
2018-08-20 10:25:03 +03:00
|
|
|
expectParseEq parser input expected = expectParseEqOn parser input id expected
|
2018-08-19 21:01:20 +03:00
|
|
|
|
2018-08-20 10:25:03 +03:00
|
|
|
-- | Like expectParseEq, but also takes a transform function
|
|
|
|
-- to call on the parse result before comparing it.
|
2018-08-19 21:01:20 +03:00
|
|
|
expectParseEqOn :: (Monoid st, Eq b, Show b, HasCallStack) =>
|
|
|
|
StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> (a -> b) -> b -> E.Test ()
|
2018-08-20 10:25:03 +03:00
|
|
|
expectParseEqOn parser input f expected = do
|
2018-08-19 21:01:20 +03:00
|
|
|
ep <- E.io $ runParserT (evalStateT parser mempty) "" input
|
|
|
|
either (fail.("parse error at "++).parseErrorPretty) (expectEq' expected . f) ep
|
|
|
|
|
2018-08-17 15:38:58 +03:00
|
|
|
-- * HUnit helpers
|
|
|
|
|
2015-08-19 23:47:26 +03:00
|
|
|
-- | Get a Test's label, or the empty string.
|
2018-08-18 09:33:41 +03:00
|
|
|
testName :: U.Test -> String
|
2015-08-19 23:47:26 +03:00
|
|
|
testName (TestLabel n _) = n
|
|
|
|
testName _ = ""
|
|
|
|
|
|
|
|
-- | Flatten a Test containing TestLists into a list of single tests.
|
2018-08-18 09:33:41 +03:00
|
|
|
flattenTests :: U.Test -> [U.Test]
|
2015-08-19 23:47:26 +03:00
|
|
|
flattenTests (TestLabel _ t@(TestList _)) = flattenTests t
|
|
|
|
flattenTests (TestList ts) = concatMap flattenTests ts
|
|
|
|
flattenTests t = [t]
|
|
|
|
|
|
|
|
-- | Filter TestLists in a Test, recursively, preserving the structure.
|
2018-08-18 09:33:41 +03:00
|
|
|
filterTests :: (U.Test -> Bool) -> U.Test -> U.Test
|
2015-08-19 23:47:26 +03:00
|
|
|
filterTests p (TestLabel l ts) = TestLabel l (filterTests p ts)
|
|
|
|
filterTests p (TestList ts) = TestList $ filter (any p . flattenTests) $ map (filterTests p) ts
|
|
|
|
filterTests _ t = t
|
|
|
|
|
|
|
|
-- | Simple way to assert something is some expected value, with no label.
|
|
|
|
is :: (Eq a, Show a) => a -> a -> Assertion
|
2018-08-04 18:34:42 +03:00
|
|
|
a `is` e = assertEqual "" e a -- XXX should it have a message ?
|
2015-08-19 23:47:26 +03:00
|
|
|
|
|
|
|
-- | Assert a parse result is successful, printing the parse error on failure.
|
2016-07-29 18:57:10 +03:00
|
|
|
assertParse :: (Show t, Show e) => (Either (ParseError t e) a) -> Assertion
|
2015-08-19 23:47:26 +03:00
|
|
|
assertParse parse = either (assertFailure.show) (const (return ())) parse
|
|
|
|
|
2016-07-29 18:57:10 +03:00
|
|
|
|
2015-08-19 23:47:26 +03:00
|
|
|
-- | Assert a parse result is successful, printing the parse error on failure.
|
2016-07-29 18:57:10 +03:00
|
|
|
assertParseFailure :: (Either (ParseError t e) a) -> Assertion
|
2015-08-19 23:47:26 +03:00
|
|
|
assertParseFailure parse = either (const $ return ()) (const $ assertFailure "parse should not have succeeded") parse
|
|
|
|
|
|
|
|
-- | Assert a parse result is some expected value, printing the parse error on failure.
|
2016-07-29 18:57:10 +03:00
|
|
|
assertParseEqual :: (Show a, Eq a, Show t, Show e) => (Either (ParseError t e) a) -> a -> Assertion
|
2015-08-19 23:47:26 +03:00
|
|
|
assertParseEqual parse expected = either (assertFailure.show) (`is` expected) parse
|
|
|
|
|
2018-08-15 13:11:38 +03:00
|
|
|
-- | Assert that the parse result returned from an identity monad is some expected value,
|
|
|
|
-- on failure printing the parse error or differing values.
|
2018-08-04 18:34:42 +03:00
|
|
|
assertParseEqual' :: (Show a, Eq a, Show t, Show e) => Identity (Either (ParseError t e) a) -> a -> Assertion
|
2018-08-15 13:11:38 +03:00
|
|
|
assertParseEqual' parse expected =
|
|
|
|
either
|
|
|
|
(assertFailure . ("parse error: "++) . pshow)
|
|
|
|
(\actual -> assertEqual (unlines ["expected: " ++ show expected, " but got: " ++ show actual]) expected actual)
|
|
|
|
$ runIdentity parse
|
|
|
|
|
2018-08-18 09:33:41 +03:00
|
|
|
---- | Labelled version of assertParseEqual'.
|
|
|
|
--assertParseEqual'' :: (Show a, Eq a, Show t, Show e) => String -> Identity (Either (ParseError t e) a) -> a -> Assertion
|
|
|
|
--assertParseEqual'' label parse expected =
|
|
|
|
-- either
|
|
|
|
-- (assertFailure . ("parse error: "++) . pshow)
|
|
|
|
-- (\actual -> assertEqual (unlines [label, "expected: " ++ show expected, " but got: " ++ show actual]) expected actual)
|
|
|
|
-- $ runIdentity parse
|
2018-08-04 18:34:42 +03:00
|
|
|
|
2018-08-15 21:43:29 +03:00
|
|
|
-- | Run some hunit tests, returning True if there was a problem.
|
|
|
|
-- With arguments, runs only tests whose names contain the first argument
|
|
|
|
-- (case sensitive).
|
2018-08-18 09:33:41 +03:00
|
|
|
runHunitTests :: [String] -> U.Test -> IO Bool
|
2018-08-15 21:43:29 +03:00
|
|
|
runHunitTests args hunittests = do
|
|
|
|
let ts =
|
|
|
|
(case args of
|
|
|
|
a:_ -> filterTests ((a `isInfixOf`) . testName)
|
|
|
|
_ -> id
|
|
|
|
) hunittests
|
|
|
|
results <- liftM (fst . flip (,) 0) $ runTestTTStdout ts
|
|
|
|
return $ errors results > 0 || failures results > 0
|
|
|
|
where
|
|
|
|
-- | Like runTestTT but prints to stdout.
|
|
|
|
runTestTTStdout t = do
|
2018-08-18 09:33:41 +03:00
|
|
|
(counts, 0) <- U.runTestText (putTextToHandle stdout True) t
|
2018-08-15 21:43:29 +03:00
|
|
|
return counts
|
|
|
|
|
|
|
|
-- matchedTests opts ts
|
|
|
|
-- | tree_ $ reportopts_ opts =
|
|
|
|
-- -- Tests, filtered by any arguments, in a flat list with simple names.
|
|
|
|
-- TestList $
|
|
|
|
-- filter (matchesAccount (queryFromOpts nulldate $ reportopts_ opts) . T.pack . testName) $
|
|
|
|
-- flattenTests ts
|
|
|
|
-- | otherwise =
|
|
|
|
-- -- Tests, filtered by any arguments, in the original suites with hierarchical names.
|
|
|
|
-- filterTests (matchesAccount (queryFromOpts nulldate $ reportopts_ opts) . T.pack . testName)
|
|
|
|
-- ts
|
|
|
|
|
|
|
|
-- -- | Like runTestTT but can optionally not erase progress output.
|
|
|
|
-- runTestTT' verbose t = do
|
|
|
|
-- (counts, 0) <- runTestText' (f stderr True) t
|
|
|
|
-- return counts
|
|
|
|
-- where f | verbose = putTextToHandle'
|
|
|
|
-- | otherwise = putTextToHandle
|
|
|
|
|
|
|
|
-- -- | Like runTestText but also prints test names if any.
|
|
|
|
-- runTestText' :: PutText st -> Test -> IO (Counts, st)
|
|
|
|
-- runTestText' _pt _t@(TestLabel _label _) = error "HERE" -- hPutStrLn stderr label >> runTestText pt t
|
|
|
|
-- runTestText' pt t = runTestText pt t
|
|
|
|
|
|
|
|
-- -- runTestText' (PutText put us0) t = do
|
|
|
|
-- -- (counts', us1) <- trace "XXX" $ performTest reportStart reportError reportFailure us0 t
|
|
|
|
-- -- us2 <- put (showCounts counts' ++ " :::: " ++ testName t) True us1
|
|
|
|
-- -- return (counts', us2)
|
|
|
|
-- -- where
|
|
|
|
-- -- reportStart ss us = put (showCounts (counts ss)) False us
|
|
|
|
-- -- reportError = reportProblem "Error:" "Error in: "
|
|
|
|
-- -- reportFailure = reportProblem "Failure:" "Failure in: "
|
|
|
|
-- -- reportProblem p0 p1 loc msg ss us = put line True us
|
|
|
|
-- -- where line = "### " ++ kind ++ path' ++ "\n" ++ formatLocation loc ++ msg
|
|
|
|
-- -- kind = if null path' then p0 else p1
|
|
|
|
-- -- path' = showPath (path ss)
|
|
|
|
|
|
|
|
-- -- formatLocation :: Maybe SrcLoc -> String
|
|
|
|
-- -- formatLocation Nothing = ""
|
|
|
|
-- -- formatLocation (Just loc) = srcLocFile loc ++ ":" ++ show (srcLocStartLine loc) ++ "\n"
|
|
|
|
|
|
|
|
-- -- | Like putTextToHandle but does not erase progress lines.
|
|
|
|
-- putTextToHandle'
|
|
|
|
-- :: Handle
|
|
|
|
-- -> Bool -- ^ Write progress lines to handle?
|
|
|
|
-- -> PutText Int
|
|
|
|
-- putTextToHandle' handle showProgress = PutText put initCnt
|
|
|
|
-- where
|
|
|
|
-- initCnt = if showProgress then 0 else -1
|
|
|
|
-- put line pers (-1) = do when pers (hPutStrLn handle line); return (-1)
|
|
|
|
-- put line True cnt = do hPutStrLn handle (erase cnt ++ line); return 0
|
|
|
|
-- put line False _ = do hPutStr handle ('\n' : line); return (length line)
|
|
|
|
-- -- The "erasing" strategy with a single '\r' relies on the fact that the
|
|
|
|
-- -- lengths of successive summary lines are monotonically nondecreasing.
|
|
|
|
-- erase cnt = if cnt == 0 then "" else "\r" ++ replicate cnt ' ' ++ "\r"
|