packages feed

looksee-0.3.0: test/Main.hs

{-# LANGUAGE NegativeLiterals #-}
{-# LANGUAGE OverloadedStrings #-}

module Main (main) where

import Control.Applicative (Alternative (..), liftA2)
import Control.Exception (throwIO)
import Data.Foldable (toList)
import Data.Sequence (Seq (..))
import Data.Sequence qualified as Seq
import Data.String (IsString)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Void (Void)
import Looksee
import Looksee.Examples
import Test.Tasty (TestName, TestTree, defaultMain, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))

newtype Error = Error {unError :: String} deriving (Eq, Show, IsString)

type TestParser = Parser Error

type TestResult = Either (Err Error)

data ParserCase a = ParserCase !TestName !(TestParser a) !Text !(TestResult (a, Int))

err :: Span Int -> Reason Error (Err Error) -> TestResult (a, Int)
err ra re = Left (Err (ErrF ra re))

errAlt :: Span Int -> [(AltPhase, Span Int, Reason Error (Err Error))] -> TestResult (a, Int)
errAlt ra tups = Left (Err (ErrF ra (ReasonAlt (Seq.fromList (fmap f tups)))))
 where
  f (ap, ra', re) = (ap, Err (ErrF ra' re))

errInfix :: Span Int -> [(Int, InfixPhase, Span Int, Reason Error (Err Error))] -> TestResult (a, Int)
errInfix ra tups = Left (Err (ErrF ra (ReasonInfix (Seq.fromList (fmap f tups)))))
 where
  f (ix, ip, ra', re) = (ix, ip, Err (ErrF ra' re))

errLook :: Span Int -> Span Int -> Reason Error (Err Error) -> TestResult (a, Int)
errLook ra1 ra2 re = Left (Err (ErrF ra1 (ReasonLook (Err (ErrF ra2 re)))))

suc :: a -> Int -> TestResult (a, Int)
suc a i = Right (a, i)

testParserCase :: (Show a, Eq a) => ParserCase a -> TestTree
testParserCase (ParserCase name parser input expected) = testCase name $ do
  let parser' = liftA2 (,) parser dropAllP
      actual = parse parser' input
  actual @?= expected

testBasic :: TestTree
testBasic =
  testGroup "basic" $
    fmap
      (uncurry testGroup)
      [ ("empty", testEmpty)
      , ("pure", testPure)
      , ("fail", testFail)
      , ("head", testHead)
      , ("take", testTake)
      , ("drop", testDrop)
      , ("end", testEnd)
      , ("expectHead", testExpectHead)
      , ("expect", testExpect)
      , ("greedy", testGreedy)
      , ("greedy1", testGreedy1)
      , ("or", testOr)
      , ("alt", testAlt)
      , ("opt (empty)", testOptEmpty)
      , ("opt", testOpt)
      , ("bind (1)", testBind1)
      , ("bind (2)", testBind2)
      , ("throw", testThrow)
      , ("throw (consume)", testConsumeThrow)
      , ("throw (opt)", testOptThrow)
      , ("throw (opt consume)", testOptConsumeThrow)
      , ("throw (mixed)", testThrowMixed)
      , ("throw (mixed flip)", testThrowMixedFlip)
      , ("backtrack", testBacktrack)
      , ("look (pure)", testLookPure)
      , ("look (success)", testLookSuccess)
      , ("look (failure)", testLookFailure)
      , ("takeWhile", testTakeWhile)
      , ("takeWhile1", testTakeWhile1)
      , ("dropWhile", testDropWhile)
      , ("dropWhile1", testDropWhile1)
      , ("infixR", testInfixR)
      , ("someInfixR", testSomeInfixR)
      , ("break", testBreak)
      , ("someBreak", testSomeBreak)
      , ("split", testSplit)
      , ("split1", testSplit1)
      ]

testEmpty :: [TestTree]
testEmpty = fmap testParserCase cases
 where
  parser = emptyP :: TestParser Int
  cases =
    [ ParserCase "empty" parser "" (err (Span 0 0) ReasonEmpty)
    , ParserCase "non-empty" parser "hi" (err (Span 0 2) ReasonEmpty)
    ]

testPure :: [TestTree]
testPure = fmap testParserCase cases
 where
  parser = pure 'x'
  cases =
    [ ParserCase "empty" parser "" (suc 'x' 0)
    , ParserCase "non-empty" parser "hi" (suc 'x' 2)
    ]

