packages feed

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

-- |
-- Module:     Trace.Hpc.Codecov.Discover
-- Copyright:  (c) 2021 8c6794b6
-- License:    BSD3
-- Maintainer: 8c6794b6 <8c6794b6@gmail.com>
--
-- Walk through directories and find hpc data.

module Trace.Hpc.Codecov.Discover
  ( -- * Discover function and types
    discover
  , DiscoverArgs(..)
  , BuildTool(..)

    -- * Auxiliary
  , foldDir
  , defaultIgnored
  , foldDirWithIgnoring
  ) where

-- base
import Control.Exception           (throwIO)
import Control.Monad               (when)
import Data.Maybe                  (isNothing)
import System.IO                   (hPutStrLn, stderr)

-- directory
import System.Directory            (doesDirectoryExist, doesFileExist,
                                    listDirectory)

-- filepath
import System.FilePath             (splitFileName, takeExtension,
                                    takeFileName, (<.>), (</>))

-- Internal
import Trace.Hpc.Codecov.Exception
import Trace.Hpc.Codecov.Report


-- ------------------------------------------------------------------------
--
-- Types
--
-- ------------------------------------------------------------------------

-- | Data type to hold arguments of 'discover' function.
data DiscoverArgs = DiscoverArgs
  { da_tool      :: BuildTool
    -- ^ Tool used to build Haskell cabal package.
  , da_testsuite :: String
    -- ^ Test suite name to search for @.tix@ file.
  , da_rootdir   :: FilePath
    -- ^ The project root directory.
  , da_builddir  :: Maybe String
    -- ^ Name of the temporary build directory made by the build tool.
  , da_skipdirs  :: [String]
    -- ^ Directories to skip while searching for scanning data.
  , da_verbose   :: Bool
    -- ^ Flag for shwoing verbose information.
  }

-- | Tool used for building Haskell package source codes.
data BuildTool
  = Cabal
  -- ^ For <https://www.haskell.org/cabal/index.html cabal-install>.
  | Stack
  -- ^ For <https://docs.haskellstack.org/en/stable/README/ stack>.
  deriving (Eq)

instance Show BuildTool where
  show tool = case tool of
    Cabal -> "cabal"
    Stack -> "stack"

-- | Walk thorugh directory and search for @.mix@ directories, Haskell
-- source code directories, and @.tix@ file.
discover :: DiscoverArgs -> IO Report
discover da = do
  let build_dir = case da_builddir da of
        Nothing  -> defaultBuildDirName (da_tool da)
        Just dir -> dir
      list_msg = concatMap (\p -> "    - " ++ p ++ "\n")
      skipped_dirs_msg =
        if null (da_skipdirs da)
           then "No directory specified to skip during discover"
           else "Skipping directories: " ++ unwords (da_skipdirs da)

  say da $
    "Starting discover for " ++ show (da_tool da) ++ "\n" ++
    "Scanning under \"" ++ da_rootdir da ++ "\"" ++
    " for .cabal files and \"" ++ build_dir ++ "\"\n" ++
    skipped_dirs_msg

  (src_dirs, build_dirs) <- findSrcDirsAndBuildDirs da build_dir
  say da $
    "Scanned:\n" ++
    "  Directories containing .cabal files:\n" ++ list_msg src_dirs ++
    "  Build dirs:\n" ++ list_msg build_dirs

  tix_path <- parseTixish (da_testsuite da)
  (mb_tix, mixs) <- findTixAndMix da tix_path build_dirs

  found_tix_path <- case mb_tix of
    Just tix -> pure tix
    Nothing  -> throwIO $ TestSuiteNotFound (da_testsuite da)

  say da $
    "Discovered:\n" ++
    "  Tix file: \n" ++ list_msg [found_tix_path] ++
    "  Mix dirs: \n" ++ list_msg mixs

  return $ mempty
    { reportTix = found_tix_path
    , reportMixDirs = mixs
    , reportSrcDirs = src_dirs
    }

data TixPath
  = UnresolvedTixPath FilePath
  | ResolvedTixPath FilePath

parseTixish :: String -> IO TixPath
parseTixish str = do
  let tix1 = if takeExtension str == ".tix"
                then str
                else str <.> "tix"
  tix1_found <- doesFileExist tix1
  if tix1_found
     then return $ ResolvedTixPath tix1
     else return $ UnresolvedTixPath tix1

defaultBuildDirName :: BuildTool -> String
defaultBuildDirName tool = case tool of
  Cabal -> "dist-newstyle"
  Stack -> ".stack-work"

-- | Show mssage to 'stdrr'.
say :: DiscoverArgs -> String -> IO ()
say da msg = when (da_verbose da) $ hPutStrLn stderr msg

findSrcDirsAndBuildDirs
  :: DiscoverArgs -> String -> IO ([FilePath], [FilePath])
