packages feed

moonlight-pale-0.1.0.0: test/ghc-surface/Hie/OracleSpec.hs

module Hie.OracleSpec (tests) where

import Control.Exception (bracket)
import Control.Monad (replicateM)
import Data.ByteString (ByteString)
import Data.ByteString.Char8 qualified as ByteStringChar8
import Data.Char (isAlpha, toUpper)
import Data.List (find, intercalate, isSuffixOf, stripPrefix)
import Data.Map.Strict qualified as Map
import Data.Maybe (mapMaybe)
import Data.Set qualified as Set
import GHC.Clock (getMonotonicTimeNSec)
import Moonlight.Pale.Ghc.Hie.Oracle (ModuleNameOracle (..), ResolvedOrigin, mkResolvedOrigin, occResolvesUniquely)
import Moonlight.Pale.Ghc.Hie.Read (HieReadError (..), indexHieRoots)
import Moonlight.Pale.Ghc.Hie.SourceKey
  ( HieOracleArtifact (..),
    HieSourceKeyKind (..),
    OracleLookup (..),
    OracleQuery (..),
    TriedKey (..),
    buildHieOracleIndex,
    lookupModuleOracle,
  )
import Moonlight.Pale.TestSupport.CompileHieFixture
  ( CompiledHieFixture (compiledHieFixtureOracle),
    compileHieFixture,
    mkHieFixtureModuleName,
  )
import System.Directory
  ( createDirectoryIfMissing,
    createDirectoryLink,
    getTemporaryDirectory,
    removePathForcibly,
  )
import System.Exit (ExitCode (..))
import System.FilePath (normalise, takeFileName, (</>))
import System.Process (readProcessWithExitCode)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertFailure, testCase)

