summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2026-01-02 22:42:31 +0100
committerJoel Klinghed <the_jk@spawned.biz>2026-01-02 22:42:31 +0100
commit6ed8f5151719fbc14ec0ac6d28a346d1f74cf2ca (patch)
treeebe7588e89e1aa2ae5376acf85f3a3a7b2ec7e10 /test
Initial commitHEADmain
Diffstat (limited to 'test')
-rw-r--r--test/args.cc412
-rw-r--r--test/cfg.cc190
-rw-r--r--test/data/test.jpegbin0 -> 734 bytes
-rw-r--r--test/data/test.pngbin0 -> 171 bytes
-rw-r--r--test/data/test.svg62
-rw-r--r--test/data/test.xpm17
-rw-r--r--test/image_processor.cc251
-rw-r--r--test/io.cc274
-rw-r--r--test/io_test_helper.cc165
-rw-r--r--test/io_test_helper.hh27
-rw-r--r--test/line.cc182
-rw-r--r--test/paths.cc63
-rw-r--r--test/str.cc164
-rw-r--r--test/testdir.cc37
-rw-r--r--test/testdir.hh22
-rw-r--r--test/testenv.cc44
-rw-r--r--test/testenv.hh22
-rw-r--r--test/u8.cc276
18 files changed, 2208 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/cfg.cc b/test/cfg.cc
new file mode 100644
index 0000000..0c27989
--- /dev/null
+++ b/test/cfg.cc
@@ -0,0 +1,190 @@
+#include "cfg.hh"
+
+#include "testdir.hh"
+#include "testenv.hh"
+
+#include <fstream>
+#include <gtest/gtest.h>
+#include <string>
+#include <vector>
+
+namespace {
+
+class ConfigTest : public TestEnv {
+ protected:
+ void SetUp() override { ASSERT_TRUE(dir_.good()); }
+
+ TestDir dir_;
+};
+
+} // namespace
+
+TEST_F(ConfigTest, empty) {
+ auto does_not_exist = dir_.path() / "does-not-exist";
+ std::vector<std::string> errors;
+ auto cfg = cfg::load_one(does_not_exist, errors);
+ ASSERT_TRUE(cfg);
+ EXPECT_EQ(1, errors.size());
+
+ EXPECT_FALSE(cfg->has(""));
+ EXPECT_FALSE(cfg->get("").has_value());
+
+ EXPECT_FALSE(cfg->has("foo"));
+ EXPECT_FALSE(cfg->get("foo").has_value());
+}
+
+TEST_F(ConfigTest, values) {
+ auto file = dir_.path() / "file";
+ {
+ std::ofstream out(file);
+ out << "# Comment\n"
+ << "key=value\n"
+ << " foo = bar \n"
+ << "i1 = 12\n"
+ << "b1 = true\n"
+ << "b2=FaLSe\n"
+ << "i2 = -12313\n";
+ }
+ std::vector<std::string> errors;
+ auto cfg = cfg::load_one(file, errors);
+ ASSERT_TRUE(cfg);
+ EXPECT_EQ(0, errors.size());
+
+ EXPECT_FALSE(cfg->has(""));
+ EXPECT_FALSE(cfg->get("").has_value());
+
+ EXPECT_TRUE(cfg->has("key"));
+ EXPECT_EQ("value", cfg->get("key").value_or(""));
+
+ EXPECT_TRUE(cfg->has("foo"));
+ EXPECT_EQ("bar", cfg->get("foo").value_or(""));
+ EXPECT_FALSE(cfg->get_int64("foo").has_value());
+
+ EXPECT_TRUE(cfg->has("i1"));
+ EXPECT_EQ("12", cfg->get("i1").value_or(""));
+ EXPECT_EQ(12, cfg->get_int64("i1").value_or(0));
+ EXPECT_EQ(12, cfg->get_uint64("i1").value_or(0));
+
+ EXPECT_TRUE(cfg->has("b1"));
+ EXPECT_EQ("true", cfg->get("b1").value_or(""));
+ EXPECT_TRUE(cfg->get_bool("b1").value_or(false));
+
+ EXPECT_TRUE(cfg->has("b2"));
+ EXPECT_EQ("FaLSe", cfg->get("b2").value_or(""));
+ EXPECT_FALSE(cfg->get_bool("b2").value_or(true));
+
+ EXPECT_TRUE(cfg->has("i2"));
+ EXPECT_EQ("-12313", cfg->get("i2").value_or(""));
+ EXPECT_EQ(-12313, cfg->get_int64("i2").value_or(0));
+ EXPECT_EQ(0, cfg->get_uint64("i2").value_or(0));
+}
+
+TEST_F(ConfigTest, bools) {
+ auto file = dir_.path() / "file";
+ {
+ std::ofstream out(file);
+ out << "key1=True\n"
+ << "key2=yES\n"
+ << "key3=false\n"
+ << "key4=NO\n"
+ << "key5=ja\n";
+ }
+ std::vector<std::string> errors;
+ auto cfg = cfg::load_one(file, errors);
+ ASSERT_TRUE(cfg);
+ EXPECT_EQ(0, errors.size());
+
+ EXPECT_TRUE(cfg->get_bool("key1").value_or(false));
+ EXPECT_TRUE(cfg->get_bool("key2").value_or(false));
+ EXPECT_FALSE(cfg->get_bool("key3").value_or(true));
+ EXPECT_FALSE(cfg->get_bool("key4").value_or(true));
+ EXPECT_FALSE(cfg->get_bool("key5").has_value());
+}
+
+TEST_F(ConfigTest, errors) {
+ auto file = dir_.path() / "file";
+ {
+ std::ofstream out(file);
+ out << "bad line\n"
+ << "key=value\n"
+ << "key=duplicate\n";
+ }
+ std::vector<std::string> errors;
+ auto cfg = cfg::load_one(file, errors);
+ ASSERT_TRUE(cfg);
+ EXPECT_EQ(2, errors.size());
+}
+
+TEST_F(ConfigTest, merge) {
+ auto dir1 = dir_.path() / "dir1";
+ auto dir2 = dir_.path() / "dir2";
+ auto dir3 = dir_.path() / "dir3";
+ std::filesystem::create_directory(dir1);
+ std::filesystem::create_directory(dir2);
+ std::filesystem::create_directory(dir3);
+ setenv("XDG_CONFIG_HOME", dir1);
+ setenv("XDG_CONFIG_DIRS", dir2.string() + ":" + dir3.string());
+ auto file1 = dir1 / "file";
+ auto file2 = dir2 / "file";
+ {
+ std::ofstream out(file2);
+ out << "key1 = value1\n"
+ << "key2 = value2\n";
+ }
+ {
+ std::ofstream out(file1);
+ out << "key1 = 12\n"
+ << "key3 = value3";
+ }
+ std::vector<std::string> errors;
+ auto cfg = cfg::load_all("file", errors);
+ ASSERT_TRUE(cfg);
+ EXPECT_EQ(0, errors.size());
+
+ EXPECT_FALSE(cfg->has(""));
+ EXPECT_FALSE(cfg->get("").has_value());
+
+ EXPECT_TRUE(cfg->has("key1"));
+ EXPECT_EQ("12", cfg->get("key1").value_or(""));
+
+ EXPECT_TRUE(cfg->has("key2"));
+ EXPECT_EQ("value2", cfg->get("key2").value_or(""));
+
+ EXPECT_TRUE(cfg->has("key3"));
+ EXPECT_EQ("value3", cfg->get("key3").value_or(""));
+}
+
+TEST_F(ConfigTest, merge_errors) {
+ auto dir1 = dir_.path() / "dir1";
+ auto dir2 = dir_.path() / "dir2";
+ std::filesystem::create_directory(dir1);
+ std::filesystem::create_directory(dir2);
+ setenv("XDG_CONFIG_HOME", dir1);
+ setenv("XDG_CONFIG_DIRS", dir2);
+ auto file1 = dir1 / "file";
+ auto file2 = dir2 / "file";
+ {
+ std::ofstream out(file2);
+ out << "key1 = value1\n"
+ << "key2 = value2\n";
+ }
+ {
+ std::ofstream out(file1);
+ out << "invalid line";
+ }
+ std::vector<std::string> errors;
+ auto cfg = cfg::load_all("file", errors);
+ ASSERT_TRUE(cfg);
+ EXPECT_EQ(1, errors.size());
+
+ EXPECT_FALSE(cfg->has(""));
+ EXPECT_FALSE(cfg->get("").has_value());
+
+ EXPECT_TRUE(cfg->has("key1"));
+ EXPECT_EQ("value1", cfg->get("key1").value_or(""));
+
+ EXPECT_TRUE(cfg->has("key2"));
+ EXPECT_EQ("value2", cfg->get("key2").value_or(""));
+
+ EXPECT_FALSE(cfg->has("key3"));
+}
diff --git a/test/data/test.jpeg b/test/data/test.jpeg
new file mode 100644
index 0000000..0bc367a
--- /dev/null
+++ b/test/data/test.jpeg
Binary files differ
diff --git a/test/data/test.png b/test/data/test.png
new file mode 100644
index 0000000..6c4877b
--- /dev/null
+++ b/test/data/test.png
Binary files differ
diff --git a/test/data/test.svg b/test/data/test.svg
new file mode 100644
index 0000000..155a68a
--- /dev/null
+++ b/test/data/test.svg
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="10"
+ height="10"
+ viewBox="0 0 10 10"
+ version="1.1"
+ id="svg1"
+ inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
+ sodipodi:docname="test.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1" />
+ <defs
+ id="defs1" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <rect
+ style="fill:#ff0000;fill-rule:evenodd;"
+ id="rect1"
+ width="5"
+ height="5"
+ x="0"
+ y="0" />
+ <rect
+ style="opacity:0.5;fill:#ff0000;fill-rule:evenodd"
+ id="rect1-36"
+ width="5"
+ height="5"
+ x="5"
+ y="5" />
+ <rect
+ style="fill:#00ff00;fill-rule:evenodd"
+ id="rect1-3"
+ width="5"
+ height="5"
+ x="5"
+ y="0" />
+ <rect
+ style="fill:#0000ff;fill-rule:evenodd"
+ id="rect1-3-1"
+ width="5"
+ height="5"
+ x="0"
+ y="5" />
+ </g>
+</svg>
diff --git a/test/data/test.xpm b/test/data/test.xpm
new file mode 100644
index 0000000..12eb337
--- /dev/null
+++ b/test/data/test.xpm
@@ -0,0 +1,17 @@
+/* XPM */
+static char * test_xpm[] = {
+"10 10 4 1",
+" c None",
+". c #FF0000",
+"+ c #00FF00",
+"@ c #0000FF",
+".....+++++",
+".....+++++",
+".....+++++",
+".....+++++",
+".....+++++",
+"@@@@@ ",
+"@@@@@ ",
+"@@@@@ ",
+"@@@@@ ",
+"@@@@@ "};
diff --git a/test/image_processor.cc b/test/image_processor.cc
new file mode 100644
index 0000000..c005f01
--- /dev/null
+++ b/test/image_processor.cc
@@ -0,0 +1,251 @@
+#include "image_processor.hh"
+
+#include "config.h"
+#include "image.hh"
+#include "image_loader.hh"
+#include "spawner.hh"
+
+#include <bit>
+#include <cmath>
+#include <cstdint>
+#include <filesystem>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <memory>
+#include <string>
+#include <utility>
+
+#ifndef TESTDIR
+# define TESTDIR "."
+#endif
+
+namespace {
+
+// JPEG compression at quality 90
+constexpr int kLossyError = 32;
+
+MATCHER_P(ColourLossyEq, expected, "") {
+ uint8_t a1 = arg >> 24;
+ uint8_t e1 = expected >> 24;
+ uint8_t a2 = arg >> 16;
+ uint8_t e2 = expected >> 16;
+ uint8_t a3 = arg >> 8;
+ uint8_t e3 = expected >> 8;
+ uint8_t a4 = arg;
+ uint8_t e4 = expected;
+ return abs(static_cast<int>(a1) - static_cast<int>(e1)) <= kLossyError &&
+ abs(static_cast<int>(a2) - static_cast<int>(e2)) <= kLossyError &&
+ abs(static_cast<int>(a3) - static_cast<int>(e3)) <= kLossyError &&
+ abs(static_cast<int>(a4) - static_cast<int>(e4)) <= kLossyError;
+}
+
+std::string error2str(ImageLoadError err) {
+ switch (err) {
+ case ImageLoadError::kNoSuchFile:
+ return "no such file";
+ case ImageLoadError::kError:
+ return "error";
+ case ImageLoadError::kUnsupportedFormat:
+ return "unsupported format";
+ case ImageLoadError::kProcessError:
+ return "process error";
+ }
+ std::unreachable();
+}
+
+class ImageProcessorTest : public testing::Test {
+ protected:
+ void SetUp() override {
+ {
+ auto spawner = Spawner::create();
+ ASSERT_TRUE(spawner.has_value());
+ spawner_ = std::move(spawner.value());
+ }
+
+ {
+ auto process = spawner_->run(Spawner::Exec::kImageProcessor);
+ ASSERT_TRUE(process.has_value());
+ process_ = std::move(process.value());
+ }
+ }
+
+ [[nodiscard]]
+ Process& process() const {
+ return *process_;
+ }
+
+ [[nodiscard]]
+ static std::filesystem::path testdir() {
+ return TESTDIR;
+ }
+
+ void peek(std::string const& name) {
+ auto ret = image_processor::peek(process(), testdir() / name);
+ ASSERT_TRUE(ret.has_value()) << error2str(ret.error());
+ EXPECT_EQ(10, ret->width);
+ EXPECT_EQ(10, ret->height);
+ }
+
+ private:
+ std::unique_ptr<Spawner> spawner_;
+ std::unique_ptr<Process> process_;
+};
+
+enum class AlphaMode : uint8_t {
+ kFull,
+ kMono,
+ kNone,
+};
+
+class ImageProcessorTestWithFormat : public ImageProcessorTest, public testing::WithParamInterface<Image::Format> {
+ protected:
+ void load(std::string const& name, AlphaMode alpha = AlphaMode::kFull, bool lossy = false) {
+ auto ret = image_processor::load(process(), testdir() / name, GetParam());
+ ASSERT_TRUE(ret.has_value()) << error2str(ret.error());
+ EXPECT_EQ(10, ret.value()->width());
+ EXPECT_EQ(10, ret.value()->height());
+ EXPECT_EQ(GetParam(), ret.value()->format());
+
+ EXPECT_GE(4 * ret.value()->width(), ret.value()->scanline());
+ if (lossy) {
+ for (uint32_t y = 0; y < ret.value()->height(); ++y) {
+ for (uint32_t x = 0; x < ret.value()->width(); ++x) {
+ EXPECT_THAT(reinterpret_cast<uint32_t const*>(
+ ret.value()->data() + (y * ret.value()->scanline()))[x],
+ ColourLossyEq(expected_color(x, y, alpha)))
+ << x << "x" << y;
+ }
+ }
+ } else {
+ for (uint32_t y = 0; y < ret.value()->height(); ++y) {
+ for (uint32_t x = 0; x < ret.value()->width(); ++x) {
+ EXPECT_EQ(expected_color(x, y, alpha),
+ reinterpret_cast<uint32_t const*>(
+ ret.value()->data() + (y * ret.value()->scanline()))[x])
+ << x << "x" << y;
+ }
+ }
+ }
+ }
+
+ private:
+ static uint32_t expected_argb(uint32_t x, uint32_t y) {
+ uint32_t argb;
+ if (x < 5) {
+ if (y < 5) {
+ argb = 0xffff0000;
+ } else {
+ argb = 0xff0000ff;
+ }
+ } else {
+ if (y < 5) {
+ argb = 0xff00ff00;
+ } else {
+ argb = 0x80ff0000;
+ }
+ }
+ return argb;
+ }
+
+ static uint32_t argb_to_format(uint32_t argb) {
+ if constexpr (std::endian::native == std::endian::big) {
+ switch (GetParam()) {
+ case Image::Format::ARGB_8888:
+ return argb;
+ case Image::Format::BGRA_8888:
+ return std::byteswap(argb);
+ case Image::Format::RGBA_8888:
+ return std::rotl(argb, 8);
+ case Image::Format::ABGR_8888:
+ return std::byteswap(std::rotl(argb, 8));
+ }
+ } else {
+ switch (GetParam()) {
+ case Image::Format::ARGB_8888:
+ return std::byteswap(argb);
+ case Image::Format::BGRA_8888:
+ return argb;
+ case Image::Format::RGBA_8888:
+ return std::byteswap(std::rotl(argb, 8));
+ case Image::Format::ABGR_8888:
+ return std::rotl(argb, 8);
+ }
+ }
+ std::unreachable();
+ }
+
+ static uint32_t expected_color(uint32_t x, uint32_t y, AlphaMode alpha) {
+ uint32_t argb = expected_argb(x, y);
+ switch (alpha) {
+ case AlphaMode::kFull:
+ break;
+ case AlphaMode::kMono:
+ if (argb >> 24 != 0xff)
+ argb = 0;
+ break;
+ case AlphaMode::kNone:
+ argb |= 0xff000000;
+ break;
+ }
+ return argb_to_format(argb);
+ }
+
+ static uint32_t expected_color_no_alpha(uint32_t x, uint32_t y) {
+ return argb_to_format(0xff000000 | expected_argb(x, y));
+ }
+};
+
+} // namespace
+
+TEST_F(ImageProcessorTest, peek_no_such_file) {
+ auto ret = image_processor::peek(process(), testdir() / "does_not_exist.png");
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(ImageLoadError::kNoSuchFile, ret.error());
+}
+
+#if HAVE_PNG
+TEST_F(ImageProcessorTest, peek_png) {
+ peek("test.png");
+}
+
+TEST_P(ImageProcessorTestWithFormat, load_png) {
+ load("test.png");
+}
+#endif
+
+#if HAVE_JPEG
+TEST_F(ImageProcessorTest, peek_jpeg) {
+ peek("test.jpeg");
+}
+
+TEST_P(ImageProcessorTestWithFormat, load_jpeg) {
+ load("test.jpeg", AlphaMode::kNone, /* lossy */ true);
+}
+#endif
+
+#if HAVE_XPM
+TEST_F(ImageProcessorTest, peek_xpm) {
+ peek("test.xpm");
+}
+
+TEST_P(ImageProcessorTestWithFormat, load_xpm) {
+ load("test.xpm", AlphaMode::kMono);
+}
+#endif
+
+#if HAVE_RSVG
+TEST_F(ImageProcessorTest, peek_svg) {
+ peek("test.svg");
+}
+
+TEST_P(ImageProcessorTestWithFormat, load_svg) {
+ load("test.svg");
+}
+#endif
+
+INSTANTIATE_TEST_SUITE_P(ImageProcessor,
+ ImageProcessorTestWithFormat,
+ testing::Values(Image::Format::RGBA_8888,
+ Image::Format::ARGB_8888,
+ Image::Format::BGRA_8888,
+ Image::Format::ABGR_8888));
diff --git a/test/io.cc b/test/io.cc
new file mode 100644
index 0000000..39d558b
--- /dev/null
+++ b/test/io.cc
@@ -0,0 +1,274 @@
+#include "io.hh"
+
+#include "io_test_helper.hh"
+
+#include <cerrno>
+#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 ret = io::createat(dirfd(), name, false);
+ ASSERT_TRUE(ret.has_value());
+ auto ret2 = ret.value()->repeat_write(value.data(), value.size());
+ EXPECT_TRUE(ret2.has_value());
+ if (ret2.has_value()) {
+ EXPECT_EQ(ret2.value(), value.size());
+ }
+ EXPECT_TRUE(ret.value()->close().has_value());
+ }
+
+ void cat(const std::string& name, const std::string& value) {
+ auto ret = io::openat(dirfd(), name);
+ ASSERT_TRUE(ret.has_value());
+ std::string tmp(value.size(), ' ');
+ auto ret2 = ret.value()->repeat_read(tmp.data(), tmp.size());
+ ASSERT_TRUE(ret2.has_value());
+ EXPECT_EQ(value.size(), ret2.value());
+ EXPECT_EQ(tmp, value);
+ ret2 = ret.value()->repeat_read(tmp.data(), tmp.size());
+ ASSERT_FALSE(ret2.has_value());
+ EXPECT_EQ(io::ReadError::Eof, ret2.error());
+ }
+
+ 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);
+}
+
+TEST_F(IoTest, create) {
+ auto ret = io::createat(dirfd(), "test", false);
+ ASSERT_TRUE(ret.has_value());
+ std::string tmp{"hello world"};
+ auto ret2 = ret.value()->repeat_write(tmp.data(), tmp.size());
+ ASSERT_TRUE(ret2.has_value());
+ EXPECT_EQ(11, ret2.value());
+ EXPECT_TRUE(ret.value()->close().has_value());
+
+ cat("test", "hello world");
+}
+
+TEST_F(IoTest, create_exists) {
+ touch("test");
+
+ auto ret = io::createat(dirfd(), "test", false);
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(io::CreateError::Exists, ret.error());
+}
+
+TEST_F(IoTest, create_overwrite) {
+ touch("test");
+
+ auto ret = io::createat(dirfd(), "test", true);
+ ASSERT_TRUE(ret.has_value());
+ std::string tmp{"hello world"};
+ auto ret2 = ret.value()->repeat_write(tmp.data(), tmp.size());
+ ASSERT_TRUE(ret2.has_value());
+ EXPECT_EQ(11, ret2.value());
+ EXPECT_TRUE(ret.value()->close().has_value());
+
+ cat("test", "hello world");
+}
+
+TEST_F(IoTest, write_block) {
+ auto ret = io::createat(dirfd(), "test", false);
+ ASSERT_TRUE(ret.has_value());
+ auto ret2 = io_make_max_block(std::move(ret.value()), 2);
+ std::string tmp{"hello world"};
+ auto ret3 = ret2->repeat_write(tmp.data(), tmp.size());
+ ASSERT_TRUE(ret3.has_value());
+ EXPECT_EQ(11, ret3.value());
+ EXPECT_TRUE(ret2->close().has_value());
+
+ cat("test", "hello world");
+}
+
+TEST_F(IoTest, pipe) {
+ auto ret = io::pipe();
+ ASSERT_TRUE(ret.has_value());
+
+ std::string tmp{"hello world"};
+ auto ret2 = ret.value().second->repeat_write(tmp.data(), tmp.size());
+ ASSERT_TRUE(ret2.has_value());
+ EXPECT_EQ(11, ret2.value());
+ ret.value().second.reset();
+
+ std::string tmp2(12, ' ');
+ auto ret3 = ret.value().first->repeat_read(tmp2.data(), tmp2.size());
+ ASSERT_TRUE(ret3.has_value());
+ EXPECT_EQ(11, ret3.value());
+ tmp2.resize(ret3.value());
+ EXPECT_EQ(tmp, tmp2);
+}
diff --git a/test/io_test_helper.cc b/test/io_test_helper.cc
new file mode 100644
index 0000000..880a114
--- /dev/null
+++ b/test/io_test_helper.cc
@@ -0,0 +1,165 @@
+#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;
+ }
+
+ [[nodiscard]]
+ int raw_fd() const override {
+ return reader_->raw_fd();
+ }
+
+ private:
+ std::unique_ptr<io::Reader> reader_;
+ size_t offset_;
+ io::ReadError const error_;
+};
+
+class BreakingWriter : public io::Writer {
+ public:
+ BreakingWriter(std::unique_ptr<io::Writer> writer, size_t offset,
+ io::WriteError error)
+ : writer_(std::move(writer)), offset_(offset), error_(error) {}
+
+ [[nodiscard]]
+ std::expected<size_t, io::WriteError> write(void const* dst,
+ size_t size) override {
+ if (offset_ == 0)
+ return std::unexpected(error_);
+ size_t avail = std::min(offset_, size);
+ auto ret = writer_->write(dst, avail);
+ if (ret.has_value()) {
+ offset_ -= ret.value();
+ }
+ return ret;
+ }
+
+ [[nodiscard]]
+ std::expected<void, io::WriteError> close() override {
+ return writer_->close();
+ }
+
+ [[nodiscard]]
+ int raw_fd() const override {
+ return writer_->raw_fd();
+ }
+
+ private:
+ std::unique_ptr<io::Writer> writer_;
+ size_t offset_;
+ io::WriteError 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);
+ }
+
+ [[nodiscard]]
+ int raw_fd() const override {
+ return reader_->raw_fd();
+ }
+
+ private:
+ std::unique_ptr<io::Reader> reader_;
+ size_t const max_block_size_;
+};
+
+class MaxBlockWriter : public io::Writer {
+ public:
+ MaxBlockWriter(std::unique_ptr<io::Writer> writer, size_t max_block_size)
+ : writer_(std::move(writer)), max_block_size_(max_block_size) {}
+
+ [[nodiscard]]
+ std::expected<size_t, io::WriteError> write(void const* dst,
+ size_t size) override {
+ size_t avail = std::min(max_block_size_, size);
+ return writer_->write(dst, avail);
+ }
+
+ [[nodiscard]]
+ std::expected<void, io::WriteError> close() override {
+ return writer_->close();
+ }
+
+ [[nodiscard]]
+ int raw_fd() const override {
+ return writer_->raw_fd();
+ }
+
+ private:
+ std::unique_ptr<io::Writer> writer_;
+ 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::Writer> io_make_breaking(std::unique_ptr<io::Writer> writer,
+ size_t offset,
+ io::WriteError error) {
+ return std::make_unique<BreakingWriter>(std::move(writer), 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);
+}
+
+std::unique_ptr<io::Writer> io_make_max_block(
+ std::unique_ptr<io::Writer> writer, size_t max_block_size) {
+ return std::make_unique<MaxBlockWriter>(std::move(writer), max_block_size);
+}
diff --git a/test/io_test_helper.hh b/test/io_test_helper.hh
new file mode 100644
index 0000000..eb7bbb3
--- /dev/null
+++ b/test/io_test_helper.hh
@@ -0,0 +1,27 @@
+#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::Writer> io_make_breaking(
+ std::unique_ptr<io::Writer> writer, size_t offset = 0,
+ io::WriteError error = io::WriteError::Error);
+
+[[nodiscard]]
+std::unique_ptr<io::Reader> io_make_max_block(
+ std::unique_ptr<io::Reader> reader, size_t max_block_size);
+
+[[nodiscard]]
+std::unique_ptr<io::Writer> io_make_max_block(
+ std::unique_ptr<io::Writer> writer, 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/paths.cc b/test/paths.cc
new file mode 100644
index 0000000..b07e459
--- /dev/null
+++ b/test/paths.cc
@@ -0,0 +1,63 @@
+#include "paths.hh"
+
+#include "testenv.hh"
+
+#include <gtest/gtest.h>
+
+namespace {
+
+class PathsTest : public TestEnv {};
+
+} // namespace
+
+TEST_F(PathsTest, home) {
+ setenv("HOME", "foo-bar");
+ EXPECT_EQ("foo-bar", paths::home());
+ unsetenv("HOME");
+ EXPECT_NE("foo-bar", paths::home());
+ setenv("HOME", "");
+ EXPECT_NE("", paths::home());
+}
+
+TEST_F(PathsTest, config_dirs) {
+ setenv("XDG_CONFIG_HOME", "foo");
+ setenv("XDG_CONFIG_DIRS", "bar:fum::foo");
+ auto dirs = paths::config_dirs();
+ EXPECT_EQ(3, dirs.size());
+ if (dirs.size() >= 3) {
+ EXPECT_EQ("foo", dirs[0]);
+ EXPECT_EQ("bar", dirs[1]);
+ EXPECT_EQ("fum", dirs[2]);
+ }
+ setenv("HOME", "home");
+ unsetenv("XDG_CONFIG_HOME");
+ unsetenv("XDG_CONFIG_DIRS");
+ dirs = paths::config_dirs();
+ EXPECT_EQ(2, dirs.size());
+ if (dirs.size() >= 2) {
+ EXPECT_EQ("home/.config", dirs[0]);
+ EXPECT_EQ("/etc/xdg", dirs[1]);
+ }
+}
+
+TEST_F(PathsTest, data_dirs) {
+ setenv("XDG_DATA_HOME", "foo");
+ setenv("XDG_DATA_DIRS", "bar:fum::foo");
+ auto dirs = paths::data_dirs();
+ EXPECT_EQ(3, dirs.size());
+ if (dirs.size() >= 3) {
+ EXPECT_EQ("foo", dirs[0]);
+ EXPECT_EQ("bar", dirs[1]);
+ EXPECT_EQ("fum", dirs[2]);
+ }
+ setenv("HOME", "home");
+ unsetenv("XDG_DATA_HOME");
+ unsetenv("XDG_DATA_DIRS");
+ dirs = paths::data_dirs();
+ EXPECT_EQ(3, dirs.size());
+ if (dirs.size() >= 3) {
+ EXPECT_EQ("home/.local/share", dirs[0]);
+ EXPECT_EQ("/usr/local/share/", dirs[1]);
+ EXPECT_EQ("/usr/share/", dirs[2]);
+ }
+}
diff --git a/test/str.cc b/test/str.cc
new file mode 100644
index 0000000..0184ad5
--- /dev/null
+++ b/test/str.cc
@@ -0,0 +1,164 @@
+#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, split2) {
+ auto ret = str::split("", "\r\n");
+ EXPECT_EQ(0, ret.size());
+
+ ret = str::split("", "\r\n", true);
+ ASSERT_EQ(1, ret.size());
+ EXPECT_EQ("", ret[0]);
+
+ ret = str::split("\r\n", "\r\n");
+ EXPECT_EQ(0, ret.size());
+
+ ret = str::split("\r\n", "\r\n", true);
+ ASSERT_EQ(2, ret.size());
+ EXPECT_EQ("", ret[0]);
+ EXPECT_EQ("", ret[1]);
+
+ ret = str::split("\r\na\r\nb\r\n", "\r\n");
+ ASSERT_EQ(2, ret.size());
+ EXPECT_EQ("a", ret[0]);
+ EXPECT_EQ("b", ret[1]);
+
+ ret = str::split("\r\na\r\nb\r\n", "\r\n", 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("\r\na\r\nb", "\r\n", true);
+ ASSERT_EQ(3, ret.size());
+ EXPECT_EQ("", ret[0]);
+ EXPECT_EQ("a", ret[1]);
+ EXPECT_EQ("b", ret[2]);
+
+ ret = str::split("\ra\nb", "\r\n");
+ ASSERT_EQ(1, ret.size());
+ EXPECT_EQ("\ra\nb", ret[0]);
+}
+
+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);
+}
+
+TEST(str, ltrim) {
+ auto ret = str::ltrim("");
+ EXPECT_EQ("", ret);
+
+ ret = str::ltrim(" ");
+ EXPECT_EQ("", ret);
+
+ ret = str::ltrim(" ");
+ EXPECT_EQ("", ret);
+
+ ret = str::ltrim("foo");
+ EXPECT_EQ("foo", ret);
+
+ ret = str::ltrim(" foo");
+ EXPECT_EQ("foo", ret);
+
+ ret = str::ltrim(" foo ");
+ EXPECT_EQ("foo ", ret);
+
+ ret = str::ltrim("foo ");
+ EXPECT_EQ("foo ", ret);
+
+ ret = str::ltrim(" foo bar ");
+ EXPECT_EQ("foo bar ", ret);
+
+ ret = str::ltrim("\tfoo bar\r\n");
+ EXPECT_EQ("foo bar\r\n", ret);
+}
+
+TEST(str, rtrim) {
+ auto ret = str::rtrim("");
+ EXPECT_EQ("", ret);
+
+ ret = str::rtrim(" ");
+ EXPECT_EQ("", ret);
+
+ ret = str::rtrim(" ");
+ EXPECT_EQ("", ret);
+
+ ret = str::rtrim("foo");
+ EXPECT_EQ("foo", ret);
+
+ ret = str::rtrim(" foo");
+ EXPECT_EQ(" foo", ret);
+
+ ret = str::rtrim(" foo ");
+ EXPECT_EQ(" foo", ret);
+
+ ret = str::rtrim("foo ");
+ EXPECT_EQ("foo", ret);
+
+ ret = str::rtrim(" foo bar ");
+ EXPECT_EQ(" foo bar", ret);
+
+ ret = str::rtrim("\tfoo bar\r\n");
+ EXPECT_EQ("\tfoo bar", ret);
+}
diff --git a/test/testdir.cc b/test/testdir.cc
new file mode 100644
index 0000000..d5aef22
--- /dev/null
+++ b/test/testdir.cc
@@ -0,0 +1,37 @@
+#include "testdir.hh"
+
+#include <cstdlib>
+#include <filesystem>
+#include <format>
+#include <gtest/gtest.h>
+#include <system_error>
+
+namespace {
+
+std::filesystem::path mktmpdir() {
+ std::error_code ec;
+ auto base = std::filesystem::temp_directory_path(ec);
+ if (ec)
+ return {};
+ for (int i = 0; i < 10; ++i) {
+ auto name = std::format(
+ "{}-{}",
+ ::testing::UnitTest::GetInstance()->current_test_info()->name(),
+ rand());
+ auto tmpdir = base / name;
+ if (std::filesystem::exists(tmpdir))
+ continue;
+ if (std::filesystem::create_directory(tmpdir, ec))
+ return tmpdir;
+ }
+ return {};
+}
+
+} // namespace
+
+TestDir::TestDir() : path_(mktmpdir()) {}
+
+TestDir::~TestDir() {
+ if (!path_.empty())
+ std::filesystem::remove_all(path_);
+}
diff --git a/test/testdir.hh b/test/testdir.hh
new file mode 100644
index 0000000..b59e09a
--- /dev/null
+++ b/test/testdir.hh
@@ -0,0 +1,22 @@
+#ifndef TESTDIR_HH
+#define TESTDIR_HH
+
+#include <filesystem> // IWYU pragma: export
+
+class TestDir {
+ public:
+ TestDir();
+ ~TestDir();
+
+ TestDir(TestDir const&) = delete;
+ TestDir& operator=(TestDir const&) = delete;
+
+ bool good() const { return !path_.empty(); };
+
+ std::filesystem::path const& path() const { return path_; };
+
+ private:
+ std::filesystem::path path_;
+};
+
+#endif // TESTDIR_HH
diff --git a/test/testenv.cc b/test/testenv.cc
new file mode 100644
index 0000000..56701a4
--- /dev/null
+++ b/test/testenv.cc
@@ -0,0 +1,44 @@
+#include "testenv.hh"
+
+#include <cstdlib>
+#include <optional>
+#include <string>
+
+void TestEnv::setenv(std::string const& name, std::string const& value) {
+ saveenv(name);
+
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ ::setenv(name.c_str(), value.c_str(), 1);
+}
+
+void TestEnv::unsetenv(std::string const& name) {
+ saveenv(name);
+
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ ::unsetenv(name.c_str());
+}
+
+void TestEnv::TearDown() {
+ for (auto const& pair : env_) {
+ if (pair.second.has_value()) {
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ ::setenv(pair.first.c_str(), pair.second->c_str(), 1);
+ } else {
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ ::unsetenv(pair.first.c_str());
+ }
+ }
+}
+
+void TestEnv::saveenv(std::string const& name) {
+ auto it = env_.find(name);
+ if (it != env_.end())
+ return;
+
+ auto* str = getenv(name.c_str());
+ if (str == nullptr) {
+ env_.emplace(name, std::nullopt);
+ } else {
+ env_.emplace(name, str);
+ }
+}
diff --git a/test/testenv.hh b/test/testenv.hh
new file mode 100644
index 0000000..abe0bc8
--- /dev/null
+++ b/test/testenv.hh
@@ -0,0 +1,22 @@
+#ifndef TESTENV_HH
+#define TESTENV_HH
+
+#include <gtest/gtest.h>
+#include <map>
+#include <optional>
+#include <string>
+
+class TestEnv : public testing::Test {
+ protected:
+ void setenv(std::string const& name, std::string const& value);
+ void unsetenv(std::string const& name);
+
+ void TearDown() override;
+
+ private:
+ void saveenv(std::string const& name);
+
+ std::map<std::string, std::optional<std::string>> env_;
+};
+
+#endif // TESTENV_HH
diff --git a/test/u8.cc b/test/u8.cc
new file mode 100644
index 0000000..4378d9c
--- /dev/null
+++ b/test/u8.cc
@@ -0,0 +1,276 @@
+#include "u8.hh"
+
+#include <gtest/gtest.h>
+#include <iterator>
+#include <vector>
+
+TEST(u8, empty) {
+ std::vector<uint8_t> empty;
+ auto it = empty.begin();
+ auto ret = u8::read(it, empty.end());
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(u::ReadError::End, ret.error());
+
+ auto ret_replace = u8::read_replace(it, empty.end(), false);
+ ASSERT_FALSE(ret_replace.has_value());
+ EXPECT_EQ(u::ReadErrorReplace::End, ret_replace.error());
+
+ EXPECT_FALSE(u8::write(it, empty.end(), 0x40));
+
+ EXPECT_FALSE(u8::skip(it, empty.end()));
+}
+
+TEST(u8, examples) {
+ {
+ std::vector<uint8_t> literal{0x57};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_TRUE(ret.has_value());
+ EXPECT_EQ(0x57, *ret);
+ EXPECT_EQ(it, literal.end());
+
+ it = literal.begin();
+ EXPECT_TRUE(u8::skip(it, literal.end()));
+ EXPECT_EQ(it, literal.end());
+ }
+ {
+ std::vector<uint8_t> literal{0xce, 0x92};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_TRUE(ret.has_value());
+ EXPECT_EQ(0x392, *ret);
+ EXPECT_EQ(it, literal.end());
+
+ it = literal.begin();
+ EXPECT_TRUE(u8::skip(it, literal.end()));
+ EXPECT_EQ(it, literal.end());
+ }
+ {
+ std::vector<uint8_t> literal{0xec, 0x9c, 0x84};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_TRUE(ret.has_value());
+ EXPECT_EQ(0xc704, *ret);
+ EXPECT_EQ(it, literal.end());
+
+ it = literal.begin();
+ EXPECT_TRUE(u8::skip(it, literal.end()));
+ EXPECT_EQ(it, literal.end());
+ }
+ {
+ std::vector<uint8_t> literal{0xf0, 0x90, 0x8d, 0x85};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_TRUE(ret.has_value());
+ EXPECT_EQ(0x10345, *ret);
+ EXPECT_EQ(it, literal.end());
+
+ it = literal.begin();
+ auto ret_replace = u8::read_replace(it, literal.end(), false);
+ ASSERT_TRUE(ret_replace.has_value());
+ EXPECT_EQ(0x10345, *ret_replace);
+ EXPECT_EQ(it, literal.end());
+
+ it = literal.begin();
+ EXPECT_TRUE(u8::skip(it, literal.end()));
+ EXPECT_EQ(it, literal.end());
+ }
+ {
+ std::vector<uint8_t> literal(1, 0x0);
+ auto it = literal.begin();
+ EXPECT_TRUE(u8::write(it, literal.end(), 0x57));
+ EXPECT_EQ(0x57, literal[0]);
+ EXPECT_EQ(it, literal.end());
+ }
+ {
+ std::vector<uint8_t> literal(2, 0x0);
+ auto it = literal.begin();
+ EXPECT_TRUE(u8::write(it, literal.end(), 0x392));
+ EXPECT_EQ(0xce, literal[0]);
+ EXPECT_EQ(0x92, literal[1]);
+ EXPECT_EQ(it, literal.end());
+ }
+ {
+ std::vector<uint8_t> literal(3, 0x0);
+ auto it = literal.begin();
+ EXPECT_TRUE(u8::write(it, literal.end(), 0xc704));
+ EXPECT_EQ(0xec, literal[0]);
+ EXPECT_EQ(0x9c, literal[1]);
+ EXPECT_EQ(0x84, literal[2]);
+ EXPECT_EQ(it, literal.end());
+ }
+ {
+ std::vector<uint8_t> literal(4, 0x0);
+ auto it = literal.begin();
+ EXPECT_TRUE(u8::write(it, literal.end(), 0x10345));
+ EXPECT_EQ(0xf0, literal[0]);
+ EXPECT_EQ(0x90, literal[1]);
+ EXPECT_EQ(0x8d, literal[2]);
+ EXPECT_EQ(0x85, literal[3]);
+ EXPECT_EQ(it, literal.end());
+ }
+}
+
+TEST(u8, overlong) {
+ {
+ std::vector<uint8_t> literal{0xc0, 0x80};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(u::ReadError::Invalid, ret.error());
+ }
+}
+
+TEST(u8, incomplete) {
+ {
+ std::vector<uint8_t> literal{0xce};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(u::ReadError::Incomplete, ret.error());
+ }
+ {
+ std::vector<uint8_t> literal{0xec};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(u::ReadError::Incomplete, ret.error());
+ }
+ {
+ std::vector<uint8_t> literal{0xec, 0x9c};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(u::ReadError::Incomplete, ret.error());
+
+ it = literal.begin();
+ auto ret_replace = u8::read_replace(it, literal.end(), false);
+ ASSERT_FALSE(ret_replace.has_value());
+ EXPECT_EQ(u::ReadErrorReplace::Incomplete, ret_replace.error());
+
+ it = literal.begin();
+ ret_replace = u8::read_replace(it, literal.end(), true);
+ ASSERT_TRUE(ret_replace.has_value());
+ EXPECT_EQ(0xfffd, ret_replace.value());
+ }
+ {
+ std::vector<uint8_t> literal{0xf0};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(u::ReadError::Incomplete, ret.error());
+ }
+ {
+ std::vector<uint8_t> literal{0xf0, 0x90};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(u::ReadError::Incomplete, ret.error());
+ }
+ {
+ std::vector<uint8_t> literal{0xf0, 0x90, 0x8d};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(u::ReadError::Incomplete, ret.error());
+ }
+}
+
+TEST(u8, invalid) {
+ {
+ std::vector<uint8_t> literal{0xf0, 0xf0, 0xf0, 0xf0};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(u::ReadError::Invalid, ret.error());
+ it = literal.begin();
+ auto ret_replace = u8::read_replace(it, literal.end(), false);
+ ASSERT_TRUE(ret_replace.has_value());
+ EXPECT_EQ(0xfffd, *ret_replace);
+ EXPECT_EQ(it, std::next(literal.begin()));
+ }
+ {
+ std::vector<uint8_t> literal{0xa0};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(u::ReadError::Invalid, ret.error());
+ it = literal.begin();
+ auto ret_replace = u8::read_replace(it, literal.end(), false);
+ ASSERT_TRUE(ret_replace.has_value());
+ EXPECT_EQ(0xfffd, *ret_replace);
+ EXPECT_EQ(it, std::next(literal.begin()));
+ }
+ {
+ std::vector<uint8_t> literal{0xce, 0xff};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(u::ReadError::Invalid, ret.error());
+ it = literal.begin();
+ auto ret_replace = u8::read_replace(it, literal.end(), false);
+ ASSERT_TRUE(ret_replace.has_value());
+ EXPECT_EQ(0xfffd, *ret_replace);
+ EXPECT_EQ(it, std::next(literal.begin()));
+ }
+ {
+ std::vector<uint8_t> literal{0xec, 0xff, 0x84};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(u::ReadError::Invalid, ret.error());
+ it = literal.begin();
+ auto ret_replace = u8::read_replace(it, literal.end(), false);
+ ASSERT_TRUE(ret_replace.has_value());
+ EXPECT_EQ(0xfffd, *ret_replace);
+ EXPECT_EQ(it, std::next(literal.begin()));
+ }
+ {
+ std::vector<uint8_t> literal{0xec, 0x9c, 0xff};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(u::ReadError::Invalid, ret.error());
+ it = literal.begin();
+ auto ret_replace = u8::read_replace(it, literal.end(), false);
+ ASSERT_TRUE(ret_replace.has_value());
+ EXPECT_EQ(0xfffd, *ret_replace);
+ EXPECT_EQ(it, std::next(literal.begin()));
+ }
+ {
+ std::vector<uint8_t> literal{0xf0, 0xff, 0x8d, 0x85};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(u::ReadError::Invalid, ret.error());
+ it = literal.begin();
+ auto ret_replace = u8::read_replace(it, literal.end(), false);
+ ASSERT_TRUE(ret_replace.has_value());
+ EXPECT_EQ(0xfffd, *ret_replace);
+ EXPECT_EQ(it, std::next(literal.begin()));
+ }
+ {
+ std::vector<uint8_t> literal{0xf0, 0x90, 0xff, 0x85};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(u::ReadError::Invalid, ret.error());
+ it = literal.begin();
+ auto ret_replace = u8::read_replace(it, literal.end(), false);
+ ASSERT_TRUE(ret_replace.has_value());
+ EXPECT_EQ(0xfffd, *ret_replace);
+ EXPECT_EQ(it, std::next(literal.begin()));
+ }
+ {
+ std::vector<uint8_t> literal{0xf0, 0x90, 0x8d, 0xff};
+ auto it = literal.begin();
+ auto ret = u8::read(it, literal.end());
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(u::ReadError::Invalid, ret.error());
+ it = literal.begin();
+ auto ret_replace = u8::read_replace(it, literal.end(), false);
+ ASSERT_TRUE(ret_replace.has_value());
+ EXPECT_EQ(0xfffd, *ret_replace);
+ EXPECT_EQ(it, std::next(literal.begin()));
+ }
+}