packages feed

waargonaut-0.5.2.2: test/Main.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections     #-}
module Main (main) where

import           GHC.Word                    (Word8)

import           Control.Lens                (( # ), (^.), (^?), _2)
import qualified Control.Lens                as L
import           Control.Monad               (when)

import           Data.Either                 (isLeft)

import qualified Data.Scientific             as Sci

import Data.Functor.Contravariant ((>$<))
import           Data.Maybe                  (fromMaybe)
import           Data.Semigroup              ((<>))
import           Data.Text                   (Text)
import qualified Data.Text                   as Text
import qualified Data.Text.Encoding          as Text
import qualified Data.Text.IO                as Text

import qualified Data.Text.Lazy              as TextL
import qualified Data.Text.Lazy.Builder      as TB

import qualified Data.ByteString             as BS

import qualified Data.Sequence               as S

import           Hedgehog
import qualified Hedgehog.Gen                as Gen
import qualified Hedgehog.Range              as Range

import           Test.Tasty
import           Test.Tasty.Hedgehog
import           Test.Tasty.HUnit

import           Natural                     (_Natural)

import           Data.Digit                  (HeXDigit)

import           Waargonaut                  (Json)
import qualified Waargonaut                  as W
import qualified Waargonaut.Types.CommaSep   as CommaSep

import           Waargonaut.Types.JChar      (JChar)
import qualified Waargonaut.Types.JChar      as JChar

import qualified Waargonaut.Types.JNumber    as JNumber
import qualified Waargonaut.Types.Whitespace as WS

import qualified Waargonaut.Decode           as D
import           Waargonaut.Decode.Error     (DecodeError)
import           Waargonaut.Decode.Internal  (CursorHistory' (..),
                                              ZipperMove (..), compressHistory)
import qualified Waargonaut.Encode           as E

import           Waargonaut.Generic          (mkDecoder, mkEncoder)

import qualified Types.CommaSep              as CS
import qualified Types.Common                as Common
import qualified Types.Json                  as J
import qualified Types.Whitespace            as WS

import qualified Decoder
import qualified Decoder.Laws
import qualified Encoder
import qualified Encoder.Laws
import qualified Json

encodeText
  :: Json
  -> Text
encodeText = TextL.toStrict
  . TB.toLazyText
  . W.waargonautBuilder WS.wsBuilder

decode
  :: Text
  -> Either DecodeError Json
decode =
  Common.parseText

prop_history_condense :: Property
prop_history_condense = property $ do
  n <- forAll $ Gen.int (Range.linear 1 10)
  m <- forAll $ Gen.int (Range.linear 1 10)

  let
    ixa = 1 :: Int
    ixb = 2
    mkCH = CursorHistory' . S.fromList
    mcA cn cm n' m' = mkCH [(cn (n' ^. _Natural), ixa), (cm (m' ^. _Natural), ixb)]
    mcB c x i = mkCH [(c (x ^. _Natural), i)]

  -- * [R n, R m]   = [R (n + m)]
  compressHistory (mcA R R n m) === mcB R (n + m) ixb

  -- * [L n, R m]   = [L (n + m)]
  compressHistory (mcA L L n m) === mcB L (n + m) ixb

  let
    rlch = compressHistory (mcA R L n m)
    lrch = compressHistory (mcA L R n m)
  when (n > m) $ do
    -- * [R n, L m]   = [R (n - m)] where n > m
    rlch === mcB R (n - m) ixa
    -- * [L n, R m]   = [L (n - m)] where n > m
    lrch === mcB L (n - m) ixa

  when (n < m) $ do
    -- * [R n, L m]   = [L (m - n)] where n < m
    rlch === mcB L (m - n) ixb
    -- * [L n, R m]   = [R (m - n)] where n < m
    lrch === mcB R (m - n) ixb

  -- * [DAt k, R n] = [DAt k]
  compressHistory (mkCH [(DAt "KeyName", ixa), (R (n ^. _Natural), ixb)]) === mkCH [(DAt "KeyName", ixa)]

prop_uncons_consCommaSep :: Property
prop_uncons_consCommaSep = property $ do
  cs <- forAll $ CS.genCommaSeparated WS.genWS Gen.bool
  let
    elems = (^. CommaSep._CommaSeparated . _2)

    cs' = do
      (e,xs) <- CommaSep.unconsCommaSep cs
      let trailing = fromMaybe (CommaSep.Comma, mempty) (fst e)
      elems $ CommaSep.consCommaSep (trailing, snd e) xs

  elems cs === cs'

prop_uncons_consCommaSepVal :: Property
prop_uncons_consCommaSepVal = property $ do
  cs <- forAll $ CS.genCommaSeparated WS.genEmptyWS Gen.bool
  let
    elems = (^. CommaSep._CommaSeparated . _2)

  elems cs === (elems . uncurry L.cons =<< L.uncons cs)

prop_jchar :: Property
prop_jchar = property $ do
  c <- forAll Gen.unicodeAll
  tripping c toJChar (fmap fromJChar)
  where
    fromJChar :: JChar HeXDigit -> Char
    fromJChar = (JChar._JChar #)

    toJChar :: Char -> Maybe (JChar HeXDigit)
    toJChar = (^? JChar._JChar)

prop_jnumber_scientific_prism :: Property
prop_jnumber_scientific_prism = property $ do
  sci <- forAll $ Sci.scientific
    <$> Gen.integral (Range.linear 0 maxI)
    <*> Gen.int Range.linearBounded

  tripping sci (JNumber._JNumberScientific #) (^? JNumber._JNumberScientific)
  where
    maxI :: Integer
    maxI = 2 ^ (32 :: Integer)

simpleDecodeWith :: D.Decoder L.Identity a -> TextL.Text -> Either (DecodeError, D.CursorHistory) a
simpleDecodeWith d = D.simpleDecode d Common.parseBS . Text.encodeUtf8 . TextL.toStrict

prop_tripping_int_list :: Property
prop_tripping_int_list = property $ do
  xs <- forAll . Gen.list (Range.linear 0 100) $ Gen.int (Range.linear 0 9999)
  tripping xs
    (E.simplePureEncodeNoSpaces (E.traversable E.int))
    (simpleDecodeWith (D.list D.int))

prop_tripping_image_record_generic :: Property
prop_tripping_image_record_generic = withTests 1 . property $
  Common.prop_generic_tripping mkEncoder mkDecoder Common.testImageDataType

prop_tripping_newtype_fudge_generic :: Property
prop_tripping_newtype_fudge_generic = withTests 1 . property $
  Common.prop_generic_tripping mkEncoder mkDecoder Common.testFudge

prop_tripping_maybe_bool_generic :: Property
prop_tripping_maybe_bool_generic = property $
  forAll (Gen.maybe Gen.bool) >>= Common.prop_generic_tripping mkEncoder mkDecoder

prop_tripping_int_list_generic :: Property
prop_tripping_int_list_generic = property $ do
  xs <- forAll . Gen.list (Range.linear 0 100) $ Gen.int (Range.linear 0 9999)
  Common.prop_generic_tripping mkEncoder mkDecoder xs

prop_tripping :: Property
prop_tripping = withTests 200 . property $
  forAll J.genJson >>= (\j -> tripping j encodeText decode)

prop_print_parse_print_id :: Property
prop_print_parse_print_id = withTests 200 . property $ do
  printedA <- forAll $ encodeText <$> J.genJson
  Right printedA === (encodeText <$> decode printedA)

prop_maybe_maybe :: Property
prop_maybe_maybe = withTests 1 . property $ do
  let
    n   = Nothing
    jn  = Just Nothing
    jjt = Just (Just True)
    jjf = Just (Just False)

  trippin' n
  trippin' jn
  trippin' jjt
  trippin' jjf
  where
    trippin' a = tripping a
      (E.simplePureEncodeNoSpaces enc)
      (simpleDecodeWith dec)

    enc = E.maybeOrNull' . E.mapLikeObj' . E.atKey' "boop"
      $ E.maybeOrNull' (E.mapLikeObj' (E.atKey' "beep" E.bool'))
      -- $ E.mapLikeObj (E.atKey "beep" (E.maybeOrNull E.bool))

    dec = D.maybeOrNull $ D.atKey "boop"
      $ D.maybeOrNull (D.atKey "beep" D.bool)
      -- $ D.atKey "beep" (D.maybeOrNull D.bool)

tripping_properties :: TestTree
tripping_properties = testGroup "Properties"
  [ testProperty "CommaSeparated: cons . uncons = id"                  prop_uncons_consCommaSep
  , testProperty "CommaSeparated (disregard WS): cons . uncons = id"   prop_uncons_consCommaSepVal
  , testProperty "Char -> JChar Digit -> Maybe Char = Just id"         prop_jchar
  , testProperty "Scientific -> JNumber -> Maybe Scientific = Just id" prop_jnumber_scientific_prism
  , testProperty "(Maybe (Maybe Bool))"                                prop_maybe_maybe
  , testProperty "[Int]"                                               prop_tripping_int_list
  , testProperty "[Int] (generic)"                                     prop_tripping_int_list_generic
  , testProperty "Maybe Bool (generic)"                                prop_tripping_maybe_bool_generic
  , testProperty "Image record (generic)"                              prop_tripping_image_record_generic
  , testProperty "Newtype with Options (generic)"                      prop_tripping_newtype_fudge_generic
  , testProperty "Condensing History"                                  prop_history_condense
  ]

parser_properties :: TestTree
parser_properties = testGroup "Parser Round-Trip"
  [ testProperty "parse . print = id"            prop_tripping
  , testProperty "print . parse . print = print" prop_print_parse_print_id
  ]

parsePrint :: Text -> Either DecodeError Text
parsePrint = fmap encodeText . decode

readTestFile :: FilePath -> IO Text
readTestFile fp = Text.readFile ("test/json-data" <> "/" <> fp)

testFile :: FilePath -> Assertion
testFile fp = do
  s <- readTestFile fp
  parsePrint s @?= Right s

testFileFailure :: FilePath -> Assertion
testFileFailure fp = do
  s <- readTestFile fp
  assertBool (fp <> " should fail to parse!") (isLeft $ parsePrint s)

unitTests :: TestTree
unitTests =
  testGroup "File Tests - (print . parse = id)" (toTest <$> fs)
  where
    toTest f = testCase f (testFile f)

    fs =
      [ "test1.json"
      , "test2.json"
      , "test3.json"
      , "test5.json"
      , "test7.json"
      , "numbers.json"
      ]

mishandlingOfCharVsUtf8Bytes :: TestTree
mishandlingOfCharVsUtf8Bytes = testCaseSteps "Mishandling of UTF-8 Bytes vs Haskell Char" $ \step -> do
  let
    valChar      = '\128'          :: Char
    valText      = "\128"          :: Text
    valStr       = [valChar]        :: String
    encVal       = "\"\128\""      :: Text
    valUtf8Bytes = [34,194,128,34] :: [Word8]

  step "Pack String to Text"
  Text.pack valStr @?= valText

  step "Encoder via Text"
  x <- TextL.toStrict <$> E.simpleEncodeNoSpaces E.text valText
  x @?= encVal

  step "encoder output ~ packed bytes"
  Text.encodeUtf8 x @?= BS.pack valUtf8Bytes

  step "Decoder via Text"
  y <- D.runDecode D.text Common.parseBS (D.mkCursor (Text.encodeUtf8 x))
  y @?= Right valText

regressionTests :: TestTree
regressionTests =
  testGroup "Regression Tests - Failure to parse = Success" (toTestFail <$> fs)
  where
    toTestFail (dsc, f) =
      testCase dsc (testFileFailure f)

    fs =
      [ ("[11 12 13] (test4.json)","test4.json")
      , ("{\"foo\":3\"bar\":4} (test6.json)", "test6.json")
      ]

main :: IO ()
main = defaultMain $ testGroup "Waargonaut All Tests"
  [ parser_properties
  , tripping_properties
  , unitTests
  , regressionTests
  , Json.jsonTests
  , mishandlingOfCharVsUtf8Bytes

  , Decoder.decoderTests
  , Encoder.encoderTests

  , Decoder.Laws.decoderLaws
  , Encoder.Laws.encoderLaws

  , testGroup "text gen - text e/d"
    [ testProperty "unicode"       $ p Gen.text Gen.unicode E.text D.text
    , testProperty "latin1"        $ p Gen.text Gen.latin1 E.text D.text
    , testProperty "ascii"         $ p Gen.text Gen.ascii E.text D.text
    ]
  , testGroup "bytestring gen - via text e/d"
    [ testProperty "unicode"       $ p Gen.utf8 Gen.unicode bsE bsD
    , testProperty "latin1"        $ p Gen.utf8 Gen.latin1 bsE bsD
    , testProperty "ascii"         $ p Gen.utf8 Gen.ascii bsE bsD
    ]
  ]
  where
    bsE = Text.decodeUtf8 >$< E.text
    bsD = Text.encodeUtf8 <$> D.text

    p :: ( Eq a
         , Show a
         )
      => (Range Int -> Gen Char -> Gen a)
      -> Gen Char
      -> E.Encoder' a
      -> D.Decoder L.Identity a
      -> Property
    p f g e d = property $ do
      inp <- forAll $ f (Range.linear 0 1000) g
      tripping inp (E.simplePureEncodeNoSpaces e) (simpleDecodeWith d)