@@ -11,13 +11,13 @@ import Data.API.Tools.Combinators
1111import Data.API.Tools.Datatypes
1212import Data.API.Types
1313
14- import GHC.Generics
1514import Control.Applicative
15+ import Control.Monad
1616import Data.Monoid
1717import Data.Time
1818import Language.Haskell.TH
19- import Test.QuickCheck as QC
2019import 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
2626quickCheckTool :: APITool
2727quickCheckTool = 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.
3430mkArbitraryInstance :: 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).
5649gen_sn_ab :: Tool (APINode , SpecNewtype )
5750gen_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
7481gen_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
92129gen_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
107155gen_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.
117167arbitraryIntRange :: IntRange -> Gen Int
0 commit comments