Skip to content

Commit ee08f85

Browse files
committed
Try to generate shrinkers without generics
Implement: * `shrinkNewtype` to shrink `SpecNewType`; * `shrinkUnion` to shrink `SpecUnion`; * `SpecEnum` does not shrink; * `shrinkRecord` (partially implemented) to shrink `SpecRecord`;
1 parent fccda00 commit ee08f85

File tree

6 files changed

+158
-29
lines changed

6 files changed

+158
-29
lines changed

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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ module Data.API.Tools.Datatypes
1010
, nodeConE
1111
, nodeConP
1212
, nodeNewtypeConE
13+
, nodeNewtypeConP
1314
, nodeFieldE
1415
, nodeFieldP
1516
, nodeAltConE
1617
, nodeAltConP
1718
, newtypeProjectionE
19+
, pref_field_nm
1820
) where
1921

2022
import Data.API.TH
@@ -252,6 +254,9 @@ nodeConP an = conP (rep_type_nm an)
252254
nodeNewtypeConE :: ToolSettings -> APINode -> SpecNewtype -> ExpQ
253255
nodeNewtypeConE ts an sn = conE $ newtype_con_nm (newtypeSmartConstructors ts && isJust (snFilter sn)) an
254256

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

src/Data/API/Tools/QuickCheck.hs

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ 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
15+
import Control.Monad
1616
import Data.Monoid
1717
import Data.Time
1818
import Language.Haskell.TH
19-
import Test.QuickCheck as QC
2019
import Prelude
20+
import Test.QuickCheck as QC
2121

2222

2323
-- | Tool to generate 'Arbitrary' instances for generated types. This tool generates
@@ -26,22 +26,15 @@ import Prelude
2626
quickCheckTool :: APITool
2727
quickCheckTool = apiNodeTool $ apiSpecTool gen_sn_ab gen_sr_ab gen_su_ab gen_se_ab mempty
2828

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.
29+
-- | Helper to create an 'Arbitrary' implementation.
3430
mkArbitraryInstance :: ToolSettings
3531
-> TypeQ
3632
-> ExpQ
3733
-- ^ The body of the 'arbitrary' method.
34+
-> ExpQ
35+
-- ^ The body of the 'shrink' method.
3836
-> 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 [] |]
37+
mkArbitraryInstance ts typeQ arbitraryBody shrinkBody = do
4538
optionalInstanceD ts ''QC.Arbitrary [typeQ]
4639
[ simpleD 'arbitrary arbitraryBody
4740
, simpleD 'shrink shrinkBody
@@ -55,24 +48,38 @@ mkArbitraryInstance ts typeQ arbitraryBody = do
5548
-- values).
5649
gen_sn_ab :: Tool (APINode, SpecNewtype)
5750
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 |]
51+
Nothing | snType sn == BTint -> mk_instance ts an sn [e| QC.arbitraryBoundedIntegral |] (shrinkNewtype ts an sn)
52+
| otherwise -> mk_instance ts an sn [e| arbitrary |] (shrinkNewtype ts an sn)
53+
Just (FtrIntg ir) ->
54+
mk_instance ts an sn [e| arbitraryIntRange ir |] (shrinkNewtype ts an sn)
55+
Just (FtrUTC ur) ->
56+
mk_instance ts an sn [e| arbitraryUTCRange ur |] (shrinkNewtype ts an sn)
6257
Just (FtrStrg _) -> return []
6358
where
6459
mk_instance ts an sn arb =
6560
mkArbitraryInstance ts (nodeRepT an) [e| fmap $(nodeNewtypeConE ts an sn) $arb |]
6661

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

6875
-- | Generate an 'Arbitrary' instance for a record:
6976
--
7077
-- > instance Arbitrary Foo where
7178
-- > arbitrary = sized $ \ x -> Foo <$> resize (x `div` 2) arbitrary <*> ... <*> resize (x `div` 2) arbitrary
72-
-- > shrink = genericShrink
79+
-- > shrink = (TH-derived shrinker)
7380

