diff --git a/CMakeLists.txt b/CMakeLists.txt
index 80ab8d33..ae9f804c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -465,7 +465,7 @@ endif()
add_custom_target(symlinks ALL DEPENDS ${SYMLINKS})
if(WITH_TESTS OR WITH_BENCHMARKS OR WITH_FUZZ)
- add_library(dwarfs_test_helpers OBJECT
+ add_library(dwarfs_test_helpers
test/test_helpers.cpp
test/test_iolayer.cpp
test/loremipsum.cpp
@@ -477,6 +477,16 @@ if(WITH_TESTS OR WITH_BENCHMARKS OR WITH_FUZZ)
endif()
target_link_libraries(dwarfs_test_helpers PUBLIC dwarfs_reader dwarfs_writer dwarfs_tool)
set_property(TARGET dwarfs_test_helpers PROPERTY CXX_STANDARD ${DWARFS_CXX_STANDARD})
+ target_compile_definitions(dwarfs_test_helpers
+ PUBLIC TEST_DATA_DIR=\"${CMAKE_SOURCE_DIR}/test\"
+ TOOLS_BIN_DIR=\"${CMAKE_CURRENT_BINARY_DIR}\")
+endif()
+
+if(WITH_TESTS AND WITH_TOOLS)
+ add_library(dwarfs_tool_main_tester test/test_tool_main_tester.cpp)
+ target_link_libraries(dwarfs_tool_main_tester PUBLIC dwarfs_test_helpers GTest::gtest)
+ target_link_libraries(dwarfs_tool_main_tester PRIVATE mkdwarfs_main dwarfsck_main dwarfsextract_main)
+ set_property(TARGET dwarfs_tool_main_tester PROPERTY CXX_STANDARD ${DWARFS_CXX_STANDARD})
endif()
if(WITH_TESTS)
@@ -572,7 +582,7 @@ if(WITH_TESTS)
endif()
add_library(dwarfs_test_main OBJECT test/test_main.cpp)
- target_link_libraries(dwarfs_test_main PUBLIC gtest gmock)
+ target_link_libraries(dwarfs_test_main PUBLIC GTest::gtest GTest::gmock)
foreach (test ${DWARFS_TESTS})
if(NOT TARGET ${test})
@@ -598,10 +608,6 @@ if(WITH_TESTS)
target_include_directories(${test} SYSTEM BEFORE PRIVATE ${gmock_include_dirs} ${gtest_include_dirs})
endif()
- target_compile_definitions(${test}
- PRIVATE TEST_DATA_DIR=\"${CMAKE_SOURCE_DIR}/test\"
- TOOLS_BIN_DIR=\"${CMAKE_CURRENT_BINARY_DIR}\")
-
if(WIN32)
target_compile_options(${test} PRIVATE /bigobj)
endif()
@@ -610,7 +616,7 @@ if(WITH_TESTS)
endforeach()
if(TARGET tool_main_test)
- target_link_libraries(tool_main_test PRIVATE mkdwarfs_main dwarfsck_main dwarfsextract_main PkgConfig::LIBARCHIVE)
+ target_link_libraries(tool_main_test PRIVATE dwarfs_tool_main_tester PkgConfig::LIBARCHIVE)
endif()
if(TARGET dwarfs_unit_tests)
diff --git a/test/test_tool_main_tester.cpp b/test/test_tool_main_tester.cpp
new file mode 100644
index 00000000..d387e779
--- /dev/null
+++ b/test/test_tool_main_tester.cpp
@@ -0,0 +1,368 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/**
+ * \author Marcus Holland-Moritz (github@mhxnet.de)
+ * \copyright Copyright (c) Marcus Holland-Moritz
+ *
+ * This file is part of dwarfs.
+ *
+ * dwarfs is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dwarfs is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dwarfs. If not, see .
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include
+
+#include
+
+#include "test_tool_main_tester.h"
+
+namespace dwarfs::test {
+
+namespace fs = std::filesystem;
+
+namespace {
+
+struct locale_setup_helper {
+ locale_setup_helper() { setup_default_locale(); }
+};
+
+inline void setup_locale() { static locale_setup_helper helper; }
+
+} // namespace
+
+fs::path const test_dir = fs::path(TEST_DATA_DIR).make_preferred();
+fs::path const audio_data_dir = test_dir / "pcmaudio";
+fs::path const fits_data_dir = test_dir / "fits";
+
+std::ostream& operator<<(std::ostream& os, input_mode m) {
+ switch (m) {
+ case input_mode::from_file:
+ os << "from_file";
+ break;
+ case input_mode::from_stdin:
+ os << "from_stdin";
+ break;
+ }
+ return os;
+}
+
+std::ostream& operator<<(std::ostream& os, path_type m) {
+ switch (m) {
+ case path_type::relative:
+ os << "relative";
+ break;
+ case path_type::absolute:
+ os << "absolute";
+ break;
+ case path_type::mixed:
+ os << "mixed";
+ break;
+ }
+ return os;
+}
+
+void tool_main_test::SetUp() {
+ setup_locale();
+ iol = std::make_unique();
+}
+
+void tool_main_test::TearDown() { iol.reset(); }
+
+tester_common::tester_common(main_ptr_t mp, std::string toolname,
+ std::shared_ptr pos)
+ : fa{std::make_shared()}
+ , os{std::move(pos)}
+ , iol{std::make_unique(os, fa)}
+ , main_{mp}
+ , toolname_{std::move(toolname)} {
+ setup_locale();
+}
+
+int tester_common::run(std::vector args) {
+ args.insert(args.begin(), toolname_);
+ return tool::main_adapter(main_)(args, iol->get());
+}
+
+int tester_common::run(std::initializer_list args) {
+ return run(std::vector(args));
+}
+
+int tester_common::run(std::string args) { return run(test::parse_args(args)); }
+
+mkdwarfs_tester::mkdwarfs_tester(std::shared_ptr pos)
+ : tester_common(tool::mkdwarfs_main, "mkdwarfs", std::move(pos)) {}
+
+mkdwarfs_tester::mkdwarfs_tester()
+ : mkdwarfs_tester(test::os_access_mock::create_test_instance()) {}
+
+mkdwarfs_tester mkdwarfs_tester::create_empty() {
+ return mkdwarfs_tester(std::make_shared());
+}
+
+void mkdwarfs_tester::add_stream_logger(std::ostream& st,
+ logger::level_type level) {
+ lgr = std::make_unique(std::make_shared(),
+ st, logger_options{.threshold = level});
+}
+
+void mkdwarfs_tester::add_root_dir() {
+ os->add("", {1, 040755, 1, 0, 0, 10, 42, 0, 0, 0});
+}
+
+void mkdwarfs_tester::add_special_files() {
+ static constexpr file_stat::off_type const size = 10;
+ std::string data(size, 'x');
+ os->add("suid", {1001, 0104755, 1, 0, 0, size, 0, 3333, 2222, 1111}, data);
+ os->add("sgid", {1002, 0102755, 1, 0, 0, size, 0, 0, 0, 0}, data);
+ os->add("sticky", {1003, 0101755, 1, 0, 0, size, 0, 0, 0, 0}, data);
+ os->add("block", {1004, 060666, 1, 0, 0, 0, 77, 0, 0, 0}, std::string{});
+ os->add("sock", {1005, 0140666, 1, 0, 0, 0, 0, 0, 0, 0}, std::string{});
+}
+
+std::vector>
+mkdwarfs_tester::add_random_file_tree(random_file_tree_options const& opt) {
+ size_t max_size{128 * static_cast(opt.avg_size)};
+ std::mt19937_64 rng{42};
+ std::exponential_distribution<> size_dist{1 / opt.avg_size};
+ std::uniform_int_distribution<> path_comp_size_dist{0, opt.max_name_len};
+ std::uniform_int_distribution<> invalid_dist{0, 1};
+ std::vector> paths;
+
+ auto random_path_component = [&] {
+ auto size = path_comp_size_dist(rng);
+ if (opt.with_invalid_utf8 && invalid_dist(rng) == 0) {
+ return test::create_random_string(size, 96, 255, rng);
+ }
+ return test::create_random_string(size, 'A', 'Z', rng);
+ };
+
+ test::lz_params text_lzp{};
+ test::lz_params binary_lzp{};
+ text_lzp.text_mode = true;
+ binary_lzp.text_mode = false;
+ text_lzp.seed = rng();
+ binary_lzp.seed = rng();
+ test::lz_synthetic_generator text_gen{text_lzp};
+ test::lz_synthetic_generator binary_gen{binary_lzp};
+
+ for (int x = 0; x < opt.dimension; ++x) {
+ fs::path d1{random_path_component() + std::to_string(x)};
+ os->add_dir(d1);
+
+ for (int y = 0; y < opt.dimension; ++y) {
+ fs::path d2{d1 / (random_path_component() + std::to_string(y))};
+ os->add_dir(d2);
+
+ for (int z = 0; z < opt.dimension; ++z) {
+ fs::path f{d2 / (random_path_component() + std::to_string(z))};
+ auto const size =
+ std::min(max_size, static_cast(size_dist(rng)));
+ std::string data;
+
+ auto const choice = rng() % 4;
+ switch (choice) {
+ case 0:
+ data = test::create_random_string(size, rng);
+ break;
+ case 1:
+ data = test::loremipsum(size);
+ break;
+ case 3:
+ data = text_gen.generate(size);
+ break;
+ case 4:
+ data = binary_gen.generate(size);
+ break;
+ }
+
+ os->add_file(f, data);
+ paths.emplace_back(f, data);
+
+ if (opt.with_errors) {
+ auto failpath = fs::path{"/"} / f;
+ switch (rng() % 8) {
+ case 0:
+ os->set_access_fail(failpath);
+ [[fallthrough]];
+ case 1:
+ case 2:
+ os->set_map_file_error(
+ failpath,
+ std::make_exception_ptr(std::runtime_error("map_file_error")),
+ rng() % 4);
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return paths;
+}
+
+void mkdwarfs_tester::add_test_file_tree() {
+ for (auto const& [stat, name] : test::test_dirtree()) {
+ auto path = name.substr(name.size() == 5 ? 5 : 6);
+
+ switch (stat.type()) {
+ case posix_file_type::regular:
+ os->add(path, stat,
+ [size = stat.size] { return test::loremipsum(size); });
+ break;
+ case posix_file_type::symlink:
+ os->add(path, stat, test::loremipsum(stat.size));
+ break;
+ default:
+ os->add(path, stat);
+ break;
+ }
+ }
+}
+
+reader::filesystem_v2
+mkdwarfs_tester::fs_from_data(std::string data,
+ reader::filesystem_options const& opt) {
+ if (!lgr) {
+ lgr = std::make_unique();
+ }
+ auto mm = std::make_shared(std::move(data));
+ return reader::filesystem_v2(*lgr, *os, mm, opt);
+}
+
+reader::filesystem_v2
+mkdwarfs_tester::fs_from_file(std::string path,
+ reader::filesystem_options const& opt) {
+ auto fsimage = fa->get_file(path);
+ if (!fsimage) {
+ throw std::runtime_error("file not found: " + path);
+ }
+ return fs_from_data(std::move(fsimage.value()), opt);
+}
+
+reader::filesystem_v2
+mkdwarfs_tester::fs_from_stdout(reader::filesystem_options const& opt) {
+ return fs_from_data(out(), opt);
+}
+
+dwarfsck_tester::dwarfsck_tester(std::shared_ptr pos)
+ : tester_common(tool::dwarfsck_main, "dwarfsck", std::move(pos)) {}
+
+dwarfsck_tester::dwarfsck_tester()
+ : dwarfsck_tester(std::make_shared()) {}
+
+dwarfsck_tester dwarfsck_tester::create_with_image(std::string image) {
+ auto os = std::make_shared();
+ os->add("", {1, 040755, 1, 0, 0, 10, 42, 0, 0, 0});
+ os->add_file("image.dwarfs", std::move(image));
+ return dwarfsck_tester(std::move(os));
+}
+
+dwarfsck_tester dwarfsck_tester::create_with_image() {
+ return create_with_image(build_test_image());
+}
+
+dwarfsextract_tester::dwarfsextract_tester(
+ std::shared_ptr pos)
+ : tester_common(tool::dwarfsextract_main, "dwarfsextract", std::move(pos)) {
+}
+
+dwarfsextract_tester::dwarfsextract_tester()
+ : dwarfsextract_tester(std::make_shared()) {}
+
+dwarfsextract_tester
+dwarfsextract_tester::create_with_image(std::string image) {
+ auto os = std::make_shared();
+ os->add("", {1, 040755, 1, 0, 0, 10, 42, 0, 0, 0});
+ os->add_file("image.dwarfs", std::move(image));
+ return dwarfsextract_tester(std::move(os));
+}
+
+dwarfsextract_tester dwarfsextract_tester::create_with_image() {
+ return create_with_image(build_test_image());
+}
+
+int mkdwarfs_main_test::run(std::vector args) {
+ args.insert(args.begin(), "mkdwarfs");
+ return tool::main_adapter(tool::mkdwarfs_main)(args, iol->get());
+}
+
+int dwarfsck_main_test::run(std::vector args) {
+ args.insert(args.begin(), "dwarfsck");
+ return tool::main_adapter(tool::dwarfsck_main)(args, iol->get());
+}
+
+int dwarfsextract_main_test::run(std::vector args) {
+ args.insert(args.begin(), "dwarfsextract");
+ return tool::main_adapter(tool::dwarfsextract_main)(args, iol->get());
+}
+
+std::string build_test_image(std::vector extra_args,
+ std::map extra_files) {
+ mkdwarfs_tester t;
+ for (auto const& [name, contents] : extra_files) {
+ t.fa->set_file(name, contents);
+ }
+ std::vector args = {"-i", "/", "-o", "-"};
+ args.insert(args.end(), extra_args.begin(), extra_args.end());
+ if (t.run(args) != 0) {
+ throw std::runtime_error("failed to build test image:\n" + t.err());
+ }
+ return t.out();
+}
+
+std::tuple, mkdwarfs_tester>
+build_with_args(std::vector opt_args) {
+ std::string const image_file = "test.dwarfs";
+ mkdwarfs_tester t;
+ std::vector args = {"-i", "/", "-o", image_file};
+ args.insert(args.end(), opt_args.begin(), opt_args.end());
+ if (t.run(args) != 0) {
+ return {std::nullopt, std::move(t)};
+ }
+ return {t.fs_from_file(image_file), std::move(t)};
+}
+
+std::set get_all_fs_times(reader::filesystem_v2 const& fs) {
+ std::set times;
+ fs.walk([&](auto const& e) {
+ auto st = fs.getattr(e.inode());
+ times.insert(st.atime());
+ times.insert(st.ctime());
+ times.insert(st.mtime());
+ });
+ return times;
+}
+
+std::set get_all_fs_uids(reader::filesystem_v2 const& fs) {
+ std::set uids;
+ fs.walk([&](auto const& e) {
+ auto st = fs.getattr(e.inode());
+ uids.insert(st.uid());
+ });
+ return uids;
+}
+
+std::set get_all_fs_gids(reader::filesystem_v2 const& fs) {
+ std::set gids;
+ fs.walk([&](auto const& e) {
+ auto st = fs.getattr(e.inode());
+ gids.insert(st.gid());
+ });
+ return gids;
+}
+
+} // namespace dwarfs::test
diff --git a/test/test_tool_main_tester.h b/test/test_tool_main_tester.h
new file mode 100644
index 00000000..b9d865b6
--- /dev/null
+++ b/test/test_tool_main_tester.h
@@ -0,0 +1,214 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/**
+ * \author Marcus Holland-Moritz (github@mhxnet.de)
+ * \copyright Copyright (c) Marcus Holland-Moritz
+ *
+ * This file is part of dwarfs.
+ *
+ * dwarfs is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dwarfs is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dwarfs. If not, see .
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include
+#include
+#include
+#include
+#include