packages feed

benc-0.1.1.0: test/Test.hs

{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}  -- Arbitrary instances
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck

import Control.Applicative
import Control.Monad
import qualified Data.ByteString as B
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Char8 as BC
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy.Char8 as BLC
import qualified Data.Foldable as F
import qualified Data.Map as M
import qualified Data.Vector as V

import qualified Data.Bencode.Decode as D
import qualified Data.Bencode.Encode as E
import qualified Data.Bencode.Type as Ben

main :: IO ()
main = defaultMain $ localOption (QuickCheckTests 2000) $ testGroup "Tests"
  [ astTests
  , decodeTests
  , encodeTests
  , encodeDecodeTests
  ]

-- Would like to test Data.Bencode.AST.parseOnly but since it's not exposed
-- test via D.decode D.value.
astTests :: TestTree
astTests = testGroup "AST"
  [ testGroup "valid Bencode"
    [ testGroup "string"
      [ testCase "0:" $ D.decode D.value "0:" @?= Right (Ben.String "")
      , testCase "3:foo" $ D.decode D.value "3:foo" @?= Right (Ben.String "foo")
      , testCase "<binary>" $ D.decode D.value "2:\x00\xff" @?= Right (Ben.String "\x00\xff")
      ]
    , testGroup "integer"
      [ testCase "i0e" $ D.decode D.value "i0e" @?= Right (Ben.Integer 0)
      , testCase "i1e" $ D.decode D.value "i1e" @?= Right (Ben.Integer 1)
      , testCase "i-1e" $ D.decode D.value "i-1e" @?= Right (Ben.Integer (-1))
      , testCase "i98765432109876543210e" $ D.decode D.value "i98765432109876543210e" @?= Right (Ben.Integer 98765432109876543210)
      ]
    , testGroup "list"
      [ testCase "le" $ D.decode D.value "le" @?= Right (Ben.List [])
      , testCase "li1e3:fooe" $ D.decode D.value "li1e3:fooe" @?= Right (Ben.List [Ben.Integer 1, Ben.String "foo"])
      , testCase "lllll3:fooeeeee" $ D.decode D.value "lllll3:fooeeeee" @?= Right (Ben.List [Ben.List [Ben.List [Ben.List [Ben.List [Ben.String "foo"]]]]])
      ]
    , testGroup "dict"
      [ testCase "de" $ D.decode D.value "de" @?= Right (Ben.Dict [])
      , testCase "d3:fooi0ee" $ D.decode D.value "d3:fooi1ee" @?= Right (Ben.Dict [("foo", Ben.Integer 1)])
      , testCase "d3:bari0e3:fooi1ee" $ D.decode D.value "d3:bari0e3:fooi1ee" @?= Right (Ben.Dict [("bar", Ben.Integer 0), ("foo", Ben.Integer 1)])
      , testCase "d3:food3:foodeee" $ D.decode D.value "d3:food3:foodeee" @?= Right (Ben.Dict [("foo", Ben.Dict [("foo", Ben.Dict [])])])
      ]
    ]
  , testGroup "invalid Bencode fails"
    [ testGroup "no items"
      [ testCase "<empty>" $ D.decode D.value "" @?= Left "ParseErrorAt 0: ExpectedOneOfButGot [Digit,'i','l','d'] EOF"
      , testCase "a" $ D.decode D.value "a" @?= Left "ParseErrorAt 0: ExpectedOneOfButGot [Digit,'i','l','d'] 'a'"
      , testCase ":" $ D.decode D.value ":" @?= Left "ParseErrorAt 0: ExpectedOneOfButGot [Digit,'i','l','d'] ':'"
      ]
    , testGroup "string"
      [ let x = show (2^(64 :: Int) :: Integer) {- overflows to 0 -} in
        testCase (x <> ":") $ D.decode D.value (BC.pack x <> ":") @?= Left "ParseErrorAt 0: TooLargeStringLength"
      , let x = show (fromIntegral (maxBound :: Int) + 1 :: Integer) {- overflows to -1 -} in
        testCase (x <> ":") $ D.decode D.value (BC.pack x <> ":") @?= Left "ParseErrorAt 0: TooLargeStringLength"
      , testCase "3" $ D.decode D.value "3" @?= Left "ParseErrorAt 1: ExpectedOneOfButGot [':'] EOF"
      , testCase "3foo" $ D.decode D.value "3foo" @?= Left "ParseErrorAt 1: ExpectedOneOfButGot [':'] 'f'"
      , testCase "4:foo" $ D.decode D.value "4:foo" @?= Left "ParseErrorAt 0: TooLargeStringLength"
      ]
    ]
    , testGroup "integer"
      [ testCase "i" $ D.decode D.value "i" @?= Left "ParseErrorAt 1: ExpectedOneOfButGot [Digit] EOF"
      , testCase "i01e" $ D.decode D.value "i01" @?= Left "ParseErrorAt 2: ExpectedOneOfButGot ['e'] '1'"
      , testCase "i-" $ D.decode D.value "i-" @?= Left "ParseErrorAt 2: ExpectedOneOfButGot [NonZeroDigit] EOF"
      , testCase "i-0e" $ D.decode D.value "i-0e" @?= Left "ParseErrorAt 2: ExpectedOneOfButGot [NonZeroDigit] 'e'"
      , testCase "ifooe" $ D.decode D.value "ifooe" @?= Left "ParseErrorAt 1: ExpectedOneOfButGot [Digit,'-'] 'f'"
      , testCase "i12" $ D.decode D.value "i12" @?= Left "ParseErrorAt 3: ExpectedOneOfButGot ['e'] EOF"
      , testCase "i12d" $ D.decode D.value "i12d" @?= Left "ParseErrorAt 3: ExpectedOneOfButGot ['e'] 'd'"
      ]
    , testGroup "list"
      [ testCase "l" $ D.decode D.value "l" @?= Left "ParseErrorAt 1: ExpectedOneOfButGot [Digit,'i','l','d','e'] EOF"
      , testCase "lfoo" $ D.decode D.value "lfoo" @?= Left "ParseErrorAt 1: ExpectedOneOfButGot [Digit,'i','l','d','e'] 'f'"
      , testCase "l3:foo" $ D.decode D.value "l3:foo" @?= Left "ParseErrorAt 6: ExpectedOneOfButGot [Digit,'i','l','d','e'] EOF"
      , testCase "l3:foobar" $ D.decode D.value "l3:foobar" @?= Left "ParseErrorAt 6: ExpectedOneOfButGot [Digit,'i','l','d','e'] 'b'"
      ]
    , testGroup "dict"
      [ testCase "d" $ D.decode D.value "d" @?= Left "ParseErrorAt 1: ExpectedOneOfButGot [Digit,'e'] EOF"
      , testCase "dfoo" $ D.decode D.value "dfoo" @?= Left "ParseErrorAt 1: ExpectedOneOfButGot [Digit,'e'] 'f'"
      , testCase "d3:foo" $ D.decode D.value "d3:foo" @?= Left "ParseErrorAt 6: ExpectedOneOfButGot [Digit,'i','l','d'] EOF"
      , testCase "d3:foobar" $ D.decode D.value "d3:foobar" @?= Left "ParseErrorAt 6: ExpectedOneOfButGot [Digit,'i','l','d'] 'b'"
      , testCase "di1e3:foo" $ D.decode D.value "di1e3:foo" @?= Left "ParseErrorAt 1: ExpectedOneOfButGot [Digit,'e'] 'i'"
      , testCase "d3:fooi0e3:bari1ee" $ D.decode D.value "d3:fooi0e3:bari1ee" @?= Left "ParseErrorAt 9: UnsortedKeys \"foo\" \"bar\""
      ]
    , testGroup "leftover"
      [ testCase "3:foo3:bar" $ D.decode D.value "3:foo3:bar" @?= Left "ParseErrorAt 5: ExpectedEOF"
      ]
  ]

