wireform-core-0.2.0.0: test/Wireform/Parser/Test.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Wireform.Parser.Test (spec) where
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.Int
import Data.Word
import Test.QuickCheck
import Test.Syd
import Wireform.Parser
import Wireform.Parser.Driver (parseByteString)
import Wireform.Parser.Error
import Wireform.Parser.Internal (Pure)
type P = Parser Pure String
ok :: (Show a, Eq a) => Either (ParseError String) a -> a -> Expectation
ok (Right a) expected = a `shouldBe` expected
ok (Left e) _ = expectationFailure ("parse failed: " <> show e)
bad :: (Show a) => Either (ParseError String) a -> Expectation
bad (Left _) = pure ()
bad (Right a) = expectationFailure ("expected failure, got: " <> show a)
spec :: Spec
spec = describe "Parser" $ do
describe "byte primitives" $ do
it "anyWord8" $
ok (parseByteString (anyWord8 :: P Word8) "\x42") 0x42
it "anyWord8 on empty" $
bad (parseByteString (anyWord8 :: P Word8) "")
it "anyWord16 native" $
ok (parseByteString (anyWord16 :: P Word16) "\x01\x02") (if isLE then 0x0201 else 0x0102)
it "anyWord32be" $
ok (parseByteString (anyWord32be :: P Word32) "\x00\x00\x01\x00") 256
it "anyWord32le" $
ok (parseByteString (anyWord32le :: P Word32) "\x00\x01\x00\x00") 256
it "anyWord64le" $
ok (parseByteString (anyWord64le :: P Word64) "\x01\x00\x00\x00\x00\x00\x00\x00") 1
it "anyWord64be" $
ok (parseByteString (anyWord64be :: P Word64) "\x00\x00\x00\x00\x00\x00\x00\x01") 1
describe "signed integers" $ do
it "anyInt8" $
ok (parseByteString (anyInt8 :: P Int8) "\xFF") (-1)
it "anyInt16be" $
ok (parseByteString (anyInt16be :: P Int16) "\xFF\xFE") (-2)
it "anyInt32le" $
ok (parseByteString (anyInt32le :: P Int32) "\xFE\xFF\xFF\xFF") (-2)
describe "floating point" $ do
it "anyFloatle parses 1.0" $
ok (parseByteString (anyFloatle :: P Float) "\x00\x00\x80\x3F") 1.0
it "anyDoublebe parses 1.0" $
ok (parseByteString (anyDoublebe :: P Double) "\x3F\xF0\x00\x00\x00\x00\x00\x00") 1.0
describe "sequencing" $ do
it "two bytes" $ do
let p = (,) <$> anyWord8 <*> anyWord8 :: P (Word8, Word8)
ok (parseByteString p "\xAA\xBB") (0xAA, 0xBB)
it "insufficient for sequence" $ do
let p = (,) <$> anyWord8 <*> anyWord8 :: P (Word8, Word8)
bad (parseByteString p "\xAA")
it "three-way sequence" $ do
let p = (,,) <$> anyWord8 <*> anyWord16be <*> anyWord32be :: P (Word8, Word16, Word32)
ok (parseByteString p "\x01\x00\x02\x00\x00\x00\x03") (1, 2, 3)
describe "alternatives" $ do
it "second branch" $ do
let p = (word8 0x01 *> pure "a") <|> (word8 0x02 *> pure "b") :: P String
ok (parseByteString p "\x02") "b"
it "first branch" $ do
let p = (word8 0x01 *> pure "a") <|> (word8 0x02 *> pure "b") :: P String
ok (parseByteString p "\x01") "a"
it "both fail" $ do
let p = (word8 0x01 *> pure "a") <|> (word8 0x02 *> pure "b") :: P String
bad (parseByteString p "\x03")
it "backtracking restores position" $ do
let p = (word8 0x01 *> word8 0x99) <|> (word8 0x01 *> word8 0x02) :: P ()
ok (parseByteString p "\x01\x02") ()
it "three-way alternative" $ do
let p = word8 0x01 <|> word8 0x02 <|> word8 0x03 :: P ()
ok (parseByteString p "\x03") ()
describe "cut/err" $ do
it "cut converts Fail to Err" $ do
let p = cut (word8 0x01 *> anyWord8 *> word8 0xFF) "bad" :: P ()
case parseByteString p "\x01\x42\xAA" of
Left (ParseErr _ e) -> e `shouldBe` "bad"
other -> expectationFailure (show other)
it "err produces Err" $ do
let p = err "fatal" :: P ()
case parseByteString p "x" of
Left (ParseErr _ e) -> e `shouldBe` "fatal"
other -> expectationFailure (show other)
it "Err bypasses alternatives" $ do
let p = (err "x" :: P ()) <|> pure ()
case parseByteString p "x" of
Left (ParseErr _ _) -> pure ()
other -> expectationFailure (show other)
it "try converts Err to Fail" $ do
let p = try (err "x" :: P ()) <|> pure ()
ok (parseByteString p "x") ()
describe "withError" $ do
it "catches and handles Err" $ do
let p = withError (\e -> pure (e <> "!")) (err "boom") :: P String
ok (parseByteString p "x") "boom!"
describe "byte matching" $ do
it "byteString match" $
ok (parseByteString (byteString "hello" :: P ()) "hello") ()
it "byteString mismatch" $
bad (parseByteString (byteString "hello" :: P ()) "hxllo")
it "byteString empty" $
ok (parseByteString (byteString "" :: P ()) "anything") ()
describe "takeBs" $ do
it "takes n bytes" $
ok (parseByteString (takeBs 3 :: P ByteString) "abcde") "abc"
it "fails if short" $
bad (parseByteString (takeBs 10 :: P ByteString) "abc")
it "take 0 bytes" $
ok (parseByteString (takeBs 0 :: P ByteString) "x") ""
describe "skip" $ do
it "skip and continue" $ do
let p = skip 2 *> anyWord8 :: P Word8
ok (parseByteString p "\x00\x00\x42") 0x42
describe "takeRest" $ do
it "consumes all" $
ok (parseByteString (takeRest :: P ByteString) "hello") "hello"
it "empty on empty" $
ok (parseByteString (takeRest :: P ByteString) "") ""
describe "eof" $ do
it "succeeds at end" $
ok (parseByteString (eof :: P ()) "") ()
it "fails with remaining" $
bad (parseByteString (eof :: P ()) "x")
describe "atEnd / remaining" $ do
it "atEnd true on empty" $
ok (parseByteString (atEnd :: P Bool) "") True
it "atEnd false with data" $
ok (parseByteString (atEnd :: P Bool) "x") False
it "remaining counts bytes" $
ok (parseByteString (remaining :: P Int) "hello") 5
describe "UTF-8" $ do
it "ASCII char" $
ok (parseByteString (anyAsciiChar :: P Char) "A") 'A'
it "rejects non-ASCII" $
bad (parseByteString (satisfyAscii (const True) :: P Char) "\xC3\xA9")
it "2-byte UTF-8 (é)" $
ok (parseByteString (anyChar :: P Char) "\xC3\xA9") '\x00E9'
it "3-byte UTF-8 (€)" $
ok (parseByteString (anyChar :: P Char) "\xE2\x82\xAC") '\x20AC'
it "4-byte UTF-8 (😀)" $
ok (parseByteString (anyChar :: P Char) "\xF0\x9F\x98\x80") '\x1F600'
describe "satisfy" $ do
it "satisfy matches" $
ok (parseByteString (satisfy (== 'A') :: P Char) "A") 'A'
it "satisfy rejects" $
bad (parseByteString (satisfy (== 'A') :: P Char) "B")
describe "ASCII decimal" $ do
it "parses number" $
ok (parseByteString (anyAsciiDecimalWord :: P Word) "12345x") 12345
it "fails on non-digit" $
bad (parseByteString (anyAsciiDecimalWord :: P Word) "abc")
it "single digit" $
ok (parseByteString (anyAsciiDecimalWord :: P Word) "0") 0
describe "hex" $ do
it "parses hex" $
ok (parseByteString (anyAsciiHexWord :: P Word) "FF") 255
it "mixed case" $
ok (parseByteString (anyAsciiHexWord :: P Word) "aB") 0xAB
describe "lookahead and negative lookahead" $ do
it "lookahead does not consume" $ do
let p = lookahead anyWord8 *> anyWord8 :: P Word8
ok (parseByteString p "\x42") 0x42
it "fails succeeds" $ do
let p = fails (word8 0x01) :: P ()
ok (parseByteString p "\x02") ()
it "notFollowedBy" $ do
let p = notFollowedBy (word8 0x01) :: P ()
ok (parseByteString p "\x02") ()
describe "many/some/many_/some_" $ do
it "many_ then read" $ do
let p = many_ (word8 0x41) *> anyWord8 :: P Word8
ok (parseByteString p "\x41\x41\x42") 0x42
it "many_ on empty" $
ok (parseByteString (many_ (word8 0x41) *> eof :: P ()) "") ()
it "many collects" $ do
let p = many (word8 0x41 *> pure 'A') :: P [Char]
ok (parseByteString p "\x41\x41\x42") "AA"
it "some requires one" $
bad (parseByteString (some (word8 0x41) :: P [()]) "\x42")
describe "isolate" $ do
it "isolate consumes exact bytes" $ do
let p = isolate 3 (takeBs 3) :: P ByteString
ok (parseByteString p "abcdef") "abc"
it "isolate fails if inner underconsumed" $ do
let p = isolate 3 (takeBs 2) :: P ByteString
bad (parseByteString p "abcdef")
describe "chainl" $ do
it "left-associative chain" $ do
let digit = anyAsciiDecimalInt :: P Int
plus = word8 0x2B *> digit
p = chainl (+) digit plus
ok (parseByteString p "1+2+3x") 6
describe "position and span" $ do
it "getPos returns 0 at start" $ do
ok (parseByteString (getPos :: P Pos) "hello") (Pos 0)
it "getPos advances" $ do
let p = skip 3 *> getPos :: P Pos
ok (parseByteString p "hello") (Pos 3)
it "byteStringOf captures consumed bytes" $ do
let p = byteStringOf (skip 3) :: P ByteString
ok (parseByteString p "hello") "hel"
it "withSpan captures span" $ do
let p = withSpan (skip 3) (\_ (Span s e) -> pure (subPos e s)) :: P Int
ok (parseByteString p "hello") 3
describe "skipBack" $ do
it "skips backward and re-reads" $ do
let p = do
_ <- anyWord8
_ <- anyWord8
skipBack 2
anyWord8 :: P Word8
ok (parseByteString p "\xAA\xBB") 0xAA
it "fails when skipping past start" $ do
let p = skipBack 1 :: P ()
bad (parseByteString p "x")
it "skip forward then back" $ do
let p = do
skip 3
skipBack 2
anyWord8 :: P Word8
-- "hello" -> skip 3 -> at 'l' -> back 2 -> at 'e'
ok (parseByteString p "hello") (fromIntegral (fromEnum 'e'))
describe "marks" $ do
it "mark and restore" $ do
let p = do
m <- mark
_ <- anyWord8
_ <- anyWord8
restore m
anyWord8 :: P Word8
ok (parseByteString p "\xAA\xBB") 0xAA
where
isLE :: Bool
isLE =
BS.pack [1, 0]
== BS.pack
( let w = 1 :: Word16
in [fromIntegral w, fromIntegral (w `div` 256)]
)