llm
Download
453 lines 19.6 KiBTomoModified 2026-09-02 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
# Tests for file paths

test "root paths exist"
    >> (/).exists()
    >> (~/).exists()
    assert (/).exists()
    assert (~/).exists()

test "path literals with special characters"
    >> (~/Downloads/file(1).txt)
    >> (/half\)paren)
    assert (~/Downloads/file(1).txt) == ~/Downloads/file(1).txt
    assert (/half\)paren) == /half\)paren

test "child paths"
    >> filename := "example.txt"
    >> (~).child(filename)
    assert (~).child(filename) == ~/example.txt

test "unique directory and subdirectories"
    >> tmpdir := (/tmp/tomo-test-path-XXXXXX).unique_directory()!
    >> (/tmp).subdirectories()!.has(tmpdir)
    assert (/tmp).subdirectories()!.has(tmpdir)

test "optional path"
    >> optional_path : Path? = ./foo
    >> optional_path
    assert optional_path == ./foo

test "reading and writing files"
    >> tmpdir := (/tmp/tomo-test-path-XXXXXX).unique_directory()!
    >> tmpfile := tmpdir ++ ./one.txt
    >> tmpfile.write("Hello world")!
    >> tmpfile.append("!")!
    >> tmpfile.read()
    >> tmpfile.read_bytes()!
    >> tmpdir.files()!.has(tmpfile)
    assert tmpfile.read() == "Hello world!"
    assert tmpfile.read_bytes()! == [0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x21]
    assert tmpdir.files()!.has(tmpfile)

    if tmp_lines := tmpfile.by_line() then
        assert [line for line in tmp_lines] == ["Hello world!"]
    else
        fail("Couldn't read lines in $tmpfile")

    assert (./does-not-exist.xxx).read() == none
    assert (./does-not-exist.xxx).read_bytes() == none
    if (./does-not-exist.xxx).by_line()
        fail("I could read lines in a nonexistent file")
    else
        pass

    >> tmpfile.remove()!

    >> tmpdir.files()!.has(tmpfile)
    assert not tmpdir.files()!.has(tmpfile)

    >> tmpdir.remove()!

test "enumeration preserves the form of the path"
    # children(), files(), subdirectories(), each_child(), walk() and glob() used
    # to resolve their argument against the current directory before reading it,
    # so every path they handed back was absolute no matter which form it was
    # asked about. Only the syscalls need a real path now, so `~` is expanded for
    # those and the caller still gets back what they asked in.
    home_dir := (~/.tomo-test-form-XXXXXX).unique_directory()!
    (home_dir ++ ./a.txt).write("")!
    (home_dir ++ ./sub).create_directory()!
    >> home_dir.components()![1]
    assert home_dir.components()![1] == "~"
    >> home_dir.children()
    # Child ordering is unspecified, so these compare sorted:
    assert home_dir.children()!.sorted() == [home_dir ++ ./a.txt, home_dir ++ ./sub].sorted()
    assert home_dir.files()! == [home_dir ++ ./a.txt]
    assert home_dir.subdirectories()! == [home_dir ++ ./sub]
    assert [c for c in home_dir.each_child()!].sorted() == [home_dir ++ ./a.txt, home_dir ++ ./sub].sorted()
    assert [p for p in home_dir.walk()!].sorted() == [home_dir, home_dir ++ ./a.txt, home_dir ++ ./sub].sorted()
    assert home_dir.glob("*.txt")! == [home_dir ++ ./a.txt]
    >> home_dir.remove()!

    rel_dir := (./tomo-test-form-XXXXXX).unique_directory()!
    (rel_dir ++ ./a.txt).write("")!
    >> rel_dir.components()![1]
    assert rel_dir.components()![1] == "."
    >> rel_dir.children()
    assert rel_dir.children()! == [rel_dir ++ ./a.txt]
    assert [p for p in rel_dir.walk()!].sorted() == [rel_dir, rel_dir ++ ./a.txt].sorted()
    assert rel_dir.glob("*.txt")! == [rel_dir ++ ./a.txt]
    >> rel_dir.remove()!

    abs_dir := (/tmp/tomo-test-form-XXXXXX).unique_directory()!
    (abs_dir ++ ./a.txt).write("")!
    assert abs_dir.children()! == [abs_dir ++ ./a.txt]
    >> abs_dir.remove()!

