packages feed

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 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)+  },+}