packages feed

hie-bios-0.19.0: tests/BiosTests.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeApplications #-}
module Main where

import Utils

import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.ExpectedFailure
import qualified Test.Tasty.Ingredients as Tasty
import qualified Test.Tasty.Options     as Tasty
import qualified Test.Tasty.Runners     as Tasty
import HIE.Bios
import HIE.Bios.Cradle
import HIE.Bios.Cradle.Cabal (cabalBuildDir)
import Control.Monad (forM_)
import Control.Monad.Extra (unlessM)
import Control.Monad.IO.Class
import Data.Foldable (for_)
import Data.List ( sort, isPrefixOf )
import Data.Typeable
import System.Exit (ExitCode(ExitSuccess, ExitFailure))
import System.Directory
import System.FilePath ((</>), makeRelative)
import System.Info.Extra (isWindows)
import System.IO (BufferMode (LineBuffering), hSetBuffering, stderr, stdout)
import qualified HIE.Bios.Ghc.Gap as Gap


argDynamic :: [String]
argDynamic = ["-dynamic" | Gap.hostIsDynamic]

-- | This ghc version is assumed to be tested by CI to validate
-- the "with-compiler" field is honoured by hie-bios.
--
-- If you change this version, make sure to also update 'cabal.project'
-- in 'tests\/projects\/cabal-with-ghc'.
extraGhcVersion :: String
extraGhcVersion = "9.4.8"

extraGhc :: String
extraGhc = "ghc-" ++ extraGhcVersion

-- | To get all logs, run the tests via:
--
-- @
-- cabal run test:bios-tests --test-options="--debug"
-- @
--
-- or
--
-- @
-- TASTY_DEBUG=TRUE cabal test test:bios-tests
-- @
--
-- to avoid recompilation.
main :: IO ()
main = do
  for_ [stderr, stdout] (`hSetBuffering` LineBuffering)
  writeStackYamlFiles
  stackDep <- checkToolIsAvailable "stack"
  cabalDep <- checkToolIsAvailable "cabal"
  extraGhcDep <- checkToolIsAvailable extraGhc

  defaultMainWithIngredients (ignoreToolTests:verboseLogging:defaultIngredients) $
    -- Run tests sequentially on Windows, to avoid issues with locking of the
    -- package database, e.g. errors of the form:
    --   package.db/package.cache.lock: openBinaryFile: resource busy (file is locked)
    (if isWindows then localOption (Tasty.NumThreads 1) else id) $
    testGroup "Bios-tests"
      [ testGroup "Find cradle" findCradleTests
      , testGroup "Symlink" symbolicLinkTests
      , testGroup "Loading tests"
        [ testGroup "bios" biosTestCases
        , testGroup "direct" directTestCases
        , testGroupWithDependency cabalDep (cabalTestCases extraGhcDep)
        , ignoreOnUnsupportedGhc $ testGroupWithDependency stackDep stackTestCases
        ]
      ]

symbolicLinkTests :: [TestTree]
symbolicLinkTests =
  [ biosTestCase "Can load base module" $ runTestEnv "./symlink-test" $ do
      initCradle "doesNotExist.hs"
      assertCradle isMultiCradle
      step "Attempt to load symlinked module A"
      do
        loadComponentOptions "./a/A.hs"
        assertComponentOptions $ \opts ->
          componentOptions opts `shouldMatchList` ["a"] <> argDynamic

  , biosTestCase "Can load symlinked module" $ runTestEnv "./symlink-test" $ do
      initCradle "doesNotExist.hs"
      assertCradle isMultiCradle
      step "Attempt to load symlinked module A"
      do
        cradle <- askCradle
        let rooted = (cradleRootDir cradle </>)
        liftIO $ createDirectoryLink (rooted "a") (rooted "./b")
        liftIO $ unlessM (doesFileExist $ rooted "b/A.hs") $
          assertFailure "Test invariant broken, this file must exist."
        loadComponentOptions "./b/A.hs"
        assertComponentOptions $ \opts ->
          componentOptions opts `shouldMatchList` ["b"] <> argDynamic

  , biosTestCase "Can not load symlinked module that is ignored" $ runTestEnv "./symlink-test" $ do
      initCradle "doesNotExist.hs"
      assertCradle isMultiCradle
      step "Attempt to load symlinked module A"
      do
        cradle <- askCradle
        let rooted = (cradleRootDir cradle </>)
        liftIO $ createDirectoryLink (rooted "./a") (rooted "./c")
        liftIO $ unlessM (doesFileExist $ rooted "c/A.hs") $
          assertFailure "Test invariant broken, this file must exist."
        loadComponentOptions "./c/A.hs"
        assertLoadNone
  ]

