Skip to content

Commit f45a2e5

Browse files
committed
Try to generate shrinkers without generics
Implement: * `shrinkNewtype` to shrink `SpecNewType`; * `shrinkUnion` to shrink `SpecUnion`; * `SpecEnum` does not shrink;
1 parent fccda00 commit f45a2e5

6 files changed

Lines changed: 126 additions & 30 deletions

File tree

api-tools.cabal

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ Library
125125
-Wall
126126
-fwarn-tabs
127127

128-
Default-Language: Haskell2010
128+
Default-Language: GHC2021
129129

130130

131131
Executable migration-tool
@@ -144,7 +144,7 @@ Executable migration-tool
144144
-Wall
145145
-fwarn-tabs
146146

147-
Default-Language: Haskell2010
147+
Default-Language: GHC2021
148148

149149

150150
Executable perf-test
@@ -165,7 +165,7 @@ Executable perf-test
165165
-fwarn-tabs
166166
-rtsopts
167167

168-
Default-Language: Haskell2010
168+
Default-Language: GHC2021
169169

170170

171171
Test-Suite test-api-tools
@@ -206,7 +206,7 @@ Test-Suite test-api-tools
206206
GHC-Options:
207207
-Wall
208208

209-
Default-Language: Haskell2010
209+
Default-Language: GHC2021
210210

211211
Benchmark bench-time
212212
Hs-Source-Dirs: bench
@@ -225,4 +225,4 @@ Benchmark bench-time
225225
GHC-Options:
226226
-Wall
227227

228-
Default-Language: Haskell2010
228+
Default-Language: GHC2021

