hslua-module-path (empty) → 0.0.1
raw patch · 7 files changed
+876/−0 lines, 7 filesdep +basedep +filepathdep +hslua
Dependencies added: base, filepath, hslua, hslua-module-path, tasty, tasty-hunit, tasty-lua, text
Files
- CHANGELOG.md +11/−0
- LICENSE +21/−0
- README.md +183/−0
- hslua-module-path.cabal +71/−0
- src/Foreign/Lua/Module/Path.hs +429/−0
- test/test-hslua-module-path.hs +52/−0
- test/test-path.lua +109/−0
+ CHANGELOG.md view
@@ -0,0 +1,11 @@+# Changelog++`hslua-module-paths` uses [PVP Versioning][1].+The changelog is available [on GitHub][2].++## 0.0.1++* Initially created.++[1]: https://pvp.haskell.org+[2]: https://github.com/hslua/hslua-module-path/releases
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2020 Albert Krewinkel++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,183 @@+# hslua-module-path++[![GitHub CI][CI badge]](https://github.com/hslua/hslua-module-paths/actions)+[![Hackage][Hackage badge]](https://hackage.haskell.org/package/hslua-module-path)+[![Stackage Lts][Stackage Lts badge]](http://stackage.org/lts/package/hslua-module-path)+[![Stackage Nightly][Stackage Nightly badge]](http://stackage.org/nightly/package/hslua-module-path)+[![MIT license][License badge]](LICENSE)++[CI badge]: https://github.com/hslua/hslua-module-path/workflows/CI/badge.svg+[Hackage badge]: https://img.shields.io/hackage/v/hslua-module-path.svg?logo=haskell+[Stackage Lts badge]: http://stackage.org/package/hslua-module-path/badge/lts+[Stackage Nightly badge]: http://stackage.org/package/hslua-module-path/badge/nightly+[License badge]: https://img.shields.io/badge/license-MIT-blue.svg++Lua module to work with file paths.++## path++Module for file path manipulations.++#### separator++The character that separates directories.++#### search\_path\_separator++The character that is used to separate the entries in the `PATH`+environment variable.++### Functions++#### directory (filepath)++Get the directory name; move up one level.++Parameters:++filepath +path (string)++Returns:++- The filepath up to the last directory separator. (string)++#### filename (filepath)++Get the file name.++Parameters:++filepath +path (string)++Returns:++- File name part of the input path. (string)++#### is\_absolute (filepath)++Checks whether a path is absolute, i.e. not fixed to a root.++Parameters:++filepath +path (string)++Returns:++- `true` iff `filepath` is an absolute path, `false` otherwise.+ (boolean)++#### is\_relative (filepath)++Checks whether a path is relative or fixed to a root.++Parameters:++filepath +path (string)++Returns:++- `true` iff `filepath` is a relative path, `false` otherwise.+ (boolean)++#### join (filepaths)++Join path elements back together by the directory separator.++Parameters:++filepaths +path components (list of strings)++Returns:++- The joined path. (string)++#### make\_relative (path, root, unsafe)++Contract a filename, based on a relative path. Note that the resulting+path will never introduce `..` paths, as the presence of symlinks means+`../b` may not reach `a/b` if it starts from `a/c`. For a worked example+see [this blog+post](http://neilmitchell.blogspot.co.uk/2015/10/filepaths-are-subtle-symlinks-are-hard.html).++Parameters:++path +path to be made relative (string)++root +root path (string)++unsafe +whether to allow `..` in the result. (boolean)++Returns:++- contracted filename (string)++#### normalize (filepath)++Normalizes a path.++- `//` outside of the drive can be made blank+- `/` becomes the `path.separator`+- `./` -> ’’+- an empty path becomes `.`++Parameters:++filepath +path (string)++Returns:++- The normalized path. (string)++#### split (filepath)++Splits a path by the directory separator.++Parameters:++filepath +path (string)++Returns:++- List of all path components. (list of strings)++#### split\_extension (filepath)++Splits the last extension from a file path and returns the parts. The+extension, if present, includes the leading separator; if the path has+no extension, then the empty string is returned as the extension.++Parameters:++filepath +path (string)++Returns:++- filepath without extension (string)++- extension or empty string (string)++#### split\_search\_path (search\_path)++Takes a string and splits it on the `search_path_separator` character.+Blank items are ignored on Windows, and converted to `.` on Posix. On+Windows path elements are stripped of quotes.++Parameters:++search\_path +platform-specific search path (string)++Returns:++- list of directories in search path (list of strings)
+ hslua-module-path.cabal view
@@ -0,0 +1,71 @@+cabal-version: 2.4+name: hslua-module-path+version: 0.0.1+synopsis: Lua module to work with file paths.+description: Lua module to work with file paths in a platform+ independent way.+homepage: https://github.com/hslua/hslua-module-path+bug-reports: https://github.com/hslua/hslua-module-path/issues+license: MIT+license-file: LICENSE+author: Albert Krewinkel+maintainer: Albert Krewinkel <albert@zeitkraut.de>+copyright: © 2020-2021 Albert Krewinkel+category: Foreign+build-type: Simple+extra-doc-files: README.md+ CHANGELOG.md+extra-source-files: test/test-path.lua+tested-with: GHC == 8.0.2+ , GHC == 8.2.2+ , GHC == 8.4.4+ , GHC == 8.6.5+ , GHC == 8.8.4+ , GHC == 8.10.2++source-repository head+ type: git+ location: https://github.com/hslua/hslua-module-path.git++common common-options+ build-depends: base >= 4.9.1 && < 5+ , filepath >= 1.4 && < 1.5+ , hslua >= 1.2 && < 1.4+ , text >= 1.0 && < 1.3+ + ghc-options: -Wall+ -Wcompat+ -Widentities+ -Wincomplete-uni-patterns+ -Wincomplete-record-updates+ if impl(ghc >= 8.0)+ ghc-options: -Wredundant-constraints+ if impl(ghc >= 8.2)+ ghc-options: -fhide-source-paths+ if impl(ghc >= 8.4)+ ghc-options: -Wmissing-export-lists+ -Wpartial-fields+ if impl(ghc >= 8.8)+ ghc-options: -Wmissing-deriving-strategies++ default-language: Haskell2010++library+ import: common-options+ hs-source-dirs: src+ exposed-modules: Foreign.Lua.Module.Path++test-suite hslua-module-path-test+ import: common-options+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: test-hslua-module-path.hs+ build-depends: base+ , hslua-module-path+ , tasty+ , tasty-hunit+ , tasty-lua >= 0.2 && < 0.3+ , text+ ghc-options: -threaded+ -rtsopts+ -with-rtsopts=-N
+ src/Foreign/Lua/Module/Path.hs view
@@ -0,0 +1,429 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-|+Module : Foreign.Lua.Module.Path+Copyright : © 2020 Albert Krewinkel+License : MIT+Maintainer : Albert Krewinkel <albert+hslua@zeitkraut.de>+Stability : alpha+Portability : Requires GHC 8 or later.++Lua module to work with file paths.+-}+module Foreign.Lua.Module.Path (+ -- * Module+ pushModule+ , preloadModule+ , documentedModule++ -- * Path manipulations+ , add_extension+ , combine+ , directory+ , filename+ , is_absolute+ , is_relative+ , join+ , make_relative+ , normalize+ , split+ , split_extension+ , split_search_path+ , treat_strings_as_paths+ )+where++import Control.Monad (forM_)+import Data.Char (toLower)+#if !MIN_VERSION_base(4,11,0)+import Data.Semigroup (Semigroup(..)) -- includes (<>)+#endif+import Data.Text (Text)+import Foreign.Lua+ ( Lua, NumResults (..), getglobal, getmetatable, nth, pop, rawset+ , remove, top )+import Foreign.Lua.Call+import Foreign.Lua.Module hiding (preloadModule, pushModule)+import Foreign.Lua.Peek (Peeker, peekBool, peekList, peekString)+import Foreign.Lua.Push (pushBool, pushList, pushString, pushText)++import qualified Data.Text as T+import qualified Foreign.Lua.Module as Module+import qualified System.FilePath as Path++--+-- Module+--++description :: Text+description = "Module for file path manipulations."++documentedModule :: Module+documentedModule = Module+ { moduleName = "path"+ , moduleFields = fields+ , moduleDescription = description+ , moduleFunctions = functions+ }++-- | Pushes the @path@ module to the Lua stack.+pushModule :: Lua NumResults+pushModule = 1 <$ pushModule' documentedModule++-- | Add the @path@ module under the given name to the table of+-- preloaded packages.+preloadModule :: String -> Lua ()+preloadModule name = Module.preloadModule $+ documentedModule { moduleName = T.pack name }++-- | Helper function which pushes the module with its fields. This+-- function should be removed once the respective hslua bug has been+-- fixed.+pushModule' :: Module -> Lua ()+pushModule' mdl = do+ Module.pushModule mdl+ forM_ (moduleFields mdl) $ \field -> do+ pushText (fieldName field)+ fieldPushValue field+ rawset (nth 3)++--+-- Fields+--++-- | Exported fields.+fields :: [Field]+fields =+ [ separator+ , search_path_separator+ ]++-- | Wrapper for @'Path.pathSeparator'@.+separator :: Field+separator = Field+ { fieldName = "separator"+ , fieldDescription = "The character that separates directories."+ , fieldPushValue = pushString [Path.pathSeparator]+ }++-- | Wrapper for @'Path.searchPathSeparator'@.+search_path_separator :: Field+search_path_separator = Field+ { fieldName = "search_path_separator"+ , fieldDescription = "The character that is used to separate the entries in "+ <> "the `PATH` environment variable."+ , fieldPushValue = pushString [Path.searchPathSeparator]+ }++--+-- Functions+--++functions :: [(Text, HaskellFunction)]+functions =+ [ ("directory", directory)+ , ("filename", filename)+ , ("is_absolute", is_absolute)+ , ("is_relative", is_relative)+ , ("join", join)+ , ("make_relative", make_relative)+ , ("normalize", normalize)+ , ("split", split)+ , ("split_extension", split_extension)+ , ("split_search_path", split_search_path)+ , ("treat_strings_as_paths", treat_strings_as_paths)+ ]++-- | See @Path.takeDirectory@+directory :: HaskellFunction+directory = toHsFnPrecursor Path.normalise+ <#> filepathParam+ =#> [filepathResult "The filepath up to the last directory separator."]+ #? "Get the directory name; move up one level."++-- | See @Path.takeFilename@+filename :: HaskellFunction+filename = toHsFnPrecursor Path.takeFileName+ <#> filepathParam+ =#> [filepathResult "File name part of the input path."]+ #? "Get the file name."++-- | See @Path.isAbsolute@+is_absolute :: HaskellFunction+is_absolute = toHsFnPrecursor Path.isAbsolute+ <#> filepathParam+ =#> [booleanResult ("`true` iff `filepath` is an absolute path, " <>+ "`false` otherwise.")]+ #? "Checks whether a path is absolute, i.e. not fixed to a root."++-- | See @Path.isRelative@+is_relative :: HaskellFunction+is_relative = toHsFnPrecursor Path.isRelative+ <#> filepathParam+ =#> [booleanResult ("`true` iff `filepath` is a relative path, " <>+ "`false` otherwise.")]+ #? "Checks whether a path is relative or fixed to a root."++-- | See @Path.joinPath@+join :: HaskellFunction+join = toHsFnPrecursor Path.joinPath+ <#> Parameter+ { parameterPeeker = peekList peekFilePath+ , parameterDoc = ParameterDoc+ { parameterName = "filepaths"+ , parameterType = "list of strings"+ , parameterDescription = "path components"+ , parameterIsOptional = False+ }+ }+ =#> [filepathResult "The joined path."]+ #? "Join path elements back together by the directory separator."++make_relative :: HaskellFunction+make_relative = toHsFnPrecursor makeRelative+ <#> parameter+ peekFilePath+ "string"+ "path"+ "path to be made relative"+ <#> parameter+ peekFilePath+ "string"+ "root"+ "root path"+ <#> optionalParameter+ peekBool+ "boolean"+ "unsafe"+ "whether to allow `..` in the result."+ =#> [filepathResult "contracted filename"]+ #? mconcat+ [ "Contract a filename, based on a relative path. Note that the "+ , "resulting path will never introduce `..` paths, as the "+ , "presence of symlinks means `../b` may not reach `a/b` if it "+ , "starts from `a/c`. For a worked example see "+ , "[this blog post](http://neilmitchell.blogspot.co.uk"+ , "/2015/10/filepaths-are-subtle-symlinks-are-hard.html)."+ ]++-- | See @Path.normalise@+normalize :: HaskellFunction+normalize = toHsFnPrecursor Path.normalise+ <#> filepathParam+ =#> [filepathResult "The normalized path."]+ #? T.unlines+ [ "Normalizes a path."+ , ""+ , "- `//` outside of the drive can be made blank"+ , "- `/` becomes the `path.separator`"+ , "- `./` -> \'\'"+ , "- an empty path becomes `.`"+ ]++-- | See @Path.splitDirectories@.+--+-- Note that this does /not/ wrap @'Path.splitPath'@, as that function+-- adds trailing slashes to each directory, which is often inconvenient.+split :: HaskellFunction+split = toHsFnPrecursor Path.splitDirectories+ <#> filepathParam+ =#> [filepathListResult "List of all path components."]+ #? "Splits a path by the directory separator."++-- | See @Path.splitExtension@+split_extension :: HaskellFunction+split_extension = toHsFnPrecursor Path.splitExtension+ <#> filepathParam+ =#> [ FunctionResult+ { fnResultPusher = pushString . fst+ , fnResultDoc = FunctionResultDoc+ { functionResultType = "string"+ , functionResultDescription = "filepath without extension"+ }+ },+ FunctionResult+ { fnResultPusher = pushString . snd+ , fnResultDoc = FunctionResultDoc+ { functionResultType = "string"+ , functionResultDescription = "extension or empty string"+ }+ }+ ]+ #? ("Splits the last extension from a file path and returns the parts. "+ <> "The extension, if present, includes the leading separator; "+ <> "if the path has no extension, then the empty string is returned "+ <> "as the extension.")++-- | Wraps function @'Path.splitSearchPath'@.+split_search_path :: HaskellFunction+split_search_path = toHsFnPrecursor Path.splitSearchPath+ <#> Parameter+ { parameterPeeker = peekString+ , parameterDoc = ParameterDoc+ { parameterName = "search_path"+ , parameterType = "string"+ , parameterDescription = "platform-specific search path"+ , parameterIsOptional = False+ }+ }+ =#> [filepathListResult "list of directories in search path"]+ #? ("Takes a string and splits it on the `search_path_separator` "+ <> "character. Blank items are ignored on Windows, "+ <> "and converted to `.` on Posix. "+ <> "On Windows path elements are stripped of quotes.")++-- | Join two paths with a directory separator. Wraps @'Path.combine'@.+combine :: HaskellFunction+combine = toHsFnPrecursor Path.combine+ <#> filepathParam+ <#> filepathParam+ =#> [filepathResult "combined paths"]+ #? "Combine two paths with a path separator."++-- | Adds an extension to a file path. Wraps @'Path.addExtension'@.+add_extension :: HaskellFunction+add_extension = toHsFnPrecursor Path.addExtension+ <#> filepathParam+ <#> Parameter+ { parameterPeeker = peekString+ , parameterDoc = ParameterDoc+ { parameterName = "extension"+ , parameterType = "string"+ , parameterDescription = "an extension, with or without separator dot"+ , parameterIsOptional = False+ }+ }+ =#> [filepathResult "filepath with extension"]+ #? "Adds an extension, even if there is already one."++stringAugmentationFunctions :: [(String, HaskellFunction)]+stringAugmentationFunctions =+ [ ("directory", directory)+ , ("filename", filename)+ , ("is_absolute", is_absolute)+ , ("is_relative", is_relative)+ , ("normalize", normalize)+ , ("split", split)+ , ("split_extension", split_extension)+ , ("split_search_path", split_search_path)+ ]++treat_strings_as_paths :: HaskellFunction+treat_strings_as_paths = HaskellFunction+ { callFunction = do+ let addField (k, v) =+ pushString k *> pushHaskellFunction v *> rawset (nth 3)+ -- for some reason we can't just dump all functions into the+ -- string metatable, but have to use the string module for+ -- non-metamethods.+ pushString "" *> getmetatable top *> remove (nth 2)+ mapM_ addField $ [("__add", add_extension), ("__div", combine)]+ pop 1 -- string metatable++ getglobal "string"+ mapM_ addField stringAugmentationFunctions+ pop 1 -- string module++ return (0 :: NumResults)+ , functionDoc = Nothing+ }+ #? "Augment the string module such that strings can be used as path objects."++--+-- Parameters+--++-- | Retrieves a file path from the stack.+peekFilePath :: Peeker FilePath+peekFilePath = peekString++-- | Filepath function parameter.+filepathParam :: Parameter FilePath+filepathParam = Parameter+ { parameterPeeker = peekFilePath+ , parameterDoc = ParameterDoc+ { parameterName = "filepath"+ , parameterType = "string"+ , parameterDescription = "path"+ , parameterIsOptional = False+ }+ }++-- | Result of a function returning a file path.+filepathResult :: Text -- ^ Description+ -> FunctionResult FilePath+filepathResult desc = FunctionResult+ { fnResultPusher = \fp -> pushString fp+ , fnResultDoc = FunctionResultDoc+ { functionResultType = "string"+ , functionResultDescription = desc+ }+ }++-- | List of filepaths function result.+filepathListResult :: Text -- ^ Description+ -> FunctionResult [FilePath]+filepathListResult desc = FunctionResult+ { fnResultPusher = \fp -> pushList pushString fp+ , fnResultDoc = FunctionResultDoc+ { functionResultType = "list of strings"+ , functionResultDescription = desc+ }+ }++-- | Boolean function result.+booleanResult :: Text -- ^ Description+ -> FunctionResult Bool+booleanResult desc = FunctionResult+ { fnResultPusher = \b -> pushBool b+ , fnResultDoc = FunctionResultDoc+ { functionResultType = "boolean"+ , functionResultDescription = desc+ }+ }++--+-- Helpers+--++-- | Alternative version of @'Path.makeRelative'@, which introduces @..@+-- paths if desired.+makeRelative :: FilePath -- ^ path to be made relative+ -> FilePath -- ^ root directory from which to start+ -> Maybe Bool -- ^ whether to use unsafe relative paths.+ -> FilePath+makeRelative path root unsafe+ | Path.equalFilePath root path = "."+ | takeAbs root /= takeAbs path = path+ | otherwise = go (dropAbs path) (dropAbs root)+ where+ go x "" = dropWhile Path.isPathSeparator x+ go x y =+ let (x1, x2) = breakPath x+ (y1, y2) = breakPath y+ in case () of+ _ | Path.equalFilePath x1 y1 -> go x2 y2+ _ | unsafe == Just True -> Path.joinPath ["..", x1, go x2 y2]+ _ -> path++ breakPath = both (dropWhile Path.isPathSeparator)+ . break Path.isPathSeparator+ . dropWhile Path.isPathSeparator++ both f (a, b) = (f a, f b)++ leadingPathSepOnWindows = \case+ "" -> False+ x | Path.hasDrive x -> False+ c:_ -> Path.isPathSeparator c++ dropAbs x = if leadingPathSepOnWindows x then tail x else Path.dropDrive x++ takeAbs x = if leadingPathSepOnWindows x+ then [Path.pathSeparator]+ else map (\y ->+ if Path.isPathSeparator y+ then Path.pathSeparator+ else toLower y)+ (Path.takeDrive x)
+ test/test-hslua-module-path.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE OverloadedStrings #-}+{-|+Module : Main+Copyright : © 2021 Albert Krewinkel+License : MIT+Maintainer : Albert Krewinkel <albert+hslua@zeitkraut.de>+Stability : stable+Portability : Requires language extensions ForeignFunctionInterface,+ OverloadedStrings.++Tests for the `path` Lua module.+-}++import Control.Monad (void)+import Foreign.Lua (Lua)+import Foreign.Lua.Module.Path (preloadModule, pushModule)+import Test.Tasty (TestTree, defaultMain, testGroup)+import Test.Tasty.HUnit (assertEqual, testCase)+import Test.Tasty.Lua (translateResultsFromFile)++import qualified Foreign.Lua as Lua++main :: IO ()+main = do+ luaTestResults <- Lua.run $ do+ Lua.openlibs+ Lua.requirehs "path" (void pushModule)+ translateResultsFromFile "test/test-path.lua"+ defaultMain $ testGroup "hslua-module-path" [tests, luaTestResults]++-- | HSpec tests for the Lua 'system' module+tests :: TestTree+tests = testGroup "HsLua path module"+ [ testCase "path module can be pushed to the stack" $+ Lua.run (void pushModule)++ , testCase "path module can be added to the preloader" . Lua.run $ do+ Lua.openlibs+ preloadModule "path"+ assertEqual' "function not added to preloader" Lua.TypeFunction =<< do+ Lua.getglobal' "package.preload.path"+ Lua.ltype (-1)++ , testCase "path module can be loaded as hspath" . Lua.run $ do+ Lua.openlibs+ preloadModule "hspath"+ assertEqual' "loading the module fails " Lua.OK =<<+ Lua.dostring "require 'hspath'"+ ]++assertEqual' :: (Show a, Eq a) => String -> a -> a -> Lua ()+assertEqual' msg expected = Lua.liftIO . assertEqual msg expected
+ test/test-path.lua view
@@ -0,0 +1,109 @@+--+-- Tests for the system module+--+local path = require 'path'+local tasty = require 'tasty'++local group = tasty.test_group+local test = tasty.test_case+local assert = tasty.assert++-- Check existence static fields+return {+ group 'static fields' {+ test('separator', function ()+ assert.are_equal(type(path.separator), 'string')+ end),+ test('search_path_separator', function ()+ assert.are_equal(type(path.search_path_separator), 'string')+ end),+ },++ group 'splitting/joining' {+ test('split_path is inverse of join', function ()+ local paths = {'one', 'two', 'test'}+ assert.are_same(path.split(path.join(paths)), paths)+ end),+ },++ group 'split_extensions' {+ test('filename', function ()+ assert.are_equal(path.split_extension 'image.jpg', 'image')+ end),+ test('extension', function ()+ assert.is_truthy(select(2, path.split_extension 'thesis.tex'), '.tex')+ assert.are_equal(select(2, path.split_extension 'fstab'), '')+ end),+ test('concat gives inverts split', function ()+ local filenames = {'/etc/passwd', '34a90-1.bat', 'backup.tar.gz'}+ for _, filename in ipairs(filenames) do+ local base, ext = path.split_extension(filename)+ assert.are_equal(base .. ext, filename)+ end+ end),+ },++ group 'normalize' {+ test('removes leading `./`', function ()+ assert.are_equal(path.normalize('./a.md'), 'a.md')+ end),+ test('dedupe path separators', function ()+ assert.are_equal(path.normalize('a//b'), path.join{'a', 'b'})+ end)+ },++ group 'relative or absolute' {+ test('xor', function ()+ local test_paths = {+ path.join{ 'hello', 'rudi'},+ path.join{ '.', 'autoexec.bat'},+ path.join{ 'C:', 'config.sys'},+ path.join{ '/', 'etc', 'passwd'}+ }+ for _, fp in ipairs(test_paths) do+ assert.is_truthy(path.is_relative(fp) == not path.is_absolute(fp))+ end+ end)+ },++ group 'strings as path objects' {+ test('setup', path.treat_strings_as_paths),+ test('split extension', function ()+ local img = 'mandrill.jpg'+ assert.are_equal(img:split_extension(), 'mandrill')+ end),+ test('conbine paths with `/`', function ()+ assert.are_equal('a' / 'b', path.join{'a', 'b'})+ end),+ test('add extension with `+`', function ()+ assert.are_equal('a' + 'b', path.join{'a.b'})+ end),+ },++ group 'make_relative' {+ test('just the filename if file is within path', function()+ assert.are_equal(+ path.make_relative('/foo/bar/file.txt', '/foo/bar'),+ 'file.txt'+ )+ end),+ test('no change if name outside of reference dir', function()+ assert.are_equal(+ path.make_relative('/foo/baz/file.txt', '/foo/bar'),+ '/foo/baz/file.txt'+ )+ end),+ test('use `..` when allowing unsafe operation', function()+ assert.are_equal(+ path.make_relative('/foo/baz/file.txt', '/foo/bar', true),+ '../baz/file.txt'+ )+ end),+ test('return dot if both paths are the same', function()+ assert.are_equal(+ path.make_relative('/one/two/three', '/one/two/three/'),+ '.'+ )+ end)+ },+}