biosTestCases :: [TestTree]
biosTestCases =
  [ biosTestCase "failing-bios" $ runTestEnv "./failing-bios" $ do
      initCradle "B.hs"
      assertCradle isBiosCradle
      loadComponentOptions "B.hs"
      assertCradleError $ \CradleError {..} -> do
        cradleErrorExitCode @?= ExitFailure 1
        cradleErrorDependencies `shouldMatchList` ["hie.yaml"]
  , biosTestCase "failing-bios-ghc" $ runTestEnv "./failing-bios-ghc" $ do
      initCradle "B.hs"
      assertCradle isBiosCradle
      loadRuntimeGhcVersion
      ghcVersionLR <- askGhcVersionResult
      assertCradleLoadError ghcVersionLR >>= \CradleError {..} -> liftIO $ do
        cradleErrorExitCode @?= ExitSuccess
        cradleErrorDependencies `shouldMatchList` []
        length cradleErrorStderr @?= 1
        forM_ cradleErrorStderr $ \errorCtx ->
          -- On windows, this error message contains '"' around the executable name
          if isWindows
            then "Couldn't execute \"myGhc\"" `isPrefixOf` errorCtx @? "Error message should contain error information"
            else "Couldn't execute myGhc"     `isPrefixOf` errorCtx @? "Error message should contain error information"
  , biosTestCase "simple-bios-shell" $ runTestEnv "./simple-bios-shell" $ do
      testDirectoryM isBiosCradle "B.hs"
  , biosTestCase "simple-bios-shell-deps" $ runTestEnv "./simple-bios-shell" $ do
      biosCradleDeps "B.hs" ["hie.yaml"]
  ] <> concat [linuxTestCases | False] -- TODO(fendor), enable again
  where
    biosCradleDeps :: FilePath -> [FilePath] -> TestM ()
    biosCradleDeps fp deps = do
      initCradle fp
      assertCradle isBiosCradle
      loadComponentOptions fp
      assertComponentOptions $ \opts -> do
        deps @?= componentDependencies opts

    linuxTestCases =
      [ biosTestCase "simple-bios" $ runTestEnv "./simple-bios" $
          testDirectoryM isBiosCradle "B.hs"
      , biosTestCase "simple-bios-ghc" $ runTestEnv "./simple-bios-ghc" $
          testDirectoryM isBiosCradle  "B.hs"
      , biosTestCase "simple-bios-deps" $ runTestEnv "./simple-bios" $ do
          biosCradleDeps "B.hs" ["hie-bios.sh", "hie.yaml"]
      , biosTestCase "simple-bios-deps-new" $ runTestEnv "./deps-bios-new" $ do
          biosCradleDeps "B.hs" ["hie-bios.sh", "hie.yaml"]
      ]

