packages feed

hpc-codecov-0.6.2.0: src/Trace/Hpc/Codecov/Options.hs

{-# OPTIONS_HADDOCK hide #-}
-- |
-- Module:     Trace.Hpc.Codecov.Options
-- Copyright:  (c) 2022 8c6794b6
-- License:    BSD3
-- Maintainer: 8c6794b6 <8c6794b6@gmail.com>
--
-- Command line options for generating Codecov test coverage report.

module Trace.Hpc.Codecov.Options
  (
    -- * The Options type and predefined values
    Options(..)
  , defaultOptions
  , emptyOptions

    -- * Command line parser for 'Options'
  , parseOptions

    -- * Converter
  , opt2rpt

    -- * Help message and version number
  , printHelp
  , printVersion
  , printNumericVersion
  ) where

-- base
import Control.Exception           (throw, throwIO)
import Data.Version                (showVersion)
import System.Console.GetOpt       (ArgDescr (..), ArgOrder (..),
                                    OptDescr (..), getOpt, usageInfo)
import System.Environment          (getProgName)
import System.IO                   (hIsTerminalDevice, stdout)

-- directory
import System.Directory            (doesFileExist)


-- Internal
import Paths_hpc_codecov           (version)
import Trace.Hpc.Codecov.Discover
import Trace.Hpc.Codecov.Exception
import Trace.Hpc.Codecov.Report

-- | Options for generating test coverage report.
data Options = Options
  { optTix          :: FilePath
    -- ^ Input tix file.
  , optMixDirs      :: [FilePath]
    -- ^ Directory containing mix files referred by the tix file.
  , optSrcDirs      :: [FilePath]
    -- ^ Directory containing source codes referred by the mix files.
  , optExcludes     :: [String]
    -- ^ Module name strings to exclude from coverage report.
  , optOutFile      :: Maybe FilePath
    -- ^ Output file to write JSON report, if given.

  , optFormat       :: String
    -- ^ Format of generated report.

  , optVerbose      :: Bool
    -- ^ Flag for showing verbose message during coverage report
    -- generation.

  , optRootDir      :: FilePath
    -- ^ Project root directory for the build tool.
  , optBuildDir     :: Maybe FilePath
    -- ^ Name of the build directory used by the build tool
  , optSkipDirs     :: [String]
    -- ^ Directories to ignore while discovering.

  , optExprOnly     :: Bool
    -- ^ Flag for conting 'ExpBox' entries only.
    --
    -- @since 0.6.0.0
  , optIgnoreDittos :: Bool
    -- ^ Flag for ignoring repeated entries with the same source code
    -- positions.
    --
    -- @since 0.6.0.0

  , optShowVersion  :: Bool
    -- ^ Flag for showing version.
  , optShowNumeric  :: Bool
    -- ^ Flag for showing numeric version.
  , optShowHelp     :: Bool
    -- ^ Flag for showing help message.
  }

-- | Empty 'Options'.
emptyOptions :: Options
emptyOptions = Options
  { optTix = throw NoTarget
  , optMixDirs = []
  , optSrcDirs = []
  , optExcludes = []
  , optOutFile = Nothing
  , optFormat = "codecov"
  , optVerbose = False
  , optRootDir = ""
  , optBuildDir = Nothing
  , optSkipDirs = []
  , optExprOnly = False
  , optIgnoreDittos = False
  , optShowVersion = False
  , optShowNumeric = False
  , optShowHelp = False
  }

-- | The default 'Options'.
defaultOptions :: Options
defaultOptions = emptyOptions
  { optMixDirs = [".hpc"]
  , optSrcDirs = [""]
  }

-- | Commandline option oracle.
options :: [OptDescr (Options -> Options)]
options =
  [ Option ['m'] ["mix"]
           (ReqArg (\d o -> o {optMixDirs = uncommas d ++ optMixDirs o})
                   "DIR")
            ".mix file directory, can repeat\n\
            \(default: .hpc)"
  , Option ['s'] ["src"]
           (ReqArg (\d o -> o {optSrcDirs = uncommas d ++ optSrcDirs o})
                   "DIR")
           "Source directory, can repeat\n\
           \(default: current directory)"
  , Option ['x'] ["exclude"]
           (ReqArg (\m o -> o {optExcludes = uncommas m ++ optExcludes o})
                   "MODULE")
           "Module name to exclude, can repeat"
  , Option ['o'] ["out"]
           (ReqArg (\p o -> o {optOutFile = Just p}) "FILE")
           "Output file\n\
           \(default: stdout)"

  , Option ['r'] ["root"]
           (ReqArg (\d o -> o {optRootDir = d})
                   "DIR")
           "Project root directory for TOOL\n\
           \Usually the directory containing\n\
           \'stack.yaml' or 'cabal.project'\n\
           \(default: current directory)"
  , Option ['b'] ["build"]
           (ReqArg (\d o -> o {optBuildDir = Just d})
                   "DIR")
           "Build directory made by the TOOL\n\
           \(default:\n\
           \ - '.stack-work' for stack\n\
           \ - 'dist-newstyle' for cabal)"
  , Option ['X'] ["skip"]
           (ReqArg (\d o -> o {optSkipDirs = uncommas d ++ optSkipDirs o})
                   "DIR")
           "Basename of directory to skip while\n\
           \searching data for TOOL, can repeat"

  , Option ['f'] ["format"]
           (ReqArg (\s o -> o {optFormat = s})
                   "FMT")
           "Format of generated report\n\
           \'codecov', 'lcov', or 'cobertura'\n\
           \(default: codecov)"

  , Option [] ["expr-only"]
           (NoArg (\o -> o {optExprOnly = True}))
           "Count expressions only"
  , Option [] ["ignore-dittos"]
           (NoArg (\o -> o {optIgnoreDittos = True}))
           "Ignore consecutive entries with the\n\
           \same source code positions"

  , Option ['v'] ["verbose"]
           (NoArg (\o -> o {optVerbose = True}))
           "Show verbose output"
  , Option [] ["version"]
           (NoArg (\o -> o {optShowVersion = True}))
           "Show versoin and exit"
  , Option [] ["numeric-version"]
           (NoArg (\o -> o {optShowNumeric = True}))
           "Show numeric version and exit"
  , Option ['h'] ["help"]
           (NoArg (\o -> o {optShowHelp = True}))
           "Show this help and exit"
  ]

-- | Parse command line argument and return either error messages or
-- parsed 'Options'.
parseOptions :: [String] -- ^ Command line argument strings.
             -> Either [String] Options
parseOptions args =
  case getOpt Permute options args of
    (flags, rest, []) ->
      -- Not returning error messages with missing ".tix" file
      -- argument at this point, to show help and version messages
      -- without specifying ".tix" file.
      let opts0 = foldr ($) emptyOptions flags
          opts1 = fillDefaultsIfNotGiven opts0
      in  case rest of
            []      -> Right opts1
            (tix:_) -> Right (opts1 {optTix = tix})
    (_, _, errs)  -> Left errs

fillDefaultsIfNotGiven :: Options -> Options
fillDefaultsIfNotGiven opts = opts
  { optMixDirs = fillIf null optMixDirs
  , optSrcDirs = fillIf null optSrcDirs
  }
 where
   fillIf test fld =
      let orig = fld opts
      in  if test orig
             then fld defaultOptions
             else orig

-- | Representation of @TARGET@ argument.
data Target
  = TixFile FilePath
  | TestSuite BuildTool String

parseTarget :: String -> IO Target
parseTarget str = do
  -- Detecting file existence before separating with ':', to support
  -- directory path containing ':' under Windows.
  file_found <- doesFileExist str
  if file_found
     then pure $ TixFile str
     else case break (== ':') str of
       ("cabal", ':':name) -> pure $ TestSuite Cabal name
       ("stack", ':':name) -> pure $ TestSuite Stack name
       (tool, ':':_)       -> throwIO $ InvalidBuildTool tool
       _                   -> pure $ TixFile str

parseFormat :: String -> IO Format
parseFormat fmt = case fmt of
  "codecov"   -> pure Codecov
  "lcov"      -> pure Lcov
  "cobertura" -> pure Cobertura
  _           -> throwIO $ InvalidFormat fmt

uncommas :: String -> [String]
uncommas = go
  where
    go str = case break (== ',') str of
      (cs, ',':rest) -> cs : go rest
      (cs, _)        -> [cs]

-- | Make a 'Report' value from 'Optoins'.
opt2rpt :: Options -> IO Report
opt2rpt opt = do
  let rpt1 = mempty
        { reportMixDirs = optMixDirs opt
        , reportSrcDirs = optSrcDirs opt
        , reportExcludes = optExcludes opt
        , reportOutFile = optOutFile opt
        , reportVerbose = verbose
        , reportExprOnly = optExprOnly opt
        , reportIgnoreDittos = optIgnoreDittos opt
        }
      verbose = optVerbose opt
  format <- parseFormat (optFormat opt)
  target <- parseTarget (optTix opt)
  case target of
    TixFile path -> pure (rpt1 {reportTix = path
                               ,reportFormat = format})
    TestSuite tool name -> do
      rpt2 <- discover DiscoverArgs
        { da_tool = tool
        , da_testsuite = name
        , da_rootdir = optRootDir opt
        , da_builddir = optBuildDir opt
        , da_skipdirs = optSkipDirs opt
        , da_verbose = verbose
        }
      pure $ rpt1 `mappend` rpt2 {reportFormat = format}

-- | Print help messages.
printHelp :: IO ()
printHelp = do
  me <- getProgName
  is_terminal <- hIsTerminalDevice stdout
  putStrLn $ helpMessage is_terminal me

-- | Print version number of this package.
printVersion :: IO ()
printVersion = do
  me <- getProgName
  putStrLn (me ++ " version " ++ versionString)

-- | Print numeriv version number of this package.
printNumericVersion :: IO ()
printNumericVersion = putStrLn versionString

boldUnderline :: Bool -> String -> String
boldUnderline is_terminal str
  | is_terminal = "\ESC[1m\ESC[4m" ++ str ++ "\ESC[0m"
  | otherwise = str

bold :: Bool -> String -> String
bold is_terminal str
  | is_terminal = "\ESC[1m" ++ str ++ "\ESC[0m"
  | otherwise = str

-- | Help message for command line output.
helpMessage :: Bool -- ^ 'True' when showing in a terminal.
            -> String -- ^ Executable program name.
            -> String
helpMessage is_terminal name = usageInfo header options ++ footer
  where
    b = bold is_terminal
    bu = boldUnderline is_terminal
    header = "A tool to generate reports from .tix and .mix files\n\
\\n\
\" ++ bu "USAGE:" ++ " " ++ b name ++ " [OPTIONS] TARGET\n\
\\n\
\" ++ bu "ARGUMENTS:" ++ "\n\
\  <TARGET>  Either a path to a .tix file or a 'TOOL:TEST_SUITE'.\n\
\            Supported TOOL values are 'stack' and 'cabal'.\n\
\            When the TOOL is 'stack' and building a project with\n\
\            multiple packages, use 'all' as the TEST_SUITE value\n\
\            to specify the combined report.\n\
\\n\
\" ++ bu "OPTIONS:"
    footer = "\
\\n\
\For more info, see:\n\
\\n\
\  https://github.com/8c6794b6/hpc-codecov#readme\n\
\"

-- | String representation of the version number of this package.
versionString :: String
versionString = showVersion version