tests :: TestTree
tests =
  testGroup
    "pale.hie.oracle"
    [ testCase "ghc hie resolves map and composition into accepted base origins" $ do
        artifact <- compileAndReadArtifact "OracleFixture" oracleFixtureSource
        let oracle = hieArtifactOracle artifact
        acceptedMapOrigins <- acceptedOriginsFor "map"
        acceptedComposeOrigins <- acceptedOriginsFor "."
        assertBool
          "HIE discovery retains the selected artifact path"
          (takeFileName (hieArtifactPath artifact) == "OracleFixture.hie")
        assertBool "map resolves through the base registry" (occResolvesUniquely oracle "map" acceptedMapOrigins)
        assertBool "composition resolves through the base registry" (occResolvesUniquely oracle "." acceptedComposeOrigins)
        assertBool "hie evidence variables are decoded into span-indexed evidence" (not (Map.null (mnoEvidenceAtSpan oracle)))
        assertBool "hie type table is flattened into span-indexed oracle words" (not (Map.null (mnoTypeAtSpan oracle))),
      testCase "a user-defined composition operator is not accepted as base composition" $ do
        oracle <- compileAndReadOracle "ShadowFixture" shadowFixtureSource
        acceptedComposeOrigins <- acceptedOriginsFor "."
        assertBool "shadowed composition does not satisfy the base registry" (not (occResolvesUniquely oracle "." acceptedComposeOrigins)),
      testCase "source-key lookup uses suffix fallback without guessing through ambiguities" $ do
        let artifact = emptyArtifact "src/Foo/Bar.hs"
            oracleIndex = buildHieOracleIndex [artifact]
            lookupResult =
              lookupModuleOracle
                oracleIndex
                OracleQuery
                  { oqGivenPath = "compiler/foundation/demo/src/Foo/Bar.hs",
                    oqAbsolutePath = Nothing,
                    oqSourceRoots = []
                  }
        case lookupResult of
          OracleFound ModuleSuffixKey foundArtifact ->
            assertBool
              "lookup returns the selected HIE artifact"
              (hieArtifactPath foundArtifact == hieArtifactPath artifact)
          other ->
            assertFailure ("expected module suffix hit, got " <> show other),
      testCase "source-key lookup prefers exact keys over suffix keys" $ do
        let exactArtifact = emptyArtifact "app/Foo.hs"
            suffixArtifact = emptyArtifact "src/Foo.hs"
            oracleIndex = buildHieOracleIndex [exactArtifact, suffixArtifact]
        case lookupModuleOracle oracleIndex (OracleQuery "app/Foo.hs" Nothing []) of
          OracleFound GivenPathKey _ ->
            pure ()
          other ->
            assertFailure ("expected exact hit before suffix fallback, got " <> show other),
      testCase "source-key lookup attaches root-relative paths before suffix fallback" $ do
        let oracleIndex = buildHieOracleIndex [emptyArtifact "src/Foo.hs"]
        case lookupModuleOracle oracleIndex (OracleQuery "/workspace/pkg/src/Foo.hs" Nothing ["/workspace/pkg"]) of
          OracleFound RootRelativeKey _ ->
            pure ()
          other ->
            assertFailure ("expected root-relative hit, got " <> show other),
      testCase "source-key lookup stops at the longest matching suffix before shorter ambiguities" $ do
        let oracleIndex =
              buildHieOracleIndex
                [ emptyArtifact "pkg-a/src/Foo.hs",
                  emptyArtifact "pkg-b/src/Foo.hs",
                  emptyArtifact "other/Foo.hs"
                ]
        case lookupModuleOracle oracleIndex (OracleQuery "/workspace/pkg-a/src/Foo.hs" Nothing []) of
          OracleFound ModuleSuffixKey _ ->
            pure ()
          other ->
            assertFailure ("expected longest singleton suffix hit, got " <> show other),
      testCase "source-key lookup reports exact-key ambiguity" $ do
        let firstArtifact = HieOracleArtifact "first/Foo.hie" (emptyOracle "src/Foo.hs")
            secondArtifact = HieOracleArtifact "second/Foo.hie" (emptyOracle "src/Foo.hs")
            oracleIndex = buildHieOracleIndex [firstArtifact, secondArtifact]
        case lookupModuleOracle oracleIndex (OracleQuery "src/Foo.hs" Nothing []) of
          OracleAmbiguous GivenPathKey "src/Foo.hs" candidates ->
            assertBool
              "ambiguous exact lookup carries artifact paths"
              (candidates == ["first/Foo.hie", "second/Foo.hie"])
          other ->
            assertFailure ("expected exact ambiguity, got " <> show other),
      testCase "source-key lookup records tried keys for misses" $ do
        let oracleIndex = buildHieOracleIndex [emptyArtifact "src/Foo.hs"]
        case lookupModuleOracle oracleIndex (OracleQuery "src/Bar.hs" Nothing []) of
          OracleMissing triedKeys ->
            assertBool
              "miss reports exact identity before suffix identities"
              ( take 3 triedKeys
                  == [ TriedKey GivenPathKey "src/Bar.hs",
                       TriedKey ModuleSuffixKey "src/Bar.hs",
                       TriedKey ModuleSuffixKey "Bar.hs"
                     ]
              )
          other ->
            assertFailure ("expected miss, got " <> show other),
      testCase "source-key lookup preserves relative versus POSIX-root anchors" $ do
        let oracleIndex = buildHieOracleIndex [emptyArtifact "src/Foo.hs"]
        case lookupModuleOracle oracleIndex (OracleQuery "/src/Foo.hs" Nothing []) of
          OracleFound ModuleSuffixKey _ ->
            pure ()
          other ->
            assertFailure ("expected anchored path to require suffix lookup, got " <> show other),
      testCase "source-key lookup canonicalizes drive-root anchors" $ do
        let oracleIndex = buildHieOracleIndex [emptyArtifact "C:\\workspace\\src\\Foo.hs"]
        case lookupModuleOracle oracleIndex (OracleQuery "c:/workspace/src/Foo.hs" Nothing []) of
          OracleFound GivenPathKey _ ->
            pure ()
          other ->
            assertFailure ("expected drive-root exact hit, got " <> show other),
      testCase "source-key lookup preserves UNC anchors" $ do
        let oracleIndex = buildHieOracleIndex [emptyArtifact "\\\\server\\share\\src\\Foo.hs"]
        case lookupModuleOracle oracleIndex (OracleQuery "//server/share/src/Foo.hs" Nothing []) of
          OracleFound GivenPathKey _ ->
            pure ()
          other ->
            assertFailure ("expected UNC exact hit, got " <> show other),
      testCase "source-key trie agrees with the exhaustive normalized-list oracle" $ do
        assertBool
          "bounded corpus generator must retain 2,380 ordered corpora"
          (length sourceKeyOracleCorpora == 2380)
        assertBool
          "query matrix must retain all 27 structural lookup contexts"
          (length sourceKeyDifferentialQueries == 27)
        case sourceKeyDifferentialFailure of
          Nothing ->
            pure ()
          Just failure ->
            assertFailure failure,
      testCase "HIE discovery canonicalizes duplicate directory and file roots" $
        withFreshTestDirectory "duplicate-roots" $ \root -> do
          let invalidHiePath = root </> "Invalid.hie"
          writeFile invalidHiePath ""
          (failures, _oracleIndex) <-
            indexHieRoots
              [ root,
                root </> ".",
                invalidHiePath
              ]
          case failures of
            [HieReadError _ _] ->
              pure ()
            other ->
              assertFailure ("expected one canonicalized artifact failure, got " <> show other),
      testCase "HIE discovery does not follow directory symlink cycles" $
        withFreshTestDirectory "directory-symlink-cycle" $ \root -> do
          let nestedDirectory = root </> "nested"
              invalidHiePath = nestedDirectory </> "Invalid.hie"
              cyclePath = nestedDirectory </> "cycle"
          createDirectoryIfMissing True nestedDirectory
          writeFile invalidHiePath ""
          createDirectoryLink root cyclePath
          (failures, _oracleIndex) <- indexHieRoots [root]
          case failures of
            [HieReadError _ _] ->
              pure ()
            other ->
              assertFailure ("expected one artifact failure without cycle traversal, got " <> show other),
      testCase "ordinary global uses retain exact source spans" $ do
        fixtureModuleName <-
          case mkHieFixtureModuleName "SpanFixture" of
            Left failure ->
              assertFailure ("fixture module name rejected: " <> show failure)
            Right value ->
              pure value
        fixtureResult <-
          compileHieFixture fixtureModuleName spanFixtureSource
        fixture <-
          case fixtureResult of
            Left failure ->
              assertFailure ("HIE fixture compilation failed: " <> show failure)
            Right value ->
              pure value
        let useRows =
              [ (region, origins)
              | (region, names) <- Map.toList (mnoGlobalUsesAtSpan (compiledHieFixtureOracle fixture)),
                Just origins <- [Map.lookup "map" names]
              ]
        assertBool "ordinary map uses are indexed at more than one exact span" (length useRows >= 2)
        assertBool "each exact map span resolves to one origin" (all ((== 1) . Set.size . snd) useRows)
    ]

