packages feed

tilia-0.0.1.0: tests/Tilia/ProjectSpec.hs

{-# LANGUAGE OverloadedStrings #-}

-- | Finding the project a file belongs to.
module Tilia.ProjectSpec (spec) where

import System.Directory (createDirectoryIfMissing, withCurrentDirectory)
import System.FilePath ((</>))
import System.IO.Temp (withSystemTempDirectory)
import Test.Hspec
import Tilia.Project

spec :: Spec
spec = do
  describe "in this repository" $ do
    it "finds the root from the root" $ do
      root <- findProjectRoot "."
      (prMarker <$> root) `shouldBe` Just ProjectFile

    it "finds the root from a nested source directory" $ do
      root <- findProjectRoot "src/Tilia/Printer"
      (prMarker <$> root) `shouldBe` Just ProjectFile

    it "finds the root from a file rather than a directory" $ do
      root <- findProjectRoot "src/Tilia/Fixity.hs"
      (prMarker <$> root) `shouldBe` Just ProjectFile

    it "returns the same directory however it is reached" $ do
      a <- findProjectRoot "."
      b <- findProjectRoot "tests/Tilia"
      c <- findProjectRoot "src/Tilia/Printer/Internal.hs"
      (prPath <$> a, prPath <$> b) `shouldBe` (prPath <$> a, prPath <$> c)
      (prPath <$> b) `shouldBe` (prPath <$> c)

  describe "marker precedence" $ do
    it "prefers cabal.project to a bare .cabal file" $
      withTree [("cabal.project", ""), ("thing.cabal", "")] $ \dir -> do
        root <- findProjectRoot dir
        (prMarker <$> root) `shouldBe` Just ProjectFile

    it "accepts a bare .cabal file when there is no project" $
      withTree [("thing.cabal", "")] $ \dir -> do
        root <- findProjectRoot dir
        (prMarker <$> root) `shouldBe` Just (PackageFile "thing.cabal")

    it "passes over a stack.yaml, which cabal does not read" $
      withTree [("stack.yaml", ""), ("thing.cabal", "")] $ \dir -> do
        root <- findProjectRoot dir
        (prMarker <$> root) `shouldBe` Just (PackageFile "thing.cabal")

    it "climbs out of a stack project to the package it is asked about" $
      withTree [("stack.yaml", ""), ("packages/inner/inner.cabal", "")] $
        \dir -> do
          root <- findProjectRoot (dir </> "packages" </> "inner")
          prPath <$> root `shouldBe` Just (dir </> "packages" </> "inner")

    it "climbs past a package to the project that contains it"
      $ withTree
        [ ("cabal.project", ""),
          ("packages/inner/placeholder", "")
        ]
      $ \dir -> do
        root <- findProjectRoot (dir </> "packages" </> "inner")
        (prMarker <$> root) `shouldBe` Just ProjectFile

    it "climbs past a package that has its own .cabal"
      $ withTree
        [ ("cabal.project", ""),
          ("packages/inner/inner.cabal", "")
        ]
      $ \dir -> do
        root <- findProjectRoot (dir </> "packages" </> "inner")
        (prPath <$> root) `shouldBe` Just dir
        (prMarker <$> root) `shouldBe` Just ProjectFile

    it "takes the nearest project of the ones above"
      $ withTree
        [ ("cabal.project", ""),
          ("packages/inner/cabal.project", ""),
          ("packages/inner/inner.cabal", "")
        ]
      $ \dir -> do
        root <- findProjectRoot (dir </> "packages" </> "inner")
        prPath <$> root `shouldBe` Just (dir </> "packages" </> "inner")

    it "takes the nearest package when no project is above either"
      $ withTree
        [ ("outer.cabal", ""),
          ("packages/inner/inner.cabal", "")
        ]
      $ \dir -> do
        root <- findProjectRoot (dir </> "packages" </> "inner")
        (prMarker <$> root) `shouldBe` Just (PackageFile "inner.cabal")

    it "climbs to a project past a package that is not the one asked about"
      $ withTree
        [ ("cabal.project", ""),
          ("outer.cabal", ""),
          ("packages/inner/inner.cabal", "")
        ]
      $ \dir -> do
        root <- findProjectRoot (dir </> "packages" </> "inner")
        (prMarker <$> root) `shouldBe` Just ProjectFile

  describe "no project" $
    it "gives up rather than guessing" $
      withTree [("lonely/Thing.hs", "module Thing where")] $ \dir ->
        -- A temporary directory has no project above it, so this walks to
        -- the filesystem root and finds nothing.
        withCurrentDirectory dir $ do
          root <- findProjectRoot "lonely"
          case root of
            Nothing -> pure ()
            Just found ->
              -- Some machines have a stray marker in a parent of the
              -- system temporary directory; only a genuine find inside the
              -- tree would be a failure.
              prPath found `shouldNotBe` (dir </> "lonely")

-- | Build a throwaway tree of files and run an action on its root.
withTree :: [(FilePath, String)] -> (FilePath -> IO a) -> IO a
withTree files action =
  withSystemTempDirectory "tilia-project" $ \dir -> do
    mapM_ (create dir) files
    action dir
  where
    create dir (path, contents) = do
      let full = dir </> path
      createDirectoryIfMissing True (parentOf full)
      writeFile full contents
    parentOf = reverse . drop 1 . dropWhile (/= '/') . reverse