summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2025-10-07 09:12:22 +0200
committerJoel Klinghed <the_jk@spawned.biz>2025-10-07 09:13:15 +0200
commitc87f9627efc8b612eb9b000acfcc6731cad15765 (patch)
tree34eb4b9e70a51c2f3db3a97c2aef31ba0b139ec9 /test
Initial commit
Diffstat (limited to 'test')
-rw-r--r--test/args.cc412
-rw-r--r--test/buffer.cc270
-rw-r--r--test/io.cc203
-rw-r--r--test/io_test_helper.cc82
-rw-r--r--test/io_test_helper.hh18
-rw-r--r--test/line.cc182
-rw-r--r--test/str.cc67
7 files changed, 1234 insertions, 0 deletions
diff --git a/test/args.cc b/test/args.cc
new file mode 100644
index 0000000..22159d2
--- /dev/null
+++ b/test/args.cc
@@ -0,0 +1,412 @@
+#include "args.hh"
+
+#include <cstddef>
+#include <gtest/gtest.h>
+#include <memory>
+#include <sstream>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#define SETUP_OPTIONS(args) \
+ auto short_only = (args)->option('a', "", "an option"); \
+ auto short_long = (args)->option('b', "bold", "set font style to bold"); \
+ auto long_only = (args)->option("cold", "use if it is cold outside"); \
+ auto short_only_req = (args)->option_argument('d', "", "", "distance"); \
+ auto short_long_req = (args)->option_argument( \
+ 'e', "eat", "FOOD", "what to order, what to eat?"); \
+ auto long_only_req = \
+ (args)->option_argument("form", "", "circle, shape or something else?"); \
+ auto short_only_opt = (args)->option_argument('g', "", "", "", false); \
+ auto short_long_opt = (args)->option_argument('h', "hold", "", "", false); \
+ auto long_only_opt = (args)->option_argument("invert", "", "", false);
+
+namespace {
+
+class Arguments {
+ public:
+ [[nodiscard]] int c() const { return static_cast<int>(str_.size()); };
+ [[nodiscard]] char** v() const { return ptr_.get(); }
+
+ explicit Arguments(std::vector<std::string> str) : str_(std::move(str)) {
+ ptr_ = std::make_unique<char*[]>(str_.size());
+ for (size_t i = 0; i < str_.size(); ++i) {
+ ptr_[i] = const_cast<char*>(str_[i].c_str());
+ }
+ }
+
+ Arguments(Arguments const&) = delete;
+ Arguments& operator=(Arguments const&) = delete;
+
+ private:
+ std::unique_ptr<char*[]> ptr_;
+ std::vector<std::string> str_;
+};
+
+class Builder {
+ public:
+ Arguments build() { return Arguments(std::move(str_)); }
+
+ Builder& add(std::string str) {
+ str_.emplace_back(std::move(str));
+ return *this;
+ }
+
+ private:
+ std::vector<std::string> str_;
+};
+
+} // namespace
+
+TEST(args, empty) {
+ auto args = Args::create();
+ EXPECT_TRUE(args->run(0, nullptr));
+ {
+ std::stringstream ss;
+ args->print_error(ss);
+ EXPECT_EQ("", ss.str());
+ }
+ {
+ std::stringstream ss;
+ args->print_help(ss);
+ EXPECT_EQ("", ss.str());
+ }
+}
+
+TEST(args, options_not_set) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ EXPECT_TRUE(args->run(0, nullptr));
+ EXPECT_FALSE(short_only->is_set());
+ EXPECT_FALSE(short_long->is_set());
+ EXPECT_FALSE(long_only->is_set());
+ EXPECT_FALSE(short_only_req->is_set());
+ EXPECT_FALSE(short_long_req->is_set());
+ EXPECT_FALSE(long_only_req->is_set());
+ EXPECT_FALSE(short_only_opt->is_set());
+ EXPECT_FALSE(short_long_opt->is_set());
+ EXPECT_FALSE(long_only_opt->is_set());
+}
+
+TEST(args, options_not_set_arguments) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo").add("bar").add("fum").build();
+ std::vector<std::string_view> out;
+ EXPECT_TRUE(args->run(arg.c(), arg.v(), &out));
+ ASSERT_EQ(2, out.size());
+ EXPECT_EQ("bar", out[0]);
+ EXPECT_EQ("fum", out[1]);
+}
+
+TEST(args, options_set) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo")
+ .add("-a")
+ .add("-b")
+ .add("--cold")
+ .add("-d")
+ .add("10")
+ .add("-e")
+ .add("hamburger")
+ .add("--form=circle")
+ .add("-g")
+ .add("-h")
+ .add("--invert")
+ .build();
+ std::vector<std::string_view> out;
+ EXPECT_TRUE(args->run(arg.c(), arg.v(), &out));
+ EXPECT_TRUE(out.empty());
+ EXPECT_TRUE(short_only->is_set());
+ EXPECT_TRUE(short_long->is_set());
+ EXPECT_TRUE(long_only->is_set());
+ EXPECT_TRUE(short_only_req->is_set());
+ EXPECT_TRUE(short_only_req->has_argument());
+ EXPECT_EQ("10", short_only_req->argument());
+ EXPECT_TRUE(short_long_req->is_set());
+ EXPECT_TRUE(short_long_req->has_argument());
+ EXPECT_EQ("hamburger", short_long_req->argument());
+ EXPECT_TRUE(long_only_req->is_set());
+ EXPECT_TRUE(long_only_req->has_argument());
+ EXPECT_EQ("circle", long_only_req->argument());
+ EXPECT_TRUE(short_only_opt->is_set());
+ EXPECT_FALSE(short_only_opt->has_argument());
+ EXPECT_TRUE(short_long_opt->is_set());
+ EXPECT_FALSE(short_long_opt->has_argument());
+ EXPECT_TRUE(long_only_opt->is_set());
+ EXPECT_FALSE(long_only_opt->has_argument());
+}
+
+TEST(args, options_set_variant) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo")
+ .add("-a")
+ .add("--bold")
+ .add("--cold")
+ .add("-d")
+ .add("10")
+ .add("--eat=hamburger")
+ .add("--form")
+ .add("circle")
+ .add("-g")
+ .add("--hold=foo")
+ .add("--invert=bar")
+ .build();
+ std::vector<std::string_view> out;
+ EXPECT_TRUE(args->run(arg.c(), arg.v(), &out));
+ EXPECT_TRUE(out.empty());
+ EXPECT_TRUE(short_only->is_set());
+ EXPECT_TRUE(short_long->is_set());
+ EXPECT_TRUE(long_only->is_set());
+ EXPECT_TRUE(short_only_req->is_set());
+ EXPECT_TRUE(short_only_req->has_argument());
+ EXPECT_EQ("10", short_only_req->argument());
+ EXPECT_TRUE(short_long_req->is_set());
+ EXPECT_TRUE(short_long_req->has_argument());
+ EXPECT_EQ("hamburger", short_long_req->argument());
+ EXPECT_TRUE(long_only_req->is_set());
+ EXPECT_TRUE(long_only_req->has_argument());
+ EXPECT_EQ("circle", long_only_req->argument());
+ EXPECT_TRUE(short_only_opt->is_set());
+ EXPECT_FALSE(short_only_opt->has_argument());
+ EXPECT_TRUE(short_long_opt->is_set());
+ EXPECT_TRUE(short_long_opt->has_argument());
+ EXPECT_EQ("foo", short_long_opt->argument());
+ EXPECT_TRUE(long_only_opt->is_set());
+ EXPECT_TRUE(long_only_opt->has_argument());
+ EXPECT_EQ("bar", long_only_opt->argument());
+}
+
+TEST(args, options_short_missing_value) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo").add("-d").build();
+ EXPECT_FALSE(args->run(arg.c(), arg.v()));
+ std::stringstream ss;
+ args->print_error(ss);
+ EXPECT_EQ("foo: option requires an argument -- 'd'\n", ss.str());
+}
+
+TEST(args, options_short_unknown_value) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo").add("-X").build();
+ EXPECT_FALSE(args->run(arg.c(), arg.v()));
+ std::stringstream ss;
+ args->print_error(ss);
+ EXPECT_EQ("foo: invalid option -- 'X'\n", ss.str());
+}
+
+TEST(args, options_long_missing_value) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo").add("--form").build();
+ EXPECT_FALSE(args->run(arg.c(), arg.v()));
+ std::stringstream ss;
+ args->print_error(ss);
+ EXPECT_EQ("foo: option '--form' requires an argument\n", ss.str());
+}
+
+TEST(args, options_long_unsupported_value) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo").add("--cold=feet").build();
+ EXPECT_FALSE(args->run(arg.c(), arg.v()));
+ std::stringstream ss;
+ args->print_error(ss);
+ EXPECT_EQ("foo: option '--cold' doesn't allow an argument\n", ss.str());
+}
+
+TEST(args, options_long_unknown_value) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo").add("--experience").build();
+ EXPECT_FALSE(args->run(arg.c(), arg.v()));
+ std::stringstream ss;
+ args->print_error(ss);
+ EXPECT_EQ("foo: unrecognized option '--experience'\n", ss.str());
+}
+
+TEST(args, options_short_dash_value) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo").add("-d").add("-").build();
+ std::vector<std::string_view> out;
+ EXPECT_TRUE(args->run(arg.c(), arg.v(), &out));
+ EXPECT_TRUE(out.empty());
+ EXPECT_TRUE(short_only_req->is_set());
+ EXPECT_TRUE(short_only_req->has_argument());
+ EXPECT_EQ("-", short_only_req->argument());
+}
+
+TEST(args, options_long_dash_dash_value) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo").add("--eat").add("--").build();
+ std::vector<std::string_view> out;
+ EXPECT_TRUE(args->run(arg.c(), arg.v(), &out));
+ EXPECT_TRUE(out.empty());
+ EXPECT_TRUE(short_long_req->is_set());
+ EXPECT_TRUE(short_long_req->has_argument());
+ EXPECT_EQ("--", short_long_req->argument());
+}
+
+TEST(args, options_dash_dash) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo")
+ .add("--")
+ .add("-d")
+ .add("10")
+ .add("--eat=hamburger")
+ .add("--form")
+ .add("circle")
+ .build();
+ std::vector<std::string_view> out;
+ EXPECT_TRUE(args->run(arg.c(), arg.v(), &out));
+ ASSERT_EQ(5, out.size());
+ EXPECT_EQ("-d", out[0]);
+ EXPECT_EQ("10", out[1]);
+ EXPECT_EQ("--eat=hamburger", out[2]);
+ EXPECT_EQ("--form", out[3]);
+ EXPECT_EQ("circle", out[4]);
+ EXPECT_FALSE(short_only_req->is_set());
+ EXPECT_FALSE(short_long_req->is_set());
+ EXPECT_FALSE(long_only_req->is_set());
+}
+
+TEST(args, options_dash_dash_end) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo").add("--").build();
+ std::vector<std::string_view> out;
+ EXPECT_TRUE(args->run(arg.c(), arg.v(), &out));
+ EXPECT_TRUE(out.empty());
+}
+
+TEST(args, help) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ std::stringstream ss;
+ args->print_help(ss, 50);
+ EXPECT_EQ(R"(Mandatory arguments to long options are mandatory
+ for short options too.
+ -a an option
+ -b, --bold set font style to bold
+ --cold use if it is cold outside
+ -d ARG distance
+ -e, --eat=FOOD what to order, what to eat?
+ --form=ARG circle, shape or something
+ else?
+ -g
+ -h, --hold=[ARG]
+ --invert=[ARG]
+)",
+ ss.str());
+}
+
+TEST(args, help_wide) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ std::stringstream ss;
+ args->print_help(ss, 100);
+ EXPECT_EQ(
+ R"(Mandatory arguments to long options are mandatory for short options too.
+ -a an option
+ -b, --bold set font style to bold
+ --cold use if it is cold outside
+ -d ARG distance
+ -e, --eat=FOOD what to order, what to eat?
+ --form=ARG circle, shape or something else?
+ -g
+ -h, --hold=[ARG]
+ --invert=[ARG]
+)",
+ ss.str());
+}
+
+TEST(args, help_narrow) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ std::stringstream ss;
+ args->print_help(ss, 35);
+ EXPECT_EQ(R"(Mandatory arguments to long options
+ are mandatory for short options
+ too.
+ -a an option
+ -b, --bold set font style to
+ bold
+ --cold use if it is cold
+ outside
+ -d ARG distance
+ -e, --eat=FOOD
+what to order, what to eat?
+ --form=ARG
+circle, shape or something else?
+ -g
+ -h, --hold=[ARG]
+ --invert=[ARG]
+)",
+ ss.str());
+}
+
+TEST(args, help_very_narrow) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ std::stringstream ss;
+ args->print_help(ss, 20);
+ EXPECT_EQ(R"(Mandatory arguments
+ to long options are
+ mandatory for short
+ options too.
+ -a an option
+ -b, --bold
+set font style to
+ bold
+ --cold
+use if it is cold
+ outside
+ -d ARG distance
+ -e, --eat=FOOD
+what to order, what
+ to eat?
+ --form=ARG
+circle, shape or
+ something else?
+ -g
+ -h, --hold=[ARG]
+ --invert=[ARG]
+)",
+ ss.str());
+}
+
+TEST(args, help_long_word) {
+ auto args = Args::create();
+ std::stringstream ss;
+ auto opt = args->option('a', "arg", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+ args->print_help(ss, 20);
+ EXPECT_EQ(R"(Mandatory arguments
+ to long options are
+ mandatory for short
+ options too.
+ -a, --arg
+aaaaaaaaaaaaaaaaaaa-
+aaaaaaaaaaaaaaaaaaa
+)",
+ ss.str());
+}
diff --git a/test/buffer.cc b/test/buffer.cc
new file mode 100644
index 0000000..897bd88
--- /dev/null
+++ b/test/buffer.cc
@@ -0,0 +1,270 @@
+#include "buffer.hh"
+
+#include <cstdint>
+#include <cstring>
+#include <gtest/gtest.h>
+#include <memory>
+#include <utility>
+
+namespace {
+
+enum class BufferType : uint8_t {
+ Fixed,
+ Dynamic,
+};
+
+class BufferTest : public testing::TestWithParam<BufferType> {
+ protected:
+ static std::unique_ptr<Buffer> make(size_t min_size, size_t max_size) {
+ switch (GetParam()) {
+ case BufferType::Fixed:
+ return Buffer::fixed(min_size);
+ case BufferType::Dynamic:
+ return Buffer::dynamic(min_size, max_size);
+ }
+ std::unreachable();
+ }
+};
+
+} // namespace
+
+TEST_P(BufferTest, empty) {
+ auto buffer = make(10, 100);
+ EXPECT_TRUE(buffer->empty());
+ EXPECT_FALSE(buffer->full());
+ size_t avail;
+ buffer->rptr(avail);
+ EXPECT_EQ(0, avail);
+ buffer->wptr(avail);
+ EXPECT_EQ(10, avail);
+}
+
+TEST_P(BufferTest, write_read) {
+ auto buffer = make(10, 100);
+ size_t avail;
+ auto* wptr = buffer->wptr(avail);
+ EXPECT_EQ(10, avail);
+ memcpy(wptr, "Hello", 6);
+ buffer->commit(6);
+ EXPECT_FALSE(buffer->empty());
+ auto* rptr = buffer->rptr(avail);
+ EXPECT_EQ(6, avail);
+ EXPECT_STREQ("Hello", reinterpret_cast<const char*>(rptr));
+ buffer->consume(3);
+ rptr = buffer->rptr(avail);
+ EXPECT_EQ(3, avail);
+ EXPECT_STREQ("lo", reinterpret_cast<const char*>(rptr));
+ buffer->consume(3);
+ EXPECT_TRUE(buffer->empty());
+}
+
+TEST_P(BufferTest, write_read2) {
+ auto buffer = make(10, 10);
+ size_t avail;
+ auto* wptr = buffer->wptr(avail);
+ EXPECT_EQ(10, avail);
+ memcpy(wptr, "0123456789", 10);
+ buffer->commit(10);
+
+ auto* rptr = buffer->rptr(avail);
+ EXPECT_EQ(10, avail);
+ char tmp[11];
+ memcpy(tmp, rptr, 5);
+ tmp[5] = '\0';
+ EXPECT_STREQ("01234", tmp);
+ buffer->consume(5);
+
+ wptr = buffer->wptr(avail, 5);
+ EXPECT_EQ(5, avail);
+ memcpy(wptr, "abcde", 5);
+ buffer->commit(5);
+
+ rptr = buffer->rptr(avail);
+ EXPECT_LE(5, avail);
+ memcpy(tmp, rptr, 5);
+ tmp[5] = '\0';
+ EXPECT_STREQ("56789", tmp);
+ buffer->consume(5);
+
+ rptr = buffer->rptr(avail, 5);
+ EXPECT_EQ(5, avail);
+ memcpy(tmp, rptr, 5);
+ tmp[5] = '\0';
+ EXPECT_STREQ("abcde", tmp);
+ buffer->consume(5);
+}
+
+TEST_P(BufferTest, write_read3) {
+ auto buffer = make(10, 10);
+ size_t avail;
+ auto* wptr = buffer->wptr(avail);
+ EXPECT_EQ(10, avail);
+ memcpy(wptr, "0123456789", 10);
+ buffer->commit(10);
+
+ auto* rptr = buffer->rptr(avail);
+ EXPECT_EQ(10, avail);
+ char tmp[11];
+ memcpy(tmp, rptr, 5);
+ tmp[5] = '\0';
+ EXPECT_STREQ("01234", tmp);
+ buffer->consume(5);
+
+ wptr = buffer->wptr(avail, 5);
+ EXPECT_EQ(5, avail);
+ memcpy(wptr, "abcde", 5);
+ buffer->commit(5);
+
+ rptr = buffer->rptr(avail, 10);
+ EXPECT_EQ(10, avail);
+ memcpy(tmp, rptr, 10);
+ tmp[10] = '\0';
+ EXPECT_STREQ("56789abcde", tmp);
+ buffer->consume(5);
+}
+
+TEST_P(BufferTest, write_read4) {
+ auto buffer = make(10, 10);
+ size_t avail;
+ auto* wptr = buffer->wptr(avail);
+ EXPECT_EQ(10, avail);
+ memcpy(wptr, "0123456789", 10);
+ buffer->commit(10);
+
+ auto* rptr = buffer->rptr(avail);
+ EXPECT_EQ(10, avail);
+ char tmp[11];
+ memcpy(tmp, rptr, 10);
+ tmp[10] = '\0';
+ EXPECT_STREQ("0123456789", tmp);
+ buffer->consume(10);
+}
+
+TEST_P(BufferTest, write_read5) {
+ auto buffer = make(10, 10);
+ size_t avail;
+ auto* wptr = buffer->wptr(avail);
+ EXPECT_EQ(10, avail);
+ memcpy(wptr, "01234", 5);
+ buffer->commit(5);
+
+ auto* rptr = buffer->rptr(avail);
+ EXPECT_EQ(5, avail);
+ char tmp[11];
+ memcpy(tmp, rptr, 3);
+ tmp[3] = '\0';
+ EXPECT_STREQ("012", tmp);
+ buffer->consume(3);
+
+ wptr = buffer->wptr(avail, 8);
+ EXPECT_EQ(8, avail);
+ memcpy(wptr, "<xxxxxx>", 8);
+ buffer->commit(8);
+
+ rptr = buffer->rptr(avail, 10);
+ EXPECT_EQ(10, avail);
+ memcpy(tmp, rptr, 10);
+ tmp[10] = '\0';
+ EXPECT_STREQ("34<xxxxxx>", tmp);
+ buffer->consume(10);
+}
+
+TEST_P(BufferTest, write_read6) {
+ auto buffer = make(10, 10);
+ size_t avail;
+ auto* wptr = buffer->wptr(avail);
+ EXPECT_EQ(10, avail);
+ memcpy(wptr, "0123456789", 10);
+ buffer->commit(10);
+
+ auto* rptr = buffer->rptr(avail);
+ EXPECT_EQ(10, avail);
+ char tmp[11];
+ memcpy(tmp, rptr, 8);
+ tmp[8] = '\0';
+ EXPECT_STREQ("01234567", tmp);
+ buffer->consume(8);
+
+ wptr = buffer->wptr(avail, 3);
+ EXPECT_LE(3, avail);
+ memcpy(wptr, "abc", 3);
+ buffer->commit(3);
+
+ rptr = buffer->rptr(avail, 5);
+ EXPECT_EQ(5, avail);
+ memcpy(tmp, rptr, 5);
+ tmp[5] = '\0';
+ EXPECT_STREQ("89abc", tmp);
+ buffer->consume(5);
+}
+
+TEST_P(BufferTest, full) {
+ auto buffer = make(10, 10);
+ size_t avail;
+ auto* wptr = buffer->wptr(avail);
+ EXPECT_EQ(10, avail);
+ memcpy(wptr, "0123456789", 10);
+ buffer->commit(10);
+ EXPECT_TRUE(buffer->full());
+ std::ignore = buffer->wptr(avail);
+ EXPECT_EQ(0, avail);
+ buffer->commit(0);
+
+ auto* rptr = buffer->rptr(avail, 10);
+ EXPECT_EQ(10, avail);
+ char tmp[11];
+ memcpy(tmp, rptr, 5);
+ tmp[5] = '\0';
+ EXPECT_STREQ("01234", tmp);
+ buffer->consume(5);
+ EXPECT_FALSE(buffer->full());
+
+ wptr = buffer->wptr(avail, 5);
+ EXPECT_EQ(5, avail);
+ memcpy(wptr, "abcde", 5);
+ buffer->commit(5);
+ EXPECT_TRUE(buffer->full());
+
+ rptr = buffer->rptr(avail, 10);
+ EXPECT_EQ(10, avail);
+ memcpy(tmp, rptr, 10);
+ tmp[10] = '\0';
+ EXPECT_STREQ("56789abcde", tmp);
+ buffer->consume(10);
+ EXPECT_FALSE(buffer->full());
+ EXPECT_TRUE(buffer->empty());
+
+ std::ignore = buffer->rptr(avail, 10);
+ EXPECT_EQ(0, avail);
+ buffer->consume(0);
+}
+
+INSTANTIATE_TEST_SUITE_P(AllTypes, BufferTest,
+ testing::Values(BufferType::Fixed,
+ BufferType::Dynamic));
+
+TEST(buffer, dynamic_increase) {
+ auto buffer = Buffer::dynamic(10, 20);
+
+ size_t avail;
+ auto* wptr = buffer->wptr(avail, 15);
+ EXPECT_EQ(15, avail);
+ memcpy(wptr, "0123456789abcde", 15);
+ buffer->commit(15);
+ EXPECT_FALSE(buffer->full());
+ wptr = buffer->wptr(avail, 5);
+ EXPECT_EQ(5, avail);
+ memcpy(wptr, "fghij", 5);
+ buffer->commit(5);
+ EXPECT_TRUE(buffer->full());
+
+ auto* rptr = buffer->rptr(avail, 20);
+ EXPECT_EQ(20, avail);
+ char tmp[21];
+ memcpy(tmp, rptr, 20);
+ tmp[20] = '\0';
+ EXPECT_STREQ("0123456789abcdefghij", tmp);
+ buffer->consume(20);
+ EXPECT_FALSE(buffer->full());
+ EXPECT_TRUE(buffer->empty());
+}
diff --git a/test/io.cc b/test/io.cc
new file mode 100644
index 0000000..04d97d6
--- /dev/null
+++ b/test/io.cc
@@ -0,0 +1,203 @@
+#include "io.hh"
+
+#include "io_test_helper.hh"
+
+#include <cerrno>
+#include <cstdlib>
+#include <dirent.h>
+#include <fcntl.h>
+#include <gtest/gtest.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <utility>
+
+namespace {
+
+bool remove_recursive(int fd) {
+ auto* dir = fdopendir(fd);
+ if (!dir)
+ return false;
+ while (auto* ent = readdir(dir)) {
+ if (ent->d_name[0] == '.') {
+ if (ent->d_name[1] == '\0')
+ continue;
+ if (ent->d_name[1] == '.' && ent->d_name[2] == '\0')
+ continue;
+ }
+ bool is_dir;
+ if (ent->d_type == DT_DIR) {
+ is_dir = true;
+ } else if (ent->d_type == DT_UNKNOWN) {
+ struct stat buf;
+ if (fstatat(dirfd(dir), ent->d_name, &buf, AT_SYMLINK_NOFOLLOW) == 0) {
+ is_dir = S_ISDIR(buf.st_mode);
+ } else {
+ if (errno != ENOENT) {
+ closedir(dir);
+ return false;
+ }
+ is_dir = false;
+ }
+ } else {
+ is_dir = false;
+ }
+
+ if (is_dir) {
+ int fd2 = openat(dirfd(dir), ent->d_name, O_RDONLY | O_DIRECTORY);
+ if (fd2 == -1) {
+ if (errno != ENOENT) {
+ closedir(dir);
+ return false;
+ }
+ } else {
+ if (!remove_recursive(fd2)) {
+ closedir(dir);
+ return false;
+ }
+ }
+ }
+ if (unlinkat(dirfd(dir), ent->d_name, is_dir ? AT_REMOVEDIR : 0)) {
+ if (errno != ENOENT) {
+ closedir(dir);
+ return false;
+ }
+ }
+ }
+ closedir(dir);
+ return true;
+}
+
+class IoTest : public testing::Test {
+ protected:
+ void SetUp() override {
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ tmpdir_ = P_tmpdir "/jkc-test-io-XXXXXX";
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ auto* ret = mkdtemp(tmpdir_.data());
+ ASSERT_EQ(ret, tmpdir_.data());
+ dirfd_ = open(tmpdir_.c_str(), O_PATH | O_DIRECTORY);
+ ASSERT_NE(-1, dirfd_);
+ }
+
+ void TearDown() override {
+ int fd = openat(dirfd_, ".", O_RDONLY | O_DIRECTORY);
+ EXPECT_NE(-1, fd);
+ if (fd != -1) {
+ EXPECT_TRUE(remove_recursive(fd));
+ }
+ close(dirfd_);
+ rmdir(tmpdir_.c_str());
+ }
+
+ [[nodiscard]] int dirfd() const { return dirfd_; }
+
+ void touch(const std::string& name, const std::string& value = "") {
+ auto fd = openat(dirfd(), name.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0700);
+ EXPECT_NE(-1, fd);
+ if (fd == -1)
+ return;
+ size_t offset = 0;
+ while (offset < value.size()) {
+ auto ret = write(fd, value.data() + offset, value.size() - offset);
+ EXPECT_LT(0, ret);
+ if (ret <= 0) {
+ break;
+ }
+ offset += ret;
+ }
+ close(fd);
+ }
+
+ private:
+ int dirfd_{-1};
+ std::string tmpdir_;
+};
+
+} // namespace
+
+TEST_F(IoTest, no_such_file) {
+ auto ret = io::openat(dirfd(), "no-such-file");
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(io::OpenError::NoSuchFile, ret.error());
+}
+
+TEST_F(IoTest, read_empty) {
+ touch("test");
+
+ auto ret = io::openat(dirfd(), "test");
+ ASSERT_TRUE(ret.has_value());
+ std::string tmp(10, ' ');
+ auto ret2 = ret.value()->read(tmp.data(), tmp.size());
+ ASSERT_FALSE(ret2.has_value());
+ EXPECT_EQ(io::ReadError::Eof, ret2.error());
+}
+
+TEST_F(IoTest, skip_empty) {
+ touch("test");
+
+ auto ret = io::openat(dirfd(), "test");
+ ASSERT_TRUE(ret.has_value());
+ auto ret2 = ret.value()->skip(10);
+ ASSERT_FALSE(ret2.has_value());
+ EXPECT_EQ(io::ReadError::Eof, ret2.error());
+}
+
+TEST_F(IoTest, read) {
+ touch("test", "hello world");
+
+ auto ret = io::openat(dirfd(), "test");
+ ASSERT_TRUE(ret.has_value());
+ std::string tmp(12, ' ');
+ auto ret2 = ret.value()->repeat_read(tmp.data(), tmp.size());
+ ASSERT_TRUE(ret2.has_value());
+ EXPECT_EQ(11, ret2.value());
+ tmp.resize(ret2.value());
+ EXPECT_EQ("hello world", tmp);
+}
+
+TEST_F(IoTest, skip) {
+ touch("test", "hello world");
+
+ auto ret = io::openat(dirfd(), "test");
+ ASSERT_TRUE(ret.has_value());
+ auto ret2 = ret.value()->repeat_skip(6);
+ ASSERT_TRUE(ret2.has_value());
+ EXPECT_EQ(6, ret2.value());
+ std::string tmp(12, ' ');
+ auto ret3 = ret.value()->repeat_read(tmp.data(), tmp.size());
+ ASSERT_TRUE(ret3.has_value());
+ EXPECT_EQ(5, ret3.value());
+ tmp.resize(ret3.value());
+ EXPECT_EQ("world", tmp);
+}
+
+TEST_F(IoTest, read_block) {
+ touch("test", "hello world");
+
+ auto ret = io::openat(dirfd(), "test");
+ ASSERT_TRUE(ret.has_value());
+ auto ret2 = io_make_max_block(std::move(ret.value()), 2);
+ std::string tmp(12, ' ');
+ auto ret3 = ret2->repeat_read(tmp.data(), tmp.size());
+ ASSERT_TRUE(ret3.has_value());
+ EXPECT_EQ(11, ret3.value());
+ tmp.resize(ret3.value());
+ EXPECT_EQ("hello world", tmp);
+}
+
+TEST_F(IoTest, skip_block) {
+ touch("test", "hello world");
+
+ auto ret = io::openat(dirfd(), "test");
+ ASSERT_TRUE(ret.has_value());
+ auto ret2 = io_make_max_block(std::move(ret.value()), 2);
+ auto ret3 = ret2->repeat_skip(6);
+ ASSERT_TRUE(ret3.has_value());
+ EXPECT_EQ(6, ret3.value());
+ std::string tmp(12, ' ');
+ auto ret4 = ret2->repeat_read(tmp.data(), tmp.size());
+ ASSERT_TRUE(ret4.has_value());
+ EXPECT_EQ(5, ret4.value());
+ tmp.resize(ret4.value());
+ EXPECT_EQ("world", tmp);
+}
diff --git a/test/io_test_helper.cc b/test/io_test_helper.cc
new file mode 100644
index 0000000..9ac663a
--- /dev/null
+++ b/test/io_test_helper.cc
@@ -0,0 +1,82 @@
+#include "io_test_helper.hh"
+
+#include "io.hh"
+
+#include <algorithm>
+#include <cstddef>
+#include <expected>
+#include <memory>
+#include <utility>
+
+namespace {
+
+class BreakingReader : public io::Reader {
+ public:
+ BreakingReader(std::unique_ptr<io::Reader> reader, size_t offset,
+ io::ReadError error)
+ : reader_(std::move(reader)), offset_(offset), error_(error) {}
+
+ [[nodiscard]]
+ std::expected<size_t, io::ReadError> read(void* dst, size_t max) override {
+ if (offset_ == 0)
+ return std::unexpected(error_);
+ size_t avail = std::min(offset_, max);
+ auto ret = reader_->read(dst, avail);
+ if (ret.has_value()) {
+ offset_ -= ret.value();
+ }
+ return ret;
+ }
+
+ [[nodiscard]]
+ std::expected<size_t, io::ReadError> skip(size_t max) override {
+ if (offset_ == 0)
+ return std::unexpected(error_);
+ size_t avail = std::min(offset_, max);
+ auto ret = reader_->skip(avail);
+ if (ret.has_value()) {
+ offset_ -= ret.value();
+ }
+ return ret;
+ }
+
+ private:
+ std::unique_ptr<io::Reader> reader_;
+ size_t offset_;
+ io::ReadError const error_;
+};
+
+class MaxBlockReader : public io::Reader {
+ public:
+ MaxBlockReader(std::unique_ptr<io::Reader> reader, size_t max_block_size)
+ : reader_(std::move(reader)), max_block_size_(max_block_size) {}
+
+ [[nodiscard]]
+ std::expected<size_t, io::ReadError> read(void* dst, size_t max) override {
+ size_t avail = std::min(max_block_size_, max);
+ return reader_->read(dst, avail);
+ }
+
+ [[nodiscard]]
+ std::expected<size_t, io::ReadError> skip(size_t max) override {
+ size_t avail = std::min(max_block_size_, max);
+ return reader_->skip(avail);
+ }
+
+ private:
+ std::unique_ptr<io::Reader> reader_;
+ size_t const max_block_size_;
+};
+
+} // namespace
+
+std::unique_ptr<io::Reader> io_make_breaking(std::unique_ptr<io::Reader> reader,
+ size_t offset,
+ io::ReadError error) {
+ return std::make_unique<BreakingReader>(std::move(reader), offset, error);
+}
+
+std::unique_ptr<io::Reader> io_make_max_block(
+ std::unique_ptr<io::Reader> reader, size_t max_block_size) {
+ return std::make_unique<MaxBlockReader>(std::move(reader), max_block_size);
+}
diff --git a/test/io_test_helper.hh b/test/io_test_helper.hh
new file mode 100644
index 0000000..b99b8fa
--- /dev/null
+++ b/test/io_test_helper.hh
@@ -0,0 +1,18 @@
+#ifndef IO_TEST_HELPER_HH
+#define IO_TEST_HELPER_HH
+
+#include "io.hh" // IWYU pragma: export
+
+#include <cstddef>
+#include <memory>
+
+[[nodiscard]]
+std::unique_ptr<io::Reader> io_make_breaking(
+ std::unique_ptr<io::Reader> reader, size_t offset = 0,
+ io::ReadError error = io::ReadError::Error);
+
+[[nodiscard]]
+std::unique_ptr<io::Reader> io_make_max_block(
+ std::unique_ptr<io::Reader> reader, size_t max_block_size);
+
+#endif // IO_TEST_HELPER_HH
diff --git a/test/line.cc b/test/line.cc
new file mode 100644
index 0000000..e0ea002
--- /dev/null
+++ b/test/line.cc
@@ -0,0 +1,182 @@
+#include "line.hh"
+
+#include "io_test_helper.hh"
+
+#include <cstddef>
+#include <gtest/gtest.h>
+#include <limits>
+#include <utility>
+
+TEST(line, empty) {
+ auto reader = line::open(io::memory(""));
+ EXPECT_EQ(0, reader->number());
+ auto line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+ EXPECT_EQ(0, reader->number());
+}
+
+TEST(line, one_line) {
+ auto reader = line::open(io::memory("foo"));
+ EXPECT_EQ(0, reader->number());
+ auto line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("foo", line.value());
+ EXPECT_EQ(1, reader->number());
+ line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+ EXPECT_EQ(1, reader->number());
+}
+
+TEST(line, many_lines) {
+ auto reader = line::open(io::memory("foo\nbar\nfoobar\n"));
+ EXPECT_EQ(0, reader->number());
+ auto line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("foo", line.value());
+ EXPECT_EQ(1, reader->number());
+ line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("bar", line.value());
+ EXPECT_EQ(2, reader->number());
+ line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("foobar", line.value());
+ EXPECT_EQ(3, reader->number());
+ line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+ EXPECT_EQ(3, reader->number());
+}
+
+TEST(line, many_lines_mixed) {
+ auto reader = line::open(io::memory("foo\r\nbar\rfoobar\n"));
+ EXPECT_EQ(0, reader->number());
+ auto line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("foo", line.value());
+ EXPECT_EQ(1, reader->number());
+ line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("bar", line.value());
+ EXPECT_EQ(2, reader->number());
+ line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("foobar", line.value());
+ EXPECT_EQ(3, reader->number());
+ line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+ EXPECT_EQ(3, reader->number());
+}
+
+TEST(line, empty_line) {
+ auto reader = line::open(io::memory("\n"));
+ EXPECT_EQ(0, reader->number());
+ auto line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("", line.value());
+ EXPECT_EQ(1, reader->number());
+ line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+ EXPECT_EQ(1, reader->number());
+}
+
+TEST(line, max_line) {
+ auto reader = line::open(io::memory("012345678901234567890123456789"), 10);
+ EXPECT_EQ(0, reader->number());
+ auto line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("0123456789", line.value());
+ EXPECT_EQ(1, reader->number());
+ line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("0123456789", line.value());
+ EXPECT_EQ(2, reader->number());
+ line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("0123456789", line.value());
+ EXPECT_EQ(3, reader->number());
+ line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+ EXPECT_EQ(3, reader->number());
+}
+
+TEST(line, read_error) {
+ auto reader = line::open(
+ io_make_breaking(io::memory("foo bar fum\nfim zam"), /* offset */ 5));
+ auto line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Error, line.error());
+}
+
+TEST(line, read_error_newline) {
+ auto reader = line::open(
+ io_make_breaking(io::memory("foo bar\r\nfim zam"), /* offset */ 8));
+ auto line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Error, line.error());
+}
+
+TEST(line, blocky) {
+ auto reader = line::open(io_make_max_block(io::memory("foo bar\r\nfim zam"),
+ /* max_block_size */ 1));
+ auto line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("foo bar", line.value());
+ line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("fim zam", line.value());
+ line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+}
+
+TEST(line, blocky_newline) {
+ auto reader = line::open(io_make_max_block(io::memory("foo bar\r\nfim zam"),
+ /* max_block_size */ 8));
+ auto line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("foo bar", line.value());
+ line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("fim zam", line.value());
+ line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+}
+
+TEST(line, eof_newline) {
+ auto reader = line::open(io::memory("foo bar\r"));
+ auto line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("foo bar", line.value());
+ line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+}
+
+TEST(line, max_newline) {
+ auto reader = line::open(io::memory("foo bar\r"), 6);
+ auto line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("foo ba", line.value());
+ line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("r", line.value());
+ line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+}
+
+TEST(line, max_line_overflow) {
+ EXPECT_DEATH_IF_SUPPORTED(
+ {
+ std::ignore =
+ line::open(io::memory(""), std::numeric_limits<size_t>::max());
+ },
+ "");
+}
diff --git a/test/str.cc b/test/str.cc
new file mode 100644
index 0000000..6d7edf2
--- /dev/null
+++ b/test/str.cc
@@ -0,0 +1,67 @@
+#include "str.hh"
+
+#include <gtest/gtest.h>
+
+TEST(str, split) {
+ auto ret = str::split("");
+ EXPECT_EQ(0, ret.size());
+
+ ret = str::split("", ' ', true);
+ ASSERT_EQ(1, ret.size());
+ EXPECT_EQ("", ret[0]);
+
+ ret = str::split(" ");
+ EXPECT_EQ(0, ret.size());
+
+ ret = str::split(" ", ' ', true);
+ ASSERT_EQ(2, ret.size());
+ EXPECT_EQ("", ret[0]);
+ EXPECT_EQ("", ret[1]);
+
+ ret = str::split(" a b ");
+ ASSERT_EQ(2, ret.size());
+ EXPECT_EQ("a", ret[0]);
+ EXPECT_EQ("b", ret[1]);
+
+ ret = str::split(" a b ", ' ', true);
+ ASSERT_EQ(4, ret.size());
+ EXPECT_EQ("", ret[0]);
+ EXPECT_EQ("a", ret[1]);
+ EXPECT_EQ("b", ret[2]);
+ EXPECT_EQ("", ret[3]);
+
+ ret = str::split(" a b", ' ', true);
+ ASSERT_EQ(3, ret.size());
+ EXPECT_EQ("", ret[0]);
+ EXPECT_EQ("a", ret[1]);
+ EXPECT_EQ("b", ret[2]);
+}
+
+TEST(str, trim) {
+ auto ret = str::trim("");
+ EXPECT_EQ("", ret);
+
+ ret = str::trim(" ");
+ EXPECT_EQ("", ret);
+
+ ret = str::trim(" ");
+ EXPECT_EQ("", ret);
+
+ ret = str::trim("foo");
+ EXPECT_EQ("foo", ret);
+
+ ret = str::trim(" foo");
+ EXPECT_EQ("foo", ret);
+
+ ret = str::trim(" foo ");
+ EXPECT_EQ("foo", ret);
+
+ ret = str::trim("foo ");
+ EXPECT_EQ("foo", ret);
+
+ ret = str::trim(" foo bar ");
+ EXPECT_EQ("foo bar", ret);
+
+ ret = str::trim("\tfoo bar\r\n");
+ EXPECT_EQ("foo bar", ret);
+}