tilia-0.1.0.0: tests/Tilia/Fixity/HiFileSpec.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Interface files read directly, against what @ghc --show-iface@ makes of
-- them, for the modules of the packages this project is built against.
--
-- Every module is decoded, but only those that offer fixities or hold a
-- key the table passed over, and a fixed eighth of the rest, are checked
-- against @ghc@, which costs a process per module and was most of the
-- suite's run time.
module Tilia.Fixity.HiFileSpec (spec) where
import Control.Applicative ((<|>))
import Control.Exception (evaluate)
import Data.Bits (xor)
import Data.ByteString qualified as BS
import Data.Char (ord)
import Data.Foldable (for_)
import Data.Map.Strict qualified as Map
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Data.Word (Word64)
import Test.Hspec
import Tilia.Fixity (Fixities, Namespace (..), OpName (..))
import Tilia.Fixity.HiFile
( HiExport (..),
HiFile (..),
HiName (..),
decodeHiFile,
primopFixities,
)
import Tilia.Fixity.Interface (Interface (..), fromHiFile, parseInterface)
import Tilia.Fixity.PackageDb (readInstalledPackages)
import Tilia.Fixity.Plan (BuildPlan)
import Tilia.Process (readProgramOutput)
import Tilia.Utils (inParallel)
import Tilia.WithProjectPlan
( Dependency (..),
dependenciesOf,
testsFor,
withProjectPlan,
)
spec :: Spec
spec = withProjectPlan withPlan
withPlan :: BuildPlan -> Spec
withPlan plan = do
installed <- runIO readInstalledPackages
dependencies <- runIO (dependenciesOf (const True) plan installed)
let modules = concatMap depModules dependencies
declaredIn <- runIO (Map.fromList . concat <$> traverse declarations modules)
let declared m = Map.lookup m declaredIn <|> Map.lookup m primopFixities
describe "interface files read directly" $ do
it "are there to read" $
length modules `shouldSatisfy` (>= 500)
it "decode, every one of them" $ do
failures <- concat <$> inParallel undecodable modules
failures `shouldBe` []
for_ (concatMap testsFor dependencies) $ \(label, chunk) ->
it ("agree with ghc --show-iface on " <> label) $ do
found <- concat <$> inParallel (disagreeing declared) chunk
found `shouldBe` []
where
declarations (m, path) = do
bytes <- BS.readFile path
pure
[ (m, Map.fromList [((namespace, op), fixity) | (namespace, op, fixity) <- hiFixities hi])
| Right hi <- [decodeHiFile bytes]
]
undecodable (m, path) = do
bytes <- BS.readFile path
evaluate [(m, why) | Left why <- [decodeHiFile bytes]]
-- Forced before it is returned, so that the output of ghc, which is
-- hundreds of megabytes across a run, is not kept alive until the end.
disagreeing declared (m, path) = do
decoded <- either (const Nothing) Just . decodeHiFile <$> BS.readFile path
case decoded of
Just hi
| Right d <- fromHiFile m hi,
not (Set.null (offered declared m d)) || passesOver hi || sampled m -> do
shown <-
maybe Nothing (parseInterface m)
<$> readProgramOutput "ghc" ["--show-iface", path]
let found = [(m, x) | Just s <- [shown], x <- disagreements declared m d s]
found <$ evaluate (length found)
_ -> pure []
-- | Whether an interface holds a key the table for its series passed over.
--
-- A key the table should have listed would be passed over too, and the
-- module could then look as if it offered nothing.
passesOver :: HiFile -> Bool
passesOver = any (elem Unneeded . names) . hiExports
where
names (Avail n) = [n]
names (AvailTC p ns) = p : ns
-- | Whether a module is in the fixed eighth of the rest, which are checked
-- against @ghc@ all the same.
--
-- Chosen by an FNV-1a hash of the name, so that it is the same modules on
-- every run and machine.
sampled :: Text -> Bool
sampled m = T.foldl' step 14695981039346656037 m `mod` 8 == (0 :: Word64)
where
step h c = (h `xor` fromIntegral (ord c)) * 1099511628211
-- | Where a direct reading and the text disagree about what fixities depend
-- on, beyond what the text cannot tell.
--
-- A fixity the text puts among terms may be among types all the same: the
-- text leaves out the declarations of the types GHC wires in.
disagreements ::
-- | The fixities each module declares, where they can be read.
(Text -> Maybe Fixities) ->
Text ->
Interface ->
Interface ->
[Text]
disagreements declared m direct shown =
[ "fixities"
| not (inTypes (interfaceDeclares shown) `Map.isSubmapOf` interfaceDeclares direct)
|| byOperator (interfaceDeclares direct) /= byOperator (interfaceDeclares shown)
]
<> ["offered fixities" | offered declared m direct /= offered declared m shown]
<> ["members" | members direct /= members shown]
where
inTypes = Map.filterWithKey (\(namespace, _) _ -> namespace == InTypes)
members i =
Map.filter (not . Set.null) $
Map.map (Set.filter (`Set.member` withFixities)) (interfaceMembers i)
withFixities = Set.map (OpName . fst) (offered declared m direct)
-- | Every fixity a module offers, its own and those of what it reexports,
-- by operator.
offered :: (Text -> Maybe Fixities) -> Text -> Interface -> Set (Text, String)
offered declared m i =
byOperator (interfaceDeclares i)
<> Set.unions
[ byOperator (Map.filterWithKey (\(_, o) _ -> o == op) fixities)
| (from, op) <- interfaceReexports i,
from /= m,
Just fixities <- [declared from]
]
-- | Fixities by the operator alone, namespace aside.
byOperator :: Fixities -> Set (Text, String)
byOperator = Set.fromList . fmap (\((_, OpName op), fixity) -> (op, show fixity)) . Map.toList