testFail :: [TestTree]
testFail = fmap testParserCase cases
 where
  parser = fail "i give up" :: TestParser Int
  cases =
    [ ParserCase "empty" parser "" (err (Span 0 0) (ReasonFail "i give up"))
    , ParserCase "non-empty" parser "hi" (err (Span 0 2) (ReasonFail "i give up"))
    ]

testHead :: [TestTree]
testHead = fmap testParserCase cases
 where
  parser = headP
  cases =
    [ ParserCase "empty" parser "" (err (Span 0 0) (ReasonDemand 1 0))
    , ParserCase "non-empty" parser "hi" (suc 'h' 1)
    ]

testTake :: [TestTree]
testTake = fmap testParserCase cases
 where
  parser = takeP 2
  cases =
    [ ParserCase "len 0" parser "" (suc "" 0)
    , ParserCase "len 1" parser "h" (suc "h" 0)
    , ParserCase "len 2" parser "hi" (suc "hi" 0)
    , ParserCase "len 3" parser "hii" (suc "hi" 1)
    ]

testDrop :: [TestTree]
testDrop = fmap testParserCase cases
 where
  parser = dropP 2
  cases =
    [ ParserCase "len 0" parser "" (suc 0 0)
    , ParserCase "len 1" parser "h" (suc 1 0)
    , ParserCase "len 2" parser "hi" (suc 2 0)
    , ParserCase "len 3" parser "hii" (suc 2 1)
    ]

testEnd :: [TestTree]
testEnd = fmap testParserCase cases
 where
  parser = endP
  cases =
    [ ParserCase "empty" parser "" (suc () 0)
    , ParserCase "non-empty" parser "hi" (err (Span 0 2) (ReasonLeftover 2))
    ]

testExpectHead :: [TestTree]
testExpectHead = fmap testParserCase cases
 where
  parser = charP 'h'
  cases =
    [ ParserCase "empty" parser "" (err (Span 0 0) (ReasonExpect "h" ""))
    , ParserCase "non-empty" parser "hi" (suc 'h' 1)
    , ParserCase "non-match" parser "bye" (err (Span 1 3) (ReasonExpect "h" "b"))
    ]

testExpect :: [TestTree]
testExpect = fmap testParserCase cases
 where
  parser = textP "hi"
  cases =
    [ ParserCase "empty" parser "" (err (Span 0 0) (ReasonExpect "hi" ""))
    , ParserCase "non-empty" parser "hi" (suc "hi" 0)
    , ParserCase "prefix" parser "hiya" (suc "hi" 2)
    , ParserCase "partial" parser "hey" (err (Span 2 3) (ReasonExpect "hi" "he"))
    , ParserCase "non-match" parser "bye" (err (Span 2 3) (ReasonExpect "hi" "by"))
    , ParserCase "short" parser "h" (err (Span 1 1) (ReasonExpect "hi" "h"))
    ]

testGreedy :: [TestTree]
testGreedy = fmap testParserCase cases
 where
  parser = fmap (T.pack . toList) (greedyP (charP 'h')) :: TestParser Text
  cases =
    [ ParserCase "empty" parser "" (suc "" 0)
    , ParserCase "non-empty" parser "hi" (suc "h" 1)
    , ParserCase "repeat" parser "hhi" (suc "hh" 1)
    , ParserCase "full" parser "hhh" (suc "hhh" 0)
    , ParserCase "non-match" parser "bye" (suc "" 3)
    ]

testGreedy1 :: [TestTree]
testGreedy1 = fmap testParserCase cases
 where
  parser = fmap (T.pack . toList) (greedy1P (charP 'h')) :: TestParser Text
  cases =
    [ ParserCase "empty" parser "" (err (Span 0 0) (ReasonExpect "h" ""))
    , ParserCase "non-empty" parser "hi" (suc "h" 1)
    , ParserCase "repeat" parser "hhi" (suc "hh" 1)
    , ParserCase "full" parser "hhh" (suc "hhh" 0)
    , ParserCase "non-match" parser "bye" (err (Span 1 3) (ReasonExpect "h" "b"))
    ]

