From 6ed8f5151719fbc14ec0ac6d28a346d1f74cf2ca Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Fri, 2 Jan 2026 22:42:31 +0100 Subject: Initial commit --- test/args.cc | 412 ++++++++++++++++++++++++++++++++++++++++++++++++ test/cfg.cc | 190 ++++++++++++++++++++++ test/data/test.jpeg | Bin 0 -> 734 bytes test/data/test.png | Bin 0 -> 171 bytes test/data/test.svg | 62 ++++++++ test/data/test.xpm | 17 ++ test/image_processor.cc | 251 +++++++++++++++++++++++++++++ test/io.cc | 274 ++++++++++++++++++++++++++++++++ test/io_test_helper.cc | 165 +++++++++++++++++++ test/io_test_helper.hh | 27 ++++ test/line.cc | 182 +++++++++++++++++++++ test/paths.cc | 63 ++++++++ test/str.cc | 164 +++++++++++++++++++ test/testdir.cc | 37 +++++ test/testdir.hh | 22 +++ test/testenv.cc | 44 ++++++ test/testenv.hh | 22 +++ test/u8.cc | 276 ++++++++++++++++++++++++++++++++ 18 files changed, 2208 insertions(+) create mode 100644 test/args.cc create mode 100644 test/cfg.cc create mode 100644 test/data/test.jpeg create mode 100644 test/data/test.png create mode 100644 test/data/test.svg create mode 100644 test/data/test.xpm create mode 100644 test/image_processor.cc create mode 100644 test/io.cc create mode 100644 test/io_test_helper.cc create mode 100644 test/io_test_helper.hh create mode 100644 test/line.cc create mode 100644 test/paths.cc create mode 100644 test/str.cc create mode 100644 test/testdir.cc create mode 100644 test/testdir.hh create mode 100644 test/testenv.cc create mode 100644 test/testenv.hh create mode 100644 test/u8.cc (limited to 'test') 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 +#include +#include +#include +#include +#include +#include +#include + +#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(str_.size()); }; + [[nodiscard]] char** v() const { return ptr_.get(); } + + explicit Arguments(std::vector str) : str_(std::move(str)) { + ptr_ = std::make_unique(str_.size()); + for (size_t i = 0; i < str_.size(); ++i) { + ptr_[i] = const_cast(str_[i].c_str()); + } + } + + Arguments(Arguments const&) = delete; + Arguments& operator=(Arguments const&) = delete; + + private: + std::unique_ptr ptr_; + std::vector 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 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 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 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 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 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 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 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 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 +#include +#include +#include + +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 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 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 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 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 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 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 Binary files /dev/null and b/test/data/test.jpeg differ diff --git a/test/data/test.png b/test/data/test.png new file mode 100644 index 0000000..6c4877b Binary files /dev/null and b/test/data/test.png 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 @@ + + + + + + + + + + + + + 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#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(a1) - static_cast(e1)) <= kLossyError && + abs(static_cast(a2) - static_cast(e2)) <= kLossyError && + abs(static_cast(a3) - static_cast(e3)) <= kLossyError && + abs(static_cast(a4) - static_cast(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_; + std::unique_ptr process_; +}; + +enum class AlphaMode : uint8_t { + kFull, + kMono, + kNone, +}; + +class ImageProcessorTestWithFormat : public ImageProcessorTest, public testing::WithParamInterface { + 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( + 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( + 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 +#include +#include +#include +#include +#include +#include + +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 +#include +#include +#include +#include + +namespace { + +class BreakingReader : public io::Reader { + public: + BreakingReader(std::unique_ptr reader, size_t offset, + io::ReadError error) + : reader_(std::move(reader)), offset_(offset), error_(error) {} + + [[nodiscard]] + std::expected 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 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 reader_; + size_t offset_; + io::ReadError const error_; +}; + +class BreakingWriter : public io::Writer { + public: + BreakingWriter(std::unique_ptr writer, size_t offset, + io::WriteError error) + : writer_(std::move(writer)), offset_(offset), error_(error) {} + + [[nodiscard]] + std::expected 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 close() override { + return writer_->close(); + } + + [[nodiscard]] + int raw_fd() const override { + return writer_->raw_fd(); + } + + private: + std::unique_ptr writer_; + size_t offset_; + io::WriteError const error_; +}; + +class MaxBlockReader : public io::Reader { + public: + MaxBlockReader(std::unique_ptr reader, size_t max_block_size) + : reader_(std::move(reader)), max_block_size_(max_block_size) {} + + [[nodiscard]] + std::expected read(void* dst, size_t max) override { + size_t avail = std::min(max_block_size_, max); + return reader_->read(dst, avail); + } + + [[nodiscard]] + std::expected 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 reader_; + size_t const max_block_size_; +}; + +class MaxBlockWriter : public io::Writer { + public: + MaxBlockWriter(std::unique_ptr writer, size_t max_block_size) + : writer_(std::move(writer)), max_block_size_(max_block_size) {} + + [[nodiscard]] + std::expected 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 close() override { + return writer_->close(); + } + + [[nodiscard]] + int raw_fd() const override { + return writer_->raw_fd(); + } + + private: + std::unique_ptr writer_; + size_t const max_block_size_; +}; + +} // namespace + +std::unique_ptr io_make_breaking(std::unique_ptr reader, + size_t offset, + io::ReadError error) { + return std::make_unique(std::move(reader), offset, error); +} + +std::unique_ptr io_make_breaking(std::unique_ptr writer, + size_t offset, + io::WriteError error) { + return std::make_unique(std::move(writer), offset, error); +} + +std::unique_ptr io_make_max_block( + std::unique_ptr reader, size_t max_block_size) { + return std::make_unique(std::move(reader), max_block_size); +} + +std::unique_ptr io_make_max_block( + std::unique_ptr writer, size_t max_block_size) { + return std::make_unique(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 +#include + +[[nodiscard]] +std::unique_ptr io_make_breaking( + std::unique_ptr reader, size_t offset = 0, + io::ReadError error = io::ReadError::Error); + +[[nodiscard]] +std::unique_ptr io_make_breaking( + std::unique_ptr writer, size_t offset = 0, + io::WriteError error = io::WriteError::Error); + +[[nodiscard]] +std::unique_ptr io_make_max_block( + std::unique_ptr reader, size_t max_block_size); + +[[nodiscard]] +std::unique_ptr io_make_max_block( + std::unique_ptr 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 +#include +#include +#include + +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::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 + +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 + +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 +#include +#include +#include +#include + +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 // 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 +#include +#include + +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 +#include +#include +#include + +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> 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 +#include +#include + +TEST(u8, empty) { + std::vector 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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())); + } +} -- cgit v1.2.3-70-g09d2