diff --git a/include/components/config.hpp b/include/components/config.hpp index 84bf1094..b3d0c1ee 100644 --- a/include/components/config.hpp +++ b/include/components/config.hpp @@ -18,19 +18,30 @@ POLYBAR_NS DEFINE_ERROR(value_error); DEFINE_ERROR(key_error); +using valuemap_t = std::unordered_map; +using sectionmap_t = std::map; +using file_list = vector; + class config { public: - using valuemap_t = std::unordered_map; - using sectionmap_t = std::map; - using make_type = const config&; static make_type make(string path = "", string bar = ""); - explicit config(const logger& logger, string&& path = "", string&& bar = ""); + explicit config(const logger& logger, string&& path = "", string&& bar = "") + : m_log(logger), m_file(move(path)), m_barname(move(bar)){}; - string filepath() const; + const string& filepath() const; string section() const; + /** + * \brief Instruct the config to connect to the xresource manager + */ + void use_xrm(); + + void set_sections(sectionmap_t sections); + + void set_included(file_list included); + void warn_deprecated(const string& section, const string& key, string replacement) const; /** @@ -193,7 +204,6 @@ class config { } protected: - void parse_file(); void copy_inherited(); template @@ -366,6 +376,12 @@ class config { string m_file; string m_barname; sectionmap_t m_sections{}; + + /** + * Absolute path of all files that were parsed in the process of parsing the + * config (Path of the main config file also included) + */ + file_list m_included; #if WITH_XRM unique_ptr m_xrm; #endif diff --git a/include/components/config_parser.hpp b/include/components/config_parser.hpp new file mode 100644 index 00000000..8388f16b --- /dev/null +++ b/include/components/config_parser.hpp @@ -0,0 +1,249 @@ +#pragma once + +#include + +#include "common.hpp" +#include "components/config.hpp" +#include "components/logger.hpp" +#include "errors.hpp" +#include "utils/file.hpp" +#include "utils/string.hpp" + +POLYBAR_NS + +DEFINE_ERROR(parser_error); + +/** + * \brief Exception object for syntax errors + * + * Contains filepath and line number where syntax error was found + */ +class syntax_error : public parser_error { + public: + /** + * Default values are used when the thrower doesn't know the position. + * parse_line has to catch, set the proper values and rethrow + */ + explicit syntax_error(string msg, const string& file = "", int line_no = -1) + : parser_error(file + ":" + to_string(line_no) + ": " + msg), msg(move(msg)) {} + + const string& get_msg() { + return msg; + }; + + private: + string msg; +}; + +class invalid_name_error : public syntax_error { + public: + /** + * type is either Header or Key + */ + invalid_name_error(const string& type, const string& name) + : syntax_error(type + " '" + name + "' contains forbidden characters.") {} +}; + +/** + * \brief All different types a line in a config can be + */ +enum class line_type { KEY, HEADER, COMMENT, EMPTY, UNKNOWN }; + +/** + * \brief Storage for a single config line + * + * More sanitized than the actual string of the comment line, with information + * about line type and structure + */ +struct line_t { + /** + * Whether or not this struct represents a "useful" line, a line that has + * any semantic significance (key-value or header line) + * If false all other fields are not set. + * Set this to false, if you want to return a line that has no effect + * (for example when you parse a comment line) + */ + bool useful; + + /** + * Index of the config_parser::files vector where this line is from + */ + int file_index; + int line_no; + + /** + * We access header, if is_header == true otherwise we access key, value + */ + bool is_header; + + /** + * Only set for header lines + */ + string header; + + /** + * Only set for key-value lines + */ + string key, value; +}; + +class config_parser { + public: + config_parser(const logger& logger, string&& file, string&& bar); + + /** + * \brief Performs the parsing of the main config file m_file + * + * \returns config class instance populated with the parsed config + * + * \throws syntax_error If there was any kind of syntax error + * \throws parser_error If aynthing else went wrong + */ + config::make_type parse(); + + protected: + /** + * \brief Converts the `lines` vector to a proper sectionmap + */ + sectionmap_t create_sectionmap(); + + /** + * \brief Parses the given file, extracts key-value pairs and section + * headers and adds them onto the `lines` vector + * + * This method directly resolves `include-file` directives and checks for + * cyclic dependencies + * + * `file` is expected to be an already resolved absolute path + */ + void parse_file(const string& file, file_list path); + + /** + * \brief Parses the given line string to create a line_t struct + * + * We use the INI file syntax (https://en.wikipedia.org/wiki/INI_file) + * Whitespaces (tested with isspace()) at the beginning and end of a line are ignored + * Keys and section names can contain any character except for the following: + * - spaces + * - equal sign (=) + * - semicolon (;) + * - pound sign (#) + * - Any kind of parentheses ([](){}) + * - colon (:) + * - period (.) + * - dollar sign ($) + * - backslash (\) + * - percent sign (%) + * - single and double quotes ('") + * So basically any character that has any kind of special meaning is prohibited. + * + * Comment lines have to start with a semicolon (;) or a pound sign (#), + * you cannot put a comment after another type of line. + * + * key and section names are case-sensitive. + * + * Keys are specified as `key = value`, spaces around the equal sign, as + * well as double quotes around the value are ignored + * + * sections are defined as [section], everything inside the square brackets is part of the name + * + * \throws syntax_error if the line isn't well formed. The syntax error + * does not contain the filename or line numbers because parse_line + * doesn't know about those. Whoever calls parse_line needs to + * catch those exceptions and set the file path and line number + */ + line_t parse_line(const string& line); + + /** + * \brief Determines the type of a line read from a config file + * + * Expects that line is trimmed + * This mainly looks at the first character and doesn't check if the line is + * actually syntactically correct. + * HEADER ('['), COMMENT (';' or '#') and EMPTY (None) are uniquely + * identified by their first character (or lack thereof). Any line that + * is none of the above and contains an equal sign, is treated as KEY. + * All others are UNKNOWN + */ + static line_type get_line_type(const string& line); + + /** + * \brief Parse a line containing a section header and returns the header name + * + * Only assumes that the line starts with '[' and is trimmed + * + * \throws syntax_error if the line doesn't end with ']' or the header name + * contains forbidden characters + */ + string parse_header(const string& line); + + /** + * \brief Parses a line containing a key-value pair and returns the key name + * and the value string inside an std::pair + * + * Only assumes that the line contains '=' at least once and is trimmed + * + * \throws syntax_error if the key contains forbidden characters + */ + std::pair parse_key(const string& line); + + /** + * \brief Name of all the files the config includes values from + * + * The line_t struct uses indices to this vector to map lines to their + * original files. This allows us to point the user to the exact location + * of errors + */ + file_list m_files; + + private: + /** + * \brief Checks if the given name doesn't contain any spaces or characters + * in config_parser::m_forbidden_chars + */ + bool is_valid_name(const string& name); + + /** + * \brief Whether or not an xresource manager should be used + * + * Is set to true if any ${xrdb...} references are found + */ + bool use_xrm{false}; + + const logger& m_log; + + /** + * \brief Absolute path to the main config file + */ + string m_config; + + /** + * Is used to resolve ${root...} references + */ + string m_barname; + + /** + * \brief List of all the lines in the config (with included files) + * + * The order here matters, as we have not yet associated key-value pairs + * with sections + */ + vector m_lines; + + /** + * \brief None of these characters can be used in the key and section names + */ + const string m_forbidden_chars{"\"'=;#[](){}:.$\\%"}; + + /** + * \brief List of names that cannot be used as section names + * + * These strings have a special meaning inside references and so the + * section [self] could never be referenced. + * + * Note: BAR is deprecated + */ + const std::set m_reserved_section_names = {"self", "BAR", "root"}; +}; + +POLYBAR_NS_END diff --git a/include/utils/string.hpp b/include/utils/string.hpp index b3c99641..abc073be 100644 --- a/include/utils/string.hpp +++ b/include/utils/string.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include "common.hpp" @@ -27,7 +26,7 @@ namespace { a.erase(a.size() - b.size()); } } -} +} // namespace class sstream { public: @@ -77,6 +76,10 @@ namespace string_util { string strip(const string& haystack, char needle); string strip_trailing_newline(const string& haystack); + string ltrim(string value, function pred); + string rtrim(string value, function pred); + string trim(string value, function pred); + string ltrim(string&& value, const char& needle = ' '); string rtrim(string&& value, const char& needle = ' '); string trim(string&& value, const char& needle = ' '); @@ -96,6 +99,6 @@ namespace string_util { string filesize(unsigned long long kbytes, size_t precision = 0, bool fixed = false, const string& locale = ""); hash_type hash(const string& src); -} +} // namespace string_util POLYBAR_NS_END diff --git a/src/components/config.cpp b/src/components/config.cpp index 2e29a5ad..f9bf299a 100644 --- a/src/components/config.cpp +++ b/src/components/config.cpp @@ -6,8 +6,6 @@ #include "utils/color.hpp" #include "utils/env.hpp" #include "utils/factory.hpp" -#include "utils/file.hpp" -#include "utils/math.hpp" #include "utils/string.hpp" POLYBAR_NS @@ -19,39 +17,10 @@ config::make_type config::make(string path, string bar) { return *factory_util::singleton>(logger::make(), move(path), move(bar)); } -/** - * Construct config object - */ -config::config(const logger& logger, string&& path, string&& bar) - : m_log(logger), m_file(forward(path)), m_barname(forward(bar)) { - if (!file_util::exists(m_file)) { - throw application_error("Could not find config file: " + m_file); - } - - m_log.info("Loading config: %s", m_file); - - parse_file(); - copy_inherited(); - - bool found_bar{false}; - for (auto&& p : m_sections) { - if (p.first == section()) { - found_bar = true; - break; - } - } - - if (!found_bar) { - throw application_error("Undefined bar: " + m_barname); - } - - m_log.trace("config: Current bar section: [%s]", section()); -} - /** * Get path of loaded file */ -string config::filepath() const { +const string& config::filepath() const { return m_file; } @@ -62,6 +31,28 @@ string config::section() const { return "bar/" + m_barname; } +void config::use_xrm() { +#if WITH_XRM + /* + * Initialize the xresource manager if there are any xrdb refs + * present in the configuration + */ + if (!m_xrm) { + m_log.info("Enabling xresource manager"); + m_xrm.reset(new xresource_manager{connection::make()}); + } +#endif +} + +void config::set_sections(sectionmap_t sections) { + m_sections = move(sections); + copy_inherited(); +} + +void config::set_included(file_list included) { + m_included = move(included); +} + /** * Print a deprecation warning if the given parameter is set */ @@ -74,117 +65,17 @@ void config::warn_deprecated(const string& section, const string& key, string re } } -/** - * Parse key/value pairs from the configuration file - */ -void config::parse_file() { - vector> lines; - vector files{m_file}; - - std::function pushline = [&](int lineno, string&& line) { - // Ignore empty lines and comments - if (line.empty() || line[0] == ';' || line[0] == '#') { - return; - } - - string key, value; - string::size_type pos; - - // Filter lines by: - // - key/value pairs - // - section headers - if ((pos = line.find('=')) != string::npos) { - key = forward(string_util::trim(forward(line.substr(0, pos)))); - value = forward(string_util::trim(line.substr(pos + 1))); - } else if (line[0] != '[' || line[line.length() - 1] != ']') { - return; - } - - if (key == "include-file") { - auto file_path = file_util::expand(value); - if (file_path.empty() || !file_util::exists(file_path)) { - throw value_error("Invalid include file \"" + file_path + "\" defined on line " + to_string(lineno)); - } - if (std::find(files.begin(), files.end(), file_path) != files.end()) { - throw value_error("Recursive include file \"" + file_path + "\""); - } - files.push_back(file_util::expand(file_path)); - m_log.trace("config: Including file \"%s\"", file_path); - for (auto&& l : string_util::split(file_util::contents(file_path), '\n')) { - pushline(lineno, forward(l)); - } - files.pop_back(); - } else { - lines.emplace_back(make_pair(lineno, line)); - } - }; - - int lineno{0}; - string line; - std::ifstream in(m_file); - while (std::getline(in, line)) { - pushline(++lineno, string_util::replace_all(line, "\t", "")); - } - - string section; - for (auto&& l : lines) { - auto& lineno = l.first; - auto& line = l.second; - - // New section - if (line[0] == '[' && line[line.length() - 1] == ']') { - section = line.substr(1, line.length() - 2); - continue; - } else if (section.empty()) { - continue; - } - - size_t equal_pos; - - // Check for key-value pair equal sign - if ((equal_pos = line.find('=')) == string::npos) { - continue; - } - - string key{forward(string_util::trim(forward(line.substr(0, equal_pos))))}; - string value; - - auto it = m_sections[section].find(key); - if (it != m_sections[section].end()) { - throw key_error("Duplicate key name \"" + key + "\" defined on line " + to_string(lineno)); - } - - if (equal_pos + 1 < line.size()) { - value = forward(string_util::trim(line.substr(equal_pos + 1))); - size_t len{value.size()}; - if (len > 2 && value[0] == '"' && value[len - 1] == '"') { - value.erase(len - 1, 1).erase(0, 1); - } - } - -#if WITH_XRM - // Initialize the xresource manage if there are any xrdb refs - // present in the configuration - if (!m_xrm && value.find("${xrdb") != string::npos) { - m_xrm.reset(new xresource_manager{connection::make()}); - } -#endif - - m_sections[section].emplace_hint(it, move(key), move(value)); - } -} - /** * Look for sections set up to inherit from a base section * and copy the missing parameters * - * [sub/seciton] + * [sub/section] * inherit = base/section */ void config::copy_inherited() { for (auto&& section : m_sections) { for (auto&& param : section.second) { - if (param.first.find("inherit") == 0) { + if (param.first == "inherit") { // Get name of base section auto inherit = param.second; if ((inherit = dereference(section.first, param.first, inherit, inherit)).empty()) { @@ -199,10 +90,12 @@ void config::copy_inherited() { m_log.trace("config: Copying missing params (sub=\"%s\", base=\"%s\")", section.first, inherit); - // Iterate the base and copy the parameters - // that hasn't been defined for the sub-section + /* + * Iterate the base and copy the parameters that haven't been defined + * for the sub-section + */ for (auto&& base_param : base_section->second) { - section.second.insert(make_pair(base_param.first, base_param.second)); + section.second.emplace(base_param.first, base_param.second); } } } diff --git a/src/components/config_parser.cpp b/src/components/config_parser.cpp new file mode 100644 index 00000000..afb6c7f5 --- /dev/null +++ b/src/components/config_parser.cpp @@ -0,0 +1,269 @@ +#include +#include + +#include "components/config_parser.hpp" + +POLYBAR_NS + +config_parser::config_parser(const logger& logger, string&& file, string&& bar) + : m_log(logger), m_config(file_util::expand(file)), m_barname(move(bar)) {} + +config::make_type config_parser::parse() { + m_log.info("Parsing config file: %s", m_config); + + parse_file(m_config, {}); + + sectionmap_t sections = create_sectionmap(); + + if (sections.find("bar/" + m_barname) == sections.end()) { + throw application_error("Undefined bar: " + m_barname); + } + + /* + * The first element in the files vector is always the main config file and + * because it has unique filenames, we can use all the elements from the + * second element onwards for the included list + */ + file_list included(m_files.begin() + 1, m_files.end()); + config::make_type result = config::make(m_config, m_barname); + + // Cast to non-const to set sections, included and xrm + config& m_conf = const_cast(result); + + m_conf.set_sections(move(sections)); + m_conf.set_included(move(included)); + if (use_xrm) { + m_conf.use_xrm(); + } + + return result; +} + +sectionmap_t config_parser::create_sectionmap() { + sectionmap_t sections{}; + + string current_section{}; + + for (const line_t& line : m_lines) { + if (!line.useful) { + continue; + } + + if (line.is_header) { + current_section = line.header; + } else { + // The first valid line in the config is not a section definition + if (current_section.empty()) { + throw syntax_error("First valid line in config must be section header", m_files[line.file_index], line.line_no); + } + + const string& key = line.key; + const string& value = line.value; + + valuemap_t& valuemap = sections[current_section]; + + if (valuemap.find(key) == valuemap.end()) { + valuemap.emplace(key, value); + } else { + // Key already exists in this section + throw syntax_error("Duplicate key name \"" + key + "\" defined in section \"" + current_section + "\"", + m_files[line.file_index], line.line_no); + } + } + } + + return sections; +} + +void config_parser::parse_file(const string& file, file_list path) { + if (std::find(path.begin(), path.end(), file) != path.end()) { + string path_str{}; + + for (const auto& p : path) { + path_str += ">\t" + p + "\n"; + } + + path_str += ">\t" + file; + + // We have already parsed this file in this path, so there are cyclic dependencies + throw application_error("include-file: Dependency cycle detected:\n" + path_str); + } + + m_log.trace("config_parser: Parsing %s", file); + + int file_index; + + auto found = std::find(m_files.begin(), m_files.end(), file); + + if (found == m_files.end()) { + file_index = m_files.size(); + m_files.push_back(file); + } else { + /* + * `file` is already in the `files` vector so we calculate its index. + * + * This means that the file was already parsed, this can happen without + * cyclic dependencies, if the file is included twice + */ + file_index = found - m_files.begin(); + } + + path.push_back(file); + + int line_no = 0; + + string line_str{}; + + std::ifstream in(file); + + if (!in) { + throw application_error("Failed to open config file " + file + ": " + strerror(errno)); + } + + while (std::getline(in, line_str)) { + line_no++; + line_t line; + try { + line = parse_line(line_str); + + // parse_line doesn't set these + line.file_index = file_index; + line.line_no = line_no; + } catch (syntax_error& err) { + /* + * Exceptions thrown by parse_line doesn't have the line + * numbers and files set, so we have to add them here + */ + throw syntax_error(err.get_msg(), m_files[file_index], line_no); + } + + // Skip useless lines (comments, empty lines) + if (!line.useful) { + continue; + } + + if (!line.is_header && line.key == "include-file") { + parse_file(file_util::expand(line.value), path); + } else { + m_lines.push_back(line); + } + } +} + +line_t config_parser::parse_line(const string& line) { + string line_trimmed = string_util::trim(line, isspace); + line_type type = get_line_type(line_trimmed); + + line_t result = {}; + + if (type == line_type::EMPTY || type == line_type::COMMENT) { + result.useful = false; + return result; + } + + if (type == line_type::UNKNOWN) { + throw syntax_error("Unknown line type: " + line_trimmed); + } + + result.useful = true; + + if (type == line_type::HEADER) { + result.is_header = true; + result.header = parse_header(line_trimmed); + } else if (type == line_type::KEY) { + result.is_header = false; + auto key_value = parse_key(line_trimmed); + result.key = key_value.first; + result.value = key_value.second; + } + + return result; +} + +line_type config_parser::get_line_type(const string& line) { + if (line.empty()) { + return line_type::EMPTY; + } + + switch (line[0]) { + case '[': + return line_type::HEADER; + + case ';': + case '#': + return line_type::COMMENT; + + default: + if (string_util::contains(line, "=")) { + return line_type::KEY; + } else { + return line_type::UNKNOWN; + } + } +} + +string config_parser::parse_header(const string& line) { + if (line.back() != ']') { + throw syntax_error("Missing ']' in header '" + line + "'"); + } + + // Stripping square brackets + string header = line.substr(1, line.size() - 2); + + if (!is_valid_name(header)) { + throw invalid_name_error("Section", header); + } + + if (m_reserved_section_names.find(header) != m_reserved_section_names.end()) { + throw syntax_error("'" + header + "' is reserved and cannot be used as a section name"); + } + + return header; +} + +std::pair config_parser::parse_key(const string& line) { + size_t pos = line.find_first_of('='); + + string key = string_util::trim(line.substr(0, pos), isspace); + string value = string_util::trim(line.substr(pos + 1), isspace); + + if (!is_valid_name(key)) { + throw invalid_name_error("Key", key); + } + + /* + * Only if the string is surrounded with double quotes, do we treat them + * not as part of the value and remove them. + */ + if (value.size() >= 2 && value.front() == '"' && value.back() == '"') { + value = value.substr(1, value.size() - 2); + } + + // TODO check value for references + +#if WITH_XRM + // Use xrm, if at least one value is an xrdb reference + if (!use_xrm && value.find("${xrdb") == 0) { + use_xrm = true; + } +#endif + + return {move(key), move(value)}; +} + +bool config_parser::is_valid_name(const string& name) { + if (name.empty()) { + return false; + } + + for (const char c : name) { + // Names with forbidden chars or spaces are not valid + if (isspace(c) || m_forbidden_chars.find_first_of(c) != string::npos) { + return false; + } + } + + return true; +} + +POLYBAR_NS_END diff --git a/src/main.cpp b/src/main.cpp index 7df263ae..f0048310 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,7 @@ #include "components/bar.hpp" #include "components/command_line.hpp" #include "components/config.hpp" +#include "components/config_parser.hpp" #include "components/controller.hpp" #include "components/ipc.hpp" #include "utils/env.hpp" @@ -81,7 +82,8 @@ int main(int argc, char** argv) { printf("%s: %ix%i+%i+%i (XRandR monitor%s)\n", mon->name.c_str(), mon->w, mon->h, mon->x, mon->y, mon->primary ? ", primary" : ""); } else { - printf("%s: %ix%i+%i+%i%s\n", mon->name.c_str(), mon->w, mon->h, mon->x, mon->y, mon->primary ? " (primary)" : ""); + printf("%s: %ix%i+%i+%i%s\n", mon->name.c_str(), mon->w, mon->h, mon->x, mon->y, + mon->primary ? " (primary)" : ""); } } return EXIT_SUCCESS; @@ -112,7 +114,8 @@ int main(int argc, char** argv) { throw application_error("Define configuration using --config=PATH"); } - config::make_type conf{config::make(move(confpath), cli->get(0))}; + config_parser parser{logger, move(confpath), cli->get(0)}; + config::make_type conf = parser.parse(); //================================================== // Dump requested data diff --git a/src/utils/string.cpp b/src/utils/string.cpp index 8c855b12..cf6d2622 100644 --- a/src/utils/string.cpp +++ b/src/utils/string.cpp @@ -1,5 +1,4 @@ #include -#include #include #include #include @@ -110,6 +109,29 @@ namespace string_util { return str; } + /** + * Trims all characters that match pred from the left + */ + string ltrim(string value, function pred) { + value.erase(value.begin(), find_if(value.begin(), value.end(), not1(pred))); + return value; + } + + /** + * Trims all characters that match pred from the right + */ + string rtrim(string value, function pred) { + value.erase(find_if(value.rbegin(), value.rend(), not1(pred)).base(), value.end()); + return value; + } + + /** + * Trims all characters that match pred from both sides + */ + string trim(string value, function pred) { + return ltrim(rtrim(move(value), pred), pred); + } + /** * Remove needle from the start of the string */ @@ -275,6 +297,6 @@ namespace string_util { hash_type hash(const string& src) { return std::hash()(src); } -} +} // namespace string_util POLYBAR_NS_END diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e787fa82..a09d2907 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -57,6 +57,7 @@ add_unit_test(components/command_line) add_unit_test(components/bar) add_unit_test(components/builder) add_unit_test(components/parser) +add_unit_test(components/config_parser) # Run make check to build and run all unit tests add_custom_target(check diff --git a/tests/unit_tests/components/config_parser.cpp b/tests/unit_tests/components/config_parser.cpp new file mode 100644 index 00000000..7766f7d4 --- /dev/null +++ b/tests/unit_tests/components/config_parser.cpp @@ -0,0 +1,236 @@ +#include "components/config_parser.hpp" +#include "common/test.hpp" +#include "components/logger.hpp" + +using namespace polybar; +using namespace std; + +/** + * \brief Testing-only subclass of config_parser to change access level + */ +class TestableConfigParser : public config_parser { + using config_parser::config_parser; + + public: + using config_parser::get_line_type; + + public: + using config_parser::parse_key; + + public: + using config_parser::parse_header; + + public: + using config_parser::parse_line; + + public: + using config_parser::m_files; +}; + +/** + * \brief Fixture class + */ +class ConfigParser : public ::testing::Test { + protected: + unique_ptr parser = + make_unique(logger(loglevel::NONE), "/dev/zero", "TEST"); +}; + +// ParseLineTest {{{ +class ParseLineInValidTest : public ConfigParser, public ::testing::WithParamInterface {}; + +class ParseLineHeaderTest : public ConfigParser, public ::testing::WithParamInterface> {}; + +class ParseLineKeyTest : public ConfigParser, + public ::testing::WithParamInterface, string>> {}; + +vector parse_line_invalid_list = { + " # comment", + "; comment", + "\t#", + "", + " ", + "\t ", +}; + +vector> parse_line_header_list = { + {"section", "\t[section]"}, + {"section", "\t[section] "}, + {"bar/name", "\t[bar/name] "}, +}; + +vector, string>> parse_line_key_list = { + {{"key", "value"}, " key = value"}, + {{"key", ""}, " key\t = \"\""}, + {{"key", "\""}, " key\t = \"\"\""}, +}; + +INSTANTIATE_TEST_SUITE_P(Inst, ParseLineInValidTest, ::testing::ValuesIn(parse_line_invalid_list)); + +TEST_P(ParseLineInValidTest, correctness) { + line_t line = parser->parse_line(GetParam()); + + EXPECT_FALSE(line.useful); +} + +INSTANTIATE_TEST_SUITE_P(Inst, ParseLineHeaderTest, ::testing::ValuesIn(parse_line_header_list)); + +TEST_P(ParseLineHeaderTest, correctness) { + line_t line = parser->parse_line(GetParam().second); + + EXPECT_TRUE(line.useful); + + EXPECT_TRUE(line.is_header); + EXPECT_EQ(GetParam().first, line.header); +} + +INSTANTIATE_TEST_SUITE_P(Inst, ParseLineKeyTest, ::testing::ValuesIn(parse_line_key_list)); + +TEST_P(ParseLineKeyTest, correctness) { + line_t line = parser->parse_line(GetParam().second); + + EXPECT_TRUE(line.useful); + + EXPECT_FALSE(line.is_header); + EXPECT_EQ(GetParam().first.first, line.key); + EXPECT_EQ(GetParam().first.second, line.value); +} + +TEST_F(ParseLineInValidTest, throwsSyntaxError) { + EXPECT_THROW(parser->parse_line("unknown"), syntax_error); +} +// }}} + +// GetLineTypeTest {{{ + +/** + * \brief Class for parameterized tests on get_line_type + * + * Parameters are pairs of the expected line type and a string that should be + * detected as that line type + */ +class GetLineTypeTest : public ConfigParser, public ::testing::WithParamInterface> {}; + +/** + * \brief Helper function generate GetLineTypeTest parameter values + */ +vector> line_type_transform(vector&& in, line_type type) { + vector> out; + + out.reserve(in.size()); + for (const auto& i : in) { + out.emplace_back(type, i); + } + + return out; +} + +/** + * \brief Parameter values for GetLineTypeTest + */ +auto line_type_key = line_type_transform({"a = b", " a =b", " a\t =\t \t b", "a = "}, line_type::KEY); +auto line_type_header = line_type_transform({"[section]", "[section]", "[section/sub]"}, line_type::HEADER); +auto line_type_comment = line_type_transform({";abc", "#abc", ";", "#"}, line_type::COMMENT); +auto line_type_empty = line_type_transform({""}, line_type::EMPTY); +auto line_type_unknown = line_type_transform({"|a", " |a", "a"}, line_type::UNKNOWN); + +/** + * Instantiate GetLineTypeTest for the different line types + */ +INSTANTIATE_TEST_SUITE_P(LineTypeKey, GetLineTypeTest, ::testing::ValuesIn(line_type_key)); +INSTANTIATE_TEST_SUITE_P(LineTypeHeader, GetLineTypeTest, ::testing::ValuesIn(line_type_header)); +INSTANTIATE_TEST_SUITE_P(LineTypeComment, GetLineTypeTest, ::testing::ValuesIn(line_type_comment)); +INSTANTIATE_TEST_SUITE_P(LineTypeEmpty, GetLineTypeTest, ::testing::ValuesIn(line_type_empty)); +INSTANTIATE_TEST_SUITE_P(LineTypeUnknown, GetLineTypeTest, ::testing::ValuesIn(line_type_unknown)); + +/** + * \brief Parameterized test for get_line_type + */ +TEST_P(GetLineTypeTest, correctness) { + EXPECT_EQ(GetParam().first, parser->get_line_type(GetParam().second)); +} + +// }}} + +// ParseKeyTest {{{ + +/** + * \brief Class for parameterized tests on parse_key + * + * The first element of the pair is the expected key-value pair and the second + * element is the string to be parsed, has to be trimmed and valid. + */ +class ParseKeyTest : public ConfigParser, public ::testing::WithParamInterface, string>> {}; + +vector, string>> parse_key_list = { + {{"key", "value"}, "key = value"}, + {{"key", "value"}, "key=value"}, + {{"key", "value"}, "key =\"value\""}, + {{"key", "value"}, "key\t=\t \"value\""}, + {{"key", "\"value"}, "key = \"value"}, + {{"key", "value\""}, "key = value\""}, + {{"key", "= value"}, "key == value"}, + {{"key", ""}, "key ="}, + {{"key", ""}, R"(key ="")"}, + {{"key", "\"\""}, R"(key ="""")"}, +}; + +INSTANTIATE_TEST_SUITE_P(Inst, ParseKeyTest, ::testing::ValuesIn(parse_key_list)); + +/** + * Parameterized test for parse_key with valid line + */ +TEST_P(ParseKeyTest, correctness) { + EXPECT_EQ(GetParam().first, parser->parse_key(GetParam().second)); +} + +/** + * Tests if exception is thrown for invalid key line + */ +TEST_F(ParseKeyTest, throwsSyntaxError) { + EXPECT_THROW(parser->parse_key("= empty name"), syntax_error); + EXPECT_THROW(parser->parse_key("forbidden char = value"), syntax_error); + EXPECT_THROW(parser->parse_key("forbidden\tchar = value"), syntax_error); +} +// }}} + +// ParseHeaderTest {{{ + +/** + * \brief Class for parameterized tests on parse_key + * + * The first element of the pair is the expected key-value pair and the second + * element is the string to be parsed, has to be trimmed and valid + */ +class ParseHeaderTest : public ConfigParser, public ::testing::WithParamInterface> {}; + +vector> parse_header_list = { + {"section", "[section]"}, + {"bar/name", "[bar/name]"}, + {"with_underscore", "[with_underscore]"}, +}; + +INSTANTIATE_TEST_SUITE_P(Inst, ParseHeaderTest, ::testing::ValuesIn(parse_header_list)); + +/** + * Parameterized test for parse_header with valid line + */ +TEST_P(ParseHeaderTest, correctness) { + EXPECT_EQ(GetParam().first, parser->parse_header(GetParam().second)); +} + +/** + * Tests if exception is thrown for invalid header line + */ +TEST_F(ParseHeaderTest, throwsSyntaxError) { + EXPECT_THROW(parser->parse_header("[]"), syntax_error); + EXPECT_THROW(parser->parse_header("[no_end"), syntax_error); + EXPECT_THROW(parser->parse_header("[forbidden char]"), syntax_error); + EXPECT_THROW(parser->parse_header("[forbidden\tchar]"), syntax_error); + + // Reserved names + EXPECT_THROW(parser->parse_header("[self]"), syntax_error); + EXPECT_THROW(parser->parse_header("[BAR]"), syntax_error); + EXPECT_THROW(parser->parse_header("[root]"), syntax_error); +} +// }}} diff --git a/tests/unit_tests/utils/string.cpp b/tests/unit_tests/utils/string.cpp index 48fad050..b74f3296 100644 --- a/tests/unit_tests/utils/string.cpp +++ b/tests/unit_tests/utils/string.cpp @@ -1,7 +1,5 @@ -#include - -#include "common/test.hpp" #include "utils/string.hpp" +#include "common/test.hpp" using namespace polybar; @@ -57,6 +55,11 @@ TEST(String, trim) { EXPECT_EQ("test", string_util::trim("xxtestxx", 'x')); } +TEST(String, trimPredicate) { + EXPECT_EQ("x\t x", string_util::trim("\t x\t x ", isspace)); + EXPECT_EQ("x\t x", string_util::trim("x\t x ", isspace)); +} + TEST(String, join) { EXPECT_EQ("A, B, C", string_util::join({"A", "B", "C"}, ", ")); }