spanFixtureSource :: ByteString
spanFixtureSource =
  ByteStringChar8.pack
    ( unlines
        [ "module SpanFixture where",
          "first = map id []",
          "second = map id []"
        ]
    )

withFreshTestDirectory :: FilePath -> (FilePath -> IO value) -> IO value
withFreshTestDirectory label action = do
  temporaryDirectory <- getTemporaryDirectory
  uniqueSuffix <- show <$> getMonotonicTimeNSec
  let root =
        temporaryDirectory
          </> "pale-hie-traversal-spec"
          </> (label <> "-" <> uniqueSuffix)
      prepare = do
        createDirectoryIfMissing True root
        pure root
  bracket prepare removePathForcibly action

emptyOracle :: FilePath -> ModuleNameOracle
emptyOracle sourcePath =
  oracleAt (normalise sourcePath)

emptyArtifact :: FilePath -> HieOracleArtifact
emptyArtifact sourcePath =
  HieOracleArtifact
    { hieArtifactPath = sourcePath <> ".hie",
      hieArtifactOracle = emptyOracle sourcePath
    }

oracleAt :: FilePath -> ModuleNameOracle
oracleAt sourcePath =
  ModuleNameOracle
    { mnoSourcePath = sourcePath,
      mnoGlobalUsesAtSpan = Map.empty,
      mnoGlobalUses = Map.empty,
      mnoEvidenceAtSpan = Map.empty,
      mnoTypeAtSpan = Map.empty
    }

