1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
|
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
module PropUtils (
encodeInteger,
encodeDouble,
toParseJSON,
toParseJSON1,
roundTripEq,
roundTripKey,
roundtripReadShow,
toFromJSON,
sameAs,
sameAs1,
sameAs1Agree,
modifyFailureProp,
parserThrowErrorProp,
parserCatchErrorProp,
-- * Predicates
isEmptyArray,
isTaggedObject,
isString,
isObjectWithSingleField,
is2ElemArray,
isNullaryTaggedObject,
isUntaggedValueETI,
) where
import Prelude.Compat
import Data.Aeson (eitherDecode, encode)
import Data.Aeson.Encoding (encodingToLazyByteString)
import Data.Aeson.Parser (value)
import Data.Aeson.Types
import qualified Data.Aeson.Key as Key
import qualified Data.Aeson.KeyMap as KM
import Data.HashMap.Strict (HashMap)
import Data.Hashable (Hashable)
import Data.Map (Map)
import Encoders
import Instances ()
import Test.QuickCheck (Arbitrary(..), Property, Testable, (===), (.&&.), counterexample)
import Types
import Text.Read (readMaybe)
import qualified Data.Attoparsec.Lazy as L
import qualified Data.ByteString.Lazy.Char8 as L
import qualified Data.Vector as V
import qualified Data.Aeson.Decoding as Dec
encodeDouble :: Double -> Double -> Property
encodeDouble num denom
| isNaN d = encode d === "null"
| isInfinite d = if d > 0 then encode d === "\"+inf\"" else encode d === "\"-inf\""
| otherwise = (read . L.unpack . encode) d === d
where d = num / denom
encodeInteger :: Integer -> Property
encodeInteger i = encode i === L.pack (show i)
toParseJSON :: (Eq a, Show a) =>
(Value -> Parser a) -> (a -> Value) -> a -> Property
toParseJSON parsejson tojson x =
case iparse parsejson . tojson $ x of
IError path msg -> failure "parse" (formatError path msg) x
ISuccess x' -> x === x'
toParseJSON1
:: (Eq (f Int), Show (f Int))
=> (forall a. LiftParseJSON f a)
-> (forall a. LiftToJSON f a)
-> f Int
-> Property
toParseJSON1 parsejson1 tojson1 = toParseJSON parsejson tojson
where
parsejson = parsejson1 parseJSON (listParser parseJSON)
tojson = tojson1 toJSON (listValue toJSON)
roundTripEnc :: (FromJSON a, ToJSON a, Show a) =>
(a -> a -> Property) -> a -> Property
roundTripEnc eq i =
case fmap ifromJSON . L.parse value . encode $ i of
L.Done _ (ISuccess v) -> v `eq` i
L.Done _ (IError path err) -> failure "fromJSON" (formatError path err) i
L.Fail _ _ err -> failure "parse" err i
roundTripDecEnc :: (FromJSON a, ToJSON a, Show a) =>
(a -> a -> Property) -> a -> Property
roundTripDecEnc eq i =
case Dec.eitherDecodeStrict . L.toStrict . encode $ i of
Right v -> v `eq` i
Left err -> failure "parse" err i
roundTripNoEnc :: (FromJSON a, ToJSON a, Show a) =>
(a -> a -> Property) -> a -> Property
roundTripNoEnc eq i =
case ifromJSON . toJSON $ i of
(ISuccess v) -> v `eq` i
(IError path err) -> failure "fromJSON" (formatError path err) i
roundTripEq :: (Eq a, FromJSON a, ToJSON a, Show a) => a -> Property
roundTripEq y = roundTripEnc (===) y .&&. roundTripNoEnc (===) y .&&. roundTripDecEnc (===) y
roundtripReadShow :: Value -> Property
roundtripReadShow v = readMaybe (show v) === Just v
-- We test keys by encoding HashMap and Map with it
roundTripKey
:: (Ord a, Hashable a, FromJSONKey a, ToJSONKey a, Show a)
=> HashMap a Int -> Map a Int -> Property
roundTripKey h m = roundTripEq h .&&. roundTripEq m
toFromJSON :: (Arbitrary a, Eq a, FromJSON a, ToJSON a, Show a) => a -> Property
toFromJSON x = case ifromJSON (toJSON x) of
IError path err -> failure "fromJSON" (formatError path err) x
ISuccess x' -> x === x'
modifyFailureProp :: String -> String -> Bool
modifyFailureProp orig added =
result == Error (added ++ orig)
where
parser = const $ modifyFailure (added ++) $ fail orig
result :: Result ()
result = parse parser ()
parserThrowErrorProp :: String -> Property
parserThrowErrorProp msg =
result === Error msg
where
parser = const $ parserThrowError [] msg
result :: Result ()
result = parse parser ()
-- | Tests (also) that we catch the JSONPath and it has elements in the right order.
parserCatchErrorProp :: [String] -> String -> Property
parserCatchErrorProp path msg =
result === Success ([Key "outer", Key "inner"] ++ jsonPath, msg)
where
parser = parserCatchError outer (curry pure)
outer = inner <?> Key "outer"
inner = parserThrowError jsonPath msg <?> Key "inner"
result :: Result (JSONPath, String)
result = parse (const parser) ()
jsonPath = map (Key . Key.fromString) path
-- | Perform a structural comparison of the results of two encoding
-- methods. Compares decoded values to account for HashMap-driven
-- variation in JSON object key ordering.
sameAs :: (a -> Value) -> (a -> Encoding) -> a -> Property
sameAs toVal toEnc v =
counterexample (show s) $
eitherDecode s === Right (toVal v)
where
s = encodingToLazyByteString (toEnc v)
sameAs1
:: (forall a. LiftToJSON f a)
-> (forall a. LiftToEncoding f a)
-> f Int
-> Property
sameAs1 toVal1 toEnc1 v = lhs === rhs
where
rhs = Right $ toVal1 toJSON (listValue toJSON) v
lhs = eitherDecode . encodingToLazyByteString $
toEnc1 toEncoding (listEncoding toEncoding) v
sameAs1Agree
:: ToJSON a
=> (f a -> Encoding)
-> (forall b. LiftToEncoding f b)
-> f a
-> Property
sameAs1Agree toEnc toEnc1 v = rhs === lhs
where
rhs = encodingToLazyByteString $ toEnc v
lhs = encodingToLazyByteString $ toEnc1 toEncoding (listEncoding toEncoding) v
--------------------------------------------------------------------------------
-- Value properties
--------------------------------------------------------------------------------
-- | Add the formatted @Value@ to the printed counterexample when the property
-- fails.
checkValue :: Testable a => (Value -> a) -> Value -> Property
checkValue prop v = counterexample (L.unpack (encode v)) (prop v)
isString :: Value -> Bool
isString (String _) = True
isString _ = False
is2ElemArray :: Value -> Bool
is2ElemArray (Array v) = V.length v == 2 && isString (V.head v)
is2ElemArray _ = False
{-
isTaggedObjectValue :: Value -> Bool
isTaggedObjectValue (Object obj) = "tag" `KM.member` obj &&
"contents" `KM.member` obj
isTaggedObjectValue _ = False
-}
isNullaryTaggedObject :: Value -> Bool
isNullaryTaggedObject obj = isTaggedObject' obj && isObjectWithSingleField obj
isTaggedObject :: Value -> Property
isTaggedObject = checkValue isTaggedObject'
isTaggedObject' :: Value -> Bool
isTaggedObject' (Object obj) = "tag" `KM.member` obj
isTaggedObject' _ = False
isObjectWithSingleField :: Value -> Bool
isObjectWithSingleField (Object obj) = KM.size obj == 1
isObjectWithSingleField _ = False
-- | is untaggedValue of EitherTextInt
isUntaggedValueETI :: Value -> Bool
isUntaggedValueETI (String s)
| s == "nonenullary" = True
isUntaggedValueETI (Bool _) = True
isUntaggedValueETI (Number _) = True
isUntaggedValueETI (Array a) = length a == 2
isUntaggedValueETI _ = False
isEmptyArray :: Value -> Property
isEmptyArray = checkValue isEmptyArray'
isEmptyArray' :: Value -> Bool
isEmptyArray' = (Array mempty ==)
|