diff options
author | Julian Andres Klode <jak@debian.org> | 2020-02-03 13:26:41 +0000 |
---|---|---|
committer | Julian Andres Klode <jak@debian.org> | 2020-02-03 13:26:41 +0000 |
commit | 3ee6fdc08a91ef65e5a69f8857de3bf86a8fe1ad (patch) | |
tree | d7f2bed62c67b05e01095865056634db0f9b6be7 | |
parent | efc52f1fc9acb6ec815f695506bc8d5045c76834 (diff) | |
parent | 404771d0ec11f26a0b631018719e2918a049455b (diff) |
Merge branch 'pu/short-patterns' into 'master'
Implement short patterns
See merge request apt-team/apt!100
-rw-r--r-- | apt-pkg/cachefilter-patterns.cc | 245 | ||||
-rw-r--r-- | apt-pkg/cachefilter-patterns.h | 15 | ||||
-rw-r--r-- | apt-pkg/cacheset.cc | 2 | ||||
-rw-r--r-- | apt-pkg/contrib/string_view.h | 5 | ||||
-rw-r--r-- | apt-private/private-list.cc | 4 | ||||
-rw-r--r-- | doc/apt-patterns.7.xml | 53 | ||||
-rw-r--r-- | test/libapt/pattern_test.cc | 139 |
7 files changed, 399 insertions, 64 deletions
diff --git a/apt-pkg/cachefilter-patterns.cc b/apt-pkg/cachefilter-patterns.cc index a9f76ff1d..5a58a9767 100644 --- a/apt-pkg/cachefilter-patterns.cc +++ b/apt-pkg/cachefilter-patterns.cc @@ -17,6 +17,32 @@ namespace APT namespace Internal { +static const constexpr struct +{ + APT::StringView shortName; + APT::StringView longName; + bool takesArgument; +} shortPatterns[] = { + {"r"_sv, "?architecture"_sv, true}, + {"A"_sv, "?archive"_sv, true}, + {"M"_sv, "?automatic"_sv, false}, + {"b"_sv, "?broken"_sv, false}, + {"c"_sv, "?config-files"_sv, false}, + {"E"_sv, "?essential"_sv, false}, + {"F"_sv, "?false"_sv, false}, + {"g"_sv, "?garbage"_sv, false}, + {"i"_sv, "?installed"_sv, false}, + {"n"_sv, "?name"_sv, true}, + {"o"_sv, "?obsolete"_sv, false}, + {"O"_sv, "?origin"_sv, true}, + {"s"_sv, "?section"_sv, true}, + {"e"_sv, "?source-package"_sv, true}, + {"T"_sv, "?true"_sv, false}, + {"U"_sv, "?upgradable"_sv, false}, + {"V"_sv, "?version"_sv, true}, + {"v"_sv, "?virtual"_sv, false}, +}; + template <class... Args> std::string rstrprintf(Args... args) { @@ -32,41 +58,197 @@ std::unique_ptr<PatternTreeParser::Node> PatternTreeParser::parseTop() auto node = parse(); skipSpace(); + if (node == nullptr) + throw Error{Node{0, sentence.size()}, "Expected pattern"}; + if (node->end != sentence.size()) + throw Error{Node{node->end, sentence.size()}, "Expected end of file"}; + + return node; +} + +// Parse any pattern +std::unique_ptr<PatternTreeParser::Node> PatternTreeParser::parse() +{ + return parseOr(); +} + +std::unique_ptr<PatternTreeParser::Node> PatternTreeParser::parseOr() +{ + auto start = state.offset; + std::vector<std::unique_ptr<PatternTreeParser::Node>> nodes; + + auto firstNode = parseAnd(); + + if (firstNode == nullptr) + return nullptr; + + nodes.push_back(std::move(firstNode)); + for (skipSpace(); sentence[state.offset] == '|'; skipSpace()) + { + state.offset++; + skipSpace(); + auto node = parseAnd(); + + if (node == nullptr) + throw Error{Node{state.offset, sentence.size()}, "Expected pattern after |"}; + + nodes.push_back(std::move(node)); + } + + if (nodes.size() == 0) + return nullptr; + if (nodes.size() == 1) + return std::move(nodes[0]); + + auto node = std::make_unique<PatternNode>(); + node->start = start; + node->end = nodes[nodes.size() - 1]->end; + node->term = "?or"; + node->arguments = std::move(nodes); + node->haveArgumentList = true; + + return node; +} + +std::unique_ptr<PatternTreeParser::Node> PatternTreeParser::parseAnd() +{ + auto start = state.offset; + std::vector<std::unique_ptr<PatternTreeParser::Node>> nodes; + + for (skipSpace(); state.offset < sentence.size(); skipSpace()) { - Node node2; + auto node = parseUnary(); + + if (node == nullptr) + break; - node2.start = node->end; - node2.end = sentence.size(); - throw Error{node2, "Expected end of file"}; + nodes.push_back(std::move(node)); } + if (nodes.size() == 0) + return nullptr; + if (nodes.size() == 1) + return std::move(nodes[0]); + + auto node = std::make_unique<PatternNode>(); + node->start = start; + node->end = nodes[nodes.size() - 1]->end; + node->term = "?and"; + node->arguments = std::move(nodes); + node->haveArgumentList = true; + return node; } -// Parse any pattern -std::unique_ptr<PatternTreeParser::Node> PatternTreeParser::parse() +std::unique_ptr<PatternTreeParser::Node> PatternTreeParser::parseUnary() +{ + + if (sentence[state.offset] != '!') + return parsePrimary(); + + auto start = ++state.offset; + auto primary = parsePrimary(); + + if (primary == nullptr) + throw Error{Node{start, sentence.size()}, "Expected pattern"}; + + auto node = std::make_unique<PatternNode>(); + node->start = start; + node->end = primary->end; + node->term = "?not"; + node->arguments.push_back(std::move(primary)); + node->haveArgumentList = true; + return node; +} + +std::unique_ptr<PatternTreeParser::Node> PatternTreeParser::parsePrimary() { std::unique_ptr<Node> node; + if ((node = parseShortPattern()) != nullptr) + return node; if ((node = parsePattern()) != nullptr) return node; + if ((node = parseGroup()) != nullptr) + return node; + + return nullptr; +} + +std::unique_ptr<PatternTreeParser::Node> PatternTreeParser::parseGroup() +{ + if (sentence[state.offset] != '(') + return nullptr; + + auto start = state.offset++; + + skipSpace(); + auto node = parse(); + if (node == nullptr) + throw Error{Node{state.offset, sentence.size()}, + "Expected pattern after '('"}; + skipSpace(); + + if (sentence[state.offset] != ')') + throw Error{Node{state.offset, sentence.size()}, + "Expected closing parenthesis"}; + + auto end = ++state.offset; + node->start = start; + node->end = end; + return node; +} + +std::unique_ptr<PatternTreeParser::Node> PatternTreeParser::parseArgument(bool shrt) +{ + std::unique_ptr<Node> node; if ((node = parseQuotedWord()) != nullptr) return node; - if ((node = parseWord()) != nullptr) + if ((node = parseWord(shrt)) != nullptr) + return node; + if ((node = parse()) != nullptr) + return node; + + throw Error{Node{state.offset, sentence.size()}, + "Expected pattern, quoted word, or word"}; +} + +// Parse a short pattern +std::unique_ptr<PatternTreeParser::Node> PatternTreeParser::parseShortPattern() +{ + if (sentence[state.offset] != '~') + return nullptr; + + for (auto &sp : shortPatterns) + { + if (sentence.substr(state.offset + 1, sp.shortName.size()) != sp.shortName) + continue; + + auto node = std::make_unique<PatternNode>(); + node->end = node->start = state.offset; + node->term = sp.longName; + + state.offset += sp.shortName.size() + 1; + if (sp.takesArgument) + { + node->arguments.push_back(parseArgument(true)); + node->haveArgumentList = true; + } + node->end = state.offset; + return node; + } - Node eNode; - eNode.end = eNode.start = state.offset; - throw Error{eNode, "Expected pattern, quoted word, or word"}; + throw Error{Node{state.offset, sentence.size()}, "Unknown short pattern"}; } // Parse a list pattern (or function call pattern) std::unique_ptr<PatternTreeParser::Node> PatternTreeParser::parsePattern() { - static const APT::StringView CHARS("0123456789" - "abcdefghijklmnopqrstuvwxyz" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "-"); + static constexpr auto CHARS = ("0123456789" + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "-"_sv); if (sentence[state.offset] != '?') return nullptr; @@ -81,6 +263,9 @@ std::unique_ptr<PatternTreeParser::Node> PatternTreeParser::parsePattern() node->term = sentence.substr(node->start, state.offset - node->start); + if (node->term.size() <= 1) + throw Error{*node, "Pattern must have a term/name"}; + node->end = skipSpace(); // We don't have any arguments, return node; if (sentence[state.offset] != '(') @@ -97,7 +282,7 @@ std::unique_ptr<PatternTreeParser::Node> PatternTreeParser::parsePattern() return node; } - node->arguments.push_back(parse()); + node->arguments.push_back(parseArgument(false)); skipSpace(); while (sentence[state.offset] == ',') { @@ -106,7 +291,7 @@ std::unique_ptr<PatternTreeParser::Node> PatternTreeParser::parsePattern() // This was a trailing comma - allow it and break the loop if (sentence[state.offset] == ')') break; - node->arguments.push_back(parse()); + node->arguments.push_back(parseArgument(false)); skipSpace(); } @@ -146,10 +331,13 @@ std::unique_ptr<PatternTreeParser::Node> PatternTreeParser::parseQuotedWord() } // Parse a bare word atom -std::unique_ptr<PatternTreeParser::Node> PatternTreeParser::parseWord() +std::unique_ptr<PatternTreeParser::Node> PatternTreeParser::parseWord(bool shrt) { - static const APT::StringView DISALLOWED_START("?~,()\0", 6); - static const APT::StringView DISALLOWED(",()\0", 4); + static const constexpr auto DISALLOWED_START = "!?~|,() \0"_sv; + static const constexpr auto DISALLOWED_LONG = "|,()\0"_sv; + static const constexpr auto DISALLOWED_SHRT = "|,() ?\0"_sv; + const auto DISALLOWED = shrt ? DISALLOWED_SHRT : DISALLOWED_LONG; + if (DISALLOWED_START.find(sentence[state.offset]) != APT::StringView::npos) return nullptr; @@ -167,20 +355,21 @@ std::unique_ptr<PatternTreeParser::Node> PatternTreeParser::parseWord() // Rendering of the tree in JSON for debugging std::ostream &PatternTreeParser::PatternNode::render(std::ostream &os) { - os << "{" - << "\"term\": \"" << term.to_string() << "\",\n" - << "\"arguments\": [\n"; - for (auto &node : arguments) - node->render(os) << "," << std::endl; - os << "null]\n"; - os << "}\n"; + + os << term.to_string(); + if (haveArgumentList) + { + os << "("; + for (auto &node : arguments) + node->render(os) << ","; + os << ")"; + } return os; } std::ostream &PatternTreeParser::WordNode::render(std::ostream &os) { - os << '"' << word.to_string() << '"'; - return os; + return quoted ? os << '"' << word.to_string() << '"' : os << word.to_string(); } std::nullptr_t PatternTreeParser::Node::error(std::string message) diff --git a/apt-pkg/cachefilter-patterns.h b/apt-pkg/cachefilter-patterns.h index bd8ce7e7e..4eeb68594 100644 --- a/apt-pkg/cachefilter-patterns.h +++ b/apt-pkg/cachefilter-patterns.h @@ -39,6 +39,8 @@ struct PatternTreeParser size_t start = 0; size_t end = 0; + explicit Node(size_t start = 0, size_t end = 0) : start(start), end(end) {} + virtual std::ostream &render(std::ostream &os) { return os; }; std::nullptr_t error(std::string message); }; @@ -71,7 +73,7 @@ struct PatternTreeParser struct State { - off_t offset = 0; + size_t offset = 0; }; APT::StringView sentence; @@ -90,11 +92,18 @@ struct PatternTreeParser /// There may not be anything before or after the pattern, except for /// whitespace. std::unique_ptr<Node> parseTop(); + std::unique_ptr<Node> parse(); // public for test cases only private: - std::unique_ptr<Node> parse(); + std::unique_ptr<Node> parseOr(); + std::unique_ptr<Node> parseAnd(); + std::unique_ptr<Node> parseUnary(); + std::unique_ptr<Node> parsePrimary(); + std::unique_ptr<Node> parseGroup(); std::unique_ptr<Node> parsePattern(); - std::unique_ptr<Node> parseWord(); + std::unique_ptr<Node> parseShortPattern(); + std::unique_ptr<Node> parseArgument(bool shrt); + std::unique_ptr<Node> parseWord(bool shrt); std::unique_ptr<Node> parseQuotedWord(); }; diff --git a/apt-pkg/cacheset.cc b/apt-pkg/cacheset.cc index f5251eda8..ae1d5ee3e 100644 --- a/apt-pkg/cacheset.cc +++ b/apt-pkg/cacheset.cc @@ -295,7 +295,7 @@ bool CacheSetHelper::PackageFromPackageName(PackageContainerInterface * const pc bool CacheSetHelper::PackageFromPattern(PackageContainerInterface *const pci, pkgCacheFile &Cache, std::string const &pattern) { - if (pattern.size() < 1 || pattern[0] != '?') + if (pattern.size() < 1 || (pattern[0] != '?' && pattern[0] != '~')) return false; auto compiledPattern = APT::CacheFilter::ParsePattern(pattern, &Cache); diff --git a/apt-pkg/contrib/string_view.h b/apt-pkg/contrib/string_view.h index f4f0c645f..05aad3327 100644 --- a/apt-pkg/contrib/string_view.h +++ b/apt-pkg/contrib/string_view.h @@ -123,7 +123,10 @@ static inline int StringViewCompareFast(StringView a, StringView b) { return memcmp(a.data(), b.data(), a.size()); } - +static constexpr inline APT::StringView operator""_sv(const char *data, size_t size) +{ + return APT::StringView(data, size); +} } inline bool operator ==(const char *other, APT::StringView that); diff --git a/apt-private/private-list.cc b/apt-private/private-list.cc index 6071129a7..f5c31bbcd 100644 --- a/apt-private/private-list.cc +++ b/apt-private/private-list.cc @@ -48,8 +48,8 @@ class PackageNameMatcher : public Matcher { std::string pattern = patterns[i]; APT::CacheFilter::Matcher *cachefilter = NULL; - if (pattern.size() > 0 && pattern[0] == '?') - cachefilter = APT::CacheFilter::ParsePattern(pattern, &cacheFile).release(); + if (pattern.size() > 0 && (pattern[0] == '?' || pattern[0] == '~')) + cachefilter = APT::CacheFilter::ParsePattern(pattern, &cacheFile).release(); else if(_config->FindB("APT::Cmd::Use-Regexp", false) == true) cachefilter = new APT::CacheFilter::PackageNameMatchesRegEx(pattern); else diff --git a/doc/apt-patterns.7.xml b/doc/apt-patterns.7.xml index b94a9b226..6fa945058 100644 --- a/doc/apt-patterns.7.xml +++ b/doc/apt-patterns.7.xml @@ -43,19 +43,19 @@ patterns. </para> <variablelist> - <varlistentry><term><code>?and(PATTERN, PATTERN, ...)</code></term> + <varlistentry><term><code>?and(PATTERN, PATTERN, ...)</code></term><term><code>PATTERN PATTERN ...</code></term> <listitem><para>Selects objects where all specified patterns match.</para></listitem> </varlistentry> - <varlistentry><term><code>?false</code></term> + <varlistentry><term><code>?false</code></term><term><code>~F</code></term> <listitem><para>Selects nothing.</para></listitem> </varlistentry> - <varlistentry><term><code>?not(PATTERN)</code></term> + <varlistentry><term><code>?not(PATTERN)</code></term><term><code>!PATTERN</code></term> <listitem><para>Selects objects where PATTERN does not match.</para></listitem> </varlistentry> <varlistentry><term><code>?or(PATTERN, PATTERN, ...)</code></term> <listitem><para>Selects objects where at least one of the specified patterns match.</para></listitem> </varlistentry> - <varlistentry><term><code>?true</code></term> + <varlistentry><term><code>?true</code></term><term><code>~T</code></term> <listitem><para>Selects all objects.</para></listitem> </varlistentry> </variablelist> @@ -83,40 +83,40 @@ These patterns select specific packages. </para> <variablelist> - <varlistentry><term><code>?architecture(WILDCARD)</code></term> + <varlistentry><term><code>?architecture(WILDCARD)</code></term><term><code>~rWILDCARD</code></term> <listitem><para>Selects packages matching the specified architecture, which may contain wildcards using any.</para></listitem> </varlistentry> - <varlistentry><term><code>?automatic</code></term> + <varlistentry><term><code>?automatic</code></term><term><code>~M</code></term> <listitem><para>Selects packages that were installed automatically.</para></listitem> </varlistentry> - <varlistentry><term><code>?broken</code></term> + <varlistentry><term><code>?broken</code></term><term><code>~b</code></term> <listitem><para>Selects packages that have broken dependencies.</para></listitem> </varlistentry> - <varlistentry><term><code>?config-files</code></term> + <varlistentry><term><code>?config-files</code></term><term><code>~c</code></term> <listitem><para>Selects packages that are not fully installed, but have solely residual configuration files left.</para></listitem> </varlistentry> - <varlistentry><term><code>?essential</code></term> + <varlistentry><term><code>?essential</code></term><term><code>~E</code></term> <listitem><para>Selects packages that have Essential: yes set in their control file.</para></listitem> </varlistentry> <varlistentry><term><code>?exact-name(NAME)</code></term> <listitem><para>Selects packages with the exact specified name.</para></listitem> </varlistentry> - <varlistentry><term><code>?garbage</code></term> + <varlistentry><term><code>?garbage</code></term><term><code>~g</code></term> <listitem><para>Selects packages that can be removed automatically.</para></listitem> </varlistentry> - <varlistentry><term><code>?installed</code></term> + <varlistentry><term><code>?installed</code></term><term><code>~i</code></term> <listitem><para>Selects packages that are currently installed.</para></listitem> </varlistentry> - <varlistentry><term><code>?name(REGEX)</code></term> + <varlistentry><term><code>?name(REGEX)</code></term><term><code>~nREGEX</code></term> <listitem><para>Selects packages where the name matches the given regular expression.</para></listitem> </varlistentry> - <varlistentry><term><code>?obsolete</code></term> + <varlistentry><term><code>?obsolete</code></term><term><code>~o</code></term> <listitem><para>Selects packages that no longer exist in repositories.</para></listitem> </varlistentry> - <varlistentry><term><code>?upgradable</code></term> + <varlistentry><term><code>?upgradable</code></term><term><code>~U</code></term> <listitem><para>Selects packages that can be upgraded (have a newer candidate).</para></listitem> </varlistentry> - <varlistentry><term><code>?virtual</code></term> + <varlistentry><term><code>?virtual</code></term><term><code>~v</code></term> <listitem><para>Selects all virtual packages; that is packages without a version. These exist when they are referenced somewhere in the archive, for example because something depends on that name.</para></listitem> @@ -129,22 +129,22 @@ These patterns select specific versions of a package. </para> <variablelist> - <varlistentry><term><code>?archive(REGEX)</code></term> + <varlistentry><term><code>?archive(REGEX)</code></term><term><code>~AREGEX</code></term> <listitem><para>Selects versions that come from the archive that matches the specified regular expression. Archive, here, means the values after <code>a=</code> in <command>apt-cache policy</command>.</para></listitem> </varlistentry> - <varlistentry><term><code>?origin(REGEX)</code></term> + <varlistentry><term><code>?origin(REGEX)</code></term><term><code>~OREGEX</code></term> <listitem><para>Selects versions that come from the origin that matches the specified regular expression. Origin, here, means the values after <code>o=</code> in <command>apt-cache policy</command>.</para></listitem> </varlistentry> - <varlistentry><term><code>?section(REGEX)</code></term> + <varlistentry><term><code>?section(REGEX)</code></term><term><code>~sREGEX</code></term> <listitem><para>Selects versions where the section matches the specified regular expression.</para></listitem> </varlistentry> - <varlistentry><term><code>?source-package(REGEX)</code></term> + <varlistentry><term><code>?source-package(REGEX)</code></term><term><code>~eREGEX</code></term> <listitem><para>Selects versions where the source package name matches the specified regular expression.</para></listitem> </varlistentry> <varlistentry><term><code>?source-version(REGEX)</code></term> <listitem><para>Selects versions where the source package version matches the specified regular expression.</para></listitem> </varlistentry> - <varlistentry><term><code>?version(REGEX)</code></term> + <varlistentry><term><code>?version(REGEX)</code></term><term><code>~VREGEX</code></term> <listitem><para>Selects versions where the version string matching the specified regular expression.</para></listitem> </varlistentry> </variablelist> @@ -168,9 +168,6 @@ </para> <itemizedlist> <listitem> - <para>Only long forms — the ones starting with ? — are supported</para> - </listitem> - <listitem> <para> Syntax is uniform: If there is an opening parenthesis after a term, it is always assumed to be the beginning of an argument list. </para> @@ -193,6 +190,16 @@ <listitem> <para>?narrow accepts infinite arguments</para> </listitem> + <listitem> + <para><code>foo</code> cannot be used as a shortform for <code>?name(foo)</code>, as this can cause typos to go unnoticed: + Consider <code>?and(...,~poptional)</code>: + this requires the package to have <code>required</code> priority, but + if you do not type the <code>~</code>, it would require the package name to contain <code>poptional</code>.</para> + </listitem> + <listitem> + <para>Grouping patterns with <code>(...)</code> or writing <code>?or(A,B)</code> as <code>A|B</code> are not supported. We + do not believe that the use of <code>|</code> is that common, and the grouping is not necessary without it.</para> + </listitem> </itemizedlist> </refsect1> diff --git a/test/libapt/pattern_test.cc b/test/libapt/pattern_test.cc index de2fbceb9..bfcaf2093 100644 --- a/test/libapt/pattern_test.cc +++ b/test/libapt/pattern_test.cc @@ -14,21 +14,53 @@ using namespace APT::Internal; +#define EXPECT_EXCEPTION(exp, exc, msg) \ + caught = false; \ + try \ + { \ + exp; \ + } \ + catch (exc & e) \ + { \ + caught = true; \ + EXPECT_TRUE(e.message.find(msg) != std::string::npos) << msg << " not in " << e.message; \ + }; \ + EXPECT_TRUE(caught) << #exp "should have thrown an exception" + +TEST(TreeParserTest, ParseInvalid) +{ + bool caught = false; + + // Not a valid pattern: Reject + EXPECT_EXCEPTION(PatternTreeParser("?").parse(), PatternTreeParser::Error, "Pattern must have a term"); + EXPECT_EXCEPTION(PatternTreeParser("?AB?").parse(), PatternTreeParser::Error, "Pattern must have a term"); + EXPECT_EXCEPTION(PatternTreeParser("~").parse(), PatternTreeParser::Error, "Unknown short pattern"); + + // Not a pattern at all: Report nullptr + EXPECT_EQ(PatternTreeParser("A?").parse(), nullptr); +} + TEST(TreeParserTest, ParseWord) { - auto node = PatternTreeParser("word").parseTop(); - auto wordNode = dynamic_cast<PatternTreeParser::WordNode *>(node.get()); + auto node = PatternTreeParser("?word(word)").parseTop(); + auto patternNode = dynamic_cast<PatternTreeParser::PatternNode *>(node.get()); - EXPECT_EQ(node.get(), wordNode); + ASSERT_EQ(patternNode->arguments.size(), 1u); + auto wordNode = dynamic_cast<PatternTreeParser::WordNode *>(patternNode->arguments[0].get()); + + EXPECT_EQ(patternNode->arguments[0].get(), wordNode); EXPECT_EQ(wordNode->word, "word"); } TEST(TreeParserTest, ParseQuotedWord) { - auto node = PatternTreeParser("\"a word\"").parseTop(); - auto wordNode = dynamic_cast<PatternTreeParser::WordNode *>(node.get()); + auto node = PatternTreeParser("?word(\"a word\")").parseTop(); + auto patternNode = dynamic_cast<PatternTreeParser::PatternNode *>(node.get()); - EXPECT_EQ(node.get(), wordNode); + ASSERT_EQ(patternNode->arguments.size(), 1u); + auto wordNode = dynamic_cast<PatternTreeParser::WordNode *>(patternNode->arguments[0].get()); + + EXPECT_EQ(patternNode->arguments[0].get(), wordNode); EXPECT_EQ(wordNode->word, "a word"); } @@ -93,3 +125,98 @@ TEST(TreeParserTest, ParseWithManyArgsWithSpacesWithTrailingComma) EXPECT_EQ(patternNode->term, "?hello"); EXPECT_EQ(2u, patternNode->arguments.size()); } + +// Helper +static bool samePattern(const std::unique_ptr<PatternTreeParser::Node> &a, const std::unique_ptr<PatternTreeParser::Node> &b) +{ + auto pa = dynamic_cast<const PatternTreeParser::PatternNode *>(a.get()); + auto pb = dynamic_cast<const PatternTreeParser::PatternNode *>(b.get()); + + if (pa && pb) + { + if (pa->term != pb->term || pa->haveArgumentList != pb->haveArgumentList || pa->arguments.size() != pb->arguments.size()) + return false; + + for (size_t i = 0; i < pa->arguments.size(); i++) + { + if (!samePattern(pa->arguments[i], pb->arguments[i])) + return false; + } + return true; + } + + auto wa = dynamic_cast<const PatternTreeParser::WordNode *>(a.get()); + auto wb = dynamic_cast<const PatternTreeParser::WordNode *>(b.get()); + if (wa && wb) + return wa->word == wb->word && wa->quoted == wb->quoted; + + return false; +} + +#define EXPECT_PATTERN_EQ(shrt, lng) \ + EXPECT_TRUE(samePattern(PatternTreeParser(shrt).parseTop(), PatternTreeParser(lng).parseTop())) +#define EXPECT_PATTERN_EQ_ATOMIC(shrt, lng) \ + EXPECT_TRUE(PatternTreeParser(shrt).parseTop()); \ + caught = false; \ + try \ + { \ + PatternTreeParser(shrt "XXX").parseTop(); \ + } \ + catch (PatternTreeParser::Error & e) \ + { \ + caught = true; \ + }; \ + EXPECT_TRUE(caught) << shrt "XXX should have thrown an exception"; \ + EXPECT_PATTERN_EQ(shrt, lng) + +TEST(TreeParserTest, ParseShortPattern) +{ + bool caught; + EXPECT_PATTERN_EQ("~ramd64", "?architecture(amd64)"); + EXPECT_PATTERN_EQ("~AanArchive", "?archive(anArchive)"); + EXPECT_PATTERN_EQ_ATOMIC("~M", "?automatic"); + EXPECT_PATTERN_EQ_ATOMIC("~b", "?broken"); + EXPECT_PATTERN_EQ_ATOMIC("~c", "?config-files"); + EXPECT_PATTERN_EQ_ATOMIC("~E", "?essential"); + EXPECT_PATTERN_EQ_ATOMIC("~F", "?false"); + EXPECT_PATTERN_EQ_ATOMIC("~g", "?garbage"); + EXPECT_PATTERN_EQ_ATOMIC("~i", "?installed"); + EXPECT_PATTERN_EQ("~napt", "?name(apt)"); + EXPECT_PATTERN_EQ_ATOMIC("~o", "?obsolete"); + EXPECT_PATTERN_EQ("~Obar", "?origin(bar)"); + EXPECT_PATTERN_EQ("~sfoo", "?section(foo)"); + EXPECT_PATTERN_EQ("~esourcename", "?source-package(sourcename)"); + EXPECT_PATTERN_EQ_ATOMIC("~T", "?true"); + EXPECT_PATTERN_EQ_ATOMIC("~U", "?upgradable"); + EXPECT_PATTERN_EQ("~Vverstr", "?version(verstr)"); + EXPECT_PATTERN_EQ_ATOMIC("~v", "?virtual"); + EXPECT_PATTERN_EQ("!?foo", "?not(?foo)"); + + caught = false; + try + { + PatternTreeParser("!x").parseTop(); + } + catch (PatternTreeParser::Error &e) + { + caught = true; + }; + EXPECT_TRUE(caught) << "!X should have thrown an exception"; + + EXPECT_PATTERN_EQ("?a?b", "?and(?a, ?b)"); + EXPECT_PATTERN_EQ("~T~F", "?and(?true, ?false)"); + EXPECT_PATTERN_EQ("~T ~F", "?and(?true, ?false)"); + EXPECT_PATTERN_EQ("~T !~F", "?and(?true, ?not(?false))"); + EXPECT_PATTERN_EQ("!~F ~T", "?and(?not(?false), ?true)"); + EXPECT_PATTERN_EQ("!~F~T", "?and(?not(?false), ?true)"); + + EXPECT_PATTERN_EQ("!~F~T | ~T", "?or(?and(?not(?false), ?true), ?true)"); + EXPECT_PATTERN_EQ("~ramd64|~rall", "?or(?architecture(amd64), ?architecture(all))"); + EXPECT_PATTERN_EQ("~ramd64 | ~rall", "?or(?architecture(amd64), ?architecture(all))"); + EXPECT_PATTERN_EQ("~ramd64?name(foo)", "?and(?architecture(amd64), ?name(foo))"); + + EXPECT_PATTERN_EQ("(?A|?B)?C", "?and(?or(?A, ?B), ?C)"); + EXPECT_PATTERN_EQ("?A|?B?C", "?or(?A, ?and(?B, ?C))"); + EXPECT_PATTERN_EQ("?A|(?B?C)", "?or(?A, ?and(?B, ?C))"); + EXPECT_PATTERN_EQ("(?B?C)|?A", "?or(?and(?B, ?C), ?A)"); +} |