packages feed

language-thrift-0.4.0.0: test/Language/Thrift/TypesSpec.hs

{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE CPP                 #-}
{-# LANGUAGE FlexibleInstances   #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Language.Thrift.TypesSpec where

#if __GLASGOW_HASKELL__ < 709
import Control.Applicative
#endif

import Control.Monad           (unless)
import Data.Text               (Text)
import Test.Hspec
import Test.Hspec.QuickCheck
import Test.QuickCheck
import Text.PrettyPrint.Leijen (Doc)

import Text.Trifecta       (parseString)
import Text.Trifecta.Delta (Delta (..))

import qualified Data.Text     as Text
import qualified Text.Trifecta as Tri

import qualified Language.Thrift.Parser as P
import qualified Language.Thrift.Pretty as PP
import qualified Language.Thrift.Types  as T


-- | Halve the maximum size of generated values.
--
-- Generally speaking, it's a good idea to use this for calls that will
-- recursively generate lists of things so that they terminate at some point.
halfSize :: Gen a -> Gen a
halfSize = scale (\n -> truncate (fromIntegral n / 2 :: Double))

newtype Identifier = Identifier { getIdentifier :: Text }

instance Arbitrary Identifier where
    arbitrary = Identifier . Text.pack <$> listOf1 (elements charset)
      where
        charset = ['a'..'z'] ++ ['A'..'Z']

newtype Docstring = Docstring { getDocstring :: Maybe Text }

instance Arbitrary Docstring where
    arbitrary = Docstring <$> oneof [return Nothing, comment]
      where
        commentLine =
            Text.unwords <$> listOf (getIdentifier <$> arbitrary)
        comment = do
            s <- Text.strip . Text.unlines <$> listOf1 (halfSize commentLine)
            if Text.null s
                then return Nothing
                else return (Just s)


instance Arbitrary (T.Program ()) where
    arbitrary = T.Program <$> arbitrary <*> arbitrary


instance Arbitrary (T.Definition ()) where
    arbitrary = oneof [arbitraryConst, arbitraryType, arbitraryService]
      where
        arbitraryConst =
            T.ConstDefinition
                <$> arbitrary
                <*> (getIdentifier <$> arbitrary)
                <*> arbitrary
                <*> (getDocstring <$> arbitrary)
                <*> pure ()

        arbitraryType =
            T.TypeDefinition
                <$> arbitrary
                <*> arbitrary

        arbitraryService =
            T.ServiceDefinition
                <$> (getIdentifier <$> arbitrary)
                <*> (fmap getIdentifier <$> arbitrary)
                <*> arbitrary
                <*> arbitrary
                <*> (getDocstring <$> arbitrary)
                <*> pure ()


instance Arbitrary T.Header where
    arbitrary = oneof
        [ T.Include   <$> (getIdentifier <$> arbitrary)
        , T.Namespace <$> elements scopes
                      <*> (getIdentifier <$> arbitrary)
        ]
      where
        scopes = ["py", "rb", "java", "hs", "cpp"]


instance Arbitrary (T.Field ()) where
    arbitrary =
        T.Field
            <$> (fmap getPositive <$> arbitrary)
            <*> arbitrary
            <*> halfSize arbitrary
            <*> (getIdentifier <$> arbitrary)
            <*> halfSize arbitrary
            <*> arbitrary
            <*> (getDocstring <$> arbitrary)
            <*> pure ()


instance Arbitrary (T.Function ()) where
    arbitrary =
        T.Function
            <$> elements [True, False]
            <*> arbitrary
            <*> (getIdentifier <$> arbitrary)
            <*> arbitrary
            <*> arbitrary
            <*> arbitrary
            <*> (getDocstring <$> arbitrary)
            <*> pure ()


instance Arbitrary T.TypeAnnotation where
    arbitrary =
        T.TypeAnnotation
            <$> (getIdentifier <$> arbitrary)
            <*> (getIdentifier <$> arbitrary)


instance Arbitrary (T.EnumDef ()) where
    arbitrary =
        T.EnumDef
            <$> (getIdentifier <$> arbitrary)
            <*> arbitrary
            <*> arbitrary
            <*> (getDocstring <$> arbitrary)
            <*> pure ()


instance Arbitrary (T.Type ()) where
    arbitrary = oneof
        [ T.Typedef
            <$> arbitrary
            <*> (getIdentifier <$> arbitrary)
            <*> (getDocstring <$> arbitrary)
            <*> pure ()
        , T.Enum
            <$> (getIdentifier <$> arbitrary)
            <*> arbitrary
            <*> (getDocstring <$> arbitrary)
            <*> pure ()
        , T.Struct
            <$> (getIdentifier <$> arbitrary)
            <*> arbitrary
            <*> (getDocstring <$> arbitrary)
            <*> pure ()
        , T.Union
            <$> (getIdentifier <$> arbitrary)
            <*> arbitrary
            <*> (getDocstring <$> arbitrary)
            <*> pure ()
        , T.Exception
            <$> (getIdentifier <$> arbitrary)
            <*> arbitrary
            <*> (getDocstring <$> arbitrary)
            <*> pure ()
        , T.Senum
            <$> (getIdentifier <$> arbitrary)
            <*> (map getIdentifier <$> arbitrary)
            <*> (getDocstring <$> arbitrary)
            <*> pure ()
        ]


instance Arbitrary (T.FieldRequiredness) where
    arbitrary = elements [T.Required, T.Optional]

instance Arbitrary T.FieldType where
    arbitrary = oneof
        [ T.DefinedType . getIdentifier <$> arbitrary

        , T.StringType <$> arbitrary
        , T.BinaryType <$> arbitrary
        , T.SListType  <$> arbitrary
        , T.BoolType   <$> arbitrary
        , T.ByteType   <$> arbitrary
        , T.I16Type    <$> arbitrary
        , T.I32Type    <$> arbitrary
        , T.I64Type    <$> arbitrary
        , T.DoubleType <$> arbitrary

        , halfSize $
            T.MapType <$> arbitrary <*> arbitrary <*> arbitrary
        , halfSize $
            T.SetType  <$> arbitrary <*> arbitrary
        , halfSize $
            T.ListType <$> arbitrary <*> arbitrary
        ]


newtype BasicConstValue = BasicConstValue {
    getBasicConstValue :: T.ConstValue
  }


instance Arbitrary BasicConstValue where
    arbitrary = BasicConstValue <$> oneof
        [ T.ConstFloat                      <$> choose (0.0, 10000.0)
        , T.ConstInt                        <$> arbitrary
        , T.ConstLiteral    . getIdentifier <$> arbitrary
        , T.ConstIdentifier . getIdentifier <$> arbitrary
        ]

-- | newtype wrapper around const values so that we're not generating lists
-- and maps that go on forever.
newtype FiniteConstValue =
    FiniteConstValue { getFiniteConstValue :: T.ConstValue }


instance Arbitrary FiniteConstValue where
    arbitrary = FiniteConstValue <$> oneof
        [ basicConsts
        , T.ConstList <$> constList
        , T.ConstMap  <$> constMap
        ]
      where
        basicConsts = getBasicConstValue <$> arbitrary
        constList
            = listOf $ halfSize $ getFiniteConstValue <$> arbitrary
        constMap
            = listOf $
                (,) <$> basicConsts
                    <*> halfSize (getFiniteConstValue <$> arbitrary)


instance Arbitrary T.ConstValue where
    arbitrary = getFiniteConstValue <$> arbitrary


spec :: Spec
spec =
    describe "Can round trip" $ do

        prop "field types" $
            roundtrip PP.fieldType P.fieldType

        prop "constant values" $
            roundtrip PP.constantValue P.constantValue

        prop "functions" $
            roundtrip PP.function (Tri.whiteSpace *> P.function)

        prop "definitions" $
            roundtrip PP.definition (Tri.whiteSpace *> P.definition)

        prop "headers" $
            roundtrip (const PP.header) P.header

        prop "documents" $
            roundtrip PP.program P.program


roundtrip
    :: (Show a, Eq a)
    => (PP.Config -> a -> Doc)
    -> P.ThriftParser Tri.Parser () a
    -> a
    -> IO ()
roundtrip printer parser value = do
  let pretty = show . printer (PP.Config 4)
      triParser = P.runThriftParser (return ()) parser
      result =
          parseString triParser (Directed "memory" 0 0 0 0) (pretty value)
  case result of
    Tri.Success parsed ->
      unless (parsed == value) $ expectationFailure $
        "expected: " ++ show value ++ "\n but got: " ++ show parsed ++
        "\n\n expected (pretty): " ++ pretty value ++
        "\n but got (pretty): " ++ pretty parsed
    Tri.Failure msg -> expectationFailure $
      "failed to parse "  ++ pretty value ++
      "\n with " ++ show msg