// Copyright (c) 2016 Sandstorm Development Group, Inc. and contributors // Licensed under the MIT License: // // 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. #include "filesystem.h" #include "test.h" namespace kj { namespace { KJ_TEST("Path") { KJ_EXPECT(Path(nullptr).toString() == "."); KJ_EXPECT(Path(nullptr).toString(true) == "/"); KJ_EXPECT(Path("foo").toString() == "foo"); KJ_EXPECT(Path("foo").toString(true) == "/foo"); KJ_EXPECT(Path({"foo", "bar"}).toString() == "foo/bar"); KJ_EXPECT(Path({"foo", "bar"}).toString(true) == "/foo/bar"); KJ_EXPECT(Path::parse("foo/bar").toString() == "foo/bar"); KJ_EXPECT(Path::parse("foo//bar").toString() == "foo/bar"); KJ_EXPECT(Path::parse("foo/./bar").toString() == "foo/bar"); KJ_EXPECT(Path::parse("foo/../bar").toString() == "bar"); KJ_EXPECT(Path::parse("foo/bar/..").toString() == "foo"); KJ_EXPECT(Path::parse("foo/bar/../..").toString() == "."); KJ_EXPECT(Path({"foo", "bar"}).eval("baz").toString() == "foo/bar/baz"); KJ_EXPECT(Path({"foo", "bar"}).eval("./baz").toString() == "foo/bar/baz"); KJ_EXPECT(Path({"foo", "bar"}).eval("baz/qux").toString() == "foo/bar/baz/qux"); KJ_EXPECT(Path({"foo", "bar"}).eval("baz//qux").toString() == "foo/bar/baz/qux"); KJ_EXPECT(Path({"foo", "bar"}).eval("baz/./qux").toString() == "foo/bar/baz/qux"); KJ_EXPECT(Path({"foo", "bar"}).eval("baz/../qux").toString() == "foo/bar/qux"); KJ_EXPECT(Path({"foo", "bar"}).eval("baz/qux/..").toString() == "foo/bar/baz"); KJ_EXPECT(Path({"foo", "bar"}).eval("../baz").toString() == "foo/baz"); KJ_EXPECT(Path({"foo", "bar"}).eval("baz/../../qux/").toString() == "foo/qux"); KJ_EXPECT(Path({"foo", "bar"}).eval("/baz/qux").toString() == "baz/qux"); KJ_EXPECT(Path({"foo", "bar"}).eval("//baz/qux").toString() == "baz/qux"); KJ_EXPECT(Path({"foo", "bar"}).eval("/baz/../qux").toString() == "qux"); KJ_EXPECT(Path({"foo", "bar"}).basename()[0] == "bar"); KJ_EXPECT(Path({"foo", "bar", "baz"}).parent().toString() == "foo/bar"); KJ_EXPECT(Path({"foo", "bar"}).append("baz").toString() == "foo/bar/baz"); KJ_EXPECT(Path({"foo", "bar"}).append(Path({"baz", "qux"})).toString() == "foo/bar/baz/qux"); { // Test methods which are overloaded for && on a non-rvalue path. Path path({"foo", "bar"}); KJ_EXPECT(path.eval("baz").toString() == "foo/bar/baz"); KJ_EXPECT(path.eval("./baz").toString() == "foo/bar/baz"); KJ_EXPECT(path.eval("baz/qux").toString() == "foo/bar/baz/qux"); KJ_EXPECT(path.eval("baz//qux").toString() == "foo/bar/baz/qux"); KJ_EXPECT(path.eval("baz/./qux").toString() == "foo/bar/baz/qux"); KJ_EXPECT(path.eval("baz/../qux").toString() == "foo/bar/qux"); KJ_EXPECT(path.eval("baz/qux/..").toString() == "foo/bar/baz"); KJ_EXPECT(path.eval("../baz").toString() == "foo/baz"); KJ_EXPECT(path.eval("baz/../../qux/").toString() == "foo/qux"); KJ_EXPECT(path.eval("/baz/qux").toString() == "baz/qux"); KJ_EXPECT(path.eval("/baz/../qux").toString() == "qux"); KJ_EXPECT(path.basename()[0] == "bar"); KJ_EXPECT(path.parent().toString() == "foo"); KJ_EXPECT(path.append("baz").toString() == "foo/bar/baz"); KJ_EXPECT(path.append(Path({"baz", "qux"})).toString() == "foo/bar/baz/qux"); } KJ_EXPECT(kj::str(Path({"foo", "bar"})) == "foo/bar"); } KJ_TEST("Path exceptions") { KJ_EXPECT_THROW_MESSAGE("invalid path component", Path("")); KJ_EXPECT_THROW_MESSAGE("invalid path component", Path(".")); KJ_EXPECT_THROW_MESSAGE("invalid path component", Path("..")); KJ_EXPECT_THROW_MESSAGE("NUL character", Path(StringPtr("foo\0bar", 7))); KJ_EXPECT_THROW_MESSAGE("break out of starting", Path::parse("..")); KJ_EXPECT_THROW_MESSAGE("break out of starting", Path::parse("../foo")); KJ_EXPECT_THROW_MESSAGE("break out of starting", Path::parse("foo/../..")); KJ_EXPECT_THROW_MESSAGE("expected a relative path", Path::parse("/foo")); KJ_EXPECT_THROW_MESSAGE("NUL character", Path::parse(kj::StringPtr("foo\0bar", 7))); KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"foo", "bar"}).eval("../../..")); KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"foo", "bar"}).eval("../baz/../../..")); KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"foo", "bar"}).eval("baz/../../../..")); KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"foo", "bar"}).eval("/..")); KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"foo", "bar"}).eval("/baz/../..")); KJ_EXPECT_THROW_MESSAGE("root path has no basename", Path(nullptr).basename()); KJ_EXPECT_THROW_MESSAGE("root path has no parent", Path(nullptr).parent()); } KJ_TEST("Win32 Path") { KJ_EXPECT(Path({"foo", "bar"}).toWin32String() == "foo\\bar"); KJ_EXPECT(Path({"foo", "bar"}).toWin32String(true) == "\\\\foo\\bar"); KJ_EXPECT(Path({"c:", "foo", "bar"}).toWin32String(true) == "c:\\foo\\bar"); KJ_EXPECT(Path({"A:", "foo", "bar"}).toWin32String(true) == "A:\\foo\\bar"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz").toWin32String() == "foo\\bar\\baz"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32("./baz").toWin32String() == "foo\\bar\\baz"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz/qux").toWin32String() == "foo\\bar\\baz\\qux"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz//qux").toWin32String() == "foo\\bar\\baz\\qux"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz/./qux").toWin32String() == "foo\\bar\\baz\\qux"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz/../qux").toWin32String() == "foo\\bar\\qux"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz/qux/..").toWin32String() == "foo\\bar\\baz"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32("../baz").toWin32String() == "foo\\baz"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz/../../qux/").toWin32String() == "foo\\qux"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32(".\\baz").toWin32String() == "foo\\bar\\baz"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz\\qux").toWin32String() == "foo\\bar\\baz\\qux"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz\\\\qux").toWin32String() == "foo\\bar\\baz\\qux"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz\\.\\qux").toWin32String() == "foo\\bar\\baz\\qux"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz\\..\\qux").toWin32String() == "foo\\bar\\qux"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz\\qux\\..").toWin32String() == "foo\\bar\\baz"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32("..\\baz").toWin32String() == "foo\\baz"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz\\..\\..\\qux\\").toWin32String() == "foo\\qux"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz\\../..\\qux/").toWin32String() == "foo\\qux"); KJ_EXPECT(Path({"c:", "foo", "bar"}).evalWin32("/baz/qux") .toWin32String(true) == "c:\\baz\\qux"); KJ_EXPECT(Path({"c:", "foo", "bar"}).evalWin32("\\baz\\qux") .toWin32String(true) == "c:\\baz\\qux"); KJ_EXPECT(Path({"c:", "foo", "bar"}).evalWin32("d:\\baz\\qux") .toWin32String(true) == "d:\\baz\\qux"); KJ_EXPECT(Path({"c:", "foo", "bar"}).evalWin32("d:\\baz\\..\\qux") .toWin32String(true) == "d:\\qux"); KJ_EXPECT(Path({"c:", "foo", "bar"}).evalWin32("\\\\baz\\qux") .toWin32String(true) == "\\\\baz\\qux"); KJ_EXPECT(Path({"foo", "bar"}).evalWin32("d:\\baz\\..\\qux") .toWin32String(true) == "d:\\qux"); KJ_EXPECT(Path({"foo", "bar", "baz"}).evalWin32("\\qux") .toWin32String(true) == "\\\\foo\\bar\\qux"); } KJ_TEST("Win32 Path exceptions") { KJ_EXPECT_THROW_MESSAGE("colons are prohibited", Path({"c:", "foo", "bar"}).toWin32String()); KJ_EXPECT_THROW_MESSAGE("colons are prohibited", Path({"c:", "foo:bar"}).toWin32String(true)); KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"con"}).toWin32String()); KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"CON", "bar"}).toWin32String()); KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"foo", "cOn"}).toWin32String()); KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"prn"}).toWin32String()); KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"aux"}).toWin32String()); KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"NUL"}).toWin32String()); KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"nul.txt"}).toWin32String()); KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"com3"}).toWin32String()); KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"lpt9"}).toWin32String()); KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"com1.hello"}).toWin32String()); KJ_EXPECT_THROW_MESSAGE("drive letter or netbios", Path({"?", "foo"}).toWin32String(true)); KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"foo", "bar"}).evalWin32("../../..")); KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"foo", "bar"}).evalWin32("../baz/../../..")); KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"foo", "bar"}).evalWin32("baz/../../../..")); KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"foo", "bar"}).evalWin32("c:\\..\\..")); KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"c:", "foo", "bar"}).evalWin32("/baz/../../..")); KJ_EXPECT_THROW_MESSAGE("must specify drive letter", Path({"foo"}).evalWin32("\\baz\\qux")); } KJ_TEST("WriteMode operators") { WriteMode createOrModify = WriteMode::CREATE | WriteMode::MODIFY; KJ_EXPECT(has(createOrModify, WriteMode::MODIFY)); KJ_EXPECT(has(createOrModify, WriteMode::CREATE)); KJ_EXPECT(!has(createOrModify, WriteMode::CREATE_PARENT)); KJ_EXPECT(has(createOrModify, createOrModify)); KJ_EXPECT(!has(createOrModify, createOrModify | WriteMode::CREATE_PARENT)); KJ_EXPECT(!has(createOrModify, WriteMode::CREATE | WriteMode::CREATE_PARENT)); KJ_EXPECT(!has(WriteMode::CREATE, createOrModify)); KJ_EXPECT(createOrModify != WriteMode::MODIFY); KJ_EXPECT(createOrModify != WriteMode::CREATE); KJ_EXPECT(createOrModify - WriteMode::CREATE == WriteMode::MODIFY); KJ_EXPECT(WriteMode::CREATE + WriteMode::MODIFY == createOrModify); // Adding existing bit / subtracting non-existing bit are no-ops. KJ_EXPECT(createOrModify + WriteMode::MODIFY == createOrModify); KJ_EXPECT(createOrModify - WriteMode::CREATE_PARENT == createOrModify); } // ====================================================================================== class TestClock final: public Clock { public: void tick() { time += 1 * SECONDS; } Date now() override { return time; } void expectChanged(FsNode& file) { KJ_EXPECT(file.stat().lastModified == time); time += 1 * SECONDS; } void expectUnchanged(FsNode& file) { KJ_EXPECT(file.stat().lastModified != time); } private: Date time = UNIX_EPOCH + 1 * SECONDS; }; KJ_TEST("InMemoryFile") { TestClock clock; auto file = newInMemoryFile(clock); clock.expectChanged(*file); KJ_EXPECT(file->readAllText() == ""); clock.expectUnchanged(*file); file->writeAll("foo"); clock.expectChanged(*file); KJ_EXPECT(file->readAllText() == "foo"); file->write(3, StringPtr("bar").asBytes()); clock.expectChanged(*file); KJ_EXPECT(file->readAllText() == "foobar"); file->write(3, StringPtr("baz").asBytes()); clock.expectChanged(*file); KJ_EXPECT(file->readAllText() == "foobaz"); file->write(9, StringPtr("qux").asBytes()); clock.expectChanged(*file); KJ_EXPECT(file->readAllText() == kj::StringPtr("foobaz\0\0\0qux", 12)); file->truncate(6); clock.expectChanged(*file); KJ_EXPECT(file->readAllText() == "foobaz"); file->truncate(18); clock.expectChanged(*file); KJ_EXPECT(file->readAllText() == kj::StringPtr("foobaz\0\0\0\0\0\0\0\0\0\0\0\0", 18)); { auto mapping = file->mmap(0, 18); auto privateMapping = file->mmapPrivate(0, 18); auto writableMapping = file->mmapWritable(0, 18); clock.expectUnchanged(*file); KJ_EXPECT(mapping.size() == 18); KJ_EXPECT(privateMapping.size() == 18); KJ_EXPECT(writableMapping->get().size() == 18); clock.expectUnchanged(*file); KJ_EXPECT(writableMapping->get().begin() == mapping.begin()); KJ_EXPECT(privateMapping.begin() != mapping.begin()); KJ_EXPECT(kj::str(mapping.slice(0, 6).asChars()) == "foobaz"); KJ_EXPECT(kj::str(privateMapping.slice(0, 6).asChars()) == "foobaz"); clock.expectUnchanged(*file); file->write(0, StringPtr("qux").asBytes()); clock.expectChanged(*file); KJ_EXPECT(kj::str(mapping.slice(0, 6).asChars()) == "quxbaz"); KJ_EXPECT(kj::str(privateMapping.slice(0, 6).asChars()) == "foobaz"); file->write(12, StringPtr("corge").asBytes()); KJ_EXPECT(kj::str(mapping.slice(12, 17).asChars()) == "corge"); // Can shrink. file->truncate(6); KJ_EXPECT(kj::str(mapping.slice(12, 17).asChars()) == kj::StringPtr("\0\0\0\0\0", 5)); // Can regrow. file->truncate(18); KJ_EXPECT(kj::str(mapping.slice(12, 17).asChars()) == kj::StringPtr("\0\0\0\0\0", 5)); // Can't grow past previoous capacity. KJ_EXPECT_THROW_MESSAGE("cannot resize the file backing store", file->truncate(100)); clock.expectChanged(*file); writableMapping->changed(writableMapping->get().slice(0, 3)); clock.expectChanged(*file); writableMapping->sync(writableMapping->get().slice(0, 3)); clock.expectChanged(*file); } // But now we can since the mapping is gone. file->truncate(100); file->truncate(6); clock.expectChanged(*file); KJ_EXPECT(file->readAllText() == "quxbaz"); file->zero(3, 3); clock.expectChanged(*file); KJ_EXPECT(file->readAllText() == StringPtr("qux\0\0\0", 6)); } KJ_TEST("InMemoryFile::copy()") { TestClock clock; auto source = newInMemoryFile(clock); source->writeAll("foobarbaz"); auto dest = newInMemoryFile(clock); dest->writeAll("quxcorge"); clock.expectChanged(*dest); KJ_EXPECT(dest->copy(3, *source, 6, kj::maxValue) == 3); clock.expectChanged(*dest); KJ_EXPECT(dest->readAllText() == "quxbazge"); KJ_EXPECT(dest->copy(0, *source, 3, 4) == 4); clock.expectChanged(*dest); KJ_EXPECT(dest->readAllText() == "barbazge"); KJ_EXPECT(dest->copy(0, *source, 128, kj::maxValue) == 0); clock.expectUnchanged(*dest); KJ_EXPECT(dest->copy(4, *source, 3, 0) == 0); clock.expectUnchanged(*dest); String bigString = strArray(repeat("foobar", 10000), ""); source->truncate(bigString.size() + 1000); source->write(123, bigString.asBytes()); dest->copy(321, *source, 123, bigString.size()); KJ_EXPECT(dest->readAllText().slice(321) == bigString); } KJ_TEST("File::copy()") { TestClock clock; auto source = newInMemoryFile(clock); source->writeAll("foobarbaz"); auto dest = newInMemoryFile(clock); dest->writeAll("quxcorge"); clock.expectChanged(*dest); KJ_EXPECT(dest->File::copy(3, *source, 6, kj::maxValue) == 3); clock.expectChanged(*dest); KJ_EXPECT(dest->readAllText() == "quxbazge"); KJ_EXPECT(dest->File::copy(0, *source, 3, 4) == 4); clock.expectChanged(*dest); KJ_EXPECT(dest->readAllText() == "barbazge"); KJ_EXPECT(dest->File::copy(0, *source, 128, kj::maxValue) == 0); clock.expectUnchanged(*dest); KJ_EXPECT(dest->File::copy(4, *source, 3, 0) == 0); clock.expectUnchanged(*dest); String bigString = strArray(repeat("foobar", 10000), ""); source->truncate(bigString.size() + 1000); source->write(123, bigString.asBytes()); dest->File::copy(321, *source, 123, bigString.size()); KJ_EXPECT(dest->readAllText().slice(321) == bigString); } KJ_TEST("InMemoryDirectory") { TestClock clock; auto dir = newInMemoryDirectory(clock); clock.expectChanged(*dir); KJ_EXPECT(dir->listNames() == nullptr); KJ_EXPECT(dir->listEntries() == nullptr); KJ_EXPECT(!dir->exists(Path("foo"))); KJ_EXPECT(dir->tryOpenFile(Path("foo")) == nullptr); KJ_EXPECT(dir->tryOpenFile(Path("foo"), WriteMode::MODIFY) == nullptr); clock.expectUnchanged(*dir); { auto file = dir->openFile(Path("foo"), WriteMode::CREATE); clock.expectChanged(*dir); file->writeAll("foobar"); clock.expectUnchanged(*dir); } clock.expectUnchanged(*dir); KJ_EXPECT(dir->exists(Path("foo"))); clock.expectUnchanged(*dir); { auto stats = dir->lstat(Path("foo")); clock.expectUnchanged(*dir); KJ_EXPECT(stats.type == FsNode::Type::FILE); KJ_EXPECT(stats.size == 6); } { auto list = dir->listNames(); clock.expectUnchanged(*dir); KJ_ASSERT(list.size() == 1); KJ_EXPECT(list[0] == "foo"); } { auto list = dir->listEntries(); clock.expectUnchanged(*dir); KJ_ASSERT(list.size() == 1); KJ_EXPECT(list[0].name == "foo"); KJ_EXPECT(list[0].type == FsNode::Type::FILE); } KJ_EXPECT(dir->openFile(Path("foo"))->readAllText() == "foobar"); clock.expectUnchanged(*dir); KJ_EXPECT(dir->tryOpenFile(Path({"foo", "bar"}), WriteMode::MODIFY) == nullptr); KJ_EXPECT(dir->tryOpenFile(Path({"bar", "baz"}), WriteMode::MODIFY) == nullptr); KJ_EXPECT_THROW_MESSAGE("parent is not a directory", dir->tryOpenFile(Path({"bar", "baz"}), WriteMode::CREATE)); clock.expectUnchanged(*dir); { auto file = dir->openFile(Path({"bar", "baz"}), WriteMode::CREATE | WriteMode::CREATE_PARENT); clock.expectChanged(*dir); file->writeAll("bazqux"); clock.expectUnchanged(*dir); } clock.expectUnchanged(*dir); KJ_EXPECT(dir->openFile(Path({"bar", "baz"}))->readAllText() == "bazqux"); clock.expectUnchanged(*dir); { auto stats = dir->lstat(Path("bar")); clock.expectUnchanged(*dir); KJ_EXPECT(stats.type == FsNode::Type::DIRECTORY); } { auto list = dir->listNames(); clock.expectUnchanged(*dir); KJ_ASSERT(list.size() == 2); KJ_EXPECT(list[0] == "bar"); KJ_EXPECT(list[1] == "foo"); } { auto list = dir->listEntries(); clock.expectUnchanged(*dir); KJ_ASSERT(list.size() == 2); KJ_EXPECT(list[0].name == "bar"); KJ_EXPECT(list[0].type == FsNode::Type::DIRECTORY); KJ_EXPECT(list[1].name == "foo"); KJ_EXPECT(list[1].type == FsNode::Type::FILE); } { auto subdir = dir->openSubdir(Path("bar")); clock.expectUnchanged(*dir); clock.expectUnchanged(*subdir); KJ_EXPECT(subdir->openFile(Path("baz"))->readAllText() == "bazqux"); clock.expectUnchanged(*subdir); } auto subdir = dir->openSubdir(Path("corge"), WriteMode::CREATE); clock.expectChanged(*dir); subdir->openFile(Path("grault"), WriteMode::CREATE)->writeAll("garply"); clock.expectUnchanged(*dir); clock.expectChanged(*subdir); KJ_EXPECT(dir->openFile(Path({"corge", "grault"}))->readAllText() == "garply"); dir->openFile(Path({"corge", "grault"}), WriteMode::CREATE | WriteMode::MODIFY) ->write(0, StringPtr("rag").asBytes()); KJ_EXPECT(dir->openFile(Path({"corge", "grault"}))->readAllText() == "ragply"); clock.expectUnchanged(*dir); { auto replacer = dir->replaceFile(Path({"corge", "grault"}), WriteMode::CREATE | WriteMode::MODIFY); clock.expectUnchanged(*subdir); replacer->get().writeAll("rag"); clock.expectUnchanged(*subdir); // Don't commit. } clock.expectUnchanged(*subdir); KJ_EXPECT(dir->openFile(Path({"corge", "grault"}))->readAllText() == "ragply"); { auto replacer = dir->replaceFile(Path({"corge", "grault"}), WriteMode::CREATE | WriteMode::MODIFY); clock.expectUnchanged(*subdir); replacer->get().writeAll("rag"); clock.expectUnchanged(*subdir); replacer->commit(); clock.expectChanged(*subdir); KJ_EXPECT(dir->openFile(Path({"corge", "grault"}))->readAllText() == "rag"); } KJ_EXPECT(dir->openFile(Path({"corge", "grault"}))->readAllText() == "rag"); { auto appender = dir->appendFile(Path({"corge", "grault"}), WriteMode::MODIFY); appender->write("waldo", 5); appender->write("fred", 4); } KJ_EXPECT(dir->openFile(Path({"corge", "grault"}))->readAllText() == "ragwaldofred"); KJ_EXPECT(dir->exists(Path("foo"))); clock.expectUnchanged(*dir); dir->remove(Path("foo")); clock.expectChanged(*dir); KJ_EXPECT(!dir->exists(Path("foo"))); KJ_EXPECT(!dir->tryRemove(Path("foo"))); clock.expectUnchanged(*dir); KJ_EXPECT(dir->exists(Path({"bar", "baz"}))); clock.expectUnchanged(*dir); dir->remove(Path({"bar", "baz"})); clock.expectUnchanged(*dir); KJ_EXPECT(!dir->exists(Path({"bar", "baz"}))); KJ_EXPECT(dir->exists(Path("bar"))); KJ_EXPECT(!dir->tryRemove(Path({"bar", "baz"}))); clock.expectUnchanged(*dir); KJ_EXPECT(dir->exists(Path("corge"))); KJ_EXPECT(dir->exists(Path({"corge", "grault"}))); clock.expectUnchanged(*dir); dir->remove(Path("corge")); clock.expectChanged(*dir); KJ_EXPECT(!dir->exists(Path("corge"))); KJ_EXPECT(!dir->exists(Path({"corge", "grault"}))); KJ_EXPECT(!dir->tryRemove(Path("corge"))); clock.expectUnchanged(*dir); } KJ_TEST("InMemoryDirectory symlinks") { TestClock clock; auto dir = newInMemoryDirectory(clock); clock.expectChanged(*dir); dir->symlink(Path("foo"), "bar/qux/../baz", WriteMode::CREATE); clock.expectChanged(*dir); KJ_EXPECT(!dir->trySymlink(Path("foo"), "bar/qux/../baz", WriteMode::CREATE)); clock.expectUnchanged(*dir); { auto stats = dir->lstat(Path("foo")); clock.expectUnchanged(*dir); KJ_EXPECT(stats.type == FsNode::Type::SYMLINK); } KJ_EXPECT(dir->readlink(Path("foo")) == "bar/qux/../baz"); // Broken link into non-existing directory cannot be opened in any mode. KJ_EXPECT(dir->tryOpenFile(Path("foo")) == nullptr); KJ_EXPECT(dir->tryOpenFile(Path("foo"), WriteMode::CREATE) == nullptr); KJ_EXPECT(dir->tryOpenFile(Path("foo"), WriteMode::MODIFY) == nullptr); KJ_EXPECT_THROW_MESSAGE("parent is not a directory", dir->tryOpenFile(Path("foo"), WriteMode::CREATE | WriteMode::MODIFY)); KJ_EXPECT_THROW_MESSAGE("parent is not a directory", dir->tryOpenFile(Path("foo"), WriteMode::CREATE | WriteMode::MODIFY | WriteMode::CREATE_PARENT)); // Create the directory. auto subdir = dir->openSubdir(Path("bar"), WriteMode::CREATE); clock.expectChanged(*dir); // Link still points to non-existing file so cannot be open in most modes. KJ_EXPECT(dir->tryOpenFile(Path("foo")) == nullptr); KJ_EXPECT(dir->tryOpenFile(Path("foo"), WriteMode::CREATE) == nullptr); KJ_EXPECT(dir->tryOpenFile(Path("foo"), WriteMode::MODIFY) == nullptr); clock.expectUnchanged(*dir); // But... CREATE | MODIFY works. dir->openFile(Path("foo"), WriteMode::CREATE | WriteMode::MODIFY) ->writeAll("foobar"); clock.expectUnchanged(*dir); // Change is only to subdir! KJ_EXPECT(dir->openFile(Path({"bar", "baz"}))->readAllText() == "foobar"); KJ_EXPECT(dir->openFile(Path("foo"))->readAllText() == "foobar"); KJ_EXPECT(dir->openFile(Path("foo"), WriteMode::MODIFY)->readAllText() == "foobar"); // operations that modify the symlink dir->symlink(Path("foo"), "corge", WriteMode::MODIFY); KJ_EXPECT(dir->openFile(Path({"bar", "baz"}))->readAllText() == "foobar"); KJ_EXPECT(dir->readlink(Path("foo")) == "corge"); KJ_EXPECT(!dir->exists(Path("foo"))); KJ_EXPECT(dir->lstat(Path("foo")).type == FsNode::Type::SYMLINK); KJ_EXPECT(dir->tryOpenFile(Path("foo")) == nullptr); dir->remove(Path("foo")); KJ_EXPECT(!dir->exists(Path("foo"))); KJ_EXPECT(dir->tryOpenFile(Path("foo")) == nullptr); } KJ_TEST("InMemoryDirectory link") { TestClock clock; auto src = newInMemoryDirectory(clock); auto dst = newInMemoryDirectory(clock); src->openFile(Path({"foo", "bar"}), WriteMode::CREATE | WriteMode::CREATE_PARENT) ->writeAll("foobar"); src->openFile(Path({"foo", "baz", "qux"}), WriteMode::CREATE | WriteMode::CREATE_PARENT) ->writeAll("bazqux"); clock.expectChanged(*src); clock.expectUnchanged(*dst); dst->transfer(Path("link"), WriteMode::CREATE, *src, Path("foo"), TransferMode::LINK); clock.expectUnchanged(*src); clock.expectChanged(*dst); KJ_EXPECT(dst->openFile(Path({"link", "bar"}))->readAllText() == "foobar"); KJ_EXPECT(dst->openFile(Path({"link", "baz", "qux"}))->readAllText() == "bazqux"); KJ_EXPECT(dst->exists(Path({"link", "bar"}))); src->remove(Path({"foo", "bar"})); KJ_EXPECT(!dst->exists(Path({"link", "bar"}))); } KJ_TEST("InMemoryDirectory copy") { TestClock clock; auto src = newInMemoryDirectory(clock); auto dst = newInMemoryDirectory(clock); src->openFile(Path({"foo", "bar"}), WriteMode::CREATE | WriteMode::CREATE_PARENT) ->writeAll("foobar"); src->openFile(Path({"foo", "baz", "qux"}), WriteMode::CREATE | WriteMode::CREATE_PARENT) ->writeAll("bazqux"); clock.expectChanged(*src); clock.expectUnchanged(*dst); dst->transfer(Path("link"), WriteMode::CREATE, *src, Path("foo"), TransferMode::COPY); clock.expectUnchanged(*src); clock.expectChanged(*dst); KJ_EXPECT(src->openFile(Path({"foo", "bar"}))->readAllText() == "foobar"); KJ_EXPECT(src->openFile(Path({"foo", "baz", "qux"}))->readAllText() == "bazqux"); KJ_EXPECT(dst->openFile(Path({"link", "bar"}))->readAllText() == "foobar"); KJ_EXPECT(dst->openFile(Path({"link", "baz", "qux"}))->readAllText() == "bazqux"); KJ_EXPECT(dst->exists(Path({"link", "bar"}))); src->remove(Path({"foo", "bar"})); KJ_EXPECT(dst->openFile(Path({"link", "bar"}))->readAllText() == "foobar"); } KJ_TEST("InMemoryDirectory move") { TestClock clock; auto src = newInMemoryDirectory(clock); auto dst = newInMemoryDirectory(clock); src->openFile(Path({"foo", "bar"}), WriteMode::CREATE | WriteMode::CREATE_PARENT) ->writeAll("foobar"); src->openFile(Path({"foo", "baz", "qux"}), WriteMode::CREATE | WriteMode::CREATE_PARENT) ->writeAll("bazqux"); clock.expectChanged(*src); clock.expectUnchanged(*dst); dst->transfer(Path("link"), WriteMode::CREATE, *src, Path("foo"), TransferMode::MOVE); clock.expectChanged(*src); KJ_EXPECT(!src->exists(Path({"foo"}))); KJ_EXPECT(dst->openFile(Path({"link", "bar"}))->readAllText() == "foobar"); KJ_EXPECT(dst->openFile(Path({"link", "baz", "qux"}))->readAllText() == "bazqux"); } KJ_TEST("InMemoryDirectory createTemporary") { TestClock clock; auto dir = newInMemoryDirectory(clock); auto file = dir->createTemporary(); file->writeAll("foobar"); KJ_EXPECT(file->readAllText() == "foobar"); KJ_EXPECT(dir->listNames() == nullptr); } } // namespace } // namespace kj