test "enumerating a directory that isn't there gives none"
    # These were typed as non-optional while the C returned a none sentinel:
    # children() surfaced as an empty list, indistinguishable from a real empty
    # directory, and each_child() surfaced as a null closure that segfaulted the
    # moment the loop called it.
    missing := (./tomo-no-such-directory)
    assert not missing.exists()
    assert missing.children() == none
    assert missing.files() == none
    assert missing.subdirectories() == none
    assert missing.each_child() == none

    # ...but a directory that exists and is empty is still an empty list:
    empty_dir := (/tmp/tomo-test-empty-XXXXXX).unique_directory()!
    >> empty_dir.children()
    assert empty_dir.children()! == []
    assert empty_dir.files()! == []
    assert empty_dir.subdirectories()! == []
    assert [c for c in empty_dir.each_child()!] == []
    >> empty_dir.remove()!

test "move refuses to overwrite unless asked, and understands ~"
    # Path$move called rename(2) directly, which replaces the destination
    # without complaint, so `overwrite=no` -- the default -- did not prevent
    # anything: moving onto an existing file destroyed it and reported Success.
    # It also passed the path to rename(2) unexpanded, so a "~" path failed
    # with ENOENT even though .exists() said otherwise.
    dir := (/tmp/tomo-test-move-XXXXXX).unique_directory()!
    (dir ++ ./src.txt).write("SOURCE")!
    (dir ++ ./dest.txt).write("DESTINATION")!

    >> (dir ++ ./src.txt).move(dir ++ ./dest.txt)
    assert (dir ++ ./dest.txt).read()! == "DESTINATION", "the destination was clobbered"
    assert (dir ++ ./src.txt).exists(), "the source was consumed by a move that failed"

    assert (dir ++ ./src.txt).move(dir ++ ./dest.txt, overwrite=yes).Success
    assert (dir ++ ./dest.txt).read()! == "SOURCE"

    # Home-based paths reach rename(2) expanded now:
    home_src := (~/.tomo-test-move-XXXXXX).unique_directory()!
    home_dest := home_src.sibling("tomo-test-move-destination")!
    assert home_src.move(home_dest).Success
    assert home_dest.is_directory()
    assert not home_src.exists()
    >> home_dest.remove()!
    >> dir.remove()!

test "set_owner leaves out what it is not given"
    # The none checks were inverted: passing an owner skipped the lookup and
    # left the uid as -1, while passing none looked up the empty user name, so
    # every call failed with "Not a valid user: ".
    dir := (/tmp/tomo-test-owner-XXXXXX).unique_directory()!
    file := dir ++ ./f.txt
    file.write("")!
    assert file.set_owner().Success
    if group := file.group()
        assert file.set_owner(group=group).Success
    assert not file.set_owner(owner="tomo-no-such-user").Success
    >> dir.remove()!

test "unique paths do not alias each other"
    # unique_directory() and write_unique() built their name in a static
    # buffer and handed it to Path$from_str(), which does not copy, so every
    # result pointed at the same storage: making a second temporary directory
    # silently rewrote the path of the first.
    a := (/tmp/tomo-test-alias-a-XXXXXX).unique_directory()!
    b := (/tmp/tomo-test-alias-b-XXXXXX).unique_directory()!
    >> a
    >> b
    assert a != b, "the first temporary directory was overwritten by the second"
    assert a.is_directory() and b.is_directory()
    >> a.remove()!
    >> b.remove()!

    f := (/tmp/tomo-test-alias-f-XXXXXX.txt).write_unique("first")!
    g := (/tmp/tomo-test-alias-g-XXXXXX.txt).write_unique("second")!
    assert f != g, "the first unique file was overwritten by the second"
    assert f.read()! == "first"
    assert g.read()! == "second"
    >> f.remove()!
    >> g.remove()!

test "the root has no parent and so no siblings"
    # sibling() appended "/../name" and normalized, so (/).sibling("x") gave
    # (/x) while (/).parent() gave none: the root had a sibling but nothing to
    # be a sibling of. It is written as the .parent().child(name) it is
    # documented to be now, so the two agree.
    assert (/).parent() == none
    assert (/).sibling("x") == none
    assert (/foo/baz).sibling("doop") == (/foo/doop)
    assert (/foo).sibling("bar") == (/bar)
    # It really is parent-then-child, for every form:
    for p in [(/foo/baz), (./foo/baz), (~/foo/baz), (../baz)]
        assert p.sibling("doop") == p.parent()!.child("doop")

    # "/.." is "/", so climbing past the root stays there:
    assert ((/) ++ (../..)) == (/)
    assert ((/foo/bar) ++ (../../..)) == (/)
    # ...while a relative path keeps climbing, having no root to stop at:
    assert ((..) ++ (..)) == (../..)

