hpc-lcov-1.0.0: exe/Main.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
import Control.Monad (forM)
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Types as JSON
import Data.HashMap.Lazy (HashMap, (!))
import Data.List (find, isPrefixOf)
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import qualified Data.Text as Text
import qualified Data.Yaml as Yaml
import qualified Options.Applicative as Opt
import Path (Abs, Dir, File, Path, Rel, reldir, relfile, (</>))
import qualified Path
import Path.IO (listDir, listDirRecur, resolveFile')
import System.Process (readProcessWithExitCode)
import Trace.Hpc.Lcov (generateLcovFromTix, writeReport)
import Trace.Hpc.Mix (Mix(..), readMix)
import Trace.Hpc.Tix (Tix(..), TixModule, readTix, tixModuleName)
data CLIOptions = CLIOptions
{ cliTixFiles :: [FilePath]
, cliMainPackage :: Maybe String
, cliOutput :: FilePath
}
getCLIOptions :: IO CLIOptions
getCLIOptions = Opt.execParser
$ Opt.info (Opt.helper <*> parseCLIOptions) $ Opt.progDesc description
where
parseCLIOptions = CLIOptions
<$> parseCLITixFiles
<*> parseCLIMainPackage
<*> parseCLIOutput
parseCLITixFiles = Opt.many $ Opt.strOption $ mconcat
[ Opt.long "file"
, Opt.short 'f'
, Opt.metavar "FILE"
, Opt.help "Manually specify .tix file(s) to convert"
]
parseCLIMainPackage = Opt.optional $ Opt.strOption $ mconcat
[ Opt.long "main-package"
, Opt.metavar "PACKAGE"
, Opt.help "The package that built the coverage-enabled executable"
]
parseCLIOutput = Opt.strOption $ mconcat
[ Opt.long "output"
, Opt.short 'o'
, Opt.metavar "FILE"
, Opt.help "The file to save coverage information (default: lcov.info)"
, Opt.value "lcov.info"
]
description = "Convert HPC coverage output into the LCOV format"
main :: IO ()
main = do
CLIOptions{..} <- getCLIOptions
stackRoot <- getStackRoot
tixFiles <- if null cliTixFiles
then findTixModules
else mapM resolveFile' cliTixFiles
tixModules <- filter (not . isPathsModule) . concat <$> mapM readTixPath tixFiles
distDir <- getStackDistPath
packages <- getPackages
let mixDirectories = map (getMixDirectory distDir . snd) packages
moduleToMixList <- forM tixModules $ \tixModule -> do
Mix fileLoc _ _ _ mixEntries <- readMixPath mixDirectories (Right tixModule)
fileLocRelPath <- Path.parseRelFile fileLoc
let modulePackageName = case tixModuleName tixModule of
"Main" -> fromMaybe
(error "Found executable in coverage file, --main-package was not provided")
cliMainPackage
_ -> getPackageName tixModule
modulePath <- case modulePackageName `lookup` packages of
Just packagePath -> do
let modulePathAbs = packagePath </> fileLocRelPath
maybe (fail $ show modulePathAbs ++ " is not a subpath of " ++ show stackRoot) return $
Path.stripProperPrefix stackRoot modulePathAbs
Nothing -> fail $ "Could not find package: " ++ modulePackageName
return (tixModuleName tixModule, (Path.toFilePath modulePath, mixEntries))
let moduleToMix = Map.toList . Map.fromListWith checkDupeMix $ moduleToMixList
checkDupeMix mix1 mix2 = if mix1 == mix2
then mix1
else error $ ".mix files differ: " ++ show (mix1, mix2)
report = generateLcovFromTix moduleToMix tixModules
writeReport cliOutput report
{- HPC file discovery -}
-- | Find all .tix files in the HPC root.
findTixModules :: IO [Path Abs File]
findTixModules = do
hpcRoot <- getStackHpcRoot
(_, files) <- listDirRecur hpcRoot
let tixFiles = filter (hasExt ".tix") files
-- Find all.tix, if one exists, which Stack automatically generates
-- if multiple .tix files are generated.
mAllTix = find ((== [relfile|all.tix|]) . Path.filename) tixFiles
return $ maybe tixFiles (:[]) mAllTix
getMixDirectory :: Path Rel Dir -> Path Abs Dir -> Path Abs Dir
getMixDirectory distDir packageDir = packageDir </> distDir </> [reldir|hpc|]
{- HPC file readers -}
readTixPath :: Path b File -> IO [TixModule]
readTixPath path = do
Tix tixModules <- readTix (Path.toFilePath path) >>=
maybe (fail $ "Could not find tix file: " ++ Path.toFilePath path) return
return tixModules
readMixPath :: [Path b Dir] -> Either String TixModule -> IO Mix
readMixPath = readMix . map Path.toFilePath
{- Haskell package/module helpers -}
getPackageName :: TixModule -> String
getPackageName = Text.unpack . takePackageName . Text.pack . tixModuleName
where
-- Tix module name is either just the module name or in the format `PACKAGE-VERSION-HASH/MODULE`
takePackageName s = case Text.splitOn "/" s of
[packageVersionHash, _] ->
Text.intercalate "-" . dropEnd 2 . Text.splitOn "-" $ packageVersionHash
_ -> s
dropEnd n xs = take (length xs - n) xs
isPathsModule :: TixModule -> Bool
isPathsModule = ("Paths_" `isPrefixOf`) . tixModuleName
{- Stack helpers -}
-- | Get the root of the stack project.
getStackRoot :: IO (Path Abs Dir)
getStackRoot = do
-- assume that the stack.yaml file is at the root of the stack projet
configPath <- readStack ["path", "--config-location"]
Path.parent <$> Path.parseAbsFile configPath
getStackHpcRoot :: IO (Path Abs Dir)
getStackHpcRoot = Path.parseAbsDir =<< readStack ["path", "--local-hpc-root"]
getStackDistPath :: IO (Path Rel Dir)
getStackDistPath = Path.parseRelDir =<< readStack ["path", "--dist-dir"]
-- | Get a list of package names in the stack project and their location.
getPackages :: IO [(String, Path Abs Dir)]
getPackages = do
stackConfigPath <- readStack ["path", "--config-location"]
stackConfig <- Yaml.decodeFileEither @(HashMap String JSON.Value) stackConfigPath >>=
either (\e -> fail $ "Could not decode file `" ++ stackConfigPath ++ "`: " ++ show e) return
stackConfigDir <- Path.parent <$> Path.parseAbsFile stackConfigPath
packagePaths <- maybe (fail $ "Invalid packages field: " ++ show stackConfig) return $
JSON.parseMaybe JSON.parseJSON (stackConfig ! "packages")
forM packagePaths $ \packagePath -> do
packageDir <- case packagePath of
-- special case since Path doesn't support `..`. Not spending too much effort on more complex
-- cases
".." -> return $ Path.parent stackConfigDir
package -> (stackConfigDir </>) <$> Path.parseRelDir package
(_, files) <- listDir packageDir
packageName <- case filter (hasExt ".cabal") files of
[] -> fail $ "No .cabal file found in " ++ Path.toFilePath packageDir
[cabal] -> Path.toFilePath . Path.filename <$> removeExtension cabal
_ -> fail $ "Multiple .cabal files found in " ++ Path.toFilePath packageDir
return (packageName, packageDir)
readStack :: [String] -> IO String
readStack args = do
(_, stdout, _) <- readProcessWithExitCode "stack" args ""
return . head . lines $ stdout
{- Utilities -}
hasExt :: String -> Path b File -> Bool
hasExt ext = (== ext') . Path.fileExtension
where
#if MIN_VERSION_path(0,7,0)
ext' = Just ext
#else
ext' = ext
#endif
removeExtension :: Path b File -> IO (Path b File)
#if MIN_VERSION_path(0,7,0)
removeExtension = fmap fst . Path.splitExtension
#else
removeExtension = Path.setFileExtension ""
#endif