tasty-coverage-0.1.1.0: src/Test/Tasty/CoverageReporter.hs
{-# LANGUAGE InstanceSigs, NamedFieldPuns #-}
module Test.Tasty.CoverageReporter (coverageReporter) where
import Test.Tasty
import Test.Tasty.Ingredients
import Test.Tasty.Options
import Test.Tasty.Runners
import Test.Tasty.Providers
import Data.Typeable
import Trace.Hpc.Reflect ( clearTix, examineTix )
import Trace.Hpc.Tix ( writeTix, Tix(..), TixModule(..) )
import System.FilePath ( (<.>), (</>) )
import Control.Monad (forM_)
import qualified Data.List.NonEmpty as NE
import Data.List.NonEmpty ( NonEmpty )
import Data.Foldable (fold)
import Data.Bifunctor (first)
newtype ReportCoverage = MkReportCoverage Bool
deriving (Eq, Ord, Typeable)
instance IsOption ReportCoverage where
defaultValue = MkReportCoverage False
parseValue = fmap MkReportCoverage . safeReadBool
optionName = pure "report-coverage"
optionHelp = pure "Generate per-test coverage data"
optionCLParser = mkFlagCLParser mempty (MkReportCoverage True)
newtype RemoveTixHash = MkRemoveTixHash Bool
deriving (Eq, Ord, Typeable)
instance IsOption RemoveTixHash where
defaultValue = MkRemoveTixHash False
parseValue = fmap MkRemoveTixHash . safeReadBool
optionName = pure "remove-tix-hash"
optionHelp = pure "Remove hash from tix file (used for golden tests)"
optionCLParser = mkFlagCLParser mempty (MkRemoveTixHash True)
coverageOptions :: [OptionDescription]
coverageOptions = [ Option (Proxy :: Proxy ReportCoverage)
, Option (Proxy :: Proxy RemoveTixHash)
]
tixDir :: FilePath
tixDir = "tix"
-- | Obtain the list of all tests in the suite
testNames :: OptionSet -> TestTree -> IO ()
testNames os tree = forM_ (foldTestTree coverageFold os tree) $ \(s,f) -> f (fold (NE.intersperse "." s))
-- | Collect all tests and
coverageFold :: TreeFold [(NonEmpty TestName, String -> IO ())]
coverageFold = trivialFold
{ foldSingle = \opts name test -> do
let f n = do
-- Collect the coverage data for exactly this test.
clearTix
result <- run opts test (\_ -> pure ())
tix <- examineTix
let filepath = tixFilePath n result
writeTix filepath (removeHash opts tix)
putStrLn ("Wrote coverage file: " <> filepath)
pure (NE.singleton name, f),
-- Append the name of the testgroup to the list of TestNames
foldGroup = \_ groupName acc -> fmap (first (NE.cons groupName)) acc
}
tixFilePath :: TestName -> Result -> FilePath
tixFilePath tn Result { resultOutcome } =
tixDir </> generateValidFilepath tn <.> outcomeSuffix resultOutcome <.> ".tix"
-- | We want to compute the file suffix that we use to distinguish
-- tix files for failing and succeeding tests.
outcomeSuffix :: Outcome -> String
outcomeSuffix Success = "PASSED"
outcomeSuffix (Failure TestFailed) = "FAILED"
outcomeSuffix (Failure (TestThrewException _)) = "EXCEPTION"
outcomeSuffix (Failure (TestTimedOut _)) = "TIMEOUT"
outcomeSuffix (Failure TestDepFailed) = "SKIPPED"
coverageReporter :: Ingredient
coverageReporter = TestManager coverageOptions coverageRunner
coverageRunner :: OptionSet -> TestTree -> Maybe (IO Bool)
coverageRunner opts tree = case lookupOption opts of
MkReportCoverage False -> Nothing
MkReportCoverage True -> Just $ do
testNames opts tree
pure True
-- | Removes all path separators from the input String in order
-- to generate a valid filepath.
-- The names of some tests contain path separators, so we have to
-- remove them.
generateValidFilepath :: String -> FilePath
generateValidFilepath = filter (`notElem` pathSeparators)
where
-- Include both Windows and Posix, so that generated .tix files
-- are consistent among systems.
pathSeparators = ['\\','/']
removeHash :: OptionSet -> Tix -> Tix
removeHash opts (Tix txs) = case lookupOption opts of
MkRemoveTixHash False -> Tix txs
MkRemoveTixHash True -> Tix (fmap removeHashModule txs)
removeHashModule :: TixModule -> TixModule
removeHashModule (TixModule name _hash i is) = TixModule name 0 i is