Skip to content

Commit 8e3d325

Browse files
authored
Merge pull request #918 from haskell/better-shrink
Add ordNub to shrink
2 parents 74ecc02 + 1214ad7 commit 8e3d325

File tree

3 files changed

+87
-19
lines changed

3 files changed

+87
-19
lines changed

aeson.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ test-suite aeson-tests
194194
, generic-deriving >=1.10 && <1.15
195195
, ghc-prim >=0.2
196196
, hashable
197+
, indexed-traversable
197198
, integer-logarithms >=1 && <1.1
198199
, OneTuple
199200
, primitive

src/Data/Aeson/Types/Internal.hs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ import qualified Data.Aeson.KeyMap as KM
113113
import qualified Data.Scientific as Sci
114114
import qualified Data.Text as T
115115
import qualified Test.QuickCheck as QC
116+
import Witherable (ordNub)
116117

117118
-- | Elements of a JSON path used to describe the location of an
118119
-- error.
@@ -395,12 +396,13 @@ instance Show Value where
395396
instance QC.Arbitrary Value where
396397
arbitrary = QC.sized arbValue
397398

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))
399+
shrink = ordNub . go where
400+
go Null = []
401+
go (Bool b) = Null : map Bool (QC.shrink b)
402+
go (String x) = Null : map (String . T.pack) (QC.shrink (T.unpack x))
403+
go (Number x) = Null : map Number (shrScientific x)
404+
go (Array x) = Null : V.toList x ++ map (Array . V.fromList) (QC.liftShrink go (V.toList x))
405+
go (Object x) = Null : KM.elems x ++ map (Object . KM.fromList) (QC.liftShrink (QC.liftShrink go) (KM.toList x))
404406

405407
-- | @since 2.0.3.0
406408
instance QC.CoArbitrary Value where

tests/PropertyQC.hs

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,93 @@
11
{-# LANGUAGE BangPatterns #-}
2+
{-# LANGUAGE OverloadedStrings #-}
23
{-# LANGUAGE NoImplicitPrelude #-}
4+
{-# LANGUAGE TypeApplications #-}
35
module PropertyQC (quickcheckTests) where
46

57
import Prelude.Compat
68

79
import Instances ()
810
import Test.Tasty (TestTree, testGroup)
9-
import Test.Tasty.QuickCheck (testProperty)
11+
import Test.Tasty.QuickCheck (Arbitrary, Property, testProperty, counterexample, property)
1012
import Test.QuickCheck (shrink)
13+
import Data.Char (isLower, isUpper, isDigit, isSpace)
14+
import Data.Foldable (foldl')
15+
import Data.Foldable.WithIndex (ifoldl')
1116

12-
import Data.Aeson (Value)
17+
import qualified Data.Text as T
18+
import qualified Data.Aeson.Key as K
19+
import qualified Data.Scientific as Sci
20+
21+
import Data.Aeson (Value (..))
1322

1423
quickcheckTests :: TestTree
1524
quickcheckTests = testGroup "QuickCheck"
16-
[ testProperty "shrink terminates" shrink_prop
25+
[ testGroup "shrink terminates"
26+
[ testProperty "Int" $ shrink_prop @Int
27+
, testProperty "Bool" $ shrink_prop @Int
28+
, testProperty "Integer" $ shrink_prop @Integer
29+
, testProperty "Char" $ shrink_prop @Char
30+
, testProperty "Text" $ shrink_prop @T.Text
31+
, testProperty "(Int,Int)" $ shrink_prop @(Integer, Int)
32+
, testProperty "Scientific" $ shrink_prop @Sci.Scientific
33+
, testProperty "Value" $ shrink_prop @Value
34+
]
1735
]
1836

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-
37+
shrink_prop :: (Show a, ShrinkMetric a) => a -> Property
38+
shrink_prop v = case vs' of
39+
[] -> property True
40+
v' : _ -> counterexample (show vs') $
41+
counterexample (show (metric v, metric v', v)) False
42+
where
43+
vs = shrink v
44+
45+
-- we check only 50 first ones, otherwise it would take too long.
46+
vs' = filter (not . predicate) $ take 50 vs
47+
48+
-- shrunk v's should be smaller.
49+
predicate v' = metric v' < metric v
50+
51+
class Arbitrary a => ShrinkMetric a where
52+
metric :: a -> Integer
53+
54+
instance (ShrinkMetric a, ShrinkMetric b) => ShrinkMetric (a, b) where
55+
metric (a, b) = (1 + metric a) * (1 + metric b)
56+
57+
instance ShrinkMetric Bool where
58+
metric b = if b then 1 else 0
59+
60+
instance ShrinkMetric Int where
61+
metric = metric . toInteger
62+
63+
instance ShrinkMetric Integer where
64+
metric i = if i < 0 then 1 + negate i else i
65+
66+
-- Char shrinking is tricky.
67+
-- See: https://hackage.haskell.org/package/QuickCheck-2.14.2/docs/src/Test.QuickCheck.Arbitrary.html#line-664
68+
instance ShrinkMetric Char where
69+
metric c = toInteger $ foldl' (+) 0
70+
[ if not $ isLower c then 0x2000000 else 0
71+
, if not $ isUpper c then 0x1000000 else 0
72+
, if not $ isDigit c then 0x0800000 else 0
73+
, if not $ c == ' ' then 0x0400000 else 0
74+
, if not $ isSpace c then 0x0200000 else 0
75+
, fromEnum c
76+
]
77+
78+
instance ShrinkMetric T.Text where
79+
metric = foldl' (\acc c -> acc + 1 + metric c) 0 . T.unpack
80+
81+
instance ShrinkMetric K.Key where
82+
metric = metric . K.toText
83+
84+
instance ShrinkMetric Sci.Scientific where
85+
metric s = metric (Sci.coefficient s, Sci.base10Exponent s)
86+
87+
instance ShrinkMetric Value where
88+
metric Null = 0
89+
metric (Bool b) = 1 + metric b
90+
metric (String t) = 1 + metric t
91+
metric (Number n) = 1 + metric n
92+
metric (Array xs) = foldl' (\acc x -> acc + 1 + metric x) 1 xs
93+
metric (Object xs) = ifoldl' (\k acc x -> acc + metric (k, x)) 1 xs

0 commit comments

Comments
 (0)