refactor: factor out helpers to simplify/split tool_main_test.cpp

This commit is contained in:
Marcus Holland-Moritz 2025-08-28 10:37:44 +02:00
parent a8d8e0ad84
commit b64d186a26
4 changed files with 597 additions and 444 deletions

View File

@ -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)

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#include <dwarfs/util.h>
#include <dwarfs_tool_main.h>
#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<test::test_iolayer>();
}
void tool_main_test::TearDown() { iol.reset(); }
tester_common::tester_common(main_ptr_t mp, std::string toolname,
std::shared_ptr<test::os_access_mock> pos)
: fa{std::make_shared<test::test_file_access>()}
, os{std::move(pos)}
, iol{std::make_unique<test::test_iolayer>(os, fa)}
, main_{mp}
, toolname_{std::move(toolname)} {
setup_locale();
}
int tester_common::run(std::vector<std::string> args) {
args.insert(args.begin(), toolname_);
return tool::main_adapter(main_)(args, iol->get());
}
int tester_common::run(std::initializer_list<std::string> args) {
return run(std::vector<std::string>(args));
}
int tester_common::run(std::string args) { return run(test::parse_args(args)); }
mkdwarfs_tester::mkdwarfs_tester(std::shared_ptr<test::os_access_mock> 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<test::os_access_mock>());
}
void mkdwarfs_tester::add_stream_logger(std::ostream& st,
logger::level_type level) {
lgr = std::make_unique<stream_logger>(std::make_shared<test::test_terminal>(),
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<std::pair<fs::path, std::string>>
mkdwarfs_tester::add_random_file_tree(random_file_tree_options const& opt) {
size_t max_size{128 * static_cast<size_t>(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<std::pair<fs::path, std::string>> 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_t>(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<test::test_logger>();
}
auto mm = std::make_shared<test::mmap_mock>(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<test::os_access_mock> pos)
: tester_common(tool::dwarfsck_main, "dwarfsck", std::move(pos)) {}
dwarfsck_tester::dwarfsck_tester()
: dwarfsck_tester(std::make_shared<test::os_access_mock>()) {}
dwarfsck_tester dwarfsck_tester::create_with_image(std::string image) {
auto os = std::make_shared<test::os_access_mock>();
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<test::os_access_mock> pos)
: tester_common(tool::dwarfsextract_main, "dwarfsextract", std::move(pos)) {
}
dwarfsextract_tester::dwarfsextract_tester()
: dwarfsextract_tester(std::make_shared<test::os_access_mock>()) {}
dwarfsextract_tester
dwarfsextract_tester::create_with_image(std::string image) {
auto os = std::make_shared<test::os_access_mock>();
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<std::string> args) {
args.insert(args.begin(), "mkdwarfs");
return tool::main_adapter(tool::mkdwarfs_main)(args, iol->get());
}
int dwarfsck_main_test::run(std::vector<std::string> args) {
args.insert(args.begin(), "dwarfsck");
return tool::main_adapter(tool::dwarfsck_main)(args, iol->get());
}
int dwarfsextract_main_test::run(std::vector<std::string> args) {
args.insert(args.begin(), "dwarfsextract");
return tool::main_adapter(tool::dwarfsextract_main)(args, iol->get());
}
std::string build_test_image(std::vector<std::string> extra_args,
std::map<std::string, std::string> extra_files) {
mkdwarfs_tester t;
for (auto const& [name, contents] : extra_files) {
t.fa->set_file(name, contents);
}
std::vector<std::string> 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<std::optional<reader::filesystem_v2>, mkdwarfs_tester>
build_with_args(std::vector<std::string> opt_args) {
std::string const image_file = "test.dwarfs";
mkdwarfs_tester t;
std::vector<std::string> 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<uint64_t> get_all_fs_times(reader::filesystem_v2 const& fs) {
std::set<uint64_t> 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<uint64_t> get_all_fs_uids(reader::filesystem_v2 const& fs) {
std::set<uint64_t> uids;
fs.walk([&](auto const& e) {
auto st = fs.getattr(e.inode());
uids.insert(st.uid());
});
return uids;
}
std::set<uint64_t> get_all_fs_gids(reader::filesystem_v2 const& fs) {
std::set<uint64_t> gids;
fs.walk([&](auto const& e) {
auto st = fs.getattr(e.inode());
gids.insert(st.gid());
});
return gids;
}
} // namespace dwarfs::test

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#include <array>
#include <filesystem>
#include <initializer_list>
#include <iosfwd>
#include <map>
#include <memory>
#include <optional>
#include <random>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <gtest/gtest.h>
#include <dwarfs/config.h>
#include <dwarfs/logger.h>
#include <dwarfs/reader/filesystem_options.h>
#include <dwarfs/reader/filesystem_v2.h>
#include <dwarfs/tool/main_adapter.h>
#include "loremipsum.h"
#include "lz_synthetic_generator.h"
#include "mmap_mock.h"
#include "test_helpers.h"
#include "test_logger.h"
namespace dwarfs::test {
// TODO: this is a workaround for older Clang versions
struct fs_path_hash {
auto operator()(std::filesystem::path const& p) const noexcept {
return std::filesystem::hash_value(p);
}
};
extern std::filesystem::path const test_dir;
extern std::filesystem::path const audio_data_dir;
extern std::filesystem::path const fits_data_dir;
constexpr std::array<std::string_view, 6> const log_level_strings{
"error", "warn", "info", "verbose", "debug", "trace"};
enum class input_mode {
from_file,
from_stdin,
};
constexpr std::array input_modes{
input_mode::from_file,
input_mode::from_stdin,
};
std::ostream& operator<<(std::ostream& os, input_mode m);
enum class path_type {
relative,
absolute,
mixed,
};
constexpr std::array path_types{path_type::relative, path_type::absolute,
path_type::mixed};
std::ostream& operator<<(std::ostream& os, path_type m);
class tester_common {
public:
using main_ptr_t = tool::main_adapter::main_fn_type;
tester_common(main_ptr_t mp, std::string toolname,
std::shared_ptr<test::os_access_mock> pos);
int run(std::vector<std::string> args);
int run(std::initializer_list<std::string> args);
int run(std::string args);
std::string out() const { return iol->out(); }
std::string err() const { return iol->err(); }
std::shared_ptr<test::test_file_access> fa;
std::shared_ptr<test::os_access_mock> os;
std::unique_ptr<test::test_iolayer> iol;
private:
main_ptr_t main_;
std::string toolname_;
};
struct random_file_tree_options {
double avg_size{4096.0};
int dimension{20};
int max_name_len{50};
bool with_errors{false};
bool with_invalid_utf8{false};
};
constexpr auto default_fs_opts = reader::filesystem_options{
.block_cache = {.max_bytes = 256 * 1024,
.sequential_access_detector_threshold = 4},
.metadata = {.check_consistency = true}};
class mkdwarfs_tester : public tester_common {
public:
mkdwarfs_tester();
explicit mkdwarfs_tester(std::shared_ptr<test::os_access_mock> pos);
static mkdwarfs_tester create_empty();
void add_stream_logger(std::ostream& st,
logger::level_type level = logger::VERBOSE);
void add_root_dir();
void add_special_files();
void add_test_file_tree();
std::vector<std::pair<std::filesystem::path, std::string>>
add_random_file_tree(
random_file_tree_options const& opt = random_file_tree_options{});
reader::filesystem_v2
fs_from_data(std::string data,
reader::filesystem_options const& opt = default_fs_opts);
reader::filesystem_v2
fs_from_file(std::string path,
reader::filesystem_options const& opt = default_fs_opts);
reader::filesystem_v2
fs_from_stdout(reader::filesystem_options const& opt = default_fs_opts);
std::unique_ptr<logger> lgr;
};
std::string
build_test_image(std::vector<std::string> extra_args = {},
std::map<std::string, std::string> extra_files = {});
class dwarfsck_tester : public tester_common {
public:
dwarfsck_tester();
explicit dwarfsck_tester(std::shared_ptr<test::os_access_mock> pos);
static dwarfsck_tester create_with_image();
static dwarfsck_tester create_with_image(std::string image);
};
class dwarfsextract_tester : public tester_common {
public:
dwarfsextract_tester();
explicit dwarfsextract_tester(std::shared_ptr<test::os_access_mock> pos);
static dwarfsextract_tester create_with_image();
static dwarfsextract_tester create_with_image(std::string image);
};
std::tuple<std::optional<reader::filesystem_v2>, mkdwarfs_tester>
build_with_args(std::vector<std::string> opt_args = {});
std::set<uint64_t> get_all_fs_times(reader::filesystem_v2 const& fs);
std::set<uint64_t> get_all_fs_uids(reader::filesystem_v2 const& fs);
std::set<uint64_t> get_all_fs_gids(reader::filesystem_v2 const& fs);
class tool_main_test : public testing::Test {
public:
void SetUp() override;
void TearDown() override;
std::string out() const { return iol->out(); }
std::string err() const { return iol->err(); }
std::unique_ptr<test::test_iolayer> iol;
};
class mkdwarfs_main_test : public tool_main_test {
public:
int run(std::vector<std::string> args);
};
class dwarfsck_main_test : public tool_main_test {
public:
int run(std::vector<std::string> args);
};
class dwarfsextract_main_test : public tool_main_test {
public:
int run(std::vector<std::string> args);
};
} // namespace dwarfs::test

View File

@ -22,16 +22,10 @@
*/
#include <algorithm>
#include <array>
#include <filesystem>
#include <iostream>
#include <random>
#include <regex>
#include <set>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <archive.h>
#include <archive_entry.h>
@ -51,451 +45,22 @@
#include <range/v3/view/join.hpp>
#include <range/v3/view/map.hpp>
#include <dwarfs/config.h>
#include <dwarfs/file_util.h>
#include <dwarfs/history.h>
#include <dwarfs/logger.h>
#include <dwarfs/reader/filesystem_options.h>
#include <dwarfs/reader/filesystem_v2.h>
#include <dwarfs/reader/fsinfo_options.h>
#include <dwarfs/reader/iovec_read_buf.h>
#include <dwarfs/sorted_array_map.h>
#include <dwarfs/string.h>
#include <dwarfs/tool/main_adapter.h>
#include <dwarfs/util.h>
#include <dwarfs_tool_main.h>
#include "filter_test_data.h"
#include "loremipsum.h"
#include "lz_synthetic_generator.h"
#include "mmap_mock.h"
#include "test_helpers.h"
#include "test_logger.h"
#include "test_tool_main_tester.h"
using namespace dwarfs;
using namespace dwarfs::test;
namespace fs = std::filesystem;
using tool::iolayer;
namespace {
// TODO: this is a workaround for older Clang versions
struct fs_path_hash {
auto operator()(std::filesystem::path const& p) const noexcept {
return std::filesystem::hash_value(p);
}
};
auto test_dir = fs::path(TEST_DATA_DIR).make_preferred();
auto audio_data_dir = test_dir / "pcmaudio";
auto fits_data_dir = test_dir / "fits";
constexpr std::array<std::string_view, 6> const log_level_strings{
"error", "warn", "info", "verbose", "debug", "trace"};
enum class input_mode {
from_file,
from_stdin,
};
constexpr std::array input_modes{
input_mode::from_file,
input_mode::from_stdin,
};
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;
}
enum class path_type {
relative,
absolute,
mixed,
};
constexpr std::array path_types{path_type::relative, path_type::absolute,
path_type::mixed};
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;
}
struct locale_setup_helper {
locale_setup_helper() { setup_default_locale(); }
};
void setup_locale() { static locale_setup_helper helper; }
class tool_main_test : public testing::Test {
public:
void SetUp() override {
setup_locale();
iol = std::make_unique<test::test_iolayer>();
}
void TearDown() override { iol.reset(); }
std::string out() const { return iol->out(); }
std::string err() const { return iol->err(); }
std::unique_ptr<test::test_iolayer> iol;
};
class tester_common {
public:
using main_ptr_t = tool::main_adapter::main_fn_type;
tester_common(main_ptr_t mp, std::string toolname,
std::shared_ptr<test::os_access_mock> pos)
: fa{std::make_shared<test::test_file_access>()}
, os{std::move(pos)}
, iol{std::make_unique<test::test_iolayer>(os, fa)}
, main_{mp}
, toolname_{std::move(toolname)} {
setup_locale();
}
int run(std::vector<std::string> args) {
args.insert(args.begin(), toolname_);
return tool::main_adapter(main_)(args, iol->get());
}
int run(std::initializer_list<std::string> args) {
return run(std::vector<std::string>(args));
}
int run(std::string args) { return run(test::parse_args(args)); }
std::string out() const { return iol->out(); }
std::string err() const { return iol->err(); }
std::shared_ptr<test::test_file_access> fa;
std::shared_ptr<test::os_access_mock> os;
std::unique_ptr<test::test_iolayer> iol;
private:
main_ptr_t main_;
std::string toolname_;
};
struct random_file_tree_options {
double avg_size{4096.0};
int dimension{20};
int max_name_len{50};
bool with_errors{false};
bool with_invalid_utf8{false};
};
constexpr auto default_fs_opts = reader::filesystem_options{
.block_cache = {.max_bytes = 256 * 1024,
.sequential_access_detector_threshold = 4},
.metadata = {.check_consistency = true}};
class mkdwarfs_tester : public tester_common {
public:
mkdwarfs_tester(std::shared_ptr<test::os_access_mock> pos)
: tester_common(tool::mkdwarfs_main, "mkdwarfs", std::move(pos)) {}
mkdwarfs_tester()
: mkdwarfs_tester(test::os_access_mock::create_test_instance()) {}
static mkdwarfs_tester create_empty() {
return mkdwarfs_tester(std::make_shared<test::os_access_mock>());
}
void add_stream_logger(std::ostream& st,
logger::level_type level = logger::VERBOSE) {
lgr =
std::make_unique<stream_logger>(std::make_shared<test::test_terminal>(),
st, logger_options{.threshold = level});
}
void add_root_dir() { os->add("", {1, 040755, 1, 0, 0, 10, 42, 0, 0, 0}); }
void 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<std::pair<fs::path, std::string>> add_random_file_tree(
random_file_tree_options const& opt = random_file_tree_options{}) {
size_t max_size{128 * static_cast<size_t>(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<std::pair<fs::path, std::string>> 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_t>(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 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
fs_from_data(std::string data,
reader::filesystem_options const& opt = default_fs_opts) {
if (!lgr) {
lgr = std::make_unique<test::test_logger>();
}
auto mm = std::make_shared<test::mmap_mock>(std::move(data));
return reader::filesystem_v2(*lgr, *os, mm, opt);
}
reader::filesystem_v2
fs_from_file(std::string path,
reader::filesystem_options const& opt = default_fs_opts) {
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
fs_from_stdout(reader::filesystem_options const& opt = default_fs_opts) {
return fs_from_data(out(), opt);
}
std::unique_ptr<logger> lgr;
};
std::string
build_test_image(std::vector<std::string> extra_args = {},
std::map<std::string, std::string> extra_files = {}) {
mkdwarfs_tester t;
for (auto const& [name, contents] : extra_files) {
t.fa->set_file(name, contents);
}
std::vector<std::string> 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();
}
class dwarfsck_tester : public tester_common {
public:
dwarfsck_tester(std::shared_ptr<test::os_access_mock> pos)
: tester_common(tool::dwarfsck_main, "dwarfsck", std::move(pos)) {}
dwarfsck_tester()
: dwarfsck_tester(std::make_shared<test::os_access_mock>()) {}
static dwarfsck_tester create_with_image(std::string image) {
auto os = std::make_shared<test::os_access_mock>();
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));
}
static dwarfsck_tester create_with_image() {
return create_with_image(build_test_image());
}
};
class dwarfsextract_tester : public tester_common {
public:
dwarfsextract_tester(std::shared_ptr<test::os_access_mock> pos)
: tester_common(tool::dwarfsextract_main, "dwarfsextract",
std::move(pos)) {}
dwarfsextract_tester()
: dwarfsextract_tester(std::make_shared<test::os_access_mock>()) {}
static dwarfsextract_tester create_with_image(std::string image) {
auto os = std::make_shared<test::os_access_mock>();
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));
}
static dwarfsextract_tester create_with_image() {
return create_with_image(build_test_image());
}
};
std::tuple<std::optional<reader::filesystem_v2>, mkdwarfs_tester>
build_with_args(std::vector<std::string> opt_args = {}) {
std::string const image_file = "test.dwarfs";
mkdwarfs_tester t;
std::vector<std::string> 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<uint64_t> get_all_fs_times(reader::filesystem_v2 const& fs) {
std::set<uint64_t> 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<uint64_t> get_all_fs_uids(reader::filesystem_v2 const& fs) {
std::set<uint64_t> uids;
fs.walk([&](auto const& e) {
auto st = fs.getattr(e.inode());
uids.insert(st.uid());
});
return uids;
}
std::set<uint64_t> get_all_fs_gids(reader::filesystem_v2 const& fs) {
std::set<uint64_t> gids;
fs.walk([&](auto const& e) {
auto st = fs.getattr(e.inode());
gids.insert(st.gid());
});
return gids;
}
} // namespace
class mkdwarfs_main_test : public tool_main_test {
public:
int run(std::vector<std::string> args) {
args.insert(args.begin(), "mkdwarfs");
return tool::main_adapter(tool::mkdwarfs_main)(args, iol->get());
}
};
class dwarfsck_main_test : public tool_main_test {
public:
int run(std::vector<std::string> args) {
args.insert(args.begin(), "dwarfsck");
return tool::main_adapter(tool::dwarfsck_main)(args, iol->get());
}
};
class dwarfsextract_main_test : public tool_main_test {
public:
int run(std::vector<std::string> args) {
args.insert(args.begin(), "dwarfsextract");
return tool::main_adapter(tool::dwarfsextract_main)(args, iol->get());
}
};
TEST_F(mkdwarfs_main_test, no_cmdline_args) {
auto exit_code = run({});
EXPECT_EQ(exit_code, 0);