summaryrefslogtreecommitdiff
path: root/test/configparser.cpp
blob: f50625d82439c5998f0f207bd134106e83fcf788 (plain) (blame)
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
#include "configparser.h"

#include "3rd-party/catch.hpp"
#include "test-helpers.h"
#include "keymap.h"

using namespace newsboat;

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("data/config-space-backticks"));
		REQUIRE_FALSE(keys.get_operation("s", "all") == OP_NIL);
	}

	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("data/config-unbind-all");

		for (int i = OP_QUIT; i < OP_NB_MAX; ++i) {
			REQUIRE(keys.getkey(static_cast<Operation>(i), "all") == "<none>");
		}
	}

	SECTION("For a specific context")
	{
		KeyMap keys(KM_NEWSBOAT);
		cfgparser.register_handler("unbind-key", &keys);
		cfgparser.parse("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.getkey(op, "help") == default_keys.getkey(op, "help"));
		}

		for (int i = OP_QUIT; i < OP_NB_MAX; ++i) {
			REQUIRE(keys.getkey(static_cast<Operation>(i), "article") == "<none>");
		}
	}
}

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 on not found file")
	{
		REQUIRE_THROWS(cfgparser.parse("data/config-missing-include"));
	}
	SECTION("Terminates on recursive include")
	{
		REQUIRE_THROWS(cfgparser.parse("data/config-recursive-include"));
	}
	SECTION("Successfully includes existing file")
	{
		REQUIRE_NOTHROW(cfgparser.parse("data/config-absolute-include"));
	}
	SECTION("Success on relative includes")
	{
		REQUIRE_NOTHROW(cfgparser.parse("data/config-relative-include"));
	}
	SECTION("Diamond of death includes pass") {
		REQUIRE_NOTHROW(cfgparser.parse("data/diamond-of-death/A"));
	}
	SECTION("File including itself only gets evaluated once") {
		TestHelpers::TempFile testfile;
		TestHelpers::EnvVar tmpfile("TMPFILE"); // $TMPFILE used in conf file
		tmpfile.set(testfile.get_path());

		REQUIRE_NOTHROW(cfgparser.parse("data/recursive-include-side-effect")); // recursive includes don't fail
		// 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
	}
}