data SlowPathAnchor
  = SlowRelativeAnchor
  | SlowPosixRootAnchor
  | SlowDriveRootAnchor !Char
  | SlowUncRootAnchor !String !String
  deriving stock (Eq, Ord, Show)

data SlowCanonicalPath = SlowCanonicalPath
  { slowPathAnchor :: !SlowPathAnchor,
    slowPathComponents :: ![FilePath]
  }
  deriving stock (Eq, Ord, Show)

sourceKeyDifferentialFailure :: Maybe String
sourceKeyDifferentialFailure =
  foldr compareCorpus Nothing sourceKeyOracleCorpora
  where
    compareCorpus sourcePaths nextFailure =
      let artifacts = fmap artifactAt sourcePaths
          oracleIndex = buildHieOracleIndex artifacts
       in case
            find
              ( \query ->
                  lookupModuleOracle oracleIndex query
                    /= slowLookupModuleOracle artifacts query
              )
              sourceKeyDifferentialQueries
            of
            Nothing ->
              nextFailure
            Just query ->
              let expected = slowLookupModuleOracle artifacts query
                  actual = lookupModuleOracle oracleIndex query
               in Just
                    ( "source-key differential failed"
                        <> "\ncorpus: "
                        <> show sourcePaths
                        <> "\nquery: "
                        <> show query
                        <> "\nexpected: "
                        <> show expected
                        <> "\nactual: "
                        <> show actual
                    )

artifactAt :: FilePath -> HieOracleArtifact
artifactAt sourcePath =
  HieOracleArtifact
    { hieArtifactPath = "hie/" <> show sourcePath <> ".hie",
      hieArtifactOracle = oracleAt sourcePath
    }

sourceKeyOracleCorpora :: [[FilePath]]
sourceKeyOracleCorpora =
  concatMap
    (`replicateM` sourceKeyPathUniverse)
    [0 .. 3]

sourceKeyPathUniverse :: [FilePath]
sourceKeyPathUniverse =
  [ "src/Foo.hs",
    "./src/./Foo.hs",
    "app/Foo.hs",
    "pkg-a/src/Foo.hs",
    "pkg-b/src/Foo.hs",
    "other/Foo.hs",
    "/workspace/pkg/src/Foo.hs",
    "/other/pkg/src/Foo.hs",
    "C:\\workspace\\src\\Foo.hs",
    "c:/workspace/src/Foo.hs",
    "\\\\server\\share\\src\\Foo.hs",
    "//server/share/src/Foo.hs",
    "src/Bar.hs"
  ]