testOr :: [TestTree]
testOr = fmap testParserCase cases
 where
  parser = textP "h" <|> textP "xi" :: TestParser Text
  cases =
    [ ParserCase "empty" parser "" $
        errAlt
          (Span 0 0)
          [ (AltPhaseBranch, Span 0 0, ReasonExpect "h" "")
          , (AltPhaseBranch, Span 0 0, ReasonExpect "xi" "")
          ]
    , ParserCase "first" parser "hi" (suc "h" 1)
    , ParserCase "second" parser "xi" (suc "xi" 0)
    , ParserCase "non-match" parser "bye" $
        errAlt
          (Span 0 3)
          [ (AltPhaseBranch, Span 1 3, ReasonExpect "h" "b")
          , (AltPhaseBranch, Span 2 3, ReasonExpect "xi" "by")
          ]
    ]

testAlt :: [TestTree]
testAlt = fmap testParserCase cases
 where
  parser = altP [textP "h", "y" <$ headP, textP "xi"] :: TestParser Text
  cases =
    [ ParserCase "empty" parser "" $
        errAlt
          (Span 0 0)
          [ (AltPhaseBranch, Span 0 0, ReasonExpect "h" "")
          , (AltPhaseBranch, Span 0 0, ReasonDemand 1 0)
          , (AltPhaseBranch, Span 0 0, ReasonExpect "xi" "")
          ]
    , ParserCase "first" parser "hi" (suc "h" 1)
    , ParserCase "middle" parser "zi" (suc "y" 1)
    , ParserCase "last" parser "xi" (suc "y" 1)
    ]

testOptEmpty :: [TestTree]
testOptEmpty = fmap testParserCase cases
 where
  parser = optP emptyP :: TestParser (Maybe ())
  cases =
    [ ParserCase "empty" parser "" (suc Nothing 0)
    , ParserCase "non-empty" parser "hi" (suc Nothing 2)
    ]

testOpt :: [TestTree]
testOpt = fmap testParserCase cases
 where
  parser = optP (charP 'h') :: TestParser (Maybe Char)
  cases =
    [ ParserCase "non-match empty" parser "" (suc Nothing 0)
    , ParserCase "match" parser "hi" (suc (Just 'h') 1)
    , ParserCase "non-match" parser "bye" (suc Nothing 3)
    ]

testBind1 :: [TestTree]
testBind1 = fmap testParserCase cases
 where
  parser = charP 'x' >>= \c -> pure [c, c]
  cases =
    [ ParserCase "empty" parser "" (err (Span 0 0) (ReasonExpect "x" ""))
    , ParserCase "first" parser "hi" (err (Span 1 2) (ReasonExpect "x" "h"))
    , ParserCase "second" parser "xi" (suc "xx" 1)
    ]

testBind2 :: [TestTree]
testBind2 = fmap testParserCase cases
 where
  parser = headP >>= \x -> if x == 'x' then pure 'y' else emptyP
  cases =
    [ ParserCase "empty" parser "" (err (Span 0 0) (ReasonDemand 1 0))
    , ParserCase "first" parser "hi" (err (Span 1 2) ReasonEmpty)
    , ParserCase "second" parser "xi" (suc 'y' 1)
    ]

testThrow :: [TestTree]
testThrow = fmap testParserCase cases
 where
  cust = Error "boo"
  parser = throwP cust :: TestParser Int
  cases =
    [ ParserCase "empty" parser "" (err (Span 0 0) (ReasonCustom cust))
    , ParserCase "non-empty" parser "hi" (err (Span 0 2) (ReasonCustom cust))
    ]

testConsumeThrow :: [TestTree]
testConsumeThrow = fmap testParserCase cases
 where
  cust = Error "boo"
  parser = headP *> throwP cust :: TestParser Int
  cases =
    [ ParserCase "empty" parser "" (err (Span 0 0) (ReasonDemand 1 0))
    , ParserCase "non-empty" parser "hi" (err (Span 1 2) (ReasonCustom cust))
    ]

testOptThrow :: [TestTree]
testOptThrow = fmap testParserCase cases
 where
  cust = Error "boo"
  parser = optP (throwP cust) :: TestParser (Maybe Int)
  cases =
    [ ParserCase "empty" parser "" (suc Nothing 0)
    , ParserCase "non-empty" parser "hi" (suc Nothing 2)
    ]

testOptConsumeThrow :: [TestTree]
testOptConsumeThrow = fmap testParserCase cases
 where
  cust = Error "boo"
  parser = optP (headP *> throwP cust) :: TestParser (Maybe Int)
  cases =
    [ ParserCase "empty" parser "" (suc Nothing 0)
    , ParserCase "non-empty" parser "hi" (suc Nothing 2)
    ]

