tilia-0.0.1.0: tests/Tilia/PackageSpec.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Information obtained from @.cabal@ files.
module Tilia.PackageSpec (spec) where
import Data.List (isSuffixOf, sort)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import GHC.LanguageExtensions.Type (Extension (..))
import System.Directory (createDirectoryIfMissing, removeFile)
import System.FilePath ((</>))
import System.IO.Temp (withSystemTempDirectory)
import Test.Hspec
import Tilia.Package (PackageProblem (..), newPackageReader)
spec :: Spec
spec = do
describe "the component a file belongs to" $ do
it "is the one whose source directory holds it" $
inPackage twoComponents ["src", "test"] $ \root -> do
library <- asked (root </> "src" </> "M.hs")
suite <- asked (root </> "test" </> "S.hs")
(has ImportQualifiedPost library, has ImportQualifiedPost suite)
`shouldBe` (True, False)
it "settles the extensions separately for each" $
inPackage twoComponents ["src", "test"] $ \root -> do
library <- asked (root </> "src" </> "M.hs")
suite <- asked (root </> "test" </> "S.hs")
(has OverloadedStrings library, has OverloadedStrings suite)
`shouldBe` (False, True)
it "is none of them when the file is outside every source directory" $
inPackage twoComponents ["src", "test", "scratch"] $ \root ->
(unclaimed <$> asked (root </> "scratch" </> "X.hs"))
`shouldReturn` True
it "is the package's own directory when it names no source directory" $
inPackage besideTheCabalFile [] $ \root ->
(has ImportQualifiedPost <$> asked (root </> "M.hs"))
`shouldReturn` True
it "is the nearer one when a wider component covers it as well" $
inPackage overTheWholeTree ["tests"] $ \root ->
(has ImportQualifiedPost <$> asked (root </> "tests" </> "S.hs"))
`shouldReturn` True
it "is still the wider one for a file only it covers" $
inPackage overTheWholeTree ["tests"] $ \root ->
(has ImportQualifiedPost <$> asked (root </> "M.hs"))
`shouldReturn` False
describe "the extensions a component puts in force" $ do
it "are the language edition's" $
inPackage twoComponents ["src"] $ \root -> do
library <- asked (root </> "src" </> "M.hs")
(length <$> library) `shouldSatisfy` either (const False) (> 40)
it "can be turned off again by default-extensions" $
inPackage refusesAnEdition ["src"] $ \root ->
(has ImportQualifiedPost <$> asked (root </> "src" </> "M.hs"))
`shouldReturn` False
describe "a file nothing can be settled for" $ do
it "says so when there is no package above it" $
withSystemTempDirectory "tilia-nopackage" $ \root ->
asked (root </> "M.hs")
`shouldReturn` Left NoPackageFile
it "says so, and how, when the package does not parse" $
inPackage "library\n hs-source-dirs\n" [] $ \root ->
asked (root </> "M.hs") >>= \case
Left (PackageMalformed file complaints) -> do
file `shouldSatisfy` (("demo.cabal" `isSuffixOf`))
complaints `shouldSatisfy` not . null
other -> expectationFailure ("expected a parse failure, got " <> show other)
describe "a reader kept between files" $ do
it "answers as a fresh one would" $
inPackage twoComponents ["src", "test"] $ \root -> do
ask <- newPackageReader
kept <- traverse ask (modules root)
fresh <- traverse asked (modules root)
kept `shouldBe` fresh
it "reads a package once, not once per file" $
inPackage twoComponents ["src"] $ \root -> do
ask <- newPackageReader
first <- ask (root </> "src" </> "A.hs")
removeFile (root </> "demo.cabal")
ask (root </> "src" </> "B.hs") `shouldReturn` first
it "remembers every directory the walk went through" $
inPackage twoComponents ["src" </> "deep"] $ \root -> do
ask <- newPackageReader
deep <- ask (root </> "src" </> "deep" </> "A.hs")
removeFile (root </> "demo.cabal")
ask (root </> "src" </> "B.hs") `shouldReturn` deep
it "is what makes those pass, and not the file surviving" $
inPackage twoComponents ["src"] $ \root -> do
removeFile (root </> "demo.cabal")
(unreadableOrMissing <$> asked (root </> "src" </> "A.hs"))
`shouldReturn` True
----------------------------------------------------------------------------
-- The packages the tests are run against
-- | A library on @GHC2021@ and a test suite on @Haskell2010@, so that the
-- two disagree about everything worth asking.
twoComponents :: Text
twoComponents =
T.unlines
[ "cabal-version: 2.4",
"name: demo",
"version: 0",
"",
"library",
" exposed-modules: M",
" hs-source-dirs: src",
" default-language: GHC2021",
"",
"test-suite spec",
" type: exitcode-stdio-1.0",
" main-is: S.hs",
" hs-source-dirs: test",
" default-language: Haskell2010",
" default-extensions: OverloadedStrings"
]
-- | A library that names no @hs-source-dirs@, so its modules sit beside the
-- @.cabal@ file.
besideTheCabalFile :: Text
besideTheCabalFile =
T.unlines
[ "cabal-version: 2.4",
"name: demo",
"version: 0",
"",
"library",
" exposed-modules: M",
" default-language: GHC2021"
]
-- | A library that spreads over the whole tree, and a suite inside it.
--
-- The library names no @hs-source-dirs@ and so takes the package
-- directory, which holds the test suite's directory as well as its own
-- modules.
overTheWholeTree :: Text
overTheWholeTree =
T.unlines
[ "cabal-version: 2.4",
"name: demo",
"version: 0",
"",
"library",
" exposed-modules: M",
" default-language: Haskell2010",
"",
"test-suite spec",
" type: exitcode-stdio-1.0",
" main-is: S.hs",
" hs-source-dirs: tests",
" default-language: GHC2021"
]
-- | An edition, and then one of the things it brings taken back out.
refusesAnEdition :: Text
refusesAnEdition =
T.unlines
[ "cabal-version: 2.4",
"name: demo",
"version: 0",
"",
"library",
" exposed-modules: M",
" hs-source-dirs: src",
" default-language: GHC2021",
" default-extensions: NoImportQualifiedPost"
]
----------------------------------------------------------------------------
-- Running one
-- | Write a @.cabal@ file and the given directories, and hand back the root.
inPackage :: Text -> [FilePath] -> (FilePath -> IO a) -> IO a
inPackage contents dirs use =
withSystemTempDirectory "tilia-package" $ \root -> do
T.writeFile (root </> "demo.cabal") contents
mapM_ (createDirectoryIfMissing True . (root </>)) (sort dirs)
use root
-- | Ask about one file with a reader of its own, which is what a test that
-- is not about caching wants.
asked :: FilePath -> IO (Either PackageProblem [Extension])
asked path = do
ask <- newPackageReader
ask path
-- | One module in each of the two components.
modules :: FilePath -> [FilePath]
modules root = [root </> "src" </> "M.hs", root </> "test" </> "S.hs"]
unreadableOrMissing :: Either PackageProblem [Extension] -> Bool
unreadableOrMissing = either (const True) (const False)
has :: Extension -> Either PackageProblem [Extension] -> Bool
has e = either (const False) (e `elem`)
unclaimed :: Either PackageProblem [Extension] -> Bool
unclaimed = either isFileUnclaimed (const False)
where
isFileUnclaimed = \case
FileUnclaimed _ -> True
_ -> False