tilia-0.1.4.0: tests/Tilia/Fixity/CacheSpec.hs
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
-- | The on-disk cache of what was read out of a package.
module Tilia.Fixity.CacheSpec (spec) where
import Control.Monad (replicateM, replicateM_)
import Data.Choice (pattern Do, pattern Don't)
import Data.Foldable (traverse_)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import System.Directory (getModificationTime, listDirectory, setModificationTime)
import System.FilePath ((</>))
import System.IO.Temp (withSystemTempDirectory)
import Test.Hspec
import Tilia.Fixity
import Tilia.Fixity.Cache
import Tilia.Fixity.PackageDb (Installed (..), InstalledPackage (..))
import Tilia.TestUtils (underAsciiLocale, withEnvironment)
import Tilia.Utils (inParallel)
spec :: Spec
spec = do
tokens
database
describe "a cache not to be used" $
it "remembers nothing, and nothing is made on disk for it" $
withSystemTempDirectory "tilia-cache" $ \dir -> do
cache <-
withEnvironment [("XDG_CACHE_HOME", dir)] $
openCache (Don't #useCache) (PlanToken "plan")
storeModules cache "thing-1.0" ["A"]
cachedModules cache "thing-1.0" `shouldReturn` Nothing
listDirectory dir `shouldReturn` []
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 "what reading a module established" $ do
it "round-trips every direction" $ \cache -> do
let established =
declaring
[ ((InTerms, OpName "<+>"), Fixity LeftAssoc 6),
((InTerms, OpName ">>="), Fixity RightAssoc 1),
((InTerms, OpName "==="), Fixity NoAssoc 4)
]
storeEstablished cache "thing-1.0" "A.B" established
cachedEstablished cache "thing-1.0" "A.B" `shouldReturn` Just established
it "round-trips the extremes of precedence" $ \cache -> do
let established =
declaring
[ ((InTerms, OpName "!"), Fixity LeftAssoc 0),
((InTerms, OpName "?"), Fixity LeftAssoc 9),
((InTerms, OpName "->"), Fixity RightAssoc (-1))
]
storeEstablished cache "thing-1.0" "Edges" established
cachedEstablished cache "thing-1.0" "Edges" `shouldReturn` Just established
it "round-trips a name that is not ASCII under any locale" $ \cache -> do
let established =
declaring [((InTerms, OpName "∘"), Fixity RightAssoc 9)]
underAsciiLocale $ do
storeEstablished cache "thing-1.0" "Compose" established
cachedEstablished cache "thing-1.0" "Compose"
`shouldReturn` Just established
it "remembers that a module declares nothing" $ \cache -> do
storeEstablished cache "thing-1.0" "Quiet" mempty
cachedEstablished cache "thing-1.0" "Quiet" `shouldReturn` Just mempty
it "remembers that a module could not be read" $ \cache -> do
storeEstablished cache "thing-1.0" "Opaque" unreadable
cachedEstablished cache "thing-1.0" "Opaque" `shouldReturn` Just unreadable
it "remembers the way down to where reading gave up" $ \cache -> do
storeEstablished cache "thing-1.0" "Opaque" (stoppedAt ["Middle", "Deep.Down"])
cachedEstablished cache "thing-1.0" "Opaque"
`shouldReturn` Just (stoppedAt ["Middle", "Deep.Down"])
it "tells one stopped below it from one stopped on its own account" $ \cache -> do
storeEstablished cache "thing-1.0" "Blamed" (stoppedAt ["Deep.Down"])
storeEstablished cache "thing-1.0" "Itself" unreadable
blamed <- cachedEstablished cache "thing-1.0" "Blamed"
itself <- cachedEstablished cache "thing-1.0" "Itself"
(blamed, itself) `shouldBe` (Just (stoppedAt ["Deep.Down"]), Just unreadable)
it "round-trips the names it leaves unsettled, by namespace and way down" $ \cache -> do
let established =
mempty
{ establishedUnsettled =
Map.fromList
[ (["Deep.Down"], Set.fromList [(InTypes, OpName ":+:"), (InTerms, OpName "===")]),
([], Set.fromList [(InTerms, OpName "<+>"), (InTerms, OpName "v")])
]
}
storeEstablished cache "thing-1.0" "M" established
cachedEstablished cache "thing-1.0" "M" `shouldReturn` Just established
it "tells an unread module from one it was never told about" $ \cache -> do
storeEstablished cache "thing-1.0" "Opaque" unreadable
unread <- cachedEstablished cache "thing-1.0" "Opaque"
never <- cachedEstablished cache "thing-1.0" "Absent"
(unread, never) `shouldBe` (Just unreadable, Nothing)
it "tells an unread module from one that declares nothing" $ \cache -> do
storeEstablished cache "thing-1.0" "Opaque" unreadable
storeEstablished cache "thing-1.0" "Quiet" mempty
opaque <- cachedEstablished cache "thing-1.0" "Opaque"
quiet <- cachedEstablished cache "thing-1.0" "Quiet"
(opaque, quiet) `shouldBe` (Just unreadable, Just mempty)
it "replaces an unread answer once the module can be read" $ \cache -> do
storeEstablished cache "thing-1.0" "M" unreadable
storeEstablished cache "thing-1.0" "M" (declaring [((InTerms, OpName "!"), Fixity LeftAssoc 9)])
cachedEstablished cache "thing-1.0" "M"
`shouldReturn` Just (declaring [((InTerms, OpName "!"), Fixity LeftAssoc 9)])
it "knows nothing about a module it was never told about" $ \cache ->
cachedEstablished cache "thing-1.0" "Absent" `shouldReturn` Nothing
it "keeps packages apart" $ \cache -> do
let ops = declaring [((InTerms, OpName "<>"), Fixity RightAssoc 6)]
storeEstablished cache "a-1.0" "M" ops
storeEstablished cache "b-1.0" "M" mempty
a <- cachedEstablished cache "a-1.0" "M"
b <- cachedEstablished cache "b-1.0" "M"
(a, b) `shouldBe` (Just ops, Just mempty)
it "treats a different hash in the key as a different package" $ \cache -> do
storeEstablished cache "thing-1.0-aaaa" "M" (declaring [((InTerms, OpName "!"), Fixity LeftAssoc 9)])
cachedEstablished cache "thing-1.0-bbbb" "M" `shouldReturn` Nothing
it "overwrites a previous answer for the same key" $ \cache -> do
storeEstablished cache "thing-1.0" "M" (declaring [((InTerms, OpName "!"), Fixity LeftAssoc 9)])
storeEstablished cache "thing-1.0" "M" (declaring [((InTerms, OpName "!"), Fixity RightAssoc 3)])
cachedEstablished cache "thing-1.0" "M"
`shouldReturn` Just (declaring [((InTerms, OpName "!"), Fixity RightAssoc 3)])
it "gives back no half of an answer two writers store at once" $ \cache -> do
let answers =
[ declaring
[ ((InTerms, OpName (T.replicate n "+")), Fixity LeftAssoc k)
| n <- [1 .. 200]
]
| k <- [1, 2]
]
store = storeEstablished cache "thing-1.0" "M"
storing established = [] <$ replicateM_ 200 (store established)
recalling = replicateM 200 (cachedEstablished cache "thing-1.0" "M")
traverse_ store answers
recalled' <-
concat
<$> inParallel id (fmap storing answers <> [recalling, recalling])
filter (`notElem` fmap Just answers) recalled' `shouldBe` []
it "round-trips the members of each name, a name without members among them" $ \cache -> do
let established =
mempty
{ establishedMembers =
Map.fromList
[ (OpName "Empty", Set.empty),
(OpName "NonEmpty", Set.fromList [OpName ":|"]),
(OpName "Seq", Set.fromList [OpName ":<|", OpName ":|>"])
]
}
storeEstablished cache "thing-1.0" "M" established
cachedEstablished cache "thing-1.0" "M" `shouldReturn` Just established
it "round-trips what it certainly brings in and the members of its types, by namespace" $ \cache -> do
let established =
mempty
{ establishedCertain =
Certain
( Set.fromList
[ (InTypes, OpName "T"),
(InTerms, OpName "T"),
(InTerms, OpName ":|"),
(InTypes, OpName "Empty")
]
)
( Map.fromList
[ (OpName "T", Set.fromList [(InTerms, OpName "T"), (InTypes, OpName "F")]),
(OpName "Empty", Set.empty)
]
),
establishedMembers =
Map.fromList
[ (OpName "T", Set.fromList [OpName "T", OpName "F"]),
(OpName "Empty", Set.empty)
]
}
storeEstablished cache "thing-1.0" "M" established
cachedEstablished cache "thing-1.0" "M" `shouldReturn` Just established
it "round-trips the members of a name beyond its certain members" $ \cache -> do
let established =
mempty
{ establishedCertain =
Certain
(Set.fromList [(InTypes, OpName "T"), (InTerms, OpName "A")])
(Map.singleton (OpName "T") (Set.singleton (InTerms, OpName "A"))),
establishedMembers =
Map.fromList
[ (OpName "T", Set.fromList [OpName "A", OpName "B"]),
(OpName "U", Set.empty)
]
}
storeEstablished cache "thing-1.0" "M" established
cachedEstablished cache "thing-1.0" "M" `shouldReturn` Just established
it "round-trips all of it at once" $ \cache -> do
let established =
Established
{ establishedFixities = Map.singleton (InTerms, OpName "<+>") (Fixity LeftAssoc 6),
establishedUnsettled = Map.singleton ["Below"] (Set.singleton (InTerms, OpName "==>")),
establishedUntold = Set.fromList [["Below"], ["Other", "Further"]],
establishedCertain =
Certain
(Set.fromList [(InTerms, OpName "<+>"), (InTerms, OpName "==>")])
Map.empty,
establishedMembers = Map.singleton (OpName "T") (Set.singleton (OpName "A"))
}
storeEstablished cache "thing-1.0" "M" established
cachedEstablished cache "thing-1.0" "M" `shouldReturn` Just established
describe "what the project's own modules say" $ do
it "round-trips every kind of thing a summary holds" $ \cache -> do
let summaries = Just (crowded :| [bare, unlisted])
storeSummaries cache "m" "stamp" summaries
cachedSummaries cache "m" "stamp" `shouldReturn` Just summaries
it "remembers a module none of whose configurations parsed" $ \cache -> do
storeSummaries cache "m" "stamp" Nothing
cachedSummaries cache "m" "stamp" `shouldReturn` Just Nothing
it "gives back nothing for what it was not read from" $ \cache -> do
storeSummaries cache "m" "before" (Just (crowded :| []))
cachedSummaries cache "m" "after" `shouldReturn` Nothing
it "keeps only what the latest text of a module said" $ \cache -> do
storeSummaries cache "m" "before" (Just (crowded :| []))
storeSummaries cache "m" "after" (Just (bare :| []))
cachedSummaries cache "m" "before" `shouldReturn` Nothing
cachedSummaries cache "m" "after" `shouldReturn` Just (Just (bare :| []))
it "keeps modules apart" $ \cache -> do
storeSummaries cache "m" "stamp" (Just (crowded :| []))
cachedSummaries cache "n" "stamp" `shouldReturn` Nothing
describe "where cabal keeps downloaded sources" $
it "keeps projects apart under one plan" $ \cache -> do
storePackageCache cache "/one" "/one/packages"
cachedPackageCache cache "/another" `shouldReturn` Nothing
cachedPackageCache cache "/one" `shouldReturn` Just "/one/packages"
describe "module names with dots" $
it "files a deeply qualified module without confusion" $ \cache -> do
storeEstablished cache "thing-1.0" "A.B.C.D" (declaring [((InTerms, OpName "%"), Fixity NoAssoc 5)])
cachedEstablished cache "thing-1.0" "A.B.C.D"
`shouldReturn` Just (declaring [((InTerms, OpName "%"), Fixity NoAssoc 5)])
-- | A summary with something of every kind in it.
crowded :: ModuleSummary
crowded =
ModuleSummary
{ summaryName = Just "M.N",
summaryExports =
Just
[ ExportName InTerms Nothing (OpName "<+>"),
ExportName InTerms Nothing (OpName "-"),
ExportName InTerms (Just "Q") (OpName "<->"),
ExportName InTypes Nothing (OpName ":|:"),
ExportAll Nothing (OpName "T"),
ExportAll (Just "Q") (OpName "U"),
ExportSome Nothing (OpName "C") [OpName "method", OpName "F"],
ExportSome (Just "Q") (OpName "V") [],
ExportModule "Data.List"
],
summaryImports =
[ Import "Prelude" False "Prelude" Nothing,
Import "Data.List" True "Q" Nothing,
Import
"Data.Map"
False
"Data.Map"
( Just
( False,
[ ImportedName (OpName "!"),
ImportedAll (OpName "Map"),
ImportedSome (OpName "NonEmpty") [OpName ":|", OpName "toList"]
]
)
),
Import "Data.Set" False "S" (Just (True, [ImportedName (OpName "\\\\")])),
Import "Data.Void" False "Data.Void" (Just (False, []))
],
summaryFixities =
Map.fromList
[ ((InTerms, OpName "<+>"), Fixity LeftAssoc 6),
((InTypes, OpName ":|:"), Fixity RightAssoc 5),
((InTerms, OpName "div"), Fixity NoAssoc 7)
],
summaryNames =
Set.fromList
[ (InTerms, OpName "<+>"),
(InTypes, OpName "T"),
(InTerms, OpName "T"),
(InTypes, OpName ":|:")
],
summaryDeclaredMembers =
Map.fromList
[ (OpName "T", Set.fromList [(InTerms, OpName "A"), (InTerms, OpName ":|:")]),
(OpName "C", Set.fromList [(InTerms, OpName "method"), (InTypes, OpName "F")]),
(OpName "Empty", Set.empty)
],
summaryListedMembers = Map.fromList [(OpName "T", Set.singleton (OpName "A"))]
}
-- | A summary with nothing in it, not even a name.
bare :: ModuleSummary
bare = ModuleSummary Nothing Nothing [] Map.empty Set.empty Map.empty Map.empty
-- | One whose export list lists nothing, which is not the same as having
-- no export list.
unlisted :: ModuleSummary
unlisted = bare{summaryName = Just "M", summaryExports = Just []}
-- | 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" $ \_ ->
withSystemTempDirectory "tilia-cache" $ \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" $ \_ ->
withSystemTempDirectory "tilia-cache" $ \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" $ \_ ->
withSystemTempDirectory "tilia-cache" $ \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]
it "carries what a package exposes that another one holds" $ \cache ->
withDatabase $ \db -> do
storeInstalled cache (Installed [prim] [db])
cachedInstalled cache `shouldReturn` Just [prim]
it "keeps the spaces in a path" $ \cache ->
withSystemTempDirectory "tilia db" $ \db -> do
setModificationTime db =<< getModificationTime "/"
let spaced = containers{ipImportDirs = ["/no where/containers 0.7"]}
storeInstalled cache (Installed [spaced] [db])
cachedInstalled cache `shouldReturn` Just [spaced]
where
containers =
InstalledPackage
{ ipName = "containers",
ipVersion = "0.7",
ipModules = ["Data.Map", "Data.Map.Strict", "Data.Set"],
ipReexports = [],
ipImportDirs = ["/nowhere/containers-0.7"]
}
prim =
InstalledPackage
{ ipName = "ghc-prim",
ipVersion = "0.13.1",
ipModules = [],
ipReexports =
[ ("GHC.Prim", "GHC.Internal.Prim"),
("GHC.Types", "GHC.Internal.Types")
],
ipImportDirs = ["/nowhere/ghc-prim-0.13.1"]
}
quiet =
InstalledPackage
{ ipName = "rts",
ipVersion = "1.0",
ipModules = [],
ipReexports = [],
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 that leaves names unsettled is tied to, and what it is
-- not.
tokens :: Spec
tokens = around (withSystemTempDirectory "tilia-cache") $ do
it "does not offer an unread answer written under another token" $ \dir -> do
before' <- open dir (PlanToken "one")
storeEstablished before' "thing-1.0" "M" unreadable
after' <- open dir (PlanToken "two")
cachedEstablished after' "thing-1.0" "M" `shouldReturn` Nothing
it "does not offer one that names what it leaves unsettled either" $ \dir -> do
before' <- open dir (PlanToken "one")
storeEstablished
before'
"thing-1.0"
"M"
mempty{establishedUnsettled = Map.singleton ["Below"] (Set.singleton (InTerms, OpName "==>"))}
after' <- open dir (PlanToken "two")
cachedEstablished after' "thing-1.0" "M" `shouldReturn` Nothing
it "still offers one written under the same token" $ \dir -> do
before' <- open dir (PlanToken "one")
storeEstablished before' "thing-1.0" "M" unreadable
again <- open dir (PlanToken "one")
cachedEstablished again "thing-1.0" "M" `shouldReturn` Just unreadable
it "keeps an answer that settles everything, whatever the token" $ \dir -> do
let established = declaring [((InTerms, OpName "<+>"), Fixity RightAssoc 6)]
before' <- open dir (PlanToken "one")
storeEstablished before' "thing-1.0" "M" established
after' <- open dir (PlanToken "two")
cachedEstablished after' "thing-1.0" "M" `shouldReturn` Just established
-- | What reading a module that settles every name and declares these
-- fixities established.
declaring :: [((Namespace, OpName), Fixity)] -> Established
declaring fixities = mempty{establishedFixities = Map.fromList fixities}
-- | What reading a module that gave up on the way down given established.
stoppedAt :: [Text] -> Established
stoppedAt chain = mempty{establishedUntold = Set.singleton chain}
-- | 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 =
withSystemTempDirectory "tilia-cache" (\dir -> open dir (PlanToken "plan") >>= action)
-- | Open a cache in a given directory, under a given token.
open :: FilePath -> PlanToken -> IO Cache
open dir token =
withEnvironment [("XDG_CACHE_HOME", dir)] (openCache (Do #useCache) token)