(namespace test.path (test.case "path.sep" (assert.eq! path.sep (match platform.os-family (:windows "\\") (_ "/"))) ) (test.case "path.root" ;; Relative paths and empty string have no root on all platforms. (assert.eq! (path.root "relative") "") (assert.eq! (path.root "") "") (match platform.os-family (:windows (seq ;; Rooted by backslash or forward-slash → canonical "\" (assert.eq! (path.root "\\a\\b") "\\") (assert.eq! (path.root "/a/b") "\\") ;; Drive-letter absolute ("X:\" or "X:/") (assert.eq! (path.root "C:\\a\\b") "C:\\") (assert.eq! (path.root "C:/a/b") "C:\\") (assert.eq! (path.root "C:\\") "C:\\") ;; Drive letter WITHOUT a separator is relative on Windows (assert.eq! (path.root "C:") "") (assert.eq! (path.root "C:relative") "") )) (_ (seq (assert.eq! (path.root "/") "/") (assert.eq! (path.root "/a/b") "/") )) ) ) (test.case "path.absolute?" ;; "/" is recognised as absolute on all platforms. (assert.ok! (path.absolute? "/absolute/unix")) (assert.ok! (path.absolute? "/")) (assert.not! (path.absolute? "relative/unix")) (assert.not! (path.absolute? "./relative/unix")) (match platform.os-family (:windows (seq ;; Drive-letter prefix (both separators) (assert.ok! (path.absolute? "C:\\absolute\\windows")) (assert.ok! (path.absolute? "C:/absolute/windows")) ;; UNC / rooted backslash (assert.ok! (path.absolute? "\\absolute\\unc")) ;; Relative Windows paths (assert.not! (path.absolute? "relative\\windows")) (assert.not! (path.absolute? ".\\relative\\windows")) ;; Drive letter without separator is relative (assert.not! (path.absolute? "C:relative")) )) (_ (void)) ) ) (test.case "path.relative?" (assert.ok! (path.relative? "relative/unix")) (assert.not! (path.relative? "/absolute/unix")) (match platform.os-family (:windows (seq (assert.ok! (path.relative? "relative\\windows")) (assert.not! (path.relative? "C:\\absolute\\windows")) (assert.not! (path.relative? "\\absolute\\unc")) (assert.ok! (path.relative? "C:relative")) )) (_ (void)) ) ) (test.case "path.split" (assert.eq! (path.split "") []) (assert.eq! (path.split "a") ["a"]) (match platform.os-family (:windows (seq (assert.eq! (path.split "a\\b\\c") ["a" "b" "c"]) ;; Windows also accepts "/" as separator (assert.eq! (path.split "a/b/c") ["a" "b" "c"]) ;; Drive-letter prefix is kept as the first component (round-trip with join) (assert.eq! (path.split "C:\\a\\b") ["C:" "a" "b"]) (assert.eq! (path.split "C:/a/b") ["C:" "a" "b"]) )) (_ (seq (assert.eq! (path.split "a/b/c") ["a" "b" "c"]) )) ) ) (test.case "path.join" (assert.eq! (path.join []) "") (assert.eq! (path.join ["a"]) "a") (match platform.os-family (:windows (seq (assert.eq! (path.join ["a" "b" "c"]) "a\\b\\c") (assert.eq! (path.join ["C:" "a" "b"]) "C:\\a\\b") )) (_ (seq (assert.eq! (path.join ["a" "b" "c"]) (str.concat "a" path.sep "b" path.sep "c")) )) ) ) (test.case "path.normalize" (match platform.os-family (:windows (seq ;; Dot component is stripped (assert.eq! (path.normalize "a\\.\\b") (path.join ["a" "b"])) ;; Double-dot collapses parent (assert.eq! (path.normalize "a\\..\\b") "b") ;; Rooted-backslash absolute path (assert.eq! (path.normalize "\\a\\..\\b") (str.concat path.sep "b")) ;; Bug fix: absolute path collapsing to root returns the root, not "" (assert.eq! (path.normalize "\\") "\\") (assert.eq! (path.normalize "\\a\\..") "\\") ;; Drive-letter paths: no spurious leading "\" added (assert.eq! (path.normalize "C:\\a\\..\\b") "C:\\b") (assert.eq! (path.normalize "C:\\") "C:\\") (assert.eq! (path.normalize "C:\\a\\b\\..\\..") "C:\\") ;; Windows normalises "/" separators as well (assert.eq! (path.normalize "a/./b") (path.join ["a" "b"])) (assert.eq! (path.normalize "C:/a/../b") "C:\\b") )) (_ (seq ;; Dot component is stripped (assert.eq! (path.normalize "a/./b") (path.join ["a" "b"])) ;; Double-dot collapses parent (assert.eq! (path.normalize "a/../b") "b") ;; Rooted absolute path (assert.eq! (path.normalize "/a/../b") (str.concat path.sep "b")) ;; Bug fix: absolute path collapsing to root returns "/", not "" (assert.eq! (path.normalize "/") "/") (assert.eq! (path.normalize "/a/..") "/") (assert.eq! (path.normalize "/a/b/../..") "/") )) ) ) (test.case "path.parent" ;; Top-level relative path always returns "" on every platform. (assert.eq! (path.parent "a") "") (match platform.os-family (:windows (seq (assert.eq! (path.parent "a\\b\\c") "a\\b") (assert.eq! (path.parent "a\\b") "a") ;; Bug fix: rooted paths return "\" not hardcoded "/" (assert.eq! (path.parent "\\") "\\") (assert.eq! (path.parent "\\..\\a\\.\\..\\") "\\") ;; Drive-letter paths (assert.eq! (path.parent "C:\\") "C:\\") (assert.eq! (path.parent "C:\\a") "C:\\") (assert.eq! (path.parent "C:\\a\\b") "C:\\a") (assert.eq! (path.parent "C:\\a\\b\\c") "C:\\a\\b") )) (_ (seq (assert.eq! (path.parent "a/b/c") "a/b") (assert.eq! (path.parent "a/b") "a") (assert.eq! (path.parent "/") "/") (assert.eq! (path.parent "/../a/./../") "/") )) ) ) (test.case "path.basename" (assert.eq! (path.basename "file.txt") "file.txt") (match platform.os-family (:windows (seq (assert.eq! (path.basename "a\\b\\c") "c") ;; Trailing separator → empty basename (assert.eq! (path.basename "a\\b\\") "") (assert.eq! (path.basename "C:\\Users\\file.txt") "file.txt") ;; Windows also handles "/" separator (assert.eq! (path.basename "a/b/c") "c") )) (_ (seq (assert.eq! (path.basename "a/b/c") "c") (assert.eq! (path.basename "a/b/") "") )) ) ) (test.case "path.ext" ;; Basic cases work on all platforms (basename is a simple name) (assert.eq! (path.ext "file.txt") ".txt") (assert.eq! (path.ext "file") "") (assert.eq! (path.ext ".hidden") "") (assert.eq! (path.ext "archive.tar.gz") ".gz") (assert.eq! (path.ext "file.") "") (assert.eq! (path.ext "") "") (match platform.os-family (:windows (seq (assert.eq! (path.ext "C:\\Users\\file.txt") ".txt") (assert.eq! (path.ext "a\\b\\no-ext") "") )) (_ (seq (assert.eq! (path.ext "a/b/file.txt") ".txt") (assert.eq! (path.ext "a/b/no-ext") "") )) ) ) (test.case "path.stem" (assert.eq! (path.stem "file.txt") "file") (assert.eq! (path.stem "file") "file") (assert.eq! (path.stem ".hidden") ".hidden") (assert.eq! (path.stem "archive.tar.gz") "archive.tar") (assert.eq! (path.stem "") "") (match platform.os-family (:windows (seq (assert.eq! (path.stem "C:\\Users\\file.txt") "file") )) (_ (seq (assert.eq! (path.stem "a/b/file.txt") "file") )) ) ) (test.case "path.with-ext" (assert.eq! (path.with-ext "file.txt" ".md") "file.md") (assert.eq! (path.with-ext "file.txt" "") "file") (assert.eq! (path.with-ext "file" ".txt") "file.txt") (assert.eq! (path.with-ext "archive.tar.gz" ".bz2") "archive.tar.bz2") ;; Hidden file: no extension → new-ext is appended (assert.eq! (path.with-ext ".hidden" ".txt") ".hidden.txt") ;; Directory prefix is preserved (match platform.os-family (:windows (seq (assert.eq! (path.with-ext "C:\\a\\file.txt" ".md") "C:\\a\\file.md") )) (_ (seq (assert.eq! (path.with-ext "a/b/file.txt" ".md") "a/b/file.md") )) ) ) (test.case "path.resolve" (match platform.os-family (:windows (seq ;; Relative path is joined to base and normalized (assert.eq! (path.resolve "C:\\base" "file.txt") "C:\\base\\file.txt") ;; ".." is resolved against base (assert.eq! (path.resolve "C:\\base\\dir" "..\\file.txt") "C:\\base\\file.txt") ;; Absolute path ignores base (assert.eq! (path.resolve "C:\\other" "C:\\absolute") "C:\\absolute") ;; Deep traversal (assert.eq! (path.resolve "C:\\a\\b\\c" "..\\..\\d") "C:\\a\\d") )) (_ (seq (assert.eq! (path.resolve "/base" "file.txt") "/base/file.txt") (assert.eq! (path.resolve "/base/dir" "../file.txt") "/base/file.txt") ;; Absolute path ignores base (assert.eq! (path.resolve "/other" "/absolute") "/absolute") ;; Deep traversal (assert.eq! (path.resolve "/a/b/c" "../../d") "/a/d") ;; Relative base + relative path (assert.eq! (path.resolve "base/dir" "file.txt") "base/dir/file.txt") )) ) ) (test.case "path.relative-to" (match platform.os-family (:windows (seq ;; Child path (assert.eq! (path.relative-to "C:\\a\\b" "C:\\a\\b\\c\\d") "c\\d") ;; Sibling path (assert.eq! (path.relative-to "C:\\a\\b" "C:\\a\\c") "..\\c") ;; Ancestor path (assert.eq! (path.relative-to "C:\\a\\b\\c" "C:\\a") "..\\..") ;; Same path → "." (assert.eq! (path.relative-to "C:\\a\\b" "C:\\a\\b") ".") ;; From root (assert.eq! (path.relative-to "C:\\" "C:\\a\\b") "a\\b") ;; Different drives → return normalized target unchanged (assert.eq! (path.relative-to "C:\\a" "D:\\b") "D:\\b") )) (_ (seq (assert.eq! (path.relative-to "/a/b" "/a/b/c/d") "c/d") (assert.eq! (path.relative-to "/a/b" "/a/c") "../c") (assert.eq! (path.relative-to "/a/b/c" "/a") "../..") ;; Same path → "." (assert.eq! (path.relative-to "/a/b" "/a/b") ".") ;; From root (assert.eq! (path.relative-to "/" "/a/b") "a/b") ;; Relative paths (assert.eq! (path.relative-to "a/b" "a/b/c") "c") (assert.eq! (path.relative-to "a/b" "a/c") "../c") )) ) ) )