findSrcDirsAndBuildDirs da build_dir = do
    ds <- if null $ da_rootdir da
        then listDirectory "."
        else pure [da_rootdir da]
    foldDirWithIgnoring ignored f z ds
  where
    z = ([], [])
    f p acc@(src_dirs, dirs)
      | takeExtension p_file == ".cabal" = pure (p_dir:src_dirs, dirs)
      | p_file == build_dir  = pure (src_dirs, p:dirs)
      | otherwise = pure acc
      where
        (p_dir, p_file) = splitFileName p
    ignored = build_dir : (defaultIgnored ++ da_skipdirs da)

findTixAndMix
  :: DiscoverArgs -> TixPath -> [FilePath]
  -> IO (Maybe FilePath, [FilePath])
findTixAndMix da tixish build_dirs = case da_tool da of
  Stack -> findForStack excludes tixish build_dirs
  Cabal -> findForCabal excludes tixish build_dirs
  where
    excludes = defaultIgnored ++ da_skipdirs da


-- ------------------------------------------------------------------------
--
-- Searching mix and tix for stack
--
-- ------------------------------------------------------------------------

findForStack
  :: [String] -> TixPath -> [FilePath] -> IO (Maybe FilePath, [FilePath])
findForStack excludes tx dirs = do
  mb_tix <- case tx of
    ResolvedTixPath path   -> pure $ Just path
    UnresolvedTixPath name -> findStackTix excludes name dirs
  mixs <- findStackMix excludes dirs
  pure (mb_tix, mixs)

findStackMix :: [String] -> [FilePath] -> IO [FilePath]
findStackMix ignored dirs = foldDirWithIgnoring ignored f [] dist_dirs
  where
    dist_dirs = map (</> "dist") dirs
    f p acc =
        pure $ if takeFileName p == "hpc"
                 then p : acc
                 else acc

findStackTix :: [String] -> String -> [FilePath] -> IO (Maybe FilePath)
findStackTix ignored tix_name dirs = go f Nothing install_dirs
  where
    go = foldDirWithIgnoring ignored
    install_dirs = map (</> "install") dirs
    f _ (Just tix) = return (Just tix)
    f p Nothing = if takeFileName p == tix_name
        then return (Just p)
        else return Nothing


-- ------------------------------------------------------------------------
--
-- Searching mix and tix for cabal-install
--
-- ------------------------------------------------------------------------

findForCabal
  :: [String] -> TixPath -> [FilePath] -> IO (Maybe FilePath, [FilePath])
findForCabal ignored tx = foldDirWithIgnoring ignored f z
  where
    f = case tx of
      ResolvedTixPath _ -> findVanilla ignored
      UnresolvedTixPath tix_name -> \p acc@(mb_tix, dirs) -> do
        if isNothing mb_tix && takeFileName p == tix_name
          then return (Just p, dirs)
          else findVanilla ignored p acc

    z = case tx of
      ResolvedTixPath path -> (Just path, [])
      _                    -> (Nothing, [])

findVanilla
  :: [String] -> FilePath -> (Maybe FilePath, [FilePath])
  -> IO (Maybe FilePath, [FilePath])
findVanilla ignored p acc@(mb_tix, dirs) = do
  if takeFileName p == "vanilla"
    then do
      let mix = p </> "mix"
      mix_exist <- doesDirectoryExist mix
      if mix_exist
         then do
           let f xs = [mix </> x| x <- xs, x `notElem` ignored]
           contents <- f <$> listDirectory mix
           -- From cabal-install 3.12, the 'mix' directory itself is
           -- used as the root directory of .mix files.
           return (mb_tix, mix : contents ++ dirs)
         else return (mb_tix, dirs)
    else return acc


-- ------------------------------------------------------------------------
--
-- Simple directory walker
--
-- ------------------------------------------------------------------------

-- | Variant of 'foldDirWithIgnoring' with 'defaultIgnored'.
foldDir :: (FilePath -> a -> IO a) -> a -> [FilePath] -> IO a
foldDir = foldDirWithIgnoring defaultIgnored

-- | Default directory base names to ignore.
defaultIgnored :: [String]
defaultIgnored = [".git", ".github"]

-- | Fold under given directory.
foldDirWithIgnoring
  :: [String]
  -- ^ Directory base names to skip.
  -> (FilePath -> a -> IO a)
  -- ^ Accumulator function.
  -> a
  -- ^ Initial accumulator value.
  -> [FilePath]
  -- ^ Directories to walk through.
  -> IO a
foldDirWithIgnoring ignored f = go
  where
    go acc0 [] = return acc0
    go acc0 (dir:dirs) = do
      acc1 <- f dir acc0
      if takeFileName dir `elem` ignored
         then go acc1 dirs
         else do
           is_dir <- doesDirectoryExist dir
           if not is_dir
             then go acc1 dirs
             else do
               contents <- map (dir </>) <$> listDirectory dir
               acc2 <- go acc1 contents
               go acc2 dirs