#include "3rd-party/catch.hpp" #include #include "cliargsparser.h" using namespace newsboat; /// Helper class to create argc and argv arguments for CliArgsParser /// /// When testing CliArgsParser, resource management turned out to be a problem: /// CliArgsParser requires char** pointing to arguments, but such a pointer /// can't be easily obtained from any of standard containers. To overcome that, /// I wrote Opts, which simply copies elements of initializer_list into /// separate unique_ptr, and presents useful accessors argc() and argv() /// whose results can be passed right into CliArgsParser. Problem solved! class Opts { /// Individual elements of argv. std::vector> m_opts; /// This is argv as main() knows it. std::unique_ptr m_data; /// This is argc as main() knows it. std::size_t m_argc = 0; public: /// Turns \a opts into argc and argv. Opts(std::initializer_list opts) : m_argc(opts.size()) { m_opts.reserve(m_argc); for (const std::string& option : opts) { // Copy string into separate char[], managed by // unique_ptr. auto ptr = std::unique_ptr( new char[option.size() + 1]); std::copy(option.cbegin(), option.cend(), ptr.get()); // C and C++ require argv to be NULL-terminated: // https://stackoverflow.com/questions/18547114/why-do-we-need-argc-while-there-is-always-a-null-at-the-end-of-argv ptr.get()[option.size()] = '\0'; // Hold onto the smart pointer to keep the entry in argv // alive. m_opts.emplace_back(std::move(ptr)); } // Copy out intermediate argv vector into its final storage. m_data = std::unique_ptr(new char*[m_argc + 1]); int i = 0; for (const auto& ptr : m_opts) { m_data.get()[i++] = ptr.get(); } m_data.get()[i] = nullptr; } std::size_t argc() const { return m_argc; } char** argv() const { return m_data.get(); } }; TEST_CASE( "Asks to print usage info and exit with failure if unknown option is " "provided", "[CliArgsParser]") { auto check = [](Opts opts) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.should_print_usage); REQUIRE(args.should_return); REQUIRE(args.return_code == EXIT_FAILURE); }; SECTION("Example No.1") { check({"newsboat", "--some-unknown-option"}); } SECTION("Example No.2") { check({"newsboat", "-s"}); } SECTION("Example No.3") { check({"newsboat", "-m ix"}); } SECTION("Example No.4") { check({"newsboat", "-wtf"}); } } TEST_CASE( "Sets `do_import` and `importfile` if -i/--import-from-opml is " "provided", "[CliArgsParser]") { const std::string filename("blogroll.opml"); auto check = [&filename](Opts opts) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.do_import); REQUIRE(args.importfile == filename); }; SECTION("-i") { check({"newsboat", "-i", filename}); } SECTION("--import-from-opml") { check({"newsboat", "--import-from-opml=" + filename}); } } TEST_CASE( "Asks to print usage and exit with failure if both " "-i/--import-from-opml and -e/--export-to-opml are provided", "[CliArgsParser]") { const std::string importf("import.opml"); const std::string exportf("export.opml"); auto check = [](Opts opts) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.should_print_usage); REQUIRE(args.should_return); REQUIRE(args.return_code == EXIT_FAILURE); }; SECTION("-i first") { check({"newsboat", "-i", importf, "-e", exportf}); } SECTION("-e first") { check({"newsboat", "-e", exportf, "-i", importf}); } } TEST_CASE("Sets `refresh_on_start` if -r/--refresh-on-start is provided", "[CliArgsParser]") { auto check = [](Opts opts) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.refresh_on_start); }; SECTION("-r") { check({"newsboat", "-r"}); } SECTION("--refresh-on-start") { check({"newsboat", "--refresh-on-start"}); } } TEST_CASE("Requests silent mode if -e/--export-to-opml is provided", "[CliArgsParser]") { const Opts opts{"newsboat", "-e"}; CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.silent); } TEST_CASE("Sets `do_export` if -e/--export-to-opml is provided", "[CliArgsParser]") { auto check = [](Opts opts) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.do_export); }; SECTION("-e") { check({"newsboat", "-e"}); } SECTION("--export-to-opml") { check({"newsboat", "--export-to-opml"}); } } TEST_CASE("Asks to print usage and exit with success if -h/--help is provided", "[CliArgsParser]") { auto check = [](Opts opts) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.should_print_usage); REQUIRE(args.should_return); REQUIRE(args.return_code == EXIT_SUCCESS); }; SECTION("-h") { check({"newsboat", "-h"}); } SECTION("--help") { check({"newsboat", "--help"}); } } TEST_CASE( "Sets `url_file`, `set_url_file`, and " "`using_nonstandard_configs` if -u/--url-file is provided", "[CliArgsParser]") { const std::string filename("urlfile"); auto check = [&filename](Opts opts) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.set_url_file); REQUIRE(args.url_file == filename); REQUIRE(args.using_nonstandard_configs); }; SECTION("-u") { check({"newsboat", "-u", filename}); } SECTION("--url-file") { check({"newsboat", "--url-file=" + filename}); } } TEST_CASE( "Sets `cache_file`, `lock_file`, `set_cache_file`, `set_lock_file`, " "and " "`using_nonstandard_configs` if -c/--cache-file is provided", "[CliArgsParser]") { const std::string filename("cache.db"); auto check = [&filename](Opts opts) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.set_cache_file); REQUIRE(args.cache_file == filename); REQUIRE(args.set_lock_file); REQUIRE_THAT( args.lock_file, Catch::Matchers::StartsWith(filename)); REQUIRE(args.using_nonstandard_configs); }; SECTION("-c") { check({"newsboat", "-c", filename}); } SECTION("--cache-file") { check({"newsboat", "--cache-file=" + filename}); } } TEST_CASE( "Sets `config_file`, `set_config_file`, and " "`using_nonstandard_configs` if -C/--config-file is provided", "[CliArgsParser]") { const std::string filename("config file"); auto check = [&filename](Opts opts) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.set_config_file); REQUIRE(args.config_file == filename); REQUIRE(args.using_nonstandard_configs); }; SECTION("-C") { check({"newsboat", "-C", filename}); } SECTION("--config-file") { check({"newsboat", "--config-file=" + filename}); } } TEST_CASE("Sets `do_vacuum` if -X/--vacuum is provided", "[CliArgsParser]") { auto check = [](Opts opts) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.do_vacuum); }; SECTION("-X") { check({"newsboat", "-X"}); } SECTION("--vacuum") { check({"newsboat", "--vacuum"}); } } TEST_CASE("Increases `show_version` with each -v/-V/--version provided", "[CliArgsParser]") { auto check = [](Opts opts, int expected_version) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.show_version == expected_version); }; SECTION("-v => 1") { check({"newsboat", "-v"}, 1); } SECTION("-V => 1") { check({"newsboat", "-V"}, 1); } SECTION("--version => 1") { check({"newsboat", "--version"}, 1); } SECTION("-vV => 2") { check({"newsboat", "-vV"}, 2); } SECTION("--version -v => 2") { check({"newsboat", "--version", "-v"}, 2); } SECTION("-V --version -v => 3") { check({"newsboat", "-V", "--version", "-v"}, 3); } SECTION("-VvVVvvvvV => 9") { check({"newsboat", "-VvVVvvvvV"}, 9); } } TEST_CASE("Requests silent mode if -x/--execute is provided", "[CliArgsParser]") { auto check = [](Opts opts) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.silent); }; SECTION("-x") { check({"newsboat", "-x", "reload"}); } SECTION("--execute") { check({"newsboat", "--execute", "reload"}); } } TEST_CASE("Sets `execute_cmds` if -x/--execute is provided", "[CliArgsParser]") { auto check = [](Opts opts) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.execute_cmds); }; SECTION("-x") { check({"newsboat", "-x", "reload"}); } SECTION("--execute") { check({"newsboat", "--execute", "reload"}); } } TEST_CASE("Inserts commands to cmds_to_execute if -x/--execute is provided", "[CliArgsParser]") { auto check = [](Opts opts, const std::vector& cmds) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.cmds_to_execute == cmds); }; SECTION("-x reload") { check({"newsboat", "-x", "reload"}, {"reload"}); } SECTION("--execute reload") { check({"newsboat", "--execute", "reload"}, {"reload"}); } SECTION("-x reload print-unread") { check({"newsboat", "-x", "reload", "print-unread"}, {"reload", "print-unread"}); } SECTION("--execute reload print-unread") { check({"newsboat", "--execute", "reload", "print-unread"}, {"reload", "print-unread"}); } } TEST_CASE("Requests silent mode if -q/--quiet is provided", "[CliArgsParser]") { auto check = [](Opts opts) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.silent); }; SECTION("-q") { check({"newsboat", "-q"}); } SECTION("--quiet") { check({"newsboat", "--quiet"}); } } TEST_CASE( "Sets `do_read_import` and `readinfofile` if -I/--import-from-file " "is provided", "[CliArgsParser]") { const std::string filename("filename"); auto check = [&filename](Opts opts) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.do_read_import); REQUIRE(args.readinfofile == filename); }; SECTION("-I") { check({"newsboat", "-I", filename}); } SECTION("--import-from-file") { check({"newsboat", "--import-from-file=" + filename}); } } TEST_CASE( "Sets `do_read_export` and `readinfofile` if -E/--export-to-file " "is provided", "[CliArgsParser]") { const std::string filename("filename"); auto check = [&filename](Opts opts) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.do_read_export); REQUIRE(args.readinfofile == filename); }; SECTION("-E") { check({"newsboat", "-E", filename}); } SECTION("--export-from-file") { check({"newsboat", "--export-to-file=" + filename}); } } TEST_CASE( "Asks to print usage info and exit with failure if both " "-I/--import-from-file and -E/--export-to-file are provided", "[CliArgsParser]") { const std::string importf("import.opml"); const std::string exportf("export.opml"); auto check = [](Opts opts) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.should_print_usage); REQUIRE(args.should_return); REQUIRE(args.return_code == EXIT_FAILURE); }; SECTION("-I first") { check({"newsboat", "-I", importf, "-E", exportf}); } SECTION("-E first") { check({"newsboat", "-E", exportf, "-I", importf}); } } TEST_CASE("Sets `set_log_file` and `log_file` if -d/--log-file is provided", "[CliArgsParser]") { const std::string filename("log file.txt"); auto check = [&filename](Opts opts) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.set_log_file); REQUIRE(args.log_file == filename); }; SECTION("-d") { check({"newsboat", "-d", filename}); } SECTION("--log-file") { check({"newsboat", "--log-file=" + filename}); } } TEST_CASE( "Sets `set_log_level` and `log_level` if argument to " "-l/--log-level is in range of [1; 6]", "[CliArgsParser]") { auto check = [](Opts opts, Level expected) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE(args.set_log_level); REQUIRE(args.log_level == expected); }; SECTION("--log-level=1 means USERERROR") { check({"newsboat", "--log-level=1"}, Level::USERERROR); } SECTION("--log-level=2 means CRITICAL") { check({"newsboat", "--log-level=2"}, Level::CRITICAL); } SECTION("-l3 means ERROR") { check({"newsboat", "-l3"}, Level::ERROR); } SECTION("--log-level=4 means WARN") { check({"newsboat", "--log-level=4"}, Level::WARN); } SECTION("-l5 means INFO") { check({"newsboat", "-l5"}, Level::INFO); } SECTION("-l6 means DEBUG") { check({"newsboat", "-l6"}, Level::DEBUG); } } TEST_CASE( "Sets `display_msg` and asks to exit with failure if argument to " "-l/--log-level is outside of [1; 6]", "[CliArgsParser]") { auto check = [](Opts opts) { CliArgsParser args(opts.argc(), opts.argv()); REQUIRE_FALSE(args.display_msg.empty()); REQUIRE(args.should_return); REQUIRE(args.return_code == EXIT_FAILURE); }; SECTION("-l0") { check({"newsboat", "-l0"}); } SECTION("--log-level=7") { check({"newsboat", "--log-level=7"}); } SECTION("--log-level=9001") { check({"newsboat", "--log-level=90001"}); } }