refactor(pager): make pager somewhat testable

This commit is contained in:
Marcus Holland-Moritz 2024-01-19 23:06:21 +01:00
parent 813de13d0e
commit 047b9bed61
9 changed files with 176 additions and 47 deletions

View File

@ -24,6 +24,7 @@
#include <chrono>
#include <filesystem>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <system_error>
@ -65,5 +66,7 @@ class os_access {
std::error_code& ec) const = 0;
virtual std::chrono::nanoseconds
thread_get_cpu_time(std::thread::id tid, std::error_code& ec) const = 0;
virtual std::filesystem::path
find_executable(std::filesystem::path const& name) const = 0;
};
} // namespace dwarfs

View File

@ -53,5 +53,7 @@ class os_access_generic : public os_access {
std::error_code& ec) const override;
std::chrono::nanoseconds
thread_get_cpu_time(std::thread::id tid, std::error_code& ec) const override;
std::filesystem::path
find_executable(std::filesystem::path const& name) const override;
};
} // namespace dwarfs

View File

@ -21,10 +21,21 @@
#pragma once
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
namespace dwarfs {
bool show_in_pager(std::string text);
class os_access;
struct pager_program {
std::filesystem::path name;
std::vector<std::string> args;
};
std::optional<pager_program> find_pager_program(os_access const& os);
void show_in_pager(pager_program const& pager, std::string text);
} // namespace dwarfs

View File

@ -25,6 +25,8 @@
#include <folly/portability/PThread.h>
#include <folly/portability/Unistd.h>
#include <boost/process/search_path.hpp>
#include "dwarfs/mmap.h"
#include "dwarfs/os_access_generic.h"
#include "dwarfs/util.h"
@ -193,4 +195,9 @@ os_access_generic::thread_get_cpu_time(std::thread::id tid,
#endif
}
std::filesystem::path
os_access_generic::find_executable(std::filesystem::path const& name) const {
return boost::process::search_path(name.wstring()).wstring();
}
} // namespace dwarfs

View File