testThrowMixed :: [TestTree]
testThrowMixed = fmap testParserCase cases
 where
  cust = Error "boo"
  parser = throwP cust <|> pure 1 :: TestParser Int
  cases =
    [ ParserCase "non-empty" parser "hi" (suc 1 2)
    ]

testThrowMixedFlip :: [TestTree]
testThrowMixedFlip = fmap testParserCase cases
 where
  cust = Error "boo"
  parser = pure 1 <|> throwP cust :: TestParser Int
  cases =
    [ ParserCase "non-empty" parser "hi" (suc 1 2)
    ]

testBacktrack :: [TestTree]
testBacktrack = fmap testParserCase cases
 where
  parser = (textP "x" <|> textP "xz") <* (textP_ "z" *> endP)
  cases =
    [ ParserCase "non-empty" parser "xzz" (suc "xz" 0)
    ]

testLookPure :: [TestTree]
testLookPure = fmap testParserCase cases
 where
  parser = lookP (pure 1) :: TestParser Int
  cases =
    [ ParserCase "empty" parser "" (suc 1 0)
    , ParserCase "non-empty" parser "hi" (suc 1 2)
    ]

testLookSuccess :: [TestTree]
testLookSuccess = fmap testParserCase cases
 where
  parser = lookP headP
  cases =
    [ ParserCase "non-match empty" parser "" (errLook (Span 0 0) (Span 0 0) (ReasonDemand 1 0))
    , ParserCase "non-empty" parser "hi" (suc 'h' 2)
    ]

testLookFailure :: [TestTree]
testLookFailure = fmap testParserCase cases
 where
  cust = Error "boo"
  parser = lookP (headP *> throwP cust) :: TestParser Char
  cases =
    [ ParserCase "non-match empty" parser "" (errLook (Span 0 0) (Span 0 0) (ReasonDemand 1 0))
    , ParserCase "non-empty" parser "hi" (errLook (Span 0 2) (Span 1 2) (ReasonCustom cust))
    ]

testTakeWhile :: [TestTree]
testTakeWhile = fmap testParserCase cases
 where
  parser = takeWhileP (== 'h') :: TestParser Text
  cases =
    [ ParserCase "empty" parser "" (suc "" 0)
    , ParserCase "non-match" parser "i" (suc "" 1)
    , ParserCase "match" parser "hi" (suc "h" 1)
    , ParserCase "match 2" parser "hhi" (suc "hh" 1)
    , ParserCase "match end" parser "hh" (suc "hh" 0)
    ]

testTakeWhile1 :: [TestTree]
testTakeWhile1 = fmap testParserCase cases
 where
  parser = takeWhile1P (== 'h') :: TestParser Text
  cases =
    [ ParserCase "empty" parser "" (err (Span 0 0) ReasonTakeNone)
    , ParserCase "non-match" parser "i" (err (Span 0 1) ReasonTakeNone)
    , ParserCase "match" parser "hi" (suc "h" 1)
    , ParserCase "match 2" parser "hhi" (suc "hh" 1)
    , ParserCase "match end" parser "hh" (suc "hh" 0)
    ]

testDropWhile :: [TestTree]
testDropWhile = fmap testParserCase cases
 where
  parser = dropWhileP (== 'h') :: TestParser Int
  cases =
    [ ParserCase "empty" parser "" (suc 0 0)
    , ParserCase "non-match" parser "i" (suc 0 1)
    , ParserCase "match" parser "hi" (suc 1 1)
    , ParserCase "match 2" parser "hhi" (suc 2 1)
    , ParserCase "match end" parser "hh" (suc 2 0)
    ]

testDropWhile1 :: [TestTree]
testDropWhile1 = fmap testParserCase cases
 where
  parser = dropWhile1P (== 'h') :: TestParser Int
  cases =
    [ ParserCase "empty" parser "" (err (Span 0 0) ReasonTakeNone)
    , ParserCase "non-match" parser "i" (err (Span 0 1) ReasonTakeNone)
    , ParserCase "match" parser "hi" (suc 1 1)
    , ParserCase "match 2" parser "hhi" (suc 2 1)
    , ParserCase "match end" parser "hh" (suc 2 0)
    ]

