{-# LANGUAGE QuasiQuotes #-} {- | Module : HCovGuard.Coverage.Calculator Description : Coverage data types and calculation functions. Copyright : (c) Trevis Elser, 2026 License : MIT Maintainer : oss@treviselser.com This is just about building the coverage information only. It is only about what the current state, not what that state should be. -} module HCovGuard.Coverage.Calculator ( CoverageCounts (..) , CoverageStats (..) , ModuleCoverage (..) , calculateModuleCoverage , showModuleCoverage ) where import qualified Data.List as List import qualified Data.String.Interpolate as Interpolate import qualified Data.Text as Text import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Builder as Builder import qualified Data.Text.Lazy.Builder.Int as Builder import qualified Trace.Hpc.Mix as HpcMix import qualified Trace.Hpc.Tix as HpcTix data CoverageCounts = CoverageCounts { countsTotal :: !Int , countsCovered :: !Int } data CoverageStats = CoverageStats { expressionCoverage :: !CoverageCounts , topLevelCoverage :: !CoverageCounts , alternativeCoverage :: !CoverageCounts , localCoverage :: !CoverageCounts } data ModuleCoverage = ModuleCoverage { moduleName :: !Text.Text , moduleStats :: !CoverageStats } calculateModuleCoverage :: HpcTix.TixModule -> HpcMix.Mix -> ModuleCoverage calculateModuleCoverage (HpcTix.TixModule name _hash _count ticks) (HpcMix.Mix _file _time _mixHash _tabStop entries) = let stats = List.foldl' (flip countEntry) emptyStats (zip entries ticks) in ModuleCoverage { moduleName = Text.pack (stripPackagePrefix name) , moduleStats = stats } stripPackagePrefix :: String -> String stripPackagePrefix name = case dropWhile (/= '/') name of '/' : rest -> rest _ -> name emptyStats :: CoverageStats emptyStats = CoverageStats { expressionCoverage = CoverageCounts 0 0 , topLevelCoverage = CoverageCounts 0 0 , alternativeCoverage = CoverageCounts 0 0 , localCoverage = CoverageCounts 0 0 } {-# INLINE countEntry #-} countEntry :: (HpcMix.MixEntry, Integer) -> CoverageStats -> CoverageStats countEntry ((_pos, boxLabel), count) stats = case boxLabel of HpcMix.ExpBox _ -> stats{expressionCoverage = addCount (expressionCoverage stats) count} HpcMix.TopLevelBox _ -> stats{topLevelCoverage = addCount (topLevelCoverage stats) count} HpcMix.LocalBox _ -> stats{localCoverage = addCount (localCoverage stats) count} HpcMix.BinBox _ _ -> stats{alternativeCoverage = addCount (alternativeCoverage stats) count} {-# INLINE addCount #-} addCount :: CoverageCounts -> Integer -> CoverageCounts addCount (CoverageCounts total covered) count = CoverageCounts { countsTotal = total + 1 , countsCovered = if count > 0 then covered + 1 else covered } showModuleCoverage :: ModuleCoverage -> Text.Text showModuleCoverage modCov = let name = moduleName modCov stats = moduleStats modCov in Text.unlines [ [Interpolate.i|Module: |] <> name , [Interpolate.i| Expression: |] <> showCoverageCounts (expressionCoverage stats) , [Interpolate.i| Top-level: |] <> showCoverageCounts (topLevelCoverage stats) , [Interpolate.i| Alternative:|] <> showCoverageCounts (alternativeCoverage stats) , [Interpolate.i| Local: |] <> showCoverageCounts (localCoverage stats) ] showCoverageCounts :: CoverageCounts -> Text.Text showCoverageCounts (CoverageCounts total covered) = TL.toStrict . Builder.toLazyText $ Builder.decimal covered <> Builder.singleton '/' <> Builder.decimal total <> Builder.fromText [Interpolate.i| covered|]