/* 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 .
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#ifndef _WIN32
#include
#include
#include
#ifdef __APPLE__
#include
#else
#include
#endif
#endif
#include
#include
#include
#if FMT_VERSION >= 110000
#include
#endif
#include
#include
#include
#include
#include
#include
#include
#include "test_helpers.h"
namespace {
namespace bp = boost::process;
namespace fs = std::filesystem;
auto test_dir = fs::path(TEST_DATA_DIR).make_preferred();
auto test_data_dwarfs = test_dir / "data.dwarfs";
auto test_catdata_dwarfs = test_dir / "catdata.dwarfs";
#ifdef _WIN32
#define EXE_EXT ".exe"
#else
#define EXE_EXT ""
#endif
#ifndef MKDWARFS_BINARY
#define MKDWARFS_BINARY tools_dir / "mkdwarfs" EXE_EXT
#endif
#ifndef DWARFSCK_BINARY
#define DWARFSCK_BINARY tools_dir / "dwarfsck" EXE_EXT
#endif
#ifndef DWARFSEXTRACT_BINARY
#define DWARFSEXTRACT_BINARY tools_dir / "dwarfsextract" EXE_EXT
#endif
auto tools_dir = fs::path(TOOLS_BIN_DIR).make_preferred();
auto mkdwarfs_bin = fs::path{MKDWARFS_BINARY};
auto fuse3_bin = tools_dir / "dwarfs" EXE_EXT;
auto fuse2_bin = tools_dir / "dwarfs2" EXE_EXT;
auto dwarfsextract_bin = fs::path{DWARFSEXTRACT_BINARY};
auto dwarfsck_bin = fs::path{DWARFSCK_BINARY};
auto universal_bin = tools_dir / "universal" / "dwarfs-universal" EXE_EXT;
class scoped_no_leak_check {
public:
#ifdef DWARFS_TEST_RUNNING_ON_ASAN
static constexpr auto const kEnvVar = "ASAN_OPTIONS";
static constexpr auto const kNoLeakCheck = "detect_leaks=0";
scoped_no_leak_check() {
std::string new_asan_options;
if (auto const* asan_options = ::getenv(kEnvVar)) {
old_asan_options_.emplace(asan_options);
new_asan_options = *old_asan_options_ + ":" + std::string(kNoLeakCheck);
} else {
new_asan_options.assign(kNoLeakCheck);
}
::setenv(kEnvVar, new_asan_options.c_str(), 1);
unset_asan_ = true;
}
~scoped_no_leak_check() {
if (unset_asan_) {
if (old_asan_options_) {
::setenv(kEnvVar, old_asan_options_->c_str(), 1);
} else {
::unsetenv(kEnvVar);
}
}
}
private:
std::optional old_asan_options_;
bool unset_asan_{false};
#else
// suppress unused variable warning
~scoped_no_leak_check() {}
#endif
};
#ifdef DWARFS_WITH_FUSE_DRIVER
bool skip_fuse_tests() {
return dwarfs::getenv_is_enabled("DWARFS_SKIP_FUSE_TESTS");
}
#if !(defined(_WIN32) || defined(__APPLE__))
pid_t get_dwarfs_pid(fs::path const& path) {
return dwarfs::to(dwarfs::getxattr(path, "user.dwarfs.driver.pid"));
}
#endif
bool wait_until_file_ready(fs::path const& path,
std::chrono::milliseconds timeout) {
auto end = std::chrono::steady_clock::now() + timeout;
std::error_code ec;
while (!fs::exists(path, ec)) {
#ifdef _WIN32
if (ec && ec.value() != ERROR_OPERATION_ABORTED) {
#else
if (ec) {
#endif
std::cerr << "*** exists: " << ec.message() << "\n";
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
if (std::chrono::steady_clock::now() >= end) {
return false;
}
}
return true;
}
#endif
bool read_file(fs::path const& path, std::string& out) {
std::ifstream ifs(path, std::ios::binary);
if (!ifs.is_open()) {
return false;
}
std::stringstream tmp;
tmp << ifs.rdbuf();
out = tmp.str();
return true;
}
struct compare_directories_result {
std::set mismatched;
std::set directories;
std::set symlinks;
std::set regular_files;
size_t total_regular_file_size{0};
};
std::ostream&
operator<<(std::ostream& os, compare_directories_result const& cdr) {
for (auto const& m : cdr.mismatched) {
os << "*** mismatched: " << m << "\n";
}
for (auto const& m : cdr.regular_files) {
os << "*** regular: " << m << "\n";
}
for (auto const& m : cdr.directories) {
os << "*** directory: " << m << "\n";
}
for (auto const& m : cdr.symlinks) {
os << "*** symlink: " << m << "\n";
}
return os;
}
template
void find_all(fs::path const& root, T const& func) {
std::deque q;
q.push_back(root);
while (!q.empty()) {
auto p = q.front();
q.pop_front();
for (auto const& e : fs::directory_iterator(p)) {
func(e);
if (e.symlink_status().type() == fs::file_type::directory) {
q.push_back(e.path());
}
}
}
}
bool compare_directories(fs::path const& p1, fs::path const& p2,
compare_directories_result* res = nullptr) {
std::map m1, m2;
std::set s1, s2;
find_all(p1, [&](auto const& e) {
auto rp = e.path().lexically_relative(p1);
m1.emplace(rp, e);
s1.insert(rp);
});
find_all(p2, [&](auto const& e) {
auto rp = e.path().lexically_relative(p2);
m2.emplace(rp, e);
s2.insert(rp);
});
if (res) {
res->mismatched.clear();
res->directories.clear();
res->symlinks.clear();
res->regular_files.clear();
res->total_regular_file_size = 0;
}
bool rv = true;
std::set common;
std::set_intersection(s1.begin(), s1.end(), s2.begin(), s2.end(),
std::inserter(common, common.end()));
if (s1.size() != common.size() || s2.size() != common.size()) {
if (res) {
std::set_symmetric_difference(
s1.begin(), s1.end(), s2.begin(), s2.end(),
std::inserter(res->mismatched, res->mismatched.end()));
}
rv = false;
}
for (auto const& p : common) {
auto const& e1 = m1[p];
auto const& e2 = m2[p];
if (e1.symlink_status().type() != e2.symlink_status().type() ||
(e1.symlink_status().type() != fs::file_type::directory &&
e1.file_size() != e2.file_size())) {
if (res) {
res->mismatched.insert(p);
}
rv = false;
continue;
}
switch (e1.symlink_status().type()) {
case fs::file_type::regular: {
std::string c1, c2;
if (!read_file(e1.path(), c1) || !read_file(e2.path(), c2) || c1 != c2) {
if (res) {
res->mismatched.insert(p);
}
rv = false;
}
}
if (res) {
res->regular_files.insert(p);
res->total_regular_file_size += e1.file_size();
}
break;
case fs::file_type::directory:
if (res) {
res->directories.insert(p);
}
break;
case fs::file_type::symlink:
if (fs::read_symlink(e1.path()) != fs::read_symlink(e2.path())) {
if (res) {
res->mismatched.insert(p);
}
rv = false;
}
if (res) {
res->symlinks.insert(p);
}
break;
default:
break;
}
}
return rv;
}
#ifdef _WIN32
struct new_process_group : public ::boost::process::detail::handler_base {
template
void on_setup(WindowsExecutor& e [[maybe_unused]]) const {
e.creation_flags |= CREATE_NEW_PROCESS_GROUP;
}
};
#endif
void ignore_sigpipe() {
#ifdef __APPLE__
static bool already_ignoring{false};
if (!already_ignoring) {
struct sigaction sa;
std::memset(&sa, 0, sizeof(sa));
sa.sa_handler = SIG_IGN;
int res = ::sigaction(SIGPIPE, &sa, NULL);
if (res != 0) {
std::cerr << "sigaction(SIGPIPE, SIG_IGN): " << std::strerror(errno)
<< "\n";
std::abort();
}
already_ignoring = true;
}
#endif
}
template
concept subprocess_arg = std::convertible_to ||
std::convertible_to> ||
std::convertible_to;
class subprocess {
public:
template
subprocess(std::filesystem::path const& prog, Args&&... args)
: subprocess(nullptr, prog, std::forward(args)...) {}
template
subprocess(boost::asio::io_service& ios, std::filesystem::path const& prog,
Args&&... args)
: subprocess(&ios, prog, std::forward(args)...) {}
~subprocess() {
if (pt_) {
std::cerr << "subprocess still running in destructor: " << cmdline()
<< "\n";
pt_->join();
}
}
std::string cmdline() const {
std::string cmd = prog_.string();
if (!cmdline_.empty()) {
cmd += fmt::format(" {}", fmt::join(cmdline_, " "));
}
return cmd;
}
void run() {
if (!ios_) {
throw std::runtime_error("processes with external io_service must be run "
"externally and then waited for");
}
ios_->run();
wait();
}
void wait() {
c_.wait();
outs_ = out_.get();
errs_ = err_.get();
}
void run_background() {
if (pt_) {
throw std::runtime_error("already running in background");
}
pt_ = std::make_unique([this] { run(); });
}
void wait_background() {
if (!pt_) {
throw std::runtime_error("no process running in background");
}
pt_->join();
pt_.reset();
}
void interrupt() {
std::cerr << "interrupting: " << cmdline() << "\n";
#ifdef _WIN32
::GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid());
#else
if (auto rv = ::kill(pid(), SIGINT); rv != 0) {
std::cerr << "kill(" << pid() << ", SIGINT) = " << rv << "\n";
}
#endif
}
std::string const& out() const { return outs_; }
std::string const& err() const { return errs_; }
bp::pid_t pid() const { return c_.id(); }
int exit_code() const { return c_.exit_code(); }
template
static std::tuple run(Args&&... args) {
auto p = subprocess(std::forward(args)...);
p.run();
return {p.out(), p.err(), p.exit_code()};
}
template
static std::optional check_run(Args&&... args) {
auto const [out, err, ec] = run(std::forward(args)...);
if (ec != 0) {
std::cerr << "stdout:\n" << out << "\nstderr:\n" << err << "\n";
return std::nullopt;
}
return out;
}
private:
template
subprocess(boost::asio::io_service* ios, std::filesystem::path const& prog,
Args&&... args)
: prog_{prog} {
(append_arg(cmdline_, std::forward(args)), ...);
ignore_sigpipe();
if (!ios) {
ios_ = std::make_unique();
ios = ios_.get();
}
try {
// std::cerr << "running: " << cmdline() << "\n";
c_ = bp::child(prog.string(), bp::args(cmdline_), bp::std_in.close(),
bp::std_out > out_, bp::std_err > err_, *ios
#ifdef _WIN32
,
new_process_group()
#endif
);
} catch (...) {
std::cerr << "failed to create subprocess: " << cmdline() << "\n";
throw;
}
}
static void append_arg(std::vector& args, fs::path const& arg) {
args.emplace_back(arg.string());
}
static void append_arg(std::vector& args,
std::vector const& more) {
args.insert(args.end(), more.begin(), more.end());
}
template T>
static void append_arg(std::vector& args, T const& arg) {
args.emplace_back(arg);
}
bp::child c_;
std::unique_ptr ios_;
std::future out_;
std::future err_;
std::string outs_;
std::string errs_;
std::unique_ptr pt_;
std::filesystem::path const prog_;
std::vector cmdline_;
};
#if !(defined(_WIN32) || defined(__APPLE__))
class process_guard {
public:
process_guard() = default;
explicit process_guard(pid_t pid)
: pid_{pid} {
auto proc_dir = fs::path("/proc") / dwarfs::to(pid);
proc_dir_fd_ = ::open(proc_dir.c_str(), O_DIRECTORY);
if (proc_dir_fd_ < 0) {
throw std::runtime_error("could not open " + proc_dir.string());
}
}
~process_guard() {
if (proc_dir_fd_ >= 0) {
::close(proc_dir_fd_);
}
}
bool check_exit(std::chrono::milliseconds timeout) {
auto end = std::chrono::steady_clock::now() + timeout;
while (::faccessat(proc_dir_fd_, "fd", F_OK, 0) == 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
if (std::chrono::steady_clock::now() >= end) {
::kill(pid_, SIGTERM);
return false;
}
}
return true;
}
private:
pid_t pid_{-1};
int proc_dir_fd_{-1};
};
#endif
#ifdef DWARFS_WITH_FUSE_DRIVER
class driver_runner {
public:
struct foreground_t {};
static constexpr foreground_t foreground = foreground_t();
driver_runner() = default;
template
driver_runner(fs::path const& driver, bool tool_arg, fs::path const& image,
fs::path const& mountpoint, Args&&... args)
: mountpoint_{mountpoint} {
setup_mountpoint(mountpoint);
#ifdef _WIN32
process_ =
std::make_unique(driver, make_tool_arg(tool_arg), image,
mountpoint, std::forward(args)...);
process_->run_background();
wait_until_file_ready(mountpoint, std::chrono::seconds(5));
#else
std::vector options;
if (!subprocess::check_run(driver, make_tool_arg(tool_arg), image,
mountpoint, options,
std::forward(args)...)) {
throw std::runtime_error("error running " + driver.string());
}
#ifdef __APPLE__
wait_until_file_ready(mountpoint, std::chrono::seconds(5));
#else
dwarfs_guard_ = process_guard(get_dwarfs_pid(mountpoint));
#endif
#endif
}
template
driver_runner(foreground_t, fs::path const& driver, bool tool_arg,
fs::path const& image, fs::path const& mountpoint,
Args&&... args)
: mountpoint_{mountpoint} {
setup_mountpoint(mountpoint);
process_ = std::make_unique(driver, make_tool_arg(tool_arg),
image, mountpoint,
#ifndef _WIN32
"-f",
#endif
std::forward(args)...);
process_->run_background();
#if !(defined(_WIN32) || defined(__APPLE__))
dwarfs_guard_ = process_guard(process_->pid());
#endif
}
bool unmount() {
#ifdef _WIN32
static constexpr int const kSigIntExitCode{-1073741510};
#elif !defined(__APPLE__)
static constexpr int const kSigIntExitCode{SIGINT};
#endif
if (!mountpoint_.empty()) {
#ifdef __APPLE__
auto diskutil = dwarfs::test::find_binary("diskutil");
if (!diskutil) {
throw std::runtime_error("no diskutil binary found");
}
auto t0 = std::chrono::steady_clock::now();
for (;;) {
auto [out, err, ec] =
subprocess::run(diskutil.value(), "unmount", mountpoint_);
if (ec == 0) {
break;
}
std::cerr << "driver failed to unmount:\nout:\n"
<< out << "err:\n"
<< err << "exit code: " << ec << "\n";
if (std::chrono::steady_clock::now() - t0 > std::chrono::seconds(5)) {
throw std::runtime_error(
"driver still failed to unmount after 5 seconds");
}
std::cerr << "retrying...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
bool rv{true};
if (process_) {
process_->wait_background();
auto ec = process_->exit_code();
if (ec != 0) {
std::cerr << "driver failed to unmount:\nout:\n"
<< process_->out() << "err:\n"
<< process_->err() << "exit code: " << ec << "\n";
rv = false;
}
}
process_.reset();
mountpoint_.clear();
return rv;
#else
#ifndef _WIN32
if (process_) {
#endif
process_->interrupt();
process_->wait_background();
auto ec = process_->exit_code();
bool is_expected_exit_code = ec == 0 || ec == kSigIntExitCode;
if (!is_expected_exit_code) {
std::cerr << "driver failed to unmount:\nout:\n"
<< process_->out() << "err:\n"
<< process_->err() << "exit code: " << ec << "\n";
}
process_.reset();
mountpoint_.clear();
return is_expected_exit_code;
#ifndef _WIN32
} else {
auto fusermount = find_fusermount();
for (int i = 0; i < 5; ++i) {
if (subprocess::check_run(fusermount, "-u", mountpoint_)) {
break;
}
std::cerr << "retrying fusermount...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
mountpoint_.clear();
return dwarfs_guard_.check_exit(std::chrono::seconds(5));
}
#endif
#endif
}
return false;
}
std::string cmdline() const {
std::string rv;
if (process_) {
rv = process_->cmdline();
}
return rv;
}
~driver_runner() {
if (!mountpoint_.empty()) {
if (!unmount()) {
std::abort();
}
}
}
static std::vector make_tool_arg(bool tool_arg) {
std::vector rv;
if (tool_arg) {
rv.push_back("--tool=dwarfs");
}
return rv;
}
private:
#if !(defined(_WIN32) || defined(__APPLE__))
static fs::path find_fusermount() {
auto fusermount_bin = dwarfs::test::find_binary("fusermount");
if (!fusermount_bin) {
fusermount_bin = dwarfs::test::find_binary("fusermount3");
}
if (!fusermount_bin) {
throw std::runtime_error("no fusermount binary found");
}
return *fusermount_bin;
}
#endif
static void setup_mountpoint(fs::path const& mp) {
if (fs::exists(mp)) {
fs::remove(mp);
}
#ifndef _WIN32
fs::create_directory(mp);
#endif
}
fs::path mountpoint_;
std::unique_ptr process_;
#if !(defined(_WIN32) || defined(__APPLE__))
process_guard dwarfs_guard_;
#endif
};
bool check_readonly(fs::path const& p, bool readonly) {
auto st = fs::status(p);
bool is_writable =
(st.permissions() & fs::perms::owner_write) != fs::perms::none;
if (is_writable == readonly) {
std::cerr << "readonly=" << readonly << ", st_mode="
<< fmt::format("{0:o}", uint16_t(st.permissions())) << "\n";
return false;
}
#ifndef _WIN32
{
auto r = ::access(p.string().c_str(), W_OK);
if (readonly) {
if (r != -1 || errno != EACCES) {
std::cerr << "access(" << p << ", W_OK) = " << r << " (errno=" << errno
<< ") [readonly]\n";
return false;
}
} else {
if (r != 0) {
std::cerr << "access(" << p << ", W_OK) = " << r << "\n";
return false;
}
}
}
#endif
return true;
}
#endif
size_t num_hardlinks(fs::path const& p) {
#ifdef _WIN32
dwarfs::file_stat stat(p);
return stat.nlink();
#else
return fs::hard_link_count(p);
#endif
}
enum class binary_mode {
standalone,
universal_tool,
universal_symlink,
};
std::ostream& operator<<(std::ostream& os, binary_mode m) {
switch (m) {
case binary_mode::standalone:
os << "standalone";
break;
case binary_mode::universal_tool:
os << "universal-tool";
break;
case binary_mode::universal_symlink:
os << "universal-symlink";
break;
}
return os;
}
std::vector tools_test_modes{
binary_mode::standalone,
#ifdef DWARFS_HAVE_UNIVERSAL_BINARY
binary_mode::universal_tool,
binary_mode::universal_symlink,
#endif
};
class tools_test : public ::testing::TestWithParam {};
} // namespace
TEST_P(tools_test, end_to_end) {
auto mode = GetParam();
#ifdef DWARFS_WITH_FUSE_DRIVER
std::chrono::seconds const timeout{5};
#endif
dwarfs::temporary_directory tempdir("dwarfs");
auto td = fs::path(tempdir.path().string());
auto image = td / "test.dwarfs";
auto image_hdr = td / "test_hdr.dwarfs";
auto fsdata_dir = td / "fsdata";
auto header_data = fsdata_dir / "format.sh";
auto universal_symlink_dwarfs_bin = td / "dwarfs" EXE_EXT;
auto universal_symlink_mkdwarfs_bin = td / "mkdwarfs" EXE_EXT;
auto universal_symlink_dwarfsck_bin = td / "dwarfsck" EXE_EXT;
auto universal_symlink_dwarfsextract_bin = td / "dwarfsextract" EXE_EXT;
std::vector dwarfs_tool_arg;
std::vector mkdwarfs_tool_arg;
std::vector dwarfsck_tool_arg;
std::vector dwarfsextract_tool_arg;
fs::path const* mkdwarfs_test_bin = &mkdwarfs_bin;
fs::path const* dwarfsck_test_bin = &dwarfsck_bin;
fs::path const* dwarfsextract_test_bin = &dwarfsextract_bin;
if (mode == binary_mode::universal_symlink) {
fs::create_symlink(universal_bin, universal_symlink_dwarfs_bin);
fs::create_symlink(universal_bin, universal_symlink_mkdwarfs_bin);
fs::create_symlink(universal_bin, universal_symlink_dwarfsck_bin);
fs::create_symlink(universal_bin, universal_symlink_dwarfsextract_bin);
}
if (mode == binary_mode::universal_tool) {
mkdwarfs_test_bin = &universal_bin;
dwarfsck_test_bin = &universal_bin;
dwarfsextract_test_bin = &universal_bin;
dwarfs_tool_arg.push_back("--tool=dwarfs");
mkdwarfs_tool_arg.push_back("--tool=mkdwarfs");
dwarfsck_tool_arg.push_back("--tool=dwarfsck");
dwarfsextract_tool_arg.push_back("--tool=dwarfsextract");
}
{
auto out = subprocess::check_run(*mkdwarfs_test_bin, mkdwarfs_tool_arg);
ASSERT_TRUE(out);
EXPECT_THAT(*out, ::testing::HasSubstr("Usage:"));
EXPECT_THAT(*out, ::testing::HasSubstr("--long-help"));
}
if (mode == binary_mode::universal_tool) {
auto out = subprocess::check_run(universal_bin);
ASSERT_TRUE(out);
EXPECT_THAT(*out, ::testing::HasSubstr("--tool="));
}
ASSERT_TRUE(fs::create_directory(fsdata_dir));
ASSERT_TRUE(subprocess::check_run(*dwarfsextract_test_bin,
dwarfsextract_tool_arg, "-i",
test_data_dwarfs, "-o", fsdata_dir));
EXPECT_EQ(num_hardlinks(fsdata_dir / "format.sh"), 3);
EXPECT_TRUE(fs::is_symlink(fsdata_dir / "foobar"));
EXPECT_EQ(fs::read_symlink(fsdata_dir / "foobar"), fs::path("foo") / "bar");
auto unicode_symlink_name = u8"יוניקוד";
auto unicode_symlink = fsdata_dir / unicode_symlink_name;
auto unicode_symlink_target = fs::path("unicode") / u8"我爱你" / u8"☀️ Sun" /
u8"Γειά σας" / u8"مرحبًا" / u8"⚽️" /
u8"Карибського";
std::string unicode_file_contents;
EXPECT_TRUE(fs::is_symlink(unicode_symlink));
EXPECT_EQ(fs::read_symlink(unicode_symlink), unicode_symlink_target);
EXPECT_TRUE(read_file(unicode_symlink, unicode_file_contents));
EXPECT_EQ(unicode_file_contents, "unicode\n");
EXPECT_TRUE(
read_file(fsdata_dir / unicode_symlink_target, unicode_file_contents));
EXPECT_EQ(unicode_file_contents, "unicode\n");
ASSERT_TRUE(subprocess::check_run(*mkdwarfs_test_bin, mkdwarfs_tool_arg, "-i",
fsdata_dir, "-o", image, "--no-progress",
"--no-history", "--no-create-timestamp"));
ASSERT_TRUE(fs::exists(image));
ASSERT_GT(fs::file_size(image), 1000);
{
auto out = subprocess::check_run(
*mkdwarfs_test_bin, mkdwarfs_tool_arg, "-i", fsdata_dir, "-o", "-",
"--no-progress", "--no-history", "--no-create-timestamp");
ASSERT_TRUE(out);
std::string ref;
ASSERT_TRUE(read_file(image, ref));
EXPECT_EQ(ref.size(), out->size());
EXPECT_EQ(ref, *out);
}
ASSERT_TRUE(subprocess::check_run(
*mkdwarfs_test_bin, mkdwarfs_tool_arg, "-i", image, "-o", image_hdr,
"--no-progress", "--recompress=none", "--header", header_data));
ASSERT_TRUE(fs::exists(image_hdr));
ASSERT_GT(fs::file_size(image_hdr), 1000);
auto mountpoint = td / "mnt";
auto extracted = td / "extracted";
auto untared = td / "untared";
#ifdef DWARFS_WITH_FUSE_DRIVER
std::vector drivers;
switch (mode) {
case binary_mode::standalone:
drivers.push_back(fuse3_bin);
if (fs::exists(fuse2_bin)) {
drivers.push_back(fuse2_bin);
}
break;
case binary_mode::universal_tool:
drivers.push_back(universal_bin);
break;
case binary_mode::universal_symlink:
drivers.push_back(universal_symlink_dwarfs_bin);
break;
}
unicode_symlink = mountpoint / unicode_symlink_name;
if (skip_fuse_tests()) {
drivers.clear();
}
for (auto const& driver : drivers) {
{
scoped_no_leak_check no_leak_check;
auto const [out, err, ec] =
subprocess::run(driver, dwarfs_tool_arg, "--help");
EXPECT_THAT(out, ::testing::HasSubstr("Usage:"));
}
{
scoped_no_leak_check no_leak_check;
std::vector args;
#if DWARFS_PERFMON_ENABLED
args.push_back("-operfmon=fuse+inode_reader_v2+block_cache");
#endif
driver_runner runner(driver_runner::foreground, driver,
mode == binary_mode::universal_tool, image,
mountpoint, args);
ASSERT_TRUE(wait_until_file_ready(mountpoint / "format.sh", timeout))
<< runner.cmdline();
compare_directories_result cdr;
ASSERT_TRUE(compare_directories(fsdata_dir, mountpoint, &cdr))
<< runner.cmdline() << ": " << cdr;
EXPECT_EQ(cdr.regular_files.size(), 26)
<< runner.cmdline() << ": " << cdr;
EXPECT_EQ(cdr.directories.size(), 19) << runner.cmdline() << ": " << cdr;
EXPECT_EQ(cdr.symlinks.size(), 2) << runner.cmdline() << ": " << cdr;
EXPECT_EQ(1, num_hardlinks(mountpoint / "format.sh")) << runner.cmdline();
EXPECT_TRUE(fs::is_symlink(unicode_symlink)) << runner.cmdline();
EXPECT_EQ(fs::read_symlink(unicode_symlink), unicode_symlink_target)
<< runner.cmdline();
EXPECT_TRUE(read_file(unicode_symlink, unicode_file_contents))
<< runner.cmdline();
EXPECT_EQ(unicode_file_contents, "unicode\n") << runner.cmdline();
EXPECT_TRUE(
read_file(mountpoint / unicode_symlink_target, unicode_file_contents))
<< runner.cmdline();
EXPECT_EQ(unicode_file_contents, "unicode\n") << runner.cmdline();
#ifndef _WIN32
{
struct statfs stfs;
ASSERT_EQ(0, ::statfs(mountpoint.c_str(), &stfs)) << runner.cmdline();
EXPECT_EQ(stfs.f_files, 44) << runner.cmdline();
}
#endif
{
static constexpr auto kInodeInfoXattr{"user.dwarfs.inodeinfo"};
std::vector>> xattr_tests{
{mountpoint,
{"user.dwarfs.driver.pid", "user.dwarfs.driver.perfmon",
kInodeInfoXattr}},
{mountpoint / "format.sh", {kInodeInfoXattr}},
{mountpoint / "empty", {kInodeInfoXattr}},
};
for (auto const& [path, ref] : xattr_tests) {
EXPECT_EQ(dwarfs::listxattr(path), ref) << runner.cmdline();
auto xattr = dwarfs::getxattr(path, kInodeInfoXattr);
nlohmann::json info;
EXPECT_NO_THROW(info = nlohmann::json::parse(xattr))
<< runner.cmdline() << ", " << xattr;
EXPECT_TRUE(info.count("uid"));
EXPECT_TRUE(info.count("gid"));
EXPECT_TRUE(info.count("mode"));
}
auto perfmon =
dwarfs::getxattr(mountpoint, "user.dwarfs.driver.perfmon");
#if DWARFS_PERFMON_ENABLED
EXPECT_THAT(perfmon, ::testing::HasSubstr("[fuse.op_init]"));
EXPECT_THAT(perfmon, ::testing::HasSubstr("p99 latency"));
#else
EXPECT_THAT(perfmon,
::testing::StartsWith("no performance monitor support"));
#endif
EXPECT_THAT(
[&] { dwarfs::getxattr(mountpoint, "user.something.nonexistent"); },
::testing::Throws());
std::error_code ec;
dwarfs::getxattr(mountpoint, "user.something.nonexistent", ec);
EXPECT_TRUE(ec);
#ifdef __APPLE__
EXPECT_EQ(ec.value(), ENOATTR);
#else
EXPECT_EQ(ec.value(), ENODATA);
#endif
}
EXPECT_TRUE(runner.unmount()) << runner.cmdline();
}
{
auto const [out, err, ec] = subprocess::run(
driver,
driver_runner::make_tool_arg(mode == binary_mode::universal_tool),
image_hdr, mountpoint);
EXPECT_NE(0, ec) << driver << "\n"
<< "stdout:\n"
<< out << "\nstderr:\n"
<< err;
}
std::vector all_options{
"-s",
#ifndef _WIN32
"-oenable_nlink",
"-oreadonly",
#endif
};
#ifndef __APPLE__
// macFUSE is notoriously slow to start, so let's skip these tests
if (!dwarfs::test::skip_slow_tests()) {
all_options.push_back("-omlock=try");
all_options.push_back("-ono_cache_image");
all_options.push_back("-ocache_files");
all_options.push_back("-otidy_strategy=time");
}
#endif
unsigned const combinations = 1 << all_options.size();
for (unsigned bitmask = 0; bitmask < combinations; ++bitmask) {
std::vector args;
#ifndef _WIN32
bool enable_nlink{false};
bool readonly{false};
#endif
for (size_t i = 0; i < all_options.size(); ++i) {
if ((1 << i) & bitmask) {
auto const& opt = all_options[i];
#ifndef _WIN32
if (opt == "-oreadonly") {
readonly = true;
}
if (opt == "-oenable_nlink") {
enable_nlink = true;
}
#endif
args.push_back(opt);
}
}
args.push_back("-otidy_interval=1s");
args.push_back("-otidy_max_age=2s");
{
driver_runner runner(driver, mode == binary_mode::universal_tool, image,
mountpoint, args);
ASSERT_TRUE(wait_until_file_ready(mountpoint / "format.sh", timeout))
<< runner.cmdline();
EXPECT_TRUE(fs::is_symlink(mountpoint / "foobar")) << runner.cmdline();
EXPECT_EQ(fs::read_symlink(mountpoint / "foobar"),
fs::path("foo") / "bar")
<< runner.cmdline();
compare_directories_result cdr;
ASSERT_TRUE(compare_directories(fsdata_dir, mountpoint, &cdr))
<< runner.cmdline() << ": " << cdr;
EXPECT_EQ(cdr.regular_files.size(), 26)
<< runner.cmdline() << ": " << cdr;
EXPECT_EQ(cdr.directories.size(), 19)
<< runner.cmdline() << ": " << cdr;
EXPECT_EQ(cdr.symlinks.size(), 2) << runner.cmdline() << ": " << cdr;
#ifndef _WIN32
// TODO: https://github.com/winfsp/winfsp/issues/511
EXPECT_EQ(enable_nlink ? 3 : 1, num_hardlinks(mountpoint / "format.sh"))
<< runner.cmdline();
// This doesn't really work on Windows (yet)
EXPECT_TRUE(check_readonly(mountpoint / "format.sh", readonly))
<< runner.cmdline();
#endif
auto perfmon =
dwarfs::getxattr(mountpoint, "user.dwarfs.driver.perfmon");
#if DWARFS_PERFMON_ENABLED
EXPECT_THAT(perfmon,
::testing::StartsWith("performance monitor is disabled"));
#else
EXPECT_THAT(perfmon,
::testing::StartsWith("no performance monitor support"));
#endif
}
args.push_back("-ooffset=auto");
{
driver_runner runner(driver, mode == binary_mode::universal_tool,
image_hdr, mountpoint, args);
ASSERT_TRUE(wait_until_file_ready(mountpoint / "format.sh", timeout))
<< runner.cmdline();
EXPECT_TRUE(fs::is_symlink(mountpoint / "foobar")) << runner.cmdline();
EXPECT_EQ(fs::read_symlink(mountpoint / "foobar"),
fs::path("foo") / "bar")
<< runner.cmdline();
compare_directories_result cdr;
ASSERT_TRUE(compare_directories(fsdata_dir, mountpoint, &cdr))
<< runner.cmdline() << ": " << cdr;
EXPECT_EQ(cdr.regular_files.size(), 26)
<< runner.cmdline() << ": " << cdr;
EXPECT_EQ(cdr.directories.size(), 19)
<< runner.cmdline() << ": " << cdr;
EXPECT_EQ(cdr.symlinks.size(), 2) << runner.cmdline() << ": " << cdr;
#ifndef _WIN32
// TODO: https://github.com/winfsp/winfsp/issues/511
EXPECT_EQ(enable_nlink ? 3 : 1, num_hardlinks(mountpoint / "format.sh"))
<< runner.cmdline();
// This doesn't really work on Windows (yet)
EXPECT_TRUE(check_readonly(mountpoint / "format.sh", readonly))
<< runner.cmdline();
#endif
}
}
}
#endif
auto meta_export = td / "test.meta";
ASSERT_TRUE(
subprocess::check_run(*dwarfsck_test_bin, dwarfsck_tool_arg, image));
ASSERT_TRUE(subprocess::check_run(*dwarfsck_test_bin, dwarfsck_tool_arg,
image, "--check-integrity"));
ASSERT_TRUE(subprocess::check_run(*dwarfsck_test_bin, dwarfsck_tool_arg,
image, "--export-metadata", meta_export));
{
std::string header;
EXPECT_TRUE(read_file(header_data, header));
auto output = subprocess::check_run(*dwarfsck_test_bin, dwarfsck_tool_arg,
image_hdr, "-H");
ASSERT_TRUE(output);
EXPECT_EQ(header, *output);
}
EXPECT_GT(fs::file_size(meta_export), 1000);
ASSERT_TRUE(fs::create_directory(extracted));
ASSERT_TRUE(subprocess::check_run(*dwarfsextract_test_bin,
dwarfsextract_tool_arg, "-i", image, "-o",
extracted));
EXPECT_EQ(3, num_hardlinks(extracted / "format.sh"));
EXPECT_TRUE(fs::is_symlink(extracted / "foobar"));
EXPECT_EQ(fs::read_symlink(extracted / "foobar"), fs::path("foo") / "bar");
compare_directories_result cdr;
ASSERT_TRUE(compare_directories(fsdata_dir, extracted, &cdr)) << cdr;
EXPECT_EQ(cdr.regular_files.size(), 26) << cdr;
EXPECT_EQ(cdr.directories.size(), 19) << cdr;
EXPECT_EQ(cdr.symlinks.size(), 2) << cdr;
}
#ifdef DWARFS_WITH_FUSE_DRIVER
#define EXPECT_EC_IMPL(ec, cat, val) \
EXPECT_TRUE(ec) << runner.cmdline(); \
EXPECT_EQ(cat, (ec).category()) << runner.cmdline(); \
EXPECT_EQ(val, (ec).value()) << runner.cmdline() << ": " << (ec).message()
#ifdef _WIN32
#define EXPECT_EC_UNIX_MAC_WIN(ec, unix, mac, windows) \
EXPECT_EC_IMPL(ec, std::system_category(), windows)
#elif defined(__APPLE__)
#define EXPECT_EC_UNIX_MAC_WIN(ec, unix, mac, windows) \
EXPECT_EC_IMPL(ec, std::generic_category(), mac)
#else
#define EXPECT_EC_UNIX_MAC_WIN(ec, unix, mac, windows) \
EXPECT_EC_IMPL(ec, std::generic_category(), unix)
#endif
#define EXPECT_EC_UNIX_WIN(ec, unix, windows) \
EXPECT_EC_UNIX_MAC_WIN(ec, unix, unix, windows)
TEST_P(tools_test, mutating_and_error_ops) {
auto mode = GetParam();
if (skip_fuse_tests()) {
GTEST_SKIP() << "skipping FUSE tests";
}
#ifdef DWARFS_WITH_FUSE_DRIVER
std::chrono::seconds const timeout{5};
#endif
dwarfs::temporary_directory tempdir("dwarfs");
auto td = fs::path(tempdir.path().string());
auto mountpoint = td / "mnt";
auto file = mountpoint / "bench.sh";
auto empty_dir = mountpoint / "empty";
auto non_empty_dir = mountpoint / "foo";
auto name_inside_fs = mountpoint / "some_random_name";
auto name_outside_fs = td / "some_random_name";
auto universal_symlink_dwarfs_bin = td / "dwarfs" EXE_EXT;
if (mode == binary_mode::universal_symlink) {
fs::create_symlink(universal_bin, universal_symlink_dwarfs_bin);
}
std::vector drivers;
switch (mode) {
case binary_mode::standalone:
drivers.push_back(fuse3_bin);
if (fs::exists(fuse2_bin)) {
drivers.push_back(fuse2_bin);
}
break;
case binary_mode::universal_tool:
drivers.push_back(universal_bin);
break;
case binary_mode::universal_symlink:
drivers.push_back(universal_symlink_dwarfs_bin);
break;
}
for (auto const& driver : drivers) {
driver_runner runner(driver_runner::foreground, driver,
mode == binary_mode::universal_tool, test_data_dwarfs,
mountpoint);
ASSERT_TRUE(wait_until_file_ready(mountpoint / "format.sh", timeout))
<< runner.cmdline();
// remove (unlink)
{
std::error_code ec;
EXPECT_FALSE(fs::remove(file, ec)) << runner.cmdline();
EXPECT_EC_UNIX_WIN(ec, ENOSYS, ERROR_ACCESS_DENIED);
}
{
std::error_code ec;
EXPECT_FALSE(fs::remove(empty_dir, ec)) << runner.cmdline();
EXPECT_EC_UNIX_WIN(ec, ENOSYS, ERROR_ACCESS_DENIED);
}
{
std::error_code ec;
EXPECT_FALSE(fs::remove(non_empty_dir, ec)) << runner.cmdline();
EXPECT_EC_UNIX_WIN(ec, ENOSYS, ERROR_ACCESS_DENIED);
}
{
std::error_code ec;
EXPECT_EQ(static_cast(-1),
fs::remove_all(non_empty_dir, ec))
<< runner.cmdline();
EXPECT_EC_UNIX_WIN(ec, ENOSYS, ERROR_ACCESS_DENIED);
}
// rename
{
std::error_code ec;
fs::rename(file, name_inside_fs, ec);
EXPECT_EC_UNIX_MAC_WIN(ec, ENOSYS, EACCES, ERROR_ACCESS_DENIED);
}
{
std::error_code ec;
fs::rename(file, name_outside_fs, ec);
EXPECT_EC_UNIX_WIN(ec, EXDEV, ERROR_ACCESS_DENIED);
}
{
std::error_code ec;
fs::rename(empty_dir, name_inside_fs, ec);
EXPECT_EC_UNIX_MAC_WIN(ec, ENOSYS, EACCES, ERROR_ACCESS_DENIED);
}
{
std::error_code ec;
fs::rename(empty_dir, name_outside_fs, ec);
EXPECT_EC_UNIX_WIN(ec, EXDEV, ERROR_ACCESS_DENIED);
}
// hard link
{
std::error_code ec;
fs::create_hard_link(file, name_inside_fs, ec);
EXPECT_EC_UNIX_MAC_WIN(ec, ENOSYS, EACCES, ERROR_ACCESS_DENIED);
}
{
std::error_code ec;
fs::create_hard_link(file, name_outside_fs, ec);
EXPECT_EC_UNIX_WIN(ec, EXDEV, ERROR_ACCESS_DENIED);
}
// symbolic link
{
std::error_code ec;
fs::create_symlink(file, name_inside_fs, ec);
EXPECT_EC_UNIX_MAC_WIN(ec, ENOSYS, EACCES, ERROR_ACCESS_DENIED);
}
{
std::error_code ec;
fs::create_symlink(file, name_outside_fs, ec);
EXPECT_FALSE(ec) << runner.cmdline(); // this actually works :)
EXPECT_TRUE(fs::remove(name_outside_fs, ec)) << runner.cmdline();
}
{
std::error_code ec;
fs::create_directory_symlink(empty_dir, name_inside_fs, ec);
EXPECT_EC_UNIX_MAC_WIN(ec, ENOSYS, EACCES, ERROR_ACCESS_DENIED);
}
{
std::error_code ec;
fs::create_directory_symlink(empty_dir, name_outside_fs, ec);
EXPECT_FALSE(ec) << runner.cmdline(); // this actually works :)
EXPECT_TRUE(fs::remove(name_outside_fs, ec)) << runner.cmdline();
}
// truncate
{
std::error_code ec;
fs::resize_file(file, 1, ec);
EXPECT_EC_UNIX_WIN(ec, ENOSYS, ERROR_ACCESS_DENIED);
}
// create directory
{
std::error_code ec;
fs::create_directory(name_inside_fs, ec);
EXPECT_EC_UNIX_MAC_WIN(ec, ENOSYS, EACCES, ERROR_ACCESS_DENIED);
}
// read directory as file (non-mutating)
{
std::error_code ec;
auto tmp = dwarfs::read_file(mountpoint / "empty", ec);
EXPECT_TRUE(ec);
EXPECT_EC_UNIX_WIN(ec, EISDIR, ERROR_ACCESS_DENIED);
}
// open file as directory (non-mutating)
{
std::error_code ec;
fs::directory_iterator it{mountpoint / "format.sh", ec};
EXPECT_EC_UNIX_WIN(ec, ENOTDIR, ERROR_DIRECTORY);
}
// try open non-existing symlink
{
std::error_code ec;
auto tmp = fs::read_symlink(mountpoint / "doesnotexist", ec);
EXPECT_EC_UNIX_WIN(ec, ENOENT, ERROR_FILE_NOT_FOUND);
}
// Open non-existent file for writing
{
auto p = mountpoint / "nonexistent";
EXPECT_FALSE(fs::exists(p));
std::error_code ec;
dwarfs::write_file(p, "hello", ec);
EXPECT_TRUE(ec);
EXPECT_EC_UNIX_MAC_WIN(ec, ENOSYS, EACCES, ERROR_ACCESS_DENIED);
}
// Open existing file for writing
{
auto p = mountpoint / "format.sh";
EXPECT_TRUE(fs::exists(p));
std::error_code ec;
dwarfs::write_file(p, "hello", ec);
EXPECT_TRUE(ec);
EXPECT_EC_UNIX_WIN(ec, EACCES, ERROR_ACCESS_DENIED);
}
EXPECT_TRUE(runner.unmount()) << runner.cmdline();
}
}
#endif
TEST_P(tools_test, categorize) {
auto mode = GetParam();
std::chrono::seconds const timeout{5};
dwarfs::temporary_directory tempdir("dwarfs");
auto td = fs::path(tempdir.path().string());
auto image = td / "test.dwarfs";
auto image_recompressed = td / "test2.dwarfs";
auto fsdata_dir = td / "fsdata";
auto universal_symlink_dwarfs_bin = td / "dwarfs" EXE_EXT;
auto universal_symlink_mkdwarfs_bin = td / "mkdwarfs" EXE_EXT;
auto universal_symlink_dwarfsck_bin = td / "dwarfsck" EXE_EXT;
auto universal_symlink_dwarfsextract_bin = td / "dwarfsextract" EXE_EXT;
std::vector mkdwarfs_tool_arg;
std::vector dwarfsck_tool_arg;
std::vector dwarfsextract_tool_arg;
fs::path const* mkdwarfs_test_bin = &mkdwarfs_bin;
fs::path const* dwarfsck_test_bin = &dwarfsck_bin;
fs::path const* dwarfsextract_test_bin = &dwarfsextract_bin;
if (mode == binary_mode::universal_symlink) {
fs::create_symlink(universal_bin, universal_symlink_dwarfs_bin);
fs::create_symlink(universal_bin, universal_symlink_mkdwarfs_bin);
fs::create_symlink(universal_bin, universal_symlink_dwarfsck_bin);
fs::create_symlink(universal_bin, universal_symlink_dwarfsextract_bin);
}
if (mode == binary_mode::universal_tool) {
mkdwarfs_test_bin = &universal_bin;
dwarfsck_test_bin = &universal_bin;
dwarfsextract_test_bin = &universal_bin;
mkdwarfs_tool_arg.push_back("--tool=mkdwarfs");
dwarfsck_tool_arg.push_back("--tool=dwarfsck");
dwarfsextract_tool_arg.push_back("--tool=dwarfsextract");
}
ASSERT_TRUE(fs::create_directory(fsdata_dir));
ASSERT_TRUE(subprocess::check_run(*dwarfsextract_test_bin,
dwarfsextract_tool_arg, "-i",
test_catdata_dwarfs, "-o", fsdata_dir));
ASSERT_TRUE(fs::exists(fsdata_dir / "random"));
EXPECT_EQ(4096, fs::file_size(fsdata_dir / "random"));
std::vector mkdwarfs_args{"-i",
fsdata_dir.string(),
"-o",
image.string(),
"--no-progress",
"--categorize",
"-S",
"16",
"-W",
"pcmaudio/waveform::8"};
ASSERT_TRUE(subprocess::check_run(*mkdwarfs_test_bin, mkdwarfs_tool_arg,
mkdwarfs_args));
ASSERT_TRUE(fs::exists(image));
auto const image_size = fs::file_size(image);
EXPECT_GT(image_size, 150000);
EXPECT_LT(image_size, 300000);
std::vector mkdwarfs_args_recompress{
"-i",
image.string(),
"-o",
image_recompressed.string(),
"--no-progress",
"--recompress=block",
"--recompress-categories=pcmaudio/waveform",
"-C",
#ifdef DWARFS_HAVE_FLAC
"pcmaudio/waveform::flac:level=8"
#else
"pcmaudio/waveform::zstd:level=19"
#endif
};
ASSERT_TRUE(subprocess::check_run(*mkdwarfs_test_bin, mkdwarfs_tool_arg,
mkdwarfs_args_recompress));
ASSERT_TRUE(fs::exists(image_recompressed));
{
auto const image_size_recompressed = fs::file_size(image_recompressed);
EXPECT_GT(image_size_recompressed, 100000);
EXPECT_LT(image_size_recompressed, image_size);
}
#ifdef DWARFS_WITH_FUSE_DRIVER
if (!skip_fuse_tests()) {
auto mountpoint = td / "mnt";
fs::path driver;
switch (mode) {
case binary_mode::standalone:
driver = fuse3_bin;
break;
case binary_mode::universal_tool:
driver = universal_bin;
break;
case binary_mode::universal_symlink:
driver = universal_symlink_dwarfs_bin;
break;
}
driver_runner runner(driver_runner::foreground, driver,
mode == binary_mode::universal_tool, image,
mountpoint);
ASSERT_TRUE(wait_until_file_ready(mountpoint / "random", timeout))
<< runner.cmdline();
compare_directories_result cdr;
ASSERT_TRUE(compare_directories(fsdata_dir, mountpoint, &cdr))
<< runner.cmdline() << ": " << cdr;
EXPECT_EQ(cdr.regular_files.size(), 151) << runner.cmdline() << ": " << cdr;
EXPECT_EQ(cdr.total_regular_file_size, 56'741'701)
<< runner.cmdline() << ": " << cdr;
EXPECT_TRUE(runner.unmount()) << runner.cmdline();
}
#endif
auto json_info = subprocess::check_run(*dwarfsck_test_bin, dwarfsck_tool_arg,
image_recompressed, "--json");
ASSERT_TRUE(json_info);
nlohmann::json info;
EXPECT_NO_THROW(info = nlohmann::json::parse(*json_info)) << *json_info;
EXPECT_EQ(info["block_size"], 65'536);
EXPECT_EQ(info["image_offset"], 0);
EXPECT_EQ(info["inode_count"], 154);
EXPECT_EQ(info["original_filesystem_size"], 56'741'701);
EXPECT_EQ(info["categories"].size(), 4);
EXPECT_TRUE(info.count("created_by"));
EXPECT_TRUE(info.count("created_on"));
{
auto it = info["categories"].find("");
ASSERT_NE(it, info["categories"].end());
EXPECT_EQ((*it)["block_count"].get(), 1);
}
{
auto it = info["categories"].find("incompressible");
ASSERT_NE(it, info["categories"].end());
EXPECT_EQ((*it)["block_count"].get(), 1);
EXPECT_EQ((*it)["compressed_size"].get(), 4'096);
EXPECT_EQ((*it)["uncompressed_size"].get(), 4'096);
}
{
auto it = info["categories"].find("pcmaudio/metadata");
ASSERT_NE(it, info["categories"].end());
EXPECT_EQ((*it)["block_count"].get(), 3);
}
{
auto it = info["categories"].find("pcmaudio/waveform");
ASSERT_NE(it, info["categories"].end());
EXPECT_EQ((*it)["block_count"].get(), 48);
}
ASSERT_EQ(info["history"].size(), 2);
for (auto const& entry : info["history"]) {
ASSERT_TRUE(entry.count("arguments"));
EXPECT_TRUE(entry.count("compiler_id"));
EXPECT_TRUE(entry.count("libdwarfs_version"));
EXPECT_TRUE(entry.count("system_id"));
EXPECT_TRUE(entry.count("timestamp"));
}
{
nlohmann::json ref_args;
ref_args.push_back(mkdwarfs_test_bin->string());
std::copy(mkdwarfs_args.begin(), mkdwarfs_args.end(),
std::back_inserter(ref_args));
EXPECT_EQ(ref_args, info["history"][0]["arguments"]);
}
{
nlohmann::json ref_args;
ref_args.push_back(mkdwarfs_test_bin->string());
std::copy(mkdwarfs_args_recompress.begin(), mkdwarfs_args_recompress.end(),
std::back_inserter(ref_args));
EXPECT_EQ(ref_args, info["history"][1]["arguments"]);
}
}
INSTANTIATE_TEST_SUITE_P(dwarfs, tools_test,
::testing::ValuesIn(tools_test_modes));
#ifdef DWARFS_BUILTIN_MANPAGE
class manpage_test
: public ::testing::TestWithParam> {};
std::vector const manpage_test_tools{
"mkdwarfs",
"dwarfsck",
"dwarfsextract",
#ifdef DWARFS_WITH_FUSE_DRIVER
"dwarfs",
#endif
};
TEST_P(manpage_test, manpage) {
auto [mode, tool] = GetParam();
std::map tools{
{"dwarfs", fuse3_bin},
{"mkdwarfs", mkdwarfs_bin},
{"dwarfsck", dwarfsck_bin},
{"dwarfsextract", dwarfsextract_bin},
};
std::vector args;
fs::path const* test_bin{nullptr};
if (mode == binary_mode::universal_tool) {
test_bin = &universal_bin;
args.push_back("--tool=" + tool);
} else {
test_bin = &tools.at(tool);
}
scoped_no_leak_check no_leak_check;
auto out = subprocess::check_run(*test_bin, args, "--man");
ASSERT_TRUE(out);
EXPECT_GT(out->size(), 1000) << *out;
EXPECT_THAT(*out, ::testing::HasSubstr(tool));
EXPECT_THAT(*out, ::testing::HasSubstr("SYNOPSIS"));
EXPECT_THAT(*out, ::testing::HasSubstr("DESCRIPTION"));
EXPECT_THAT(*out, ::testing::HasSubstr("AUTHOR"));
EXPECT_THAT(*out, ::testing::HasSubstr("COPYRIGHT"));
}
namespace {
std::vector manpage_test_modes{
binary_mode::standalone,
#ifdef DWARFS_HAVE_UNIVERSAL_BINARY
binary_mode::universal_tool,
#endif
};
} // namespace
INSTANTIATE_TEST_SUITE_P(
dwarfs, manpage_test,
::testing::Combine(::testing::ValuesIn(manpage_test_modes),
::testing::ValuesIn(manpage_test_tools)));
#endif
TEST(tools_test, dwarfsextract_progress) {
dwarfs::temporary_directory tempdir("dwarfs");
auto td = fs::path(tempdir.path().string());
auto tarfile = td / "output.tar";
auto out =
subprocess::check_run(dwarfsextract_bin, "-i", test_catdata_dwarfs, "-o",
tarfile, "-f", "gnutar", "--stdout-progress");
ASSERT_TRUE(out);
EXPECT_TRUE(fs::exists(tarfile));
EXPECT_GT(out->size(), 100) << *out;
#ifdef _WIN32
EXPECT_THAT(*out, ::testing::EndsWith("100%\r\n"));
#else
EXPECT_THAT(*out, ::testing::EndsWith("100%\n"));
EXPECT_THAT(*out, ::testing::MatchesRegex("^\r([0-9][0-9]*%\r)*100%\n"));
#endif
}
TEST(tools_test, dwarfsextract_stdout) {
dwarfs::temporary_directory tempdir("dwarfs");
auto td = fs::path(tempdir.path().string());
auto out = subprocess::check_run(dwarfsextract_bin, "-i", test_catdata_dwarfs,
"-f", "mtree");
ASSERT_TRUE(out);
EXPECT_GT(out->size(), 1000) << *out;
EXPECT_THAT(*out, ::testing::StartsWith("#mtree\n"));
EXPECT_THAT(*out, ::testing::HasSubstr("type=file"));
}
TEST(tools_test, dwarfsextract_file_out) {
dwarfs::temporary_directory tempdir("dwarfs");
auto td = fs::path(tempdir.path().string());
auto outfile = td / "output.mtree";
auto out = subprocess::check_run(dwarfsextract_bin, "-i", test_catdata_dwarfs,
"-f", "mtree", "-o", outfile);
ASSERT_TRUE(out);
EXPECT_TRUE(out->empty());
ASSERT_TRUE(fs::exists(outfile));
std::string mtree;
ASSERT_TRUE(read_file(outfile, mtree));
EXPECT_GT(mtree.size(), 1000) << *out;
EXPECT_THAT(mtree, ::testing::StartsWith("#mtree\n"));
EXPECT_THAT(mtree, ::testing::HasSubstr("type=file"));
}