7481
gen_sr_ab :: Tool (APINode, SpecRecord)
75-
gen_sr_ab = mkTool $ \ ts (an, sr) -> mkArbitraryInstance ts (nodeRepT an) (bdy an sr)
82+
gen_sr_ab = mkTool $ \ ts (an, sr) -> mkArbitraryInstance ts (nodeRepT an) (bdy an sr) (shrinkRecord an sr)
7683
where
7784
-- Reduce size of fields to avoid generating massive test data
7885
-- by giving an arbitrary implementation like this:
@@ -83,35 +90,78 @@ gen_sr_ab = mkTool $ \ ts (an, sr) -> mkArbitraryInstance ts (nodeRepT an) (bdy
8390
replicate (length $ srFields sr) $
8491
[e| QC.resize ($(varE x) `div` 2) arbitrary |]
8592

93+
-- For records, using the same principle behind 'genericShrink', we need
94+
-- to generate a list of lists, each sublist being the shrinking of a single
95+
-- individual field, and finally mconcat everything together.
96+
-- Example:
97+
-- shrink = \(Foo a b c) ->
98+
-- (Foo <$> shrink a <*> pure b <*> pure c) ++
99+
-- (Foo <$> pure a <*> shrink b <*> pure c) ++
100+
-- (Foo <$> pure a <*> pure b <*> shrink c)
101+
shrinkRecord :: APINode -> SpecRecord -> ExpQ
102+
shrinkRecord an sr = do
103+
x <- newName "x"
104+
-- Matches the fields of the record with fresh variables
105+
-- [( "field1", "field1"), ("field2", "field2") ... ]
106+
recordPatterns <-
107+
forM (srFields sr) $ \(fn,_) -> do
108+
let freshRecName = pref_field_nm an fn
109+
freshPatName <- nodeFieldP an fn
110+
pure (freshRecName,freshPatName)
111+
112+
lamE [varP x] $
113+
caseE (varE x) [
114+
-- temporary, not correct. it won't shrink properly.
115+
match (recP nm (map pure recordPatterns))
116+
(normalB $ applicativeE (nodeConE an) $
117+
flip map recordPatterns $ \(fld, _pat) ->
118+
[e| QC.shrink $(varE fld) |]
119+
) []
120+
]
121+
where
122+
nm = rep_type_nm an
86123

87124
-- | Generate an 'Arbitrary' instance for a union:
88125
--
89126
-- > instance Arbitrary Foo where
90127
-- > arbitrary = oneOf [ fmap Bar arbitrary, fmap Baz arbitrary ]
91128

92129
gen_su_ab :: Tool (APINode, SpecUnion)
93-
gen_su_ab = mkTool $ \ ts (an, su) -> mkArbitraryInstance ts (nodeRepT an) (bdy an su)
130+
gen_su_ab = mkTool $ \ ts (an, su) -> mkArbitraryInstance ts (nodeRepT an) (bdy an su) (shrinkUnion an su)
94131
where
95132
bdy an su | null (suFields su) = nodeConE an
96133
| otherwise = [e| oneof $(listE alts) |]
97134
where
98135
alts = [ [e| fmap $(nodeAltConE an k) arbitrary |]
99136
| (k, _) <- suFields su ]
100137

138+
-- For a union, we shrink the individual wrappers.
139+
shrinkUnion :: APINode -> SpecUnion -> ExpQ
140+
shrinkUnion an su = do
141+
x <- newName "x"
142+
y <- newName "y"
143+
lamE [varP x] $ caseE (varE x) (map (shrink_alt y) (suFields su))
144+
where
145+
shrink_alt y (fn,_) =
146+
match (nodeAltConP an fn [varP y])
147+
(normalB [| map $(nodeAltConE an fn) (QC.shrink $(varE y)) |])
148+
[]
101149

102150
-- | Generate an 'Arbitrary' instance for an enumeration:
103151
--
104152
-- > instance Arbitrary Foo where
105153
-- > arbitrary = elements [Bar, Baz]
106154

107155
gen_se_ab :: Tool (APINode, SpecEnum)
108-
gen_se_ab = mkTool $ \ ts (an, se) -> mkArbitraryInstance ts (nodeRepT an) (bdy an se)
156+
gen_se_ab = mkTool $ \ ts (an, se) -> mkArbitraryInstance ts (nodeRepT an) (bdy an se) noShrink
109157
where
110158
bdy an se | null ks = nodeConE an
111159
| otherwise = varE 'elements `appE` listE ks
112160
where
113161
ks = map (nodeAltConE an . fst) $ seAlts se
114162

163+
noShrink :: ExpQ
164+
noShrink = [e| \_ -> [] |]
115165

116166
-- | Generate an arbitrary 'Int' in a given range.
117167
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)