packages feed

tilia-0.0.2.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 declare fixities 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.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 qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Data.Word (Word64)
import Test.Hspec
import Tilia.Fixity.HiFile (decodeHiFile)
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
  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 chunk
        found `shouldBe` []
  where
    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 (m, path) = do
      direct <-
        either (const Nothing) (Just . fromHiFile m) . decodeHiFile
          <$> BS.readFile path
      case direct of
        Just (Right d)
          | not (Map.null (interfaceDeclares d)) || sampled m -> do
              shown <-
                maybe Nothing (parseInterface m)
                  <$> readProgramOutput "ghc" ["--show-iface", path]
              let found = [(m, x) | Just s <- [shown], x <- disagreements m d s]
              found <$ evaluate (length found)
        _ -> pure []

-- | Whether a module is in the fixed eighth of those that declare no
-- fixities and 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 beyond what the text
-- cannot tell.
disagreements :: Text -> Interface -> Interface -> [Text]
disagreements m direct shown =
  [ "fixities"
  | not (interfaceDeclares shown `Map.isSubmapOf` interfaceDeclares direct)
      || byOperator (interfaceDeclares direct) /= byOperator (interfaceDeclares shown)
  ]
    <> [ "re-exports"
       | Set.fromList (interfaceReexports direct)
           /= Set.fromList [r | r@(from, _) <- interfaceReexports shown, from /= m]
       ]
    <> ["children" | interfaceChildren direct /= interfaceChildren shown]
  where
    byOperator = Set.fromList . fmap (\((_, op), fixity) -> (op, show fixity)) . Map.toList