import Control.Monad (when) import Data.Char (toUpper) import Data.Dynamic (fromDynamic) import Data.List (intercalate, isPrefixOf, isSuffixOf) import Data.Maybe (catMaybes) import Data.Monoid (Monoid (mappend, mempty, mconcat)) import qualified DynFlags as G (defaultDynFlags, PackageFlag (ExposePackage, HidePackage, IgnorePackage)) import qualified GHC as G (GhcMonad, packageFlags, getModuleGraph, ms_mod, setContext, moduleName, moduleNameString, targetAllowObjCode, targetContents, ghcLink, flags, hscTarget, load, modInfoTyThings, getModuleInfo, setTargets, dynCompileExpr, runGhc, defaultErrorHandler, getSessionDynFlags, setSessionDynFlags, ghcMode, GhcMode (CompManager), LoadHowMuch (LoadAllTargets), SuccessFlag (Failed, Succeeded), TyThing (AnId), GhcLink (LinkInMemory), TargetId (TargetFile), Target (Target, targetId), HscTarget (HscInterpreted), DynFlag (Opt_ImplicitImportQualified)) import qualified GHC.Paths as G (libdir) import qualified MonadUtils as G (liftIO) import qualified Name as G (nameOccName) import qualified OccName as G (occNameString) import System.Console.GetOpt (usageInfo, getOpt, OptDescr (Option), ArgOrder (ReturnInOrder), ArgDescr (ReqArg, NoArg)) import System.Directory (getDirectoryContents) import System.Environment (getArgs) import System.Exit (exitWith, exitFailure, exitSuccess, ExitCode (ExitSuccess, ExitFailure)) import System.IO (hPutStrLn, stderr) import Test.QuickCheck (Arbitrary (arbitrary, coarbitrary), variant, vector) import qualified Var as G (varName) {- This is how to embed QuickCheck properties in your code: make them top-level declarations whose names start with `prop_`. You don't need to include each property explicitly in a list; that would make it too easy to add a property but forget to run it. Instead, `hstest` looks through your program and builds the list of properties to run itself. -} prop_Count_Monoid_leftIdentity, prop_Count_Monoid_rightIdentity :: Count -> Bool prop_Count_Monoid_associative :: Count -> Count -> Count -> Bool prop_Count_Monoid_leftIdentity x = mappend mempty x == x prop_Count_Monoid_rightIdentity x = mappend x mempty == x prop_Count_Monoid_associative x y z = mappend x (mappend y z) == mappend (mappend x y) z data TestResult = Failed [String] | Passed | Exhausted Int | Unparsed String reportResult n (Unparsed s) = concat ["While testing ", n, ":\n", s] reportResult n (Exhausted c) = concat ["Exhausted ", n, " after ", show c, " tests\n"] reportResult n Passed = concat ["Passed ", n, "\n"] reportResult n (Failed params) = unlines (concat ["Failed ", n, ":"] : map f params) where f param = concat [" * ", param] shouldExplicitlyReportResult Passed = False shouldExplicitlyReportResult _ = True data Count = Count [Int] deriving (Eq, Show) instance Monoid Count where mempty = Count (replicate cResultIndicies 0) mappend (Count xs) (Count ys) = Count (zipWith (+) xs ys) instance Arbitrary Count where arbitrary = fmap Count $ vector cResultIndicies coarbitrary (Count xs) z = foldr variant z xs countResultI i = Count $ map f [0 .. cResultIndicies - 1] where f i' = if i == i' then 1 else 0 reportCount s' (Count cs) = concat [s', ": ", if null s then "0 tests!" else s] where s = ucfirst $ intercalate ", " . catMaybes . zipWith f ss $ cs f _ 0 = Nothing f (s, (s', _)) 1 = Just $ concat [s, " 1 ", s'] f (s, (_, s')) c = Just $ concat [s, " ", show c, " ", s'] ss = [("failed", sProperties), ("couldn't parse results of", sProperties), ("couldn't compile", sFiles), ("exhausted arguments while checking", sProperties), ("passed", sProperties)] sFiles = ("file", "files") sProperties = ("property", "properties") ucfirst "" = "" ucfirst (ch : s) = toUpper ch : s putCountLn s c = putStrLn (reportCount s c) >> return c iFailedProperty : iUnparsedProperty : iUncompiledFile : -- FAIL iExhaustedProperty : -- not proven iPassedProperty : -- WIN cResultIndicies : _ = [0..] -- must be grouped like this for `exitCodeFor` to work exitCodeFor (Count cs) = case length $ takeWhile (== 0) cs of c | c < iExhaustedProperty -> ExitFailure 2 c | c < iPassedProperty -> ExitFailure 1 _ -> ExitSuccess countResult (Failed _) = countResultI iFailedProperty countResult Passed = countResultI iPassedProperty countResult (Exhausted _) = countResultI iExhaustedProperty countResult (Unparsed _) = countResultI iUnparsedProperty runTest :: G.GhcMonad m => String -> m Count runTest nTest = do result <- fmap (maybe wrongTestResult divineTestResult . fromDynamic) (G.dynCompileExpr expr) when (shouldExplicitlyReportResult result) (G.liftIO $ putStr $ reportResult nTest result) return (countResult result) where -- `expr` originally copied from Test.QuickCheck, copyright © Koen Claessen , licenced under BSD3 expr = concat ["let gen = Test.QuickCheck.evaluate (Test.QuickCheck.property ", nTest, ") in ", "let f rnd0 cPassed cMissed stamps", "| cPassed == 100 = \"Passed\"", "| cMissed == 1000 = \"Exhausted\"", "| otherwise = let (rnd1, rnd2) = System.Random.split rnd0 in ", "let result = Test.QuickCheck.generate ((cPassed `div` 2) + 3) rnd2 gen in ", "case Test.QuickCheck.ok result of", "{Nothing -> f rnd1 cPassed (1 + cMissed) stamps;", "Just True -> f rnd1 (1 + cPassed) cMissed ", "(Test.QuickCheck.stamp result : stamps);", "Just False -> unlines (\"Failed\" : Test.QuickCheck.arguments result)} in ", "f (System.IO.Unsafe.unsafePerformIO System.Random.newStdGen) 0 0 []"] divineTestResult :: String -> TestResult divineTestResult "Passed" = Passed divineTestResult "Exhausted" = Exhausted 1000 divineTestResult s | "Failed" `isPrefixOf` s = Failed (tail $ lines s) | otherwise = Unparsed s wrongTestResult = Failed ["Was expecting property to be of type Testable a => a"] runTests :: Flags -> String -> IO Count runTests flags nf = G.runGhc (Just G.libdir) (G.defaultErrorHandler G.defaultDynFlags init) >>= putCountLn nf where init = do -- have to get and then set dynamic flags even if I don't want to change them -- somehow this initialises fields I don't want to care about dynFlags <- G.getSessionDynFlags G.setSessionDynFlags dynFlags {G.ghcMode = G.CompManager, G.ghcLink = G.LinkInMemory, G.hscTarget = G.HscInterpreted, G.flags = G.Opt_ImplicitImportQualified : G.flags dynFlags, G.packageFlags = packageFlags ++ G.packageFlags dynFlags} G.setTargets targets G.load G.LoadAllTargets >>= loaded loaded G.Failed = return (countResultI iUncompiledFile) loaded G.Succeeded = G.getModuleGraph >>= loadedModule . map G.ms_mod loadedModule [onlyModule] = G.getModuleInfo onlyModule >>= performTests . fmap (catMaybes . map nTestFromTyThing . G.modInfoTyThings) where performTests Nothing = error "Was expecting module to be loaded" performTests (Just ns) = G.setContext [onlyModule] [] >> fmap mconcat (mapM runTest ns) loadedModule mods = error (concat ["loadedModule was expecting only one module but got ", show (length mods), ": ", intercalate ", " $ map (G.moduleNameString . G.moduleName) mods]) nTestFromTyThing (G.AnId identity) = if "prop_" `isPrefixOf` n then Just n else Nothing where n = G.occNameString . G.nameOccName $ G.varName identity nTestFromTyThing _ = Nothing targets = [G.Target {G.targetId = G.TargetFile nf Nothing, G.targetAllowObjCode = False, G.targetContents = Nothing}] packageFlags = map f (pkgsFromFlags flags) where f (ExposePkg n) = G.ExposePackage n f (HidePkg n) = G.HidePackage n f (IgnorePkg n) = G.IgnorePackage n cmdLineDescr = [Option "" ["help"] (NoArg $ withHelpOption True) "display this help", Option "" ["expose-package"] (ReqArg (withPkg . ExposePkg) "PACKAGE") "expose a package", Option "" ["hide-package"] (ReqArg (withPkg . HidePkg) "PACKAGE") "hide a package", Option "" ["ignore-package"] (ReqArg (withPkg . IgnorePkg) "PACKAGE") "ignore a package"] usageMsg = usageInfo "Usage: hstest [FLAGS] [SOURCE FILES]" cmdLineDescr getCommandLineOptions wrap = getArgs >>= act . getOpt (ReturnInOrder wrap) cmdLineDescr where act (opts, _, []) = return opts act (_, _, errs) = hPutStrLn stderr (unlines errs ++ usageMsg) >> exitFailure data Options = Options {helpFlagFromOptions :: Bool, nfsFromOptions :: [String], flagsFromOptions :: Flags} noOptions = Options False [] defaultFlags withHelpOption flag opts = opts {helpFlagFromOptions = flag} withNfOption nf opts = opts {nfsFromOptions = nf : nfsFromOptions opts} withNfsOption nfs opts = opts {nfsFromOptions = nfs} alterFlags f opts = opts {flagsFromOptions = f (flagsFromOptions opts)} data Flags = Flags {pkgsFromFlags :: [PkgFlag]} data PkgFlag = ExposePkg String | HidePkg String | IgnorePkg String defaultFlags = Flags [] withPkg pkg = alterFlags $ \flags -> flags {pkgsFromFlags = pkg : pkgsFromFlags flags} main = fmap (foldr ($) noOptions) (getCommandLineOptions withNfOption) >>= defaultNfs >>= act >>= exitWith . exitCodeFor where act Options {helpFlagFromOptions = True} = putStrLn usageMsg >> exitSuccess act opts @ Options {nfsFromOptions = [nf]} = runTests (flagsFromOptions opts) nf act opts = mapM (runTests $ flagsFromOptions opts) (nfsFromOptions opts) >>= putCountLn "Total" . mconcat defaultNfs opts @ Options {nfsFromOptions = []} = fmap ((`withNfsOption` opts) . filter (".hs" `isSuffixOf`)) (getDirectoryContents ".") defaultNfs opts = return opts