test "relative_to and resolved agree about the current directory"
    # relative_to() returned an empty path when the two sides matched, because
    # normalize_inplace()'s "nothing was written" fallback compared a pointer it
    # had already incremented and so never fired. An empty path is one the type
    # cannot otherwise hold: Path$from_str() maps "" to (/).
    assert (/tmp).relative_to((/tmp)) == (.)
    assert (/).relative_to((/)) == (.)

    # resolved() concatenated onto its base without making the base absolute,
    # so with the default base of (./) it returned a relative path despite its
    # name and its documented return value.
    assert (./foo).resolved() == Path.current_dir().child("foo")
    assert (.).resolved() == Path.current_dir()
    assert (./a/b).resolved(relative_to=(/tmp)) == (/tmp/a/b)

test "walking something that isn't there gives none"
    # walk() handed back an iterator over the path itself even when nothing was
    # at that path, so an empty tree and a missing one looked the same. It is
    # optional now, like each_child().
    assert (./tomo-no-such-path).walk() == none
    assert (.).walk() != none

    # A path that exists but is not a directory still walks over just itself:
    dir := (/tmp/tomo-test-walk-XXXXXX).unique_directory()!
    file := dir ++ ./only.txt
    file.write("")!
    assert [p for p in file.walk()!] == [file]
    assert [p for p in dir.walk()!].sorted() == [dir, file].sorted()
    >> dir.remove()!

test "filesystem operations do not race another process"
    # These used to look before they acted, which is a window another process
    # can change the answer in. move() now asks the kernel to rename only if
    # the destination does not exist, in one operation, and the rest either
    # attempt the act and read the error, or confirm what they found.
    dir := (/tmp/tomo-test-race-XXXXXX).unique_directory()!
    (dir ++ ./src.txt).write("SOURCE")!
    (dir ++ ./dest.txt).write("DESTINATION")!

    # move() and copy_to() both decline, and neither touches the destination:
    assert not (dir ++ ./src.txt).move(dir ++ ./dest.txt).Success
    assert not (dir ++ ./src.txt).copy_to(dir ++ ./dest.txt).Success
    assert (dir ++ ./dest.txt).read()! == "DESTINATION"
    assert (dir ++ ./src.txt).read()! == "SOURCE"

    # ...and overwrite=yes still replaces a file or a non-empty directory:
    assert (dir ++ ./src.txt).copy_to(dir ++ ./dest.txt, overwrite=yes).Success
    assert (dir ++ ./dest.txt).read()! == "SOURCE"
    (dir ++ ./d1).create_directory()!
    (dir ++ ./d2).create_directory()!
    (dir ++ ./d2/inner.txt).write("x")!
    assert (dir ++ ./d1).move(dir ++ ./d2, overwrite=yes).Success

    # create_directory() treats EEXIST as success, which is the race-free way
    # to create one, but only a directory counts:
    (dir ++ ./afile).write("x")!
    assert not (dir ++ ./afile).create_directory().Success
    assert (dir ++ ./realdir).create_directory().Success
    assert (dir ++ ./realdir).create_directory().Success # Already there is fine

    # remove() attempts the removal rather than asking what is there first:
    assert not (dir ++ ./gone).remove().Success
    assert (dir ++ ./gone).remove(ignore_missing=yes).Success
    >> dir.remove()!

test "a recursive remove reports failure instead of aborting"
    # nftw(3) passes no user data to its callback, so the callback used to
    # fail(), which exits the process: one unremovable file partway through a
    # recursive delete killed the program instead of returning Failure. It
    # leaves the reason in a _Thread_local for Path$remove() to collect now.
    dir := (/tmp/tomo-test-rm-XXXXXX).unique_directory()!
    (dir ++ ./sub/deeper).create_directory()!
    (dir ++ ./top.txt).write("")!
    (dir ++ ./sub/mid.txt).write("")!
    (dir ++ ./sub/deeper/leaf.txt).write("")!
    assert dir.remove().Success
    assert not dir.exists()

    # A missing path is a Failure the caller can act on, not an exit:
    gone := (/tmp/tomo-test-rm-not-there)
    assert not gone.remove().Success
    assert gone.remove(ignore_missing=yes).Success
    say("still running after a failed remove")