sourceKeyDifferentialQueries :: [OracleQuery]
sourceKeyDifferentialQueries =
  fmap (\sourcePath -> OracleQuery sourcePath Nothing []) sourceKeyPathUniverse
    <> [ OracleQuery
           { oqGivenPath = "app/Foo.hs",
             oqAbsolutePath = Just "/workspace/pkg/src/Foo.hs",
             oqSourceRoots = ["/workspace/pkg"]
           },
         OracleQuery
           { oqGivenPath = "missing/Foo.hs",
             oqAbsolutePath = Just "/workspace/pkg/src/Foo.hs",
             oqSourceRoots = ["/workspace/pkg"]
           },
         OracleQuery
           { oqGivenPath = "/workspace/pkg/src/Foo.hs",
             oqAbsolutePath = Nothing,
             oqSourceRoots = ["/workspace/pkg"]
           },
         OracleQuery
           { oqGivenPath = "/workspace/pkg/src/Foo.hs",
             oqAbsolutePath = Nothing,
             oqSourceRoots = ["/workspace", "/workspace/pkg"]
           },
         OracleQuery
           { oqGivenPath = "c:/workspace/src/Foo.hs",
             oqAbsolutePath = Nothing,
             oqSourceRoots = ["C:\\workspace"]
           },
         OracleQuery
           { oqGivenPath = "//server/share/src/Foo.hs",
             oqAbsolutePath = Nothing,
             oqSourceRoots = ["\\\\server\\share"]
           },
         OracleQuery "/checkout/pkg-a/src/Foo.hs" Nothing [],
         OracleQuery "/checkout/src/Foo.hs" Nothing [],
         OracleQuery "/checkout/Foo.hs" Nothing [],
         OracleQuery
           { oqGivenPath = "/workspace/src/Missing.hs",
             oqAbsolutePath = Just "/workspace/src/Missing.hs",
             oqSourceRoots = ["/workspace"]
           },
         OracleQuery "pkg/../src/./Foo.hs" Nothing [],
         OracleQuery "/src/Foo.hs" Nothing [],
         OracleQuery "c:/workspace/src/Foo.hs" Nothing [],
         OracleQuery "//server/share/src/Foo.hs" Nothing []
       ]

slowLookupModuleOracle :: [HieOracleArtifact] -> OracleQuery -> OracleLookup
slowLookupModuleOracle artifacts query =
  case slowFirstExactLookup artifacts (slowExactQueryKeys query) of
    Just exactLookup ->
      exactLookup
    Nothing ->
      case
          find
            (not . null . (`slowSuffixCandidates` artifacts))
            (slowComponentSuffixes (slowPathComponents (slowCanonicalPath (oqGivenPath query))))
        of
        Nothing ->
          OracleMissing (slowExactTriedKeys query <> slowSuffixTriedKeys query)
        Just matchedComponents ->
          slowLookupOutcome
            ModuleSuffixKey
            (intercalate "/" matchedComponents)
            (slowSuffixCandidates matchedComponents artifacts)

slowFirstExactLookup ::
  [HieOracleArtifact] ->
  [(HieSourceKeyKind, SlowCanonicalPath)] ->
  Maybe OracleLookup
slowFirstExactLookup artifacts =
  foldr
    ( \(keyKind, pathValue) nextLookup ->
        case slowExactCandidates pathValue artifacts of
          [] ->
            nextLookup
          candidates ->
            Just
              ( slowLookupOutcome
                  keyKind
                  (slowRenderCanonicalPath pathValue)
                  candidates
              )
    )
    Nothing

slowLookupOutcome ::
  HieSourceKeyKind ->
  FilePath ->
  [HieOracleArtifact] ->
  OracleLookup
slowLookupOutcome keyKind matchedKey candidates =
  case candidates of
    [] ->
      OracleMissing [TriedKey keyKind matchedKey]
    [artifact] ->
      OracleFound keyKind artifact
    ambiguous ->
      OracleAmbiguous keyKind matchedKey (fmap hieArtifactPath ambiguous)

slowExactCandidates ::
  SlowCanonicalPath ->
  [HieOracleArtifact] ->
  [HieOracleArtifact]
slowExactCandidates pathValue =
  filter
    ( (== pathValue)
        . slowCanonicalPath
        . mnoSourcePath
        . hieArtifactOracle
    )

slowSuffixCandidates ::
  [FilePath] ->
  [HieOracleArtifact] ->
  [HieOracleArtifact]
slowSuffixCandidates suffixComponents =
  filter
    ( (suffixComponents `isSuffixOf`)
        . slowPathComponents
        . slowCanonicalPath
        . mnoSourcePath
        . hieArtifactOracle
    )

