packages feed

gigaparsec-0.2.2.2: test/Text/Gigaparsec/Internal/Test.hs

-- A collection of test helpers
{-# LANGUAGE AllowAmbiguousTypes, RecordWildCards, StandaloneDeriving, DeriveAnyClass #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# HLINT ignore "Use newtype instead of data" #-}
module Text.Gigaparsec.Internal.Test where

import Test.Tasty.HUnit

import Text.Gigaparsec.Internal.TestError

import Text.Gigaparsec
import Text.Gigaparsec.Internal
import Text.Gigaparsec.Internal.RT

import Control.Exception (catches, catch, evaluate, Exception, SomeException(..), Handler(..), throwIO)
import Control.Monad (unless, forM_)
import Type.Reflection (typeOf, typeRep)
import Control.DeepSeq (rnf, NFData)

data LiftedState = Lifted State

testParse :: Parsec a -> String -> Result TestError a
testParse = parse @TestError

testParseAll :: Parsec a -> String -> Result TestError a
testParseAll p = testParse (p <* eof)

-- TODO: could we use quick-check to generate states?
-- | Tests to ensure that running the parser on the given string does nothing to the state
pureParseWith :: HasCallStack => Parsec a -> String -> Assertion
pureParseWith (Parsec p) inp = do
  run initSt
  run (initSt { consumed = 1 })
  run (initSt { line = 10, col = 20 })
  run (initSt { consumed = 200, line = 10, col = 20 })
  where initSt = emptyState inp
        run :: State -> Assertion
        run st = do
          let st' = runRT (p st (\ !_ s -> return (Lifted s)) (\ _ s -> return (Lifted s)))
          unless (Lifted st == st') $
            assertFailure ("expected no change to internal state\n"
                        ++ "initial state: " ++ show (Lifted st) ++ "\n       became: " ++ show st')

-- TODO: could we use quick-check to generate inputs?
-- | Tests to ensure that running the parser does nothing to the state
pureParse :: HasCallStack => Parsec a -> Assertion
pureParse p = do
  pureParseWith p ""
  pureParseWith p "a"
  pureParseWith p ":@279"

-- TODO: could we use quick-check to generate states?
-- | Tests to ensure that running the parser on the given string does something to the state
impureParseWith :: HasCallStack => Parsec a -> String -> Assertion
impureParseWith p inp = do
  run initSt
  --run (initSt { consumed = True })
  run (initSt { line = 10, col = 20 })
  --run (initSt { consumed = True, line = 10, col = 20 })
  where initSt = emptyState inp
        run :: State -> Assertion
        run st = do
          let st' = parseState p st
          assertBool (show (Lifted st) ++ " should be altered") (st' /= Lifted st)

-- TODO: could we use quick-check to generate inputs?
-- | Tests to ensure that running the parser does something to the state
impureParse :: HasCallStack => Parsec a -> Assertion
impureParse p = do
  impureParseWith p ""
  impureParseWith p "a"
  impureParseWith p ":@279"

consume :: a -> Parsec a
consume x = Parsec $ \st good _ -> good x (st { consumed = consumed st + 1})

ensureFails :: (Show a, HasCallStack) => Parsec a -> String -> Assertion
ensureFails p inp = case testParse p inp of
  Failure{} -> return ()
  Success x -> assertFailure ("parser must fail, but produced: " ++ show x)

throws :: forall e a. (HasCallStack, Exception e) => a -> Assertion
throws x = do
  catches (evaluate x >> assertFailure ("expected: " ++ show (typeRep @e)))
    [ Handler $ \ ((!_) :: e) -> return ()
    , Handler $ \ (ex :: HUnitFailure) -> throwIO ex
    , Handler $ \ (SomeException ex) -> assertFailure ("expected: " ++ show (typeRep @e) ++ "\n"
                                                    ++ " but got: " ++ show (typeOf ex))
    ]

notThrow :: NFData a => a -> Assertion
notThrow x = catch (evaluate (rnf x)) $ \(SomeException ex) ->
  assertFailure ("expected no exception but got " ++ show (typeOf ex))

-- TODO: we want result/error comparison later down the line
(~~) :: HasCallStack => Parsec a -> Parsec a -> [String] -> Assertion
(p ~~ q) inps =
  forM_ inps $ \inp -> do
    let st = emptyState inp
        pSt = parseState p st
        qSt = parseState q st
    unless (pSt == qSt) $
      assertFailure ("expected both parsers have the same effect on the state"
                  ++ "\ninitial state: " ++ show (Lifted st)
                  ++ "\n          got: " ++ show pSt
                  ++ "\n     expected: " ++ show qSt)

parseState :: Parsec a -> State -> LiftedState
parseState (Parsec p) st = runRT (p st (\ !_ st' -> return (Lifted st')) (\ _ st' -> return (Lifted st')))

deriving anyclass instance (NFData e, NFData a) => NFData (Result e a)

-- don't @ me
instance Eq LiftedState where
  (==) :: LiftedState -> LiftedState -> Bool
  Lifted (State input1 consumed1 line1 col1 _hintValidOffset1 _hints1 _debugLevel1) ==
    Lifted (State input2 consumed2 line2 col2 _hintValidOffset2 _hints2 _debugLevel2) =
       consumed1 == consumed2 && line1 == line2 && col1 == col2 && input1 == input2
    -- this throws off a whole bunch of tests, understandably
    -- && hintValidOffset1 == hintValidOffset2 && hints1 == hints2
instance Show LiftedState where
  showsPrec :: Int -> LiftedState -> ShowS
  showsPrec p (Lifted State{..}) = showParen (p > 10) $ showString "State { input = "
                                                      . shows input
                                                      . showString ", consumed = "
                                                      . shows consumed
                                                      . showString ", line = "
                                                      . shows line
                                                      . showString ", col = "
                                                      . shows col
                                                      . showString ", hintsValidOffset = "
                                                      . shows hintsValidOffset
                                                      -- . showString ", hints = "
                                                      -- . shows hints
                                                      . showChar '}'