test: integrate manpage coverage tests and remove check_manpage script

This commit is contained in:
Marcus Holland-Moritz 2024-08-10 19:24:13 +02:00
parent 34cdbf5056
commit 2f08968187
3 changed files with 121 additions and 107 deletions

View File

@ -1,68 +0,0 @@
#!/bin/env python3
import os
import re
import subprocess
import sys
import termcolor
def extract_options(text):
options = set()
for line in text.splitlines():
match = re.search(r"--(\w[\w-]*)", line)
if match:
options.add(match.group(1))
return options
if __name__ == "__main__":
if len(sys.argv) < 3 or len(sys.argv) > 4:
print("Usage: check_manpage.py <manpage> <program> [<help-arg>]")
sys.exit(1)
manpage = sys.argv[1]
program = sys.argv[2]
help_arg = sys.argv[3] if len(sys.argv) == 4 else "--help"
manpage_basename = os.path.basename(manpage)
result = subprocess.run([program, help_arg], capture_output=True)
program_help = (result.stdout + result.stderr).decode("utf-8")
manpage_text = open(manpage).read()
program_options = extract_options(program_help)
manpage_options = extract_options(manpage_text)
# print(f'Program options: {program_options}')
# print(f'Manpage options: {manpage_options}')
# subset of options that are in the manpage but not in the program
obsolete_options = manpage_options - program_options
# subset of options that are in the program but not in the manpage
missing_options = program_options - manpage_options
exit_code = 0
if obsolete_options:
print(termcolor.colored(f"{manpage_basename}: obsolete options:", "red"))
for option in obsolete_options:
print(f" --{option}")
exit_code = 1
if missing_options:
print(termcolor.colored(f"{manpage_basename}: missing options:", "red"))
for option in missing_options:
print(f" --{option}")
exit_code = 1
if exit_code == 0:
print(
termcolor.colored(f"{manpage_basename}: OK", "green")
+ f" (checked {len(program_options)} options)"
)
sys.exit(exit_code)

View File

@ -1,32 +0,0 @@
#!/bin/bash
script_dir="$(cd $(dirname $0); pwd)"
manpage_dir="$script_dir/../doc"
check_manpage_script="$script_dir/check_manpage.py"
# function to check tool help vs manpage
function check_manpage() {
local tool="./$1"
local tool_help=$2
local manpage="$manpage_dir/$1.md"
# check that the manpage exists
if [ ! -f "$manpage" ]; then
echo "ERROR: '$manpage' does not exist"
exit 1
fi
# check that the tool exists
if [ ! -f "$tool" ]; then
echo "ERROR: '$tool' does not exist"
exit 1
fi
$check_manpage_script "$manpage" "$tool" "$tool_help"
}
check_manpage mkdwarfs --long-help
check_manpage dwarfsck --help
check_manpage dwarfsextract --help
# cannot easily check `dwarfs` at the moment

View File

