233 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Haskell
		
	
	
	
	
	
			
		
		
	
	
			233 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Haskell
		
	
	
	
	
	
| {-# LANGUAGE CPP #-}
 | |
| {-# LANGUAGE FlexibleContexts #-}
 | |
| {-# LANGUAGE OverloadedStrings #-}
 | |
| {-# LANGUAGE ScopedTypeVariables #-}
 | |
| 
 | |
| module Hledger.Utils.Test (
 | |
|    HasCallStack
 | |
|   ,module EasyTest
 | |
|   ,runEasytests
 | |
|   ,tests
 | |
|   ,_tests
 | |
|   ,test
 | |
|   ,_test
 | |
|   ,it
 | |
|   ,_it
 | |
|   ,is
 | |
|   ,expectEqPP
 | |
|   ,expectParse
 | |
|   ,expectParseE
 | |
|   ,expectParseError
 | |
|   ,expectParseErrorE
 | |
|   ,expectParseEq
 | |
|   ,expectParseEqE
 | |
|   ,expectParseEqOn
 | |
|   ,expectParseEqOnE
 | |
|   ,expectParseStateOn
 | |
| )
 | |
| where
 | |
| 
 | |
| import Control.Exception
 | |
| import Control.Monad.Except (ExceptT, runExceptT)
 | |
| import Control.Monad.State.Strict (StateT, evalStateT, execStateT)
 | |
| #if !(MIN_VERSION_base(4,11,0))
 | |
| import Data.Monoid ((<>))
 | |
| #endif
 | |
| import Data.CallStack
 | |
| import Data.List
 | |
| import qualified Data.Text as T
 | |
| import Safe
 | |
| import System.Exit
 | |
| import Text.Megaparsec
 | |
| import Text.Megaparsec.Custom
 | |
| 
 | |
| import EasyTest hiding (char, char', tests)  -- reexported
 | |
| import qualified EasyTest as E               -- used here
 | |
| 
 | |
| import Hledger.Utils.Debug (pshow)
 | |
| import Hledger.Utils.UTF8IOCompat (error')
 | |
| 
 | |
| -- * 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".
 | |
| -- If called in a monadic sequence of tests, also skips following tests.
 | |
| _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), and any following tests in a monadic sequence.
 | |
| -- 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 ()
 | |
| tests name = E.scope name . E.tests
 | |
| 
 | |
| -- | Skip the given list of tests, and any following tests in a monadic sequence,
 | |
| -- with the same type signature as "group".
 | |
| _tests :: T.Text -> [E.Test ()] -> E.Test ()
 | |
| _tests _name = (E.skip >>) . E.tests
 | |
| 
 | |
| -- | Run some easytest tests, catching easytest's ExitCode exception,
 | |
| -- returning True if there was a problem.
 | |
| -- With arguments, runs only the scope (or single test) named by the first argument
 | |
| -- (exact, 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 tests = (do
 | |
|   case args of
 | |
|     []    -> E.run tests
 | |
|     [a]   -> E.runOnly (T.pack a) tests
 | |
|     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) tests
 | |
|   return False
 | |
|   )
 | |
|   `catch` (\(_::ExitCode) -> return True)
 | |
| 
 | |
| -- | Like easytest's expectEq (asserts the second (actual) value equals the first (expected) value)
 | |
| -- but pretty-prints the values in the failure output.
 | |
| expectEqPP :: (Eq a, Show a, HasCallStack) => a -> a -> E.Test ()
 | |
| expectEqPP expected actual = if expected == actual then E.ok else E.crash $
 | |
|   "\nexpected:\n" <> T.pack (pshow expected) <> "\nbut got:\n" <> T.pack (pshow actual) <> "\n"
 | |
| 
 | |
| -- | Shorter and flipped version of expectEqPP. The expected value goes last.
 | |
| is :: (Eq a, Show a, HasCallStack) => a -> a -> Test ()
 | |
| is = flip expectEqPP
 | |
| 
 | |
| -- | Test that this stateful parser runnable in IO successfully parses
 | |
| -- all of the given input text, showing the parse error if it fails.
 | |
| -- Suitable for hledger's JournalParser parsers.
 | |
| expectParse :: (Monoid st, Eq a, Show a, HasCallStack) =>
 | |
|   StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> E.Test ()
 | |
| expectParse parser input = do
 | |
|   ep <- E.io (runParserT (evalStateT (parser <* eof) mempty) "" input)
 | |
|   either (fail.(++"\n").("\nparse error at "++).customErrorBundlePretty)
 | |
|          (const ok)
 | |
|          ep
 | |
| 
 | |
| -- Suitable for hledger's ErroringJournalParser parsers.
 | |
| expectParseE
 | |
|   :: (Monoid st, Eq a, Show a, HasCallStack)
 | |
|   => StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a
 | |
|   -> T.Text
 | |
|   -> E.Test ()
 | |
| expectParseE parser input = do
 | |
|   let filepath = ""
 | |
|   eep <- E.io $ runExceptT $
 | |
|            runParserT (evalStateT (parser <* eof) mempty) filepath input
 | |
|   case eep of
 | |
|     Left finalErr ->
 | |
|       let prettyErr = finalErrorBundlePretty $ attachSource filepath input finalErr
 | |
|       in  fail $ "parse error at " <> prettyErr
 | |
|     Right ep ->
 | |
|       either (fail.(++"\n").("\nparse error at "++).customErrorBundlePretty)
 | |
|              (const ok)
 | |
|              ep
 | |
| 
 | |
| -- | Test that this stateful parser runnable in IO fails to parse
 | |
| -- the given input text, with a parse error containing the given string.
 | |
| expectParseError :: (Monoid st, Eq a, Show a, HasCallStack) =>
 | |
|   StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> String -> E.Test ()
 | |
| expectParseError parser input errstr = do
 | |
|   ep <- E.io (runParserT (evalStateT parser mempty) "" input)
 | |
|   case ep of
 | |
|     Right v -> fail $ "\nparse succeeded unexpectedly, producing:\n" ++ pshow v ++ "\n"
 | |
|     Left e  -> do
 | |
|       let e' = customErrorBundlePretty e
 | |
|       if errstr `isInfixOf` e'
 | |
|       then ok
 | |
|       else fail $ "\nparse error is not as expected:\n" ++ e' ++ "\n"
 | |
| 
 | |
| expectParseErrorE
 | |
|   :: (Monoid st, Eq a, Show a, HasCallStack)
 | |
|   => StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a
 | |
|   -> T.Text
 | |
|   -> String
 | |
|   -> E.Test ()
 | |
| expectParseErrorE parser input errstr = do
 | |
|   let filepath = ""
 | |
|   eep <- E.io $ runExceptT $ runParserT (evalStateT parser mempty) filepath input
 | |
|   case eep of
 | |
|     Left finalErr -> do
 | |
|       let prettyErr = finalErrorBundlePretty $ attachSource filepath input finalErr
 | |
|       if errstr `isInfixOf` prettyErr
 | |
|       then ok
 | |
|       else fail $ "\nparse error is not as expected:\n" ++ prettyErr ++ "\n"
 | |
|     Right ep -> case ep of
 | |
|       Right v -> fail $ "\nparse succeeded unexpectedly, producing:\n" ++ pshow v ++ "\n"
 | |
|       Left e  -> do
 | |
|         let e' = customErrorBundlePretty e
 | |
|         if errstr `isInfixOf` e'
 | |
|         then ok
 | |
|         else fail $ "\nparse error is not as expected:\n" ++ e' ++ "\n"
 | |
| 
 | |
| -- | Like expectParse, but also test the parse result is an expected value,
 | |
| -- pretty-printing both if it fails.
 | |
| expectParseEq :: (Monoid st, Eq a, Show a, HasCallStack) =>
 | |
|   StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> a -> E.Test ()
 | |
| expectParseEq parser input expected = expectParseEqOn parser input id expected
 | |
| 
 | |
| expectParseEqE
 | |
|   :: (Monoid st, Eq a, Show a, HasCallStack)
 | |
|   => StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a
 | |
|   -> T.Text
 | |
|   -> a
 | |
|   -> E.Test ()
 | |
| expectParseEqE parser input expected = expectParseEqOnE parser input id expected
 | |
| 
 | |
| -- | Like expectParseEq, but transform the parse result with the given function
 | |
| -- before comparing it.
 | |
| expectParseEqOn :: (Monoid st, Eq b, Show b, HasCallStack) =>
 | |
|   StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> (a -> b) -> b -> E.Test ()
 | |
| expectParseEqOn parser input f expected = do
 | |
|   ep <- E.io $ runParserT (evalStateT (parser <* eof) mempty) "" input
 | |
|   either (fail . (++"\n") . ("\nparse error at "++) . customErrorBundlePretty)
 | |
|          (expectEqPP expected . f)
 | |
|          ep
 | |
| 
 | |
| expectParseEqOnE
 | |
|   :: (Monoid st, Eq b, Show b, HasCallStack)
 | |
|   => StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a
 | |
|   -> T.Text
 | |
|   -> (a -> b)
 | |
|   -> b
 | |
|   -> E.Test ()
 | |
| expectParseEqOnE parser input f expected = do
 | |
|   let filepath = ""
 | |
|   eep <- E.io $ runExceptT $
 | |
|            runParserT (evalStateT (parser <* eof) mempty) filepath input
 | |
|   case eep of
 | |
|     Left finalErr ->
 | |
|       let prettyErr = finalErrorBundlePretty $ attachSource filepath input finalErr
 | |
|       in  fail $ "parse error at " <> prettyErr
 | |
|     Right ep ->
 | |
|       either (fail . (++"\n") . ("\nparse error at "++) . customErrorBundlePretty)
 | |
|              (expectEqPP expected . f)
 | |
|              ep
 | |
| 
 | |
| -- | Run a stateful parser in IO like expectParse, then compare the
 | |
| -- final state (the wrapped state, not megaparsec's internal state),
 | |
| -- transformed by the given function, with the given expected value.
 | |
| expectParseStateOn :: (HasCallStack, Monoid st, Eq b, Show b) =>
 | |
|      StateT st (ParsecT CustomErr T.Text IO) a
 | |
|   -> T.Text
 | |
|   -> (st -> b)
 | |
|   -> b
 | |
|   -> E.Test ()
 | |
| expectParseStateOn parser input f expected = do
 | |
|   es <- E.io $ runParserT (execStateT (parser <* eof) mempty) "" input
 | |
|   case es of
 | |
|     Left err -> fail $ (++"\n") $ ("\nparse error at "++) $ customErrorBundlePretty err
 | |
|     Right s  -> expectEqPP expected $ f s
 |