1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
|
#include "configparser.h"
#include <fstream>
#include <utility>
#include <vector>
#include "3rd-party/catch.hpp"
#include "configexception.h"
#include "keycombination.h"
#include "keymap.h"
#include "test_helpers/envvar.h"
#include "test_helpers/tempfile.h"
using namespace newsboat;
namespace {
class ConfigHandlerHistoryDummy : public ConfigActionHandler {
public:
void handle_action(const std::string& action,
const std::vector<std::string>& params) override
{
history.emplace_back(action, params);
};
void dump_config(std::vector<std::string>&) const override {};
std::vector<std::pair<std::string, std::vector<std::string>>> history;
};
} // Anonymous namespace
TEST_CASE("parse_line() handles both lines with and without quoting",
"[ConfigParser]")
{
ConfigParser cfgParser;
ConfigHandlerHistoryDummy handler;
cfgParser.register_handler("command-name", handler);
const std::string location = "dummy-location";
SECTION("unknown command results in exception") {
REQUIRE_THROWS_AS(cfgParser.parse_line("foo", location), ConfigException);
REQUIRE_THROWS_AS(cfgParser.parse_line("foo arg1 arg2", location),
ConfigException);
}
SECTION("different combinations of (un)quoted commands/arguments have the same result") {
const std::vector<std::string> inputs = {
R"(command-name arg1 arg2)",
R"("command-name" "arg1" "arg2")",
R"(command-name "arg1" arg2)",
R"("command-name" arg1 arg2)",
R"("command-name""arg1""arg2")", // whitespace can be omitted between quoted arguments
};
for (std::string input : inputs) {
DYNAMIC_SECTION("input: " << input) {
cfgParser.parse_line(input, location);
REQUIRE(handler.history.size() == 1);
REQUIRE(handler.history[0].first == "command-name");
REQUIRE(handler.history[0].second == std::vector<std::string>({"arg1", "arg2"}));
}
}
}
}
TEST_CASE("parse_line() does not care about whitespace at start or end of line",
"[ConfigParser]")
{
ConfigParser cfgParser;
ConfigHandlerHistoryDummy handler;
cfgParser.register_handler("command-name", handler);
const std::string location = "dummy-location";
SECTION("no whitespace") {
cfgParser.parse_line("command-name arg1", location);
REQUIRE(handler.history.size() == 1);
REQUIRE(handler.history[0].first == "command-name");
REQUIRE(handler.history[0].second == std::vector<std::string>({"arg1"}));
}
SECTION("some whitespace") {
const std::vector<std::string> inputs = {
"\r\n\t command-name arg1",
"command-name arg1\r\n\t ",
"\r\n\t command-name\t\targ1\r\n\t ",
};
for (std::string input : inputs) {
DYNAMIC_SECTION("input: " << input) {
cfgParser.parse_line(input, location);
REQUIRE(handler.history.size() == 1);
REQUIRE(handler.history[0].first == "command-name");
REQUIRE(handler.history[0].second == std::vector<std::string>({"arg1"}));
}
}
}
}
TEST_CASE("parse_line() processes backslash escapes in quoted commands and arguments",
"[ConfigParser]")
{
ConfigParser cfgParser;
ConfigHandlerHistoryDummy handler;
cfgParser.register_handler("command", handler);
const std::string location = "dummy-location";
auto check_output = [&](const std::string& input,
const std::string& expected_command,
const std::vector<std::string>& expected_arguments) {
cfgParser.parse_line(input, location);
REQUIRE(handler.history.size() >= 1);
REQUIRE(handler.history.back().first == expected_command);
REQUIRE(handler.history.back().second == expected_arguments);
};
SECTION("escapes are handled when inside quoted string") {
check_output(R"(command "arg")", "command", {"arg"});
check_output(R"(command "arg\n")", "command", {"arg\n"});
check_output(R"(command "a\"r\"g")", "command", {R"(a"r"g)"});
}
SECTION("no escape handling outside of quoted parts of string") {
check_output(R"(command arg)", "command", {"arg"});
check_output(R"(command arg\n)", "command", {R"(arg\n)"});
check_output(R"(command \"arg)", "command", {R"(\"arg)"});
}
}
TEST_CASE("evaluate_backticks replaces command in backticks with its output",
"[ConfigParser]")
{
SECTION("substitutes command output") {
REQUIRE(ConfigParser::evaluate_backticks("") == "");
REQUIRE(ConfigParser::evaluate_backticks("hello world") ==
"hello world");
// backtick evaluation with true (empty string)
REQUIRE(ConfigParser::evaluate_backticks("foo`true`baz") ==
"foobaz");
// backtick evaluation with true (2)
REQUIRE(ConfigParser::evaluate_backticks("foo `true` baz") ==
"foo baz");
REQUIRE(ConfigParser::evaluate_backticks("foo`barbaz") ==
"foo`barbaz");
REQUIRE(ConfigParser::evaluate_backticks(
"foo `true` baz `xxx") == "foo baz `xxx");
REQUIRE(ConfigParser::evaluate_backticks(
"`echo hello world`") == "hello world");
REQUIRE(ConfigParser::evaluate_backticks("xxx`echo yyy`zzz") ==
"xxxyyyzzz");
REQUIRE(ConfigParser::evaluate_backticks(
"`seq 10 | tail -1`") == "10");
}
SECTION("subsistutes multiple shellouts") {
REQUIRE(ConfigParser::evaluate_backticks("xxx`echo aaa`yyy`echo bbb`zzz") ==
"xxxaaayyybbbzzz");
}
SECTION("backticks can be escaped with backslash") {
REQUIRE(ConfigParser::evaluate_backticks(
"hehe \\`two at a time\\`haha") ==
"hehe `two at a time`haha");
}
SECTION("single backticks have to be escaped too") {
REQUIRE(ConfigParser::evaluate_backticks(
"a single literal backtick: \\`") ==
"a single literal backtick: `");
}
SECTION("commands with space are evaluated by backticks") {
ConfigParser cfgparser;
KeyMap keys(KM_NEWSBOAT);
cfgparser.register_handler("bind-key", keys);
REQUIRE_NOTHROW(cfgparser.parse_file("data/config-space-backticks"));
REQUIRE(keys.get_operation(KeyCombination("s"), "feedlist") == OP_SORT);
}
SECTION("Unbalanced backtick does *not* start a command") {
const auto input1 = std::string("one `echo two three");
REQUIRE(ConfigParser::evaluate_backticks(input1) == input1);
const auto input2 = std::string("this `echo is a` test `here");
const auto expected2 = std::string("this is a test `here");
REQUIRE(ConfigParser::evaluate_backticks(input2) == expected2);
}
// One might think that putting one or both backticks inside a string will
// "escape" them, the same way as backslash does. But it doesn't, and
// shouldn't: when parsing a config, we need to evaluate *all* commands
// there are, no matter where they're placed.
SECTION("Backticks inside double quotes are not ignored") {
const auto input1 = std::string(R"#("`echo hello`")#");
REQUIRE(ConfigParser::evaluate_backticks(input1) == R"#("hello")#");
const auto input2 = std::string(R"#(a "b `echo c" d e` f)#");
// The line above asks the shell to run 'echo c" d e', which is an
// invalid command--the double quotes are not closed. The standard
// output of that command would be empty, so nothing will be inserted
// in place of backticks.
const auto expected2 = std::string(R"#(a "b f)#");
REQUIRE(ConfigParser::evaluate_backticks(input2) == expected2);
}
}
TEST_CASE("\"unbind-key -a\" removes all key bindings", "[ConfigParser]")
{
ConfigParser cfgparser;
SECTION("In all contexts by default") {
KeyMap keys(KM_NEWSBOAT);
cfgparser.register_handler("unbind-key", keys);
cfgparser.parse_file("data/config-unbind-all");
for (int i = OP_QUIT; i < OP_NB_MAX; ++i) {
REQUIRE(keys.get_keys(static_cast<Operation>(i),
"feedlist") == std::vector<KeyCombination>());
REQUIRE(keys.get_keys(static_cast<Operation>(i),
"podboat") == std::vector<KeyCombination>());
}
}
SECTION("For a specific context") {
KeyMap keys(KM_NEWSBOAT);
cfgparser.register_handler("unbind-key", keys);
cfgparser.parse_file("data/config-unbind-all-context");
INFO("it doesn't affect the help dialog");
KeyMap default_keys(KM_NEWSBOAT);
for (int i = OP_QUIT; i < OP_NB_MAX; ++i) {
const auto op = static_cast<Operation>(i);
REQUIRE(keys.get_keys(op, "help") == default_keys.get_keys(op, "help"));
}
for (int i = OP_QUIT; i < OP_NB_MAX; ++i) {
REQUIRE(keys.get_keys(static_cast<Operation>(i),
"article") == std::vector<KeyCombination>());
}
}
}
TEST_CASE("Concatenates lines that end with a backslash", "[ConfigParser]")
{
ConfigParser cfgparser;
KeyMap k(KM_NEWSBOAT);
cfgparser.register_handler("macro", k);
REQUIRE_NOTHROW(cfgparser.parse_file("data/config-multi-line"));
auto p_macro = k.get_macro(KeyCombination("p"));
REQUIRE(!p_macro.empty());
REQUIRE(p_macro[0].op == newsboat::OP_OPEN);
REQUIRE(p_macro[1].op == newsboat::OP_RELOAD);
REQUIRE(p_macro[2].op == newsboat::OP_QUIT);
REQUIRE(p_macro[3].op == newsboat::OP_QUIT);
auto contrived = k.get_macro(KeyCombination("j"));
REQUIRE(!contrived.empty());
REQUIRE(contrived[0].op == newsboat::OP_QUIT);
REQUIRE(contrived[1].op == newsboat::OP_QUIT);
REQUIRE(contrived[2].op == newsboat::OP_QUIT);
REQUIRE(contrived[3].op == newsboat::OP_QUIT);
}
TEST_CASE("`include` directive includes other config files", "[ConfigParser]")
{
// TODO: error messages should be more descriptive than "file couldn't be opened"
ConfigParser cfgparser;
SECTION("Errors if file is not found") {
REQUIRE_THROWS_AS(cfgparser.parse_file("data/config-missing-include"),
ConfigException);
}
SECTION("Errors on invalid UTF-8 in file") {
REQUIRE_THROWS_AS(cfgparser.parse_file("data/config-invalid-utf-8"),
ConfigException);
}
SECTION("Terminates on recursive include") {
REQUIRE_THROWS_AS(cfgparser.parse_file("data/config-recursive-include"),
ConfigException);
}
SECTION("Successfully includes existing file") {
REQUIRE_NOTHROW(cfgparser.parse_file("data/config-absolute-include"));
}
SECTION("Success on relative includes") {
REQUIRE_NOTHROW(cfgparser.parse_file("data/config-relative-include"));
}
SECTION("Diamond of death includes pass") {
REQUIRE_NOTHROW(cfgparser.parse_file("data/diamond-of-death/A"));
}
SECTION("File including itself only gets evaluated once") {
test_helpers::TempFile testfile;
test_helpers::EnvVar tmpfile("TMPFILE"); // $TMPFILE used in conf file
tmpfile.set(testfile.get_path());
// recursive includes don't fail
REQUIRE_NOTHROW(
cfgparser.parse_file("data/recursive-include-side-effect"));
// I think it will never get below here and fail? If it recurses, the above fails
int line_count = 0;
{
// from https://stackoverflow.com/a/19140230
std::ifstream in(testfile.get_path());
std::string line;
while (std::getline(in, line)) {
line_count++;
}
}
REQUIRE(line_count == 1); // only 1 line from date command
}
}
|