tilia-0.0.1.0: tests/Tilia/Fixity/CacheSpec.hs
{-# LANGUAGE OverloadedStrings #-}
-- | The on-disk cache of what was read out of a package.
module Tilia.Fixity.CacheSpec (spec) where
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import System.Directory (getModificationTime, setModificationTime)
import System.Environment (setEnv, unsetEnv)
import System.FilePath ((</>))
import System.IO.Temp (withSystemTempDirectory)
import Test.Hspec
import Tilia.Fixity
import Tilia.Fixity.Cache
import Tilia.Fixity.PackageDb (Installed (..), InstalledPackage (..))
spec :: Spec
spec = do
tokens
database
around withIsolatedCache $ do
describe "modules" $ do
it "remembers a package's module list" $ \cache -> do
storeModules cache "thing-1.0-abc" ["A.B", "C"]
cachedModules cache "thing-1.0-abc" `shouldReturn` Just ["A.B", "C"]
it "knows nothing about a package it was never told about" $ \cache ->
cachedModules cache "absent-1.0" `shouldReturn` Nothing
it "remembers an empty list as a fact, not as absence" $ \cache -> do
storeModules cache "empty-1.0" []
cachedModules cache "empty-1.0" `shouldReturn` Just []
describe "fixities" $ do
it "round-trips every direction" $ \cache -> do
let fixities =
Map.fromList
[ ((InTerms, OpName "<+>"), Fixity LeftAssoc 6),
((InTerms, OpName ">>="), Fixity RightAssoc 1),
((InTerms, OpName "==="), Fixity NoAssoc 4)
]
storeFixities cache "thing-1.0" "A.B" (Declares fixities)
cachedFixities cache "thing-1.0" "A.B" `shouldReturn` Just (Declares fixities)
it "round-trips the extremes of precedence" $ \cache -> do
let fixities =
Map.fromList
[ ((InTerms, OpName "!"), Fixity LeftAssoc 0),
((InTerms, OpName "?"), Fixity LeftAssoc 9),
((InTerms, OpName "->"), Fixity RightAssoc (-1))
]
storeFixities cache "thing-1.0" "Edges" (Declares fixities)
cachedFixities cache "thing-1.0" "Edges" `shouldReturn` Just (Declares fixities)
it "remembers that a module declares nothing" $ \cache -> do
storeFixities cache "thing-1.0" "Quiet" (Declares Map.empty)
cachedFixities cache "thing-1.0" "Quiet" `shouldReturn` Just (Declares Map.empty)
it "remembers that a module could not be read" $ \cache -> do
storeFixities cache "thing-1.0" "Opaque" (Unreadable Nothing)
cachedFixities cache "thing-1.0" "Opaque" `shouldReturn` Just (Unreadable Nothing)
it "remembers which module below it stopped the reading" $ \cache -> do
storeFixities cache "thing-1.0" "Opaque" (Unreadable (Just "Deep.Down"))
cachedFixities cache "thing-1.0" "Opaque"
`shouldReturn` Just (Unreadable (Just "Deep.Down"))
it "tells one stopped below it from one stopped on its own account" $ \cache -> do
storeFixities cache "thing-1.0" "Blamed" (Unreadable (Just "Deep.Down"))
storeFixities cache "thing-1.0" "Itself" (Unreadable Nothing)
blamed <- cachedFixities cache "thing-1.0" "Blamed"
itself <- cachedFixities cache "thing-1.0" "Itself"
(blamed, itself)
`shouldBe` (Just (Unreadable (Just "Deep.Down")), Just (Unreadable Nothing))
it "tells an unread module from one it was never told about" $ \cache -> do
storeFixities cache "thing-1.0" "Opaque" (Unreadable Nothing)
unread <- cachedFixities cache "thing-1.0" "Opaque"
never <- cachedFixities cache "thing-1.0" "Absent"
(unread, never) `shouldBe` (Just (Unreadable Nothing), Nothing)
it "tells an unread module from one that declares nothing" $ \cache -> do
storeFixities cache "thing-1.0" "Opaque" (Unreadable Nothing)
storeFixities cache "thing-1.0" "Quiet" (Declares Map.empty)
opaque <- cachedFixities cache "thing-1.0" "Opaque"
quiet <- cachedFixities cache "thing-1.0" "Quiet"
(opaque, quiet) `shouldBe` (Just (Unreadable Nothing), Just (Declares Map.empty))
it "replaces an unread answer once the module can be read" $ \cache -> do
storeFixities cache "thing-1.0" "M" (Unreadable Nothing)
storeFixities cache "thing-1.0" "M" (Declares (Map.fromList [((InTerms, OpName "!"), Fixity LeftAssoc 9)]))
cachedFixities cache "thing-1.0" "M"
`shouldReturn` Just (Declares (Map.fromList [((InTerms, OpName "!"), Fixity LeftAssoc 9)]))
it "knows nothing about a module it was never told about" $ \cache ->
cachedFixities cache "thing-1.0" "Absent" `shouldReturn` Nothing
it "keeps packages apart" $ \cache -> do
let ops = Map.fromList [((InTerms, OpName "<>"), Fixity RightAssoc 6)]
storeFixities cache "a-1.0" "M" (Declares ops)
storeFixities cache "b-1.0" "M" (Declares Map.empty)
a <- cachedFixities cache "a-1.0" "M"
b <- cachedFixities cache "b-1.0" "M"
(a, b) `shouldBe` (Just (Declares ops), Just (Declares Map.empty))
it "treats a different hash in the key as a different package" $ \cache -> do
storeFixities cache "thing-1.0-aaaa" "M" (Declares (Map.fromList [((InTerms, OpName "!"), Fixity LeftAssoc 9)]))
cachedFixities cache "thing-1.0-bbbb" "M" `shouldReturn` Nothing
it "overwrites a previous answer for the same key" $ \cache -> do
storeFixities cache "thing-1.0" "M" (Declares (Map.fromList [((InTerms, OpName "!"), Fixity LeftAssoc 9)]))
storeFixities cache "thing-1.0" "M" (Declares (Map.fromList [((InTerms, OpName "!"), Fixity RightAssoc 3)]))
cachedFixities cache "thing-1.0" "M"
`shouldReturn` Just (Declares (Map.fromList [((InTerms, OpName "!"), Fixity RightAssoc 3)]))
describe "export names" $ do
it "round-trips the names an export list gave" $ \cache -> do
let names = Exports (Set.fromList [OpName "<+>", OpName ":|", OpName "f"])
storeExportNames cache "thing-1.0" "M" names
cachedExportNames cache "thing-1.0" "M" `shouldReturn` Just names
it "remembers a module that keeps its own counsel" $ \cache -> do
storeExportNames cache "thing-1.0" "M" Untellable
cachedExportNames cache "thing-1.0" "M" `shouldReturn` Just Untellable
it "tells one that keeps its own counsel from one never asked about" $ \cache -> do
storeExportNames cache "thing-1.0" "Quiet" Untellable
cachedExportNames cache "thing-1.0" "Quiet" `shouldReturn` Just Untellable
cachedExportNames cache "thing-1.0" "Unasked" `shouldReturn` Nothing
it "tells one that exports nothing from one that will not say" $ \cache -> do
storeExportNames cache "thing-1.0" "Bare" (Exports Set.empty)
storeExportNames cache "thing-1.0" "Quiet" Untellable
cachedExportNames cache "thing-1.0" "Bare"
`shouldReturn` Just (Exports Set.empty)
cachedExportNames cache "thing-1.0" "Quiet" `shouldReturn` Just Untellable
it "keeps packages apart" $ \cache -> do
storeExportNames cache "one-1.0" "M" (Exports (Set.singleton (OpName "<+>")))
storeExportNames cache "two-1.0" "M" (Exports (Set.singleton (OpName "<?>")))
cachedExportNames cache "one-1.0" "M"
`shouldReturn` Just (Exports (Set.singleton (OpName "<+>")))
it "keeps them apart from the fixities of the same module" $ \cache -> do
storeFixities cache "thing-1.0" "M" (Unreadable Nothing)
storeExportNames cache "thing-1.0" "M" (Exports (Set.singleton (OpName "<+>")))
cachedFixities cache "thing-1.0" "M" `shouldReturn` Just (Unreadable Nothing)
cachedExportNames cache "thing-1.0" "M"
`shouldReturn` Just (Exports (Set.singleton (OpName "<+>")))
it "overwrites a previous answer for the same key" $ \cache -> do
storeExportNames cache "thing-1.0" "M" Untellable
storeExportNames cache "thing-1.0" "M" (Exports (Set.singleton (OpName "<+>")))
cachedExportNames cache "thing-1.0" "M"
`shouldReturn` Just (Exports (Set.singleton (OpName "<+>")))
describe "what a name carries with it" $ do
it "round-trips what each name carries" $ \cache -> do
let kept =
Map.fromList
[ (OpName "NonEmpty", Set.fromList [OpName ":|"]),
(OpName "Seq", Set.fromList [OpName ":<|", OpName ":|>"])
]
storeChildren cache "thing-1.0" "M" kept
cachedChildren cache "thing-1.0" "M" `shouldReturn` Just kept
it "remembers a module that carries nothing anywhere" $ \cache -> do
storeChildren cache "thing-1.0" "Bare" Map.empty
cachedChildren cache "thing-1.0" "Bare" `shouldReturn` Just Map.empty
it "tells that from a module it was never told about" $ \cache -> do
storeChildren cache "thing-1.0" "Bare" Map.empty
cachedChildren cache "thing-1.0" "Unasked" `shouldReturn` Nothing
it "remembers a name that carries nothing among ones that do" $ \cache -> do
let kept =
Map.fromList
[ (OpName "Empty", Set.empty),
(OpName "NonEmpty", Set.singleton (OpName ":|"))
]
storeChildren cache "thing-1.0" "M" kept
cachedChildren cache "thing-1.0" "M" `shouldReturn` Just kept
it "keeps packages apart" $ \cache -> do
storeChildren cache "one-1.0" "M" (Map.singleton (OpName "T") (Set.singleton (OpName ":|")))
cachedChildren cache "two-1.0" "M" `shouldReturn` Nothing
describe "module names with dots" $
it "files a deeply qualified module without confusion" $ \cache -> do
storeFixities cache "thing-1.0" "A.B.C.D" (Declares (Map.fromList [((InTerms, OpName "%"), Fixity NoAssoc 5)]))
cachedFixities cache "thing-1.0" "A.B.C.D"
`shouldReturn` Just (Declares (Map.fromList [((InTerms, OpName "%"), Fixity NoAssoc 5)]))
-- | What the compiler can see, and what it takes to stop believing it.
--
-- The database stands in for @ghc-pkg@ here: what is under test is that a
-- change to it is noticed, not what @ghc-pkg@ would have said about it.
database :: Spec
database = around withIsolatedCache $ do
it "gives back what it was told, while the database sits still" $ \cache ->
withDatabase $ \db -> do
storeInstalled cache (Installed [containers] [db])
cachedInstalled cache `shouldReturn` Just [containers]
it "gives back nothing once a package has been registered" $ \cache ->
withDatabase $ \db -> do
storeInstalled cache (Installed [containers] [db])
writeFile (db </> "new-1.0.conf") ""
cachedInstalled cache `shouldReturn` Nothing
it "gives back nothing once the database is gone" $ \cache -> do
db <- withDatabase pure
storeInstalled cache (Installed [containers] [db])
cachedInstalled cache `shouldReturn` Nothing
it "remembers nothing it has no way to stop believing" $ \cache -> do
storeInstalled cache (Installed [containers] [])
cachedInstalled cache `shouldReturn` Nothing
it "gives back nothing to a token it was not written under" $ \_ ->
withIsolatedDirectory $ \dir ->
withDatabase $ \db -> do
before' <- open dir (PlanToken "one")
storeInstalled before' (Installed [containers] [db])
after' <- open dir (PlanToken "two")
cachedInstalled after' `shouldReturn` Nothing
it "gives it back under the token it was written under" $ \_ ->
withIsolatedDirectory $ \dir ->
withDatabase $ \db -> do
before' <- open dir (PlanToken "one")
storeInstalled before' (Installed [containers] [db])
again <- open dir (PlanToken "one")
cachedInstalled again `shouldReturn` Just [containers]
it "keeps one token's answer when another writes its own" $ \_ ->
withIsolatedDirectory $ \dir ->
withDatabase $ \db -> do
one <- open dir (PlanToken "one")
storeInstalled one (Installed [containers] [db])
two <- open dir (PlanToken "two")
storeInstalled two (Installed [quiet] [db])
cachedInstalled one `shouldReturn` Just [containers]
cachedInstalled two `shouldReturn` Just [quiet]
it "carries a package that exposes nothing" $ \cache ->
withDatabase $ \db -> do
storeInstalled cache (Installed [containers, quiet] [db])
cachedInstalled cache `shouldReturn` Just [containers, quiet]
where
containers =
InstalledPackage
{ ipName = "containers",
ipVersion = "0.7",
ipModules = ["Data.Map", "Data.Map.Strict", "Data.Set"],
ipImportDirs = ["/nowhere/containers-0.7"]
}
quiet =
InstalledPackage
{ ipName = "rts",
ipVersion = "1.0",
ipModules = [],
ipImportDirs = []
}
-- | A directory standing in for a package database, with a timestamp that
-- can be set rather than waited for.
withDatabase :: (FilePath -> IO a) -> IO a
withDatabase action =
withSystemTempDirectory "tilia-db" $ \db -> do
-- Something long ago, so that anything happening to the directory
-- afterwards is a change whatever the clock's resolution.
setModificationTime db =<< getModificationTime "/"
action db
-- | What an answer of \"could not be read\" is tied to, and what it is not.
tokens :: Spec
tokens = around withIsolatedDirectory $ do
it "does not offer an unread answer written under another token" $ \dir -> do
before' <- open dir (PlanToken "one")
storeFixities before' "thing-1.0" "M" (Unreadable Nothing)
after' <- open dir (PlanToken "two")
cachedFixities after' "thing-1.0" "M" `shouldReturn` Nothing
it "still offers one written under the same token" $ \dir -> do
before' <- open dir (PlanToken "one")
storeFixities before' "thing-1.0" "M" (Unreadable Nothing)
again <- open dir (PlanToken "one")
cachedFixities again "thing-1.0" "M" `shouldReturn` Just (Unreadable Nothing)
it "keeps an answer that was read, whatever the token" $ \dir -> do
let fixities = Map.fromList [((InTerms, OpName "<+>"), Fixity RightAssoc 6)]
before' <- open dir (PlanToken "one")
storeFixities before' "thing-1.0" "M" (Declares fixities)
after' <- open dir (PlanToken "two")
cachedFixities after' "thing-1.0" "M" `shouldReturn` Just (Declares fixities)
it "keeps what an export list said, whatever the token" $ \dir -> do
before' <- open dir (PlanToken "one")
storeExportNames before' "thing-1.0" "M" Untellable
after' <- open dir (PlanToken "two")
cachedExportNames after' "thing-1.0" "M" `shouldReturn` Just Untellable
-- | Give each test its own cache directory, so nothing leaks between them
-- or into the developer's real cache.
withIsolatedCache :: (Cache -> IO ()) -> IO ()
withIsolatedCache action =
withIsolatedDirectory (\dir -> open dir (PlanToken "plan") >>= action)
withIsolatedDirectory :: (FilePath -> IO ()) -> IO ()
withIsolatedDirectory = withSystemTempDirectory "tilia-cache"
-- | Open a cache in a given directory, under a given token.
open :: FilePath -> PlanToken -> IO Cache
open dir token = do
setEnv "XDG_CACHE_HOME" dir
opened <- openCache token
unsetEnv "XDG_CACHE_HOME"
case opened of
Nothing -> fail "could not open a cache in a temporary directory"
Just cache -> pure cache