cabalTestCases :: ToolDependency -> [TestTree]
cabalTestCases extraGhcDep =
  [
    biosTestCase "failing-cabal" $ runTestEnv "./failing-cabal" $ do
      cabalAttemptLoad "MyLib.hs"
      assertCradleError (\CradleError {..} -> do
        cradleErrorExitCode @?= ExitFailure 1
        cradleErrorDependencies `shouldMatchList` ["failing-cabal.cabal", "cabal.project", "cabal.project.local"])
  , biosTestCase "failing-cabal-multi-repl-with-shrink-error-files" $ runTestEnv "./failing-multi-repl-cabal-project" $ do
      cabalAttemptLoadFiles "multi-repl-cabal-fail/app/Main.hs" ["multi-repl-cabal-fail/src/Lib.hs", "multi-repl-cabal-fail/src/Fail.hs", "NotInPath.hs"]
      root <- askRoot
      multiSupported <- isCabalMultipleCompSupported'
      if multiSupported
        then
          assertCradleError (\CradleError {..} -> do
            cradleErrorExitCode @?= ExitFailure 1
            cradleErrorDependencies `shouldMatchList` ["cabal.project","cabal.project.local","multi-repl-cabal-fail.cabal"]
            -- NotInPath.hs does not match the cradle for `app/Main.hs`, so it should not be tried.
            (makeRelative root <$> cradleErrorLoadingFiles) `shouldMatchList` ["multi-repl-cabal-fail/app/Main.hs","multi-repl-cabal-fail/src/Fail.hs","multi-repl-cabal-fail/src/Lib.hs"])
        else assertLoadSuccess >>= \ComponentOptions {} -> do
          return ()
  , biosTestCase "simple-cabal" $ runTestEnv "./simple-cabal" $ do
      testDirectoryM isCabalCradle "B.hs"
  , biosTestCase "build-dir" $ runTestEnv "./simple-cabal" $ do
      initCradle "B.hs"
      assertCradle isCabalCradle
      root <- askRoot
      buildDir <- liftIO $ cabalBuildDir root
      -- use --multi-repl, as that was the codepath with the bug
      loadFileGhcMultiStyle "B.hs" []
      liftIO $ do
        -- Check we aren't trampling over dist-newstyle
        distNewstyleExists <- doesDirectoryExist (root </> "dist-newstyle")
        assertBool "dist-newstyle was created" (not distNewstyleExists)
        -- Check we are using the correct build directory
        buildDirExists <- doesDirectoryExist buildDir
        assertBool "build dir does not exist" buildDirExists
  , biosTestCase "nested-cabal" $ runTestEnv "./nested-cabal" $ do
      cabalAttemptLoad "sub-comp/Lib.hs"
      assertComponentOptions $ \opts -> do
        componentDependencies opts `shouldMatchList`
          [ "sub-comp" </> "sub-comp.cabal"
          , "cabal.project"
          , "cabal.project.local"
          ]
  , biosTestCase "nested-cabal2" $ runTestEnv "./nested-cabal" $ do
      cabalAttemptLoad "MyLib.hs"
      assertComponentOptions $ \opts -> do
        componentDependencies opts `shouldMatchList`
          [ "nested-cabal.cabal"
          , "cabal.project"
          , "cabal.project.local"
          ]
  , biosTestCase "nested-cabal multi-mode includes enclosing deps for extra files" $ runTestEnv "./nested-cabal" $ do
      -- Initialize cradle first, since capability checks use the current cradle.
      initCradle "sub-comp/Lib.hs"
      assertCradle isCabalCradle
      multiSupported <- isCabalMultipleCompSupported'
      if multiSupported
        then do
          loadComponentOptionsMultiStyle "sub-comp/Lib.hs" ["MyLib.hs"]
          assertComponentOptions $ \opts -> do
            -- Expect both the main component's cabal file and the enclosing cabal for the extra file,
            -- plus project files.
            componentDependencies opts `shouldMatchList`
              [ "sub-comp" </> "sub-comp.cabal"
              , "nested-cabal.cabal"
              , "cabal.project"
              , "cabal.project.local"
              ]
        else do
          -- On older cabal/ghc combos, multi-repl isn't supported; just ensure load succeeds.
          loadComponentOptions "sub-comp/Lib.hs"
          _ <- assertLoadSuccess
          pure ()
  , biosTestCase "nested-cabal multi-mode includes enclosing deps when extra file is subcomp" $ runTestEnv "./nested-cabal" $ do
      -- Initialize cradle at the top level, then treat the sub-component file as an extra file.
      initCradle "MyLib.hs"
      assertCradle isCabalCradle
      multiSupported <- isCabalMultipleCompSupported'
      if multiSupported
        then do
          loadComponentOptionsMultiStyle "MyLib.hs" ["sub-comp/Lib.hs"]
          assertComponentOptions $ \opts -> do
            componentDependencies opts `shouldMatchList`
              [ "nested-cabal.cabal"
              , "sub-comp" </> "sub-comp.cabal"
              , "cabal.project"
              , "cabal.project.local"
              ]
        else do
          loadComponentOptions "MyLib.hs"
          _ <- assertLoadSuccess
          pure ()
  , biosTestCase "multi-cabal" $ runTestEnv "./multi-cabal" $ do
      {- tests if both components can be loaded -}
      testDirectoryM isCabalCradle "app/Main.hs"
      testDirectoryM isCabalCradle "src/Lib.hs"
  , {- issue https://github.com/mpickering/hie-bios/issues/200 -}
    biosTestCase "monorepo-cabal" $ runTestEnv "./monorepo-cabal" $ do
      testDirectoryM isCabalCradle "A/Main.hs"
      testDirectoryM isCabalCradle "B/MyLib.hs"
  , testGroup "Implicit cradle tests" $
      [ biosTestCase "implicit-cabal" $ runTestEnv "./implicit-cabal" $ do
          testImplicitDirectoryM isCabalCradle "Main.hs"
      , biosTestCase "implicit-cabal-no-project" $ runTestEnv "./implicit-cabal-no-project" $ do
          testImplicitDirectoryM isCabalCradle "Main.hs"
      , biosTestCase "implicit-cabal-deep-project" $ runTestEnv "./implicit-cabal-deep-project" $ do
          testImplicitDirectoryM isCabalCradle "foo/Main.hs"
      ]
  , testGroupWithDependency extraGhcDep
    [ biosTestCase "Appropriate ghc and libdir" $ runTestEnvLocal "./cabal-with-ghc" $ do
        initCradle "src/MyLib.hs"
        assertCradle isCabalCradle
        loadRuntimeGhcLibDir
        assertLibDirVersionIs extraGhcVersion
        loadRuntimeGhcVersion
        assertGhcVersionIs extraGhcVersion
        step "Find Component Options"
        loadComponentOptions "src/MyLib.hs"
        _ <- assertLoadSuccess
        pure ()
    ]
  , testGroup "Cabal cabalProject"
    [ biosTestCase "cabal-with-project, options propagated" $ runTestEnv "cabal-with-project" $ do
        opts <- cabalLoadOptions "src/MyLib.hs"
        liftIO $ do
          "-O2" `elem` componentOptions opts
            @? "Options must contain '-O2'"
    , biosTestCase "cabal-with-project, load" $ runTestEnv "cabal-with-project" $ do
        testDirectoryM isCabalCradle "src/MyLib.hs"
    , biosTestCase "multi-cabal-with-project, options propagated" $ runTestEnv "multi-cabal-with-project" $ do
        optsAppA <- cabalLoadOptions "appA/src/Lib.hs"
        liftIO $ do
          "-O2" `elem` componentOptions optsAppA
            @? "Options must contain '-O2'"
        optsAppB <- cabalLoadOptions "appB/src/Lib.hs"
        liftIO $ do
          "-O2" `notElem` componentOptions optsAppB
            @? "Options must not contain '-O2'"
    , biosTestCase "multi-cabal-with-project, load" $ runTestEnv "multi-cabal-with-project" $ do
        testDirectoryM isCabalCradle "appB/src/Lib.hs"
        testDirectoryM isCabalCradle "appB/src/Lib.hs"
    , testGroupWithDependency extraGhcDep
      [ biosTestCase "Honours extra ghc setting" $ runTestEnv "cabal-with-ghc-and-project" $ do
          initCradle "src/MyLib.hs"
          assertCradle isCabalCradle
          loadRuntimeGhcLibDir
          assertLibDirVersionIs extraGhcVersion
          loadRuntimeGhcVersion
          assertGhcVersionIs extraGhcVersion
          step "Find Component Options"
          loadComponentOptions "src/MyLib.hs"
          _ <- assertLoadSuccess
          pure ()
      ]
    , biosTestCase "force older Cabal version in custom setup" $ runTestEnv "cabal-with-custom-setup" $ do
        -- Specifically tests whether cabal 3.16 works as expected with
        -- an older lib:Cabal version that doesn't support '--with-repl'.
        -- This test doesn't hurt for other cases as well, so we enable it for
        -- all configurations.
        testDirectoryM isCabalCradle "src/MyLib.hs"
    , biosTestCase "force older Cabal version in custom setup with multi mode" $ runTestEnv "cabal-with-custom-setup" $ do
        -- Specifically tests whether cabal 3.16 works as expected with
        -- an older lib:Cabal version that doesn't support '--with-repl'.
        -- This test doesn't hurt for other cases as well, so we enable it for
        -- all configurations.
        let target = "src/MyLib.hs"
        initCradle target
        assertCradle isCabalCradle
        loadRuntimeGhcLibDir
        assertLibDirVersion
        loadRuntimeGhcVersion
        assertGhcVersion
        -- suffices to force loading cabal's `--enable-multi-repl` codepath
        loadFileGhcMultiStyle target []
    ]
  ]
  where
    cabalAttemptLoad :: FilePath -> TestM ()
    cabalAttemptLoad fp = do
      initCradle fp
      assertCradle isCabalCradle
      loadComponentOptions fp

    cabalAttemptLoadFiles :: FilePath -> [FilePath] -> TestM ()
    cabalAttemptLoadFiles fp fps = do
      initCradle fp
      assertCradle isCabalCradle
      loadComponentOptionsMultiStyle fp fps

    cabalLoadOptions :: FilePath -> TestM ComponentOptions
    cabalLoadOptions fp = do
      initCradle fp
      assertCradle isCabalCradle
      loadComponentOptions fp
      assertLoadSuccess

stackTestCases :: [TestTree]
stackTestCases =
  [ expectFailBecause "stack repl does not fail on an invalid cabal file" $
      biosTestCase "failing-stack" $ runTestEnv "./failing-stack" $ do
        stackAttemptLoad "src/Lib.hs"
        assertCradleError $ \CradleError {..} -> do
            cradleErrorExitCode @?= ExitFailure 1
            cradleErrorDependencies `shouldMatchList` ["failing-stack.cabal", "stack.yaml", "package.yaml"]
  , biosTestCase "simple-stack" $ runTestEnv "./simple-stack" $ do
      testDirectoryM isStackCradle "B.hs"
  , biosTestCase "multi-stack" $ runTestEnv "./multi-stack" $ do {- tests if both components can be loaded -}
      testDirectoryM isStackCradle "app/Main.hs"
      testDirectoryM isStackCradle "src/Lib.hs"
  , biosTestCase "nested-stack" $ runTestEnv "./nested-stack" $ do
      stackAttemptLoad "sub-comp/Lib.hs"
      assertComponentOptions $ \opts ->
        componentDependencies opts `shouldMatchList` ["sub-comp" </> "sub-comp.cabal", "sub-comp" </> "package.yaml", "stack.yaml"]
  , biosTestCase "nested-stack2" $ runTestEnv "./nested-stack" $ do
      stackAttemptLoad "MyLib.hs"
      assertComponentOptions $ \opts ->
        componentDependencies opts `shouldMatchList` ["nested-stack.cabal", "package.yaml", "stack.yaml"]
  , biosTestCase "stack-with-yaml" $ runTestEnv "./stack-with-yaml" $ do
      {- tests if both components can be loaded -}
      testDirectoryM isStackCradle "app/Main.hs"
      testDirectoryM isStackCradle "src/Lib.hs"
  , biosTestCase "multi-stack-with-yaml" $ runTestEnv "./multi-stack-with-yaml" $ do
      {- tests if both components can be loaded -}
      testDirectoryM isStackCradle "appA/src/Lib.hs"
      testDirectoryM isStackCradle "appB/src/Lib.hs"
  ,
    -- Test for special characters in the path for parsing of the ghci-scripts.
    -- Issue https://github.com/mpickering/hie-bios/issues/162
    biosTestCase "space stack" $ runTestEnv "./space stack" $ do
      testDirectoryM isStackCradle "A.hs"
      testDirectoryM isStackCradle "B.hs"
  , testGroup "Implicit cradle tests"
      [ biosTestCase "implicit-stack" $ runTestEnv "./implicit-stack" $
          testImplicitDirectoryM isStackCradle "Main.hs"
      , biosTestCase "implicit-stack-multi" $ runTestEnv "./implicit-stack-multi" $ do
          testImplicitDirectoryM isStackCradle "Main.hs"
          testImplicitDirectoryM isStackCradle "other-package/Main.hs"
      ]
  ]
  where
    stackAttemptLoad :: FilePath -> TestM ()
    stackAttemptLoad fp = do
      initCradle fp
      assertCradle isStackCradle
      loadComponentOptions fp

directTestCases :: [TestTree]
directTestCases =
  [ biosTestCase "simple-direct" $ runTestEnv  "./simple-direct" $ do
      testDirectoryM isDirectCradle "B.hs"
  , biosTestCase "multi-direct" $ runTestEnv "./multi-direct" $ do
      {- tests if both components can be loaded -}
      testDirectoryM isMultiCradle "A.hs"
      testDirectoryM isMultiCradle "B.hs"
  ]

findCradleTests :: [TestTree]
findCradleTests =
  [ cradleFileTest "Simple Existing File" "./simple-cabal"  "B.hs" (Just "hie.yaml")
  -- Checks if we can find a hie.yaml even when the given filepath
  -- is unknown. This functionality is required by Haskell IDE Engine.
  , cradleFileTest "Existing File" "cabal-with-ghc" "src/MyLib.hs" (Just "hie.yaml")
  , cradleFileTest "Non-existing file" "cabal-with-ghc" "src/MyLib2.hs" (Just "hie.yaml")
  , cradleFileTest "Non-existing file 2" "cabal-with-ghc" "MyLib2.hs" (Just "hie.yaml")
  , cradleFileTest "Directory 1" "cabal-with-ghc" "src/" (Just "hie.yaml")
  , cradleFileTest "Directory 2" "simple-cabal" "" (Just "hie.yaml")
  -- Unknown directory in a project, ought to work as well.
  , cradleFileTest "Directory 3" "simple-cabal" "src/" (Just "hie.yaml")
  , cradleFileTest "Directory does not exist" "doesnotexist" "A.hs" Nothing
  ]
  where
    cradleFileTest :: String -> FilePath -> FilePath -> Maybe FilePath -> TestTree
    cradleFileTest testName dir fpTarget result = biosTestCase testName $ do
      runTestEnv dir $ do
        findCradleForModuleM fpTarget result

-- ------------------------------------------------------------------
-- Unit-test Helper functions
-- ------------------------------------------------------------------

shouldMatchList :: (Show a, Ord a) => [a] -> [a] -> Assertion
shouldMatchList xs ys = sort xs @?= sort ys

infix 1 `shouldMatchList`

biosTestCase :: TestName -> (Bool -> Assertion) -> TestTree
biosTestCase name assertion = askOption @VerboseLogging (\case
  VerboseLogging verbose -> testCase name (assertion verbose)
  )

-- ------------------------------------------------------------------
-- Stack related helper functions
-- ------------------------------------------------------------------

writeStackYamlFiles :: IO ()
writeStackYamlFiles =
  forM_ stackProjects $ \(proj, syaml, pkgs) ->
    writeFile (proj </> syaml) (stackYaml stackYamlResolver pkgs)

stackProjects :: [(FilePath, FilePath, [FilePath])]
stackProjects =
  [ ("tests" </> "projects" </> "multi-stack", "stack.yaml", ["."])
  , ("tests" </> "projects" </> "failing-stack", "stack.yaml", ["."])
  , ("tests" </> "projects" </> "simple-stack", "stack.yaml", ["."])
  , ("tests" </> "projects" </> "nested-stack", "stack.yaml", [".", "./sub-comp"])
  , ("tests" </> "projects" </> "space stack", "stack.yaml", ["."])
  , ("tests" </> "projects" </> "implicit-stack", "stack.yaml", ["."])
  , ("tests" </> "projects" </> "implicit-stack-multi", "stack.yaml", ["."])
  , ("tests" </> "projects" </> "implicit-stack-multi", "stack.yaml", ["."])
  , ("tests" </> "projects" </> "multi-stack-with-yaml", "stack-alt.yaml", ["appA", "appB"])
  , ("tests" </> "projects" </> "stack-with-yaml", "stack-alt.yaml", ["."])
  ]

stackYaml :: String -> [FilePath] -> String
stackYaml resolver pkgs = unlines
  $ ["resolver: " ++ resolver, "packages:"]
  ++ map ("- " ++) pkgs

stackYamlResolver :: String
stackYamlResolver =
#if (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(9,12,0,0)))
  "nightly-2025-08-07" -- GHC 9.12.2
#elif (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(9,10,0,0)))
  "lts-24.3" -- GHC 9.10.2
#elif (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(9,8,0,0)))
  "lts-23.19" -- GHC 9.8.4
#elif (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(9,6,0,0)))
  "lts-22.44" -- GHC 9.6.7
#elif (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(9,4,0,0)))
  "lts-21.25" -- GHC 9.4.8
#elif (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(9,2,0,0)))
  "lts-20.26" -- GHC 9.2.8
#endif

-- ------------------------------------------------------------------
-- Most tests have some run-time tool dependencies.
-- We only want to run tests if these tools are available.
-- ------------------------------------------------------------------

data ToolDependency = ToolDependency
  { toolName :: String
  , toolExists :: Bool
  }

checkToolIsAvailable :: String -> IO ToolDependency
checkToolIsAvailable f = do
  exists <- maybe False (const True) <$> findExecutable f
  pure ToolDependency
    { toolName = f
    , toolExists = exists
    }

testGroupWithDependency :: ToolDependency -> [TestTree] -> TestTree
testGroupWithDependency td tc = askOption @IgnoreToolDeps (\case
  IgnoreToolDeps ignoreToolDep
    | ignoreToolDep || toolExists td -> tg
    | otherwise -> itg
  )
  where
    tg = testGroup (toolName td) tc

    itg =
      ignoreTestBecause
        ("These tests require that the following" ++
        " tool can be found on the path: " ++ toolName td)
        tg

-- ------------------------------------------------------------------
-- Run test-suite ignoring run-time tool dependencies.
-- Can be used to force CI to run the whole test-suite.
-- Makes sure that the full test-suite is being run on a properly configured
-- environment.
-- ------------------------------------------------------------------

-- | This option, when set to 'True', specifies that we should run in the
-- «list tests» mode
newtype IgnoreToolDeps = IgnoreToolDeps Bool
  deriving (Eq, Ord)

instance Tasty.IsOption IgnoreToolDeps where
  defaultValue = IgnoreToolDeps False
  parseValue = fmap IgnoreToolDeps . Tasty.safeReadBool
  optionName = pure "ignore-tool-deps"
  optionHelp = pure "Run tests whether their tool dependencies exist or not"
  optionCLParser = Tasty.flagCLParser Nothing (IgnoreToolDeps True)

-- | The ingredient that provides the "ignore missing run-time dependencies" functionality
ignoreToolTests :: Tasty.Ingredient
ignoreToolTests = Tasty.TestManager [Tasty.Option (Proxy :: Proxy IgnoreToolDeps)] $
  \_opts _tree -> Nothing

newtype VerboseLogging = VerboseLogging Bool

instance Tasty.IsOption VerboseLogging where
  defaultValue = VerboseLogging False
  parseValue = fmap VerboseLogging . Tasty.safeReadBool
  optionName = pure "debug"
  optionHelp = pure "Run the tests with verbose logging"
  optionCLParser = Tasty.flagCLParser Nothing (VerboseLogging True)

-- | The ingredient that provides the "ignore missing run-time dependencies" functionality
verboseLogging :: Tasty.Ingredient
verboseLogging = Tasty.TestManager [Tasty.Option (Proxy :: Proxy VerboseLogging)] $
  \_opts _tree -> Nothing

-- ------------------------------------------------------------------
-- Ignore test group if not supported by any stackage snapshot
-- ------------------------------------------------------------------

ignoreOnUnsupportedGhc :: TestTree -> TestTree
ignoreOnUnsupportedGhc tt =
#if (defined(MIN_VERSION_GLASGOW_HASKELL) && MIN_VERSION_GLASGOW_HASKELL(9,14,0,0))
  ignoreTestBecause "Not supported on GHC 9.14"
#endif
    tt