@ -19,38 +19,62 @@
* along with dwarfs. If not, see <https://www.gnu.org/licenses/>.
*/
#include <array>
#include <filesystem>
#include <map>
#include <regex>
#include <string>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <dwarfs/tool/main_adapter.h>
#include <dwarfs/tool/pager.h>
#include <dwarfs/tool/render_manpage.h>
#include <dwarfs_tool_main.h>
#include <dwarfs_tool_manpage.h>
#include "test_helpers.h"
using namespace dwarfs;
using namespace dwarfs::tool;
using namespace std::string_literals;
namespace {
std::map<std::string, manpage::document> const docs = {
{"mkdwarfs", manpage::get_mkdwarfs_manpage()},
{"dwarfs", manpage::get_dwarfs_manpage()},
{"dwarfsck", manpage::get_dwarfsck_manpage()},
{"dwarfsextract", manpage::get_dwarfsextract_manpage()},
struct tool_defs {
manpage::document doc;
main_adapter::main_fn_type main;
std::string_view help_option;
bool is_fuse;
};
}
std::map<std::string, tool_defs> const tools = {
{"mkdwarfs", {manpage::get_mkdwarfs_manpage(), mkdwarfs_main, "-H", false}},
{"dwarfs", {manpage::get_dwarfs_manpage(), dwarfs_main, "-h", true}},
{"dwarfsck", {manpage::get_dwarfsck_manpage(), dwarfsck_main, "-h", false}},
{"dwarfsextract",
{manpage::get_dwarfsextract_manpage(), dwarfsextract_main, "-h", false}},
};
std::array const coverage_tests{
"mkdwarfs"s,
"dwarfsck"s,
"dwarfsextract"s,
#ifndef DWARFS_TEST_RUNNING_ON_ASAN
// FUSE driver is leaky, so we don't run this test under ASAN
"dwarfs"s,
#endif
};
} // namespace
class manpage_render_test
: public ::testing::TestWithParam<std::tuple<std::string, bool>> {};
TEST_P(manpage_render_test, basic) {
auto [name, color] = GetParam();
auto doc = docs.at(name);
auto doc = tools.at(name).doc;
for (size_t width = 20; width <= 200; width += 1) {
auto out = render_manpage(doc, width, color);
EXPECT_GT(out.size(), 1000);
@ -68,6 +92,96 @@ INSTANTIATE_TEST_SUITE_P(
"dwarfsextract"),
::testing::Bool()));
namespace {
std::regex const boost_po_option{R"(\n\s+(-(\w)\s+\[\s+)?--(\w[\w-]*\w))"};
std::regex const manpage_option{R"(\n\s+(-(\w),\s+)?--(\w[\w-]*\w))"};
std::regex const fuse_option{R"(\n\s+-o\s+([\w()]+))"};
std::map<std::string, std::string>
parse_options(std::string const& text, std::regex const& re, bool is_fuse) {
std::map<std::string, std::string> options;
auto opts_begin = std::sregex_iterator(text.begin(), text.end(), re);
auto opts_end = std::sregex_iterator();
for (auto it = opts_begin; it != opts_end; ++it) {
auto match = *it;
if (is_fuse) {
auto opt = match[1].str();
if (!options.emplace(opt, std::string{}).second) {
throw std::runtime_error("duplicate option definition for " + opt);
}
} else {
auto short_opt = match[2].str();
auto long_opt = match[3].str();
if (auto it = options.find(long_opt); it != options.end()) {
if (!it->second.empty()) {
if (short_opt.empty()) {
continue;
} else {
throw std::runtime_error("duplicate option definition for " +
long_opt);
}
}
}
options[long_opt] = short_opt;
}
}
return options;
}
} // namespace
class manpage_coverage_test : public ::testing::TestWithParam<std::string> {};
TEST_P(manpage_coverage_test, options) {
auto tool_name = GetParam();
auto const& tool = tools.at(tool_name);
auto man = render_manpage(tool.doc, 80, false);
test::test_iolayer iol;
std::array<std::string_view, 2> const args{tool_name, tool.help_option};
auto rv = main_adapter{tool.main}(args, iol.get());
#ifndef _WIN32
// WinFSP exits with a non-zero code when displaying usage :-/
ASSERT_EQ(0, rv) << tool_name << " " << tool.help_option << " failed";
#endif
auto help_opts = parse_options(
iol.out(), tool.is_fuse ? fuse_option : boost_po_option, tool.is_fuse);
auto man_opts = parse_options(
man, tool.is_fuse ? fuse_option : manpage_option, tool.is_fuse);
if (tool.is_fuse) {
man_opts.erase("allow_root");
man_opts.erase("allow_other");
} else {
EXPECT_TRUE(help_opts.contains("help"))
<< tool_name << " missing help option";
}
for (auto const& [opt, short_opt] : help_opts) {
auto it = man_opts.find(opt);
if (it == man_opts.end()) {
FAIL() << "option " << opt << " not documented for " << tool_name;
} else {
EXPECT_EQ(short_opt, it->second)
<< "short option mismatch for " << opt << " for " << tool_name;
}
}
for (auto const& [opt, short_opt] : man_opts) {
auto it = help_opts.find(opt);
if (it == help_opts.end()) {
FAIL() << "option " << opt << " is obsolete for " << tool_name;
}
}
}
INSTANTIATE_TEST_SUITE_P(dwarfs, manpage_coverage_test,
::testing::ValuesIn(coverage_tests));
TEST(pager_test, find_pager_program) {
auto resolver = [](std::filesystem::path const& name) {
std::map<std::string, std::filesystem::path> const programs = {