tock-mirror/common/FlowGraphTest.hs

648 lines
29 KiB
Haskell

{-
Tock: a compiler for parallel languages
Copyright (C) 2007 University of Kent
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 2 of the License, or (at your
option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <http://www.gnu.org/licenses/>.
-}
-- #ignore-exports
-- | A module for testing building a control flow-graph from an AST.
module FlowGraphTest (qcTests) where
import Control.Monad.Identity
import Control.Monad.State
import Data.Generics
import Data.Graph.Inductive
import Data.List
import qualified Data.Map as Map
import Data.Maybe
import System.Random
import Test.HUnit hiding (Node, State, Testable)
import Test.QuickCheck
import qualified AST as A
import FlowGraph
import Metadata
import PrettyShow
import TestUtils
import Utils
-- | Makes a distinctive metatag for testing. The function is one-to-one.
makeMeta :: Int -> Meta
makeMeta n = Meta (Just "FlowGraphTest") n 0
-- To make typing the tests as short as possible (typing a function call means bracketing is needed, which is a pain):
m0 = makeMeta 0
m1 = makeMeta 1
m2 = makeMeta 2
m3 = makeMeta 3
m4 = makeMeta 4
m5 = makeMeta 5
m6 = makeMeta 6
m7 = makeMeta 7
m8 = makeMeta 8
m9 = makeMeta 9
m10 = makeMeta 10
m11 = makeMeta 11
-- | For meta tags that shouldn't be used in the graph:
mU = makeMeta (-1)
-- | A subscripting function for meta-tags produced by makeMeta
sub :: Meta -> Int -> Meta
sub m n = m {metaColumn = n}
-- Various abbreviations for unique A.Process items
sm0 = A.Skip m0
sm1 = A.Skip m1
sm2 = A.Skip m2
sm3 = A.Skip m3
sm4 = A.Skip m4
sm5 = A.Skip m5
sm6 = A.Skip m6
sm7 = A.Skip m7
sm8 = A.Skip m8
sm9 = A.Skip m9
sm10 = A.Skip m10
-- | Shows a graph as a node and edge list.
showGraph :: (Graph g, Show a, Show b) => g a b -> String
showGraph g = " Nodes: " ++ show (labNodes g) ++ " Edges: " ++ show (labEdges g)
-- | A shortcut for nextId' 1.
nextId :: Data t => t -> State (Map.Map Meta Int) Int
nextId = nextId' 1
-- | Given an AST fragment, returns a unique integer associated with that meta-tag.
-- This is for when you may add nodes based on a certain meta-tag to the tree multiple times,
-- and you want to be able to differentiate between each use.
nextId' :: Data t => Int -> t -> State (Map.Map Meta Int) Int
nextId' inc t
= do mp <- get
case Map.lookup m mp of
Just n -> do put $ Map.adjust ((+) inc) m mp
return n
Nothing -> do put $ Map.insert m inc mp
return 0
where m = findMeta t
-- | Given a test name, a list of nodes, a list of edges and an AST fragment, tests that the
-- CFG produced from the given AST matches the nodes and edges. The nodes do not have to have
-- the exact correct identifiers produced by the graph-building. Instead, the graphs are checked
-- for being isomorphic, based on the meta-tag node labels (node E in the expected list is
-- isomorphic to node A in the actual list if their meta tags are the same).
testGraph :: String -> [(Int, Meta)] -> [(Int, Int, EdgeLabel)] -> A.Process -> Test
testGraph testName nodes edges proc = testGraph' testName nodes edges (A.OnlyP emptyMeta proc)
--TODO test root nodes too
testGraph' :: String -> [(Int, Meta)] -> [(Int, Int, EdgeLabel)] -> A.Structured -> Test
testGraph' testName nodes edges code
= TestCase $
case evalState (buildFlowGraph testOps code) Map.empty of
Left err -> assertFailure (testName ++ " graph building failed: " ++ err)
Right (g,_) -> checkGraphEquality (nodes, edges) (g :: FlowGraph Identity Int)
where
-- Checks two graphs are equal by creating a node mapping from the expected graph to the real map (checkNodeEquality),
-- then mapping the edges across (transformEdge) and checking everything is right (in checkGraphEquality)
deNode :: Monad m => FNode m a -> (Meta, a)
deNode (Node (x,y,_)) = (x,y)
testOps :: GraphLabelFuncs (State (Map.Map Meta Int)) Int
testOps = GLF nextId nextId nextId nextId (nextId' 100) (nextId' 100)
checkGraphEquality :: (Graph g, Show b, Ord b, Monad m) => ([(Int, Meta)], [(Int, Int, b)]) -> g (FNode m Int) b -> Assertion
checkGraphEquality (nodes, edges) g
= do let (remainingNodes, nodeLookup, ass) = foldl checkNodeEquality (Map.fromList (map revPair nodes),Map.empty, return ()) (map (transformPair id deNode) $ labNodes g)
ass
assertBool (testName ++ " Test graph had nodes not found in the real graph: " ++ show remainingNodes ++ ", real graph: " ++ showGraph g) (Map.null remainingNodes)
edges' <- mapM (transformEdge nodeLookup) edges
assertEqual (testName ++ " Edge lists not equal") (sort $ edges') (sort $ labEdges g)
checkNodeEquality :: (Map.Map Meta Int, Map.Map Int Int, Assertion) -> (Node, (Meta, Int)) -> (Map.Map Meta Int, Map.Map Int Int, Assertion)
checkNodeEquality (metaToTestId, realToTestId, ass) (nodeId, (metaTag,metaSub))
= case Map.lookup (sub metaTag metaSub) metaToTestId of
Nothing -> (metaToTestId, realToTestId, ass >> assertFailure (testName ++ " Node with meta tag " ++ show (sub metaTag metaSub) ++ " not found in expected test data"))
Just testId -> let realToTestId' = Map.insert testId nodeId realToTestId in
let metaToTestId' = Map.delete (sub metaTag metaSub) metaToTestId in
(metaToTestId', realToTestId', ass)
transformEdge :: Show b => Map.Map Int Int -> (Int, Int, b) -> IO (Int, Int, b)
transformEdge nodeMap e@(start, end, label)
= case mergeMaybe (Map.lookup start nodeMap) (Map.lookup end nodeMap) of
Nothing -> do assertFailure ("Could not map test edge to real edge: " ++ show e)
return e
Just (start', end') -> return (start', end', label)
-- | A helper function for making simple A.Specification items.
someSpec :: Meta -> A.Specification
someSpec m = A.Specification m (simpleName $ show m) (A.DataType m A.Int)
testSeq :: Test
testSeq = TestLabel "testSeq" $ TestList
[
testSeq' 0 [(0,m1)] [] (A.Several m1 [])
,testSeq' 1 [(0,m2)] [] (A.OnlyP m1 sm2)
,testSeq' 2 [(0,m3)] [] (A.Several m1 [A.OnlyP m2 sm3])
,testSeq' 3 [(0,m3),(1,m5)] [(0,1,ESeq)] (A.Several m1 [A.OnlyP m2 sm3,A.OnlyP m4 sm5])
,testSeq' 4 [(0,m3),(1,m5),(2,m7)] [(0,1,ESeq),(1,2,ESeq)] (A.Several m1 [A.OnlyP m2 sm3,A.OnlyP m4 sm5,A.OnlyP m6 sm7])
,testSeq' 5 [(0,m3),(1,m5)] [(0,1,ESeq)] (A.Several m1 [A.Several m1 [A.OnlyP m2 sm3],A.Several m1 [A.OnlyP m4 sm5]])
,testSeq' 6 [(0,m3),(1,m5),(2,m7),(3,m9)] [(0,1,ESeq),(1,2,ESeq),(2,3,ESeq)]
(A.Several m1 [A.Several m1 [A.OnlyP m2 sm3,A.OnlyP m4 sm5,A.OnlyP m6 sm7], A.OnlyP m8 sm9])
,testSeq' 10 [(0,m1),(1,m4),(100,sub m1 100)] [(0,1,ESeq),(1,100,ESeq)] (A.Spec mU (someSpec m1) $ A.OnlyP m3 sm4)
,testSeq' 11
[(1,m1),(3,m4),(5,m5),(7,m7),(9,m10),(101,sub m1 100),(105,sub m5 100),(107,sub m7 100)]
[(1,3,ESeq),(3,101,ESeq),(101,5,ESeq),(5,7,ESeq),(7,9,ESeq),(9,107,ESeq),(107,105,ESeq)]
(A.Several m11 [A.Spec mU (someSpec m1) $ A.OnlyP m3 sm4,A.Spec mU (someSpec m5) $ A.Spec mU (someSpec m7) $ A.OnlyP m9 sm10])
]
where
testSeq' :: Int -> [(Int, Meta)] -> [(Int, Int, EdgeLabel)] -> A.Structured -> Test
testSeq' n a b s = testGraph ("testSeq " ++ show n) a b (A.Seq m0 s)
testPar :: Test
testPar = TestLabel "testPar" $ TestList
[
testPar' 0 [(0,m1)] [] (A.Several m1 [])
,testPar' 1 [(0,m2)] [] (A.OnlyP m1 sm2)
,testPar' 2 [(0,m3)] [] (A.Several m1 [A.OnlyP m2 sm3])
,testPar' 3 [(0,m1), (1, m3), (2, m5), (3,sub m1 1)]
[(0,1,EStartPar 0),(1,3,EEndPar 0), (0,2,EStartPar 0), (2,3,EEndPar 0)]
(A.Several m1 [A.OnlyP m2 sm3,A.OnlyP m4 sm5])
,testPar' 4 [(0,m1), (1,sub m1 1), (3,m3),(5,m5),(7,m7)]
[(0,3,EStartPar 0),(3,1,EEndPar 0),(0,5,EStartPar 0),(5,1,EEndPar 0),(0,7,EStartPar 0),(7,1,EEndPar 0)]
(A.Several m1 [A.OnlyP m2 sm3,A.OnlyP m4 sm5,A.OnlyP m6 sm7])
,testPar' 5 [(0,m1), (1, m3), (2, m5), (3,sub m1 1)]
[(0,1,EStartPar 0),(1,3,EEndPar 0), (0,2,EStartPar 0), (2,3,EEndPar 0)]
(A.Several m1 [A.Several m1 [A.OnlyP m2 sm3],A.Several m1 [A.OnlyP m4 sm5]])
,testPar' 6 [(0,m1), (1,sub m1 1),(3,m3),(5,m5),(7,m7),(9,m9),(10,m10),(11,sub m10 1)]
[(10,3,EStartPar 0),(10,5,EStartPar 0),(10,7,EStartPar 0),(3,11,EEndPar 0),(5,11,EEndPar 0),(7,11,EEndPar 0)
,(0,10,EStartPar 1),(11,1,EEndPar 1),(0,9,EStartPar 1),(9,1,EEndPar 1)]
(A.Several m1 [A.Several m10 [A.OnlyP m2 sm3,A.OnlyP m4 sm5,A.OnlyP m6 sm7], A.OnlyP m8 sm9])
,testPar' 10 [(0,m1), (1, m3), (2, m5), (3,sub m1 1), (6, m6),(106,sub m6 100)]
[(0,6,EStartPar 0),(6,1,ESeq),(1,106,ESeq),(106,3,EEndPar 0), (0,2,EStartPar 0), (2,3,EEndPar 0)]
(A.Several m1 [A.Spec mU (someSpec m6) $ A.OnlyP m2 sm3,A.OnlyP m4 sm5])
--TODO test nested pars
]
where
testPar' :: Int -> [(Int, Meta)] -> [(Int, Int, EdgeLabel)] -> A.Structured -> Test
testPar' n a b s = testGraph ("testPar " ++ show n) a b (A.Par m0 A.PlainPar s)
testWhile :: Test
testWhile = TestLabel "testWhile" $ TestList
[
testGraph "testWhile 0" [(0,m0), (1,m1)] [(0,1,ESeq), (1,0,ESeq)] (A.While mU (A.True m0) sm1)
,testGraph "testWhile 1" [(2,m2), (3, m3), (5, m5)] [(2,3,ESeq), (3,2,ESeq), (2,5,ESeq)]
(A.Seq m0 $ A.Several m1 [A.OnlyP m9 $ A.While mU (A.True m2) sm3,A.OnlyP m4 sm5])
,testGraph "testWhile 2" [(2,m2), (3, m3), (5, m5), (7, m7)] [(7,2,ESeq), (2,3,ESeq), (3,2,ESeq), (2,5,ESeq)]
(A.Seq m0 $ A.Several m1 [A.OnlyP m6 sm7,A.OnlyP m9 $ A.While mU (A.True m2) sm3,A.OnlyP m4 sm5])
,testGraph "testWhile 3" [(2,m2), (3, m3), (5, m5), (7, m7), (9, m9)] [(7,2,ESeq), (2,3,ESeq), (3,9,ESeq), (9,2,ESeq), (2,5,ESeq)]
(A.Seq m0 $ A.Several m1 [A.OnlyP m6 sm7,A.OnlyP mU $ A.While mU (A.True m2) $ A.Seq mU $ A.Several mU [A.OnlyP mU sm3,A.OnlyP mU sm9],A.OnlyP m4 sm5])
]
testCase :: Test
testCase = TestLabel "testCase" $ TestList
[
testGraph "testCase 0" [(0,m10),(1,m0),(2,m3)] [(0,2,ESeq),(2,1,ESeq)] (A.Case m0 (A.True m10) $ cases m1 [A.Else m2 sm3])
,testGraph "testCase 1"
[(0,m10),(1,m0),(2,m2),(3,m3)]
[(0,2,ESeq),(2,3,ESeq),(3,1,ESeq)]
(A.Case m0 (A.True m10) $ cases m1 [A.Option m2 [A.True mU] sm3])
,testGraph "testCase 2"
[(0,m10),(1,m0),(2,m2),(3,m3),(4,m4),(5,m5)]
[(0,2,ESeq),(2,3,ESeq),(3,1,ESeq), (0,4,ESeq),(4,5,ESeq),(5,1,ESeq)]
(A.Case m0 (A.True m10) $ cases m1 [A.Option m2 [A.True mU] sm3, A.Option m4 [A.True mU] sm5])
--TODO test case statements that have specs
]
where
cases :: Meta -> [A.Option] -> A.Structured
cases m = (A.Several m) . (map (A.OnlyO mU))
testIf :: Test
testIf = TestLabel "testIf" $ TestList
[
testGraph "testIf 0" [(0,m0), (1,sub m0 1), (2,m2), (3,m3)] [(0,2,ESeq),(2,3,ESeq),(3,1,ESeq)]
(A.If m0 $ ifs mU [(A.True m2, sm3)])
,testGraph "testIf 1" [(0,m0), (1,sub m0 1), (2,m2), (3,m3), (4,m4), (5, m5)]
[(0,2,ESeq),(2,3,ESeq),(3,1,ESeq), (2,4,ESeq),(4,5,ESeq),(5,1,ESeq)]
(A.If m0 $ ifs mU [(A.True m2, sm3), (A.True m4, sm5)])
,testGraph "testIf 2" [(0,m0), (1,sub m0 1), (2,m2), (3,m3), (4,m4), (5, m5), (6, m6), (7, m7)]
[(0,2,ESeq),(2,3,ESeq),(3,1,ESeq), (2,4,ESeq),(4,5,ESeq),(5,1,ESeq), (4,6,ESeq),(6,7,ESeq),(7,1,ESeq)]
(A.If m0 $ ifs mU [(A.True m2, sm3), (A.True m4, sm5), (A.True m6, sm7)])
]
where
ifs :: Meta -> [(A.Expression, A.Process)] -> A.Structured
ifs m = (A.Several m) . (map (\(e,p) -> A.OnlyC mU $ A.Choice (findMeta e) e p))
testProcFuncSpec :: Test
testProcFuncSpec = TestLabel "testProcFuncSpec" $ TestList
[
-- Single spec of process (with SKIP body):
testGraph' "testProcFuncSpec 0" [(0, m0),(1,m1),(2,sub m1 100),(3,m3)] [(1,3,ESeq), (3,2,ESeq)]
(A.Spec mU (A.Specification m1 undefined $ A.Proc mU undefined undefined sm0) $ A.Several m3 [])
-- Single spec of process (with body with SEQ SKIP SKIP):
,testGraph' "testProcFuncSpec 1" [(0, m3),(1,m6),(2,sub m6 100),(3,m8),(4,m5)] ([(1,3,ESeq), (3,2,ESeq)] ++ [(0,4,ESeq)])
(A.Spec mU (A.Specification m6 undefined $ A.Proc mU undefined undefined $
A.Seq m0 $ A.Several m1 [A.OnlyP m2 sm3,A.OnlyP m4 sm5]
) $ A.Several m8 [])
-- Nested spec of process (with bodies with SEQ SKIP SKIP):
,testGraph' "testProcFuncSpec 2" [(0,m6),(1,sub m6 100),(2,m8),(3,m2),(4,m3),(5,m4),(6,m5),(7,m7),(8,sub m7 100)]
([(0,7,ESeq), (7,2,ESeq), (2,8,ESeq), (8,1,ESeq)] ++ [(3,4,ESeq)] ++ [(5,6,ESeq)])
(A.Spec mU (A.Specification m6 undefined $ A.Proc mU undefined undefined $
A.Seq mU $ A.Several mU [A.OnlyP mU sm2,A.OnlyP mU sm3]
) $
A.Spec mU (A.Specification m7 undefined $ A.Proc mU undefined undefined $
A.Seq mU $ A.Several mU [A.OnlyP mU sm4,A.OnlyP mU sm5]
)
$ A.Several m8 [])
]
--TODO test replicated seq/par
--TODO test alts
--TODO occam stuff:
--TODO test input-case statements
--TODO test replicated ifs
-- The idea here is that each time we generate an interesting node,
-- we want to generate its replaced version too. Then combine these as
-- we go back up the tree to form a set of all possible trees (which is like the powerset of possible replacements, I think).
-- We also want to ensure that the meta tags are unique (to label replacements), and I don't think
-- QuickCheck easily supports that.
-- So how to do this, given that QuickCheck expects something of type Gen a to come
-- back out of Arbitrary? We could generate Gen [a], with the understanding that the head
-- is the source, all the others are possible replacements. But then we need to label the replacements,
-- which means we'd want Gen [([Meta],a)]
-- However, in order to ensure we make unique meta tags, we have to add the StateT Int wrapper:
-- | A newtype based on Int, to avoid confusion with other uses of Int.
newtype Id = Id Int
-- | Turns the Id newtype back into a plain Int
fromId :: Id -> Int
fromId (Id n) = n
-- | Similar to makeMeta, but takes an Id as its argument.
makeMeta' :: Id -> Meta
makeMeta' = makeMeta . fromId
-- | The monad type for generating ASTs. The StateT wrapped is needed for making
-- the meta tags unique, and the reason for the strange generation type is explained in
-- earlier comments.
type GenL a = StateT Id Gen [([Meta], a)]
-- | A helper function for making a simple meta-tag replacement operation.
replaceMeta :: Meta -> Meta
replaceMeta m = sub m 8
-- | Given a meta tag, returns the standard and replaced versions of it.
genMeta :: Meta -> GenL Meta
genMeta m = return [([],m),([m],replaceMeta m)]
-- Helper functions for dealing with the AST:
-- | The genElemN functions take an AST constructor (that has Meta as its first item)
-- then the appropriate Meta tag and optional further arguments, and returns the standard
-- and replaced combinations across all of them using the combN functions.
genElem1 :: (Meta -> b) -> Meta -> GenL b
genElem1 f m = comb1 f (genMeta m)
genElem2 :: (Meta -> a0 -> b) -> Meta -> GenL a0 -> GenL b
genElem2 f m = comb2 f (genMeta m)
genElem3 :: (Meta -> a0 -> a1 -> b) -> Meta -> GenL a0 -> GenL a1 -> GenL b
genElem3 f m = comb3 f (genMeta m)
-- | A helper function for turning any item that can't be replaced into a GenL form (esp.
-- for use as a parameter of genElemN).
comb0 :: forall a. a -> GenL a
comb0 x = return [([],x)]
-- | The combN functions (N >= 1) take a constructor, then the appropriate number of GenL
-- items, and works out all possible combinations of replacements and so on. The number
-- of replacements can get very large (2^K, where K is the number of GenL parameters that
-- can be replaced).
comb1 :: forall a0 b. (a0 -> b) -> GenL a0 -> GenL b
comb1 func list0 = list0 >>* map process1
where
process1 :: ([Meta], a0) -> ([Meta],b)
process1 = transformPair id func
comb2 :: forall a0 a1 b. (a0 -> a1 -> b) -> GenL a0 -> GenL a1 -> GenL b
comb2 func list0 list1 = (liftM2 (,)) list0 list1 >>* product2 >>* map (uncurry process2)
where
process2 :: ([Meta], a0) -> ([Meta], a1) -> ([Meta],b)
process2 (keys0, val0) (keys1, val1) = (keys0++keys1, func val0 val1)
comb3 :: forall a0 a1 a2 b. (a0 -> a1 -> a2 -> b) -> GenL a0 -> GenL a1 -> GenL a2 -> GenL b
comb3 func list0 list1 list2 = (liftM3 (,,)) list0 list1 list2 >>* product3 >>* map (uncurry3 process3)
where
process3 :: ([Meta], a0) -> ([Meta], a1) -> ([Meta],a2) -> ([Meta],b)
process3 (keys0, val0) (keys1, val1) (keys2, val2) = (keys0++keys1++keys2, func val0 val1 val2)
-- | Wrapper for Quickcheck.
-- In order to stop conflict with Quickcheck's in-built rules for things such as pairs
-- (which do not allow overlapping instances), we have to wrap such types ourself.
newtype QC a = QC a deriving (Eq, Show)
-- | We don't allow size zero for generating trees.
-- So we cheat by changing the size to 1, if it is 0.
enforceSize1 :: Gen a -> Gen a
enforceSize1 f = sized $ \n -> if n == 0 then resize 1 f else f
-- | An instance of Arbitrary for A.Structured that wraps the "genStructured" function.
instance Arbitrary (QC (A.Structured, Map.Map [Meta] A.Structured)) where
arbitrary = enforceSize1 $ sized $ \n -> evalStateT (genStructured justP n) (Id 0) >>* findEmpty >>* QC
where
-- Copies the value for the empty-list key into the first element of the tuple:
findEmpty :: [([Meta], a)] -> (a, Map.Map [Meta] a)
findEmpty xs = (fromJust $ Map.lookup [] m, m)
where m = Map.fromList xs
-- coarbitrary is for defined "twiddle" functions over data generated by arbitrary.
-- For example, we could have the twiddle functions changing an expression
-- in the tree. I don't think this would be of use right now, given what we're testing
-- | A function inside a StateT monad that returns the next unique Id.
nextIdT :: Monad m => StateT Id m Id
nextIdT = modify' incId
where
incId :: Id -> Id
incId (Id n) = (Id $ n+1)
-- | A function similar to the QuickCheck oneof function, that works on GenL stuff rather than Gen.
oneofL :: [GenL a] -> GenL a
oneofL gs = do i <- lift $ choose (0,length gs-1)
gs !! i
-- | A function that takes in a list of sized items. The first thing in the pair is the minimum size
-- of an item produced, and the second is a function that maps a size into a GenL. One of these
-- functions is chosen and returned, with the obvious constraint that only generators whose
-- minimum size is satisfied will be called.
--
-- TODO at the moment I think I've generally estimated size. Size should refer to the number of items
-- that can potentially be replaced, but I'm not sure that is always strictly kept to. Still, it's
-- a close enough approximation.
oneofLS :: [(Int, Int -> GenL a)] -> Int -> GenL a
oneofLS fs n = oneofL $ applyAll n (filterFuncs n fs)
where
filterFuncs :: Int -> [(Int, Int -> GenL a)] -> [Int -> GenL a]
filterFuncs sz = map snd . filter ((>=) sz . fst)
-- | A function that takes a "find" parameter, a "replace" parameter, and returns a monadic function
-- (for convenience) that performs the check\/replacement.
replaceM :: (Eq a, Monad m) => a -> a -> (a -> m a)
replaceM find replace x | find == x = return replace
| otherwise = return x
-- | A little helper function for generating random lists of numbers. Given a total,
-- this generates a list of random numbers that sum to that total. The function is of course recursive,
-- and each number is between 1 and the remaining total (evenly distributed). This does mean
-- that the earlier items in the list will tend to be larger than the later items, and I think
-- there is a high chance of the last item in the list being 1. But hopefully for our tests this
-- isn't major limitation.
genNumsToTotal :: Int -> Gen [Int]
genNumsToTotal 0 = return []
genNumsToTotal n = do ch <- choose (1,n)
chs <- genNumsToTotal (n-ch)
return (ch:chs)
-- | A function that takes a generator for an item, and generates a list of those,
-- dividing up the size at random. The list will be length log_2(N) on average, I think.
genList :: (Int -> GenL a) -> Int -> GenL [a]
genList _ 0 = return [([],[])]
genList f n = (lift $ genNumsToTotal n) >>= mapM f >>= foldList
where
foldList :: [[([Meta], a)]] -> StateT Id Gen [([Meta], [a])]
foldList [g] = comb1 singleton (return g)
foldList gs = return $ foldr foldX [] gs
foldX :: [([Meta], a)] -> [([Meta], [a])] -> [([Meta], [a])]
foldX xs [] = map (uncurry mix) (zip xs $ repeat ([],[]))
foldX xs ys = map (uncurry mix) (product2 (xs,ys))
mix :: ([Meta], a) -> ([Meta], [a]) -> ([Meta], [a])
mix (ms0,x) (ms1,xs) = (ms0++ms1,x:xs)
-- Helper functions for subtraction.
sub1 :: Int -> Int
sub1 x = x-1
sub2 :: Int -> Int
sub2 x = x-2
-- Be careful with the test generators; there should always be an option with value 1 (or 0)
-- in every list. Recursion should always decrease the test size, and you
-- should take the recursion into account in the required size (generally, recursive
-- generators will have value 2 at least). If you cannot have something of size 1 in the list,
-- (such as for A.Alternative) you need to take account of this in its parent items, and bump
-- up the required size for them accordingly.
-- | A type that indicates which of the OnlyX items are allowed in a given A.Structured.
-- This is to avoid generating, for example, A.If with A.OnlyA things inside them.
data OnlyAllowed = OA {
onlyP :: Bool
,onlyO :: Bool
,onlyC :: Bool
,onlyA :: Bool
}
nothing = OA False False False False
justP = nothing {onlyP = True}
justO = nothing {onlyO = True}
justC = nothing {onlyC = True}
justA = nothing {onlyA = True}
-- | Slightly cheaty way of easily masking out items:
cond :: Bool -> (Int, a) -> (Int, a)
cond True = id
cond False = const (1000000, undefined)
-- | Generates a simple expression (A.True m).
genExpression :: GenL A.Expression
genExpression = nextIdT >>* makeMeta' >>= genElem1 A.True
-- | Generates a simple, empty, expression list.
genExpressionList :: GenL A.ExpressionList
genExpressionList = nextIdT >>* makeMeta' >>= (flip $ genElem2 A.ExpressionList) (comb0 [])
-- | Generates an A.Alternative. Currently always A.AlternativeSkip.
genAlternative :: Int -> GenL A.Alternative
genAlternative n = nextIdT >>* makeMeta' >>= \m -> (flip oneofLS) n
[
(3, genElem3 A.AlternativeSkip m genExpression . genProcess . sub2)
]
-- | Generates a A.Specification.
genSpecification :: GenL A.Specification
genSpecification = nextIdT >>* makeMeta' >>= \m -> genElem3 A.Specification m (comb0 $ simpleName "x") genSpecType
where
genSpecType :: GenL A.SpecType
genSpecType = nextIdT >>* makeMeta' >>= \m -> oneofL
[
genElem3 A.Declaration m (comb0 A.Int) (comb0 Nothing)
,genElem3 A.Declaration m (comb0 A.Int) (comb1 Just genExpression)
,genElem2 (\m e -> A.IsExpr m A.ValAbbrev A.Int e) m genExpression
--TODO proc and function declaration
]
-- | Generates a A.Structured, obeying the given OnlyAllowed structure.
genStructured :: OnlyAllowed -> Int -> GenL A.Structured
genStructured allowed n = nextIdT >>* makeMeta' >>= \m -> (flip oneofLS) n
[
cond (onlyP allowed) (2,genElem2 A.OnlyP m . genProcess . sub1 )
,cond (onlyO allowed) (2,comb1 (A.OnlyO emptyMeta . A.Else emptyMeta) . genProcess . sub1 )
,cond (onlyC allowed) (3,comb2 (\e p -> A.OnlyC emptyMeta $ A.Choice emptyMeta e p) genExpression . genProcess . sub2)
,cond (onlyA allowed) (4,genElem2 A.OnlyA m . genAlternative . sub1 )
-- Specs currently don't work with Case statements TODO
,cond (not $ onlyO allowed) (3,genElem3 A.Spec m genSpecification . genStructured allowed . sub2 )
,(1,genElem2 A.Several m . genList (genStructured allowed) . sub1)
]
-- | Generates a A.Process.
genProcess :: Int -> GenL A.Process
genProcess n = nextIdT >>* makeMeta' >>= \m -> (flip oneofLS) n
[
(1,const $ genElem1 A.Skip m)
,(1,const $ genElem1 A.Stop m)
,(2,genElem2 A.Seq m . genStructured justP . sub1)
,(2,genElem3 A.Par m (comb0 A.PlainPar) . genStructured justP . sub1)
,(3,genElem3 A.While m genExpression . genProcess . sub2)
,(2,genElem2 A.If m . genStructured justC . sub1)
,(3,genElem3 A.Case m genExpression . genStructured justO . sub2)
,(2,const $ genElem3 A.Assign m (comb0 [variable "x"]) genExpressionList)
,(1,const $ genElem2 A.GetTime m (comb0 $ variable "x"))
,(1,const $ genElem3 A.Wait m (comb0 A.WaitFor) genExpression)
,(2,genElem3 A.Alt m (comb0 True) . genStructured justA . sub1)
]
-- | Generates a flow-graph from the given AST.
-- TODO put this in proper error monad
genGraph :: A.Structured -> FlowGraph Identity ()
genGraph s = either (\e -> error $ "QuickCheck graph did not build properly: " ++ e ++ ", from: " ++ pshow s) fst $ runIdentity $ buildFlowGraph funcs s
where
empty :: a -> Identity ()
empty = const (return ())
funcs = GLF empty empty empty empty empty empty
-- | Given a flow-graph, it returns a list of all the identity alteration functions,
-- for each node. Applying any, many or all of these functions to the source AST
-- should leave it unchanged.
pickFuncId :: Monad m => FlowGraph m () -> [A.Structured -> m A.Structured]
pickFuncId g = map (applyFunc . getFunc) (labNodes g)
where
getFunc (_,Node (_,_,f)) = f
applyFunc (AlterProcess f) = f return
applyFunc (AlterExpression f) = f return
applyFunc (AlterExpressionList f) = f return
applyFunc (AlterSpec f) = f return
applyFunc (AlterNothing) = return
-- | Given a flow-graph, it returns a list of the meta-tag replacement alteration functions,
-- for each meta-tag (i.e. each node).
pickFuncRep :: Monad m => FlowGraph m () -> Map.Map Meta (A.Structured -> m A.Structured)
pickFuncRep gr = Map.fromList $ map (helpApplyFunc . getMetaFunc) (labNodes gr)
where
getMetaFunc (_,Node (m,_,f)) = (m,f)
helpApplyFunc (m,f) = (m, applyFunc (m,f))
applyFunc (m,AlterProcess f) = f (g m)
applyFunc (m,AlterExpression f) = f (g m)
applyFunc (m,AlterExpressionList f) = f (g m)
applyFunc (m,AlterSpec f) = f (g m)
applyFunc (m,AlterNothing) = g m
g m = everywhereM (mkM $ replaceM m (replaceMeta m))
-- | It is important to have these functions in the right ratio. The number of possible trees is
-- 2^N, where N is the test size. Therefore I suggest keeping N <= 10 as a sensible limit.
-- Hence, if there are 1000 tests, we divide the test number by 100 to get the test size.
configForSize :: Int -> Config
configForSize n = defaultConfig { configMaxTest = n, configSize = \x -> x `div` scale }
where
scale = n `div` 10
deepCheck :: Testable a => a -> QuickCheckTest
deepCheck test level = (flip check) test $ configForSize $
case level of
QC_Low -> 100
QC_Medium -> 1000
QC_High -> 5000
QC_Extensive -> 10000
testModify :: [QuickCheckTest]
testModify =
[
deepCheck prop_Id
,deepCheck prop_Rep
,deepCheck prop_gennums
]
where
-- | Checks that applying any set (from the powerset of identity functions) of identity functions
-- does not change the AST.
prop_Id :: QC (A.Structured, Map.Map [Meta] A.Structured) -> Result
prop_Id (QC (g,_)) = collectAll $ (flip map) (map (foldFuncsM) $ powerset $ pickFuncId $ genGraph g) $ \f -> runIdentity (f g) *==* g
-- | Checks that applying any set (from the powerset of replacement functions) of replacement functions
-- produces the expected result.
prop_Rep :: QC (A.Structured, Map.Map [Meta] A.Structured) -> Result
prop_Rep (QC (g,rest)) = collectAll $ (flip map) (helper $ pickFuncRep $ genGraph g) $
\(funcs,ms) -> Just (runIdentity (applyMetas ms funcs g)) *==* Map.lookup ms rest
-- | This tests our genNumsToTotal function, which is itself a test generator; nasty!
prop_gennums :: Int -> Result
prop_gennums n = generate 0 (mkStdGen 0) (genNumsToTotal n >>* sum) *==* n
-- | Repeatedly pairs the map with each element of the powerset of its keys
helper :: Monad m => Map.Map Meta (A.Structured -> m A.Structured) -> [(Map.Map Meta (A.Structured -> m A.Structured), [Meta])]
helper fs = zip (repeat fs) (powerset $ Map.keys fs)
-- | Applies the functions associated with the given meta tags
applyMetas :: Monad m => [Meta] -> Map.Map Meta (A.Structured -> m A.Structured) -> (A.Structured -> m A.Structured)
applyMetas ms funcs = foldFuncsM $ concatMap (\m -> Map.lookup m funcs) ms
-- | Collects multiple test results together, using the first failure as its result
-- (if there is a failure; otherwise the result will be a pass).
collectAll :: [Result] -> Result
collectAll = foldl collectAll'(Result {ok = Just True, arguments = [], stamp = []})
where
-- Only keep the first failure:
collectAll' :: Result -> Result -> Result
collectAll' r0 r1 | ok r0 == Just False = r0
| otherwise = r1
-- | Returns the list of tests:
qcTests :: (Test, [QuickCheckTest])
qcTests = (TestLabel "FlowGraphTest" $ TestList
[
testCase
,testIf
,testPar
,testProcFuncSpec
,testSeq
,testWhile
]
,testModify)