{- | Module : Main Description : Integration tests for hcovguard executable Copyright : (c) Trevis Elser, 2026 License : MIT Maintainer : oss@treviselser.com -} module Main ( main ) where import qualified Data.ByteString.Lazy.Char8 as LBS8 import qualified Data.List as List import qualified System.Directory as Directory import qualified System.Environment as Environment import qualified System.Exit as Exit import qualified System.Process as Process import qualified Test.Tasty as Tasty import qualified Test.Tasty.Golden as Golden import qualified Test.Tasty.HUnit as HUnit import qualified Test.Tasty.Program as TastyProgram main :: IO () main = do mExe <- getHcovguardExecutable case mExe of Nothing -> Tasty.defaultMain . HUnit.testCase "hcovguard executable" $ HUnit.assertFailure "HCOVGUARD_EXE environment variable must be set to the path of the hcovguard executable" Just exe -> do exists <- Directory.doesFileExist exe if not exists then Tasty.defaultMain . HUnit.testCase "hcovguard executable" . HUnit.assertFailure $ "HCOVGUARD_EXE points to non-existent file: " <> exe else Tasty.defaultMain (tests exe) tests :: FilePath -> Tasty.TestTree tests exe = Tasty.testGroup "Integration" [ Tasty.testGroup "CLI" [ TastyProgram.testProgram "help flag exits successfully" exe ["--help"] Nothing , TastyProgram.testProgram "version flag exits successfully" exe ["--version"] Nothing , TastyProgram.testProgram "render-man-page flag exits successfully" exe ["--render-man-page"] Nothing ] , Tasty.testGroup "Coverage checks" [ testCaseExitCode "passes when coverage meets thresholds" exe [ "-c" , "test/integration/fixtures/config-pass.toml" , "-t" , "test/integration/fixtures/example.tix" , "-m" , "test/integration/fixtures/.hpc" ] Exit.ExitSuccess , testCaseExitCode "fails when coverage below thresholds" exe [ "-c" , "test/integration/fixtures/config-fail.toml" , "-t" , "test/integration/fixtures/example.tix" , "-m" , "test/integration/fixtures/.hpc" ] (Exit.ExitFailure 1) , testCaseExitCode "ignores modules when configured" exe [ "-c" , "test/integration/fixtures/config-ignore.toml" , "-t" , "test/integration/fixtures/example.tix" , "-m" , "test/integration/fixtures/.hpc" ] Exit.ExitSuccess , testCaseExitCode "matches modules with patterns" exe [ "-c" , "test/integration/fixtures/config-pattern.toml" , "-t" , "test/integration/fixtures/example.tix" , "-m" , "test/integration/fixtures/.hpc" ] Exit.ExitSuccess , testCaseExitCode "passes with maximumUncovered when coverage is good" exe [ "-c" , "test/integration/fixtures/config-max-pass.toml" , "-t" , "test/integration/fixtures/example.tix" , "-m" , "test/integration/fixtures/.hpc" ] Exit.ExitSuccess , testCaseExitCode "fails with maximumUncovered when too many uncovered" exe [ "-c" , "test/integration/fixtures/config-max-fail.toml" , "-t" , "test/integration/fixtures/example.tix" , "-m" , "test/integration/fixtures/.hpc" ] (Exit.ExitFailure 1) , testCaseExitCode "passes with combined min and max thresholds" exe [ "-c" , "test/integration/fixtures/config-combined.toml" , "-t" , "test/integration/fixtures/example.tix" , "-m" , "test/integration/fixtures/.hpc" ] Exit.ExitSuccess , testCaseExitCodeWithCwd "auto-discovers mix directories" exe [ "-c" , "config-pass.toml" , "-t" , "example.tix" , "--auto-discover" ] "test/integration/fixtures" Exit.ExitSuccess ] , Tasty.testGroup "Error handling" [ testCaseExitCode "fails when tix file not found" exe [ "-c" , "test/integration/fixtures/config-pass.toml" , "-t" , "test/integration/fixtures/nonexistent.tix" , "-m" , "test/integration/fixtures/.hpc" ] (Exit.ExitFailure 1) , testCaseExitCode "fails when both module and pattern specified" exe [ "-c" , "test/integration/fixtures/config-invalid-both.toml" , "-t" , "test/integration/fixtures/example.tix" , "-m" , "test/integration/fixtures/.hpc" ] (Exit.ExitFailure 1) , testCaseExitCode "fails when neither module nor pattern specified" exe [ "-c" , "test/integration/fixtures/config-invalid-neither.toml" , "-t" , "test/integration/fixtures/example.tix" , "-m" , "test/integration/fixtures/.hpc" ] (Exit.ExitFailure 1) , testCaseExitCode "fails when no mix dirs and no auto-discover" exe [ "-c" , "test/integration/fixtures/config-pass.toml" , "-t" , "test/integration/fixtures/example.tix" ] (Exit.ExitFailure 1) ] , Tasty.testGroup "Warnings" [ testCaseStderrContains "warns on unmatched pattern" exe [ "-c" , "test/integration/fixtures/config-unmatched.toml" , "-t" , "test/integration/fixtures/example.tix" , "-m" , "test/integration/fixtures/.hpc" ] "[Hcovguard-8451]" ] , Tasty.testGroup "Output verification" [ testGoldenStdout "threshold failure output" exe [ "-c" , "test/integration/fixtures/config-fail.toml" , "-t" , "test/integration/fixtures/example.tix" , "-m" , "test/integration/fixtures/.hpc" ] "test/integration/golden/config-fail.stdout.golden" , testGoldenStdout "maximum uncovered failure output" exe [ "-c" , "test/integration/fixtures/config-max-fail.toml" , "-t" , "test/integration/fixtures/example.tix" , "-m" , "test/integration/fixtures/.hpc" ] "test/integration/golden/config-max-fail.stdout.golden" , testGoldenStderr "invalid config: both module and pattern" exe [ "-c" , "test/integration/fixtures/config-invalid-both.toml" , "-t" , "test/integration/fixtures/example.tix" , "-m" , "test/integration/fixtures/.hpc" ] "test/integration/golden/config-invalid-both.stderr.golden" , testGoldenStderr "invalid config: neither module nor pattern" exe [ "-c" , "test/integration/fixtures/config-invalid-neither.toml" , "-t" , "test/integration/fixtures/example.tix" , "-m" , "test/integration/fixtures/.hpc" ] "test/integration/golden/config-invalid-neither.stderr.golden" , testGoldenStderr "tix file not found" exe [ "-c" , "test/integration/fixtures/config-pass.toml" , "-t" , "test/integration/fixtures/nonexistent.tix" , "-m" , "test/integration/fixtures/.hpc" ] "test/integration/golden/tix-not-found.stderr.golden" , testGoldenStderr "no mix dirs specified" exe [ "-c" , "test/integration/fixtures/config-pass.toml" , "-t" , "test/integration/fixtures/example.tix" ] "test/integration/golden/no-mix-dirs.stderr.golden" , testGoldenStderr "unmatched pattern warning" exe [ "-c" , "test/integration/fixtures/config-unmatched.toml" , "-t" , "test/integration/fixtures/example.tix" , "-m" , "test/integration/fixtures/.hpc" ] "test/integration/golden/unmatched-pattern.stderr.golden" ] ] -- | Get the hcovguard executable path from environment variable getHcovguardExecutable :: IO (Maybe FilePath) getHcovguardExecutable = Environment.lookupEnv "HCOVGUARD_EXE" -- | Test a program invocation and check its exit code testCaseExitCode :: String -> FilePath -> [String] -> Exit.ExitCode -> Tasty.TestTree testCaseExitCode name exe args expectedExitCode = HUnit.testCase name $ do (exitCode, _, _) <- Process.readProcessWithExitCode exe args "" exitCode HUnit.@?= expectedExitCode -- | Test a program invocation with a custom working directory testCaseExitCodeWithCwd :: String -> FilePath -> [String] -> FilePath -> Exit.ExitCode -> Tasty.TestTree testCaseExitCodeWithCwd name exe args workDir expectedExitCode = HUnit.testCase name $ do let process = (Process.proc exe args){Process.cwd = Just workDir} (exitCode, _, _) <- Process.readCreateProcessWithExitCode process "" exitCode HUnit.@?= expectedExitCode -- | Golden test comparing stdout output against a golden file testGoldenStdout :: String -> FilePath -> [String] -> FilePath -> Tasty.TestTree testGoldenStdout name exe args goldenFile = Golden.goldenVsStringDiff name (\ref new -> ["diff", "-u", ref, new]) goldenFile (fmap (LBS8.pack . snd3) (Process.readProcessWithExitCode exe args "")) -- | Golden test comparing stderr output against a golden file testGoldenStderr :: String -> FilePath -> [String] -> FilePath -> Tasty.TestTree testGoldenStderr name exe args goldenFile = Golden.goldenVsStringDiff name (\ref new -> ["diff", "-u", ref, new]) goldenFile (fmap (LBS8.pack . thd3) (Process.readProcessWithExitCode exe args "")) -- | Extract the second element of a triple snd3 :: (a, b, c) -> b snd3 (_, b, _) = b -- | Extract the third element of a triple thd3 :: (a, b, c) -> c thd3 (_, _, c) = c -- | Test that stderr contains a specific substring testCaseStderrContains :: String -> FilePath -> [String] -> String -> Tasty.TestTree testCaseStderrContains name exe args expectedSubstr = HUnit.testCase name $ do (_, _, stderr) <- Process.readProcessWithExitCode exe args "" HUnit.assertBool ("Expected stderr to contain: " <> expectedSubstr <> "\nActual stderr: " <> stderr) (expectedSubstr `List.isInfixOf` stderr)