testInfixR :: [TestTree]
testInfixR = fmap testParserCase cases
 where
  sub d = takeWhile1P (\c -> c == d || c == '+')
  parser = infixRP "+" (sub 'x') (sub 'y') :: TestParser (Text, Text)
  parserR = infixRP "+" (textP "x") (textP "x+x") :: TestParser (Text, Text)
  parserL = infixRP "+" (textP "x+x") (textP "x") :: TestParser (Text, Text)
  cases =
    [ ParserCase "empty" parser "" (err (Span 0 0) ReasonEmpty)
    , ParserCase "fail delim" parser "xy" (err (Span 0 2) ReasonEmpty)
    , ParserCase "fail first" parser "+y" (errInfix (Span 0 2) [(0, InfixPhaseLeft, Span 0 0, ReasonTakeNone)])
    , ParserCase "fail second" parser "x+" (errInfix (Span 0 2) [(1, InfixPhaseRight, Span 2 2, ReasonTakeNone)])
    , ParserCase "match" parser "x+y" (suc ("x", "y") 0)
    , ParserCase "match multi" parser "x++y" (suc ("x", "+y") 0)
    , ParserCase "match rassoc" parserR "x+x+x" (suc ("x", "x+x") 0)
    , ParserCase "fail lassoc" parserL "x+x+x" (errInfix (Span 0 5) [(1, InfixPhaseLeft, Span 1 1, ReasonExpect "x+x" "x")])
    ]

testSomeInfixR :: [TestTree]
testSomeInfixR = fmap testParserCase cases
 where
  sub d = takeWhile1P (\c -> c == d || c == '+')
  parser = someInfixRP "+" (sub 'x') (sub 'y') :: TestParser (Text, Text)
  parserR = someInfixRP "+" (textP "x") (textP "x+x") :: TestParser (Text, Text)
  parserL = someInfixRP "+" (textP "x+x") (textP "x") :: TestParser (Text, Text)
  cases =
    [ ParserCase "empty" parser "" (err (Span 0 0) ReasonEmpty)
    , ParserCase "fail delim" parser "xy" (err (Span 0 2) ReasonEmpty)
    , ParserCase "fail first" parser "+y" (errInfix (Span 0 2) [(0, InfixPhaseLeft, Span 0 0, ReasonTakeNone)])
    , ParserCase "fail second" parser "x+" (errInfix (Span 0 2) [(1, InfixPhaseRight, Span 2 2, ReasonTakeNone)])
    , ParserCase "match" parser "x+y" (suc ("x", "y") 0)
    , ParserCase "match multi" parser "x++y" (suc ("x", "+y") 0)
    , ParserCase "match rassoc" parserR "x+x+x" (suc ("x", "x+x") 0)
    , ParserCase "match lassoc" parserL "x+x+x" (suc ("x+x", "x") 0)
    ]

testBreak :: [TestTree]
testBreak = fmap testParserCase cases
 where
  parser = breakP "+" (takeWhile1P (== 'x'))
  parserR = breakP "+" (textP "x")
  parserL = breakP "+" (textP "x+x")
  cases =
    [ ParserCase "empty" parser "" (err (Span 0 0) ReasonEmpty)
    , ParserCase "fail delim" parser "x" (err (Span 0 1) ReasonEmpty)
    , ParserCase "fail first" parser "y+" (errInfix (Span 0 2) [(1, InfixPhaseLeft, Span 0 1, ReasonTakeNone)])
    , ParserCase "match" parser "x+x+y" (suc "x" 3)
    , ParserCase "match rassoc" parserR "x+x+x" (suc "x" 3)
    , ParserCase "fail lassoc" parserL "x+x+x" (errInfix (Span 0 5) [(1, InfixPhaseLeft, Span 1 1, ReasonExpect "x+x" "x")])
    ]

testSomeBreak :: [TestTree]
testSomeBreak = fmap testParserCase cases
 where
  parser = someBreakP "+" (takeWhile1P (== 'x'))
  parserR = someBreakP "+" (textP "x")
  parserL = someBreakP "+" (textP "x+x")
  cases =
    [ ParserCase "empty" parser "" (err (Span 0 0) ReasonEmpty)
    , ParserCase "fail delim" parser "x" (err (Span 0 1) ReasonEmpty)
    , ParserCase "fail first" parser "y+" (errInfix (Span 0 2) [(1, InfixPhaseLeft, Span 0 1, ReasonTakeNone)])
    , ParserCase "match" parser "x+x+y" (suc "x" 3)
    , ParserCase "match rassoc" parserR "x+x+x" (suc "x" 3)
    , ParserCase "match lassoc" parserL "x+x+x" (suc "x+x" 1)
    ]

