2016-09-04 17:00:46 +03:00
|
|
|
|
-- |
|
|
|
|
|
-- Module : Test.Hspec.Megaparsec
|
2017-05-25 12:01:26 +03:00
|
|
|
|
-- Copyright : © 2016–2017 Mark Karpov
|
2016-09-04 17:00:46 +03:00
|
|
|
|
-- License : BSD 3 clause
|
|
|
|
|
--
|
2017-05-25 12:01:26 +03:00
|
|
|
|
-- Maintainer : Mark Karpov <markkarpov92@gmail.com>
|
2016-09-04 17:00:46 +03:00
|
|
|
|
-- Stability : experimental
|
|
|
|
|
-- Portability : portable
|
|
|
|
|
--
|
|
|
|
|
-- Utility functions for testing Megaparsec parsers with Hspec.
|
|
|
|
|
|
|
|
|
|
{-# LANGUAGE BangPatterns #-}
|
|
|
|
|
{-# LANGUAGE CPP #-}
|
|
|
|
|
{-# LANGUAGE DeriveDataTypeable #-}
|
|
|
|
|
{-# LANGUAGE DeriveGeneric #-}
|
|
|
|
|
{-# LANGUAGE FlexibleContexts #-}
|
|
|
|
|
{-# LANGUAGE RankNTypes #-}
|
|
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
|
|
|
{-# LANGUAGE TypeFamilies #-}
|
|
|
|
|
|
|
|
|
|
module Test.Hspec.Megaparsec
|
|
|
|
|
( -- * Basic expectations
|
|
|
|
|
shouldParse
|
|
|
|
|
, parseSatisfies
|
|
|
|
|
, shouldSucceedOn
|
|
|
|
|
, shouldFailOn
|
|
|
|
|
-- * Testing of error messages
|
|
|
|
|
, shouldFailWith
|
|
|
|
|
-- * Error message construction
|
|
|
|
|
-- $errmsg
|
|
|
|
|
, err
|
|
|
|
|
, posI
|
|
|
|
|
, posN
|
|
|
|
|
, EC
|
|
|
|
|
, utok
|
|
|
|
|
, utoks
|
|
|
|
|
, ulabel
|
|
|
|
|
, ueof
|
|
|
|
|
, etok
|
|
|
|
|
, etoks
|
|
|
|
|
, elabel
|
|
|
|
|
, eeof
|
|
|
|
|
, cstm
|
|
|
|
|
-- * Incremental parsing
|
|
|
|
|
, failsLeaving
|
|
|
|
|
, succeedsLeaving
|
|
|
|
|
, initialState )
|
|
|
|
|
where
|
|
|
|
|
|
|
|
|
|
import Control.Monad (unless)
|
|
|
|
|
import Data.Data (Data)
|
|
|
|
|
import Data.List.NonEmpty (NonEmpty (..))
|
|
|
|
|
import Data.Proxy
|
|
|
|
|
import Data.Semigroup
|
|
|
|
|
import Data.Set (Set)
|
|
|
|
|
import Data.Typeable (Typeable)
|
|
|
|
|
import GHC.Generics
|
|
|
|
|
import Test.Hspec.Expectations
|
|
|
|
|
import Text.Megaparsec
|
|
|
|
|
import Text.Megaparsec.Pos (defaultTabWidth)
|
|
|
|
|
import qualified Data.List.NonEmpty as NE
|
|
|
|
|
import qualified Data.Set as E
|
|
|
|
|
|
|
|
|
|
----------------------------------------------------------------------------
|
|
|
|
|
-- Basic expectations
|
|
|
|
|
|
|
|
|
|
-- | Create an expectation by saying what the result should be.
|
|
|
|
|
--
|
|
|
|
|
-- > parse letterChar "" "x" `shouldParse` 'x'
|
|
|
|
|
|
|
|
|
|
shouldParse :: (Ord t, ShowToken t, ShowErrorComponent e, Eq a, Show a)
|
|
|
|
|
=> Either (ParseError t e) a
|
|
|
|
|
-- ^ Result of parsing as returned by function like 'parse'
|
|
|
|
|
-> a -- ^ Desired result
|
|
|
|
|
-> Expectation
|
|
|
|
|
r `shouldParse` v = case r of
|
|
|
|
|
Left e -> expectationFailure $ "expected: " ++ show v ++
|
|
|
|
|
"\nbut parsing failed with error:\n" ++ showParseError e
|
|
|
|
|
Right x -> unless (x == v) . expectationFailure $
|
|
|
|
|
"expected: " ++ show v ++ "\nbut got: " ++ show x
|
|
|
|
|
|
|
|
|
|
-- | Create an expectation by saying that the parser should successfully
|
|
|
|
|
-- parse a value and that the value should satisfy some predicate.
|
|
|
|
|
--
|
|
|
|
|
-- > parse (many punctuationChar) "" "?!!" `parseSatisfies` ((== 3) . length)
|
|
|
|
|
|
|
|
|
|
parseSatisfies :: (Ord t, ShowToken t, ShowErrorComponent e, Show a)
|
|
|
|
|
=> Either (ParseError t e) a
|
|
|
|
|
-- ^ Result of parsing as returned by function like 'parse'
|
|
|
|
|
-> (a -> Bool) -- ^ Predicate
|
|
|
|
|
-> Expectation
|
|
|
|
|
r `parseSatisfies` p = case r of
|
|
|
|
|
Left e -> expectationFailure $
|
|
|
|
|
"expected a parsed value to check against the predicate" ++
|
|
|
|
|
"\nbut parsing failed with error:\n" ++ showParseError e
|
|
|
|
|
Right x -> unless (p x) . expectationFailure $
|
|
|
|
|
"the value did not satisfy the predicate: " ++ show x
|
|
|
|
|
|
2017-05-25 12:01:26 +03:00
|
|
|
|
-- | Check that a parser fails on a given input.
|
2016-09-04 17:00:46 +03:00
|
|
|
|
--
|
|
|
|
|
-- > parse (char 'x') "" `shouldFailOn` "a"
|
|
|
|
|
|
|
|
|
|
shouldFailOn :: Show a
|
|
|
|
|
=> (s -> Either (ParseError t e) a)
|
|
|
|
|
-- ^ Parser that takes stream and produces result or error message
|
|
|
|
|
-> s -- ^ Input that the parser should fail on
|
|
|
|
|
-> Expectation
|
|
|
|
|
p `shouldFailOn` s = shouldFail (p s)
|
|
|
|
|
|
2017-05-25 12:01:26 +03:00
|
|
|
|
-- | Check that a parser succeeds on a given input.
|
2016-09-04 17:00:46 +03:00
|
|
|
|
--
|
|
|
|
|
-- > parse (char 'x') "" `shouldSucceedOn` "x"
|
|
|
|
|
|
|
|
|
|
shouldSucceedOn :: (Ord t, ShowToken t, ShowErrorComponent e, Show a)
|
|
|
|
|
=> (s -> Either (ParseError t e) a)
|
|
|
|
|
-- ^ Parser that takes stream and produces result or error message
|
|
|
|
|
-> s -- ^ Input that the parser should succeed on
|
|
|
|
|
-> Expectation
|
|
|
|
|
p `shouldSucceedOn` s = shouldSucceed (p s)
|
|
|
|
|
|
|
|
|
|
----------------------------------------------------------------------------
|
|
|
|
|
-- Testing of error messages
|
|
|
|
|
|
|
|
|
|
-- | Create an expectation that parser should fail producing certain
|
2016-09-26 23:01:20 +03:00
|
|
|
|
-- 'ParseError'. Use the 'err' function from this module to construct a
|
|
|
|
|
-- 'ParseError' to compare with.
|
2016-09-04 17:00:46 +03:00
|
|
|
|
--
|
|
|
|
|
-- > parse (char 'x') "" "b" `shouldFailWith` err posI (utok 'b' <> etok 'x')
|
|
|
|
|
|
|
|
|
|
shouldFailWith :: (Ord t, ShowToken t, ShowErrorComponent e, Show a)
|
|
|
|
|
=> Either (ParseError t e) a
|
|
|
|
|
-> ParseError t e
|
|
|
|
|
-> Expectation
|
|
|
|
|
r `shouldFailWith` e = case r of
|
|
|
|
|
Left e' -> unless (e == e') . expectationFailure $
|
|
|
|
|
"the parser is expected to fail with:\n" ++ showParseError e ++
|
|
|
|
|
"but it failed with:\n" ++ showParseError e'
|
|
|
|
|
Right v -> expectationFailure $
|
|
|
|
|
"the parser is expected to fail, but it parsed: " ++ show v
|
|
|
|
|
|
|
|
|
|
----------------------------------------------------------------------------
|
|
|
|
|
-- Error message construction
|
|
|
|
|
|
2017-05-25 12:01:26 +03:00
|
|
|
|
-- $errmsg
|
|
|
|
|
--
|
|
|
|
|
-- When you wish to test error message on failure, the need to construct a
|
|
|
|
|
-- error message for comparison arises. These helpers allow to construct
|
|
|
|
|
-- virtually any sort of error message easily.
|
2016-09-04 17:00:46 +03:00
|
|
|
|
|
|
|
|
|
-- | Assemble a 'ParseErorr' from source position and @'EC' t e@ value. To
|
|
|
|
|
-- create source position, two helpers are available: 'posI' and 'posN'.
|
|
|
|
|
-- @'EC' t e@ is a monoid and can be built from primitives provided by this
|
|
|
|
|
-- module, see below.
|
|
|
|
|
--
|
|
|
|
|
-- @since 0.3.0
|
|
|
|
|
|
|
|
|
|
err
|
|
|
|
|
:: NonEmpty SourcePos -- ^ 'ParseError' position
|
|
|
|
|
-> EC t e -- ^ Error components
|
|
|
|
|
-> ParseError t e -- ^ Resulting 'ParseError'
|
|
|
|
|
err pos (EC u e c) = ParseError pos u e c
|
|
|
|
|
|
|
|
|
|
-- | Initial source position with empty file name.
|
|
|
|
|
--
|
|
|
|
|
-- @since 0.3.0
|
|
|
|
|
|
|
|
|
|
posI :: NonEmpty SourcePos
|
|
|
|
|
posI = initialPos "" :| []
|
|
|
|
|
|
|
|
|
|
-- | @posN n s@ returns source position achieved by applying 'updatePos'
|
|
|
|
|
-- method corresponding to type of stream @s@ @n@ times.
|
|
|
|
|
--
|
|
|
|
|
-- @since 0.3.0
|
|
|
|
|
|
|
|
|
|
posN :: forall s n. (Stream s, Integral n)
|
|
|
|
|
=> n
|
|
|
|
|
-> s
|
|
|
|
|
-> NonEmpty SourcePos
|
|
|
|
|
posN n see = f (initialPos "") see n :| []
|
|
|
|
|
where
|
|
|
|
|
f p s !i =
|
|
|
|
|
if i > 0
|
|
|
|
|
then case uncons s of
|
|
|
|
|
Nothing -> p
|
|
|
|
|
Just (t,s') ->
|
|
|
|
|
let p' = snd $ updatePos (Proxy :: Proxy s) defaultTabWidth p t
|
|
|
|
|
in f p' s' (i - 1)
|
|
|
|
|
else p
|
|
|
|
|
|
|
|
|
|
-- | Auxiliary type for construction of 'ParseError's. Note that it's a
|
|
|
|
|
-- monoid.
|
|
|
|
|
--
|
|
|
|
|
-- @since 0.3.0
|
|
|
|
|
|
|
|
|
|
data EC t e = EC
|
|
|
|
|
{ ecUnexpected :: Set (ErrorItem t) -- ^ Unexpected items
|
|
|
|
|
, ecExpected :: Set (ErrorItem t) -- ^ Expected items
|
|
|
|
|
, _ecCustom :: Set e -- ^ Custom items
|
|
|
|
|
} deriving (Eq, Data, Typeable, Generic)
|
|
|
|
|
|
|
|
|
|
instance (Ord t, Ord e) => Semigroup (EC t e) where
|
|
|
|
|
(EC u0 e0 c0) <> (EC u1 e1 c1) =
|
|
|
|
|
EC (E.union u0 u1) (E.union e0 e1) (E.union c0 c1)
|
|
|
|
|
|
|
|
|
|
instance (Ord t, Ord e) => Monoid (EC t e) where
|
|
|
|
|
mempty = EC E.empty E.empty E.empty
|
|
|
|
|
mappend = (<>)
|
|
|
|
|
|
2017-05-25 12:01:26 +03:00
|
|
|
|
-- | Construct an “unexpected token” error component.
|
2016-09-04 17:00:46 +03:00
|
|
|
|
--
|
|
|
|
|
-- @since 0.3.0
|
|
|
|
|
|
|
|
|
|
utok :: (Ord t, Ord e) => t -> EC t e
|
|
|
|
|
utok t = mempty { ecUnexpected = (E.singleton . Tokens . nes) t }
|
|
|
|
|
|
2017-05-25 12:01:26 +03:00
|
|
|
|
-- | Construct an “unexpected tokens” error component. Empty string produces
|
2016-09-04 17:00:46 +03:00
|
|
|
|
-- 'EndOfInput'.
|
|
|
|
|
--
|
|
|
|
|
-- @since 0.3.0
|
|
|
|
|
|
|
|
|
|
utoks :: (Ord t, Ord e) => [t] -> EC t e
|
|
|
|
|
utoks t = mempty { ecUnexpected = (E.singleton . canonicalizeTokens) t }
|
|
|
|
|
|
2017-05-25 12:01:26 +03:00
|
|
|
|
-- | Construct an “unexpected label” error component. Do not use with empty
|
2016-09-04 17:00:46 +03:00
|
|
|
|
-- strings (for empty strings it's bottom).
|
|
|
|
|
--
|
|
|
|
|
-- @since 0.3.0
|
|
|
|
|
|
|
|
|
|
ulabel :: (Ord t, Ord e) => String -> EC t e
|
|
|
|
|
ulabel l = mempty { ecUnexpected = (E.singleton . Label . NE.fromList) l }
|
|
|
|
|
|
2017-05-25 12:01:26 +03:00
|
|
|
|
-- | Construct an “unexpected end of input” error component.
|
2016-09-04 17:00:46 +03:00
|
|
|
|
--
|
|
|
|
|
-- @since 0.3.0
|
|
|
|
|
|
|
|
|
|
ueof :: (Ord t, Ord e) => EC t e
|
|
|
|
|
ueof = mempty { ecUnexpected = E.singleton EndOfInput }
|
|
|
|
|
|
2017-05-25 12:01:26 +03:00
|
|
|
|
-- | Construct an “expected token” error component.
|
2016-09-04 17:00:46 +03:00
|
|
|
|
--
|
|
|
|
|
-- @since 0.3.0
|
|
|
|
|
|
|
|
|
|
etok :: (Ord t, Ord e) => t -> EC t e
|
|
|
|
|
etok t = mempty { ecExpected = (E.singleton . Tokens . nes) t }
|
|
|
|
|
|
2017-05-25 12:01:26 +03:00
|
|
|
|
-- | Construct an “expected tokens” error component. Empty string produces
|
2016-09-04 17:00:46 +03:00
|
|
|
|
-- 'EndOfInput'.
|
|
|
|
|
--
|
|
|
|
|
-- @since 0.3.0
|
|
|
|
|
|
|
|
|
|
etoks :: (Ord t, Ord e) => [t] -> EC t e
|
|
|
|
|
etoks t = mempty { ecExpected = (E.singleton . canonicalizeTokens) t }
|
|
|
|
|
|
2017-05-25 12:01:26 +03:00
|
|
|
|
-- | Construct an “expected label” error component. Do not use with empty
|
2016-09-04 17:00:46 +03:00
|
|
|
|
-- strings.
|
|
|
|
|
--
|
|
|
|
|
-- @since 0.3.0
|
|
|
|
|
|
|
|
|
|
elabel :: (Ord t, Ord e) => String -> EC t e
|
|
|
|
|
elabel l = mempty { ecExpected = (E.singleton . Label . NE.fromList) l }
|
|
|
|
|
|
2017-05-25 12:01:26 +03:00
|
|
|
|
-- | Construct an “expected end of input” error component.
|
2016-09-04 17:00:46 +03:00
|
|
|
|
--
|
|
|
|
|
-- @since 0.3.0
|
|
|
|
|
|
|
|
|
|
eeof :: (Ord t, Ord e) => EC t e
|
|
|
|
|
eeof = mempty { ecExpected = E.singleton EndOfInput }
|
|
|
|
|
|
2017-05-25 12:01:26 +03:00
|
|
|
|
-- | Construct a custom error component.
|
2016-09-04 17:00:46 +03:00
|
|
|
|
--
|
|
|
|
|
-- @since 0.3.0
|
|
|
|
|
|
|
|
|
|
cstm :: e -> EC t e
|
|
|
|
|
cstm e = EC E.empty E.empty (E.singleton e)
|
|
|
|
|
|
|
|
|
|
----------------------------------------------------------------------------
|
|
|
|
|
-- Incremental parsing
|
|
|
|
|
|
2017-05-25 12:01:26 +03:00
|
|
|
|
-- | Check that a parser fails and leaves a certain part of input
|
2016-09-04 17:00:46 +03:00
|
|
|
|
-- unconsumed. Use it with functions like 'runParser'' and 'runParserT''
|
|
|
|
|
-- that support incremental parsing.
|
|
|
|
|
--
|
|
|
|
|
-- > runParser' (many (char 'x') <* eof) (initialState "xxa")
|
|
|
|
|
-- > `failsLeaving` "a"
|
|
|
|
|
--
|
|
|
|
|
-- See also: 'initialState'.
|
|
|
|
|
|
|
|
|
|
failsLeaving :: (Show a, Eq s, Show s, Stream s)
|
|
|
|
|
=> (State s, Either (ParseError (Token s) e) a)
|
|
|
|
|
-- ^ Parser that takes stream and produces result along with actual
|
|
|
|
|
-- state information
|
|
|
|
|
-> s -- ^ Part of input that should be left unconsumed
|
|
|
|
|
-> Expectation
|
|
|
|
|
(st,r) `failsLeaving` s =
|
|
|
|
|
shouldFail r >> checkUnconsumed s (stateInput st)
|
|
|
|
|
|
|
|
|
|
-- | Check that a parser succeeds and leaves certain part of input
|
|
|
|
|
-- unconsumed. Use it with functions like 'runParser'' and 'runParserT''
|
|
|
|
|
-- that support incremental parsing.
|
|
|
|
|
--
|
|
|
|
|
-- > runParser' (many (char 'x')) (initialState "xxa")
|
|
|
|
|
-- > `succeedsLeaving` "a"
|
|
|
|
|
--
|
|
|
|
|
-- See also: 'initialState'.
|
|
|
|
|
|
|
|
|
|
succeedsLeaving :: ( ShowToken (Token s)
|
|
|
|
|
, ShowErrorComponent e
|
|
|
|
|
, Show a
|
|
|
|
|
, Eq s
|
|
|
|
|
, Show s
|
|
|
|
|
, Stream s )
|
|
|
|
|
=> (State s, Either (ParseError (Token s) e) a)
|
|
|
|
|
-- ^ Parser that takes stream and produces result along with actual
|
|
|
|
|
-- state information
|
|
|
|
|
-> s -- ^ Part of input that should be left unconsumed
|
|
|
|
|
-> Expectation
|
|
|
|
|
(st,r) `succeedsLeaving` s =
|
|
|
|
|
shouldSucceed r >> checkUnconsumed s (stateInput st)
|
|
|
|
|
|
|
|
|
|
-- | Given input for parsing, construct initial state for parser (that is,
|
|
|
|
|
-- with empty file name, default tab width and position at 1 line and 1
|
|
|
|
|
-- column).
|
|
|
|
|
|
|
|
|
|
initialState :: s -> State s
|
2017-01-31 14:17:54 +03:00
|
|
|
|
initialState s = State
|
|
|
|
|
{ stateInput = s
|
|
|
|
|
, statePos = initialPos "" :| []
|
2017-05-25 12:01:26 +03:00
|
|
|
|
#if MIN_VERSION_megaparsec(5,2,0)
|
2017-01-31 14:17:54 +03:00
|
|
|
|
, stateTokensProcessed = 0
|
2017-05-25 12:01:26 +03:00
|
|
|
|
#endif
|
2017-01-31 14:17:54 +03:00
|
|
|
|
, stateTabWidth = defaultTabWidth }
|
2016-09-04 17:00:46 +03:00
|
|
|
|
|
|
|
|
|
----------------------------------------------------------------------------
|
|
|
|
|
-- Helpers
|
|
|
|
|
|
|
|
|
|
-- | Expectation that argument is result of a failed parser.
|
|
|
|
|
|
2017-05-25 12:01:26 +03:00
|
|
|
|
shouldFail :: Show a
|
|
|
|
|
=> Either (ParseError t e) a
|
|
|
|
|
-> Expectation
|
2016-09-04 17:00:46 +03:00
|
|
|
|
shouldFail r = case r of
|
|
|
|
|
Left _ -> return ()
|
|
|
|
|
Right v -> expectationFailure $
|
|
|
|
|
"the parser is expected to fail, but it parsed: " ++ show v
|
|
|
|
|
|
|
|
|
|
-- | Expectation that argument is result of a succeeded parser.
|
|
|
|
|
|
|
|
|
|
shouldSucceed :: (Ord t, ShowToken t, ShowErrorComponent e, Show a)
|
2017-05-25 12:01:26 +03:00
|
|
|
|
=> Either (ParseError t e) a
|
|
|
|
|
-> Expectation
|
2016-09-04 17:00:46 +03:00
|
|
|
|
shouldSucceed r = case r of
|
|
|
|
|
Left e -> expectationFailure $
|
|
|
|
|
"the parser is expected to succeed, but it failed with:\n" ++
|
|
|
|
|
showParseError e
|
|
|
|
|
Right _ -> return ()
|
|
|
|
|
|
|
|
|
|
-- | Compare two streams for equality and in the case of mismatch report it.
|
|
|
|
|
|
|
|
|
|
checkUnconsumed :: (Eq s, Show s, Stream s)
|
|
|
|
|
=> s -- ^ Expected unconsumed input
|
|
|
|
|
-> s -- ^ Actual unconsumed input
|
|
|
|
|
-> Expectation
|
|
|
|
|
checkUnconsumed e a = unless (e == a) . expectationFailure $
|
|
|
|
|
"the parser is expected to leave unconsumed input: " ++ show e ++
|
|
|
|
|
"\nbut it left this: " ++ show a
|
|
|
|
|
|
2017-05-25 12:01:26 +03:00
|
|
|
|
-- | Render parse error in a way that is suitable for inserting it in a test
|
2016-09-04 17:00:46 +03:00
|
|
|
|
-- suite report.
|
|
|
|
|
|
|
|
|
|
showParseError :: (Ord t, ShowToken t, ShowErrorComponent e)
|
|
|
|
|
=> ParseError t e -> String
|
|
|
|
|
showParseError = unlines . fmap (" " ++) . lines . parseErrorPretty
|
|
|
|
|
|
|
|
|
|
-- | Make a singleton non-empty list from a value.
|
|
|
|
|
|
|
|
|
|
nes :: a -> NonEmpty a
|
|
|
|
|
nes x = x :| []
|
|
|
|
|
{-# INLINE nes #-}
|
|
|
|
|
|
|
|
|
|
-- | Construct appropriate 'ErrorItem' representation for given token
|
|
|
|
|
-- stream. Empty string produces 'EndOfInput'.
|
|
|
|
|
|
|
|
|
|
canonicalizeTokens :: [t] -> ErrorItem t
|
|
|
|
|
canonicalizeTokens ts =
|
|
|
|
|
case NE.nonEmpty ts of
|
|
|
|
|
Nothing -> EndOfInput
|
|
|
|
|
Just xs -> Tokens xs
|