18
\$\begingroup\$

We have been working on a mutation analysis tool for Haskell tests called MuCheck. It accepts any Haskell source file, and a function name to mutate, applies a defined set of mutation operators on it, and runs the specified test suite on it. The mutation of code is accomplished using the haskell-src-ext library, and SYB functions. I would like help on how to make this code better. I would really appreciate comments on improving the clarity, and also better ways of doing things.

I have run it through hlint, and have accepted most of its recommendations.

Our project along with unit tests and run instructions is here.

The library entry

-- | MuCheck base module
module Test.MuCheck (mucheck) where
import System.Environment (getArgs, withArgs)
import Control.Monad (void)

import Test.MuCheck.MuOp
import Test.MuCheck.Config
import Test.MuCheck.Mutation
import Test.MuCheck.Operators
import Test.MuCheck.Utils.Common
import Test.MuCheck.Utils.Print
import Test.MuCheck.Interpreter (mutantCheckSummary)
import Test.MuCheck.TestAdapter

-- | Perform mutation analysis
mucheck :: (Summarizable a, Show a) => ([String] -> [InterpreterOutput a] -> TSum) -> String -> FilePath -> String -> [String] -> IO ()
mucheck resFn mFn file modulename args = do
  numMutants <- genMutants mFn file
  let muts = take numMutants $ genFileNames file
  void $ mutantCheckSummary resFn muts modulename args ("./mucheck-" ++ mFn ++ ".log")

Configuration Options

-- | Configuration module
module Test.MuCheck.Config where

import Test.MuCheck.MuOp
import Test.MuCheck.Operators (allOps)

data GenerationMode
  = FirstOrderOnly
  | FirstAndHigherOrder
  deriving (Eq, Show)

data Config = Config {
-- | Mutation operators on operator or function replacement
  muOps :: [MuOp]
-- | Mutate pattern matches for functions?
--
  , doNegateGuards :: Rational
-- | Maximum number of mutants to generate.
  , maxNumMutants :: Int
-- | Generation mode, can be traditional (firstOrder) and
-- higher order (higher order is experimental)
  , genMode :: GenerationMode }
  deriving Show

-- | The default configuration
defaultConfig :: Config
defaultConfig = Config {muOps = allOps
  , doMutatePatternMatches = 1.0
  , doMutateValues = 1.0
  , doNegateIfElse = 1.0
  , doNegateGuards = 1.0
  , maxNumMutants = 300
  , genMode = FirstOrderOnly }

Interpreter for mutants