test "path components"
    >> p := /foo/baz.x/qux.tar.gz
    >> p.base_name()
    >> p.parent()
    >> p.extension()
    >> p.extension(full=no)
    assert p.base_name() == "qux.tar.gz"
    assert p.parent() == /foo/baz.x
    assert p.extension() == "tar.gz"
    assert p.extension(full=no) == "gz"
    assert p.has_extension("gz")
    assert p.has_extension(".gz")
    assert p.has_extension("tar.gz")
    assert not p.has_extension("txt")
    assert not p.has_extension("")
    assert (./foo).has_extension("")
    assert (..).has_extension("")
    assert not (~/.foo).has_extension("foo")
    # No "." in the name at all means no extension, which is none, not "":
    assert (~/.foo).extension() == none
    assert (~/foo).extension() == none
    assert (./foo.).extension() == none
    assert (..).extension() == none
    assert (~/.foo.baz.qux).extension() == "baz.qux"
    assert (~/x/.).parent() == (~)
    assert (~/x).parent() == (~)
    assert (.).parent() == (..)
    assert (..).parent() == (../..)
    assert (../foo).parent() == (..)
    assert (/).parent() == none

test "extension() and has_extension(\"\") agree"
    # For any name that is valid UTF-8, "the extension is none" and "it has no
    # extension" have to be the same question. The root used to answer no to
    # has_extension("") while reporting no extension.
    for p in [(./foo), (./foo.txt), (./.git), (./foo.), (./a.tar.gz), (..), (.), (/), (~), (~/x.y)]
        no_ext := p.has_extension("")
        assert (p.extension() == none) == no_ext,
            "$p: extension() is $(p.extension()) but has_extension of an empty text is $no_ext"

test "matches_glob matches path components from the back"
    # This used to be a single fnmatch() against the path's whole text, so
    # "*.txt" did not match (./file.txt) -- the leading "./" had to be written
    # out -- and the same file matched differently depending on whether the
    # path was written relative, absolute, or home-based. The pattern is now
    # split on "/" and matched against a trailing run of components.
    assert (./file.txt).matches_glob("*.txt")
    assert (/tmp/dir/file.txt).matches_glob("*.txt")
    assert (~/dir/file.txt).matches_glob("*.txt")
    assert not (./file.txt).matches_glob("*.jpg")

    # More components match a longer suffix, and depth is exact:
    assert (./dir/file.txt).matches_glob("dir/*.txt")
    assert (./dir/file.txt).matches_glob("./dir/*.txt")
    assert not (./dir/sub/file.txt).matches_glob("dir/*.txt")

    # A leading "/" can only line up at the front, so it anchors:
    assert (/tmp/dir/file.txt).matches_glob("/tmp/dir/*.txt")
    assert not (/other/dir/file.txt).matches_glob("/tmp/dir/*.txt")
    assert not (./dir/file.txt).matches_glob("/dir/*.txt")

    # "**" is zero or more components:
    assert (./a/b/c/file.txt).matches_glob("**/*.txt")
    assert (./file.txt).matches_glob("**/*.txt")
    assert not (./a/file.jpg).matches_glob("**/*.txt")
    assert (./a/b/file.txt).matches_glob("a/**/*.txt")
    assert (./a/file.txt).matches_glob("a/**/*.txt")
    assert (./anything/at/all).matches_glob("**")
    assert (/tmp/x/y).matches_glob("/tmp/**")
    assert not (/other/y).matches_glob("/tmp/**")

    # Wildcards stay inside one component, and a leading "." is literal:
    assert not (./dir/.hidden).matches_glob("*")
    assert (./dir/.hidden).matches_glob(".*")
    assert (./src/file.c).matches_glob("*.[ch]")
    assert not (./src/file.o).matches_glob("*.[ch]")

    # Still no brace alternation, and an empty pattern matches nothing:
    assert not (./src/file.c).matches_glob("*.{c,h}")
    assert not (./src/file.c).matches_glob("")

