| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * This file is part of the Falcon Player (FPP) and is Copyright (C) | ||
| 3 | * 2013-2026 by the Falcon Player Developers. | ||
| 4 | * | ||
| 5 | * The Falcon Player (FPP) is free software, and is covered under | ||
| 6 | * multiple Open Source licenses. Please see the included 'LICENSES' | ||
| 7 | * file for descriptions of what files are covered by each license. | ||
| 8 | * | ||
| 9 | * This source file is covered under the LGPL v2.1 as described in the | ||
| 10 | * included LICENSE.LGPL file. | ||
| 11 | */ | ||
| 12 | |||
| 13 | #include "fpp-pch.h" | ||
| 14 | |||
| 15 | #include "fpp-json.h" | ||
| 16 | |||
| 17 | #include "Condition.h" | ||
| 18 | #include "../SunRise.h" | ||
| 19 | #include "../Variables.h" | ||
| 20 | #include "../common.h" | ||
| 21 | #include "../gpio.h" | ||
| 22 | #include "../log.h" | ||
| 23 | #include "../settings.h" | ||
| 24 | #include "../util/ExpressionProcessor.h" | ||
| 25 | #include "../util/GPIOUtils.h" | ||
| 26 | |||
| 27 | #include <cmath> | ||
| 28 | #include <cstring> | ||
| 29 | #include <ctime> | ||
| 30 | #include <map> | ||
| 31 | #include <strings.h> | ||
| 32 | |||
| 33 | class GroupNode : public ConditionNode { | ||
| 34 | public: | ||
| 35 | bool isAnd = true; | ||
| 36 | bool negate = false; | ||
| 37 | std::vector<std::unique_ptr<ConditionNode>> children; | ||
| 38 | |||
| 39 | 17 | bool evaluate() const override { | |
| 40 | 17 | bool result = isAnd; // AND starts true (short-circuits on first false) | |
| 41 | 17 | int evaluatedCount = 0; | |
| 42 |
2/2✓ Branch 0 taken 27 times.
✓ Branch 1 taken 11 times.
|
38 | for (auto& c : children) { |
| 43 |
1/2✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
|
27 | bool v = c->evaluate(); |
| 44 | 27 | evaluatedCount++; | |
| 45 |
2/2✓ Branch 0 taken 16 times.
✓ Branch 1 taken 11 times.
|
27 | if (isAnd) { |
| 46 |
3/4✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✓ Branch 3 taken 4 times.
|
16 | result = result && v; |
| 47 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 12 times.
|
16 | if (!result) { |
| 48 | 4 | break; | |
| 49 | } | ||
| 50 | } else { | ||
| 51 |
3/4✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 9 times.
|
11 | result = result || v; |
| 52 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 9 times.
|
11 | if (result) { |
| 53 | 2 | break; | |
| 54 | } | ||
| 55 | } | ||
| 56 | } | ||
| 57 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 15 times.
|
17 | bool finalResult = negate ? !result : result; |
| 58 | // evaluatedCount vs children.size() shows short-circuiting in | ||
| 59 | // action - e.g. an AND group logging 2 of 5 tells you child #3 was | ||
| 60 | // the one that failed and the rest were never even evaluated. | ||
| 61 |
8/8✓ Branch 0 taken 8 times.
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 9 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 15 times.
✓ Branch 6 taken 10 times.
✓ Branch 7 taken 7 times.
|
17 | LogDebug(VB_COMMAND, "If: Group(%s%s) evaluated %d/%zu children, raw=%s -> %s\n", |
| 62 | isAnd ? "AND" : "OR", negate ? " NOT" : "", evaluatedCount, children.size(), | ||
| 63 | result ? "true" : "false", finalResult ? "true" : "false"); | ||
| 64 | 17 | return finalResult; | |
| 65 | } | ||
| 66 | }; | ||
| 67 | |||
| 68 | 1966 | static std::string EvaluateConditionExpression(const std::string& expr, bool* compiled = nullptr) { | |
| 69 |
1/2✓ Branch 0 taken 1966 times.
✗ Branch 1 not taken.
|
1966 | ExpressionProcessor proc; |
| 70 | 1966 | std::map<std::string, ExpressionProcessor::ExpressionVariable> boundVars; | |
| 71 | // getAllVariableNames() (User + read-only fpp-/mqtt- vars), matching what | ||
| 72 | // GET /api/variables?validateExpression binds (Variables.cpp) - otherwise | ||
| 73 | // an expression referencing an fpp-/mqtt- variable validates as green in | ||
| 74 | // the editor but silently evaluates as unbound at runtime. | ||
| 75 |
3/4✓ Branch 0 taken 1966 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 56037 times.
✓ Branch 3 taken 1966 times.
|
58003 | for (auto const& varName : Variables::INSTANCE.getAllVariableNames()) { |
| 76 |
1/2✓ Branch 0 taken 56037 times.
✗ Branch 1 not taken.
|
56037 | auto inserted = boundVars.try_emplace(varName, varName); |
| 77 |
3/6✓ Branch 0 taken 56037 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 56037 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 56037 times.
✗ Branch 5 not taken.
|
112074 | inserted.first->second.setValue(Variables::INSTANCE.getVariable(varName)); |
| 78 |
1/2✓ Branch 0 taken 56037 times.
✗ Branch 1 not taken.
|
56037 | proc.bindVariable(&inserted.first->second); |
| 79 | 1966 | } | |
| 80 |
1/2✓ Branch 0 taken 1966 times.
✗ Branch 1 not taken.
|
1966 | bool ok = proc.compile(expr); |
| 81 |
2/2✓ Branch 0 taken 23 times.
✓ Branch 1 taken 1943 times.
|
1966 | if (compiled) { |
| 82 | 23 | *compiled = ok; | |
| 83 | } | ||
| 84 |
2/2✓ Branch 0 taken 14 times.
✓ Branch 1 taken 1952 times.
|
1966 | if (!ok) { |
| 85 | // Debug, not Warn - a broken Expression source is re-evaluated on | ||
| 86 | // every check of an If wired to a frame/GPIO trigger, so a Warn | ||
| 87 | // here would spam the log continuously instead of once. | ||
| 88 |
2/4✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
|
14 | LogDebug(VB_COMMAND, "If: Expression source \"%s\" failed to compile\n", TruncateForLog(expr).c_str()); |
| 89 | } | ||
| 90 |
2/4✓ Branch 0 taken 1966 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1966 times.
✗ Branch 3 not taken.
|
3932 | return proc.evaluate("string"); |
| 91 | 1966 | } | |
| 92 | |||
| 93 | // Once per formula, so a Check re-run on every frame doesn't fill the log. | ||
| 94 | 10 | static void WarnFormulaOnce(const std::string& text) { | |
| 95 | static std::mutex m; | ||
| 96 |
3/4✓ Branch 0 taken 1 times.
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
|
10 | static std::set<std::string> warned; |
| 97 |
1/2✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
|
10 | std::lock_guard<std::mutex> l(m); |
| 98 |
6/8✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✓ Branch 5 taken 5 times.
✓ Branch 6 taken 5 times.
✓ Branch 7 taken 5 times.
|
10 | if (warned.size() > 100 || !warned.insert(text).second) { |
| 99 | 5 | return; | |
| 100 | } | ||
| 101 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
|
5 | if (warned.size() > 100) { |
| 102 | ✗ | LogWarn(VB_COMMAND, "If: more formulas do not compile; no more are logged\n"); | |
| 103 | ✗ | return; | |
| 104 | } | ||
| 105 |
2/4✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
|
5 | LogWarn(VB_COMMAND, "If: formula \"%s\" does not compile; the Check reads it as not found\n", |
| 106 | TruncateForLog(text).c_str()); | ||
| 107 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 5 times.
|
10 | } |
| 108 | |||
| 109 | // Does `text` contain a math/comparison operator character that ISN'T | ||
| 110 | // wholly explained by being part of a real variable name? Removes every | ||
| 111 | // real variable name found as a substring (longest-first, same rationale as | ||
| 112 | // ExpressionProcessor's own aliasExpr()) before checking what's left - so | ||
| 113 | // "mqtt-homeassistant/sensor/outside_temperature/state" alone (the whole variable | ||
| 114 | // name, hyphen/slashes and all) doesn't look like a formula, but | ||
| 115 | // "fpp_next_playlist_start+1" does (the "+1" left over after removing the | ||
| 116 | // real name is the actual signal). | ||
| 117 | // Plain text with '-' or '/' and no variable name in it ("on-air", | ||
| 118 | // "5-star-review") looks like a formula here too; ReadsAsWords() below | ||
| 119 | // takes it back as text when it doesn't compile. | ||
| 120 | 1969 | static std::string WithoutVariableNames(const std::string& text) { | |
| 121 |
1/2✓ Branch 0 taken 1969 times.
✗ Branch 1 not taken.
|
1969 | std::vector<std::string> names = Variables::INSTANCE.getAllVariableNames(); |
| 122 |
1/2✓ Branch 0 taken 1969 times.
✗ Branch 1 not taken.
|
322985 | std::sort(names.begin(), names.end(), [](const std::string& a, const std::string& b) { return a.size() > b.size(); }); |
| 123 |
1/2✓ Branch 0 taken 1969 times.
✗ Branch 1 not taken.
|
1969 | std::string remaining = text; |
| 124 |
2/2✓ Branch 0 taken 56080 times.
✓ Branch 1 taken 1969 times.
|
58049 | for (auto& n : names) { |
| 125 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 56080 times.
|
56080 | if (n.empty()) { |
| 126 | ✗ | continue; | |
| 127 | } | ||
| 128 | size_t pos; | ||
| 129 |
2/2✓ Branch 0 taken 11 times.
✓ Branch 1 taken 56080 times.
|
56091 | while ((pos = remaining.find(n)) != std::string::npos) { |
| 130 |
1/2✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
|
11 | remaining.erase(pos, n.size()); |
| 131 | } | ||
| 132 | } | ||
| 133 | 1969 | return remaining; | |
| 134 | 1969 | } | |
| 135 | 1960 | static bool LooksLikeFormula(const std::string& text) { | |
| 136 |
1/2✓ Branch 0 taken 1960 times.
✗ Branch 1 not taken.
|
1960 | return WithoutVariableNames(text).find_first_of("+-*/()<>=!^") != std::string::npos; |
| 137 | } | ||
| 138 | |||
| 139 | // A formula-looking text that fails to compile is literal text when it reads | ||
| 140 | // as words: no variable name in it, no operator but '-' and '/', and each of | ||
| 141 | // those followed by a letter ("on-air", "Front/Back", "5-star-review"; not | ||
| 142 | // "count-1" or "count+"). | ||
| 143 | 9 | static bool ReadsAsWords(const std::string& text) { | |
| 144 |
7/12✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 7 times.
✓ Branch 6 taken 9 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✓ Branch 9 taken 7 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
|
9 | if (WithoutVariableNames(text) != text || text.find_first_of("+*()<>=!^") != std::string::npos) { |
| 145 | 2 | return false; | |
| 146 | } | ||
| 147 |
2/2✓ Branch 0 taken 53 times.
✓ Branch 1 taken 3 times.
|
56 | for (size_t i = 0; i < text.size(); i++) { |
| 148 |
8/10✓ Branch 0 taken 45 times.
✓ Branch 1 taken 8 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 45 times.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✓ Branch 7 taken 4 times.
✓ Branch 8 taken 4 times.
✓ Branch 9 taken 49 times.
|
53 | if ((text[i] == '-' || text[i] == '/') && (i + 1 == text.size() || !isalpha((unsigned char)text[i + 1]))) { |
| 149 | 4 | return false; | |
| 150 | } | ||
| 151 | } | ||
| 152 | 3 | return true; | |
| 153 | } | ||
| 154 | |||
| 155 | // The merged "Variable"/"Expression" behavior: a naive "does this contain | ||
| 156 | // math-operator characters" heuristic doesn't work as the PRIMARY signal | ||
| 157 | // here, because real variable names on this system already contain '-' and | ||
| 158 | // '/' - both of which are also math operators (subtract, divide). | ||
| 159 | // "fpp_next_playlist_start" and "mqtt-homeassistant/sensor/outside_temperature/state" | ||
| 160 | // are both completely ordinary variable names. The reliable primary signal | ||
| 161 | // is exact match against the live variable list; LooksLikeFormula() above is | ||
| 162 | // only a secondary check, applied after a whole-string exact match has | ||
| 163 | // already been ruled out: | ||
| 164 | // | ||
| 165 | // 1. Trimmed text exactly matches a real, currently-known variable name -> | ||
| 166 | // plain literal lookup (Variables::getVariable), exactly the old | ||
| 167 | // "Variable" Source behavior. Keeps arbitrary content (blobs, JSON, | ||
| 168 | // anything) reaching the comparator completely unmangled - never | ||
| 169 | // routed through the expression engine when the whole field is just | ||
| 170 | // naming one real variable. | ||
| 171 | // 2. Text already starts with '=', or already contains a "%%...%%"/ | ||
| 172 | // "==...==" marker -> hand to EvaluateConditionExpression() unchanged | ||
| 173 | // (old "Expression" Source behavior, advanced/manual syntax keeps | ||
| 174 | // working exactly as before). | ||
| 175 | // 3. LooksLikeFormula(text) -> prepend '=' and hand to | ||
| 176 | // EvaluateConditionExpression(). Routes through the pure-math branch, | ||
| 177 | // whose ExpressionProcessor::compile() already aliases any embedded | ||
| 178 | // real variable name (see aliasExpr() there) via plain substring | ||
| 179 | // find/replace BEFORE tinyexpr ever tokenizes the string - so | ||
| 180 | // "fpp_next_playlist_start+1" typed bare (no %%/=) correctly computes | ||
| 181 | // "that variable's value, plus one" instead of comparing against the | ||
| 182 | // literal text, with no new aliasing code needed here. | ||
| 183 | // 4. Otherwise -> hand to EvaluateConditionExpression() unchanged, which | ||
| 184 | // is template mode: safe literal passthrough for ordinary non-formula | ||
| 185 | // text like "ON" that doesn't match any real variable and doesn't look | ||
| 186 | // like a formula. Without this step, forcing '=' on every non-matching | ||
| 187 | // string would break any plain literal Value that isn't purely | ||
| 188 | // numeric/formula-shaped (compile failure -> empty result, not a | ||
| 189 | // crash, but silently wrong). | ||
| 190 | // | ||
| 191 | // An "=" formula, template or bare formula that doesn't compile sets compiled | ||
| 192 | // false: its value is no value, so callers treat it as not found. A bare one | ||
| 193 | // that ReadsAsWords() is the literal text after all ("on-air"). | ||
| 194 | 2015 | static std::string EvaluateNameOrExpression(const std::string& text, bool& compiled) { | |
| 195 | 2015 | compiled = true; | |
| 196 | 2015 | size_t start = text.find_first_not_of(" \t"); | |
| 197 |
2/2✓ Branch 0 taken 2014 times.
✓ Branch 1 taken 1 times.
|
2015 | if (start != std::string::npos) { |
| 198 | 2014 | size_t end = text.find_last_not_of(" \t"); | |
| 199 |
1/2✓ Branch 0 taken 2014 times.
✗ Branch 1 not taken.
|
2014 | std::string trimmed = text.substr(start, end - start + 1); |
| 200 |
3/4✓ Branch 0 taken 2014 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 56299 times.
✓ Branch 3 taken 1965 times.
|
58264 | for (auto const& varName : Variables::INSTANCE.getAllVariableNames()) { |
| 201 |
2/2✓ Branch 0 taken 49 times.
✓ Branch 1 taken 56250 times.
|
56299 | if (varName == trimmed) { |
| 202 |
2/4✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 49 times.
✗ Branch 3 not taken.
|
98 | return Variables::INSTANCE.getVariable(trimmed, ""); |
| 203 | } | ||
| 204 |
2/2✓ Branch 0 taken 1965 times.
✓ Branch 1 taken 49 times.
|
2014 | } |
| 205 |
2/2✓ Branch 0 taken 1965 times.
✓ Branch 1 taken 49 times.
|
2014 | } |
| 206 |
10/10✓ Branch 0 taken 1965 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1958 times.
✓ Branch 3 taken 7 times.
✓ Branch 4 taken 1956 times.
✓ Branch 5 taken 3 times.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 1955 times.
✓ Branch 8 taken 11 times.
✓ Branch 9 taken 1955 times.
|
3922 | if ((!text.empty() && text[0] == '=') || text.find("%%") != std::string::npos || |
| 207 | 1956 | text.find("==") != std::string::npos) { | |
| 208 | 11 | return EvaluateConditionExpression(text, &compiled); | |
| 209 | } | ||
| 210 |
2/2✓ Branch 0 taken 12 times.
✓ Branch 1 taken 1943 times.
|
1955 | if (LooksLikeFormula(text)) { |
| 211 |
2/4✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
|
12 | std::string v = EvaluateConditionExpression("=" + text, &compiled); |
| 212 |
7/8✓ Branch 0 taken 6 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 4 times.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 10 times.
|
12 | if (!compiled && ReadsAsWords(text)) { |
| 213 | 2 | compiled = true; | |
| 214 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | return text; |
| 215 | } | ||
| 216 | 10 | return v; | |
| 217 | 12 | } | |
| 218 | 1943 | return EvaluateConditionExpression(text); | |
| 219 | } | ||
| 220 | |||
| 221 | // Same branch order as EvaluateNameOrExpression() above (kept as a separate | ||
| 222 | // function rather than refactored into one, so a classify-only caller like | ||
| 223 | // GET /api/variables?validateExpression doesn't need a live Variables value | ||
| 224 | // snapshot round-tripped through it) - compileOk only means something for | ||
| 225 | // Formula (a real ExpressionProcessor::compile() happened); it's always true | ||
| 226 | // for Variable/Literal since neither one runs the compiler at all. | ||
| 227 | 7 | ConditionNode::ExpressionKind ConditionNode::ClassifyNameOrExpression(const std::string& text, bool& compileOk) { | |
| 228 | 7 | compileOk = true; | |
| 229 | 7 | size_t start = text.find_first_not_of(" \t"); | |
| 230 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | if (start != std::string::npos) { |
| 231 | 7 | size_t end = text.find_last_not_of(" \t"); | |
| 232 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | std::string trimmed = text.substr(start, end - start + 1); |
| 233 |
3/4✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 169 times.
✓ Branch 3 taken 6 times.
|
175 | for (auto const& varName : Variables::INSTANCE.getAllVariableNames()) { |
| 234 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 168 times.
|
169 | if (varName == trimmed) { |
| 235 | 1 | return ExpressionKind::Variable; | |
| 236 | } | ||
| 237 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1 times.
|
7 | } |
| 238 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1 times.
|
7 | } |
| 239 |
5/8✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 5 times.
|
6 | bool explicitExpr = (!text.empty() && text[0] == '=') || text.find("%%") != std::string::npos || text.find("==") != std::string::npos; |
| 240 |
6/6✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 5 times.
✓ Branch 5 taken 1 times.
|
6 | if (explicitExpr || LooksLikeFormula(text)) { |
| 241 |
1/2✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
|
5 | ExpressionProcessor proc; |
| 242 | 5 | std::map<std::string, ExpressionProcessor::ExpressionVariable> boundVars; | |
| 243 |
3/4✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 140 times.
✓ Branch 3 taken 5 times.
|
145 | for (auto const& varName : Variables::INSTANCE.getAllVariableNames()) { |
| 244 |
1/2✓ Branch 0 taken 140 times.
✗ Branch 1 not taken.
|
140 | auto inserted = boundVars.try_emplace(varName, varName); |
| 245 |
3/6✓ Branch 0 taken 140 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 140 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 140 times.
✗ Branch 5 not taken.
|
280 | inserted.first->second.setValue(Variables::INSTANCE.getVariable(varName)); |
| 246 |
1/2✓ Branch 0 taken 140 times.
✗ Branch 1 not taken.
|
140 | proc.bindVariable(&inserted.first->second); |
| 247 | 5 | } | |
| 248 |
5/8✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 5 times.
✗ Branch 7 not taken.
|
5 | compileOk = proc.compile(explicitExpr ? text : ("=" + text)); |
| 249 |
8/10✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 2 times.
✓ Branch 8 taken 1 times.
✓ Branch 9 taken 4 times.
|
5 | if (!compileOk && !explicitExpr && ReadsAsWords(text)) { |
| 250 | 1 | compileOk = true; | |
| 251 | 1 | return ExpressionKind::Literal; // as EvaluateNameOrExpression() reads it | |
| 252 | } | ||
| 253 | 4 | return ExpressionKind::Formula; | |
| 254 | 5 | } | |
| 255 | 1 | return ExpressionKind::Literal; | |
| 256 | } | ||
| 257 | |||
| 258 | 7 | static std::string CurrentTimeHHMM() { | |
| 259 | 7 | std::time_t t = std::time(nullptr); | |
| 260 | struct tm local; | ||
| 261 | 7 | localtime_r(&t, &local); | |
| 262 | char buf[8]; | ||
| 263 | 7 | snprintf(buf, sizeof(buf), "%02d:%02d", local.tm_hour, local.tm_min); | |
| 264 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
14 | return buf; |
| 265 | } | ||
| 266 | |||
| 267 | // Same Latitude/Longitude settings + fallback coordinates + SunRise usage as | ||
| 268 | // ScheduleEntry::GetTimeFromSun (ScheduleEntry.cpp:705-741). | ||
| 269 | 8 | static std::string SunTimeHHMM(const std::string& which) { | |
| 270 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | std::string latStr = getSetting("Latitude"); |
| 271 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | std::string lonStr = getSetting("Longitude"); |
| 272 | 8 | double lat = 38.938524, lon = -104.600945; | |
| 273 |
3/6✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
|
8 | if (!latStr.empty() && !lonStr.empty()) { |
| 274 | try { | ||
| 275 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | lat = std::stod(latStr); |
| 276 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | lon = std::stod(lonStr); |
| 277 | ✗ | } catch (...) { | |
| 278 | ✗ | } | |
| 279 | } | ||
| 280 | SunRise sr; | ||
| 281 | 8 | time_t now = std::time(nullptr); | |
| 282 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | sr.calculate(lat, lon, now); |
| 283 |
3/4✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 4 times.
|
8 | time_t t = (which == "Sunrise") ? sr.riseTime : sr.setTime; |
| 284 | struct tm local; | ||
| 285 | 8 | localtime_r(&t, &local); | |
| 286 | char buf[8]; | ||
| 287 | 8 | snprintf(buf, sizeof(buf), "%02d:%02d", local.tm_hour, local.tm_min); | |
| 288 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
16 | return buf; |
| 289 | 8 | } | |
| 290 | |||
| 291 | // Shared by LeafNode::readSource() (evaluate() path) and | ||
| 292 | // ConditionNode::PreviewSourceValue() (the If editor's "Show Current Value" | ||
| 293 | // preview button) - a single source of truth for what each Check source | ||
| 294 | // actually reads. | ||
| 295 | 1033 | static std::string ReadConditionSourceValue(const std::string& source, const std::string& name, bool& found) { | |
| 296 |
2/2✓ Branch 0 taken 999 times.
✓ Branch 1 taken 34 times.
|
1033 | if (source == "Expression") { |
| 297 | // The old, separate "Variable" Source was fully removed (per | ||
| 298 | // explicit direction, not kept as a backward-compatible alias) - its | ||
| 299 | // behavior lives here now, handled by EvaluateNameOrExpression()'s | ||
| 300 | // own exact-match check: a plain literal lookup for anything that's | ||
| 301 | // an exact match to a real variable name (old "Variable" behavior, | ||
| 302 | // including what a dedicated "MQTT" source used to do - | ||
| 303 | // Variables::getVariable() already reads a cached mqtt-<topic> | ||
| 304 | // message transparently), falling through to expression evaluation | ||
| 305 | // otherwise (old "Expression" behavior, now also reachable without | ||
| 306 | // typing '=' or "%%...%%" by hand - see EvaluateNameOrExpression's | ||
| 307 | // own comment). Any existing saved condition still using | ||
| 308 | // "source":"Variable" now reads as not-found until re-edited. | ||
| 309 | bool compiled; | ||
| 310 |
1/2✓ Branch 0 taken 999 times.
✗ Branch 1 not taken.
|
999 | std::string v = EvaluateNameOrExpression(name, compiled); |
| 311 | 999 | found = compiled; | |
| 312 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 999 times.
|
999 | return v; |
| 313 |
2/2✓ Branch 0 taken 7 times.
✓ Branch 1 taken 27 times.
|
34 | } else if (source == "Time") { |
| 314 | 7 | return CurrentTimeHHMM(); | |
| 315 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 23 times.
|
27 | } else if (source == "GPIO Pin") { |
| 316 | // fppCommandLastValue only tracks pins explicitly set via the "GPIO" | ||
| 317 | // command/API (an output) - it knows nothing about a configured | ||
| 318 | // GPIO Input's actual live state. Check that too so this source | ||
| 319 | // works for testing a real input, not just a previously-commanded | ||
| 320 | // output. | ||
| 321 | 4 | bool commanded = false; | |
| 322 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
|
4 | if (GPIOManager::GetCommandLastValue(name, commanded)) { |
| 323 | ✗ | return commanded ? "1" : "0"; | |
| 324 | } | ||
| 325 | 4 | int inputValue = 0; | |
| 326 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
|
4 | if (GPIOManager::INSTANCE.GetInputPinValue(name, inputValue)) { |
| 327 | ✗ | return inputValue ? "1" : "0"; | |
| 328 | } | ||
| 329 | 4 | found = false; | |
| 330 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
8 | return ""; |
| 331 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 15 times.
|
23 | } else if (source == "Sun") { |
| 332 | 8 | return SunTimeHHMM(name); // name: "Sunrise" or "Sunset" | |
| 333 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 12 times.
|
15 | } else if (source == "Day") { |
| 334 |
3/6✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
|
12 | return Variables::INSTANCE.getVariable("fpp_day_of_week"); |
| 335 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 9 times.
|
12 | } else if (source == "Month") { |
| 336 |
3/6✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
|
12 | return Variables::INSTANCE.getVariable("fpp_current_month"); |
| 337 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 8 times.
|
9 | } else if (source == "Player Status") { |
| 338 |
3/6✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
|
4 | return Variables::INSTANCE.getVariable("fpp_status_name"); |
| 339 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 7 times.
|
8 | } else if (source == "Is Playing") { |
| 340 |
3/6✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
|
4 | return Variables::INSTANCE.getVariable("fpp_is_playing"); |
| 341 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 6 times.
|
7 | } else if (source == "Current Playlist") { |
| 342 |
3/6✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
|
4 | return Variables::INSTANCE.getVariable("fpp_current_playlist"); |
| 343 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
|
6 | } else if (source == "Current Song") { |
| 344 | ✗ | return Variables::INSTANCE.getVariable("fpp_current_song"); | |
| 345 | } | ||
| 346 | 6 | found = false; | |
| 347 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
12 | return ""; |
| 348 | } | ||
| 349 | |||
| 350 | 12 | std::string ConditionNode::PreviewSourceValue(const std::string& source, const std::string& name, bool& found) { | |
| 351 | 12 | found = true; | |
| 352 | 12 | return ReadConditionSourceValue(source, name, found); | |
| 353 | } | ||
| 354 | |||
| 355 | // std::stod() alone isn't a valid "is this string a number" check - it | ||
| 356 | // happily parses a leading numeric prefix and silently ignores the rest | ||
| 357 | // (e.g. std::stod("18:15") == 18.0, no exception), which previously made | ||
| 358 | // LeafNode::compare()'s numeric comparators do the wrong thing for anything | ||
| 359 | // that merely starts with digits, most notably HH:MM values (Time/Sun) being | ||
| 360 | // compared by hour only. Require the ENTIRE string to have been consumed. | ||
| 361 | 34 | static bool ParseFullyNumeric(const std::string& s, double& out) { | |
| 362 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 34 times.
|
34 | if (s.empty()) { |
| 363 | ✗ | return false; | |
| 364 | } | ||
| 365 | 34 | size_t pos = 0; | |
| 366 | try { | ||
| 367 |
2/2✓ Branch 0 taken 30 times.
✓ Branch 1 taken 4 times.
|
34 | out = std::stod(s, &pos); |
| 368 | 4 | } catch (...) { | |
| 369 | 4 | return false; | |
| 370 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | } |
| 371 | 30 | return pos == s.size(); | |
| 372 | } | ||
| 373 | |||
| 374 | 1032 | static std::string TrimSpaces(const std::string& s) { | |
| 375 | 1032 | size_t start = s.find_first_not_of(" \t"); | |
| 376 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1031 times.
|
1032 | if (start == std::string::npos) { |
| 377 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
2 | return ""; |
| 378 | } | ||
| 379 | 1031 | return s.substr(start, s.find_last_not_of(" \t") - start + 1); | |
| 380 | } | ||
| 381 | |||
| 382 | // Time of day, day of week and month are cyclic: their order wraps (23:59 -> | ||
| 383 | // 00:00, Saturday -> Sunday, December -> January). Compared as text they | ||
| 384 | // sort in the wrong order ("Sunday" > "Saturday", "9:00" > "18:00"), std::stod | ||
| 385 | // can't read them, so between used to be false for every one of them. Map | ||
| 386 | // each to its position in its cycle so the comparators can use real order. | ||
| 387 | enum class CyclicKind { None, | ||
| 388 | TimeOfDay, | ||
| 389 | Weekday, | ||
| 390 | Month }; | ||
| 391 | |||
| 392 | 2002 | static int NamePosition(const std::string& s, const char* const* names, int count) { | |
| 393 | // Full English name (what the Day/Month sources return) or its first | ||
| 394 | // three letters, any case. | ||
| 395 |
2/2✓ Branch 0 taken 1956 times.
✓ Branch 1 taken 46 times.
|
2002 | if (s.size() < 3) { |
| 396 | 1956 | return -1; | |
| 397 | } | ||
| 398 |
2/2✓ Branch 0 taken 378 times.
✓ Branch 1 taken 30 times.
|
408 | for (int i = 0; i < count; i++) { |
| 399 | 378 | size_t len = strlen(names[i]); | |
| 400 |
7/8✓ Branch 0 taken 312 times.
✓ Branch 1 taken 66 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 312 times.
✓ Branch 4 taken 16 times.
✓ Branch 5 taken 50 times.
✓ Branch 6 taken 16 times.
✓ Branch 7 taken 362 times.
|
378 | if ((s.size() == len || s.size() == 3) && strncasecmp(s.c_str(), names[i], s.size()) == 0) { |
| 401 | 16 | return i; | |
| 402 | } | ||
| 403 | } | ||
| 404 | 30 | return -1; | |
| 405 | } | ||
| 406 | |||
| 407 | 1032 | static CyclicKind ParseCyclic(const std::string& raw, int& pos) { | |
| 408 | static const char* const kDays[7] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }; | ||
| 409 | static const char* const kMonths[12] = { "January", "February", "March", "April", "May", "June", | ||
| 410 | "July", "August", "September", "October", "November", "December" }; | ||
| 411 |
1/2✓ Branch 0 taken 1032 times.
✗ Branch 1 not taken.
|
1032 | std::string s = TrimSpaces(raw); |
| 412 | |||
| 413 | // H:MM, HH:MM, H:MM:SS or HH:MM:SS, 24 hour | ||
| 414 |
1/2✓ Branch 0 taken 1032 times.
✗ Branch 1 not taken.
|
1032 | std::vector<std::string> parts = split(s, ':'); |
| 415 |
5/6✓ Branch 0 taken 1005 times.
✓ Branch 1 taken 27 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1005 times.
✓ Branch 4 taken 27 times.
✓ Branch 5 taken 1005 times.
|
1032 | if (parts.size() == 2 || parts.size() == 3) { |
| 416 | 27 | int fields[3] = { 0, 0, 0 }; | |
| 417 | static const int kMax[3] = { 24, 60, 60 }; | ||
| 418 |
2/2✓ Branch 0 taken 54 times.
✓ Branch 1 taken 27 times.
|
81 | for (size_t i = 0; i < parts.size(); i++) { |
| 419 | 54 | const std::string& p = parts[i]; | |
| 420 |
7/12✓ Branch 0 taken 54 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 54 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 27 times.
✓ Branch 5 taken 27 times.
✓ Branch 6 taken 27 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 54 times.
✗ Branch 10 not taken.
✓ Branch 11 taken 54 times.
|
108 | if (p.empty() || p.size() > 2 || (i > 0 && p.size() != 2) || |
| 421 | 54 | p.find_first_not_of("0123456789") != std::string::npos) { | |
| 422 | ✗ | return CyclicKind::None; | |
| 423 | } | ||
| 424 |
1/2✓ Branch 0 taken 54 times.
✗ Branch 1 not taken.
|
54 | fields[i] = std::stoi(p); |
| 425 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 54 times.
|
54 | if (fields[i] >= kMax[i]) { |
| 426 | ✗ | return CyclicKind::None; | |
| 427 | } | ||
| 428 | } | ||
| 429 | 27 | pos = fields[0] * 3600 + fields[1] * 60 + fields[2]; | |
| 430 | 27 | return CyclicKind::TimeOfDay; | |
| 431 | } | ||
| 432 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 997 times.
|
1005 | if ((pos = NamePosition(s, kDays, 7)) >= 0) { |
| 433 | 8 | return CyclicKind::Weekday; | |
| 434 | } | ||
| 435 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 989 times.
|
997 | if ((pos = NamePosition(s, kMonths, 12)) >= 0) { |
| 436 | 8 | return CyclicKind::Month; | |
| 437 | } | ||
| 438 | 989 | return CyclicKind::None; | |
| 439 | 1032 | } | |
| 440 | |||
| 441 | // Hoisted out of LeafNode (was a private member function there) so both | ||
| 442 | // LeafNode::evaluate() and ConditionNode::PreviewLeafResult() (the Check | ||
| 443 | // editor's consolidated eye-preview modal) share exactly one implementation | ||
| 444 | // instead of a second copy risking drift. | ||
| 445 | 1006 | static bool CompareValues(const std::string& comparatorStr, const std::string& lhs, const std::string& rhs) { | |
| 446 | // Both sides a time of day, both a day of week or both a month: compare | ||
| 447 | // positions in the cycle. between is inclusive at both ends like the | ||
| 448 | // numeric one below, but a range whose first value is later than its | ||
| 449 | // second wraps round the end of the cycle instead of being swapped: | ||
| 450 | // "22:00,02:00" is overnight, "Friday,Sunday" the weekend and | ||
| 451 | // "November,February" winter. Swapping those would give exactly the | ||
| 452 | // opposite range. | ||
| 453 | 1006 | int lpos = 0; | |
| 454 |
1/2✓ Branch 0 taken 1006 times.
✗ Branch 1 not taken.
|
1006 | CyclicKind lkind = ParseCyclic(lhs, lpos); |
| 455 |
2/2✓ Branch 0 taken 17 times.
✓ Branch 1 taken 989 times.
|
1006 | if (lkind != CyclicKind::None) { |
| 456 |
3/4✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✓ Branch 3 taken 8 times.
|
17 | if (comparatorStr == "between") { |
| 457 | 9 | auto commaPos = rhs.find(','); | |
| 458 | 9 | int lo = 0, hi = 0; | |
| 459 | 27 | if (commaPos != std::string::npos && | |
| 460 |
5/12✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 9 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 9 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
|
27 | ParseCyclic(rhs.substr(0, commaPos), lo) == lkind && |
| 461 |
5/12✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 9 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 9 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
|
18 | ParseCyclic(rhs.substr(commaPos + 1), hi) == lkind) { |
| 462 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 4 times.
|
9 | if (lo <= hi) { |
| 463 |
3/4✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 1 times.
|
9 | return lpos >= lo && lpos <= hi; |
| 464 | } | ||
| 465 |
3/4✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
|
4 | return lpos >= lo || lpos <= hi; |
| 466 | } | ||
| 467 | } else { | ||
| 468 | 8 | int rpos = 0; | |
| 469 |
2/4✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
|
8 | if (ParseCyclic(rhs, rpos) == lkind) { |
| 470 |
2/4✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
|
8 | if (comparatorStr == "equal to") { |
| 471 | 8 | return lpos == rpos; | |
| 472 | } | ||
| 473 |
2/4✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
|
8 | if (comparatorStr == "not equal to") { |
| 474 | ✗ | return lpos != rpos; | |
| 475 | } | ||
| 476 |
3/4✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 4 times.
|
8 | if (comparatorStr == "greater than") { |
| 477 | 4 | return lpos > rpos; | |
| 478 | } | ||
| 479 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
|
4 | if (comparatorStr == "greater or equal") { |
| 480 | ✗ | return lpos >= rpos; | |
| 481 | } | ||
| 482 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
|
4 | if (comparatorStr == "less than") { |
| 483 | 4 | return lpos < rpos; | |
| 484 | } | ||
| 485 | ✗ | if (comparatorStr == "less or equal") { | |
| 486 | ✗ | return lpos <= rpos; | |
| 487 | } | ||
| 488 | } | ||
| 489 | } | ||
| 490 | } | ||
| 491 | |||
| 492 |
3/4✓ Branch 0 taken 989 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 967 times.
✓ Branch 3 taken 22 times.
|
989 | if (comparatorStr == "equal to") { |
| 493 | 967 | return lhs == rhs; | |
| 494 | } | ||
| 495 |
3/4✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 21 times.
|
22 | if (comparatorStr == "not equal to") { |
| 496 | 1 | return lhs != rhs; | |
| 497 | } | ||
| 498 |
3/4✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 19 times.
|
21 | if (comparatorStr == "contains") { |
| 499 | 2 | return lhs.find(rhs) != std::string::npos; | |
| 500 | } | ||
| 501 |
3/4✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 14 times.
|
19 | if (comparatorStr == "between") { |
| 502 | 5 | auto commaPos = rhs.find(','); | |
| 503 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4 times.
|
5 | if (commaPos == std::string::npos) { |
| 504 | 1 | return false; | |
| 505 | } | ||
| 506 | double mn, mx, v; | ||
| 507 |
2/4✓ Branch 0 taken 1 times.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
4 | if (!ParseFullyNumeric(rhs.substr(0, commaPos), mn) || |
| 508 |
9/18✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 3 times.
✓ Branch 11 taken 1 times.
✓ Branch 12 taken 4 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 4 times.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
|
7 | !ParseFullyNumeric(rhs.substr(commaPos + 1), mx) || |
| 509 |
2/4✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
|
3 | !ParseFullyNumeric(lhs, v)) { |
| 510 | 1 | return false; | |
| 511 | } | ||
| 512 | // Plain numbers have no wrap-around, so a reversed range is taken | ||
| 513 | // as the same range written backwards. | ||
| 514 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
|
3 | if (mx < mn) { |
| 515 | 1 | std::swap(mn, mx); | |
| 516 | } | ||
| 517 |
3/4✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
3 | return v >= mn && v <= mx; |
| 518 | } | ||
| 519 | // Numeric comparators. ParseFullyNumeric (not a bare std::stod) keeps | ||
| 520 | // anything that merely starts with digits out of here: std::stod alone | ||
| 521 | // does NOT throw on "18:15", it parses just the "18" and stops at the | ||
| 522 | // colon, which compared times by hour only. Times of day normally never | ||
| 523 | // get this far (the cyclic branch at the top takes them); one only does | ||
| 524 | // when the other side isn't a time, and then plain text comparison is | ||
| 525 | // the fallback. | ||
| 526 | double a, b; | ||
| 527 |
7/10✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 9 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 9 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 9 times.
✓ Branch 9 taken 5 times.
|
14 | if (ParseFullyNumeric(lhs, a) && ParseFullyNumeric(rhs, b)) { |
| 528 |
3/4✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 4 times.
|
9 | if (comparatorStr == "greater than") { |
| 529 | 5 | return a > b; | |
| 530 | } | ||
| 531 |
3/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 3 times.
|
4 | if (comparatorStr == "greater or equal") { |
| 532 | 1 | return a >= b; | |
| 533 | } | ||
| 534 |
3/4✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1 times.
|
3 | if (comparatorStr == "less than") { |
| 535 | 2 | return a < b; | |
| 536 | } | ||
| 537 |
2/4✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
|
1 | if (comparatorStr == "less or equal") { |
| 538 | 1 | return a <= b; | |
| 539 | } | ||
| 540 | } else { | ||
| 541 |
3/4✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 3 times.
|
5 | if (comparatorStr == "greater than") { |
| 542 | 2 | return lhs > rhs; | |
| 543 | } | ||
| 544 |
2/4✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
|
3 | if (comparatorStr == "greater or equal") { |
| 545 | ✗ | return lhs >= rhs; | |
| 546 | } | ||
| 547 |
3/4✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 2 times.
|
3 | if (comparatorStr == "less than") { |
| 548 | 1 | return lhs < rhs; | |
| 549 | } | ||
| 550 |
3/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
|
2 | if (comparatorStr == "less or equal") { |
| 551 | 1 | return lhs <= rhs; | |
| 552 | } | ||
| 553 | } | ||
| 554 | 1 | return false; | |
| 555 | } | ||
| 556 | |||
| 557 | class LeafNode : public ConditionNode { | ||
| 558 | public: | ||
| 559 | std::string source; | ||
| 560 | std::string name; | ||
| 561 | std::string comparatorStr; | ||
| 562 | std::string value; | ||
| 563 | bool negate = false; | ||
| 564 | |||
| 565 | 1017 | bool evaluate() const override { | |
| 566 | 1017 | bool found = true; | |
| 567 |
1/2✓ Branch 0 taken 1017 times.
✗ Branch 1 not taken.
|
1017 | std::string lhs = ReadConditionSourceValue(source, name, found); |
| 568 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1012 times.
|
1017 | if (!found) { |
| 569 |
3/4✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 3 times.
|
5 | if (source == "Expression") { |
| 570 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | WarnFormulaOnce(name); |
| 571 | } | ||
| 572 | 5 | bool result = negate; // "not found" reads as false, unless negated | |
| 573 |
3/4✓ Branch 0 taken 2 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
|
5 | LogDebug(VB_COMMAND, "If: Leaf[%s:%s] not found -> %s\n", source.c_str(), name.c_str(), |
| 574 | result ? "true" : "false"); | ||
| 575 | 5 | return result; | |
| 576 | } | ||
| 577 | // Same auto-detecting path as the merged Variable/Expression Source | ||
| 578 | // (EvaluateNameOrExpression, above): an exact match to a real | ||
| 579 | // variable name is a plain lookup, a bare formula like | ||
| 580 | // "fpp_next_playlist_start+1" is computed automatically, and | ||
| 581 | // anything else (e.g. a fixed literal like "1" or "ON") passes | ||
| 582 | // through unchanged - so Value can reference variables/math (e.g. | ||
| 583 | // comparing one computed value against another) without needing any | ||
| 584 | // manual "=" or "%%...%%" syntax. | ||
| 585 | bool rhsCompiled; | ||
| 586 |
1/2✓ Branch 0 taken 1012 times.
✗ Branch 1 not taken.
|
1012 | std::string rhs = EvaluateNameOrExpression(value, rhsCompiled); |
| 587 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1004 times.
|
1012 | if (!rhsCompiled) { |
| 588 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | WarnFormulaOnce(value); |
| 589 | 8 | bool result = negate; // not found, as above | |
| 590 |
4/6✓ Branch 0 taken 3 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
|
8 | LogDebug(VB_COMMAND, "If: Leaf[%s:%s] value \"%s\" failed to compile -> %s\n", source.c_str(), |
| 591 | name.c_str(), TruncateForLog(value).c_str(), result ? "true" : "false"); | ||
| 592 | 8 | return result; | |
| 593 | } | ||
| 594 |
1/2✓ Branch 0 taken 1004 times.
✗ Branch 1 not taken.
|
1004 | bool cmp = CompareValues(comparatorStr, lhs, rhs); |
| 595 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1003 times.
|
1004 | bool result = negate ? !cmp : cmp; |
| 596 | // lhs/rhs are unbounded (e.g. a Variable can hold an entire fetched | ||
| 597 | // web page) - truncate before logging, see TruncateForLog() (log.h). | ||
| 598 |
7/10✓ Branch 0 taken 978 times.
✓ Branch 1 taken 26 times.
✓ Branch 2 taken 1004 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 1003 times.
✓ Branch 6 taken 1004 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1004 times.
✗ Branch 9 not taken.
|
1004 | LogDebug(VB_COMMAND, "If: Leaf[%s:%s] = \"%s\" %s%s \"%s\" -> %s\n", source.c_str(), name.c_str(), |
| 599 | TruncateForLog(lhs).c_str(), negate ? "NOT " : "", comparatorStr.c_str(), | ||
| 600 | TruncateForLog(rhs).c_str(), result ? "true" : "false"); | ||
| 601 | 1004 | return result; | |
| 602 | 1017 | } | |
| 603 | }; | ||
| 604 | |||
| 605 | // Full ad-hoc-leaf evaluation for the Check editor's consolidated eye-preview | ||
| 606 | // modal (see Part 3 of the source-merge plan): given the same | ||
| 607 | // Source/Name/Comparator/Value/Not a saved leaf would have, returns the LHS | ||
| 608 | // and RHS values it currently resolves to and the comparator's result - | ||
| 609 | // reuses ReadConditionSourceValue/EvaluateNameOrExpression/CompareValues, the | ||
| 610 | // exact same functions LeafNode::evaluate() itself calls, so this can never | ||
| 611 | // drift from what an actual saved condition would do. | ||
| 612 | 4 | void ConditionNode::PreviewLeafResult(const std::string& source, const std::string& name, | |
| 613 | const std::string& comparatorStr, const std::string& value, bool negate, | ||
| 614 | bool& lhsFound, std::string& lhsValue, std::string& rhsValue, bool& result) { | ||
| 615 | 4 | lhsFound = true; | |
| 616 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | lhsValue = ReadConditionSourceValue(source, name, lhsFound); |
| 617 | bool rhsCompiled; | ||
| 618 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | rhsValue = EvaluateNameOrExpression(value, rhsCompiled); |
| 619 |
4/4✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 2 times.
|
4 | if (!lhsFound || !rhsCompiled) { |
| 620 | 2 | result = negate; | |
| 621 | 2 | return; | |
| 622 | } | ||
| 623 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | bool cmp = CompareValues(comparatorStr, lhsValue, rhsValue); |
| 624 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
|
3 | result = negate ? !cmp : cmp; |
| 625 | 1 | } | |
| 626 | |||
| 627 | 1038 | std::unique_ptr<ConditionNode> ConditionNode::FromJSON(const Json::Value& j) { | |
| 628 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1038 times.
|
1038 | if (!j.isObject()) { |
| 629 | ✗ | return nullptr; | |
| 630 | } | ||
| 631 |
2/2✓ Branch 0 taken 17 times.
✓ Branch 1 taken 1021 times.
|
1038 | if (j.isMember("op")) { |
| 632 |
1/2✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
|
17 | auto group = std::make_unique<GroupNode>(); |
| 633 |
3/6✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 17 times.
✗ Branch 5 not taken.
|
17 | group->isAnd = j["op"].asString() != "OR"; |
| 634 |
3/6✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 17 times.
✗ Branch 5 not taken.
|
17 | group->negate = j.get("not", false).asBool(); |
| 635 |
6/12✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 17 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 17 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 17 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 17 times.
✗ Branch 11 not taken.
|
17 | if (j.isMember("conditions") && j["conditions"].isArray()) { |
| 636 |
8/14✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 17 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 30 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 30 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 47 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 30 times.
✓ Branch 13 taken 17 times.
|
47 | for (auto& c : j["conditions"]) { |
| 637 |
1/2✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
|
30 | auto child = FromJSON(c); |
| 638 |
2/2✓ Branch 0 taken 28 times.
✓ Branch 1 taken 2 times.
|
30 | if (child) { |
| 639 |
1/2✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
|
28 | group->children.push_back(std::move(child)); |
| 640 | } else { | ||
| 641 | // Silently dropping a malformed child would otherwise look | ||
| 642 | // identical to the user's tree just having fewer | ||
| 643 | // conditions than they configured - log it so a bad edit | ||
| 644 | // (e.g. a hand-edited preset) is diagnosable. | ||
| 645 |
6/10✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 1 times.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
|
4 | LogWarn(VB_COMMAND, "If: dropping malformed condition entry under %s group: %s\n", |
| 646 | group->isAnd ? "AND" : "OR", TruncateForLog(SaveJsonToString(c)).c_str()); | ||
| 647 | } | ||
| 648 | 30 | } | |
| 649 | } | ||
| 650 | 17 | return group; | |
| 651 | 17 | } | |
| 652 |
2/2✓ Branch 0 taken 1019 times.
✓ Branch 1 taken 2 times.
|
1021 | if (j.isMember("source")) { |
| 653 |
1/2✓ Branch 0 taken 1019 times.
✗ Branch 1 not taken.
|
1019 | auto leaf = std::make_unique<LeafNode>(); |
| 654 |
2/4✓ Branch 0 taken 1019 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1019 times.
✗ Branch 3 not taken.
|
1019 | leaf->source = j["source"].asString(); |
| 655 |
3/6✓ Branch 0 taken 1019 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1019 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1019 times.
✗ Branch 5 not taken.
|
1019 | leaf->name = j.get("name", "").asString(); |
| 656 |
3/6✓ Branch 0 taken 1019 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1019 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1019 times.
✗ Branch 5 not taken.
|
1019 | leaf->comparatorStr = j.get("comparator", "").asString(); |
| 657 |
3/6✓ Branch 0 taken 1019 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1019 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1019 times.
✗ Branch 5 not taken.
|
1019 | leaf->value = j.get("value", "").asString(); |
| 658 |
4/6✓ Branch 0 taken 1019 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1019 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1018 times.
✓ Branch 5 taken 1 times.
|
1021 | leaf->negate = j.get("not", false).asBool(); |
| 659 | 1018 | return leaf; | |
| 660 | 1019 | } | |
| 661 |
4/8✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
|
2 | LogWarn(VB_COMMAND, "If: could not parse condition JSON (no \"op\" or \"source\" key): %s\n", |
| 662 | TruncateForLog(SaveJsonToString(j)).c_str()); | ||
| 663 | 2 | return nullptr; | |
| 664 | } | ||
| 665 |