{-# LANGUAGE LambdaCase #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeApplications #-} module Main where import Test.Tasty import Test.Tasty.HUnit import Test.Tasty.ExpectedFailure import qualified Test.Tasty.Options as Tasty import qualified Test.Tasty.Ingredients as Tasty import qualified GHC as G import HIE.Bios import HIE.Bios.Ghc.Api import HIE.Bios.Ghc.Load import HIE.Bios.Cradle import HIE.Bios.Environment import HIE.Bios.Types import Control.Monad.IO.Class import Control.Monad ( forM_, unless ) import Data.List ( sort, isPrefixOf, isInfixOf ) import Data.Typeable import Data.Void import System.Directory import System.FilePath (addTrailingPathSeparator, makeRelative, () ) import System.Info.Extra ( isWindows ) import System.IO.Temp import System.Exit (ExitCode(ExitSuccess, ExitFailure)) import Control.Monad.Extra (unlessM) 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 = "8.10.7" extraGhc :: String extraGhc = "ghc-" ++ extraGhcVersion main :: IO () main = do writeStackYamlFiles stackDep <- checkToolIsAvailable "stack" cabalDep <- checkToolIsAvailable "cabal" extraGhcDep <- checkToolIsAvailable extraGhc defaultMainWithIngredients (ignoreToolTests:defaultIngredients) $ testGroup "Bios-tests" [ testGroup "Find cradle" [ testCaseSteps "simple-cabal" (findCradleForModule "./tests/projects/simple-cabal/B.hs" (Just "./tests/projects/simple-cabal/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. , testCaseSteps "simple-cabal-unknown-path" (findCradleForModule "./tests/projects/simple-cabal/Foo.hs" (Just "./tests/projects/simple-cabal/hie.yaml") ) ] , testGroup "Symlink" [ testCaseSteps "Can load base module" $ \step -> do withTempCopy "./tests/projects/symlink-test" $ \tmpdir -> do crdl <- initialiseCradle isMultiCradle (addTrailingPathSeparator tmpdir) step step "Load module A" withCurrentDirectory (cradleRootDir crdl) $ do runCradle (cradleOptsProg crdl) (const (pure ())) "./a/A.hs" >>= \case CradleSuccess r -> componentOptions r `shouldMatchList` ["a"] <> argDynamic _ -> assertFailure "Cradle could not be loaded" , testCaseSteps "Can load symlinked module" $ \step -> do withTempCopy "./tests/projects/symlink-test" $ \tmpdir -> do crdl <- initialiseCradle isMultiCradle (addTrailingPathSeparator tmpdir) step step "Attemp to load symlinked module A" withCurrentDirectory (cradleRootDir crdl) $ do createDirectoryLink "./a" "./b" unlessM (doesFileExist "./b/A.hs") $ assertFailure "Test invariant broken, this file must exist." runCradle (cradleOptsProg crdl) (const (pure ())) "./b/A.hs" >>= \case CradleSuccess r -> componentOptions r `shouldMatchList` ["b"] <> argDynamic _ -> assertFailure "Cradle could not be loaded" , testCaseSteps "Can not load symlinked module that is ignored" $ \step -> do withTempCopy "./tests/projects/symlink-test" $ \tmpdir -> do crdl <- initialiseCradle isMultiCradle (addTrailingPathSeparator tmpdir) step step "Attemp to load symlinked module A" withCurrentDirectory (cradleRootDir crdl) $ do createDirectoryLink "./a" "./c" unlessM (doesFileExist "./c/A.hs") $ assertFailure "Test invariant broken, this file must exist." runCradle (cradleOptsProg crdl) (const (pure ())) "./c/A.hs" >>= \case CradleNone -> pure () _ -> assertFailure "Cradle loaded symlink" ] , testGroup "Loading tests" [ testGroup "bios" biosTestCases , testGroup "direct" directTestCases , testGroupWithDependency cabalDep (cabalTestCases extraGhcDep) , ignoreOnGhc9AndNewer $ testGroupWithDependency stackDep stackTestCases ] ] linuxExlusiveTestCases :: [TestTree] linuxExlusiveTestCases | not isWindows = [ testCaseSteps "simple-bios" $ testDirectory isBiosCradle "./tests/projects/simple-bios" "B.hs" , testCaseSteps "simple-bios-ghc" $ testDirectory isBiosCradle "./tests/projects/simple-bios-ghc" "B.hs" , testCaseSteps "simple-bios-deps" $ testLoadCradleDependencies isBiosCradle "./tests/projects/simple-bios" "B.hs" (assertEqual "dependencies" ["hie-bios.sh", "hie.yaml"]) , testCaseSteps "simple-bios-deps-new" $ testLoadCradleDependencies isBiosCradle "./tests/projects/deps-bios-new" "B.hs" (assertEqual "dependencies" ["hie-bios.sh", "hie.yaml"]) ] | otherwise = [] cabalTestCases :: ToolDependency -> [TestTree] cabalTestCases extraGhcDep = [ testCaseSteps "failing-cabal" $ testDirectoryFail isCabalCradle "./tests/projects/failing-cabal" "MyLib.hs" (\CradleError {..} -> do cradleErrorExitCode @?= ExitFailure 1 cradleErrorDependencies `shouldMatchList` ["failing-cabal.cabal", "cabal.project", "cabal.project.local"]) , testCaseSteps "simple-cabal" $ testDirectory isCabalCradle "./tests/projects/simple-cabal" "B.hs" , testCaseSteps "nested-cabal" $ testLoadCradleDependencies isCabalCradle "./tests/projects/nested-cabal" "sub-comp/Lib.hs" (\deps -> deps `shouldMatchList` ["sub-comp" "sub-comp.cabal", "cabal.project", "cabal.project.local"] ) , testCaseSteps "nested-cabal2" $ testLoadCradleDependencies isCabalCradle "./tests/projects/nested-cabal" "MyLib.hs" (\deps -> deps `shouldMatchList` ["nested-cabal.cabal", "cabal.project", "cabal.project.local"] ) , testCaseSteps "multi-cabal" {- tests if both components can be loaded -} $ testDirectory isCabalCradle "./tests/projects/multi-cabal" "app/Main.hs" >> testDirectory isCabalCradle "./tests/projects/multi-cabal" "src/Lib.hs" , testCaseSteps "monorepo-cabal" {- issue https://github.com/mpickering/hie-bios/issues/200 -} $ testDirectory isCabalCradle "./tests/projects/monorepo-cabal" "A/Main.hs" >> testDirectory isCabalCradle "./tests/projects/monorepo-cabal" "B/MyLib.hs" , testGroup "Implicit cradle tests" $ [ testCaseSteps "implicit-cabal" $ testImplicitCradle "./tests/projects/implicit-cabal" "Main.hs" Cabal , testCaseSteps "implicit-cabal-no-project" $ testImplicitCradle "./tests/projects/implicit-cabal-no-project" "Main.hs" Cabal , testCaseSteps "implicit-cabal-deep-project" $ testImplicitCradle "./tests/projects/implicit-cabal-deep-project" "foo/Main.hs" Cabal ] , testGroupWithDependency extraGhcDep [ testCaseSteps "Appropriate ghc and libdir" $ \step -> do fp <- canonicalizePath "./tests/projects/cabal-with-ghc/src/MyLib.hs" crd <- initialiseCradle isCabalCradle fp step step "Get runtime GHC library directory" testGetGhcLibDir crd extraGhcVersion step "Get runtime GHC version" testGetGhcVersion crd extraGhcVersion ] ] stackTestCases :: [TestTree] stackTestCases = [ expectFailBecause "stack repl does not fail on an invalid cabal file" $ testCaseSteps "failing-stack" $ testDirectoryFail isStackCradle "./tests/projects/failing-stack" "src/Lib.hs" (\CradleError {..} -> do cradleErrorExitCode @?= ExitFailure 1 cradleErrorDependencies `shouldMatchList` ["failing-stack.cabal", "stack.yaml", "package.yaml"]) , testCaseSteps "simple-stack" $ testDirectory isStackCradle "./tests/projects/simple-stack" "B.hs" , testCaseSteps "multi-stack" {- tests if both components can be loaded -} $ testDirectory isStackCradle "./tests/projects/multi-stack" "app/Main.hs" >> testDirectory isStackCradle "./tests/projects/multi-stack" "src/Lib.hs" , testCaseSteps "nested-stack" $ testLoadCradleDependencies isStackCradle "./tests/projects/nested-stack" "sub-comp/Lib.hs" (\deps -> deps `shouldMatchList` ["sub-comp" "sub-comp.cabal", "sub-comp" "package.yaml", "stack.yaml"] ) , testCaseSteps "nested-stack2" $ testLoadCradleDependencies isStackCradle "./tests/projects/nested-stack" "MyLib.hs" (\deps -> deps `shouldMatchList` ["nested-stack.cabal", "package.yaml", "stack.yaml"] ) , testCaseSteps "stack-with-yaml" {- tests if both components can be loaded -} $ testDirectory isStackCradle "./tests/projects/stack-with-yaml" "app/Main.hs" >> testDirectory isStackCradle "./tests/projects/stack-with-yaml" "src/Lib.hs" , testCaseSteps "multi-stack-with-yaml" {- tests if both components can be loaded -} $ testDirectory isStackCradle "./tests/projects/multi-stack-with-yaml" "appA/src/Lib.hs" >> testDirectory isStackCradle "./tests/projects/multi-stack-with-yaml" "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 testCaseSteps "space stack" $ testDirectory isStackCradle "./tests/projects/space stack" "A.hs" >> testDirectory isStackCradle "./tests/projects/space stack" "B.hs" , testGroup "Implicit cradle tests" [ testCaseSteps "implicit-stack" $ testImplicitCradle "./tests/projects/implicit-stack" "Main.hs" Stack , testCaseSteps "implicit-stack-multi" $ testImplicitCradle "./tests/projects/implicit-stack-multi" "Main.hs" Stack >> testImplicitCradle "./tests/projects/implicit-stack-multi" "other-package/Main.hs" Stack ] ] biosTestCases :: [TestTree] biosTestCases = [ testCaseSteps "failing-bios" $ testDirectoryFail isBiosCradle "./tests/projects/failing-bios" "B.hs" (\CradleError {..} -> do cradleErrorExitCode @?= ExitFailure 1 cradleErrorDependencies `shouldMatchList` ["hie.yaml"]) , testCaseSteps "failing-bios-ghc" $ testGetGhcVersionFail isBiosCradle "./tests/projects/failing-bios-ghc" "B.hs" (\CradleError {..} -> do cradleErrorExitCode @?= ExitSuccess cradleErrorDependencies `shouldMatchList` [] length cradleErrorStderr @?= 1 "Couldn't execute myGhc" `isPrefixOf` head cradleErrorStderr @? "Error message should contain basic information" ) , testCaseSteps "simple-bios-shell" $ testDirectory isBiosCradle "./tests/projects/simple-bios-shell" "B.hs" , testCaseSteps "simple-bios-shell-deps" $ testLoadCradleDependencies isBiosCradle "./tests/projects/simple-bios-shell" "B.hs" (assertEqual "dependencies" ["hie.yaml"]) ] directTestCases :: [TestTree] directTestCases = [ testCaseSteps "simple-direct" $ testDirectory isDirectCradle "./tests/projects/simple-direct" "B.hs" , testCaseSteps "multi-direct" {- tests if both components can be loaded -} $ testDirectory isMultiCradle "./tests/projects/multi-direct" "A.hs" >> testDirectory isMultiCradle "./tests/projects/multi-direct" "B.hs" ] testDirectory :: (Cradle Void -> Bool) -> FilePath -> FilePath -> (String -> IO ()) -> IO () testDirectory cradlePred rootDir file step = -- We need to copy over the directory to somewhere outside the source tree -- when we test, since the cabal.project/stack.yaml/hie.yaml file in the root -- of this repository interferes with the test cradles! withTempCopy rootDir $ \rootDir' -> do fp <- canonicalizePath (rootDir' file) crd <- initialiseCradle cradlePred fp step step "Get runtime GHC library directory" testGetGhcLibDir crd VERSION_ghc step "Get runtime GHC version" testGetGhcVersion crd VERSION_ghc step "Initialise Flags" testLoadFile crd fp step -- | Here we are testing that the cradle's method of obtaining the ghcLibDir -- always works. testGetGhcLibDir :: Cradle a -> String -> IO () testGetGhcLibDir crd ghcVersion = do libDirRes <- getRuntimeGhcLibDir crd isSuccess libDirRes where -- heuristically test that the produced $libdir makes sense. isSuccess (CradleSuccess path) = ghcVersion `isInfixOf` path @? "Expected \"" ++ ghcVersion ++ "\" to be infix of: " ++ path isSuccess _ = assertFailure "Must succeed loading ghc lib directory" -- | Here we are testing that the cradle's method of getting the runtime ghc -- version is correct - which while testing, should be the version that we have -- built the tests with. This will fail if you compiled the tests with a ghc -- that doesn't equal the ghc on your path though :( testGetGhcVersion :: Cradle a -> String -> IO () testGetGhcVersion crd ghcVersion = do version <- getRuntimeGhcVersion crd version @?= CradleSuccess ghcVersion testGetGhcVersionFail :: (Cradle Void -> Bool) -> FilePath -> FilePath -> (CradleError -> Assertion) -> (String -> IO ()) -> IO () testGetGhcVersionFail cradlePred rootDir file cradleFailPred step = testCradle cradlePred rootDir file step $ \crd _ -> do res <- getRuntimeGhcVersion crd case res of CradleSuccess _ -> liftIO $ assertFailure "Cradle loaded successfully" CradleNone -> liftIO $ assertFailure "Unexpected none-Cradle" CradleFail crdlFail -> liftIO $ cradleFailPred crdlFail testDirectoryFail :: (Cradle Void -> Bool) -> FilePath -> FilePath -> (CradleError -> Assertion) -> (String -> IO ()) -> IO () testDirectoryFail cradlePred rootDir file cradleFailPred step = testCradle cradlePred rootDir file step $ \crd fp -> testLoadFileCradleFail crd fp cradleFailPred step testCradle :: (Cradle Void -> Bool) -> FilePath -> FilePath -> (String -> IO ()) -> (Cradle Void -> FilePath -> IO a) -> IO a testCradle cradlePred rootDir file step cont = withTempCopy rootDir $ \rootDir' -> do fp <- canonicalizePath (rootDir' file) crd <- initialiseCradle cradlePred fp step step "Initialise Flags" cont crd fp initialiseCradle :: (Cradle Void -> Bool) -> FilePath -> (String -> IO ()) -> IO (Cradle Void) initialiseCradle cradlePred a_fp step = do step $ "Finding Cradle for: " ++ a_fp mcfg <- findCradle a_fp step $ "Loading Cradle: " ++ show mcfg crd <- case mcfg of Just cfg -> loadCradle cfg Nothing -> loadImplicitCradle a_fp cradlePred crd @? "Must be the correct kind of cradle" pure crd testLoadFile :: Cradle a -> FilePath -> (String -> IO ()) -> IO () testLoadFile crd a_fp step = do libDirRes <- getRuntimeGhcLibDir crd handleCradleResult libDirRes $ \libDir -> withCurrentDirectory (cradleRootDir crd) $ G.runGhc (Just libDir) $ do let relFp = makeRelative (cradleRootDir crd) a_fp res <- initializeFlagsWithCradleWithMessage (Just (\_ n _ _ -> step (show n))) relFp crd handleCradleResult res $ \(ini, _) -> do liftIO (step "Initial module load") sf <- ini case sf of -- Test resetting the targets Succeeded -> setTargetFilesWithMessage (Just (\_ n _ _ -> step (show n))) [(a_fp, a_fp)] Failed -> liftIO $ assertFailure "Module loading failed" testLoadFileCradleFail :: Cradle a -> FilePath -> (CradleError -> Assertion) -> (String -> IO ()) -> IO () testLoadFileCradleFail crd a_fp cradleErrorExpectation step = do -- don't spin up a ghc session, just run the opts program manually since -- we're not guaranteed to be able to get the ghc libdir if the cradle is -- failing withCurrentDirectory (cradleRootDir crd) $ do let relFp = makeRelative (cradleRootDir crd) a_fp res <- runCradle (cradleOptsProg crd) (step . show) relFp case res of CradleSuccess _ -> liftIO $ assertFailure "Cradle loaded successfully" CradleNone -> liftIO $ assertFailure "Unexpected none-Cradle" CradleFail crdlFail -> liftIO $ cradleErrorExpectation crdlFail testLoadCradleDependencies :: (Cradle Void -> Bool) -> FilePath -> FilePath -> ([FilePath] -> Assertion) -> (String -> IO ()) -> IO () testLoadCradleDependencies cradlePred rootDir file dependencyPred step = withTempCopy rootDir $ \rootDir' -> do a_fp <- canonicalizePath (rootDir' file) crd <- initialiseCradle cradlePred a_fp step libDirRes <- getRuntimeGhcLibDir crd handleCradleResult libDirRes $ \libDir -> do step "Initialise Flags" withCurrentDirectory (cradleRootDir crd) $ G.runGhc (Just libDir) $ do let relFp = makeRelative (cradleRootDir crd) a_fp res <- initializeFlagsWithCradleWithMessage (Just (\_ n _ _ -> step (show n))) relFp crd handleCradleResult res $ \(_, options) -> liftIO $ dependencyPred (componentDependencies options) handleCradleResult :: MonadIO m => CradleLoadResult a -> (a -> m ()) -> m () handleCradleResult (CradleSuccess x) f = f x handleCradleResult CradleNone _ = liftIO $ assertFailure "Unexpected none-Cradle" handleCradleResult (CradleFail (CradleError _deps _ex stde)) _ = liftIO $ assertFailure ("Unexpected cradle fail" ++ unlines stde) findCradleForModule :: FilePath -> Maybe FilePath -> (String -> IO ()) -> IO () findCradleForModule fp expected' step = do expected <- maybe (return Nothing) (fmap Just . canonicalizePath) expected' a_fp <- canonicalizePath fp step "Finding cradle" crdl <- findCradle a_fp crdl @?= expected testImplicitCradle :: FilePath -> FilePath -> ActionName Void -> (String -> IO ()) -> IO () testImplicitCradle rootDir file expectedActionName step = withTempCopy rootDir $ \rootDir' -> do fp <- makeAbsolute (rootDir' file) step "Inferring implicit cradle" crd <- loadImplicitCradle fp :: IO (Cradle Void) actionName (cradleOptsProg crd) @?= expectedActionName expectedCradleRootDir <- makeAbsolute rootDir' cradleRootDir crd @?= expectedCradleRootDir step "Initialize flags" testLoadFile crd fp step withTempCopy :: FilePath -> (FilePath -> IO a) -> IO a withTempCopy srcDir f = withSystemTempDirectory "hie-bios-test" $ \newDir -> do copyDir srcDir newDir f newDir copyDir :: FilePath -> FilePath -> IO () copyDir src dst = do contents <- listDirectory src forM_ contents $ \file -> do unless (file `elem` ignored) $ do let srcFp = src file dstFp = dst file isDir <- doesDirectoryExist srcFp if isDir then createDirectory dstFp >> copyDir srcFp dstFp else copyFile srcFp dstFp where ignored = ["dist", "dist-newstyle", ".stack-work"] -- ------------------------------------------------------------------ -- Helper functions -- ------------------------------------------------------------------ shouldMatchList :: (Show a, Ord a) => [a] -> [a] -> Assertion shouldMatchList xs ys = sort xs @?= sort ys infix 1 `shouldMatchList` 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,0,1,0))) "nightly-2021-08-22" -- GHC 9.0.1 #elif (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(8,10,1,0))) "lts-18.6" -- GHC 8.10.4 #elif (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(8,8,1,0))) "lts-16.31" -- GHC 8.8.4 #elif (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(8,6,5,0))) "lts-14.27" -- GHC 8.6.5 #elif (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(8,6,4,0))) "lts-13.19" -- GHC 8.6.4 #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, Typeable) 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 test listing functionality ignoreToolTests :: Tasty.Ingredient ignoreToolTests = Tasty.TestManager [Tasty.Option (Proxy :: Proxy IgnoreToolDeps)] $ \opts _tree -> case Tasty.lookupOption opts of IgnoreToolDeps False -> Nothing IgnoreToolDeps True -> Just $ pure True -- ------------------------------------------------------------------ -- Ignore test group if built with GHC 9 or newer -- ------------------------------------------------------------------ ignoreOnGhc9AndNewer :: TestTree -> TestTree ignoreOnGhc9AndNewer tt = #if (defined(MIN_VERSION_GLASGOW_HASKELL) && MIN_VERSION_GLASGOW_HASKELL(9,0,0,0)) ignoreTestBecause "Not supported on GHC >= 9" #endif tt