test "everything glob() returns matches the pattern it came from"
    # The two used to disagree: glob() would hand back (./dir/a.txt) for the
    # pattern (./dir/*.txt), which matches_glob then said did not match.
    dir := (/tmp/tomo-test-glob-XXXXXX).unique_directory()!
    (dir ++ ./a.txt).write("")!
    (dir ++ ./b.txt).write("")!
    (dir ++ ./c.jpg).write("")!
    (dir ++ ./sub).create_directory()!
    (dir ++ ./sub/d.txt).write("")!

    for pattern in ["*.txt", "*", "*.[tj]*", "sub/*.txt"]
        matches := dir.glob(pattern)!
        for f in matches
            assert f.matches_glob(pattern),
                "glob() returned $f for $pattern but matches_glob disagrees"

    # And the bare name matches wherever the file sits:
    for f in dir.glob("sub/*.txt")!
        assert f.matches_glob("*.txt")
        assert f.matches_glob("**/*.txt")
        assert f.matches_glob("sub/*.txt")

    # "**" is answered by walking rather than by glob(3), which cannot express
    # it, so the two branches have to agree with the matcher equally:
    deep := dir.glob("**/*.txt")!
    assert deep.length == 3, "expected a.txt, b.txt and sub/d.txt, got $deep"
    for f in deep
        assert f.matches_glob("**/*.txt"), "$f does not match the pattern it came from"
    assert dir.glob("**/*.jpg")! == [dir ++ ./c.jpg]

    # A directory that cannot be read is none, not an empty list:
    assert (./tomo-no-such-directory).glob("*") == none
    # ...while a pattern that matches nothing is an empty list:
    assert dir.glob("*.nothing")! == []

    >> dir.remove()!

test "paths are normalized however they are built"
    # Path literals were left exactly as written, so (./path/to/) and
    # (./path/to) were different values naming the same directory, and
    # (/foo/../bar) stayed unresolved. A literal has no interpolation, so its
    # text is entirely known while compiling: it is normalized there, and the
    # generated code carries the result rather than redoing it on every run.
    assert (/foo/../bar) == (/bar)
    assert (./path/to/) == (./path/to)
    assert (./a/./b) == (./a/b)
    assert (/a//b) == (/a/b)
    assert (~/x/../y) == (~/y)

    # The forms that mean themselves are untouched:
    assert "$(.)" == "."
    assert "$(..)" == ".."
    assert "$(/)" == "/"
    assert "$(~)" == "~"
    # ...and "~" is a stand-in for an unknown directory, so nothing pops it:
    assert "$((~).parent()!)" == "~/.."

    # Text is not known until it is run, so that normalizes when it is built:
    assert Path.from_text("./a/../b") == (./b)
    assert (.).child("a/../b") == (./b)
    assert ((/tmp) ++ (./a/../b)) == (/tmp/b)

test "path concatenation"
    # Concatenation tests:
    assert /foo ++ ./baz == /foo/baz
    assert /foo/bar ++ . == /foo/bar
    assert /foo/ ++ ./baz == /foo/baz
    assert /foo/bar ++ ./baz/ == /foo/bar/baz
    assert /foo/bar ++ ./baz/./qux == /foo/bar/baz/qux
    assert /foo/bar ++ ./baz/qux/../quux == /foo/bar/baz/quux
    assert /foo/bar/baz ++ ../qux == /foo/bar/qux
    assert /foo/bar ++ ././../baz == /foo/baz
    assert /foo ++ ./baz/../qux == /foo/qux
    assert /foo ++ ./baz//qux == /foo/baz/qux
    assert /foo/bar/baz ++ ./.././qux/./../quux == /foo/bar/quux
    >> (.).glob("*.tm")

test "a path literal at the end of the file parses without hanging"
    # parse_path's scan loop used to exit at the end-of-file boundary without
    # advancing, producing a zero-width AST that every enclosing parse loop
    # re-parsed forever: `x := 12..round()` hung the compiler. The trailing
    # path below sits against the end of this file to pin the fix.
    p := ..
    assert p == ..

test "path literals with non-ASCII characters"
    >> ./café.txt
    assert (./café.txt) == ./café.txt
    assert (./café.txt).base_name() == "café.txt"
    >> ~/日本語/naïve.txt
    assert (~/日本語/naïve.txt).base_name() == "naïve.txt"