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