decodeTests :: TestTree
decodeTests = testGroup "Decode"
  [ testGroup "string"
    [ testCase "0:" $ D.decode D.string "0:" @?= Right ""
    , testCase "3:foo" $ D.decode D.string "3:foo" @?= Right "foo"
    , testCase "type mismatch integer" $ D.decode D.string "i0e" @?= Left "TypeMismatch String Integer"
    , testCase "type mismatch list" $ D.decode D.string "le" @?= Left "TypeMismatch String List"
    , testCase "type mismatch dict" $ D.decode D.string "de" @?= Left "TypeMismatch String Dict"
    ]
  , testGroup "integer"
    [ testCase "i0e" $ D.decode D.integer "i0e" @?= Right 0
    , testCase "i-32e" $ D.decode D.integer "i-32e" @?= Right (-32)
    , testCase "i98765432109876543210e" $ D.decode D.integer "i98765432109876543210e" @?= Right 98765432109876543210
    , testCase "type mismatch string" $ D.decode D.integer "0:" @?= Left "TypeMismatch Integer String"
    , testCase "type mismatch list" $ D.decode D.integer "le" @?= Left "TypeMismatch Integer List"
    , testCase "type mismatch dict" $ D.decode D.integer "de" @?= Left "TypeMismatch Integer Dict"
    ]
  , testGroup "list"
    [ testCase "le" $ D.decode (D.list D.value) "le" @?= Right []
    , testCase "l3:foo3:bare" $ D.decode (D.list D.string) "l3:foo3:bare" @?= Right ["foo", "bar"]
    , testCase "elem type mismatch" $ D.decode (D.list D.string) "l3:fooi1ee" @?= Left "TypeMismatch String Integer"
    , testCase "type mismatch string" $ D.decode (D.list D.value) "0:" @?= Left "TypeMismatch List String"
    , testCase "type mismatch integer" $ D.decode (D.list D.value) "i0e" @?= Left "TypeMismatch List Integer"
    , testCase "type mismatch dict" $ D.decode (D.list D.value) "de" @?= Left "TypeMismatch List Dict"
    ]
  , testGroup "dict"
    [ testCase "de" $ D.decode (D.dict D.value) "de" @?= Right []
    , testCase "d3:foo3:bare" $ D.decode (D.dict D.string) "d3:foo3:bare" @?= Right [("foo", "bar")]
    , testCase "value type mismatch" $ D.decode (D.dict D.string) "d3:foo3:bar3:quxi1ee" @?= Left "TypeMismatch String Integer"
    , testCase "type mismatch string" $ D.decode (D.dict D.value) "0:" @?= Left "TypeMismatch Dict String"
    , testCase "type mismatch integer" $ D.decode (D.dict D.value) "i0e" @?= Left "TypeMismatch Dict Integer"
    , testCase "type mismatch list" $ D.decode (D.dict D.value) "le" @?= Left "TypeMismatch Dict List"
    ]
  , testGroup "text"
    [ testCase "0:" $ D.decode D.text "0:" @?= Right ""
    , testCase "3:foo" $ D.decode D.text "3:foo" @?= Right "foo"
    , testCase "こんにちは" $ D.decode D.text "15:\227\129\147\227\130\147\227\129\171\227\129\161\227\129\175" @?= Right "こんにちは"
    , testCase "invalid UTF-8" $ D.decode D.text "2:\xd8\x00" @?= Left "UTF8DecodeFailure"
    , testCase "type mismatch integer" $ D.decode D.text "i0e" @?= Left "TypeMismatch String Integer"
    , testCase "type mismatch list" $ D.decode D.text"le" @?= Left "TypeMismatch String List"
    , testCase "type mismatch dict" $ D.decode D.text"de" @?= Left "TypeMismatch String Dict"
    ]
  , testGroupIntegral "int" False D.int
  , testGroupIntegral "int64" False D.int64
  , testGroupIntegral "int32" False D.int32
  , testGroupIntegral "int16" False D.int16
  , testGroupIntegral "int8" False D.int8
  , testGroupIntegral "word" True D.word
  , testGroupIntegral "word64" True D.word64
  , testGroupIntegral "word32" True D.word32
  , testGroupIntegral "word16" True D.word16
  , testGroupIntegral "word8" True D.word8
  , testGroup "index"
    [ testCase "l3:fooe" $ D.decode (D.index 0 D.string) "l3:fooe" @?= Right "foo"
    , testCase "out of bounds -1" $ D.decode (D.index (-1) D.string) "l3:fooe" @?= Left "IndexOutOfBounds"
    , testCase "out of bounds 1" $ D.decode (D.index 1 D.string) "l3:fooe" @?= Left "IndexOutOfBounds"
    , testCase "li2ee" $ D.decode (D.index 0 D.string) "li2ee" @?= Left "TypeMismatch String Integer"
    , let p =     D.index 5 (Left <$> D.string)
              <|> D.index 0 (Right <$> D.int)
              <|> D.index 1 (Left <$> D.string) in
      testCase "alt" $ D.decode p "l3:foo3:bare" @?= Right (Left "bar")
    , let p = (,,) <$> D.index 1 D.string
                   <*> D.index 2 (D.list D.integer)
                   <*> D.index 0 D.integer in
      testCase "multiple types" $ D.decode p "li1e3:twoli0ei0ei0eee" @?= Right ("two", [0,0,0], 1)
    , testCase "type mismatch string" $ D.decode (D.index 0 D.string) "0:" @?= Left "TypeMismatch List String"
    , testCase "type mismatch integer" $ D.decode (D.index 0 D.string) "i0e" @?= Left "TypeMismatch List Integer"
    , testCase "type mismatch dict" $ D.decode (D.index 0 D.string) "de" @?= Left "TypeMismatch List Dict"
    ]
  , testGroup "list'"
    [ testCase "l3:fooe" $ D.decode (D.list' $ D.elem D.string) "l3:fooe" @?= Right "foo"
    , testCase "too short" $ D.decode (D.list' $ D.elem D.string) "le" @?= Left "ListElemsExhausted"
    , testCase "too long" $ D.decode (D.list' $ D.elem D.string) "l3:foo3:bare" @?= Left "ListElemsLeft"
    , testCase "elem type mismatch" $ D.decode (D.list' $ D.elem D.string) "li2ee" @?= Left "TypeMismatch String Integer"
    , let p = D.list' $
                    D.elem D.string
                <|> D.elem D.integer *> D.elem D.string in
      testCase "Elem alt" $ D.decode p "li1e3:fooe" @?= Right "foo"
    , let p = D.list' $
                (,,) <$> D.elem D.integer
                     <*> D.elem D.string
                     <*> D.elem (D.list D.integer) in
      testCase "multiple types" $ D.decode p "li1e3:twoli0ei0ei0eee" @?= Right (1, "two", [0,0,0])
    , testCase "type mismatch string" $ D.decode (D.list' $ pure ()) "0:" @?= Left "TypeMismatch List String"
    , testCase "type mismatch integer" $ D.decode (D.list' $ pure ()) "i0e" @?= Left "TypeMismatch List Integer"
    , testCase "type mismatch dict" $ D.decode (D.list' $ pure ()) "de" @?= Left "TypeMismatch List Dict"
    ]
  , testGroup "field"
    [ testCase "key not found" $ D.decode (D.field "foo" D.string) "de" @?= Left "KeyNotFound \"foo\""
    , testCase "d3:foo3:bare" $ D.decode (D.field "foo" D.string) "d3:foo3:bare" @?= Right "bar"
    , testCase "extra keys ignored" $ D.decode (D.field "baz" D.string) "d3:baz3:qux3:foo3:bare" @?= Right "qux"
    , let p = D.field "bar" D.string <|> D.field "foo" D.string in
      testCase "alt" $ D.decode p "d3:foo3:bare" @?= Right "bar"
    , testCase "value type mismatch" $ D.decode (D.field "foo" D.string) "d3:fooi2ee" @?= Left "TypeMismatch String Integer"
    , let p = (,,) <$> D.field "one" D.integer
                   <*> D.field "two" D.string
                   <*> D.field "three" (D.list D.integer) in
      testCase "multiple types" $ D.decode p "d3:onei1e5:threeli0ei0ei0ee3:two3:twoe" @?= Right (1, "two", [0,0,0])
    , testCase "type mismatch string" $ D.decode (D.field "foo" D.string) "0:" @?= Left "TypeMismatch Dict String"
    , testCase "type mismatch integer" $ D.decode (D.field "foo" D.string) "i0e" @?= Left "TypeMismatch Dict Integer"
    , testCase "type mismatch list" $ D.decode (D.field "foo" D.string) "le" @?= Left "TypeMismatch Dict List"
    ]
  , testGroup "dict'"
    [ testCase "key not found" $ D.decode (D.dict' $ D.field' "foo" D.string) "de" @?= Left "KeyNotFound \"foo\""
    , testCase "d3:foo3:bare" $ D.decode (D.dict' $ D.field' "foo" D.string) "d3:foo3:bare" @?= Right "bar"
    , testCase "extra keys fails" $ D.decode (D.dict' $ D.field' "baz" D.string) "d3:baz3:qux3:foo3:bare" @?= Left "UnrecognizedKey \"foo\""
    , let p = D.dict' $ D.field' "bar" D.string <|> D.field' "foo" D.string in
      testCase "Fields alt" $ D.decode p "d3:foo3:bare" @?= Right "bar"
    , testCase "value type mismatch" $ D.decode (D.dict' $ D.field' "foo" D.string) "d3:fooi2ee" @?= Left "TypeMismatch String Integer"
    , let p = D.dict' $
                (,,) <$> D.field' "one" D.integer
                     <*> D.field' "two" D.string
                     <*> D.field' "three" (D.list D.integer) in
      testCase "multiple types" $ D.decode p "d3:onei1e5:threeli0ei0ei0ee3:two3:twoe" @?= Right (1, "two", [0,0,0])
    , testCase "type mismatch string" $ D.decode (D.dict' $ pure ()) "0:" @?= Left "TypeMismatch Dict String"
    , testCase "type mismatch integer" $ D.decode (D.dict' $ pure ()) "i0e" @?= Left "TypeMismatch Dict Integer"
    , testCase "type mismatch list" $ D.decode (D.dict' $ pure ()) "le" @?= Left "TypeMismatch Dict List"
    ]
  , testGroup "fail"
    [ testCase "3:foo" $ D.decode (D.fail "error!" :: D.Parser B.ByteString) "3:foo" @?= Left "Fail: error!"
    ]
  ]
  where
    testGroupIntegral :: forall a. (Bounded a, Integral a, Show a)
                      => String -> Bool -> D.Parser a -> TestTree
    testGroupIntegral name isW p = testGroup name
      [ testCase "i0e" $ D.decode p "i0e" @?= Right 0
      , testCase "i32e" $ D.decode p "i32e" @?= Right 32
      , testCase "i-32e" $ D.decode p "i-32e" @?= if isW then Left oob else Right (-32)
      , testCase "minBound-1" $ D.decode p ("i" <> BC.pack (show (minBoundI - 1)) <> "e") @?= Left oob
      , testCase "minBound" $ D.decode p ("i" <> BC.pack (show minBoundI) <> "e") @?= Right minBound
      , testCase "maxBound" $ D.decode p ("i" <> BC.pack (show maxBoundI) <> "e") @?= Right maxBound
      , testCase "maxBound+1" $ D.decode p ("i" <> BC.pack (show (maxBoundI + 1)) <> "e") @?= Left oob
      , testCase "i98765432109876543210e" $ D.decode p "i98765432109876543210e" @?= Left oob
      , testCase "type mismatch string" $ D.decode p "0:" @?= Left "TypeMismatch Integer String"
      , testCase "type mismatch list" $ D.decode p "le" @?= Left "TypeMismatch Integer List"
      , testCase "type mismatch dict" $ D.decode p "de" @?= Left "TypeMismatch Integer Dict"
      ]
      where
        minBoundI, maxBoundI :: Integer
        minBoundI = fromIntegral (minBound :: a)
        maxBoundI = fromIntegral (maxBound :: a)
        oob = if isW then "WordOutOfBounds" else "IntOutOfBounds"

encodeTests :: TestTree
encodeTests = testGroup "Encode"
  [ testGroup "string"
    [ testCase "<empty>" $ enc E.string "" @?= "0:"
    , testCase "Hello, World!" $ enc E.string "Hello, World!" @?= "13:Hello, World!"
    ]
  , testGroup "integer"
    [ testCase "0" $ enc E.integer 0 @?= "i0e"
    , testCase "1" $ enc E.integer 1 @?= "i1e"
    , testCase "-1" $ enc E.integer (-1) @?= "i-1e"
    , testCase "98765432109876543210" $ enc E.integer 98765432109876543210 @?= "i98765432109876543210e"
    ]
  , testGroup "list"
    [ testCase "[]" $ enc (E.list E.integer) [] @?= "le"
    , testCase "[2,3,1]" $ enc (E.list E.integer) [2,3,1] @?= "li2ei3ei1ee"
    ]
  , testGroup "dict"
    [ testCase "{}" $ enc (E.dict E.integer) [] @?= "de"
    , testCase "{one:1,two:2,three:3}" $ enc (E.dict E.integer) [("one",1),("two",2),("three",3)] @?= "d3:onei1e5:threei3e3:twoi2ee"
    ]
  , testGroup "text"
    [ testCase "Hello, World!" $ enc E.text "Hello, World!" @?= "13:Hello, World!"
    , testCase "こんにちは" $ enc E.text "こんにちは" @?= "15:\227\129\147\227\130\147\227\129\171\227\129\161\227\129\175"
    ]
  , testGroupIntegral "int" False E.int
  , testGroupIntegral "int64" False E.int64
  , testGroupIntegral "int32" False E.int32
  , testGroupIntegral "int16" False E.int16
  , testGroupIntegral "int8" False E.int8
  , testGroupIntegral "word" True E.word
  , testGroupIntegral "word64" True E.word64
  , testGroupIntegral "word32" True E.word32
  , testGroupIntegral "word16" True E.word16
  , testGroupIntegral "word8" True E.word8
  , testGroup "field"
    [ testCase "{}" $ enc id (E.dict' mempty) @?= "de"
    , let e =  E.dict' $
               E.field "one" E.integer 1
            <> E.field "two" E.string "two"
            <> E.field "three" (E.list E.integer) [0,0,0] in
      testCase "{one:1,two:two,three:[0,0,0]}" $ enc id e @?= "d3:onei1e5:threeli0ei0ei0ee3:two3:twoe"
    ]
  ]
  where
    testGroupIntegral :: forall a. (Bounded a, Integral a, Show a)
                      => String -> Bool -> (a -> E.Encoding) -> TestTree
    testGroupIntegral name isW e = testGroup name $
      [ testCase "0" $ enc e 0 @?= "i0e"
      , testCase "32" $ enc e 32 @?= "i32e"
      ] ++
      [ testCase "-32" $ enc e (-32) @?= "i-32e" | not isW ] ++
      [ testCase "minBound" $ enc e minBound @?= "i" <> BLC.pack (show (minBound :: a)) <> "e"
      , testCase "maxBound" $ enc e maxBound @?= "i" <> BLC.pack (show (maxBound :: a)) <> "e"
      ]

enc :: (a -> E.Encoding) -> a -> BL.ByteString 
enc f = BB.toLazyByteString . E.toBuilder . f

encodeDecodeTests :: TestTree
encodeDecodeTests = testGroup "EncodeDecode, decode . encode == Right"
  [ testProperty "Value" $
      \v -> (D.decode D.value . toBS . E.value) v === Right v
  , testProperty "Val" $
      \(TV t v) ->
        (fmap Clean . D.decode (mkParser t) . toBS . encodeVal) v
        === Right (Clean v)
  ]
  where
    toBS = BL.toStrict . BB.toLazyByteString . E.toBuilder

instance Arbitrary Ben.Value where
  arbitrary = sized $ \n -> do
    n' <- choose (0,n)
    go (n'+1)
    where
      go 1 = do
        sOrI <- arbitrary
        if sOrI
        then Ben.String <$> arbitrary
        else Ben.Integer <$> arbitrary
      go n = do
        ns <- partition (n-1)
        lOrD <- arbitrary
        if lOrD
        then Ben.List . V.fromList <$> traverse go ns
        else Ben.Dict . M.fromList
               <$> traverse (\n' -> (,) <$> arbitrary <*> go n') ns

  shrink (Ben.String s)  = Ben.String <$> shrink s
  shrink (Ben.Integer i) = Ben.Integer <$> shrink i
  shrink (Ben.List xs)   = Ben.List . V.fromList <$> shrink (V.toList xs)
  shrink (Ben.Dict kxs)  = Ben.Dict . M.fromList <$> shrink (M.toList kxs)

instance Arbitrary B.ByteString where
  arbitrary = B.pack <$> arbitrary
  shrink = map B.pack . shrink . B.unpack

-- The code below generates some random "type" (Typ) together with a "value"
-- (Val) of that type.
-- The Typ describes how to generate a value and how to parse a value.
-- A Val can have some junk which will be encoded but not decoded, to test that
-- parsers which are supposed to ignore data do so (field and index).

data Val
  = String !B.ByteString
  | Integer !Integer
  | List !(V.Vector Val)
  | Dict !(M.Map B.ByteString Val)
  | Int !Int
  | Word !Word
  | Fields
      !(M.Map B.ByteString Val) -- ^ contents
      !(M.Map B.ByteString Val) -- ^ junk
  | Fields' !(M.Map B.ByteString Val)
  | Index
      !(V.Vector Val) -- ^ contents and junk together
      !(V.Vector Int) -- ^ indices of the contents
  | Elems !(V.Vector Val)
  deriving Show

data Sized a = Sized !Int !a deriving Show

data Typ
  = TString
  | TInteger
  | TList !(Sized Typ)
  | TDict !(Sized Typ)
  | TInt
  | TWord
  | TFields !(M.Map B.ByteString (Sized Typ)) !(M.Map B.ByteString (Sized Typ))
  | TFields' !(M.Map B.ByteString (Sized Typ))
  | TIndex !(V.Vector (Sized Typ)) !(V.Vector Int)
  | TElems !(V.Vector (Sized Typ))
  deriving Show

-- Newtype to compare the non-junk parts of a Val via Eq.
newtype Clean = Clean Val deriving Show

instance Eq Clean where
  Clean x1 == Clean x2 = case (x1,x2) of
    (String s1   , String s2   ) -> s1 == s2
    (Integer i1  , Integer i2  ) -> i1 == i2
    (List l1     , List l2     ) -> fmap Clean l1 == fmap Clean l2
    (Dict d1     , Dict d2     ) -> fmap Clean d1 == fmap Clean d2
    (Int i1      , Int i2      ) -> i1 == i2
    (Word w1     , Word w2     ) -> w1 == w2
    (Fields m1 _ , Fields m2 _ ) -> fmap Clean m1 == fmap Clean m2
    (Fields' m1  , Fields' m2  ) -> fmap Clean m1 == fmap Clean m2
    (Index l1 is1, Index l2 is2) -> fmap Clean (V.backpermute l1 is1) ==
                                    fmap Clean (V.backpermute l2 is2)
    (Elems l1    , Elems l2    ) -> fmap Clean l1 == fmap Clean l2
    _                            -> False

encodeVal :: Val -> E.Encoding
encodeVal x = case x of
  String s     -> E.string s
  Integer i    -> E.integer i
  List l       -> E.list encodeVal l
  Dict d       -> E.dict encodeVal d
  Int i        -> E.int i
  Word w       -> E.word w
  Fields m1 m2 -> E.dict' $
                    M.foldMapWithKey (\k -> E.field k encodeVal) (m1 <> m2)
  Fields' m    -> E.dict' $ M.foldMapWithKey (\k -> E.field k encodeVal) m
  Index l _    -> E.list encodeVal l
  Elems l      -> E.list encodeVal l

mkParser :: Sized Typ -> D.Parser Val
mkParser (Sized _ t) = case t of
  TString  -> String <$> D.string
  TInteger -> Integer <$> D.integer
  TList t' -> List <$> D.list (mkParser t')
  TDict t' -> Dict <$> D.dict (mkParser t')
  TInt     -> Int <$> D.int
  TWord    -> Word <$> D.word
  TFields m1 _ ->
    Fields <$> M.traverseWithKey (\k -> D.field k . mkParser) m1
           <*> pure M.empty
  TFields' m ->
    Fields' <$> D.dict' (M.traverseWithKey (\k -> D.field' k . mkParser) m)
  TIndex l is ->
    Index <$> traverse (\i -> D.index i (mkParser (l V.! i))) is
          <*> pure (V.generate (V.length is) id)
  TElems l -> Elems <$> D.list' (traverse (D.elem . mkParser) l)

data TV = TV !(Sized Typ) !Val deriving Show

instance Arbitrary TV where
  arbitrary = sized $ \n -> do
    n' <- choose (0,n)
    go (n'+1)
    where
      go n = do
        t <- genTyp n
        v <- genVal n t
        pure $ TV t v

-- | Generate a Typ and its minimum number of nodes, such that it is <= n.
genTyp :: Int -> Gen (Sized Typ)
genTyp n | n <= 0 = error "genTyp n | n <= 0"
genTyp 1 = Sized 1 <$> elements [TString, TInteger, TInt, TWord]
genTyp n = oneof
  [ Sized 1 . TList <$> genTypMany (n-1)
  , Sized 1 . TDict <$> genTypMany (n-1)
  , do
      n' <- choose (0, n-1)
      Sized n1 m1 <- goMap n'
      Sized n2 m2 <- goMap (n-1-n')
      pure $ Sized (n1+n2+1) (TFields m1 m2)
  , do
      Sized n1 m1 <- goMap (n-1)
      pure $ Sized (n1+1) (TFields' m1)
  , do
      Sized n1 v1 <- goVec (n-1)
      is <- V.fromList <$> sublistOf [0 .. V.length v1 - 1]
      pure $ Sized (n1+1) (TIndex v1 is)
  , do
      Sized n1 v1 <- goVec (n-1)
      pure $ Sized (n1+1) (TElems v1)
  ]
  where
    genTypMany m = partition m >>= genTyp . minimum
    goMap m = do
      ms <- partition m
      kvs <- traverse (\m' -> (,) <$> arbitrary <*> genTyp m') ms
      pure $ sizedF (M.fromList kvs)
    goVec m = do
      ms <- partition m
      sizedF . V.fromList <$> traverse genTyp ms
    sizedF xs = Sized (sizeF xs) xs

-- | Generate a value with at most n nodes
genVal :: Int -> Sized Typ -> Gen Val
genVal n (Sized n' _) | n < n' = error "genVal: n < size of type"
genVal n (Sized _ t) = case t of
  TString -> String <$> arbitrary
  TInteger -> Integer <$> arbitrary
  TList t' -> List . V.fromList <$> goMany (n-1) t'
  TDict t' -> do
    vs <- goMany (n-1) t'
    ks <- replicateM (length vs) arbitrary
    pure $ Dict $ M.fromList $ zip ks vs
  TInt -> Int <$> arbitrary
  TWord -> Word <$> arbitrary
  TFields mp1 mp2 -> do
    ~[m1,m2] <- partitionWithMin (n-1) [sizeF mp1, sizeF mp2]
    Fields <$> goMap m1 mp1 <*> goMap m2 mp2
  TFields' mp -> Fields' <$> goMap (n-1) mp
  TIndex l is -> flip Index is . V.fromList <$> goVec (n-1) (F.toList l)
  TElems l -> Elems . V.fromList <$> goVec (n-1) (F.toList l)
  where
    goMany m t'@(Sized n' _) = do
      ns <- partitionWithMin m (replicate (div m n') n')
      traverse (flip genVal t') ns
    goMap m mp = do
      ns <- partitionWithMin m (sizes (M.elems mp))
      M.fromList . zip (M.keys mp) <$> zipWithM genVal ns (M.elems mp)
    goVec m l = do
      ns <- partitionWithMin m (sizes l)
      zipWithM genVal ns l

sizeF :: Foldable f => f (Sized a) -> Int
sizeF = F.foldl' (\acc (Sized n _) -> acc + n) 0

sizes :: Functor f => f (Sized a) -> f Int
sizes = fmap (\(Sized n _) -> n)

-- | Partition m into pieces with a given minimum value of each piece.
partitionWithMin :: Int -> [Int] -> Gen [Int]
partitionWithMin m xs | m < sum xs = error "partitionWithMin: n not big enough"
partitionWithMin m xs = zipWith (+) xs <$> partitionN (length xs) (m - sum xs)

-- | Partition m into n pieces. Pieces can be 0.
partitionN :: Int -> Int -> Gen [Int]
partitionN n0 _ | n0 <= 0 = error "partitionN: n <= 0"
partitionN n0 m0 = go n0 m0 >>= shuffle
  where
    go 1 m = pure [m]
    go n 0 = (0:) <$> go (n-1) 0
    go n m = do
      x <- choose (1,m)
      (x:) <$> go (n-1) (m-x)

-- | Partition into pieces >= 0
partition :: Int -> Gen [Int]
partition m0 | m0 < 0 = error "partition: m < 0"
partition m0 = go m0 >>= shuffle
  where
    go 0 = pure []
    go m = do
      x <- choose (1,m)
      (x:) <$> go (m-x)