mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 21:00:13 +01:00
Rich diagnostic rendering needs a layer underneath it that knows what the attached terminal can do and can position styled text in two dimensions. This adds that layer, both as the foundation the diagnostics rendering work will build on and as something usable directly for ordinary CLI output. Nothing depends on it yet, so it lands and is reviewed on its own. Four libraries, each with its own tests: - `color`: a color, either one of the 16 named ANSI colors or a 24-bit RGB value, and the escape sequences that select it at a given color depth. - `style`: colors plus text attributes, and the escapes that move a terminal from one style to another. - `capabilities`: what the terminal behind a stream supports, detected from the environment. - `buffer`: a grid of styled cells that layout code draws into and that renders itself once. Rationale for the design decisions lives in the headers, next to what it explains. Four things are worth review attention in particular: - The color detection precedence documented on `ChooseColorMode`. It settles how a `--color` flag, `NO_COLOR`, `CLICOLOR`, `FORCE_COLOR`, and the terminal itself interact. The policy is a pure function of those inputs, so the whole table is tested without touching the process environment. - `Charset`, which decides whether any UTF-8 processing happens at all. Column counts only follow from code points if the terminal agrees about the encoding, so anything short of a locale naming UTF-8 is treated as bytes. - `Buffer` owning column accounting instead of its callers, which is what keeps double-width characters, combining marks, and stray bytes from misaligning everything after them. - The API surface, which is held to operations that nothing else covers. Junctions in line art come only from lines overlapping, and turning a style on or off is spelled as a transition to or from the default style. `terminal_benchmark` covers style transitions, full-screen rendering, and text drawing. On an M-series laptop, rendering an 80x24 screen in which every cell changes style costs about 14us with color off and 76-95us with it, and drawing a 40-column line of source costs about 140ns without UTF-8 processing and 394ns with it. Assisted-by: Gemini and Claude --------- Co-authored-by: Richard Smith <richard@metafoo.co.uk>
671 lines
24 KiB
C++
671 lines
24 KiB
C++
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
|
// Exceptions. See /LICENSE for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
|
|
#include "common/filesystem.h"
|
|
|
|
#include <gmock/gmock.h>
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <concepts>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <utility>
|
|
|
|
#include "common/error_test_helpers.h"
|
|
|
|
namespace Carbon::Filesystem {
|
|
namespace {
|
|
|
|
using ::testing::_;
|
|
using ::testing::Eq;
|
|
using ::testing::HasSubstr;
|
|
using Testing::IsError;
|
|
using Testing::IsSuccess;
|
|
using ::testing::UnorderedElementsAre;
|
|
|
|
class FilesystemTest : public ::testing::Test {
|
|
public:
|
|
explicit FilesystemTest() {
|
|
auto result = MakeTmpDir();
|
|
CARBON_CHECK(result.ok(), "{0}", result.error());
|
|
dir_ = std::move(*result);
|
|
}
|
|
|
|
~FilesystemTest() override {
|
|
auto result = std::move(dir_).Remove();
|
|
CARBON_CHECK(result.ok(), "{0}", result.error());
|
|
}
|
|
|
|
auto path() const -> const std::filesystem::path& { return dir_.path(); }
|
|
|
|
// The test's temp directory, deleted on destruction.
|
|
RemovingDir dir_;
|
|
};
|
|
|
|
TEST_F(FilesystemTest, CreateOpenCloseAndUnlink) {
|
|
auto unlink_result = dir_.Unlink("test");
|
|
ASSERT_FALSE(unlink_result.ok());
|
|
EXPECT_TRUE(unlink_result.error().no_entity());
|
|
#if defined(_GNU_SOURCE) && \
|
|
(__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 32))
|
|
EXPECT_THAT(unlink_result, IsError(HasSubstr("ENOENT")));
|
|
#endif
|
|
EXPECT_THAT(unlink_result, IsError(HasSubstr("No such file")));
|
|
|
|
auto f = dir_.OpenWriteOnly("test", CreationOptions::CreateNew);
|
|
ASSERT_THAT(f, IsSuccess(_));
|
|
auto result = (*std::move(f)).Close();
|
|
EXPECT_THAT(result, IsSuccess(_));
|
|
|
|
f = dir_.OpenWriteOnly("test", CreationOptions::CreateNew);
|
|
ASSERT_FALSE(f.ok());
|
|
EXPECT_TRUE(f.error().already_exists());
|
|
#if defined(_GNU_SOURCE) && \
|
|
(__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 32))
|
|
EXPECT_THAT(f, IsError(HasSubstr("EEXIST")));
|
|
#endif
|
|
EXPECT_THAT(f, IsError(HasSubstr("File exists")));
|
|
|
|
f = dir_.OpenWriteOnly("test");
|
|
ASSERT_THAT(f, IsSuccess(_));
|
|
result = std::move(*f).Close();
|
|
EXPECT_THAT(result, IsSuccess(_));
|
|
|
|
f = dir_.OpenWriteOnly("test");
|
|
ASSERT_THAT(f, IsSuccess(_));
|
|
result = std::move(*f).Close();
|
|
EXPECT_THAT(result, IsSuccess(_));
|
|
|
|
unlink_result = dir_.Unlink("test");
|
|
EXPECT_THAT(unlink_result, IsSuccess(_));
|
|
|
|
f = dir_.OpenWriteOnly("test");
|
|
EXPECT_FALSE(f.ok());
|
|
EXPECT_TRUE(f.error().no_entity());
|
|
#if defined(_GNU_SOURCE) && \
|
|
(__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 32))
|
|
EXPECT_THAT(f, IsError(HasSubstr("ENOENT")));
|
|
#endif
|
|
EXPECT_THAT(f, IsError(HasSubstr("No such file")));
|
|
|
|
f = dir_.OpenWriteOnly("test", CreationOptions::OpenAlways);
|
|
ASSERT_THAT(f, IsSuccess(_));
|
|
result = std::move(*f).Close();
|
|
EXPECT_THAT(result, IsSuccess(_));
|
|
|
|
unlink_result = dir_.Unlink("test");
|
|
EXPECT_THAT(unlink_result, IsSuccess(_));
|
|
}
|
|
|
|
TEST_F(FilesystemTest, BasicWriteAndRead) {
|
|
std::string content_str = "0123456789";
|
|
{
|
|
auto f = dir_.OpenWriteOnly("test", CreationOptions::CreateNew);
|
|
ASSERT_THAT(f, IsSuccess(_));
|
|
auto write_result = f->WriteFileFromString(content_str);
|
|
EXPECT_THAT(write_result, IsSuccess(_));
|
|
(*std::move(f)).Close().Check();
|
|
}
|
|
|
|
{
|
|
auto f = dir_.OpenReadOnly("test");
|
|
ASSERT_THAT(f, IsSuccess(_));
|
|
auto read_result = f->ReadFileToString();
|
|
EXPECT_THAT(read_result, IsSuccess(Eq(content_str)));
|
|
}
|
|
|
|
auto unlink_result = dir_.Unlink("test");
|
|
EXPECT_THAT(unlink_result, IsSuccess(_));
|
|
}
|
|
|
|
TEST_F(FilesystemTest, SeekReadAndWrite) {
|
|
std::string content_str = "0123456789";
|
|
// First write some initial content.
|
|
{
|
|
auto f = dir_.OpenWriteOnly("test", CreationOptions::CreateNew);
|
|
ASSERT_THAT(f, IsSuccess(_));
|
|
auto write_result = f->WriteFileFromString(content_str);
|
|
EXPECT_THAT(write_result, IsSuccess(_));
|
|
(*std::move(f)).Close().Check();
|
|
}
|
|
|
|
// Now seek and read.
|
|
{
|
|
auto f = dir_.OpenReadOnly("test");
|
|
ASSERT_THAT(f, IsSuccess(_));
|
|
auto seek_result = f->Seek(3);
|
|
ASSERT_THAT(seek_result, IsSuccess(Eq(3)));
|
|
std::array<std::byte, 4> buffer;
|
|
auto read_result = f->ReadToBuffer(buffer);
|
|
ASSERT_THAT(read_result, IsSuccess(_));
|
|
EXPECT_THAT(std::string(reinterpret_cast<char*>(read_result->data()),
|
|
read_result->size()),
|
|
Eq(content_str.substr(3, read_result->size())));
|
|
|
|
// Now test that we can seek back to the beginning and read the full file.
|
|
auto read_file_result = f->ReadFileToString();
|
|
EXPECT_THAT(read_file_result, IsSuccess(Eq(content_str)));
|
|
}
|
|
|
|
// Now a mixture of reads, writes, an seeking.
|
|
{
|
|
auto f = dir_.OpenReadWrite("test");
|
|
ASSERT_THAT(f, IsSuccess(_));
|
|
auto seek_result = f->SeekFromEnd(-6);
|
|
ASSERT_THAT(seek_result, IsSuccess(Eq(content_str.size() - 6)));
|
|
std::string new_content_str = "abcdefg";
|
|
llvm::ArrayRef<std::byte> new_content_bytes(
|
|
reinterpret_cast<std::byte*>(new_content_str.data()),
|
|
new_content_str.size());
|
|
for (auto write_bytes = new_content_bytes.slice(0, 4);
|
|
!write_bytes.empty();) {
|
|
auto write_result = f->WriteFromBuffer(write_bytes);
|
|
ASSERT_THAT(write_result, IsSuccess(_));
|
|
write_bytes = *write_result;
|
|
}
|
|
|
|
std::array<std::byte, 4> buffer;
|
|
auto read_result = f->ReadToBuffer(buffer);
|
|
ASSERT_THAT(read_result, IsSuccess(_));
|
|
EXPECT_THAT(std::string(reinterpret_cast<char*>(read_result->data()),
|
|
read_result->size()),
|
|
Eq(content_str.substr(8, read_result->size())));
|
|
|
|
EXPECT_THAT(*f->ReadFileToString(), "0123abcd89");
|
|
|
|
// Now write the entire file, also changing its size, after a fresh seek.
|
|
seek_result = f->Seek(-6);
|
|
ASSERT_THAT(seek_result, IsSuccess(Eq(content_str.size() - 6)));
|
|
auto write_file_result = f->WriteFileFromString(new_content_str);
|
|
EXPECT_THAT(write_file_result, IsSuccess(_));
|
|
EXPECT_THAT(*f->ReadFileToString(), "abcdefg");
|
|
(*std::move(f)).Close().Check();
|
|
}
|
|
|
|
auto unlink_result = dir_.Unlink("test");
|
|
EXPECT_THAT(unlink_result, IsSuccess(_));
|
|
}
|
|
|
|
TEST_F(FilesystemTest, CreateAndRemoveDirecotries) {
|
|
auto d1 = Cwd().CreateDirectories(path() / "a" / "b" / "c" / "test1");
|
|
ASSERT_THAT(d1, IsSuccess(_));
|
|
auto d2 = Cwd().CreateDirectories(path() / "a" / "b" / "c" / "test2");
|
|
ASSERT_THAT(d2, IsSuccess(_));
|
|
auto d3 = Cwd().CreateDirectories(path() / "a" / "b" / "c" / "test3");
|
|
ASSERT_THAT(d3, IsSuccess(_));
|
|
// Get a directory object to use, this shouldn't cover much new.
|
|
auto d4 = Cwd().CreateDirectories(path());
|
|
EXPECT_THAT(d4, IsSuccess(_));
|
|
// Single, present, relative component.
|
|
auto d5 = d4->CreateDirectories("a");
|
|
EXPECT_THAT(d5, IsSuccess(_));
|
|
// Multiple, present, but relative components.
|
|
auto d6 = d5->CreateDirectories(std::filesystem::path("b") / "c");
|
|
EXPECT_THAT(d6, IsSuccess(_));
|
|
// Single new component.
|
|
auto d7 = d6->CreateDirectories("test4");
|
|
ASSERT_THAT(d7, IsSuccess(_));
|
|
// Two new relative components.
|
|
auto d8 = d6->CreateDirectories(std::filesystem::path("test5") / "d");
|
|
EXPECT_THAT(d8, IsSuccess(_));
|
|
// Mixed relative components.
|
|
auto d9 = d5->CreateDirectories(std::filesystem::path("b") / "test6");
|
|
EXPECT_THAT(d9, IsSuccess(_));
|
|
|
|
{
|
|
auto f1 = d1->OpenWriteOnly("file1", CreateNew);
|
|
ASSERT_THAT(f1, IsSuccess(_));
|
|
auto f2 = d2->OpenWriteOnly("file2", CreateNew);
|
|
ASSERT_THAT(f2, IsSuccess(_));
|
|
auto f3 = d3->OpenWriteOnly("file3", CreateNew);
|
|
ASSERT_THAT(f3, IsSuccess(_));
|
|
auto f4 = d7->OpenWriteOnly("file4", CreateNew);
|
|
ASSERT_THAT(f4, IsSuccess(_));
|
|
(*std::move(f1)).Close().Check();
|
|
(*std::move(f2)).Close().Check();
|
|
(*std::move(f3)).Close().Check();
|
|
(*std::move(f4)).Close().Check();
|
|
}
|
|
|
|
auto rm_result = Cwd().Rmtree(path() / "a");
|
|
ASSERT_THAT(rm_result, IsSuccess(_));
|
|
}
|
|
|
|
TEST_F(FilesystemTest, StatAndAccess) {
|
|
auto access_result = dir_.Access("test");
|
|
ASSERT_FALSE(access_result.ok());
|
|
EXPECT_TRUE(access_result.error().no_entity());
|
|
|
|
// Make sure the flags and bit-or-ing them works in the boring case.
|
|
access_result =
|
|
dir_.Access("test", AccessCheckFlags::Read | AccessCheckFlags::Write |
|
|
AccessCheckFlags::Execute);
|
|
ASSERT_FALSE(access_result.ok());
|
|
EXPECT_TRUE(access_result.error().no_entity());
|
|
|
|
auto stat_result = dir_.Stat("test");
|
|
ASSERT_FALSE(access_result.ok());
|
|
EXPECT_TRUE(access_result.error().no_entity());
|
|
|
|
// Create a file for testing, using very unusual and minimal permissions to
|
|
// help us test. Hopefully this isn't modified on the usual `umask` tests run
|
|
// under.
|
|
std::string content_str = "0123456789";
|
|
ModeType permissions = 0450;
|
|
auto f = dir_.OpenWriteOnly("test", CreationOptions::CreateNew, permissions);
|
|
ASSERT_THAT(f, IsSuccess(_));
|
|
auto write_result = f->WriteFileFromString(content_str);
|
|
EXPECT_THAT(write_result, IsSuccess(_));
|
|
|
|
access_result = dir_.Access("test");
|
|
EXPECT_THAT(access_result, IsSuccess(_));
|
|
access_result = dir_.Access("test", AccessCheckFlags::Read);
|
|
EXPECT_THAT(access_result, IsSuccess(_));
|
|
|
|
// Neither write nor execute permission should be present though.
|
|
access_result = dir_.Access("test", AccessCheckFlags::Write);
|
|
ASSERT_FALSE(access_result.ok());
|
|
EXPECT_TRUE(access_result.error().access_denied());
|
|
access_result =
|
|
dir_.Access("test", AccessCheckFlags::Read | AccessCheckFlags::Write |
|
|
AccessCheckFlags::Execute);
|
|
ASSERT_FALSE(access_result.ok());
|
|
EXPECT_TRUE(access_result.error().access_denied());
|
|
|
|
stat_result = dir_.Stat("test");
|
|
ASSERT_THAT(stat_result, IsSuccess(_));
|
|
EXPECT_TRUE(stat_result->is_file());
|
|
EXPECT_FALSE(stat_result->is_dir());
|
|
EXPECT_FALSE(stat_result->is_symlink());
|
|
EXPECT_THAT(stat_result->size(), Eq(content_str.size()));
|
|
EXPECT_THAT(stat_result->permissions(), Eq(permissions));
|
|
|
|
// Directory instead of file.
|
|
access_result =
|
|
dir_.Access(".", AccessCheckFlags::Read | AccessCheckFlags::Write |
|
|
AccessCheckFlags::Execute);
|
|
EXPECT_THAT(access_result, IsSuccess(_));
|
|
|
|
stat_result = dir_.Stat(".");
|
|
ASSERT_THAT(stat_result, IsSuccess(_));
|
|
EXPECT_FALSE(stat_result->is_file());
|
|
EXPECT_TRUE(stat_result->is_dir());
|
|
EXPECT_FALSE(stat_result->is_symlink());
|
|
|
|
// Can remove file but still stat through the file.
|
|
auto unlink_result = dir_.Unlink("test");
|
|
ASSERT_THAT(unlink_result, IsSuccess(_));
|
|
auto file_stat_result = f->Stat();
|
|
ASSERT_THAT(file_stat_result, IsSuccess(_));
|
|
EXPECT_TRUE(file_stat_result->is_file());
|
|
EXPECT_FALSE(file_stat_result->is_dir());
|
|
EXPECT_FALSE(file_stat_result->is_symlink());
|
|
EXPECT_THAT(file_stat_result->size(), Eq(content_str.size()));
|
|
EXPECT_THAT(file_stat_result->permissions(), Eq(permissions));
|
|
(*std::move(f)).Close().Check();
|
|
}
|
|
|
|
TEST_F(FilesystemTest, Symlinks) {
|
|
auto readlink_result = dir_.Readlink("test");
|
|
ASSERT_FALSE(readlink_result.ok());
|
|
EXPECT_TRUE(readlink_result.error().no_entity());
|
|
|
|
auto lstat_result = dir_.Lstat("test");
|
|
ASSERT_FALSE(lstat_result.ok());
|
|
EXPECT_TRUE(lstat_result.error().no_entity());
|
|
|
|
auto symlink_result = dir_.Symlink("test", "abc");
|
|
EXPECT_THAT(symlink_result, IsSuccess(_));
|
|
|
|
readlink_result = dir_.Readlink("test");
|
|
EXPECT_THAT(readlink_result, IsSuccess(Eq("abc")));
|
|
|
|
symlink_result = dir_.Symlink("test", "def");
|
|
ASSERT_FALSE(symlink_result.ok());
|
|
EXPECT_TRUE(symlink_result.error().already_exists());
|
|
|
|
lstat_result = dir_.Lstat("test");
|
|
ASSERT_THAT(lstat_result, IsSuccess(_));
|
|
EXPECT_FALSE(lstat_result->is_file());
|
|
EXPECT_FALSE(lstat_result->is_dir());
|
|
EXPECT_TRUE(lstat_result->is_symlink());
|
|
EXPECT_THAT(lstat_result->size(), Eq(strlen("abc")));
|
|
|
|
auto unlink_result = dir_.Unlink("test");
|
|
EXPECT_THAT(unlink_result, IsSuccess(_));
|
|
|
|
readlink_result = dir_.Readlink("test");
|
|
ASSERT_FALSE(readlink_result.ok());
|
|
EXPECT_TRUE(readlink_result.error().no_entity());
|
|
|
|
// Try a symlink with null bytes for fun. This demonstrates that the symlink
|
|
// syscall only uses the leading C-string.
|
|
symlink_result = dir_.Symlink("test", std::string("a\0b\0c", 5));
|
|
EXPECT_THAT(symlink_result, IsSuccess(_));
|
|
readlink_result = dir_.Readlink("test");
|
|
EXPECT_THAT(readlink_result, IsSuccess(Eq("a")));
|
|
}
|
|
|
|
TEST_F(FilesystemTest, Chdir) {
|
|
auto current_result = Cwd().OpenDir(".");
|
|
ASSERT_THAT(current_result, IsSuccess(_));
|
|
|
|
auto symlink_result = dir_.Symlink("test", "abc");
|
|
EXPECT_THAT(symlink_result, IsSuccess(_));
|
|
|
|
auto chdir_result = dir_.Chdir();
|
|
EXPECT_THAT(chdir_result, IsSuccess(_));
|
|
auto readlink_result = Cwd().Readlink("test");
|
|
EXPECT_THAT(readlink_result, IsSuccess(Eq("abc")));
|
|
|
|
auto chdir_path_result = dir_.Chdir("missing");
|
|
ASSERT_FALSE(chdir_path_result.ok());
|
|
EXPECT_TRUE(chdir_path_result.error().no_entity());
|
|
|
|
// Dangling symlink.
|
|
chdir_path_result = dir_.Chdir("test");
|
|
ASSERT_FALSE(chdir_path_result.ok());
|
|
EXPECT_TRUE(chdir_path_result.error().no_entity());
|
|
|
|
// Create a regular file and try to chdir to that.
|
|
auto f = dir_.OpenWriteOnly("test2", CreationOptions::CreateNew);
|
|
ASSERT_THAT(f, IsSuccess(_));
|
|
auto write_result = f->WriteFileFromString("test2");
|
|
EXPECT_THAT(write_result, IsSuccess(_));
|
|
chdir_path_result = dir_.Chdir("test2");
|
|
ASSERT_FALSE(chdir_path_result.ok());
|
|
EXPECT_TRUE(chdir_path_result.error().not_dir());
|
|
|
|
auto d2_result = Cwd().OpenDir("test_d2", CreationOptions::CreateNew);
|
|
ASSERT_THAT(d2_result, IsSuccess(_));
|
|
symlink_result = d2_result->Symlink("test2", "def");
|
|
EXPECT_THAT(symlink_result, IsSuccess(_));
|
|
|
|
chdir_path_result = dir_.Chdir("test_d2");
|
|
ASSERT_THAT(chdir_path_result, IsSuccess(_));
|
|
readlink_result = Cwd().Readlink("test2");
|
|
EXPECT_THAT(readlink_result, IsSuccess(Eq("def")));
|
|
readlink_result = Cwd().Readlink("../test");
|
|
EXPECT_THAT(readlink_result, IsSuccess(Eq("abc")));
|
|
|
|
chdir_result = current_result->Chdir();
|
|
ASSERT_THAT(chdir_result, IsSuccess(_));
|
|
readlink_result = Cwd().Readlink("test");
|
|
ASSERT_FALSE(readlink_result.ok());
|
|
EXPECT_TRUE(readlink_result.error().no_entity());
|
|
(*std::move(f)).Close().Check();
|
|
}
|
|
|
|
TEST_F(FilesystemTest, WriteStream) {
|
|
std::string content_str = "0123456789";
|
|
auto write = dir_.OpenWriteOnly("test", CreationOptions::CreateNew);
|
|
ASSERT_THAT(write, IsSuccess(_));
|
|
{
|
|
llvm::raw_fd_ostream os = write->WriteStream();
|
|
os << content_str;
|
|
EXPECT_FALSE(os.has_error()) << os.error();
|
|
}
|
|
(*std::move(write)).Close().Check();
|
|
|
|
EXPECT_THAT(dir_.ReadFileToString("test"), IsSuccess(Eq(content_str)));
|
|
}
|
|
|
|
TEST_F(FilesystemTest, WriteCompleteBuffer) {
|
|
std::string content_str = "0123456789";
|
|
auto bytes = llvm::ArrayRef<std::byte>(
|
|
reinterpret_cast<const std::byte*>(content_str.data()),
|
|
content_str.size());
|
|
|
|
auto write = dir_.OpenWriteOnly("test", CreationOptions::CreateNew);
|
|
ASSERT_THAT(write, IsSuccess(_));
|
|
EXPECT_THAT(write->WriteCompleteBuffer(bytes), IsSuccess(_));
|
|
// Writing appends rather than replacing, unlike `WriteFileFromString`.
|
|
EXPECT_THAT(write->WriteCompleteBuffer(bytes), IsSuccess(_));
|
|
// An empty buffer is a no-op rather than an error.
|
|
EXPECT_THAT(write->WriteCompleteBuffer(llvm::ArrayRef<std::byte>()),
|
|
IsSuccess(_));
|
|
(*std::move(write)).Close().Check();
|
|
|
|
EXPECT_THAT(dir_.ReadFileToString("test"),
|
|
IsSuccess(Eq(content_str + content_str)));
|
|
}
|
|
|
|
TEST_F(FilesystemTest, StandardStreams) {
|
|
// The standard streams name descriptors the process already has, so these
|
|
// are constants and never open or close anything.
|
|
static_assert(Stdin().unix_fd() == STDIN_FILENO);
|
|
static_assert(Stdout().unix_fd() == STDOUT_FILENO);
|
|
static_assert(Stderr().unix_fd() == STDERR_FILENO);
|
|
EXPECT_TRUE(Stderr().is_valid());
|
|
|
|
// Writing through one reaches the descriptor. Tests run with stdout captured,
|
|
// so this uses a pipe put in its place for the duration.
|
|
int fds[2];
|
|
ASSERT_EQ(pipe(fds), 0);
|
|
int saved = dup(STDOUT_FILENO);
|
|
ASSERT_GE(saved, 0);
|
|
ASSERT_GE(dup2(fds[1], STDOUT_FILENO), 0);
|
|
|
|
llvm::StringRef message = "through stdout";
|
|
auto result = Stdout().WriteCompleteBuffer(llvm::ArrayRef<std::byte>(
|
|
reinterpret_cast<const std::byte*>(message.data()), message.size()));
|
|
|
|
ASSERT_GE(dup2(saved, STDOUT_FILENO), 0);
|
|
ASSERT_EQ(close(saved), 0);
|
|
ASSERT_EQ(close(fds[1]), 0);
|
|
EXPECT_THAT(result, IsSuccess(_));
|
|
|
|
char buffer[64];
|
|
ssize_t n = read(fds[0], buffer, sizeof(buffer));
|
|
ASSERT_EQ(close(fds[0]), 0);
|
|
ASSERT_GT(n, 0);
|
|
EXPECT_EQ(llvm::StringRef(buffer, n), message);
|
|
}
|
|
|
|
TEST_F(FilesystemTest, Rename) {
|
|
// Rename a file within a directory.
|
|
ASSERT_THAT(dir_.WriteFileFromString("file1", "content1"), IsSuccess(_));
|
|
EXPECT_THAT(dir_.Rename("file1", dir_, "file2"), IsSuccess(_));
|
|
EXPECT_THAT(dir_.ReadFileToString("file2"), IsSuccess(Eq("content1")));
|
|
auto read_missing = dir_.ReadFileToString("file1");
|
|
EXPECT_FALSE(read_missing.ok());
|
|
EXPECT_TRUE(read_missing.error().no_entity());
|
|
|
|
// Rename a file between two directories.
|
|
auto d1 = *dir_.CreateDirectories("subdir1");
|
|
EXPECT_THAT(dir_.Rename("file2", d1, "file1"), IsSuccess(_));
|
|
EXPECT_THAT(d1.ReadFileToString("file1"), IsSuccess(Eq("content1")));
|
|
auto d2 = *dir_.CreateDirectories("subdir2");
|
|
EXPECT_THAT(d1.Rename("file1", d2, "file1"), IsSuccess(_));
|
|
EXPECT_THAT(d2.ReadFileToString("file1"), IsSuccess(Eq("content1")));
|
|
// Close the first directory.
|
|
d1 = Filesystem::Dir();
|
|
EXPECT_THAT(dir_.Rmdir("subdir1"), IsSuccess(_))
|
|
<< "Directory should have bene empty!";
|
|
|
|
// Rename directories.
|
|
ASSERT_THAT(dir_.ReadFileToString(std::filesystem::path("subdir2") / "file1"),
|
|
IsSuccess(Eq("content1")));
|
|
EXPECT_THAT(dir_.Rename("subdir2", dir_, "subdir1"), IsSuccess(_));
|
|
EXPECT_THAT(dir_.ReadFileToString(std::filesystem::path("subdir1") / "file1"),
|
|
IsSuccess(Eq("content1")));
|
|
|
|
// The open directory `d2` should survive the rename and point at the same
|
|
// directory.
|
|
EXPECT_THAT(d2.ReadFileToString("file1"), IsSuccess(Eq("content1")));
|
|
EXPECT_THAT(d2.WriteFileFromString("file2", "content2"), IsSuccess(_));
|
|
EXPECT_THAT(dir_.ReadFileToString(std::filesystem::path("subdir1") / "file2"),
|
|
IsSuccess(Eq("content2")));
|
|
|
|
// Rename over an existing file.
|
|
EXPECT_THAT(d2.Rename("file2", d2, "file1"), IsSuccess(_));
|
|
EXPECT_THAT(d2.ReadFileToString("file1"), IsSuccess(Eq("content2")));
|
|
|
|
// Test error calls as well.
|
|
auto result = dir_.Rename("missing1", dir_, "missing2");
|
|
EXPECT_TRUE(result.error().no_entity()) << result.error();
|
|
result = d2.Rename("file1", dir_,
|
|
std::filesystem::path("missing_subdir") / "file2");
|
|
EXPECT_TRUE(result.error().no_entity()) << result.error();
|
|
// Note that `d2` was renamed `subdir1` above, which is why this creates
|
|
// infinite subdirectories.
|
|
result = dir_.Rename("subdir1", d2, "infinite_subdirs");
|
|
EXPECT_THAT(result.error().unix_errnum(), EINVAL) << result.error();
|
|
}
|
|
|
|
TEST_F(FilesystemTest, TryLock) {
|
|
auto file = dir_.OpenReadWrite("test_file", CreateNew);
|
|
ASSERT_THAT(file, IsSuccess(_));
|
|
|
|
// Acquire an exclusive lock.
|
|
auto lock = file->TryLock(FileLock::Exclusive);
|
|
ASSERT_THAT(lock, IsSuccess(_));
|
|
EXPECT_TRUE(lock->is_locked());
|
|
|
|
// Try to acquire a second lock from a different file object.
|
|
auto file2 = dir_.OpenReadOnly("test_file");
|
|
ASSERT_THAT(file2, IsSuccess(_));
|
|
auto lock2 = file2->TryLock(FileLock::Exclusive);
|
|
ASSERT_THAT(lock2, IsError(_));
|
|
EXPECT_TRUE(lock2.error().would_block());
|
|
|
|
// A shared lock should also fail.
|
|
auto lock3 = file2->TryLock(FileLock::Shared);
|
|
ASSERT_THAT(lock3, IsError(_));
|
|
EXPECT_TRUE(lock3.error().would_block());
|
|
|
|
// Release the first lock.
|
|
*lock = {};
|
|
EXPECT_FALSE(lock->is_locked());
|
|
|
|
// Now we can acquire an exclusive lock.
|
|
lock2 = file2->TryLock(FileLock::Exclusive);
|
|
ASSERT_THAT(lock2, IsSuccess(_));
|
|
EXPECT_TRUE(lock2->is_locked());
|
|
*lock2 = {};
|
|
|
|
// Test shared locks.
|
|
auto shared_lock1 = file->TryLock(FileLock::Shared);
|
|
ASSERT_THAT(shared_lock1, IsSuccess(_));
|
|
EXPECT_TRUE(shared_lock1->is_locked());
|
|
|
|
auto shared_lock2 = file2->TryLock(FileLock::Shared);
|
|
ASSERT_THAT(shared_lock2, IsSuccess(_));
|
|
EXPECT_TRUE(shared_lock2->is_locked());
|
|
|
|
// An exclusive lock should fail.
|
|
auto file3 = dir_.OpenReadOnly("test_file");
|
|
ASSERT_THAT(file3, IsSuccess(_));
|
|
auto exclusive_lock = file3->TryLock(FileLock::Exclusive);
|
|
ASSERT_THAT(exclusive_lock, IsError(_));
|
|
EXPECT_TRUE(exclusive_lock.error().would_block());
|
|
|
|
// Release locks and close files.
|
|
*shared_lock1 = {};
|
|
*shared_lock2 = {};
|
|
ASSERT_THAT((*std::move(file)).Close(), IsSuccess(_));
|
|
ASSERT_THAT((*std::move(file2)).Close(), IsSuccess(_));
|
|
ASSERT_THAT((*std::move(file3)).Close(), IsSuccess(_));
|
|
}
|
|
|
|
TEST_F(FilesystemTest, ReadAndAppendEntries) {
|
|
// Test with an empty directory.
|
|
{
|
|
auto entries = dir_.ReadEntries();
|
|
ASSERT_THAT(entries, IsSuccess(_));
|
|
EXPECT_TRUE(entries->empty());
|
|
}
|
|
{
|
|
llvm::SmallVector<std::filesystem::path> entries;
|
|
EXPECT_THAT(dir_.AppendEntriesIf(entries), IsSuccess(_));
|
|
EXPECT_TRUE(entries.empty());
|
|
}
|
|
|
|
// Create some files and directories.
|
|
ASSERT_THAT(dir_.WriteFileFromString("file1", ""), IsSuccess(_));
|
|
ASSERT_THAT(dir_.WriteFileFromString("file2", ""), IsSuccess(_));
|
|
ASSERT_THAT(dir_.WriteFileFromString(".hidden", ""), IsSuccess(_));
|
|
ASSERT_THAT(dir_.CreateDirectories("subdir1"), IsSuccess(_));
|
|
ASSERT_THAT(dir_.CreateDirectories("subdir2"), IsSuccess(_));
|
|
|
|
// Test ReadEntries.
|
|
{
|
|
auto entries = dir_.ReadEntries();
|
|
ASSERT_THAT(entries, IsSuccess(_));
|
|
EXPECT_THAT(*entries, UnorderedElementsAre(".hidden", "file1", "file2",
|
|
"subdir1", "subdir2"));
|
|
}
|
|
|
|
// Test AppendEntriesIf with no predicate.
|
|
{
|
|
llvm::SmallVector<std::filesystem::path> entries;
|
|
EXPECT_THAT(dir_.AppendEntriesIf(entries), IsSuccess(_));
|
|
EXPECT_THAT(entries, UnorderedElementsAre(".hidden", "file1", "file2",
|
|
"subdir1", "subdir2"));
|
|
}
|
|
|
|
// Test AppendEntriesIf with a predicate.
|
|
{
|
|
llvm::SmallVector<std::filesystem::path> entries;
|
|
auto result = dir_.AppendEntriesIf(
|
|
entries, [](llvm::StringRef name) { return name.starts_with("file"); });
|
|
EXPECT_THAT(result, IsSuccess(_));
|
|
EXPECT_THAT(entries, UnorderedElementsAre("file1", "file2"));
|
|
}
|
|
|
|
// Test AppendEntriesIf with directory splitting and a predicate.
|
|
{
|
|
llvm::SmallVector<std::filesystem::path> dir_entries;
|
|
llvm::SmallVector<std::filesystem::path> non_dir_entries;
|
|
auto result = dir_.AppendEntriesIf(
|
|
dir_entries, non_dir_entries,
|
|
[](llvm::StringRef name) { return !name.starts_with("."); });
|
|
EXPECT_THAT(result, IsSuccess(_));
|
|
EXPECT_THAT(dir_entries, UnorderedElementsAre("subdir1", "subdir2"));
|
|
EXPECT_THAT(non_dir_entries, UnorderedElementsAre("file1", "file2"));
|
|
}
|
|
}
|
|
|
|
TEST_F(FilesystemTest, MtimeAndUpdateTimes) {
|
|
// Test UpdateTimes on a path that doesn't exist.
|
|
auto update_missing = dir_.UpdateTimes("test_file");
|
|
ASSERT_THAT(update_missing, IsError(_));
|
|
EXPECT_TRUE(update_missing.error().no_entity());
|
|
|
|
// Create a file and get its initial modification time.
|
|
ASSERT_THAT(dir_.WriteFileFromString("test_file", "content"), IsSuccess(_));
|
|
auto stat = dir_.Stat("test_file");
|
|
ASSERT_THAT(stat, IsSuccess(_));
|
|
auto time1 = stat->mtime();
|
|
|
|
// Repeated stats have stable time.
|
|
stat = dir_.Stat("test_file");
|
|
ASSERT_THAT(stat, IsSuccess(_));
|
|
EXPECT_THAT(stat->mtime(), Eq(time1));
|
|
|
|
// Update the timestamp to a specific time in the past.
|
|
auto past_time = time1 - std::chrono::seconds(120);
|
|
ASSERT_THAT(dir_.UpdateTimes("test_file", past_time), IsSuccess(_));
|
|
stat = dir_.Stat("test_file");
|
|
ASSERT_THAT(stat, IsSuccess(_));
|
|
EXPECT_THAT(stat->mtime(), Eq(past_time));
|
|
|
|
// Now test updating times on an open file. Should still be at `past_time`.
|
|
auto file = *dir_.OpenReadWrite("test_file");
|
|
auto file_stat = file.Stat();
|
|
ASSERT_THAT(file_stat, IsSuccess(_));
|
|
EXPECT_THAT(file_stat->mtime(), Eq(past_time));
|
|
|
|
// Update the times through the file and verify those updates arrived.
|
|
ASSERT_THAT(file.UpdateTimes(time1), IsSuccess(_));
|
|
file_stat = file.Stat();
|
|
ASSERT_THAT(file_stat, IsSuccess(_));
|
|
EXPECT_THAT(file_stat->mtime(), Eq(time1));
|
|
|
|
ASSERT_THAT(std::move(file).Close(), IsSuccess(_));
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace Carbon::Filesystem
|