-- | -- Module : Test.FitSpec.Engine -- Copyright : (c) 2015-2017 Rudy Matela -- License : 3-Clause BSD (see the file LICENSE) -- Maintainer : Rudy Matela -- -- FitSpec: refining property-sets for functional testing -- -- This is the main engine, besides "Test.FitSpec.Mutable". module Test.FitSpec.Engine ( property , Property , getResults , getResultsExtra , getResultsExtraTimeout , Result (..) , Results , propertiesNTests , propertiesTestsExhausted , propertiesToMap , propertiesHold , propertiesCE , minimal , complete , reduceImplications , filterNonCanon , Conjecture (..) , conjectures ) where import Test.LeanCheck.Error import Test.FitSpec.Utils import Data.Maybe (catMaybes, listToMaybe) import Data.List ((\\),union,transpose) import Test.FitSpec.Mutable -- | An encoded representation of a property suitable for use by FitSpec. -- -- Each list of strings is a printable representation of one possible choice of -- argument values for the property. Each boolean indicate whether the -- property holds for this choice. type Property = [([String],Bool)] type Properties = [Property] -- | Given a 'Testable' type (as defined by "Test.LeanCheck"), returns a 'Property'. -- -- This function should be used on every property to create a property list to -- be passed to 'report', 'reportWith', 'mainDefault' or 'mainWith'. -- -- > property $ \x y -> x + y < y + (x::Int) property :: Testable a => a -> Property property = results propertyHolds :: Int -> Property -> Bool propertyHolds n = all snd . take n propertyCE :: Int -> Property -> Maybe String propertyCE n = listToMaybe . map (unwords . fst) . filter (not . snd) . take n propertiesToMap :: [Property] -> Int -> [Bool] propertiesToMap ps n = map (propertyHolds n) ps propertiesHold :: Int -> [Property] -> Bool propertiesHold n = all (propertyHolds n) propertiesCE :: Int -> [Property] -> Maybe String propertiesCE n = listToMaybe . catMaybes . zipWith (\n -> fmap ((show n ++ ": ") ++)) [1..] . map (propertyCE n) propertiesNTests :: Int -> [Property] -> [Int] propertiesNTests n = map (length . take n) propertiesTestsExhausted :: Int -> [Property] -> [Bool] propertiesTestsExhausted n = map (<= n) . propertiesNTests (n+1) filterNonCanon :: [Result a] -> [Result a] filterNonCanon [] = [] filterNonCanon (r:rs) = (r:) . filterNonCanon . filter (not . null . sets) . map (updateSets removeNonCanon) $ rs where removeNonCanon = filter (not . (\p' -> (p' `contains`) `any` tail (sets r))) updateSets f r = r { sets = f (sets r) } reduceImplications :: [Result a] -> [Result a] reduceImplications [] = [] reduceImplications (r:rs) = r : map (r `reduce`) (reduceImplications rs) where r `reduce` r' = if or [s `contained` s' | s <- sets r, s' <- sets r'] then r' { implied = implied r' \\ implied r } else r' -- | A line of result for a single equivalence class of properties -- with the exact same surviving mutants. data Result a = Result { sets :: [[Int]] -- ^ property-sets in the equivalence class , implied :: [Int] -- ^ properties implied by this class , survivors :: [a] -- ^ list of surviving mutants , smallestSurvivor :: Maybe a -- ^ smallest surviving mutant, if any , nSurvivors :: Int -- ^ number of surviving mutants , nKilled :: Int -- ^ number of killed mutants , totalMutants :: Int -- ^ total number of mutants generated and tested , score :: Int -- ^ percentage of killed mutants, 0-100 , maxTests :: Int -- ^ Requested number of tests (same for all rs.) , mutantsExhausted :: Bool -- ^ mutants were exhausted } type Results a = [Result a] -- | Return minimality and completeness results. See 'report'. getResults :: (Mutable a) => a -> (a -> [Property]) -> Int -> Int -> Results a getResults = getResultsExtra [] getResultsExtra :: (Mutable a) => [a] -> a -> (a -> [Property]) -> Int -> Int -> Results a getResultsExtra ems f ps nms nts = map (uncurry $ processRawResult mex nts) $ getRawResults is pmap ms where is = [1..(length $ ps f)] pmap f = propertiesToMap (ps f) nts ms' = take (nms+1) (tail $ mutants f) mex = length ms' <= nms ms = take nms ms' ++ ems getResultsExtraTimeout :: (Mutable a) => Int -> [a] -> a -> (a -> [Property]) -> Int -> Int -> IO (Results a) getResultsExtraTimeout 0 ems f ps m n = return $ getResultsExtra ems f ps m n getResultsExtraTimeout t ems f ps nm0 nt0 = lastTimeout t resultss where resultss = map fst $ takeWhileIncreasingOn ((totalMutants . head) *** id) [ (getResultsExtra ems f ps nm nt, propertiesNTests nt $ ps f) | (nm,nt) <- iterate (incHalf *** incHalf) (nm0,nt0) ] incHalf x = x + x `div` 2 processRawResult :: Bool -> Int -> [[Int]] -> [(a,Bool)] -> Result a processRawResult mex nt iss mhs = Result { sets = relevantPropertySets iss , implied = relevantImplications iss , survivors = ms , smallestSurvivor = listToMaybe ms , nSurvivors = ns , nKilled = nk , totalMutants = nm , score = nk*100 `div` nm , maxTests = nt , mutantsExhausted = mex } where ms = [m | (m,h) <- mhs, h] nm = length mhs ns = length ms nk = nm - ns minimal :: Results a -> Bool minimal (r:_) = null (implied r) && length (sets r) == 1 complete :: Results a -> Bool complete (r:_) = nSurvivors r == 0 relevantPropertySets :: Eq i => [[i]] -> [[i]] relevantPropertySets = filterU (not ... contained) . sortOn length relevantImplications :: Eq i => [[i]] -> [i] relevantImplications iss = foldr union [] iss \\ foldr union [] (relevantPropertySets iss) -- | Returns a description of property sets, grouping the ones that had the -- same surviving mutants. The resulting list is ordered starting with the -- least surviving mutants to the most surviving mutants. -- -- Arguments: -- -- * @is@: list of property ids (@length is == length (pmap x)@) -- -- * @pmap@: a property map -- -- * @ms@: list of mutants to apply to the property map -- -- Return a list of tuples containing: -- -- * a list of property sets -- * a list of mutants paired with booleans indicating whether each survived getRawResults :: [i] -> (a -> [Bool]) -> [a] -> [([[i]],[(a,Bool)])] getRawResults is ps ms = (id *** (zip ms)) `map` getRawResults' is ps ms -- | Returns a description of property sets, grouping the ones that had the -- same surviving mutants. The resulting list is ordered starting with the -- least surviving mutants to the most surviving mutants. -- -- Arguments: -- -- * @is@: list of property ids (@length is == length (pmap x)@) -- -- * @pmap@: a property map -- -- * @ms@: list of mutants to apply to the property map -- -- Return a list of tuples containing: -- -- * a list of property sets -- * a boolean list indicating whether a given mutant survived getRawResults' :: [i] -> (a -> [Bool]) -> [a] -> [([[i]],[Bool])] getRawResults' is pmap = sortOn (count id . snd) . sortAndGroupFstBySnd . zip (subsets is) . transpose . map (compositions . pmap) -- | @nSurv props fs@ returns the number of values that match -- compositions of properties on the property map. -- -- * @props@ should be a function from a value to a list of properties that -- match that value (in the case of functions, functions that "survive" those -- properties). -- -- * @fs@ is a list of values to be mapped over by @props@ -- -- > length (nSurvivors props fs) == 2 ^ (length (props fs)) -- -- This function is otherwise unused in this file. It is just a simpler -- version of 'pssurv' to serve as documentation. -- -- It is also not exported! nSurv :: (a -> [Bool]) -> [a] -> [Int] nSurv props = map (count id) . transpose . map (compositions . props) data Conjecture = Conjecture { isEq :: Bool , isIm :: Bool , cleft :: [Int] , cright :: [Int] , cscore :: Int , cnKilled :: Int , cnSurvivors :: Int } deriving Show conjectures :: [Result a] -> [Conjecture] conjectures = concatMap conjectures1 . sortOn (abs . (50-) . score) -- closer to 50 the better! . reduceImplications . filterNonCanon . reverse conjectures1 :: Result a -> [Conjecture] conjectures1 r = [ p `eq` p' | p' <- ps ] ++ [ p `im` i | (not.null) i ] where (p:ps) = sets r i = implied r eq = conj True im = conj False conj isE p p' = Conjecture { isEq = isE , isIm = not isE , cleft = p , cright = p' , cscore = score r , cnKilled = nKilled r , cnSurvivors = nSurvivors r } -- TODO: improve implication score -- implication score can be improved by -- by separating each implication on its own: -- [4] ==> [2,3] -- become -- [4] ==> [2] -- [4] ==> [3] -- Then evaluating percentage of occurences of True ==> True and other cases