packages feed

KiCS-0.8.8: src/lib/Curry/Module/EasyCheck.curry

--- Lightweight, automated, specification-based testing of Curry programs.
--- Currently, EasyCheck is only supported by the Kiel Curry System (KiCS).
--- See 
--- <a href="http://www-ps.informatik.uni-kiel.de/currywiki/tools/easycheck"
--- >here</a> for a tutorial introduction to EasyCheck.
---
--- @author Jan Christiansen and Sebastian Fischer
---
--- @version January 2008
---
module EasyCheck (

  -- test specification
  Prop, (==>), for, forValues,

  test, is, isAlways, isEventually, prop, uniquely, always, eventually, 
  failing, successful, deterministic, (-=-), (#), (<~>), (~>), (<~),

  isSameSet, isSubsetOf,

  -- test annotations
  label, trivial, classify, collect, collectAs, 

  -- test functions
  easyCheck, easyCheck1, easyCheck2, easyCheck3, easyCheck4, easyCheck5,
  verboseCheck, verboseCheck1, verboseCheck2, verboseCheck3, verboseCheck4,
  verboseCheck5,

  -- useful for other tools accessing EasyCheck
  valuesOf, Result(..), result,

  -- useful auxiliary functions
  diagonal

  ) where

import List             ( nub, group, intersperse, (\\) )
import Sort             ( leqList, leqString, mergeSort )
import Meta             ( searchTree )
import Integer          ( abs )
import Read             ( readNat )
import RandomExternal   ( split, nextInt )

infix  4 `isSameSet`, `isSubsetOf`
infix  1 `is`, `isAlways`, `isEventually`, -=-, #, <~>, ~>, <~, `trivial`
infixr 0 ==>


data Test = Test Result [String] [String]

data Result = Undef | Ok | Falsified [String] | Ambigious [Bool] [String]

type Prop = [Test]

notest :: Test
notest = Test Undef [] []

result :: Test -> Result
result (Test r _ _) = r

setResult :: Result -> Test -> Test
setResult res (Test _ s a) = Test res a s

args, stamp :: Test -> [String]
args  (Test _ a _) = a
stamp (Test _ _ s) = s

updArgs, updStamp :: ([String] -> [String]) -> Test -> Test
updArgs  upd (Test r a s) = Test r (upd a) s
updStamp upd (Test r a s) = Test r a (upd s)

-- Test Specification

--- Specify a property as predicate on the list of nondeterministic results.
test :: a -> ([a] -> Bool) -> Prop
test x f = [setResult res notest]
 where
  xs  = valuesOf x
  res = case valuesOf (f xs) of
          [True]  -> Ok
          [False] -> Falsified (map show xs)
          bs      -> Ambigious bs (map show xs)

--- Specify a property as predicate that must hold for
--- the unique result, all results or any result.
is, isAlways, isEventually :: a -> (a -> Bool) -> Prop
is x f = test x (\xs -> case xs of [y] -> f y; _ -> False)
isAlways x  = test x . all
isEventually x = test x . any

--- List boolean value to property.
prop, uniquely, always, eventually :: Bool -> Prop
prop       = uniquely
uniquely   = (`is`id)
always     = (`isAlways`id)
eventually = (`isEventually`id)

--- Properties on the number of results.
failing, successful, deterministic :: _ -> Prop
failing x = test x null
successful x = test x (not . null)
deterministic x = x `is` const True

--- Specify the number of different results.
(#) :: _ -> Int -> Prop
x # n = test x ((n==) . length . nub)

--- Deterministic equality.
--- False if any argument is non-deterministic or fails.
(-=-) :: a -> a -> Prop
x -=- y = (x,y) `is` uncurry (==)

--- Three kinds of "Nondeterministic equality".
(<~>), (~>), (<~) :: a -> a -> Prop
x <~> y = test x (isSameSet (valuesOf y))
x  ~> y = test x (isSubsetOf (valuesOf y))
x <~  y = test x (`isSubsetOf` (valuesOf y))

isSameSet, isSubsetOf, subset :: [a] -> [a] -> Bool
xs `isSameSet` ys = xs' `subset` ys' && ys' `subset` xs'
 where xs' = nub xs; ys' = nub ys
xs `isSubsetOf` ys = nub xs `subset` ys
xs `subset` ys = null (xs\\ys)

--- Implication operator to reject invalid test input.
(==>) :: Bool -> Prop -> Prop
True  ==> p = p
False ==> _ = [notest]

forAll :: (b -> Prop) -> a -> (a -> b) -> Prop
forAll c x f = forAllValues c (valuesOf x) f

forAllValues :: (b -> Prop) -> [a] -> (a -> b) -> Prop
forAllValues c xs f
  = diagonal [[ updArgs (show y:) t | t <- c (f y) ] | y <- xs ]

--- Specify a custom input generator for a property.
for :: a -> (a -> Prop) -> Prop
for = forAll id

forValues :: [a] -> (a -> Prop) -> Prop
forValues = forAllValues id

-- Test Annotations

--- Add a label to a property that is shown in the test summary.
label :: String -> Prop -> Prop
label = map . updStamp . (:)

--- Add a label to a property conditionally.
classify :: Bool -> String -> Prop -> Prop
classify True  name = label name
classify False _    = id

--- Add the label 'trivial' to a property conditionally.
trivial :: Bool -> Prop -> Prop
trivial = (`classify`"trivial")

--- Add a string representation of given value as a label to a property. 
collect :: a -> Prop -> Prop
collect = label . show

--- Like 'collect' but with discriminating prefix.
collectAs :: String -> a -> Prop -> Prop
collectAs name = label . ((name++": ")++) . show

-- Test Functions

data Config = Config Int Int (Int -> [String] -> String)

maxTest, maxFail :: Config -> Int
maxTest (Config n _ _) = n
maxFail (Config _ n _) = n

every :: Config -> Int -> [String] -> String
every (Config _ _ f) = f

setEvery :: (Int -> [String] -> String) -> Config -> Config
setEvery f (Config n m _) = Config n m f

easy :: Config
easy = Config 100 1000
        (\n _ -> let s = ' ':show (n+1) in s ++ [ chr 8 | _ <- s ])

verbose :: Config
verbose = setEvery (\n xs -> show n ++ ":\n" ++ unlines xs) easy

--- Test a property and print a summary.
easyCheck, verboseCheck :: Prop -> IO ()
easyCheck    = check easy
verboseCheck = check verbose

suc :: (a -> Prop) -> (b -> a) -> Prop
suc n = forAll n unknown

easyCheck1 :: (_ -> Prop) -> IO ()
easyCheck1 = easyCheck . suc id

easyCheck2 :: (_ -> _ -> Prop) -> IO ()
easyCheck2 = easyCheck . suc (suc id)

easyCheck3 :: (_ -> _ -> _ -> Prop) -> IO ()
easyCheck3 = easyCheck . suc (suc (suc id))

easyCheck4 :: (_ -> _ -> _ -> _ -> Prop) -> IO ()
easyCheck4 = easyCheck . suc (suc (suc (suc id)))

easyCheck5 :: (_ -> _ -> _ -> _ -> _ -> Prop) -> IO ()
easyCheck5 = easyCheck . suc (suc (suc (suc (suc id))))

verboseCheck1 :: (_ -> Prop) -> IO ()
verboseCheck1 = verboseCheck . suc id

verboseCheck2 :: (_ -> _ -> Prop) -> IO ()
verboseCheck2 = verboseCheck . suc (suc id)

verboseCheck3 :: (_ -> _ -> _ -> Prop) -> IO ()
verboseCheck3 = verboseCheck . suc (suc (suc id))

verboseCheck4 :: (_ -> _ -> _ -> _ -> Prop) -> IO ()
verboseCheck4 = verboseCheck . suc (suc (suc (suc id)))

verboseCheck5 :: (_ -> _ -> _ -> _ -> _ -> Prop) -> IO ()
verboseCheck5 = verboseCheck . suc (suc (suc (suc (suc id))))


check :: Config -> Prop -> IO ()
check config p = do
  isOrBased <- evalModeIsOrBased
  if isOrBased then tests config p 0 0 []
   else putStrLn $ unlines
    ["","EasyCheck must be run without suspending computations.",""
    ,"Please",""
    ," - type ':set or' at the command prompt in kicsi or"
    ," - put 'ChoiceMode=OrBased' in your ~/.kicsrc file",""
    ,"to enable this mode for this session or for all sessions respectively."]

tests :: Config -> [Test] -> Int -> Int -> [[String]] -> IO ()
tests _ [] ntest _ stamps = done "Passed" ntest stamps
tests config (t:ts) ntest nfail stamps
  | ntest == maxTest config = done "OK, passed" ntest stamps
  | nfail == maxFail config = done "Arguments exhausted after" ntest stamps
  | otherwise = do
      putStr (every config ntest (args t))
      case result t of
        Undef -> tests config ts ntest (nfail+1) stamps
        Ok    -> tests config ts (ntest+1) nfail (stamp t:stamps)
        Falsified results -> putStr $
          "Falsified by " ++ nth (ntest+1) ++ " test" ++
          (if null (args t) then "." else ".\nArguments:") ++ "\n" ++
          unlines (args t) ++
          if null results then "no result\n"
           else "Results:\n" ++ unlines results
        Ambigious bs results -> putStr $
          "Ambigious property yields " ++ show bs ++ " for " ++ 
          nth (ntest+1) ++ " test" ++
          (if null (args t) then "." else ".\nArguments:") ++ "\n" ++
          unlines (args t) ++
          if null results then "no result\n"
           else "Results:\n" ++ unlines results

nth :: Int -> String
nth n = case n of 1 -> "first"; 2 -> "second"; 3 -> "third"; _ -> show n++ "th"

done :: String -> Int -> [[String]] -> IO ()
done mesg ntest stamps = do
  putStr $ mesg ++ " " ++ show ntest ++ " test"
        ++ (if ntest >= 2 then "s" else "") ++ table
 where
  table = display
        . map entry
        . reverse
        . mergeSort (leqPair (<=) (leqList leqString))
        . map pairLength
        . group
        . mergeSort (leqList leqString)
        . filter (not . null)
        $ stamps

  display []         = ".\n"
  display [x]        = " - " ++ x ++ ".\n"
  display xs@(_:_:_) = ".\n" ++ unlines (map (++".") xs)

  pairLength xss@(xs:_) = (length xss,xs)

  entry (n,xs) = percentage n ntest ++ " " ++ concat (intersperse ", " xs)

  percentage n m = let s = show ((100*n)`div`m)
                    in replicate (3-length s) ' ' ++ s ++ "%"

-- Auxiliary Functions

leqPair :: (a -> a -> Bool) -> (b -> b -> Bool) -> ((a,b) -> (a,b) -> Bool)
leqPair leqa leqb (x1,y1) (x2,y2)
  | x1 == x2  = leqb y1 y2
  | otherwise = leqa x1 x2

leList :: (a -> a -> Bool) -> [a] -> [a] -> Bool
leList _  []     []     = False
leList _  []     (_:_)  = True
leList _  (_:_)  []     = False
leList le (x:xs) (y:ys) = le x y || x == y && leList le xs ys

valuesOf :: a -> [a]
valuesOf =
  rndLevelDiagFlat 3 2008 .
  searchTree . (id$##)

--- randomized diagonalization of levels. 
---
--- @param rs random seeds
--- @param t search tree
--- @return enumeration of values in given search tree
---
rndLevelDiag :: Int -> SearchTree a -> [a]
rndLevelDiag rnd t =
  [ x | Value x <- diagonal (rndLevels rnd [t]) ]

rndLevels :: Int -> [SearchTree a] -> [[SearchTree a]]
rndLevels rnd ts =
  if null ts then []
   else ts : rndLevels r (concat (zipWith shuffle rs [ us | Choice us <- ts ]))
 where
  r:rs = split rnd

--- randomized diagonalization of levels with flatening. 

rndLevelDiagFlat :: Int -> Int -> SearchTree a -> [a]
rndLevelDiagFlat d rnd t = 
  concat $ transpose (zipWith rndLevelDiag rs (flatRep d [t]))
 where
  rs = split rnd

flat :: SearchTree a -> [SearchTree a]
flat t = case t of
           Value _ -> [t]
           Choice ts -> ts
           _ -> []

flatRep :: Int -> [SearchTree a] -> [SearchTree a]
flatRep n ts | n==0      = ts
             | otherwise = flatRep (n-1) (concatMap flat ts)

-- auxiliary functions

--- list diagonalization. 
--- Fairly merges (possibly infinite) list of (possibly infinite) lists.
---
--- @param ls lists of lists
--- @return fair enumeration of all elements of inner lists of given lists
---
diagonal :: [[a]] -> [a]
diagonal = concat . foldr diags []
 where
  diags []     ys = ys
  diags (x:xs) ys = [x] : merge xs ys

  merge []       ys     = ys
  merge xs@(_:_) []     = map (:[]) xs
  merge (x:xs)   (y:ys) = (x:y) : merge xs ys


--- Computes a random permutation of the given list.
---
--- @param rnd random seed
--- @param l lists to shuffle
--- @return shuffled list
---
shuffle :: Int -> [a] -> [a]
shuffle rnd l = shuffleWithLen (nextInt rnd) (length l) l

shuffleWithLen :: [Int] -> Int -> [a] -> [a]
shuffleWithLen (r:rs) len xs
  | len == 0  = []
  | otherwise = z : shuffleWithLen rs (len-1) (ys++zs)
 where
  (ys,z:zs) = splitAt (abs r `mod` len) xs


transpose :: [[a]] -> [[a]]
transpose [] = []
transpose ([] : xss) = transpose xss
transpose ((x:xs) : xss)
  = (x : [h | (h:_) <- xss]) : transpose (xs : [t | (_:t) <- xss])

evalModeIsOrBased :: IO Bool
evalModeIsOrBased = getSearchTree (id$#unknown) >>= return . (==Value ())