{-# 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