lsp-types-2.4.0.0: test/URIFilePathSpec.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
-- We're testing our own deprecated function here!
{-# OPTIONS_GHC -Wno-deprecations #-}
{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}
#if MIN_VERSION_filepath(1,4,100)
#define OS_PATH
#endif
module URIFilePathSpec where
#ifdef OS_PATH
import qualified System.OsPath as OsPath
import Control.Exception (throwIO)
import Data.Maybe (fromJust)
#endif
import Control.Monad (when)
import Data.List
import Data.Text (Text, pack)
import Language.LSP.Protocol.Types
import GHC.IO.Encoding (setFileSystemEncoding)
import Network.URI
import System.FilePath (normalise)
import System.FilePath.Windows qualified as FPW
import System.IO
import System.Info qualified
import Test.Hspec
import Test.QuickCheck
-- ---------------------------------------------------------------------
isWindows :: Bool
isWindows = System.Info.os == "mingw32"
main :: IO ()
main = hspec spec
spec :: Spec
spec = do
describe "Platform aware URI file path functions" platformAwareUriFilePathSpec
describe "URI file path functions" uriFilePathSpec
describe "URI normalization functions" uriNormalizeSpec
describe "Normalized file path functions" normalizedFilePathSpec
windowsOS :: String
windowsOS = "mingw32"
testPosixUri :: Uri
testPosixUri = Uri $ pack "file:///home/myself/example.hs"
testPosixFilePath :: FilePath
testPosixFilePath = "/home/myself/example.hs"
relativePosixFilePath :: FilePath
relativePosixFilePath = "myself/example.hs"
testWindowsUri :: Uri
testWindowsUri = Uri $ pack "file:///c:/Users/myself/example.hs"
testWindowsFilePath :: FilePath
testWindowsFilePath = "c:\\Users\\myself\\example.hs"
platformAwareUriFilePathSpec :: Spec
platformAwareUriFilePathSpec = do
it "converts a URI to a POSIX file path" $ do
let theFilePath = platformAwareUriToFilePath "posix" testPosixUri
theFilePath `shouldBe` Just testPosixFilePath
it "converts a POSIX file path to a URI" $ do
let theUri = platformAwareFilePathToUri "posix" testPosixFilePath
theUri `shouldBe` testPosixUri
it "converts a URI to a Windows file path" $ do
let theFilePath = platformAwareUriToFilePath windowsOS testWindowsUri
theFilePath `shouldBe` Just testWindowsFilePath
it "converts a Windows file path to a URI" $ do
let theUri = platformAwareFilePathToUri windowsOS testWindowsFilePath
theUri `shouldBe` testWindowsUri
it "converts a POSIX file path to a URI" $ do
let theFilePath = platformAwareFilePathToUri "posix" "./Functional.hs"
theFilePath `shouldBe` (Uri "file://./Functional.hs")
it "converts a Windows file path to a URI" $ do
let theFilePath = platformAwareFilePathToUri windowsOS "./Functional.hs"
theFilePath `shouldBe` (Uri "file:///./Functional.hs")
it "converts a Windows file path to a URI" $ do
let theFilePath = platformAwareFilePathToUri windowsOS "c:/Functional.hs"
theFilePath `shouldBe` Uri "file:///c:/Functional.hs"
it "converts a POSIX file path to a URI and back" $ do
let theFilePath = platformAwareFilePathToUri "posix" "./Functional.hs"
theFilePath `shouldBe` Uri "file://./Functional.hs"
let Just (URI scheme' auth' path' query' frag') = parseURI "file://./Functional.hs"
(scheme', auth', path', query', frag')
`shouldBe` ( "file:"
, Just (URIAuth{uriUserInfo = "", uriRegName = ".", uriPort = ""}) -- AZ: Seems odd
, "/Functional.hs"
, ""
, ""
)
Just "./Functional.hs" `shouldBe` platformAwareUriToFilePath "posix" theFilePath
it "converts a Posix file path to a URI and back" $ property $ forAll genPosixFilePath $ \fp -> do
let uri = platformAwareFilePathToUri "posix" fp
platformAwareUriToFilePath "posix" uri `shouldBe` Just fp
it "converts a Windows file path to a URI and back" $ property $ forAll genWindowsFilePath $ \fp -> do
let uri = platformAwareFilePathToUri windowsOS fp
-- We normalise to account for changes in the path separator.
-- But driver letters are *not* normalized so we skip them
when (not $ "c:" `isPrefixOf` fp) $
platformAwareUriToFilePath windowsOS uri `shouldBe` Just (FPW.normalise fp)
it "converts a relative POSIX file path to a URI and back" $ do
let uri = platformAwareFilePathToUri "posix" relativePosixFilePath
uri `shouldBe` Uri "file://myself/example.hs"
let back = platformAwareUriToFilePath "posix" uri
back `shouldBe` Just relativePosixFilePath
testUri :: Uri
testUri
| isWindows = Uri "file:///C:/Users/myself/example.hs"
| otherwise = Uri "file:///home/myself/example.hs"
testFilePath :: FilePath
testFilePath
| isWindows = "C:\\Users\\myself\\example.hs"
| otherwise = "/home/myself/example.hs"
withCurrentDirFilePath :: FilePath
withCurrentDirFilePath
| isWindows = "C:\\Users\\.\\myself\\.\\.\\example.hs"
| otherwise = "/home/./myself/././example.hs"
fromRelativefilePathUri :: Uri
fromRelativefilePathUri
| isWindows = Uri "file:///myself/example.hs"
| otherwise = Uri "file://myself/example.hs"
relativeFilePath :: FilePath
relativeFilePath
| isWindows = "myself\\example.hs"
| otherwise = "myself/example.hs"
withLowerCaseDriveLetterFilePath :: FilePath
withLowerCaseDriveLetterFilePath = "c:\\Users\\.\\myself\\.\\.\\example.hs"
withInitialCurrentDirUriStr :: String
withInitialCurrentDirUriStr
| isWindows = "file:///Functional.hs"
| otherwise = "file://Functional.hs"
withInitialCurrentDirUriParts :: (String, Maybe URIAuth, String, String, String)
withInitialCurrentDirUriParts
| isWindows =
( "file:"
, Just (URIAuth{uriUserInfo = "", uriRegName = "", uriPort = ""}) -- JNS: And asymmetrical
, "/Functional.hs"
, ""
, ""
)
| otherwise =
( "file:"
, Just (URIAuth{uriUserInfo = "", uriRegName = "Functional.hs", uriPort = ""}) -- AZ: Seems odd
, ""
, ""
, ""
)
withInitialCurrentDirFilePath :: FilePath
withInitialCurrentDirFilePath
| isWindows = ".\\Functional.hs"
| otherwise = "./Functional.hs"
noNormalizedUriTxt :: Text
noNormalizedUriTxt
| isWindows = "file:///c:/Users/./myself/././example.hs"
| otherwise = "file:///home/./myself/././example.hs"
noNormalizedUri :: Uri
noNormalizedUri = Uri noNormalizedUriTxt
uriFilePathSpec :: Spec
uriFilePathSpec = do
it "converts a URI to a file path" $ do
let theFilePath = uriToFilePath testUri
theFilePath `shouldBe` Just testFilePath
it "converts a file path to a URI" $ do
let theUri = filePathToUri testFilePath
theUri `shouldBe` testUri
it "removes unnecessary current directory paths" $ do
let theUri = filePathToUri withCurrentDirFilePath
theUri `shouldBe` testUri
when isWindows $
it "make the drive letter upper case when converting a Windows file path to a URI" $ do
let theUri = filePathToUri withLowerCaseDriveLetterFilePath
theUri `shouldBe` testUri
it "converts a file path to a URI and back" $ property $ forAll genFilePath $ \fp -> do
let uri = filePathToUri fp
uriToFilePath uri `shouldBe` Just (normalise fp)
it "converts a relative file path to a URI and back" $ do
let uri = filePathToUri relativeFilePath
uri `shouldBe` fromRelativefilePathUri
let back = uriToFilePath uri
back `shouldBe` Just relativeFilePath
it "converts a file path with initial current dir to a URI and back" $ do
let uri = filePathToUri withInitialCurrentDirFilePath
uri `shouldBe` Uri (pack withInitialCurrentDirUriStr)
let Just (URI scheme' auth' path' query' frag') = parseURI withInitialCurrentDirUriStr
(scheme', auth', path', query', frag') `shouldBe` withInitialCurrentDirUriParts
Just "Functional.hs" `shouldBe` uriToFilePath uri
uriNormalizeSpec :: Spec
uriNormalizeSpec = do
it "ignores differences in percent-encoding" $ property $ \uri ->
toNormalizedUri (Uri $ pack $ escapeURIString isUnescapedInURI uri)
`shouldBe` toNormalizedUri (Uri $ pack $ escapeURIString (const False) uri)
it "ignores differences in percent-encoding (examples)" $ do
toNormalizedUri (Uri $ pack "http://server/path%C3%B1?param=%C3%B1")
`shouldBe` toNormalizedUri (Uri $ pack "http://server/path%c3%b1?param=%c3%b1")
toNormalizedUri (Uri $ pack "file:///path%2A")
`shouldBe` toNormalizedUri (Uri $ pack "file:///path%2a")
it "normalizes uri file path when converting from uri to normalized uri" $ do
let (NormalizedUri _ uri) = toNormalizedUri noNormalizedUri
let (Uri nuri) = testUri
uri `shouldBe` nuri
it "converts a file path with reserved uri chars to a normalized URI and back" $ do
let start = if isWindows then "C:\\" else "/"
let fp = start ++ "path;part#fragmen?param=val"
let nuri = toNormalizedUri (filePathToUri fp)
uriToFilePath (fromNormalizedUri nuri) `shouldBe` Just fp
it "converts a file path with substrings that looks like uri escaped chars and back" $ do
let start = if isWindows then "C:\\" else "/"
let fp = start ++ "ca%C3%B1a"
let nuri = toNormalizedUri (filePathToUri fp)
uriToFilePath (fromNormalizedUri nuri) `shouldBe` Just fp
it "converts a file path to a normalized URI and back" $ property $ forAll genFilePath $ \fp -> do
let nuri = toNormalizedUri (filePathToUri fp)
case uriToFilePath (fromNormalizedUri nuri) of
Just nfp -> nfp `shouldBe` (normalise fp)
Nothing -> return () -- Some unicode paths creates invalid uris, ignoring for now
genFilePath :: Gen FilePath
genFilePath
| isWindows = genWindowsFilePath
| otherwise = genPosixFilePath
genWindowsFilePath :: Gen FilePath
genWindowsFilePath = do
segments <- listOf1 pathSegment
pathSep <- elements ['/', '\\']
driveLetter <- elements ["C:", "c:"]
pure (driveLetter <> [pathSep] <> intercalate [pathSep] segments)
where
pathSegment = listOf1 (genValidUnicodeChar `suchThat` (`notElem` ['/', '\\', ':']))
genPosixFilePath :: Gen FilePath
genPosixFilePath = do
segments <- listOf1 pathSegment
pure ("/" <> intercalate "/" segments)
where
pathSegment = listOf1 (genValidUnicodeChar `suchThat` (`notElem` ['/']))
genValidUnicodeChar :: Gen Char
genValidUnicodeChar = arbitraryUnicodeChar `suchThat` isCharacter
where
isCharacter x = x /= '\65534' && x /= '\65535'
normalizedFilePathSpec :: Spec
normalizedFilePathSpec = beforeAll (setFileSystemEncoding utf8) $ do
it "makes file path normalized" $ property $ forAll genFilePath $ \fp -> do
let nfp = toNormalizedFilePath fp
fromNormalizedFilePath nfp `shouldBe` (normalise fp)
it "converts to a normalized uri and back" $ property $ forAll genFilePath $ \fp -> do
let nuri = normalizedFilePathToUri (toNormalizedFilePath fp)
case uriToNormalizedFilePath nuri of
Just nfp -> fromNormalizedFilePath nfp `shouldBe` (normalise fp)
Nothing -> return () -- Some unicode paths creates invalid uris, ignoring for now
it "converts a file path with reserved uri chars to a normalized URI and back" $ do
let start = if isWindows then "C:\\" else "/"
let fp = start ++ "path;part#fragmen?param=val"
let nuri = normalizedFilePathToUri (toNormalizedFilePath fp)
fmap fromNormalizedFilePath (uriToNormalizedFilePath nuri) `shouldBe` Just fp
it "converts a file path with substrings that looks like uri escaped chars and back" $ do
let start = if isWindows then "C:\\" else "/"
let fp = start ++ "ca%C3%B1a"
let nuri = normalizedFilePathToUri (toNormalizedFilePath fp)
fmap fromNormalizedFilePath (uriToNormalizedFilePath nuri) `shouldBe` Just fp
it "creates the same NormalizedUri than the older implementation" $ property $ forAll genFilePath $ \fp -> do
let nuri = normalizedFilePathToUri (toNormalizedFilePath fp)
let oldNuri = toNormalizedUri (filePathToUri fp)
nuri `shouldBe` oldNuri
#ifdef OS_PATH
it "converts to NormalizedFilePath and back sucessfully" $ property $ forAll genFilePath $ \fp -> do
let osPath = fromJust (OsPath.encodeUtf fp)
osPath' <- osPathToNormalizedFilePath osPath >>= normalizedFilePathToOsPath
osPath' `shouldBe` OsPath.normalise osPath
it "can not convert OsPath in non-standard encoding to NormalizedFilePath" $
-- Windows always use UTF16LE, the following test case doesn't apply
when (not isWindows) $
-- \184921 is an example that the raw bytes of UTF16 is not valid UTF8.
-- Case like this is not very common. I found it with the help of QuickCheck.
case OsPath.encodeWith utf16be utf16be "\184921" of
Left err -> throwIO err
Right osPath -> do
osPathToNormalizedFilePath osPath `shouldThrow` \(_ :: EncodingException) -> True
#endif