testSplit :: [TestTree]
testSplit = fmap testParserCase cases
 where
  parser = fmap toList (splitP "+" (takeWhile1P (== 'x')))
  cases =
    [ ParserCase "empty" parser "" (suc [] 0)
    , ParserCase "single" parser "x" (suc ["x"] 0)
    , ParserCase "no delim" parser "xy" (suc ["x"] 1)
    , ParserCase "double" parser "x+x" (suc ["x", "x"] 0)
    , ParserCase "triple" parser "x+x+x" (suc ["x", "x", "x"] 0)
    , ParserCase "two + fail" parser "x+x+y" (suc ["x", "x"] 2)
    , ParserCase "two fail" parser "x+xy" (suc ["x", "x"] 1)
    , ParserCase "fail first" parser "y+x" (suc [] 3)
    , ParserCase "fail second" parser "x+y" (suc ["x"] 2)
    ]

testSplit1 :: [TestTree]
testSplit1 = fmap testParserCase cases
 where
  parser = fmap toList (split1P "+" (takeWhile1P (== 'x')))
  cases =
    [ ParserCase "empty" parser "" (err (Span 0 0) (ReasonSplitComp SplitCompGE 1 "+" 0))
    , ParserCase "single" parser "x" (suc ["x"] 0)
    , ParserCase "no delim" parser "xy" (suc ["x"] 1)
    , ParserCase "double" parser "x+x" (suc ["x", "x"] 0)
    , ParserCase "triple" parser "x+x+x" (suc ["x", "x", "x"] 0)
    , ParserCase "two + fail" parser "x+x+y" (suc ["x", "x"] 2)
    , ParserCase "two fail" parser "x+xy" (suc ["x", "x"] 1)
    , ParserCase "fail first" parser "y+x" (err (Span 0 3) (ReasonSplitComp SplitCompGE 1 "+" 0))
    , ParserCase "fail second" parser "x+y" (suc ["x"] 2)
    ]

testSpan :: TestTree
testSpan = testCase "span" $ do
  let p :: Parser Void (Span Int, Span Int) = do
        charP_ 'x'
        r1 <- spanP
        charP_ 'y'
        r2 <- spanP
        charP_ 'z'
        pure (r1, r2)
  let doc = "xyz"
  case parse p doc of
    Left e -> throwIO e
    Right (r1, r2) -> do
      r1 @?= Span 1 3
      r2 @?= Span 2 3
  let v1 = calculateLineCol doc
  lookupLineCol (-1) v1 @?= (0, 0)
  lookupLineCol 0 v1 @?= (0, 0)
  lookupLineCol 1 v1 @?= (0, 1)
  lookupLineCol 2 v1 @?= (0, 2)
  lookupLineCol 3 v1 @?= (0, 2)
  let v2 = calculateLineCol "a\nbc\nd"
  lookupLineCol (-1) v2 @?= (0, 0)
  lookupLineCol 0 v2 @?= (0, 0)
  lookupLineCol 1 v2 @?= (0, 1)
  lookupLineCol 2 v2 @?= (1, 0)
  lookupLineCol 3 v2 @?= (1, 1)
  lookupLineCol 4 v2 @?= (1, 2)
  lookupLineCol 5 v2 @?= (2, 0)
  lookupLineCol 6 v2 @?= (2, 0)

