uvco/test/fs_test.cc
2025-12-01 20:14:29 +01:00

363 lines
9.9 KiB
C++

#include <fmt/core.h>
#include <fmt/ranges.h>
#include <gtest/gtest.h>
#include <uv.h>
#include "test_util.h"
#include "uvco/combinators.h"
#include "uvco/exception.h"
#include "uvco/fs.h"
#include "uvco/loop/loop.h"
#include "uvco/promise/multipromise.h"
#include "uvco/promise/promise.h"
#include "uvco/run.h"
#include <algorithm>
#include <cstddef>
#include <fcntl.h>
#include <filesystem>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
namespace {
using namespace uvco;
TEST(FsTest, openFile) {
auto setup = [](const Loop &loop) -> Promise<void> {
auto file = co_await File::open(loop, "/dev/null", O_RDONLY);
EXPECT_GT(file.file(), 0);
file.close();
};
run_loop(setup);
}
TEST(FsTest, fileNotFound) {
auto setup = [](const Loop &loop) -> Promise<void> {
try {
co_await File::open(loop, "/dev/does_not_exist", O_RDONLY);
EXPECT_FALSE(true) << "reached unexpected point - should have thrown";
} catch (const UvcoException &e) {
EXPECT_EQ(e.status, UV_ENOENT);
}
};
run_loop(setup);
}
TEST(FsTest, forgetClose) {
auto setup = [](const Loop &loop) -> Promise<void> {
const File file = co_await File::open(loop, "/dev/null", O_RDONLY);
EXPECT_GT(file.file(), 0);
};
run_loop(setup);
}
// Read some zeros.
TEST(FsTest, simpleRead) {
static constexpr size_t bufSize = 32;
auto setup = [](const Loop &loop) -> Promise<void> {
auto file = co_await File::open(loop, "/dev/zero", O_RDONLY);
EXPECT_GT(file.file(), 2);
std::string buffer(bufSize, 'x');
EXPECT_EQ(bufSize, buffer.size());
size_t read = co_await file.read(buffer);
EXPECT_EQ(bufSize, read);
EXPECT_EQ(bufSize, buffer.size());
EXPECT_TRUE(std::ranges::all_of(buffer, [](char c) { return c == 0; }));
file.close();
};
run_loop(setup);
}
// This type of cancelling Fs operations can cause memory corruption :(
TEST(FsTest, DISABLED_dropRead) {
auto setup = [](const Loop &loop) -> Promise<void> {
auto file = co_await File::open(loop, "/dev/zero", O_RDONLY);
EXPECT_GT(file.file(), 2);
std::string buffer(32, 'x');
EXPECT_EQ(32, buffer.size());
file.read(buffer);
};
run_loop(setup);
}
TEST(FsTest, simpleReadWriteUnlink) {
static constexpr std::string_view contents = "Hello World\n";
static constexpr std::string_view fileName = "/tmp/_uvco_test_file";
auto setup = [](const Loop &loop) -> Promise<void> {
File file = co_await File::open(loop, fileName, O_RDWR | O_CREAT);
co_await file.write(contents);
std::string buffer(64, '\0');
const size_t bytesRead = co_await file.read(buffer, 0);
EXPECT_EQ(contents.size(), bytesRead);
EXPECT_EQ(contents, buffer);
file.close();
co_await File::unlink(loop, std::string{fileName});
};
run_loop(setup);
}
TEST(FsTest, mkDirRmDir) {
static constexpr std::string_view dirName = "/tmp/_uvco_test_dir";
auto setup = [](const Loop &loop) -> Promise<void> {
co_await Directory::mkdir(loop, dirName);
try {
co_await Directory::mkdir(loop, dirName);
} catch (const UvcoException &e) {
EXPECT_EQ(e.status, UV_EEXIST);
}
co_await Directory::rmdir(loop, dirName);
};
run_loop(setup);
}
TEST(FsTest, openDir) {
static constexpr std::string_view dirName = "/tmp";
static constexpr std::string_view fileName = "/tmp/_uvco_test_file";
auto setup = [](const Loop &loop) -> Promise<void> {
auto file = co_await File::open(loop, fileName, O_RDWR | O_CREAT);
co_await file.write("hello world");
file.close();
auto dir = co_await Directory::open(loop, dirName);
// Test move ctor.
auto dir1 = std::move(dir);
dir = std::move(dir1);
auto entries = co_await dir.read();
EXPECT_GT(entries.size(), 0);
dir.close();
co_await File::unlink(loop, fileName);
};
run_loop(setup);
}
TEST(FsTest, dropDir) {
static constexpr std::string_view dirName = "/tmp";
auto setup = [](const Loop &loop) -> Promise<void> {
auto dir = co_await Directory::open(loop, dirName);
auto entries = co_await dir.read();
};
run_loop(setup);
}
TEST(FsTest, dropUnlinkOp) {
static constexpr std::string_view fileName = "/tmp/_uvco_test_file";
auto setup = [](const Loop &loop) -> Promise<void> {
// create at least one file in /tmp in case it is empty.
auto file = co_await File::open(loop, fileName, O_RDWR | O_CREAT);
co_await file.write("hello world");
file.close();
File::unlink(loop, fileName);
};
run_loop(setup);
if (std::filesystem::exists(fileName)) {
std::filesystem::remove(fileName);
}
}
TEST(FsTest, scanDir) {
static constexpr std::string_view dirName = "/tmp";
static constexpr std::string_view fileName = "/tmp/_uvco_test_file";
auto setup = [](const Loop &loop) -> Promise<void> {
// create at least one file in /tmp in case it is empty.
auto file = co_await File::open(loop, fileName, O_RDWR | O_CREAT);
co_await file.write("hello world");
file.close();
auto entries = Directory::readAll(loop, dirName);
unsigned count = 0;
std::optional<Directory::DirEnt> entry;
while ((entry = co_await entries).has_value()) {
EXPECT_GT(entry->name.size(), 0);
++count;
}
EXPECT_GT(count, 0);
co_await File::unlink(loop, fileName);
};
run_loop(setup);
}
TEST(FsWatchTest, basicFileWatch) {
static constexpr std::string_view filename = "/tmp/_uvco_watch_test";
auto setup = [](const Loop &loop) -> Promise<void> {
auto file = co_await File::open(loop, filename, O_RDWR | O_CREAT);
auto watch = co_await FsWatch::create(loop, filename);
MultiPromise<FsWatch::FileEvent> watcher = watch.watch();
co_await file.write("Hello World\n");
const std::optional<FsWatch::FileEvent> event = co_await watcher;
BOOST_ASSERT(event.has_value());
EXPECT_EQ(event->path, "_uvco_watch_test");
EXPECT_EQ(event->events, UV_CHANGE);
co_await watch.stopWatch(std::move(watcher));
watch.close();
file.close();
co_await File::unlink(loop, std::string{filename});
co_return;
};
run_loop(setup);
}
TEST(FsWatchTest, basicDirWatch) {
static constexpr std::string_view dirName = "/tmp/_uvco_watch_test_dir";
static constexpr std::string_view filename = "/tmp/_uvco_watch_test_dir/file";
static constexpr std::string_view dirName2 =
"/tmp/_uvco_watch_test_dir/subdir";
static constexpr std::string_view filename2 =
"/tmp/_uvco_watch_test_dir/subdir/subfile";
auto setup = [](const Loop &loop) -> Promise<void> {
co_await Directory::mkdir(loop, dirName);
co_await Directory::mkdir(loop, dirName2);
auto file = co_await File::open(loop, filename, O_RDWR | O_CREAT);
auto watch = co_await FsWatch::create(loop, dirName);
MultiPromise<FsWatch::FileEvent> watcher = watch.watch();
// Check that FsWatch::create is not recursive
auto file2 = co_await File::open(loop, filename2, O_RDWR | O_CREAT);
co_await file2.write("Hello World\n");
co_await file.write("Hello World\n");
const std::optional<FsWatch::FileEvent> event = co_await watcher;
BOOST_ASSERT(event.has_value());
EXPECT_EQ(event->path, "file");
EXPECT_EQ(event->events, UV_CHANGE);
co_await watch.stopWatch(std::move(watcher));
watch.close();
file.close();
file2.close();
co_await File::unlink(loop, std::string{filename});
co_await File::unlink(loop, std::string{filename2});
co_await Directory::rmdir(loop, dirName2);
co_await Directory::rmdir(loop, dirName);
co_return;
};
run_loop(setup);
}
// This test shoould only work on macOS and Windows
// https://docs.libuv.org/en/v1.x/fs_event.html#c.uv_fs_event_start
TEST(DISABLED_FsWatchTest, watchRecursive) {
static constexpr std::string_view dirName = "/tmp/_uvco_watch_test_dir";
static constexpr std::string_view subdirName =
"/tmp/_uvco_watch_test_dir/subdir";
static constexpr std::string_view filename =
"/tmp/_uvco_watch_test_dir/subdir/file";
auto setup = [](const Loop &loop) -> Promise<void> {
co_await Directory::mkdir(loop, dirName);
co_await Directory::mkdir(loop, subdirName);
auto watch = co_await FsWatch::createRecursive(loop, dirName);
MultiPromise<FsWatch::FileEvent> watcher = watch.watch();
auto file = co_await File::open(loop, filename, O_RDWR | O_CREAT);
co_await file.write("Hello World\n");
const std::optional<FsWatch::FileEvent> event = co_await watcher;
BOOST_ASSERT(event.has_value());
EXPECT_EQ(event->path, "subdir/file");
EXPECT_EQ(event->events, UV_CHANGE);
co_await watch.stopWatch(std::move(watcher));
watch.close();
file.close();
co_await File::unlink(loop, std::string{filename});
co_await Directory::rmdir(loop, subdirName);
co_await Directory::rmdir(loop, dirName);
co_return;
};
run_loop(setup);
}
TEST(FsWatchTest, watchNonExisting) {
static constexpr std::string_view filename =
"/tmp/_uvco_watch_test_non_existing";
auto setup = [](const Loop &loop) -> Promise<void> {
try {
auto watcher = co_await FsWatch::create(loop, filename);
EXPECT_FALSE(true) << "Expected UvcoException";
} catch (const UvcoException &e) {
EXPECT_EQ(e.status, UV_ENOENT);
}
co_await yield();
co_return;
};
run_loop(setup);
}
TEST(FsWatchTest, repeatedWatchFails) {
static constexpr std::string_view filename = "/tmp/_uvco_watch_test";
auto setup = [](const Loop &loop) -> Promise<void> {
auto file = co_await File::open(loop, filename, O_RDWR | O_CREAT);
auto watch = co_await FsWatch::create(loop, filename);
MultiPromise<FsWatch::FileEvent> watcher = watch.watch();
try {
auto eventGenerator2 = watch.watch();
EXPECT_FALSE(true) << "Expected UvcoException";
} catch (const UvcoException &e) {
EXPECT_EQ(e.status, UV_EBUSY);
}
co_await watch.stopWatch(std::move(watcher));
watch.close();
file.close();
co_await File::unlink(loop, std::string{filename});
co_return;
};
run_loop(setup);
}
} // namespace