src/Data/API/API/Gen.hs

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
{-# LANGUAGE StandaloneDeriving #-}
55
{-# LANGUAGE DeriveGeneric #-}
66
{-# LANGUAGE TemplateHaskell #-}
7+
{-# LANGUAGE LambdaCase #-}
8+
{-# LANGUAGE RecordWildCards #-}
9+
{-# OPTIONS_GHC -Wno-orphans #-}
710

811
-- | This module contains datatypes generated from the DSL description
912
-- of the api-tools API; they thus correspond to the types in
@@ -12,13 +15,20 @@ module Data.API.API.Gen where
1215

1316
import Data.API.API.DSL
1417
import Data.API.Tools
15-
import GHC.Generics (Generic)
1618

1719
import Language.Haskell.TH
20+
import qualified Test.QuickCheck as QC
21+
import qualified Test.QuickCheck.Arbitrary as QC
22+
import GHC.Generics
1823

1924
$(generate apiAPI)
2025

21-
deriving instance Generic TypeRef
26+
prop_genIsEqual :: (Eq a, Show a, QC.GSubterms (Rep a) a, QC.RecursivelyShrink (Rep a), Generic a, Shrinkable a)
27+
=> a
28+
-> QC.Property
29+
prop_genIsEqual a = QC.genericShrink a QC.=== shrinkable a
30+
31+
deriving instance Generic TypeRef -- no shrink
2232
deriving instance Generic Field
2333
deriving instance Generic Conversion
2434
deriving instance Generic UTCRange
@@ -32,6 +42,65 @@ deriving instance Generic Filter
3242
deriving instance Generic DefaultValue
3343
deriving instance Generic BasicType
3444

45+
instance Shrinkable a => Shrinkable (Maybe a) where
46+
shrinkable (Just x) = Nothing : [ Just x' | x' <- shrinkable x ]
47+
shrinkable Nothing = []
48+
instance Shrinkable TypeRef -- no shrink
49+
instance Shrinkable Field where
50+
shrinkable Field{..} =
51+
(Field <$> pure _fd_name <*> shrinkable _fd_type <*> pure _fd_readonly <*> pure _fd_default <*> pure _fd_comment) ++
52+
(Field <$> pure _fd_name <*> pure _fd_type <*> QC.shrink _fd_readonly <*> pure _fd_default <*> pure _fd_comment) ++
53+
(Field <$> pure _fd_name <*> pure _fd_type <*> pure _fd_readonly <*> shrinkable _fd_default <*> pure _fd_comment) ++
54+
(Field <$> pure _fd_name <*> pure _fd_type <*> pure _fd_readonly <*> pure _fd_default <*> QC.shrink _fd_comment)
55+
instance Shrinkable Conversion -- no shrink
56+
instance Shrinkable UTCRange where
57+
shrinkable (UTCRange x y) = (UTCRange <$> QC.shrink x <*> pure y) ++
58+
(UTCRange <$> pure x <*> QC.shrink y)
59+
instance Shrinkable IntRange where
60+
shrinkable (IntRange x y) = (IntRange <$> QC.shrink x <*> pure y) ++
61+
(IntRange <$> pure x <*> QC.shrink y)
62+
instance Shrinkable RegularExpression where
63+
shrinkable (RegularExpression e) = map RegularExpression (QC.shrink e)
64+
instance Shrinkable SpecNewtype where
65+
shrinkable SpecNewtype{..} =
66+
(SpecNewtype <$> shrinkable _sn_type <*> pure _sn_filter) ++
67+
(SpecNewtype <$> pure _sn_type <*> shrinkable _sn_filter)
68+
instance Shrinkable APINode where
69+
shrinkable APINode{..} =
70+
(APINode <$> QC.shrink _an_name <*> pure _an_comment <*> pure _an_prefix <*> pure _an_spec <*> pure _an_convert) ++
71+
(APINode <$> pure _an_name <*> QC.shrink _an_comment <*> pure _an_prefix <*> pure _an_spec <*> pure _an_convert) ++
72+
(APINode <$> pure _an_name <*> pure _an_comment <*> QC.shrink _an_prefix <*> pure _an_spec <*> pure _an_convert) ++
73+
(APINode <$> pure _an_name <*> pure _an_comment <*> pure _an_prefix <*> shrinkable _an_spec <*> pure _an_convert) ++
74+
(APINode <$> pure _an_name <*> pure _an_comment <*> pure _an_prefix <*> pure _an_spec <*> shrinkable _an_convert)
75+
instance Shrinkable APIType where -- OK
76+
shrinkable = \case
77+
TY_list aty -> aty : (TY_list <$> shrinkable aty)
78+
TY_maybe aty -> aty : (TY_maybe <$> shrinkable aty)
79+
TY_ref tre -> TY_ref <$> shrinkable tre
80+
TY_basic bt -> TY_basic <$> shrinkable bt
81+
TY_json i -> TY_json <$> QC.shrink i
82+
instance Shrinkable Spec where -- ok
83+
shrinkable = \case
84+
SP_newtype sn -> SP_newtype <$> shrinkable sn
85+
SP_record rc -> SP_record <$> QC.shrinkList shrinkable rc
86+
SP_union un -> SP_union <$> QC.shrinkList shrinkable un
87+
SP_enum en -> SP_enum <$> QC.shrink en -- rely on QC shrinking for Text
88+
SP_synonym sy -> SP_synonym <$> shrinkable sy
89+
instance Shrinkable Filter where
90+
shrinkable = \case
91+
FT_string re -> FT_string <$> shrinkable re
92+
FT_integer ir -> FT_integer <$> shrinkable ir
93+
FT_utc ur -> FT_utc <$> shrinkable ur
94+
instance Shrinkable DefaultValue where
95+
shrinkable = \case
96+
DV_list x -> DV_list <$> QC.shrink x
97+
DV_maybe x -> DV_maybe <$> QC.shrink x
98+
DV_string x -> DV_string <$> QC.shrink x
99+
DV_boolean x -> DV_boolean <$> QC.shrink x
100+
DV_integer x -> DV_integer <$> QC.shrink x
101+
DV_utc x -> DV_utc <$> QC.shrink x
102+
instance Shrinkable BasicType -- no shrink
103+
35104
$(generateAPITools apiAPI
36105
[ enumTool
37106
, jsonTool'

src/Data/API/Tools.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ module Data.API.Tools
3939
, quickCheckTool
4040
, safeCopyTool
4141
, samplesTool
42+
, Shrinkable(..)
4243
) where
4344

4445
import Data.API.Tools.Combinators
@@ -58,7 +59,6 @@ import Data.API.Types
5859
import qualified Data.Monoid as Monoid
5960
import Language.Haskell.TH
6061

61-
6262
-- | Generate the datatypes corresponding to an API.
6363
generate :: API -> Q [Dec]
6464
generate = generateWith defaultToolSettings

src/Data/API/Tools/Datatypes.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module Data.API.Tools.Datatypes
1010
, nodeConE
1111
, nodeConP
1212
, nodeNewtypeConE
13+
, nodeNewtypeConP
1314
, nodeFieldE
1415
, nodeFieldP
1516
, nodeAltConE
@@ -252,6 +253,9 @@ nodeConP an = conP (rep_type_nm an)
252253
nodeNewtypeConE :: ToolSettings -> APINode -> SpecNewtype -> ExpQ
253254
nodeNewtypeConE ts an sn = conE $ newtype_con_nm (newtypeSmartConstructors ts && isJust (snFilter sn)) an
254255

256+
nodeNewtypeConP :: ToolSettings -> APINode -> SpecNewtype -> [Q Pat] -> PatQ
257+
nodeNewtypeConP ts an sn ps = conP (newtype_con_nm (newtypeSmartConstructors ts && isJust (snFilter sn)) an) ps
258+
255259
-- | A record field in an API node, as an expression
256260
nodeFieldE :: APINode -> FieldName -> ExpQ
257261
nodeFieldE an fnm = varE $ pref_field_nm an fnm

src/Data/API/Tools/QuickCheck.hs

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@ import Data.API.Tools.Combinators
1111
import Data.API.Tools.Datatypes
1212
import Data.API.Types
1313

14-
import GHC.Generics
1514
import Control.Applicative
1615
import Data.Monoid
1716
import Data.Time
1817
import Language.Haskell.TH
19-
import Test.QuickCheck as QC
2018
import Prelude
19+
import Test.QuickCheck as QC
2120

2221

2322
-- | Tool to generate 'Arbitrary' instances for generated types. This tool generates
@@ -26,22 +25,15 @@ import Prelude
2625
quickCheckTool :: APITool
2726
quickCheckTool = apiNodeTool $ apiSpecTool gen_sn_ab gen_sr_ab gen_su_ab gen_se_ab mempty
2827

29-
-- | Helper to create an 'Arbitrary' implementation. It will check if we have a 'Generic'
30-
-- instance for the underlying type and, if we have, we will implement 'shrink' in terms of
31-
-- 'genericShrink', otherwise we will just alias it to '[]' (i.e. a no-op). This avoids
32-
-- imposing to the caller a mandatory 'Generic' instance on the type when using this tool,
33-
-- but it will get them a \"shrinker for free\" if they define a 'Generic' instance.
28+
-- | Helper to create an 'Arbitrary' implementation.
3429
mkArbitraryInstance :: ToolSettings
3530
-> TypeQ
3631
-> ExpQ
3732
-- ^ The body of the 'arbitrary' method.
33+
-> ExpQ
34+
-- ^ The body of the 'shrink' method.
3835
-> Q [Dec]
39-
mkArbitraryInstance ts typeQ arbitraryBody = do
40-
tq <- sequence [typeQ]
41-
hasGeneric <- isInstance ''Generic tq
42-
let shrinkBody = case hasGeneric of
43-
True -> [e| genericShrink |]
44-
False -> [e| pure [] |]
36+
mkArbitraryInstance ts typeQ arbitraryBody shrinkBody = do
4537
optionalInstanceD ts ''QC.Arbitrary [typeQ]
4638
[ simpleD 'arbitrary arbitraryBody
4739
, simpleD 'shrink shrinkBody
@@ -55,24 +47,38 @@ mkArbitraryInstance ts typeQ arbitraryBody = do
5547
-- values).
5648
gen_sn_ab :: Tool (APINode, SpecNewtype)
5749
gen_sn_ab = mkTool $ \ ts (an, sn) -> case snFilter sn of
58-
Nothing | snType sn == BTint -> mk_instance ts an sn [e| QC.arbitraryBoundedIntegral |]
59-
| otherwise -> mk_instance ts an sn [e| arbitrary |]
60-
Just (FtrIntg ir) -> mk_instance ts an sn [e| arbitraryIntRange ir |]
61-
Just (FtrUTC ur) -> mk_instance ts an sn [e| arbitraryUTCRange ur |]
50+
Nothing | snType sn == BTint -> mk_instance ts an sn [e| QC.arbitraryBoundedIntegral |] (shrinkNewtype ts an sn)
51+
| otherwise -> mk_instance ts an sn [e| arbitrary |] (shrinkNewtype ts an sn)
52+
Just (FtrIntg ir) ->
53+
mk_instance ts an sn [e| arbitraryIntRange ir |] (shrinkNewtype ts an sn)
54+
Just (FtrUTC ur) ->
55+
mk_instance ts an sn [e| arbitraryUTCRange ur |] (shrinkNewtype ts an sn)
6256
Just (FtrStrg _) -> return []
6357
where
6458
mk_instance ts an sn arb =
6559
mkArbitraryInstance ts (nodeRepT an) [e| fmap $(nodeNewtypeConE ts an sn) $arb |]
6660

61+
-- shrinking a newtype means calling shrink and repack the newtype.
62+
-- Example:
63+
-- shrink = \x -> case x of { Foo y -> map Foo (shrink y) }
64+
shrinkNewtype ts an sn = do
65+
x <- newName "x"
66+
y <- newName "y"
67+
lamE [varP x] $
68+
caseE (varE x) [
69+
match (nodeNewtypeConP ts an sn [varP y])
70+
(normalB [| map $(nodeNewtypeConE ts an sn) (QC.shrink $(varE y)) |])
71+
[]
72+
]
6773

6874
-- | Generate an 'Arbitrary' instance for a record:
6975
--
7076
-- > instance Arbitrary Foo where
7177
-- > arbitrary = sized $ \ x -> Foo <$> resize (x `div` 2) arbitrary <*> ... <*> resize (x `div` 2) arbitrary
72-
-- > shrink = genericShrink
78+
-- > shrink = (TH-derived shrinker)
7379

7480
gen_sr_ab :: Tool (APINode, SpecRecord)
75-
gen_sr_ab = mkTool $ \ ts (an, sr) -> mkArbitraryInstance ts (nodeRepT an) (bdy an sr)
81+
gen_sr_ab = mkTool $ \ ts (an, sr) -> mkArbitraryInstance ts (nodeRepT an) (bdy an sr) [e| \_ -> [] |]
7682
where
7783
-- Reduce size of fields to avoid generating massive test data
7884
-- by giving an arbitrary implementation like this:
@@ -83,35 +89,47 @@ gen_sr_ab = mkTool $ \ ts (an, sr) -> mkArbitraryInstance ts (nodeRepT an) (bdy
8389
replicate (length $ srFields sr) $
8490
[e| QC.resize ($(varE x) `div` 2) arbitrary |]
8591

86-
8792
-- | Generate an 'Arbitrary' instance for a union:
8893
--
8994
-- > instance Arbitrary Foo where
9095
-- > arbitrary = oneOf [ fmap Bar arbitrary, fmap Baz arbitrary ]
9196

9297
gen_su_ab :: Tool (APINode, SpecUnion)
93-
gen_su_ab = mkTool $ \ ts (an, su) -> mkArbitraryInstance ts (nodeRepT an) (bdy an su)
98+
gen_su_ab = mkTool $ \ ts (an, su) -> mkArbitraryInstance ts (nodeRepT an) (bdy an su) (shrinkUnion an su)
9499
where
95100
bdy an su | null (suFields su) = nodeConE an
96101
| otherwise = [e| oneof $(listE alts) |]
97102
where
98103
alts = [ [e| fmap $(nodeAltConE an k) arbitrary |]
99104
| (k, _) <- suFields su ]
100105

106+
-- For a union, we shrink the individual wrappers.
107+
shrinkUnion :: APINode -> SpecUnion -> ExpQ
108+
shrinkUnion an su = do
109+
x <- newName "x"
110+
y <- newName "y"
111+
lamE [varP x] $ caseE (varE x) (map (shrink_alt y) (suFields su))
112+
where
113+
shrink_alt y (fn,_) =
114+
match (nodeAltConP an fn [varP y])
115+
(normalB [| map $(nodeAltConE an fn) (QC.shrink $(varE y)) |])
116+
[]
101117

102118
-- | Generate an 'Arbitrary' instance for an enumeration:
103119
--
104120
-- > instance Arbitrary Foo where
105121
-- > arbitrary = elements [Bar, Baz]
106122

107123
gen_se_ab :: Tool (APINode, SpecEnum)
108-
gen_se_ab = mkTool $ \ ts (an, se) -> mkArbitraryInstance ts (nodeRepT an) (bdy an se)
124+
gen_se_ab = mkTool $ \ ts (an, se) -> mkArbitraryInstance ts (nodeRepT an) (bdy an se) noShrink
109125
where
110126
bdy an se | null ks = nodeConE an
111127
| otherwise = varE 'elements `appE` listE ks
112128
where
113129
ks = map (nodeAltConE an . fst) $ seAlts se
114130

131+
noShrink :: ExpQ
132+
noShrink = [e| \_ -> [] |]
115133

116134
-- | Generate an arbitrary 'Int' in a given range.
117135
arbitraryIntRange :: IntRange -> Gen Int

src/Data/API/Types.hs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ module Data.API.Types
3030
, UTCRange(..)
3131
, RegEx(..)
3232
, Binary(..)
33+
, Shrinkable(..)
3334
, defaultValueAsJsValue
3435
, mkRegEx
3536
, inIntRange
@@ -474,3 +475,7 @@ $(let deriveJSONs = fmap concat . mapM (deriveJSON defaultOptions)
474475
, ''APINode
475476
, ''Thing
476477
])
478+
479+
class Shrinkable a where
480+
shrinkable :: a -> [a]
481+
shrinkable = const []

0 commit comments

Comments
 (0)