hls-test-utils-2.14.0.0: src/Test/Hls/FileSystem.hs
{-# LANGUAGE OverloadedStrings #-}
module Test.Hls.FileSystem
( FileSystem(..)
, VirtualFileTree(..)
, FileTree
, Content
-- * init
, materialise
, materialiseVFT
-- * Interaction
, readFileFS
, writeFileFS
-- * Test helpers
, mkVirtualFileTree
, toNfp
, toAbsFp
-- * Builders
, file
, copy
, directory
, text
, ref
, copyDir
-- * Cradle helpers
, directCradle
, simpleCabalCradle
-- * Full project setups
, directProject
, directProjectMulti
, simpleCabalProject
, simpleCabalProject'
, atomicFileWriteString
, atomicFileWriteStringUTF8
, atomicFileWriteText
) where
import Control.Exception (onException)
import Data.Foldable (traverse_)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Development.IDE (NormalizedFilePath)
import Language.LSP.Protocol.Types (toNormalizedFilePath)
import System.Directory
import System.FilePath as FP
import System.IO.Extra (newTempFileWithin, writeFileUTF8)
import System.Process.Extra (readProcess)
-- ----------------------------------------------------------------------------
-- Top Level definitions
-- ----------------------------------------------------------------------------
-- | Representation of a 'VirtualFileTree' that has been 'materialise'd to disk.
--
data FileSystem =
FileSystem
{ fsRoot :: FilePath
, fsTree :: [FileTree]
, fsOriginalRoot :: FilePath
} deriving (Eq, Ord, Show)
-- | Virtual representation of a filesystem tree.
--
-- Operations of 'vftTree' are relative to 'vftOriginalRoot'.
-- In particular, any 'copy' etc. operation looks for the sources in 'vftOriginalRoot'.
--
-- To persist a 'VirtualFileTree', look at 'materialise' and 'materialiseVFT'.
data VirtualFileTree =
VirtualFileTree
{ vftTree :: [FileTree]
, vftOriginalRoot :: FilePath
} deriving (Eq, Ord, Show)
data FileTree
= File FilePath Content -- ^ Create a file with the given content.
| Directory FilePath [FileTree] -- ^ Create a directory with the given files.
| CopiedDirectory FilePath -- ^ Copy a directory from the test data dir.
deriving (Show, Eq, Ord)
data Content
= Inline T.Text
| Ref FilePath
deriving (Show, Eq, Ord)
-- ----------------------------------------------------------------------------
-- API with side effects
-- ----------------------------------------------------------------------------
readFileFS :: FileSystem -> FilePath -> IO T.Text
readFileFS fs fp = do
T.readFile (fsRoot fs </> FP.normalise fp)
writeFileFS :: FileSystem -> FilePath -> Content -> IO ()
writeFileFS fs fp content = do
contents <- case content of
Inline txt -> pure txt
Ref path -> T.readFile (fsOriginalRoot fs </> FP.normalise path)
T.writeFile (fsRoot fs </> FP.normalise fp) contents
-- | Materialise a virtual file tree in the 'rootDir' directory.
--
-- Synopsis: @'materialise' rootDir fileTree testDataDir@
--
-- File references in '[FileTree]' are resolved relative to the @testDataDir@.
materialise :: FilePath -> [FileTree] -> FilePath -> IO FileSystem
materialise rootDir' fileTree testDataDir' = do
let testDataDir = FP.normalise testDataDir'
rootDir = FP.normalise rootDir'
persist :: FilePath -> FileTree -> IO ()
persist root (File name cts) = case cts of
Inline txt -> T.writeFile (root </> name) txt
Ref path -> copyFile (testDataDir </> FP.normalise path) (root </> takeFileName name)
persist root (Directory name nodes) = do
createDirectory (root </> name)
mapM_ (persist (root </> name)) nodes
persist root (CopiedDirectory name) = do
copyDir' root name
copyDir' :: FilePath -> FilePath -> IO ()
copyDir' root dir = do
files <- fmap FP.normalise . lines <$> withCurrentDirectory (testDataDir </> dir) (readProcess "git" ["ls-files", "--cached", "--modified", "--others"] "")
mapM_ (createDirectoryIfMissing True . ((root </>) . takeDirectory)) files
mapM_ (\f -> copyFile (testDataDir </> dir </> f) (root </> f)) files
return ()
traverse_ (persist rootDir) fileTree
pure $ FileSystem rootDir fileTree testDataDir
-- | Materialise a virtual file tree in the 'rootDir' directory.
--
-- Synopsis: @'materialiseVFT' rootDir virtualFileTree@
--
-- File references in 'virtualFileTree' are resolved relative to the @vftOriginalRoot@.
materialiseVFT :: FilePath -> VirtualFileTree -> IO FileSystem
materialiseVFT root fs = materialise root (vftTree fs) (vftOriginalRoot fs)
-- ----------------------------------------------------------------------------
-- Test definition helpers
-- ----------------------------------------------------------------------------
mkVirtualFileTree :: FilePath -> [FileTree] -> VirtualFileTree
mkVirtualFileTree testDataDir tree =
VirtualFileTree
{ vftTree = tree
, vftOriginalRoot = testDataDir
}
toAbsFp :: FileSystem -> FilePath -> FilePath
toAbsFp fs fp = fsRoot fs </> FP.normalise fp
toNfp :: FileSystem -> FilePath -> NormalizedFilePath
toNfp fs fp =
toNormalizedFilePath $ toAbsFp fs fp
-- ----------------------------------------------------------------------------
-- Builders
-- ----------------------------------------------------------------------------
-- | Create a file in the test project with some content.
--
-- Only the filename will be used, and any directory components are *not*
-- reflected in the test project.
file :: FilePath -> Content -> FileTree
file fp cts = File fp cts
-- | Copy a filepath into a test project. The name of the file is also used
-- in the test project.
--
-- The filepath is always resolved to the root of the test data dir.
copy :: FilePath -> FileTree
copy fp = File fp (Ref fp)
-- | Copy a directory into a test project.
-- The filepath is always resolved to the root of the test data dir.
copyDir :: FilePath -> FileTree
copyDir dir = CopiedDirectory dir
directory :: FilePath -> [FileTree] -> FileTree
directory name nodes = Directory name nodes
-- | Write the given test directly into a file.
text :: T.Text -> Content
text = Inline
-- | Read the contents of the given file
-- The filepath is always resolved to the root of the test data dir.
ref :: FilePath -> Content
ref = Ref
-- ----------------------------------------------------------------------------
-- Cradle Helpers
-- ----------------------------------------------------------------------------
-- | Set up a simple direct cradle.
--
-- All arguments are added to the direct cradle file.
-- Arguments will not be escaped.
directCradle :: [T.Text] -> FileTree
directCradle args =
file "hie.yaml"
( Inline $ T.unlines $
[ "cradle:"
, " direct:"
, " arguments:"
] <>
[ " - " <> arg | arg <- args]
)
-- | Set up a simple cabal cradle.
--
-- Prefer simple cabal cradle, over custom multi cabal cradles if possible.
simpleCabalCradle :: FileTree
simpleCabalCradle =
file "hie.yaml"
(Inline $ T.unlines
[ "cradle:"
, " cabal:"
]
)
-- ----------------------------------------------------------------------------
-- Project setup builders
-- ----------------------------------------------------------------------------
-- | Set up a test project with a single haskell file.
directProject :: FilePath -> [FileTree]
directProject fp =
[ directCradle [T.pack fp]
, file fp (Ref fp)
]
-- | Set up a test project with multiple haskell files.
--
directProjectMulti :: [FilePath] -> [FileTree]
directProjectMulti fps =
[ directCradle $ fmap T.pack fps
] <> fmap copy fps
-- | Set up a simple cabal cradle project and copy all the given filepaths
-- into the test directory.
simpleCabalProject :: [FilePath] -> [FileTree]
simpleCabalProject fps =
[ simpleCabalCradle
] <> fmap copy fps
-- | Set up a simple cabal cradle project.
simpleCabalProject' :: [FileTree] -> [FileTree]
simpleCabalProject' fps =
[ simpleCabalCradle
] <> fps
atomicFileWrite :: FilePath -> (FilePath -> IO a) -> IO a
atomicFileWrite targetPath write = do
let dir = takeDirectory targetPath
createDirectoryIfMissing True dir
(tempFilePath, cleanUp) <- newTempFileWithin dir
(write tempFilePath >>= \x -> renameFile tempFilePath targetPath >> pure x)
`onException` cleanUp
atomicFileWriteString :: FilePath -> String -> IO ()
atomicFileWriteString targetPath content =
atomicFileWrite targetPath (flip writeFile content)
atomicFileWriteStringUTF8 :: FilePath -> String -> IO ()
atomicFileWriteStringUTF8 targetPath content =
atomicFileWrite targetPath (flip writeFileUTF8 content)
atomicFileWriteText :: FilePath -> T.Text -> IO ()
atomicFileWriteText targetPath content =
atomicFileWrite targetPath (flip T.writeFile content)