Skip to content

Commit 74ecc02

Browse files
authored
Merge pull request #916 from haskell/arbitrary
Add QuickCheck instances
2 parents 6ee2719 + 02702b4 commit 74ecc02

File tree

10 files changed

+184
-48
lines changed

10 files changed

+184
-48
lines changed

aeson.cabal

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: aeson
2-
version: 2.0.2.0
2+
version: 2.0.3.0
33
license: BSD3
44
license-file: LICENSE
55
category: Text, Web, JSON
@@ -119,6 +119,7 @@ library
119119
, indexed-traversable >=0.1.1 && <0.2
120120
, OneTuple >=0.3.1 && <0.4
121121
, primitive >=0.7.0.1 && <0.8
122+
, QuickCheck >=2.14.2 && <2.15
122123
, scientific >=0.3.7.0 && <0.4
123124
, semialign >=1.2 && <1.3
124125
, strict >=0.4 && <0.5
@@ -166,6 +167,7 @@ test-suite aeson-tests
166167
Properties
167168
PropertyGeneric
168169
PropertyKeys
170+
PropertyQC
169171
PropertyRoundTrip
170172
PropertyRTFunctors
171173
PropertyTH

benchmarks/aeson-benchmarks.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ library
3636
, mtl
3737
, OneTuple
3838
, primitive
39+
, QuickCheck
3940
, scientific
4041
, semialign
4142
, strict

