packages feed

seihou-core-0.6.0.0: test/Seihou/Core/ArtifactRefSpec.hs

module Seihou.Core.ArtifactRefSpec (tests) where

import Control.Lens ((^.))
import Data.Generics.Labels ()
import Data.Text qualified as T
import GHC.Generics (Generic)
import Seihou.Core.ArtifactRef
import Seihou.Core.Types
import System.Directory (createDirectoryIfMissing)
import System.FilePath ((</>))
import System.IO.Temp (withSystemTempDirectory)
import Test.Hspec
import Test.Tasty
import Test.Tasty.Hspec (testSpec)

tests :: IO TestTree
tests = testSpec "Seihou.Core.ArtifactRef" spec

-- | A project root plus the three search paths seihou discovers through, in
-- the same order as 'Seihou.Core.Module.defaultSearchPaths': the project's
-- own modules, the developer's personal modules, and the install cache.
data Roots = Roots
  { projectRoot :: !FilePath,
    projectModules :: !FilePath,
    userModules :: !FilePath,
    installed :: !FilePath
  }
  deriving stock (Generic)

searchPathsOf :: Roots -> [FilePath]
searchPathsOf roots = [roots ^. #projectModules, roots ^. #userModules, roots ^. #installed]

withRoots :: (Roots -> IO a) -> IO a
withRoots body =
  withSystemTempDirectory "seihou-artifact-ref" $ \tmpDir -> do
    let roots =
          Roots
            { projectRoot = tmpDir </> "project",
              projectModules = tmpDir </> "project" </> ".seihou" </> "modules",
              userModules = tmpDir </> "home" </> "seihou" </> "modules",
              installed = tmpDir </> "home" </> "seihou" </> "installed"
            }
    mapM_ (createDirectoryIfMissing True) ((roots ^. #projectRoot) : searchPathsOf roots)
    body roots

-- | Create @<parent>/<name>/module.dhall@ and return the module directory.
plantModule :: FilePath -> String -> IO FilePath
plantModule parent name = do
  let directory = parent </> name
  createDirectoryIfMissing True directory
  writeFile (directory </> "module.dhall") "{- fixture -}"
  pure directory

resolve :: Roots -> ArtifactOrigin -> IO (Either ArtifactRefError FilePath)
resolve roots = resolveArtifactOrigin ((roots ^. #projectRoot)) (searchPathsOf roots) "module.dhall"

remoteOrigin :: ArtifactOrigin
remoteOrigin = RemoteOrigin "https://github.com/shinzui/seihou-modules.git" "haskell-base" (Just "seihou-modules")

spec :: Spec
spec = do
  describe "resolveArtifactOrigin" $ do
    it "finds a remote-origin artifact in the install cache" $ do
      withRoots $ \roots -> do
        expected <- plantModule ((roots ^. #installed)) "haskell-base"
        resolve roots remoteOrigin `shouldReturn` Right expected

    it "lets a project-local copy shadow the installed one" $ do
      withRoots $ \roots -> do
        shadow <- plantModule ((roots ^. #projectModules)) "haskell-base"
        _ <- plantModule ((roots ^. #installed)) "haskell-base"
        resolve roots remoteOrigin `shouldReturn` Right shadow

    it "finds a local-origin artifact in the personal module directory" $ do
      withRoots $ \roots -> do
        expected <- plantModule ((roots ^. #userModules)) "scratch"
        resolve roots (LocalOrigin "scratch") `shouldReturn` Right expected

    it "reports every probed directory in order when nothing matches" $ do
      withRoots $ \roots -> do
        result <- resolve roots remoteOrigin
        result
          `shouldBe` Left
            ( ArtifactNotFoundLocally
                remoteOrigin
                [ (roots ^. #projectModules) </> "haskell-base",
                  (roots ^. #userModules) </> "haskell-base",
                  (roots ^. #installed) </> "haskell-base"
                ]
            )

    it "ignores a directory that has no definition file" $ do
      withRoots $ \roots -> do
        createDirectoryIfMissing True ((roots ^. #projectModules) </> "haskell-base")
        expected <- plantModule ((roots ^. #installed)) "haskell-base"
        resolve roots remoteOrigin `shouldReturn` Right expected

    it "resolves a project origin against the project root" $ do
      withRoots $ \roots -> do
        expected <- plantModule ((roots ^. #projectModules)) "docs"
        resolve roots (ProjectOrigin ".seihou/modules/docs") `shouldReturn` Right expected

    it "refuses to substitute an installed artifact for a missing project one" $ do
      withRoots $ \roots -> do
        _ <- plantModule ((roots ^. #installed)) "docs"
        result <- resolve roots (ProjectOrigin ".seihou/modules/docs")
        result
          `shouldBe` Left
            ( ProjectArtifactMissing
                (ProjectOrigin ".seihou/modules/docs")
                ((roots ^. #projectRoot) </> ".seihou" </> "modules" </> "docs")
            )

  describe "renderArtifactRefError" $ do
    it "names the recorded URL, every probed directory, and the install remedy" $ do
      withRoots $ \roots -> do
        Left err <- resolve roots remoteOrigin
        let message = renderArtifactRefError err
        message `shouldSatisfy` T.isInfixOf "haskell-base"
        message `shouldSatisfy` T.isInfixOf "https://github.com/shinzui/seihou-modules.git"
        message `shouldSatisfy` T.isInfixOf "seihou install https://github.com/shinzui/seihou-modules.git"
        mapM_
          (\directory -> message `shouldSatisfy` T.isInfixOf (T.pack directory))
          [ (roots ^. #projectModules) </> "haskell-base",
            (roots ^. #userModules) </> "haskell-base",
            (roots ^. #installed) </> "haskell-base"
          ]

    it "says a local-origin artifact has no upstream to fetch from" $ do
      withRoots $ \roots -> do
        Left err <- resolve roots (LocalOrigin "scratch")
        let message = renderArtifactRefError err
        message `shouldSatisfy` T.isInfixOf "no recorded upstream"
        message `shouldSatisfy` not . T.isInfixOf "seihou install"

    it "says a missing project artifact should have been committed" $ do
      withRoots $ \roots -> do
        Left err <- resolve roots (ProjectOrigin ".seihou/modules/docs")
        let message = renderArtifactRefError err
        message `shouldSatisfy` T.isInfixOf ".seihou/modules/docs"
        message `shouldSatisfy` T.isInfixOf "committed"