GCC Code Coverage Report


Directory: ./
File: commands/Condition.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 311 328 94.8%
Functions: 21 21 100.0%
Branches: 451 702 64.2%

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