@ -26,6 +26,9 @@
#include <boost/asio/io_service.hpp>
#include <boost/process.hpp>
#include <folly/portability/Unistd.h>
#include "dwarfs/os_access.h"
#include "dwarfs/pager.h"
namespace dwarfs {
@ -34,63 +37,53 @@ namespace {
namespace bp = boost::process;
struct pager_def {
std::string name;
std::vector<std::string> args;
};
std::vector<pager_def> const pagers{
std::vector<pager_program> const pagers{
{"less", {"-R"}},
};
auto find_executable(std::string name) { return bp::search_path(name); }
} // namespace
std::optional<pager_program> find_pager_program(os_access const& os) {
if (auto pager_env = os.getenv("PAGER")) {
std::string_view sv{pager_env.value()};
if (sv == "cat") {
return std::nullopt;
}
std::pair<boost::filesystem::path, std::vector<std::string>> find_pager() {
if (auto pager_env = std::getenv("PAGER")) {
std::string_view sv(pager_env);
if (sv.starts_with('"') && sv.ends_with('"')) {
sv.remove_prefix(1);
sv.remove_suffix(1);
}
if (sv == "cat") {
return {};
std::filesystem::path p{std::string(sv)};
if (os.access(p, X_OK) == 0) {
return pager_program{p, {}};
}
boost::filesystem::path p{std::string(sv)};
if (boost::filesystem::exists(p)) {
return {p.string(), {}};
}
if (auto exe = find_executable(pager_env); !exe.empty()) {
return {exe, {}};
if (auto exe = os.find_executable(p); !exe.empty()) {
return pager_program{exe, {}};
}
}
for (auto const& p : pagers) {
if (auto exe = find_executable(std::string(p.name)); !exe.empty()) {
return {exe, p.args};
if (auto exe = os.find_executable(p.name); !exe.empty()) {
return pager_program{exe, p.args};
}
}
return {};
return std::nullopt;
}
} // namespace
bool show_in_pager(std::string text) {
auto [pager_exe, pager_args] = find_pager();
if (pager_exe.empty()) {
return false;
}
void show_in_pager(pager_program const& pager, std::string text) {
boost::asio::io_service ios;
bp::child proc(pager_exe, bp::args(pager_args),
bp::child proc(pager.name.wstring(), bp::args(pager.args),
bp::std_in =
boost::asio::const_buffer(text.data(), text.size()),
bp::std_out > stdout, ios);
ios.run();
proc.wait();
return true;
}
} // namespace dwarfs

View File

@ -91,11 +91,18 @@ void add_common_options(po::options_description& opts,
#ifdef DWARFS_BUILTIN_MANPAGE
void show_manpage(manpage::document doc, iolayer const& iol) {
bool is_tty = iol.term->is_tty(iol.out);
auto content =
render_manpage(doc, iol.term->width(), is_tty && iol.term->is_fancy());
if (!is_tty || !show_in_pager(content)) {
iol.out << content;
if (is_tty) {
if (auto pager = find_pager_program(*iol.os)) {
show_in_pager(*pager, content);
return;
}
}
iol.out << content;
}
#endif

View File

@ -25,8 +25,11 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "dwarfs/pager.h"
#include "dwarfs/render_manpage.h"
#include "test_helpers.h"
using namespace dwarfs;
namespace {
@ -62,3 +65,92 @@ INSTANTIATE_TEST_SUITE_P(
::testing::Combine(::testing::Values("mkdwarfs", "dwarfs", "dwarfsck",
"dwarfsextract"),
::testing::Bool()));
TEST(pager_test, find_pager_program) {
auto resolver = [](std::filesystem::path const& name) {
std::map<std::string, std::filesystem::path> const programs = {
{"less", "/whatever/bin/less"},
{"more", "/somewhere/bin/more"},
{"cat", "/bin/cat"},
};
for (auto const& [n, p] : programs) {
if (name == n || name == p) {
return p;
}
}
return std::filesystem::path{};
};
{
test::os_access_mock os;
os.set_executable_resolver(
[](std::filesystem::path const&) { return std::filesystem::path{}; });
{
auto pager = find_pager_program(os);
ASSERT_FALSE(pager);
}
os.set_executable_resolver(resolver);
{
auto pager = find_pager_program(os);
ASSERT_TRUE(pager);
EXPECT_EQ("/whatever/bin/less", pager->name);
EXPECT_EQ(std::vector<std::string>({"-R"}), pager->args);
}
{
auto pager = find_pager_program(os);
ASSERT_TRUE(pager);
EXPECT_EQ("/whatever/bin/less", pager->name);
EXPECT_EQ(std::vector<std::string>({"-R"}), pager->args);
}
os.set_access_fail("more");
os.set_access_fail("less");
os.setenv("PAGER", "more");
{
auto pager = find_pager_program(os);
ASSERT_TRUE(pager);
EXPECT_EQ("/somewhere/bin/more", pager->name);
EXPECT_TRUE(pager->args.empty());
}
os.setenv("PAGER", "less");
{
auto pager = find_pager_program(os);
ASSERT_TRUE(pager);
EXPECT_EQ("/whatever/bin/less", pager->name);
EXPECT_TRUE(pager->args.empty());
}
os.setenv("PAGER", "cat");
{
auto pager = find_pager_program(os);
ASSERT_FALSE(pager);
}
os.setenv("PAGER", "/bla/foo");
{
auto pager = find_pager_program(os);
ASSERT_TRUE(pager);
EXPECT_EQ("/bla/foo", pager->name);
EXPECT_TRUE(pager->args.empty());
}
os.setenv("PAGER", R"("/bla/foo")");
{
auto pager = find_pager_program(os);
ASSERT_TRUE(pager);
EXPECT_EQ("/bla/foo", pager->name);
EXPECT_TRUE(pager->args.empty());
}
}
}

View File

@ -516,20 +516,25 @@ os_access_mock::thread_get_cpu_time(std::thread::id tid,
return real_os_->thread_get_cpu_time(tid, ec);
}
std::optional<fs::path> find_binary(std::string_view name) {
auto path_str = std::getenv("PATH");
if (!path_str) {
return std::nullopt;
std::filesystem::path
os_access_mock::find_executable(std::filesystem::path const& name) const {
if (executable_resolver_) {
return executable_resolver_(name);
}
std::vector<std::string> path;
folly::split(':', path_str, path);
return real_os_->find_executable(name);
}
for (auto dir : path) {
auto cand = fs::path(dir) / name;
if (fs::exists(cand) and ::access(cand.string().c_str(), X_OK) == 0) {
return cand;
}
void os_access_mock::set_executable_resolver(
executable_resolver_type resolver) {
executable_resolver_ = std::move(resolver);
}
std::optional<fs::path> find_binary(std::string_view name) {
os_access_generic os;
if (auto path = os.find_executable(name); !path.empty()) {
return path;
}
return std::nullopt;

View File

@ -73,6 +73,9 @@ class os_access_mock : public os_access {
std::variant<std::monostate, std::string, std::function<std::string()>,
std::unique_ptr<mock_directory>>;
using executable_resolver_type =
std::function<std::filesystem::path(std::filesystem::path const&)>;
os_access_mock();
~os_access_mock();
@ -126,6 +129,11 @@ class os_access_mock : public os_access {
std::chrono::nanoseconds
thread_get_cpu_time(std::thread::id tid, std::error_code& ec) const override;
std::filesystem::path
find_executable(std::filesystem::path const& name) const override;
void set_executable_resolver(executable_resolver_type resolver);
std::set<std::filesystem::path> get_failed_paths() const;
std::vector<
@ -150,6 +158,7 @@ class os_access_mock : public os_access {
std::map<std::filesystem::path, error_info> map_file_errors_;
std::map<std::string, std::string> env_;
std::shared_ptr<os_access> real_os_;
executable_resolver_type executable_resolver_;
};
class script_mock : public script {