slowExactQueryKeys :: OracleQuery -> [(HieSourceKeyKind, SlowCanonicalPath)]
slowExactQueryKeys query =
  [(GivenPathKey, slowCanonicalPath (oqGivenPath query))]
    <> maybe
      []
      (\absolutePath -> [(AbsolutePathKey, slowCanonicalPath absolutePath)])
      (oqAbsolutePath query)
    <> fmap (\relativePath -> (RootRelativeKey, relativePath)) (slowRootRelativePaths query)

slowExactTriedKeys :: OracleQuery -> [TriedKey]
slowExactTriedKeys =
  mapMaybe
    ( \(keyKind, pathValue) ->
        case slowRenderCanonicalPath pathValue of
          "" ->
            Nothing
          renderedPath ->
            Just (TriedKey keyKind renderedPath)
    )
    . slowExactQueryKeys

slowRootRelativePaths :: OracleQuery -> [SlowCanonicalPath]
slowRootRelativePaths query =
  [ relativePath
  | root <- fmap slowCanonicalPath (oqSourceRoots query),
    pathValue <-
      slowCanonicalPath (oqGivenPath query)
        : maybe [] (pure . slowCanonicalPath) (oqAbsolutePath query),
    Just relativePath <- [slowStripCanonicalRoot root pathValue]
  ]

slowStripCanonicalRoot ::
  SlowCanonicalPath ->
  SlowCanonicalPath ->
  Maybe SlowCanonicalPath
slowStripCanonicalRoot root pathValue
  | slowPathAnchor root /= slowPathAnchor pathValue =
      Nothing
  | otherwise =
      SlowCanonicalPath SlowRelativeAnchor
        <$> stripPrefix
          (slowPathComponents root)
          (slowPathComponents pathValue)

slowSuffixTriedKeys :: OracleQuery -> [TriedKey]
slowSuffixTriedKeys =
  fmap (TriedKey ModuleSuffixKey . intercalate "/")
    . slowComponentSuffixes
    . slowPathComponents
    . slowCanonicalPath
    . oqGivenPath

slowComponentSuffixes :: [FilePath] -> [[FilePath]]
slowComponentSuffixes components =
  case components of
    [] ->
      []
    _ : remaining ->
      components : slowComponentSuffixes remaining

slowCanonicalPath :: FilePath -> SlowCanonicalPath
slowCanonicalPath rawPath =
  case rawPath of
    firstSeparator : secondSeparator : remaining
      | slowPathSeparator firstSeparator,
        slowPathSeparator secondSeparator ->
          case slowSplitPathComponents remaining of
            server : share : components ->
              SlowCanonicalPath
                (SlowUncRootAnchor server share)
                (slowNormaliseComponents True components)
            components ->
              SlowCanonicalPath
                SlowPosixRootAnchor
                (slowNormaliseComponents True components)
    driveLetter : ':' : remaining
      | isAlpha driveLetter ->
          SlowCanonicalPath
            (SlowDriveRootAnchor (toUpper driveLetter))
            (slowNormaliseComponents True (slowSplitPathComponents remaining))
    firstSeparator : remaining
      | slowPathSeparator firstSeparator ->
          SlowCanonicalPath
            SlowPosixRootAnchor
            (slowNormaliseComponents True (slowSplitPathComponents remaining))
    _ ->
      SlowCanonicalPath
        SlowRelativeAnchor
        (slowNormaliseComponents False (slowSplitPathComponents rawPath))

slowSplitPathComponents :: FilePath -> [FilePath]
slowSplitPathComponents pathValue =
  case dropWhile slowPathSeparator pathValue of
    [] ->
      []
    remaining ->
      let (component, next) = break slowPathSeparator remaining
       in component : slowSplitPathComponents next