{-# LANGUAGE StandaloneDeriving, DeriveDataTypeable #-}
-- | The entry point for mucheck
module Test.MuCheck.Interpreter (mutantCheckSummary) where

import qualified Language.Haskell.Interpreter as I
import Control.Monad.Trans ( liftIO )
import Data.Typeable
import Test.MuCheck.Utils.Print (showA, showAS, (./.))
import Data.Either (partitionEithers, rights)
import Data.List(groupBy, sortBy)
import Data.Function (on)

import Test.MuCheck.TestAdapter

-- | Given the list of tests suites to check, run one test suite at a time on
-- all mutants.
mutantCheckSummary :: (Summarizable a, Show a) => ([String] -> [InterpreterOutput a] -> TSum) -> [String] -> String -> [String] -> FilePath -> IO ()
mutantCheckSummary testSummaryFn mutantFiles topModule evalSrcLst logFile  = do
  results <- mapM (runCodeOnMutants mutantFiles topModule) evalSrcLst
  let singleTestSummaries = zip evalSrcLst $ map (testSummaryFn mutantFiles) results
      tssum  = multipleCheckSummary (isSuccess . snd) results
  -- print results to terminal
  putStrLn $ delim ++ "Overall Results:"
  putStrLn $ terminalSummary tssum
  putStrLn $ showAS $ map showBrief singleTestSummaries
  putStr delim
  -- print results to logfile
  appendFile logFile $ "OVERALL RESULTS:\n" ++ tssum_log tssum ++ showAS (map showDetail singleTestSummaries)
  return ()
  where showDetail (method, msum) = delim ++ showBrief (method, msum) ++ "\n" ++ tsum_log msum
        showBrief (method, msum) = showAS [method,
           "\tTotal number of mutants:\t" ++ show (tsum_numMutants msum),
           "\tFailed to Load:\t" ++ (cpx tsum_loadError),
           "\tNot Killed:\t" ++ (cpx tsum_notKilled),
           "\tKilled:\t" ++ (cpx tsum_killed),
           "\tOthers:\t" ++ (cpx tsum_others),
           ""]
           where cpx fn = show (fn msum) ++ " " ++ (fn msum) ./. (tsum_numMutants msum)
        terminalSummary tssum = showAS [
          "Total number of mutants:\t" ++ show (tssum_numMutants tssum),
          "Total number of alive mutants:\t" ++ (cpx tssum_alive),
          "Total number of load errors:\t" ++ (cpx tssum_errors),
          ""]
           where cpx fn = show (fn tssum) ++ " " ++ (fn tssum) ./. (tssum_numMutants tssum)
        delim = "\n" ++ replicate 25 '=' ++ "\n"

-- | Run one test suite on all mutants
runCodeOnMutants mutantFiles topModule evalStr = mapM (evalMyStr evalStr) mutantFiles
  where evalMyStr evalStr file = do putStrLn $ ">" ++ ":" ++ file ++ ":" ++ topModule ++ ":" ++ evalStr
                                    I.runInterpreter (evalMethod file topModule evalStr)

-- | Given the filename, modulename, test to evaluate, evaluate, and return result as a pair.
--
-- > t = I.runInterpreter (evalMethod
-- >        "Examples/QuickCheckTest.hs"
-- >        "Examples.QuickCheckTest"
-- >        "quickCheckResult idEmp)
evalMethod :: (I.MonadInterpreter m, Typeable t) => String -> String -> String -> m (String, t)
evalMethod fileName topModule evalStr = do
  I.loadModules [fileName]
  I.setTopLevelModules [topModule]
  result <- I.interpret evalStr (I.as :: (Typeable a => IO a)) >>= liftIO
  return (fileName, result)

-- | Datatype to hold results of the entire run
data TSSum = TSSum {tssum_numMutants::Int,
                    tssum_alive::Int,
                    tssum_errors::Int,
                    tssum_log::String}

-- | Summarize the entire run
multipleCheckSummary isSuccessFunction results
  -- we assume that checking each prop results in the same number of errorCases and executedCases
  | not (checkLength results) = error "Output lengths differ for some properties."
  | otherwise = TSSum {tssum_numMutants = countMutants,
                       tssum_alive = countAlive,
                       tssum_errors= countErrors,
                       tssum_log = logMsg}
  where executedCases = groupBy ((==) `on` fst) . sortBy (compare `on` fst) . rights $ concat results
        allSuccesses = [rs | rs <- executedCases, length rs == length results, all isSuccessFunction rs]
        countAlive = length allSuccesses
        countErrors = countMutants - length executedCases
        logMsg = showA allSuccesses
        checkLength results = and $ map ((==countMutants) . length) results ++ map ((==countExecutedCases) . length) executedCases
        countExecutedCases = length . head $ executedCases
        countMutants = length . head $ results

Mutation of code

{-# LANGUAGE ImpredicativeTypes #-}
-- | Mutation happens here.
module Test.MuCheck.Mutation where

import Language.Haskell.Exts(Literal(Int), Exp(App, Var, If), QName(UnQual),
        Stmt(Qualifier), Module(Module), ModuleName(..),
        Name(Ident, Symbol), Decl(FunBind, PatBind), Match,
        Pat(PVar), Match(Match), GuardedRhs(GuardedRhs), 
        prettyPrint, fromParseResult, parseFileContents)
import Data.Maybe (fromJust)
import Data.Generics (GenericQ, mkQ, Data, Typeable, mkMp, listify)
import Data.List(nub, (\\), permutations)
import Control.Monad (liftM, zipWithM)
import System.Random
import Data.Time.Clock.POSIX

import Test.MuCheck.MuOp
import Test.MuCheck.Utils.Syb
import Test.MuCheck.Utils.Common
import Test.MuCheck.Operators
import Test.MuCheck.Config

genMutants = genMutantsWith defaultConfig

genMutantsWith args funcname filename  = liftM length $ do
    ast <- getASTFromFile filename
    g <- liftM (mkStdGen . round) getPOSIXTime
    let f = getFunc funcname ast

        ops, swapOps, valOps, ifElseNegOps, guardedBoolNegOps :: [MuOp]
        ops = relevantOps f (muOps args ++ valOps ++ ifElseNegOps ++ guardedBoolNegOps)
        swapOps = sampleF g (doMutatePatternMatches args) $ permMatches f ++ removeOnePMatch f
        valOps = sampleF g (doMutateValues args) $ selectIntOps f
        ifElseNegOps = sampleF g (doNegateIfElse args) $ selectIfElseBoolNegOps f
        guardedBoolNegOps = sampleF g (doNegateGuards args) $ selectGuardedBoolNegOps f

        patternMatchMutants, ifElseNegMutants, guardedNegMutants, operatorMutants, allMutants :: [Decl]
        allMutants = nub $ patternMatchMutants ++ operatorMutants

        patternMatchMutants = mutatesN swapOps f fstOrder
        ifElseNegMutants = mutatesN ifElseNegOps f fstOrder
        guardedNegMutants = mutatesN guardedBoolNegOps f fstOrder
        operatorMutants = case genMode args of
            FirstOrderOnly -> mutatesN ops f fstOrder
            _              -> mutates ops f

        getFunc fname ast = head $ listify (isFunctionD fname) ast
        programMutants ast =  map (putDecls ast) $ mylst ast
        mylst ast = [myfn ast x | x <- take (maxNumMutants args) allMutants]
        myfn ast fn = replace (getFunc funcname ast,fn) (getDecls ast)

    case ops ++ swapOps of
      [] -> return [] --  putStrLn "No applicable operator exists!"
      _  -> zipWithM writeFile (genFileNames filename) $ map prettyPrint (programMutants ast)
  where fstOrder = 1 -- first order
        getASTFromFile filename = liftM parseModuleFromFile $ readFile filename

-- | Mutating a function's code using a bunch of mutation operators
-- (In all the three mutate functions, we assume working
-- with functions declaration.)
mutates :: [MuOp] -> Decl -> [Decl]
mutates ops m = filter (/= m) $ concatMap (mutatesN ops m) [1..]

-- the third argument specifies whether it's first order or higher order
mutatesN :: [MuOp] -> Decl -> Int -> [Decl]
mutatesN ops m 1 = concat [mutate op m | op <- ops ]
mutatesN ops m c =  concat [mutatesN ops m 1 | m <- mutatesN ops m (c-1)]

-- | Given a function, generate all mutants after applying applying 
-- op once (op might be applied at different places).E.g.:
-- op = "<" ==> ">" and there are two instances of "<"
mutate :: MuOp -> Decl -> [Decl]
mutate op m = once (mkMp' op) m \\ [m]

-- | is the parsed expression the function we are looking for?
isFunctionD :: String -> Decl -> Bool
isFunctionD n (FunBind (Match _ (Ident n') _ _ _ _ : _)) = n == n'
isFunctionD n (FunBind (Match _ (Symbol n') _ _ _ _ : _)) = n == n'
isFunctionD n (PatBind _ (PVar (Ident n')) _ _)          = n == n'
isFunctionD _ _                                  = False

-- | Generate all operators for permutating pattern matches in
-- a function. We don't deal with permutating guards and case for now.
permMatches :: Decl -> [MuOp]
permMatches d@(FunBind ms) = d ==>* map FunBind (permutations ms \\ [ms])
permMatches _  = []

-- | generates transformations that removes one pattern match from a function
-- definition.
removeOnePMatch :: Decl -> [MuOp]
removeOnePMatch d@(FunBind [x]) = []
removeOnePMatch d@(FunBind ms) = d ==>* map FunBind (removeOneElem ms \\ [ms])
removeOnePMatch _  = []

-- | generate sub-arrays with one less element
removeOneElem :: Eq t => [t] -> [[t]]
removeOneElem l = choose l (length l - 1)

-- AST/module-related operations

-- | Parse a module. Input is the content of the file
parseModuleFromFile :: String -> Module
parseModuleFromFile inp = fromParseResult $ parseFileContents inp

getDecls :: Module -> [Decl]
getDecls (Module _ _ _ _ _ _ decls) = decls

putDecls :: Module -> [Decl] -> Module
putDecls (Module a b c d e f _) decls = Module a b c d e f decls

-- Define all operations on a value
selectValOps :: (Data a, Eq a, Typeable b, Mutable b, Eq b) => (b -> Bool) -> [b -> b] -> a -> [MuOp]
selectValOps pred fs m = concatMap (\x -> x ==>* map (\f -> f x) fs) vals
  where vals = nub $ listify pred m

selectValOps' :: (Data a, Eq a, Typeable b, Mutable b) => (b -> Bool) -> (b -> [b]) -> a -> [MuOp]
selectValOps' pred f m = concatMap (\x -> x ==>* f x) vals
  where vals = listify pred m

selectIntOps :: (Data a, Eq a) => a -> [MuOp]
selectIntOps m = selectValOps isInt [
      \(Int i) -> Int (i + 1),
      \(Int i) -> Int (i - 1),
      \(Int i) -> if abs i /= 1 then Int 0 else Int i,
      \(Int i) -> if abs (i-1) /= 1 then Int 1 else Int i] m
  where isInt (Int _) = True
        isInt _       = False

-- | negating boolean in if/else statements
selectIfElseBoolNegOps :: (Data a, Eq a) => a -> [MuOp]
selectIfElseBoolNegOps m = selectValOps isIf [\(If e1 e2 e3) -> If (App (Var (UnQual (Ident "not"))) e1) e2 e3] m
  where isIf If{} = True
        isIf _    = False

-- | negating boolean in Guards
selectGuardedBoolNegOps :: (Data a, Eq a) => a -> [MuOp]
selectGuardedBoolNegOps m = selectValOps' isGuardedRhs negateGuardedRhs m
  where isGuardedRhs GuardedRhs{} = True
        boolNegate e@(Qualifier (Var (UnQual (Ident "otherwise")))) = [e]
        boolNegate (Qualifier exp) = [Qualifier (App (Var (UnQual (Ident "not"))) exp)]
        boolNegate x = [x]
        negateGuardedRhs (GuardedRhs srcLoc stmts exp) = [GuardedRhs srcLoc s exp | s <- once (mkMp boolNegate) stmts]

Supporting routines for mutation

module Test.MuCheck.MuOp (MuOp
          , Mutable(..)
          , (==>*)
          , (*==>*)
          , (~~>)
          , mkMp'
          , same
          ) where

import Language.Haskell.Exts (Name, QName, QOp, Exp, Literal, GuardedRhs, Decl)
import qualified Data.Generics as G
import Control.Monad (MonadPlus, mzero)

data MuOp = N  (Name, Name)
          | QN (QName, QName)
          | QO (QOp, QOp)
          | E  (Exp, Exp)
          | D  (Decl, Decl)
          | L  (Literal, Literal)
          | G  (GuardedRhs, GuardedRhs)
  deriving Eq

-- boilerplate code

-- | The function `same` applies on a `MuOP` determining if transformation is
-- between same values.
same :: MuOp -> Bool
same (N (a,b)) = a == b
same (QN (a,b)) = a == b
same (E (a,b)) = a == b
same (D (a,b)) = a == b
same (L (a,b)) = a == b
same (G (a,b)) = a == b

mkMp' (N (s,t))  = G.mkMp (s ~~> t)
mkMp' (QN (s,t)) = G.mkMp (s ~~> t)
mkMp' (QO (s,t)) = G.mkMp (s ~~> t)
mkMp' (E (s,t))  = G.mkMp (s ~~> t)
mkMp' (D (s,t))  = G.mkMp (s ~~> t)
mkMp' (L (s,t))  = G.mkMp (s ~~> t)
mkMp' (G (s,t))  = G.mkMp (s ~~> t)

showM (s, t) = "\n" ++ show s ++ " ==> " ++ show t
instance Show MuOp where
    show (N a)  = showM a
    show (QN a) = showM a
    show (QO a) = showM a
    show (E a)  = showM a
    show (D a)  = showM a
    show (L a)  = showM a
    show (G a)  = showM a

-- end boilerplate code

-- Mutation operation representing translation from one fn to another fn.
class Mutable a where
    (==>) :: a -> a -> MuOp

(==>*) :: Mutable a => a -> [a] -> [MuOp]
(==>*) x lst = map (\i -> x ==> i) lst

(*==>*) :: Mutable a => [a] -> [a] -> [MuOp]
xs *==>* ys = concatMap (==>* ys) xs

-- we handle x ~~> x separately
(~~>) :: (MonadPlus m, Eq a) => a -> a -> (a -> m a)
x ~~> y = \z -> if z == x then return y else mzero

-- instances

instance Mutable Name where
    (==>) = (N .) . (,)

instance Mutable QName where
    (==>) = (QN .) . (,)

instance Mutable QOp where
    (==>) = (QO .) . (,)

instance Mutable Exp where
    (==>) = (E .) . (,)

instance Mutable Decl where
    (==>) = (D .) . (,)

instance Mutable Literal where
    (==>) = (L .) . (,)

instance Mutable GuardedRhs where
    (==>) = (G .) . (,)

A few generic routines used in other parts

-- | Common functions used by MuCheck
module Test.MuCheck.Utils.Common where

import System.FilePath (splitExtension)
import System.Random
import Data.List
import Control.Applicative

-- | The `choose` function generates subsets of a given size
choose :: [a] -> Int -> [[a]]
choose xs n = filter (\x -> length x == n) $ subsequences xs

-- | The `coupling` function produces all possible pairings, and applies the
-- given function to each
coupling fn ops = [(fn o1 o2) | o1 <- ops, o2 <- ops, o1 /= o2]


-- | The `genFileNames` function lazily generates filenames of mutants
genFileNames :: String -> [String]
genFileNames s =  map newname [1..]
    where (name, ext) = splitExtension s
          newname i= name ++ "_" ++ show i ++ ext

-- | The `replace` function replaces first element in a list given old and new values as a pair
replace :: Eq a => (a,a) -> [a] -> [a]
replace (o,n) lst = map replaceit lst
  where replaceit v
          | v == o = n
          | otherwise = v

-- | The `sample` function takes a random generator and chooses a random sample
-- subset of given size.
sample :: (RandomGen g, Num n, Eq n) => g -> n -> [t] -> [t]
sample g 0 xs = []
sample g n xs = val : sample g' (n - 1) (remElt idx xs)
  where val = xs !! idx
        (idx,g')  = randomR (0, length xs - 1) g

-- | The `sampleF` function takes a random generator, and a fraction and
-- returns subset of size given by fraction
sampleF :: (RandomGen g, Num n) => g -> Rational -> [t] -> [t]
sampleF g f xs = sample g l xs
    where l = round $ f * fromIntegral (length xs)

-- | The `remElt` function removes element at index specified from a list
remElt :: Int -> [a] -> [a]
remElt idx xs = front ++ ack
  where (front,b:ack) = splitAt idx xs

-- | The `swapElts` function swaps two elements in a list given their indices
swapElts :: Int -> Int -> [t] -> [t]
swapElts i j ls = [get k x | (k, x) <- zip [0..length ls - 1] ls]
  where get k x | k == i = ls !! j
                | k == j = ls !! i
                | otherwise = x

-- | The `genSwapped` generates a list of lists where each element has been
-- swapped by another
genSwapped :: [t] -> [[t]]
genSwapped lst = map (\(x:y:_) -> swapElts x y lst) swaplst
  where swaplst = choose [0..length lst - 1] 2

Common SYB routines

{-# LANGUAGE RankNTypes #-}
-- | SYB functions
module Test.MuCheck.Utils.Syb (relevantOps, once) where

import Data.Generics (Data, GenericM, gmapMo)
import Test.MuCheck.MuOp (mkMp', MuOp, same)
import Control.Monad (MonadPlus, mplus)
import Data.Maybe(isJust)

-- | apply a mutating function on a piece of code one at a time
-- like somewhere (from so)
once :: MonadPlus m => GenericM m -> GenericM m
once f x = f x `mplus` gmapMo (once f) x

-- | The function `relevantOps` does two filters. For the first, it
-- removes spurious transformations like "Int 1 ~~> Int 1". Secondly, it
-- tries to apply the transformation to the given program on some element 
-- if it does not succeed, then we discard that transformation.
relevantOps :: (Data a, Eq a) => a -> [MuOp] -> [MuOp]
relevantOps m oplst = filter (relevantOp m) $ filter (not . same) oplst
  -- check if an operator can be applied to a program
  where relevantOp m op = isJust $ once (mkMp' op) m

Common print routines

module Test.MuCheck.Utils.Print where
import Debug.Trace
import Data.List(intercalate)

-- | simple wrapper for adding a % at the end.
n ./. t =  "(" ++ show (n * 100 `div` t) ++ "%)"

-- | join lines together
showAS :: [String] -> String
showAS = intercalate "\n"

-- | make lists into lines in text.
showA :: Show a => [a] -> String
showA =  showAS . map show

tt v = trace (">" ++ (show v)) v

Mutation opertors

module Test.MuCheck.Operators (comparators,
                          predNums,
                          binAriths,
                          arithLists,
                          allOps) where

import Test.MuCheck.MuOp
import Test.MuCheck.Utils.Common
import Language.Haskell.Exts (Name(Symbol), Exp(Var), QName(UnQual), Name(Ident))

-- | all available operators
allOps = concat [comparators, predNums, binAriths, arithLists]

-- | comparison operators ["<", ">", "<=", ">=", "/=", "=="]
comparators = coupling (==>) $ map Symbol ["<", ">", "<=", ">=", "/=", "=="]

-- | predicates ["pred", "id", "succ"]
predNums = coupling (==>) $ map varfn ["pred", "id", "succ"]

-- | binary arithmetic ["+", "-", "*", "/"]
binAriths = coupling (==>) $ map Symbol ["+", "-", "*", "/"]

-- | functions on lists ["sum", "product", "maximum", "minimum", "head", "last"]
arithLists = coupling (==>) $ map varfn ["sum", "product", "maximum", "minimum", "head", "last"]

-- utilities
varfn = Var . UnQual . Ident

Common routines for integrating test frameworks

module Test.MuCheck.TestAdapter where

import qualified Language.Haskell.Interpreter as I
import Data.Typeable
type InterpreterOutput a = Either I.InterpreterError (String, a)

data TSum = TSum {tsum_numMutants::Int,
                  tsum_loadError::Int,
                  tsum_notKilled::Int,
                  tsum_killed::Int,
                  tsum_others::Int,
                  tsum_log::String}

-- Class/Instance declaration
type MutantFilename = String
class Typeable s => Summarizable s where
  testSummary :: [MutantFilename] -> [InterpreterOutput s] -> TSum
  isSuccess :: s -> Bool

Using the above for QuickCheck integration (Different module)

Adapter

{-# LANGUAGE StandaloneDeriving, DeriveDataTypeable, TypeSynonymInstances #-}
-- | Module for using quickcheck properties
module Test.MuCheck.TestAdapter.QuickCheck where
import qualified Test.QuickCheck.Test as Qc
import Test.MuCheck.TestAdapter
import Test.MuCheck.Utils.Print (showA, showAS)

import Data.Typeable
import Data.List((\\))
import Data.Either (partitionEithers)

deriving instance Typeable Qc.Result
type QuickCheckSummary = Qc.Result

-- | Summarizable instance of `QuickCheck.Result`
instance Summarizable QuickCheckSummary where
  testSummary mutantFiles results = TSum {
    tsum_numMutants = r,
    tsum_loadError = e,
    tsum_notKilled = s,
    tsum_killed = f,
    tsum_others = g,
    tsum_log = logMsg}
    where (errorCases, executedCases) = partitionEithers results
          [successCases, failureCases, gaveUpCases] = map (\c -> filter (c . snd) executedCases) [isSuccess, isFailure, isGaveUp]
          r = length results
          e = length errorCases
          [s,f,g] = map length [successCases, failureCases, gaveUpCases]
          errorFiles = mutantFiles \\ map fst executedCases
          logMsg = showAS ["Details:",
                           "Loading error files:", showA errorFiles,
                           "Loading error messages:", showA errorCases,
                           "Successes:", showA successCases,
                           "Failure:", showA failureCases,
                           "Gaveups:", showA gaveUpCases]
          isFailure :: Qc.Result -> Bool
          isFailure Qc.Failure{} = True
          isFailure _         = False
          isGaveUp :: Qc.Result -> Bool
          isGaveUp Qc.GaveUp{} = True
          isGaveUp _        = False
  isSuccess = Qc.isSuccess

Main

module Main where
import System.Environment (getArgs, withArgs)
import Control.Monad (void)

import Test.MuCheck (mucheck)
import Test.MuCheck.TestAdapter.QuickCheck
import Test.MuCheck.TestAdapter
import Test.MuCheck.Utils.Print

main :: IO ()
main = do
  val <- getArgs
  case val of
    ("-h" : _ ) -> help
    (fn : file : modulename : args) -> withArgs [] $ mucheck tsFn fn file modulename args
    _ -> error "Need function file modulename [args]\n\tUse -h to get help"
  where tsFn :: [MutantFilename] -> [InterpreterOutput QuickCheckSummary] -> TSum
        tsFn = testSummary


help :: IO ()
help = putStrLn $ "mucheck function file modulename [args]\n" ++ showAS ["E.g:",
       " ./mucheck qsort Examples/QuickCheckTest.hs Examples.QuickCheckTest 'quickCheckResult idEmpProp' 'quickCheckResult revProp' 'quickCheckResult modelProp'",""]
\$\endgroup\$
  • 1
    \$\begingroup\$ It has been almost 3.5 years. Do you want to review your own code by now? \$\endgroup\$ – Zeta Mar 27 '18 at 21:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.