packages feed

parsable-test-0.1.0.0: src/Test/Parsable.hs

{-|
Module      : Test.Parsable

Test functions for 'Parsable' and 'Printable'.
-}

{-# Language CPP #-}
{-# Language DerivingVia #-}
{-# Language FlexibleContexts #-}
{-# Language LambdaCase #-}
{-# Language ScopedTypeVariables #-}
{-# Language TupleSections #-}
{-# Language TypeApplications #-}

module Test.Parsable
    (
      -- * Partial parses
      ParseCoverage(..)
      -- * QuickCheck
    , parsableQuickCheck
    , parsableProp
      -- ** Generators
    , wordGen
      -- * HUnit
    , parsableHUnit
    , printableHUnit
    , parsableAssertion
    , printableAssertion
      -- * Generic functions
    , runCheckParsable
    , checkParsable
    , checkCoverage
      -- * Re-exports
    , ParseError
    , module Control.Monad.STM
    , module Data.Void
    ) where

import Control.Monad.STM
import Control.Concurrent.STM.TChan
import Data.Function (fix)
import Data.Semigroup (Last(..))
import Data.Typeable
import Data.Void
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck hiding (checkCoverage)

import Data.Parsable hiding (label)

-- | If a parse succeeds for the beginning of the input, but then fails, we
--   choose the 'PartialParse' constructor. If the entire parse was successful,
--   we choose 'CompleteParse'.
--
--   This is mostly useful for testing parsers where we need to test them
--   individually (must end with 'CompleteParse') and also composed together
--   (individual parsers may end with 'PartialParse' if they do not conclude
--   the larger parse).
data ParseCoverage
    = PartialParse String
    | CompleteParse
    deriving stock (Show, Eq, Ord)
    deriving Semigroup via Last ParseCoverage

-- | QuickCheck tests for any 'Parsable' type. Currently this only checks
--   'parsableProp'.
parsableQuickCheck :: forall proxy a.
    ( Parsable a Identity () String
    , Printable a
    , Arbitrary a
    , Eq a
    , Show a
    , Typeable a
    ) => proxy a -> STM TestTree
parsableQuickCheck p = do
    c <- newTChan
    pure $ testGroup (show (typeRep p))
        [ testProperty "parsableProp" (parsableProp @a c)
        ]

-- | QuickCheck property that verifies the "round-trip law" of any type
--   which is both 'Parsable' and 'Printable'.
parsableProp :: forall a.
    ( Parsable a Identity () String
    , Printable a
    , Eq a
    , Show a
    ) => TChan ParseError -> a -> Property
parsableProp c x = whenFail (printErrorTChan c) $ idempotentIOProperty $ do
    let s = toString x
    let r = runCheckParsable @a s
    _ <- either (atomically . writeTChan c) (const (pure ())) r
#if defined(VERBOSE_TESTS)
    pure $ label s $ r === Right (CompleteParse, x)
#else
    pure $ r === Right (CompleteParse, x)
#endif

-- | HUnit tests for a string which should be parsed as a 'Parsable'
--
--   Currently this checks:
--
--   * 'parsableAssertion'
parsableHUnit :: forall proxy a.
    ( Parsable a Identity () String
    , Printable a
    , Show a
    ) => proxy a -> String -> TestTree
parsableHUnit p str = testCase (show str) $ parsableAssertion p str

-- | HUnit tests for a 'Parsable'/'Printable' value
--
--   Currently this checks:
--
--   * 'parsableAssertion'
--   * 'printableAssertion'
printableHUnit :: forall a.
    ( Parsable a Identity () String
    , Printable a
    , Show a
    , Eq a) => a -> TestTree
printableHUnit x = testCase (show str) $ do
    parsableAssertion (Proxy @a) str
    printableAssertion x
    where str = toString x

-- | The string is parsed as a 'Parsable'/'Printable' value, which is then
--   converted back to a string. This HUnit assertion verifies that the
--   resulting string is the same as the original.
parsableAssertion :: forall proxy a.
    ( Parsable a Identity () String
    , Printable a
    , Show a
    ) => proxy a -> String -> Assertion
parsableAssertion _ s = case runCheckParsable @a s of
    Left e ->
        assertFailure $
            "Could not parse: " ++ s ++ "\n"
            ++ "Error:\n" ++ show e
    Right p@(PartialParse _, _) ->
        assertFailure $
            "Got a PartialParse when it should be a CompleteParse:\n"
            ++ "Output: " ++ show p ++ "\n"
    Right (CompleteParse, x) ->
        assertEqual "resulting string equals original" (toString x) s

-- | The 'Parsable'/'Printable' value is converted to a string, which is then
--   parsed. This HUnit assertion verifies that the parse result is the same as
--   the original value.
printableAssertion :: forall a.
    ( Parsable a Identity () String
    , Printable a
    , Show a
    , Eq a
    ) => a -> Assertion
printableAssertion x = ppAssertion f $ runCheckParsable (toString x)
    where f = assertEqual "parse result equals original" x

ppAssertion ::
    ( Show a
    ) => (a -> Assertion)
    -> Either ParseError (ParseCoverage, a)
    -> Assertion
ppAssertion f = \case
    Left e ->
        assertFailure $
            "Could not parse input string:\n"
            ++ "Error:\n" ++ show e
    Right p@(PartialParse _, _) ->
        assertFailure $
            "Got a PartialParse when it should be a CompleteParse:\n"
            ++ "Output: " ++ show p ++ "\n"
    Right (CompleteParse, x) -> f x


-- | Parses a string as the given 'Parsable' value
runCheckParsable
    :: Parsable a Identity () String
    => String
    -> Either ParseError (ParseCoverage, a)
runCheckParsable = runParser checkParsable () ""

-- | Convenience function that runs 'checkCoverage' on a 'Parsable' parser.
checkParsable
    :: forall a s u m
    .  (Stream s m Char, Parsable a m u s)
    => ParsecT s u m (ParseCoverage, a)
checkParsable = checkCoverage (parser <?> n)
    where n = getParserName $ parserName @a @m @u @s

-- | Run the specified parser, then return 'CompleteParse' if we are at
--   'eof', otherwise 'PartialParse'.
checkCoverage
    :: Stream s m Char
    => ParsecT s u m a
    -> ParsecT s u m (ParseCoverage, a)
checkCoverage p = do
    x <- p
    (,x) <$> choice
        [ CompleteParse <$ eof
        , PartialParse  <$> lookAhead (anyToken `manyTill` eof)
        ]

-- | Generator which takes two lists of predicates which specify valid
--   characters.
--
--   * The first list is specifically for characters that start
--     the string.
--   * The second list is for any subsequent characters.
--
--   This generator always creates a non-empty string.
wordGen :: [Char -> Bool] -> [Char -> Bool] -> Gen String
wordGen wordStart wordRest = do
    c  <-          arbitrary `suchThat` anySat wordStart
    cs <- listOf $ arbitrary `suchThat` anySat wordRest
    pure $ c : cs
  where
    anySat :: [Char -> Bool] -> Char -> Bool
    anySat l x = or [f x | f <- l]


printErrorTChan :: TChan ParseError -> IO ()
printErrorTChan c = fix $ \loop -> do
    me <- atomically $ tryReadTChan c
    case me of
        Just e  -> putStrLn (show e) *> loop
        Nothing -> pure ()