qc-oi-testgenerator 1.2.0.1 → 1.2.0.2
raw patch · 4 files changed
+135/−24 lines, 4 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- Test/OITestGenerator.hs +124/−13
- Test/OITestGenerator/Axiom.hs +8/−8
- Test/OITestGenerator/Op.hs +2/−2
- qc-oi-testgenerator.cabal +1/−1
Test/OITestGenerator.hs view
@@ -1,5 +1,116 @@ {-# LANGUAGE TemplateHaskell #-} +{-|+Module : Test.OITestGenerator+Description : Generate operation invariance tests from axioms at compile time+Copyright : (c) Tobias Gödderz, 2014+License : BSD3+Maintainer : haskell@tobias.goedderz.info+Stability : provisional+Portability : non-portable (Template-Haskell)++("QuickCheck"-)tests for data types are often formulated as equations, i.e.++>rev_test :: Property+>rev_test = property $ \xs ys -> reverse ys ++ reverse xs == reverse (xs ++ ys)++. There are assumptions everyone usually makes about the behaviour of @(==)@,+but does not necessarily think about:++ 1. @x == x@ should always be 'True' (reflexivity).++ 2. if @x == y@ is 'True', @y == x@ should also be 'True' (symmetry).++ 3. if @x == y@ and @y == z@ are 'True', @x == z@ should also be 'True'+ (transitivity).++ 4. if @x == y@ is true, @f x == f y@ should also be 'True' (congruence or+ operation invariance).++It is unlikely to accidentally write an 'Eq' instance that does not satisfy 1-3.+Operation invariance however is fickle, as it also implies sane behaviour of all+functions on the data type.++This framework takes "QuickCheck"-like equality tests (called axioms) of a data+type and functions on the data type (called operations), and uses them to+generate the corresponding "QuickCheck" tests, as well as additional operation+invariance tests.++For example, given a queue of integers 'IntQueue' with the following operations.++>empty :: IntQueue -- create an empty queue+>isEmpty :: IntQueue -> Bool -- test if a given queue is empty+>enqueue :: Int -> IntQueue -> IntQueue -- enqueue an integer to a queue+>dequeue :: IntQueue -> IntQueue -- dequeue the first element of a queue+>front :: IntQueue -> Int -- get the first element of a queue++A "QuickCheck" test for this might be++>property $ \x q -> not (isEmpty q) ==> dequeue (enqueue x q) == enqueue x (dequeue q)++while this would be formulated as an axiom as follows:++>q6 x q = not (isEmpty q) ===> dequeue (enqueue x q) =!= enqueue x (dequeue q)++Assuming you wrote six axioms @q1@-@q6@, the six corresponding "QuickCheck"+tests can be generated by 'generate_basic_tests'.+++@+{\-\# LANGUAGE TemplateHaskell \#-\}+...+queue_basic_tests :: [Property]+queue_basic_tests = $(+ let axs = map axiom ['q1, 'q2, 'q3, 'q4, 'q5, 'q6]+ in generate_basic_tests axs)+@++The single quotes preceding @q1@ to @q6@ quote each following name+as a Template Haskell 'Language.Haskell.TH.Syntax.Name'. Note that @$(...)@ is+a Template Haskell splice and will be evaluated during compile time. Because of+this, you might not refer to declarations of the same module except by name+(this is called the stage restriction), which is the motivation for the @let ...+in ...@ construct.++Additional operation invariance tests can be generated with 'generate_oi_tests'.++>may_dequeue :: IntQueue -> Bool+>may_dequeue = not . isEmpty+>+>may_front :: IntQueue -> Bool+>may_front = not . isEmpty+>+>adt_oi_tests :: [Property]+>adt_oi_tests = $(+> let ops = [ op 'empty, op 'enqueue, op 'isEmpty+> , op 'dequeue `withConstraint` 'may_dequeue+> , op 'front `withConstraint` 'may_front+> ]+> axs = map axiom ['q1, 'q2, 'q3, 'q4, 'q5, 'q6 ]+> in generate_oi_tests axs ops)++For every operation @f@ and every axiom @lhs =!= rhs@, a test @f lhs == f rhs@+will be generated if it is typecorrect. Note that polymorphic types aren't+supported yet, but you can simply rename and specialize them, i.e.++>reverse_ints :: [Int] -> [Int]+>reverse_ints = reverse++and test the renamed function. Besides typechecking, 'generate_oi_tests' takes+care of++ * passing axiom arguments, e.g. @f@ and @ax x = lhs x =!= rhs x@ will get+ translated to @\x -> f (lhs x) == f (rhs x)@.++ * multiple operation arguments, e.g. for an @f :: A -> A -> B -> C@ and an+ axiom with result @A@, both tests @f lhs y z == f rhs y z@ and @f x lhs z ==+ f x rhs z@ will be generated.++ * translating constraints of both axioms and operations to '==>'. Axiom+ constraints are specified via '===>', while operation constraints are+ specified via 'withConstraint'.++-} module Test.OITestGenerator ( AxiomResult, (===>), (=!=), Axiom, axiom,@@ -29,28 +140,28 @@ -- -- It returns an expression of type @[Property]@, i.e. ----- > $(generate_basic_tests axs) :: [Property]+-- >$(generate_basic_tests axs) :: [Property] generate_basic_tests :: [Axiom] -> ExpQ generate_basic_tests = liftM ListE . mapM gen_basic_test -- | Generate all operation invariance tests that typecheck. For example, given -- an operation ----- > f :: A -> A -> B -> C+-- >f :: A -> A -> B -> C -- -- and an axiom ----- > ax :: D -> AxiomResult A--- > ax x = lhs x =!= rhs x+-- >ax :: D -> AxiomResult A+-- >ax x = lhs x =!= rhs x -- -- the following tests are generated: ----- > property $ \y2 y3 -> f (lhs x) y2 y3 == f (rhs x) y2 y3--- > property $ \y1 y3 -> f y1 (lhs x) y3 == f y1 (rhs x) y3+-- >property $ \y2 y3 -> f (lhs x) y2 y3 == f (rhs x) y2 y3+-- >property $ \y1 y3 -> f y1 (lhs x) y3 == f y1 (rhs x) y3 -- -- but not ----- > property $ \y1 y2 -> f y1 y2 (lhs x) == f y1 y2 (rhs x)+-- >property $ \y1 y2 -> f y1 y2 (lhs x) == f y1 y2 (rhs x) -- -- because of the types. --@@ -58,7 +169,7 @@ -- -- It returns an expression of type @[Property]@, i.e. ----- > $(generate_oi_tests axs ops) :: [Property]+-- >$(generate_oi_tests axs ops) :: [Property] generate_oi_tests :: [Axiom] -> [Op] -> ExpQ generate_oi_tests = gen_tests @@ -66,7 +177,7 @@ -- -- It returns an expression of type @[Property]@, i.e. ----- > $(generate_axiom's_tests ax ops) :: [Property]+-- >$(generate_axiom's_tests ax ops) :: [Property] generate_axiom's_tests :: Axiom -> [Op] -> ExpQ generate_axiom's_tests ax = gen_tests [ax] @@ -78,7 +189,7 @@ -- -- It returns an expression of type @Property@, i.e. ----- > $(generate_single_test ax op) :: Property+-- >$(generate_single_test ax op) :: Property generate_single_test :: Axiom -> Op -> ExpQ generate_single_test ax opn = op2opArgs opn >>= filter_Ops ax@@ -110,12 +221,12 @@ -- The first argument can replace the default function generating a name for -- each test, which is ----- > \opn argi ax -> opn ++ show argi ++ "_" ++ ax+-- >\opn argi ax -> opn ++ show argi ++ "_" ++ ax -- -- . An example output looks like this: ----- > enqueue1_q3 :: Property--- > enqueue1_q3 = $(generate_single_test (axiom 'q3) (op 'enqueue `only` 1))+-- >enqueue1_q3 :: Property+-- >enqueue1_q3 = $(generate_single_test (axiom 'q3) (op 'enqueue `only` 1)) show_all_tests :: Maybe (String -> Int -> String -> String) -> [Name] -> [Name] -> ExpQ show_all_tests Nothing axs ops = show_all_tests (Just def_test_name) axs ops show_all_tests (Just fun) axs ops =
Test/OITestGenerator/Axiom.hs view
@@ -21,13 +21,13 @@ -- | Combine the results of the left- and right-hand sides of an axiom. -- A simple example is ----- > q1 :: AxiomResult Bool--- > q1 = isEmpty empty =!= True+-- >q1 :: AxiomResult Bool+-- >q1 = isEmpty empty =!= True -- -- while an axiom with variables would be written as a function like this: ----- > q2 :: Int -> IntQueue -> AxiomResult Bool--- > q2 x q = isEmpty (enqueue x q) =!= False+-- >q2 :: Int -> IntQueue -> AxiomResult Bool+-- >q2 x q = isEmpty (enqueue x q) =!= False (=!=) :: a -> a -> AxiomResult a lhs =!= rhs = AxiomResult (True, lhs, rhs) @@ -38,8 +38,8 @@ -- -- Example usage: ----- > q4 :: Int -> IntQueue -> AxiomResult Int--- > q4 x q = not (isEmpty q) ===> front (enqueue x q) =!= front q+-- >q4 :: Int -> IntQueue -> AxiomResult Int+-- >q4 x q = not (isEmpty q) ===> front (enqueue x q) =!= front q (===>) :: Bool -> AxiomResult a -> AxiomResult a cond ===> AxiomResult (cond', lhs, rhs) = AxiomResult (cond && cond', lhs, rhs) @@ -61,11 +61,11 @@ -- | 'Axiom' constructor. Takes a 'Language.Haskell.TH.Syntax.Name', i.e. ----- > axiom 'q1+-- >axiom 'q1 -- -- or ----- > map axiom ['q1, 'q2, 'q3]+-- >map axiom ['q1, 'q2, 'q3] -- -- . axiom :: Name -> Axiom
Test/OITestGenerator/Op.hs view
@@ -49,7 +49,7 @@ -- -- Example: ----- > op 'dequeue `but` arg 1+-- >op 'dequeue `but` arg 1 but :: Op -> Arg -> Op but op' excl = modify argis (liftM $ delete excl) op' @@ -57,7 +57,7 @@ -- -- Example: ----- > op 'dequeue `only` arg 1+-- >op 'dequeue `only` arg 1 only :: Op -> Arg -> Op only op' argi = modify argis (>>= \args -> if argi `elem` args
qc-oi-testgenerator.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/ name: qc-oi-testgenerator-version: 1.2.0.1+version: 1.2.0.2 synopsis: Compile time generation of operation invariance tests for QuickCheck -- description: --homepage: http://tobias.goedderz.info/haskell/qc-oi-testgenerator/