slowNormaliseComponents :: Bool -> [FilePath] -> [FilePath]
slowNormaliseComponents rooted =
  reverse . foldl' normaliseComponent []
  where
    normaliseComponent reversedComponents component
      | component == "." || null component =
          reversedComponents
      | component == ".." =
          case reversedComponents of
            previous : remaining
              | previous /= ".." ->
                  remaining
            _
              | rooted ->
                  reversedComponents
              | otherwise ->
                  ".." : reversedComponents
      | otherwise =
          component : reversedComponents

slowRenderCanonicalPath :: SlowCanonicalPath -> FilePath
slowRenderCanonicalPath pathValue =
  let componentText = intercalate "/" (slowPathComponents pathValue)
   in case slowPathAnchor pathValue of
        SlowRelativeAnchor ->
          componentText
        SlowPosixRootAnchor ->
          "/" <> componentText
        SlowDriveRootAnchor driveLetter ->
          driveLetter : ':' : '/' : componentText
        SlowUncRootAnchor server share ->
          "//" <> server <> "/" <> share
            <> if null componentText
              then ""
              else "/" <> componentText

slowPathSeparator :: Char -> Bool
slowPathSeparator character =
  character == '/' || character == '\\'

oracleFixtureSource :: String
oracleFixtureSource =
  unlines
    [ "module OracleFixture where",
      "composed = (.) id id",
      "mapped xs = map id xs",
      "mappedMaybe = fmap not (Just True)",
      "shown = show (Just True)"
    ]

shadowFixtureSource :: String
shadowFixtureSource =
  unlines
    [ "module ShadowFixture where",
      "import Prelude hiding ((.))",
      "(.) x = x",
      "token = ()",
      "shadow = (.) token"
    ]

acceptedOriginsFor :: String -> IO (Set.Set ResolvedOrigin)
acceptedOriginsFor occText =
  either
    (\failure -> assertFailure ("accepted-origin fixture failed to parse: " <> show failure))
    (pure . Set.fromList)
    ( traverse
        (\(unitText, moduleText) -> mkResolvedOrigin unitText moduleText occText)
        [ ("base", "GHC.Base"),
          ("base", "GHC.Internal.Base"),
          ("ghc-internal", "GHC.Internal.Base")
        ]
    )

compileAndReadOracle :: String -> String -> IO ModuleNameOracle
compileAndReadOracle moduleName sourceText =
  hieArtifactOracle <$> compileAndReadArtifact moduleName sourceText

compileAndReadArtifact :: String -> String -> IO HieOracleArtifact
compileAndReadArtifact moduleName sourceText =
  withFreshTestDirectory ("oracle-" <> moduleName) $ \root -> do
    let sourceDirectory = root </> "src"
        hieDirectory = root </> "hie"
        sourcePath = sourceDirectory </> moduleName <> ".hs"
    createDirectoryIfMissing True sourceDirectory
    createDirectoryIfMissing True hieDirectory
    writeFile sourcePath sourceText
    (exitCode, _stdoutText, stderrText) <-
      readProcessWithExitCode
        "ghc"
        [ "-fno-code",
          "-fforce-recomp",
          "-fwrite-ide-info",
          "-hiedir",
          hieDirectory,
          sourcePath
        ]
        ""
    case exitCode of
      ExitSuccess -> do
        (errors, oracleIndex) <- indexHieRoots [hieDirectory]
        let lookupResult =
              lookupModuleOracle
                oracleIndex
                OracleQuery
                  { oqGivenPath = normalise sourcePath,
                    oqAbsolutePath = Just (normalise sourcePath),
                    oqSourceRoots = [sourceDirectory]
                  }
        case (errors, lookupResult) of
          ([], OracleFound _ artifact) ->
            pure artifact
          ([], _) ->
            assertFailure ("oracle missing for " <> sourcePath <> ": " <> show lookupResult)
          (hieErrors, _) ->
            assertFailure ("hie read errors: " <> show hieErrors)
      ExitFailure _ ->
        assertFailure stderrText