Line data Source code
1 : //* This file is part of the MOOSE framework
2 : //* https://mooseframework.inl.gov
3 : //*
4 : //* All rights reserved, see COPYRIGHT for full restrictions
5 : //* https://github.com/idaholab/moose/blob/master/COPYRIGHT
6 : //*
7 : //* Licensed under LGPL 2.1, please see LICENSE for details
8 : //* https://www.gnu.org/licenses/lgpl-2.1.html
9 :
10 : #include "CapabilityRegistry.h"
11 :
12 : #include "CapabilityException.h"
13 : #include "MooseStringUtils.h"
14 :
15 : #include "peglib.h"
16 :
17 : #include <regex>
18 : #include <set>
19 : #include <utility>
20 :
21 : namespace Moose::internal
22 : {
23 :
24 : const std::set<std::string, std::less<>> CapabilityRegistry::augmented_capability_names{
25 : // TestHarness.getCapabilities()
26 : "hpc",
27 : "machine",
28 : "library_mode",
29 : // TestHarness.testers.RunApp.getAugmentedCapabilities()
30 : "mpi_procs",
31 : "num_threads"};
32 :
33 : Capability &
34 3185194 : CapabilityRegistry::add(const std::string_view name,
35 : const Capability::Value & value,
36 : const std::string_view doc)
37 : {
38 3185194 : auto it_pair = _registry.lower_bound(name);
39 3185194 : if (it_pair != _registry.end() && it_pair->first == name)
40 : {
41 84694 : auto & capability = it_pair->second;
42 84694 : if (capability.getValue() != value || capability.getDoc() != doc)
43 28 : throw CapabilityException("Capability '" + std::string(name) +
44 42 : "' already exists and is not equal");
45 84680 : return capability;
46 : }
47 :
48 : return _registry
49 3100500 : .emplace_hint(it_pair,
50 : std::piecewise_construct,
51 3100500 : std::forward_as_tuple(name),
52 6201000 : std::forward_as_tuple(name, value, doc))
53 3100500 : ->second;
54 : }
55 :
56 : const Capability *
57 9751 : CapabilityRegistry::query(std::string capability) const
58 : {
59 9751 : capability = MooseUtils::toLower(capability);
60 9751 : if (const auto it = _registry.find(capability); it != _registry.end())
61 9654 : return &it->second;
62 97 : return nullptr;
63 : }
64 :
65 : const Capability &
66 50 : CapabilityRegistry::get(const std::string & capability) const
67 : {
68 50 : if (const auto capability_ptr = query(capability))
69 46 : return *capability_ptr;
70 4 : throw CapabilityException("Capability '" + capability + "' not registered");
71 : }
72 :
73 : [[noreturn]] void
74 47 : checkException(const peg::SemanticValues & vs,
75 : const std::string & message,
76 : const std::optional<Capability> capability = {})
77 : {
78 47 : std::string msg = "Capability statement '" + vs.token_to_string() + "': ";
79 47 : if (capability)
80 16 : msg += "capability '" + capability->toString() + "' ";
81 47 : msg += message;
82 47 : throw CapabilityException(msg);
83 47 : }
84 :
85 : CapabilityRegistry::CheckResult
86 8533 : CapabilityRegistry::check(std::string requirements,
87 : const CapabilityRegistry::CheckOptions &
88 : options /* = CapabilityRegistry::CheckOptions() */) const
89 : {
90 : using namespace peg;
91 :
92 : // unquote
93 : while (true)
94 : {
95 8695 : const auto len = requirements.length();
96 17224 : if (len >= 2 && ((requirements[0] == '\'' && requirements[len - 1] == '\'') ||
97 8529 : (requirements[0] == '"' && requirements[len - 1] == '"')))
98 162 : requirements = requirements.substr(1, len - 2);
99 : else
100 8533 : break;
101 162 : }
102 :
103 8533 : CheckResult result;
104 8533 : result.state = CheckState::CERTAIN_FAIL;
105 :
106 8533 : if (requirements.length() == 0)
107 : {
108 4 : result.state = CheckState::CERTAIN_PASS;
109 4 : return result;
110 : }
111 :
112 : static parser parser(R"(
113 : Expression <- _ Bool _ LogicOperator _ Expression / Bool _
114 : Bool <- Comparison / '!' Bool / '!' Identifier / Identifier / '(' _ Expression _ ')'
115 : Comparison <- Identifier _ Operator _ Version / Identifier _ Operator _ String
116 : String <- [a-zA-Z0-9_-]+
117 : Identifier <- [a-zA-Z][a-zA-Z0-9_]*
118 : Operator <- [<>=!]+
119 : LogicOperator <- [&|]
120 : Version <- Number '.' Version / Number
121 : Number <- [0-9]+
122 : ~_ <- [ \t]*
123 8529 : )");
124 :
125 8529 : if (!static_cast<bool>(parser))
126 0 : throw CapabilityException("Capabilities parser build failure.");
127 :
128 : // Keep track of unknown capabilities in the event that
129 : // the check must be certain
130 8529 : std::set<std::string> unknown_capabilities;
131 85 : const auto add_unknown_capability = [&unknown_capabilities](const auto & name)
132 85 : { unknown_capabilities.insert(MooseUtils::toLower(name)); };
133 :
134 : // Make sure that the capabilities to ignore are valid capabilities
135 8597 : for (const auto & name : options.ignore_capabilities)
136 72 : if (!_registry.count(name))
137 4 : throw CapabilityException("Capability to ignore '" + name + "' is not known");
138 :
139 12669 : parser["Number"] = [](const SemanticValues & vs) { return vs.token_to_number<int>(); };
140 :
141 8525 : parser["Version"] = [](const SemanticValues & vs)
142 : {
143 4144 : switch (vs.choice())
144 : {
145 2271 : case 0: // Number '.' Version
146 : {
147 4542 : std::vector<int> ret{std::any_cast<int>(vs[0])};
148 2271 : const auto & vs1 = std::any_cast<std::vector<int>>(vs[1]);
149 2271 : ret.insert(ret.end(), vs1.begin(), vs1.end());
150 2271 : return ret;
151 2271 : }
152 :
153 1873 : case 1: // Number
154 5619 : return std::vector<int>{std::any_cast<int>(vs[0])};
155 : }
156 :
157 0 : checkException(vs, "unknown number match.");
158 : };
159 :
160 : enum LogicOperator
161 : {
162 : OP_AND,
163 : OP_OR
164 : };
165 :
166 8525 : parser["LogicOperator"] = [](const SemanticValues & vs)
167 : {
168 1138 : const auto op = vs.token();
169 1138 : if (op == "&")
170 1085 : return OP_AND;
171 53 : if (op == "|")
172 53 : return OP_OR;
173 0 : checkException(vs, "unknown logic operator.");
174 : };
175 :
176 : enum Operator
177 : {
178 : OP_LESS_EQ,
179 : OP_GREATER_EQ,
180 : OP_LESS,
181 : OP_GREATER,
182 : OP_NOT_EQ,
183 : OP_EQ
184 : };
185 :
186 8525 : parser["Operator"] = [](const SemanticValues & vs)
187 : {
188 4051 : const auto op = vs.token();
189 4051 : if (op == "<=")
190 74 : return OP_LESS_EQ;
191 3977 : if (op == ">=")
192 1117 : return OP_GREATER_EQ;
193 2860 : if (op == "<")
194 169 : return OP_LESS;
195 2691 : if (op == ">")
196 124 : return OP_GREATER;
197 2567 : if (op == "!=")
198 390 : return OP_NOT_EQ;
199 2177 : if (op == "=" || op == "==")
200 2171 : return OP_EQ;
201 24 : checkException(vs, "unknown operator.");
202 : };
203 :
204 10687 : parser["String"] = [](const SemanticValues & vs) { return vs.token_to_string(); };
205 18188 : parser["Identifier"] = [](const SemanticValues & vs) { return vs.token_to_string(); };
206 :
207 8525 : parser["Comparison"] =
208 4035 : [this, &add_unknown_capability, &options, &result](const SemanticValues & vs)
209 : {
210 4035 : const auto left = std::any_cast<std::string>(vs[0]);
211 4035 : const auto op = std::any_cast<Operator>(vs[1]);
212 :
213 : // check existence
214 4035 : const auto capability_ptr = query(left);
215 4035 : if (!capability_ptr)
216 : {
217 : // return an unknown if the capability does not exist, this is important as it
218 : // stays unknown upon negation
219 37 : add_unknown_capability(left);
220 37 : return CheckState::UNKNOWN;
221 : }
222 :
223 : // capability is registered by the app
224 3998 : const auto & capability = *capability_ptr;
225 3998 : const auto & name = capability.getName();
226 :
227 : // register capability as seen
228 3998 : result.capability_names.insert(name);
229 :
230 : // whether or not the capability is ignored
231 3975 : const auto is_ignored = [&name, &options]() { return options.ignore_capabilities.count(name); };
232 :
233 : // explicitly false causes any comparison to fail unless ignored
234 3998 : if (const auto bool_ptr = capability.queryBoolValue(); (bool_ptr && !(*bool_ptr)))
235 8 : return is_ignored() ? CheckState::IGNORE : CheckState::CERTAIN_FAIL;
236 :
237 : // comparator
238 3967 : auto comp = [&is_ignored](const int i, const auto & a, const auto & b)
239 : {
240 : // early exit for ignored capabilities
241 3967 : if (is_ignored())
242 10 : return CheckState::IGNORE;
243 :
244 : // do the comparison
245 7914 : const auto do_comp = [&i, &a, &b]()
246 : {
247 3957 : switch (i)
248 : {
249 72 : case OP_LESS_EQ:
250 72 : return a <= b;
251 1117 : case OP_GREATER_EQ:
252 1117 : return a >= b;
253 156 : case OP_LESS:
254 156 : return a < b;
255 104 : case OP_GREATER:
256 104 : return a > b;
257 386 : case OP_NOT_EQ:
258 386 : return a != b;
259 2122 : case OP_EQ:
260 2122 : return a == b;
261 : }
262 0 : return false;
263 : };
264 3957 : return do_comp() ? CheckState::CERTAIN_PASS : CheckState::CERTAIN_FAIL;
265 3990 : };
266 :
267 : // version comparison
268 3990 : std::vector<int> app_value_version;
269 :
270 3990 : switch (vs.choice())
271 : {
272 1848 : case 0: // Identifier _ Operator _ Version
273 : {
274 : // int comparison
275 1848 : const auto right = std::any_cast<std::vector<int>>(vs[2]);
276 1848 : if (const auto int_ptr = capability.queryIntValue())
277 : {
278 504 : if (right.size() != 1)
279 8 : checkException(vs, "cannot be compared to a version.", capability);
280 :
281 502 : return comp(op, *int_ptr, right[0]);
282 : }
283 :
284 1344 : const auto string_ptr = capability.queryStringValue();
285 1344 : if (!string_ptr)
286 16 : checkException(vs,
287 12 : "cannot be compared to a " +
288 24 : std::string(right.size() == 1 ? "number" : "version number") + ".",
289 : capability);
290 :
291 2680 : if (!MooseUtils::tokenizeAndConvert(*string_ptr, app_value_version, "."))
292 16 : checkException(vs, "cannot be compared to a version.", capability);
293 :
294 : // compare versions
295 1336 : return comp(op, app_value_version, right);
296 1848 : }
297 :
298 2142 : case 1: // Identifier _ Operator _ String
299 : {
300 : // here we would check for valid options and throw if not valid
301 2142 : const auto right = MooseUtils::toLower(std::any_cast<std::string>(vs[2]));
302 : // the capability value has to be a string
303 2142 : const auto string_ptr = capability.queryStringValue();
304 2142 : if (!string_ptr)
305 16 : checkException(vs, "cannot be compared to a string.", capability);
306 :
307 : // If this capability has an enumeration, make sure a valid
308 : // choice is used
309 2138 : if (!capability.hasEnumeration(right))
310 28 : checkException(vs,
311 42 : "'" + right + "' invalid for capability '" + left +
312 35 : "'; valid values: " + capability.enumerationToString());
313 :
314 : // Capability is a version
315 4262 : if (MooseUtils::tokenizeAndConvert(*string_ptr, app_value_version, "."))
316 8 : checkException(vs, "cannot be compared to a string.", capability);
317 :
318 2129 : return comp(op, *string_ptr, right);
319 2142 : }
320 : }
321 :
322 0 : checkException(vs, "failed comparison.", capability);
323 4058 : };
324 :
325 8525 : parser["Bool"] = [this, &add_unknown_capability, &options, &result](const SemanticValues & vs)
326 : {
327 10704 : switch (vs.choice())
328 : {
329 4452 : case 0: // Comparison
330 : case 4: // '(' _ Expression _ ')'
331 4452 : return std::any_cast<CheckState>(vs[0]);
332 :
333 630 : case 1: // '!' Bool
334 630 : switch (std::any_cast<CheckState>(vs[0]))
335 : {
336 381 : case CheckState::CERTAIN_FAIL:
337 381 : return CheckState::CERTAIN_PASS;
338 203 : case CheckState::CERTAIN_PASS:
339 203 : return CheckState::CERTAIN_FAIL;
340 21 : case CheckState::POSSIBLE_FAIL:
341 21 : return CheckState::POSSIBLE_PASS;
342 0 : case CheckState::POSSIBLE_PASS:
343 0 : return CheckState::POSSIBLE_FAIL;
344 22 : case CheckState::IGNORE:
345 22 : return CheckState::IGNORE;
346 3 : default:
347 3 : return CheckState::UNKNOWN;
348 : }
349 :
350 5622 : case 2: // '!' Identifier
351 : case 3: // Identifier
352 : {
353 5622 : const bool negated = vs.choice() == 2;
354 5622 : const auto identifier = std::any_cast<std::string>(vs[0]);
355 5622 : if (const auto capability_ptr = query(identifier))
356 : {
357 5574 : const auto & capability = *capability_ptr;
358 5574 : const auto & name = capability.getName();
359 :
360 : // explicit; cannot be a bool expression
361 5574 : if (capability.getExplicit())
362 : {
363 36 : std::string message = "capability '" + name +
364 18 : "' requires a value and cannot be used in a boolean expression";
365 18 : if (capability.queryEnumeration())
366 4 : message += "; valid values: " + capability.enumerationToString();
367 36 : checkException(vs, message);
368 18 : }
369 :
370 : // mark as used
371 5556 : result.capability_names.insert(name);
372 : // is ignored
373 5556 : if (options.ignore_capabilities.count(name))
374 48 : return CheckState::IGNORE;
375 :
376 : // helper for negating a passing value if needed
377 5508 : const auto bool_to_pass = [&negated](const bool val)
378 5508 : { return (val ^ negated) ? CheckState::CERTAIN_FAIL : CheckState::CERTAIN_PASS; };
379 : // has a boolean value, so use it
380 5508 : if (const auto bool_ptr = capability.queryBoolValue())
381 604 : return bool_to_pass(!*bool_ptr);
382 : // not ignored and doesn't have a boolean value
383 4904 : return bool_to_pass(false);
384 : }
385 :
386 48 : add_unknown_capability(identifier);
387 48 : return negated ? CheckState::POSSIBLE_PASS : CheckState::POSSIBLE_FAIL;
388 5622 : }
389 :
390 0 : default:
391 0 : throw CapabilityException("Unknown choice in Bool non-terminal");
392 : }
393 : };
394 :
395 8525 : parser["Expression"] = [](const SemanticValues & vs)
396 : {
397 10056 : switch (vs.choice())
398 : {
399 1138 : case 0: // Bool _ LogicOperator _ Expression
400 : {
401 1138 : const auto left = std::any_cast<CheckState>(vs[0]);
402 1138 : const auto right = std::any_cast<CheckState>(vs[2]);
403 1138 : const auto op = std::any_cast<LogicOperator>(vs[1]);
404 :
405 1138 : switch (op)
406 : {
407 1085 : case OP_AND:
408 1085 : if (left == CheckState::IGNORE)
409 6 : return right;
410 1079 : if (right == CheckState::IGNORE)
411 2 : return left;
412 5099 : for (const auto state : {CheckState::CERTAIN_FAIL,
413 : CheckState::POSSIBLE_FAIL,
414 : CheckState::UNKNOWN,
415 : CheckState::POSSIBLE_PASS,
416 6176 : CheckState::CERTAIN_PASS})
417 5099 : if (left == state || right == state)
418 1077 : return state;
419 0 : throw CapabilityException("Conjunction failure");
420 :
421 53 : case OP_OR:
422 53 : if (left == CheckState::IGNORE || right == CheckState::IGNORE)
423 8 : return CheckState::IGNORE;
424 143 : for (const auto state : {CheckState::CERTAIN_PASS,
425 : CheckState::POSSIBLE_PASS,
426 : CheckState::UNKNOWN,
427 : CheckState::POSSIBLE_FAIL,
428 188 : CheckState::CERTAIN_FAIL})
429 143 : if (left == state || right == state)
430 45 : return state;
431 0 : throw CapabilityException("Conjunction failure");
432 :
433 0 : default:
434 0 : throw CapabilityException("Unknown logic operator");
435 : }
436 : }
437 :
438 8918 : case 1: // Bool
439 8918 : return std::any_cast<CheckState>(vs[0]);
440 :
441 0 : default:
442 0 : throw CapabilityException("Unknown choice in Expression non-terminal");
443 : }
444 : };
445 :
446 : // (4) Parse
447 8525 : parser.enable_packrat_parsing(); // Enable packrat parsing.
448 :
449 8525 : if (!parser.parse(requirements, result.state))
450 16 : throw CapabilityException("Unable to parse requested capabilities '", requirements, "'.");
451 :
452 : // If certain and unknown capabilities were found, throw accordingly
453 8462 : if (options.certain && unknown_capabilities.size())
454 12 : throw UnknownCapabilitiesException({unknown_capabilities.begin(), unknown_capabilities.end()});
455 :
456 : // Consider an ignored state to be a pass
457 8458 : if (result.state == CheckState::IGNORE)
458 46 : result.state = CheckState::CERTAIN_PASS;
459 :
460 8458 : return result;
461 8600 : }
462 : } // namespace CapabilityUtils
|