{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}
module Main where
import Control.Applicative (liftA2, (<|>))
import Control.Monad (mplus)
import Control.Monad.Trans.Class
import qualified Control.Monad.Trans.State as S
import qualified Data.ByteString as B
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy as BL
import qualified Data.Text as T
import qualified Test.Tasty as Tasty
import qualified Test.Tasty.Runners as Tasty
import Test.Tasty.QuickCheck ((===))
import qualified Test.Tasty.QuickCheck as QC
import qualified Test.Tasty.HUnit as HU
import Test.Tasty.HUnit ((@?=), (@=?))
import qualified Test.QuickCheck.Instances ()
import qualified Xmlbf as X
--------------------------------------------------------------------------------
main :: IO ()
main = Tasty.defaultMainWithIngredients
[ Tasty.consoleTestReporter
, Tasty.listingTests
] tt_main
--------------------------------------------------------------------------------
tt_main :: Tasty.TestTree
tt_main = Tasty.testGroup "main"
[ tt_text
, tt_element
, tt_encoding
, tt_parsing
, tt_backtracking
, tt_fixpoints
]
tt_text :: Tasty.TestTree
tt_text = Tasty.testGroup "text'"
[ QC.testProperty "text: empty or one" $
QC.forAllShrink QC.arbitrary QC.shrink $ \t ->
length (X.text t) <= 1
, QC.testProperty "text: like text'" $
QC.forAllShrink QC.arbitrary QC.shrink $ \t ->
case X.text' t of
Left _ -> X.text t === []
Right n -> X.text t === [n]
]
tt_element :: Tasty.TestTree
tt_element = Tasty.testGroup "element"
[ HU.testCase "empty name" $ do
X.element' "" [] []
@?= Left "Element name is blank"
, HU.testCase "name with leading whitespace" $ do
X.element' " x" [] []
@?= Left "Element name has surrounding whitespace: \" x\""
, HU.testCase "name with trailing whitespace" $ do
X.element' "x " [] []
@?= Left "Element name has surrounding whitespace: \"x \""
, HU.testCase "empty attribute" $ do
X.element' "x" [("","a")] []
@?= Left "Attribute name is blank"
, HU.testCase "attribute with leading whitespace" $ do
X.element' "x" [(" x","a")] []
@?= Left "Attribute name has surrounding whitespace: \" x\""
, HU.testCase "attribute with trailing whitespace" $ do
X.element' "x" [("x ","a")] []
@?= Left "Attribute name has surrounding whitespace: \"x \""
]
tt_encoding :: Tasty.TestTree
tt_encoding = Tasty.testGroup "Encoding"
[ HU.testCase "empty" $ do
bsEncode [] @?= ""
, HU.testCase "xml: " $ do
bsEncode (X.element "x" [("a","y")] [])
@?= ""
, HU.testCase "xml: " $ do
bsEncode (X.element "x" [("a","y"), ("b","")] [])
@?= ""
, HU.testCase "xml: " $ do
bsEncode (X.element "x" [("a","y"), ("b","z")] [])
@?= ""
, HU.testCase "xml: foo" $ do
bsEncode (X.element "x" [] (X.text "foo"))
@?= "foo"
, HU.testCase "xml: foobar" $ do
bsEncode (X.element "x" [] (X.text "foo" <> X.text "bar"))
@?= "foobar"
, HU.testCase "xml: " $ do
bsEncode (X.element "x" [] (X.element "y" [] []))
@?= ""
, HU.testCase "xml: " $ do
bsEncode (X.element "x" []
(X.element "y" [] [] <> X.element "z" [] []))
@?= ""
, HU.testCase "xml: " $ do
bsEncode (X.element "x" [] [] <> X.element "y" [] [])
@?= ""
]
--------------------------------------------------------------------------------
tt_parsing :: Tasty.TestTree
tt_parsing = Tasty.testGroup "Parsing"
[ HU.testCase "endOfInput" $ do
Right () @=? X.parse X.pEndOfInput []
, HU.testCase "endOfInput: Not end of input yet" $ do
Left "Not end of input yet" @=? X.parse X.pEndOfInput (X.text "&")
, HU.testCase "text': empty" $ do
Left "Missing text node" @=? X.parse X.pText []
, HU.testCase "text': blank" $ do
Left "Missing text node" @=? X.parse X.pText (X.text "")
, HU.testCase "text': space" $ do
Right " \t\n" @=?
X.parse X.pText (X.text " " <> X.text "\t" <> X.text "\n")
, HU.testCase "text': missing" $ do
Left "Missing text node" @=? X.parse X.pText (X.element "a" [] [])
, HU.testCase "text'" $ do
Right "&" @=? X.parse X.pText (X.text "&")
, HU.testCase "text': concat" $ do
Right "&<" @=? X.parse X.pText
(X.text "&" <> X.text "" <> X.text "<")
, HU.testCase "text': twice" $ do
Left "Missing text node" @=? X.parse (X.pText >> X.pText)
(X.text "&" <> X.text "" <> X.text "<")
, HU.testCase "any element: empty" $ do
Left "Missing element" @=? X.parse (X.pAnyElement (pure ())) []
, HU.testCase "any element: text" $ do
Left "Missing element"
@=? X.parse (X.pAnyElement (pure ())) (X.text "a")
, HU.testCase "any element: pure" $ do
Right () @=? X.parse (X.pAnyElement (pure ())) (X.element "x" [] [])
, HU.testCase "any element: name" $ do
Right "x"
@=? X.parse (X.pAnyElement X.pName) (X.element "x" [] [])
, HU.testCase "element: empty" $ do
Left "Missing element \"x\"" @=? X.parse (X.pElement "x" (pure ())) []
, HU.testCase "element: Missing element" $ do
Left "Missing element \"x\""
@=? X.parse (X.pElement "x" (pure ())) (X.element "y" [] [])
, HU.testCase "element: pure" $ do
Right ()
@=? X.parse (X.pElement "x" (pure ())) (X.element "x" [] [])
, HU.testCase "element: name" $ do
Right "x"
@=? X.parse (X.pElement "x" X.pName) (X.element "x" [] [])
, HU.testCase "element: leading whitespace" $ do
Right ()
@=? X.parse (X.pElement "x" (pure ()))
(X.text " \n \t" <> X.element "x" [] [])
, HU.testCase "element: text'" $ do
Right "ab"
@=? X.parse (X.pElement "x" X.pText)
(X.element "x" [] (X.text "a" <> X.text "b"))
, HU.testCase "element: nested" $ do
Right ([("a","b")], "z")
@=? X.parse
(X.pElement "x" (X.pElement "y" (liftA2 (,) X.pAttrs X.pText)))
(X.element "x" [] (X.element "y" [("a","b")] (X.text "z")))
, HU.testCase "element: nested with leading whitespace" $ do
Right ([("a","b")], "z")
@=? X.parse
(X.pElement "x" (X.pElement "y" (liftA2 (,) X.pAttrs X.pText)))
(X.text " " <>
X.element "x" [] (X.text " " <>
X.element "y" [("a","b")] (X.text "z")))
, HU.testCase "element: twice" $ do
Left "Missing element \"x\""
@=? X.parse (X.pElement "x" (pure ()) >> X.pElement "x" (pure ()))
(X.element "x" [] [])
, HU.testCase "attr" $ do
Right "a"
@=? X.parse (X.pElement "x" (X.pAttr "y"))
(X.element "x" [("y","a"), ("z","b")] [])
, HU.testCase "attr: Missing" $ do
Left "Missing attribute \"y\""
@=? X.parse (X.pElement "x" (X.pAttr "y")) (X.element "x" [] [])
, HU.testCase "attrs: empty" $ do
Right []
@=? X.parse (X.pElement "x" X.pAttrs) (X.element "x" [] [])
, HU.testCase "attrs" $ do
Right [("y","a"), ("z","b")]
@=? X.parse (X.pElement "x" X.pAttrs)
(X.element "x" [("z","b"), ("y","a")] [])
, HU.testCase "attrs: twice" $ do
Right []
@=? X.parse (X.pElement "x" (X.pAttrs >> X.pAttrs))
(X.element "x" [("z","b"), ("y","a")] [])
, HU.testCase "fail: empty" $ do
(Left "x" :: Either String ())
@=? X.parse (fail "x") []
, HU.testCase "fail" $ do
(Left "x" :: Either String ())
@=? X.parse (fail "x") (X.text "y")
, HU.testCase "children: empty" $ do
Right []
@=? X.parse (X.pElement "x" X.pChildren) (X.element "x" [] [])
, HU.testCase "children: top empty" $ do
Right [] @=? X.parse X.pChildren []
, HU.testCase "children: top 1 node" $ do
Right (X.element "x" [] [])
@=? X.parse X.pChildren (X.element "x" [] [])
, HU.testCase "children: top 1 node twice" $ do
Right []
@=? X.parse (X.pChildren >> X.pChildren) (X.element "x" [] [])
, HU.testCase "children: top 2 nodes" $ do
Right (X.element "x" [] [] <> X.text "ab" <> X.element "y" [] [])
@=? X.parse X.pChildren
(X.element "x" [] [] <> X.text "a" <> X.text "b" <>
X.element "y" [] [])
, HU.testCase "children: 1 node" $ do
Right (X.text "foo")
@=? X.parse (X.pElement "x" X.pChildren)
(X.element "x" [] (X.text "foo"))
, HU.testCase "children: 1 node twice" $ do
Right []
@=? X.parse (X.pElement "x" (X.pChildren >> X.pChildren))
(X.element "x" [] (X.text "foo"))
, HU.testCase "children: 2 successive text' nodes" $ do
Right (X.text "foobar")
@=? X.parse (X.pElement "x" X.pChildren)
(X.element "x" [] (X.text "foo" <> X.text "bar"))
, HU.testCase "children: 2 text' nodes twice" $ do
Right []
@=? X.parse (X.pElement "x" (X.pChildren >> X.pChildren))
(X.element "x" [] (X.text "foo" <> X.text "bar"))
, HU.testCase "children: 3 nodes" $ do
let ns0 = X.text "foo" <> X.element "a" [] [] <> X.text "bar"
Right ns0
@=? X.parse (X.pElement "x" X.pChildren)
(X.element "x" [] ns0)
, HU.testCase "children: 3 nodes twice" $ do
Right []
@=? X.parse (X.pElement "x" (X.pChildren >> X.pChildren))
(X.element "x" []
(X.text "foo" <> X.element "a" [] [] <> X.text "bar"))
, QC.testProperty "parserT (runParserT pAnyElement) == pAnyElement" $ do
QC.forAllShrink QC.arbitrary QC.shrink $ \t ->
let p0 = X.pAnyElement ((,,) <$> X.pName <*> X.pAttrs <*> X.pChildren)
p1 = X.parserT (X.runParserT p0)
ns = X.element t [] []
in X.parse p0 ns === X.parse p1 ns
, QC.testProperty "parserT (runParserT pText) == pText" $ do
QC.forAllShrink QC.arbitrary QC.shrink $ \t ->
let p0 = X.pText
p1 = X.parserT (X.runParserT p0)
ns = X.text t
in X.parse p0 ns === X.parse p1 ns
, HU.testCase "ParserT StateT" $ do
(Right "Haskell", "b")
@=? S.runState (X.parseM (do s0 <- lift S.get
a <- X.pElement s0 X.pText
lift $ S.put (T.map succ s0)
s1 <- lift S.get
b <- X.pElement s1 X.pText
pure (a <> b))
(X.element "a" [] (X.text "Has") <>
X.element "b" [] (X.text "kell")))
"a"
]
tt_backtracking :: Tasty.TestTree
tt_backtracking = Tasty.testGroup "Backtracking"
[ HU.testCase "Alternative" $
Right "y" @=? X.parse
-- The second pText fails because the state is empty after the first
((X.pText >> X.pText >> pure "a") <|> X.pText)
(X.text "y")
, HU.testCase "MonadPlus" $
Right "y" @=? X.parse
-- The second pText fails because the state is empty after the first
(mplus (X.pText >> X.pText >> pure "a") X.pText)
(X.text "y")
]
node0 :: X.Node
Right node0 =
X.element' "a" [] $ mconcat
[ X.element "b" []
(X.element "c" [] [] <>
X.element "d" [] [])
, X.element "e" []
(X.element "f" [] [] <>
X.element "g" [] [])
]
fixvisit :: (X.Node -> S.State T.Text [X.Node])
-> (X.Node -> S.State T.Text [X.Node])
fixvisit _ n = do
let X.Element t _ _ = n
S.modify (\ts -> mappend ts t)
pure [n]
tt_fixpoints :: Tasty.TestTree
tt_fixpoints = Tasty.testGroup "fixpoints"
[ HU.testCase "dfpos: depth-first post-order?" $ do
let (ns, ts) = S.runState (X.dfposM fixvisit node0) []
ns @?= [node0]
ts @?= "cdbfgea"
, HU.testCase "dfpos: output single node" $ do
let Right n0 = X.element' "x" [] (X.text "a" <> X.text "b")
f = \k -> \case
X.Text "ab" -> X.text "foo" >>= k
X.Text "foo" -> X.text "FOO" >>= k
n -> [n]
X.element "x" [] (X.text "FOO")
@?= X.dfpos f n0
, HU.testCase "dfpos: output multiple nodes" $ do
let Right n0 = X.element' "x" [] (X.text "a")
f = \k -> \case
X.Text "a" -> let ny = X.element "y" [] [] in (ny <> ny) >>= k
X.Element "y" _ _ -> X.text "b" >>= k
X.Element "x" as cs ->
(X.element "z" as cs <> X.text "a") >>= k
n -> [n]
(X.element "z" [] (X.text "bb") <> X.text "bb")
@?= X.dfpos f n0
, HU.testCase "dfpre: depth-first pre-order?" $ do
let (ns, ts) = S.runState (X.dfpreM fixvisit node0) []
ns @?= [node0]
ts @?= "abcdefg"
, HU.testCase "dfpre: output single node" $ do
let Right n0 = X.element' "x" [] (X.text "a" <> X.text "b")
f = \k -> \case
X.Text "ab" -> X.text "foo" >>= k
X.Text "foo" -> X.text "FOO" >>= k
n -> [n]
X.element "x" [] (X.text "FOO")
@?= X.dfpre f n0
, HU.testCase "dfpre: output multiple nodes" $ do
let Right n0 = X.element' "x" [] (X.text "a")
f = \k -> \case
X.Text "a" -> let ny = X.element "y" [] [] in (ny <> ny) >>= k
X.Element "y" _ _ -> X.text "b" >>= k
X.Element "x" as cs ->
(X.element "z" as cs <> X.text "a") >>= k
n -> [n]
X.element "z" [] (X.text "bb") <> X.text "bb"
@?= X.dfpre f n0
]
--------------------------------------------------------------------------------
bsEncode :: [X.Node] -> B.ByteString
bsEncode = BL.toStrict . BB.toLazyByteString . X.encode