testJson :: TestTree
testJson = testGroup "json" (fmap test cases)
 where
  test (name, str, expected) = testCase name $ do
    let actual = either (const Nothing) Just (parse jsonParser str)
    actual @?= expected
  trueVal = JsonBool True
  falseVal = JsonBool False
  arrVal = JsonArray . Seq.fromList
  objVal = JsonObject . Seq.fromList
  cases =
    [ ("empty", "", Nothing)
    , ("bad", "bad", Nothing)
    , ("null", "null", Just JsonNull)
    , ("true", "true", Just trueVal)
    , ("false", "false", Just falseVal)
    , ("arr0", "[]", Just (arrVal []))
    , ("arr1", "[null]", Just (arrVal [JsonNull]))
    , ("arr2", "[null, false]", Just (arrVal [JsonNull, falseVal]))
    , ("arr3", "[null, false, true]", Just (arrVal [JsonNull, falseVal, trueVal]))
    , ("arrx", "[null,]", Nothing)
    , ("str0", "\"\"", Just (JsonString ""))
    , ("str1", "\"x\"", Just (JsonString "x"))
    , ("str2", "\"xy\"", Just (JsonString "xy"))
    , ("str3", "\"xyz\"", Just (JsonString "xyz"))
    , ("str4", "\"xy\\\"z\"", Just (JsonString "xy\"z"))
    , ("obj0", "{}", Just (objVal []))
    , ("obj1", "{\"x\": true}", Just (objVal [("x", trueVal)]))
    , ("obj2", "{\"x\": true, \"y\": false}", Just (objVal [("x", trueVal), ("y", falseVal)]))
    , ("num0", "0", Just (JsonNum 0))
    , ("num1", "123", Just (JsonNum 123))
    , ("num2", "123.45", Just (JsonNum 123.45))
    , ("num3", "1e100", Just (JsonNum (read "1e100")))
    , ("num4", "{\"x\": 1e100, \"y\": 123.45}", Just (objVal [("x", JsonNum (read "1e100")), ("y", JsonNum 123.45)]))
    ]

testSexp :: TestTree
testSexp = testGroup "sexp" (fmap test cases)
 where
  test (name, str, expected) = testCase name $ do
    let actual = either (const Nothing) Just (parse sexpParser str)
    actual @?= expected
  numSexp = SexpAtom (AtomInt 1)
  sciExpSexp = SexpAtom (AtomSci 1)
  identSexp = SexpAtom (AtomIdent "abc")
  stringSexp = SexpAtom (AtomString "xyz")
  sciSexp = SexpAtom (AtomSci 3.14)
  emptyList = SexpList Empty
  singletonList = SexpList (Seq.singleton numSexp)
  pairList = SexpList (Seq.fromList [numSexp, numSexp])
  cases =
    [ ("empty", "", Nothing)
    , ("empty list", "()", Just emptyList)
    , ("singleton list", "(1)", Just singletonList)
    , ("singleton empty list", "(())", Just (SexpList (Seq.fromList [emptyList])))
    , ("singleton nested list", "((1))", Just (SexpList (Seq.fromList [singletonList])))
    , ("num", "1", Just numSexp)
    , ("num neg", "-1", Just (SexpAtom (AtomInt -1)))
    , ("ident", "abc", Just identSexp)
    , ("string", "\"xyz\"", Just stringSexp)
    , ("sci", "3.14", Just sciSexp)
    , ("sci neg", "-3.14", Just (SexpAtom (AtomSci -3.14)))
    , ("sci neg exp", "314e-2", Just sciSexp)
    , ("sci neg exp 2", "31.4e-1", Just sciSexp)
    , ("sci pos exp 3", "0.314e1", Just sciSexp)
    , ("sci dec exp", "1.0", Just sciExpSexp)
    , ("sci exp", "1e0", Just sciExpSexp)
    , ("sci dec exp 2", "1.0e0", Just sciExpSexp)
    , ("multi list", "(1 abc \"xyz\" 3.14)", Just (SexpList (Seq.fromList [numSexp, identSexp, stringSexp, sciSexp])))
    , ("pair nested list", "((1 1) (1 1))", Just (SexpList (Seq.fromList [pairList, pairList])))
    ]

testArith :: TestTree
testArith = testGroup "arith" (fmap test cases)
 where
  test (name, str, expected) = testCase name $ do
    let actual = either (const Nothing) Just (parse arithParser str)
    actual @?= expected
  cases =
    [ ("plus", "1 +x+ 2", Just (ArithAdd (ArithNum 1) (ArithAdd (ArithVar "x") (ArithNum 2))))
    , ("prec1", "1 + 2 * 3", Just (ArithAdd (ArithNum 1) (ArithMul (ArithNum 2) (ArithNum 3))))
    , ("prec2", "1 * 2 + 3", Just (ArithAdd (ArithMul (ArithNum 1) (ArithNum 2)) (ArithNum 3)))
    ]

main :: IO ()
main =
  defaultMain $
    testGroup
      "Looksee"
      [ testBasic
      , testSpan
      , testJson
      , testSexp
      , testArith
      ]