changelog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
For the latest version of this document, please see [https://github.com/haskell/aeson/blob/master/changelog.md](https://github.com/haskell/aeson/blob/master/changelog.md).
22

3+
### 2.0.3.0
4+
5+
* `text-2.0` support
6+
* `bytestring-0.11.2.0` support
7+
* Rewrite pure text literal unescaper.
8+
* Add `QuickCheck`'s `Arbitrary`, `CoArbitrary` and `Function` instances
9+
310
### 2.0.2.0
411

512
* Add `IsList (KeyMap v)` instance.

src/Data/Aeson/Key.hs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import qualified Data.String
3131
import qualified Data.Text as T
3232
import qualified Data.Text.Short as ST
3333
import qualified Language.Haskell.TH.Syntax as TH
34+
import qualified Test.QuickCheck as QC
3435

3536
newtype Key = Key { unKey :: Text }
3637
deriving (Eq, Ord, Typeable, Data)
@@ -103,3 +104,16 @@ instance TH.Lift Key where
103104
#elif MIN_VERSION_template_haskell(2,16,0)
104105
liftTyped = TH.unsafeTExpCoerce . TH.lift
105106
#endif
107+
108+
-- | @since 2.0.3.0
109+
instance QC.Arbitrary Key where
110+
arbitrary = fromString <$> QC.arbitrary
111+
shrink k = fromString <$> QC.shrink (toString k)
112+
113+
-- | @since 2.0.3.0
114+
instance QC.CoArbitrary Key where
115+
coarbitrary = QC.coarbitrary . toString
116+
117+
-- | @since 2.0.3.0
118+
instance QC.Function Key where
119+
function = QC.functionMap toString fromString

src/Data/Aeson/KeyMap.hs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ module Data.Aeson.KeyMap (
4848
fromListWith,
4949
toList,
5050
toAscList,
51+
elems,
5152

5253
-- * Maps
5354
fromHashMap,
@@ -121,6 +122,7 @@ import qualified Data.Traversable.WithIndex as WI (TraversableWithIndex (..))
121122
import qualified Data.Semialign as SA
122123
import qualified Data.Semialign.Indexed as SAI
123124
import qualified GHC.Exts
125+
import qualified Test.QuickCheck as QC
124126
import qualified Witherable as W
125127

126128
#ifdef USE_ORDEREDMAP
@@ -235,12 +237,18 @@ fromListWith op = KeyMap . M.fromListWith op
235237
fromList :: [(Key, v)] -> KeyMap v
236238
fromList = KeyMap . M.fromList
237239

238-
-- | Return a list of this map's elements.
240+
-- | Return a list of this map's keys and elements.
239241
--
240242
-- The order is not stable. Use 'toAscList' for stable ordering.
241243
toList :: KeyMap v -> [(Key, v)]
242244
toList = M.toList . unKeyMap
243245

246+
-- | Return a list of this map' elements.
247+
--
248+
-- @since 2.0.2.0
249+
elems :: KeyMap v -> [v]
250+
elems = M.elems . unKeyMap
251+
244252
-- | Return a list of this map's elements in ascending order
245253
-- based of the textual key.
246254
toAscList :: KeyMap v -> [(Key, v)]
@@ -436,6 +444,12 @@ fromList = KeyMap . H.fromList
436444
toList :: KeyMap v -> [(Key, v)]
437445
toList = H.toList . unKeyMap
438446

447+
-- | Return a list of this map' elements.
448+
--
449+
-- @since 2.0.2.0
450+
elems :: KeyMap v -> [v]
451+
elems = H.elems . unKeyMap
452+
439453
-- | Return a list of this map's elements in ascending order
440454
-- based of the textual key.
441455
toAscList :: KeyMap v -> [(Key, v)]
@@ -679,3 +693,25 @@ instance W.FilterableWithIndex Key KeyMap where
679693
imapMaybe = mapMaybeWithKey
680694

681695
instance W.WitherableWithIndex Key KeyMap where
696+
697+
-------------------------------------------------------------------------------
698+
-- QuickCheck
699+
-------------------------------------------------------------------------------
700+
701+
-- | @since 2.0.3.0
702+
instance QC.Arbitrary1 KeyMap where
703+
liftArbitrary a = fmap fromList (QC.liftArbitrary (QC.liftArbitrary a))
704+
liftShrink shr m = fmap fromList (QC.liftShrink (QC.liftShrink shr) (toList m))
705+
706+
-- | @since 2.0.3.0
707+
instance QC.Arbitrary v => QC.Arbitrary (KeyMap v) where
708+
arbitrary = QC.arbitrary1
709+
shrink = QC.shrink1
710+
711+
-- | @since 2.0.3.0
712+
instance QC.CoArbitrary v => QC.CoArbitrary (KeyMap v) where
713+
coarbitrary = QC.coarbitrary . toList
714+
715+
-- | @since 2.0.3.0
716+
instance QC.Function v => QC.Function (KeyMap v) where
717+
function = QC.functionMap toList fromList

src/Data/Aeson/Types/Internal.hs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ import qualified Data.Vector as V
110110
import qualified Language.Haskell.TH.Syntax as TH
111111
import qualified Data.Aeson.Key as Key
112112
import qualified Data.Aeson.KeyMap as KM
113+
import qualified Data.Scientific as Sci
114+
import qualified Data.Text as T
115+
import qualified Test.QuickCheck as QC
113116

114117
-- | Elements of a JSON path used to describe the location of an
115118
-- error.
@@ -388,6 +391,92 @@ instance Show Value where
388391
. showsPrec 11 (KM.toAscList xs)
389392
. showChar ')'
390393

394+
-- | @since 2.0.3.0
395+
instance QC.Arbitrary Value where
396+
arbitrary = QC.sized arbValue
397+
398+
shrink Null = []
399+
shrink (Bool b) = Null : map Bool (QC.shrink b)
400+
shrink (String x) = Null : map (String . T.pack) (QC.shrink (T.unpack x))
401+
shrink (Number x) = Null : map Number (shrScientific x)
402+
shrink (Array x) = Null : V.toList x ++ map (Array . V.fromList) (QC.shrink (V.toList x))
403+
shrink (Object x) = Null : KM.elems x ++ map (Object . KM.fromList) (QC.shrink (KM.toList x))
404+
405+
-- | @since 2.0.3.0
406+
instance QC.CoArbitrary Value where
407+
coarbitrary Null = QC.variant (0 :: Int)
408+
coarbitrary (Bool b) = QC.variant (1 :: Int) . QC.coarbitrary b
409+
coarbitrary (String x) = QC.variant (2 :: Int) . QC.coarbitrary (T.unpack x)
410+
coarbitrary (Number x) = QC.variant (3 :: Int) . QC.coarbitrary (Sci.coefficient x) . QC.coarbitrary (Sci.base10Exponent x)
411+
coarbitrary (Array x) = QC.variant (4 :: Int) . QC.coarbitrary (V.toList x)
412+
coarbitrary (Object x) = QC.variant (5 :: Int) . QC.coarbitrary (KM.toList x)
413+
414+
-- | @since 2.0.3.0
415+
instance QC.Function Value where
416+
function = QC.functionMap fwd bwd where
417+
fwd :: Value -> RepValue
418+
fwd Null = Left Nothing
419+
fwd (Bool b) = Left (Just b)
420+
fwd (String x) = Right (Left (Left (T.unpack x)))
421+
fwd (Number x) = Right (Left (Right (Sci.coefficient x, Sci.base10Exponent x)))
422+
fwd (Array x) = Right (Right (Left (V.toList x)))
423+
fwd (Object x) = Right (Right (Right (KM.toList x)))
424+
425+
bwd :: RepValue -> Value
426+
bwd (Left Nothing) = Null
427+
bwd (Left (Just b)) = Bool b
428+
bwd (Right (Left (Left x))) = String (T.pack x)
429+
bwd (Right (Left (Right (x, y)))) = Number (Sci.scientific x y)
430+
bwd (Right (Right (Left x))) = Array (V.fromList x)
431+
bwd (Right (Right (Right x))) = Object (KM.fromList x)
432+
433+
-- Used to implement QC.Function Value instance
434+
type RepValue
435+
= Either (Maybe Bool) (Either (Either String (Integer, Int)) (Either [Value] [(Key, Value)]))
436+
437+
arbValue :: Int -> QC.Gen Value
438+
arbValue n
439+
| n <= 0 = QC.oneof
440+
[ pure Null
441+
, Bool <$> QC.arbitrary
442+
, String <$> arbText
443+
, Number <$> arbScientific
444+
]
445+
446+
| otherwise = QC.oneof
447+
[ Object <$> arbObject n
448+
, Array <$> arbArray n
449+
]
450+
451+
arbText :: QC.Gen Text
452+
arbText = T.pack <$> QC.arbitrary
453+
454+
arbScientific :: QC.Gen Scientific
455+
arbScientific = Sci.scientific <$> QC.arbitrary <*> QC.arbitrary
456+
457+
shrScientific :: Scientific -> [Scientific]
458+
shrScientific s = map (uncurry Sci.scientific) $
459+
QC.shrink (Sci.coefficient s, Sci.base10Exponent s)
460+
461+
arbObject :: Int -> QC.Gen Object
462+
arbObject n = do
463+
p <- arbPartition (n - 1)
464+
KM.fromList <$> traverse (\m -> (,) <$> QC.arbitrary <*> arbValue m) p
465+
466+
arbArray :: Int -> QC.Gen Array
467+
arbArray n = do
468+
p <- arbPartition (n - 1)
469+
V.fromList <$> traverse arbValue p
470+
471+
arbPartition :: Int -> QC.Gen [Int]
472+
arbPartition k = case compare k 1 of
473+
LT -> pure []
474+
EQ -> pure [1]
475+
GT -> do
476+
first <- QC.chooseInt (1, k)
477+
rest <- arbPartition $ k - first
478+
QC.shuffle (first : rest)
479+
391480
-- |
392481
--
393482
-- The ordering is total, consistent with 'Eq' instance.

tests/Instances.hs

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,15 @@ import Prelude.Compat
1414
import Control.Applicative (empty)
1515
import Control.Monad
1616
import Data.Aeson.Types
17-
import qualified Data.Aeson.KeyMap as KM
1817
import Data.Function (on)
1918
import Data.Time (ZonedTime(..), TimeZone(..))
2019
import Data.Time.Clock (UTCTime(..))
2120
import Functions
22-
import Test.QuickCheck (Arbitrary(..), elements, oneof, sized, Gen, chooseInt, shuffle)
21+
import Test.QuickCheck (Arbitrary(..), elements, oneof)
2322
import Types
24-
import qualified Data.Aeson.Key as Key
2523
import qualified Data.DList as DList
26-
import qualified Data.Vector as V
2724
import qualified Data.HashMap.Strict as HM
2825

29-
3026
import Data.Orphans ()
3127
import Test.QuickCheck.Instances ()
3228

@@ -171,41 +167,3 @@ instance (ApproxEq a) => ApproxEq [a] where
171167

172168
instance Arbitrary a => Arbitrary (DList.DList a) where
173169
arbitrary = DList.fromList <$> arbitrary
174-
175-
instance Arbitrary Key where
176-
arbitrary = Key.fromText <$> arbitrary
177-
178-
instance Arbitrary Value where
179-
arbitrary = sized arb where
180-
arb :: Int -> Gen Value
181-
arb n
182-
| n <= 1 = oneof
183-
[ return Null
184-
, fmap Bool arbitrary
185-
, fmap String arbitrary
186-
, fmap Number arbitrary
187-
]
188-
189-
| otherwise = oneof [arr n, obj n]
190-
191-
arr n = do
192-
pars <- arbPartition (n - 1)
193-
fmap (Array . V.fromList) (traverse arb pars)
194-
195-
obj n = do
196-
pars <- arbPartition (n - 1)
197-
fmap (Object . KM.fromList) (traverse pair pars)
198-
199-
pair n = do
200-
k <- arbitrary
201-
v <- arb n
202-
return (k, v)
203-
204-
arbPartition :: Int -> Gen [Int]
205-
arbPartition k = case compare k 1 of
206-
LT -> pure []
207-
EQ -> pure [1]
208-
GT -> do
209-
first <- chooseInt (1, k)
210-
rest <- arbPartition $ k - first
211-
shuffle (first : rest)

tests/PropUtils.hs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import Types
2626
import Text.Read (readMaybe)
2727
import qualified Data.Attoparsec.Lazy as L
2828
import qualified Data.ByteString.Lazy.Char8 as L
29-
import qualified Data.HashMap.Strict as H
3029
import qualified Data.Map as Map
3130
import qualified Data.Text as T
3231
import qualified Data.Vector as V

tests/Properties.hs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ import Test.QuickCheck (Property)
1111
import PropUtils
1212
import PropertyGeneric
1313
import PropertyKeys
14+
import PropertyQC
1415
import PropertyRoundTrip
1516
import PropertyTH
1617

1718

1819
tests :: TestTree
1920
tests = testGroup "properties" [
20-
testGroup "encode" [
21-
testProperty "encodeDouble" encodeDouble
21+
testGroup "encode"
22+
[ testProperty "encodeDouble" encodeDouble
2223
, testProperty "encodeInteger" encodeInteger
2324
]
2425
, testProperty "read . show = id" roundtripReadShow
@@ -38,4 +39,5 @@ tests = testGroup "properties" [
3839
]
3940
, genericTests
4041
, templateHaskellTests
42+
, quickcheckTests
4143
]

tests/PropertyQC.hs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{-# LANGUAGE BangPatterns #-}
2+
{-# LANGUAGE NoImplicitPrelude #-}
3+
module PropertyQC (quickcheckTests) where
4+
5+
import Prelude.Compat
6+
7+
import Instances ()
8+
import Test.Tasty (TestTree, testGroup)
9+
import Test.Tasty.QuickCheck (testProperty)
10+
import Test.QuickCheck (shrink)
11+
12+
import Data.Aeson (Value)
13+
14+
quickcheckTests :: TestTree
15+
quickcheckTests = testGroup "QuickCheck"
16+
[ testProperty "shrink terminates" shrink_prop
17+
]
18+
19+
-- | Test that shrink eventually (in 1000000 steps at most) terminates.
20+
shrink_prop :: Value -> Bool
21+
shrink_prop = go 0 where
22+
go :: Int -> Value -> Bool
23+
go !n v
24+
| n >= 1000000 = False
25+
| otherwise = case shrink v of
26+
[] -> True
27+
v' : _ -> go (n + 1) v'
28+

0 commit comments

Comments
 (0)