Line data Source code
1 : #include "test_helpers.h"
2 : #include "when_expr.h"
3 : #include "raii.h"
4 : #include <stdlib.h>
5 : #include <string.h>
6 : #include <time.h>
7 :
8 : /* ── when_parse tests ──────────────────────────────────────────────── */
9 :
10 1 : static void test_parse_null_empty(void) {
11 1 : ASSERT(when_parse(NULL) == NULL, "NULL expr → NULL");
12 1 : ASSERT(when_parse("") == NULL, "empty expr → NULL");
13 1 : ASSERT(when_parse(" ") == NULL, "whitespace-only → NULL");
14 : }
15 :
16 1 : static void test_parse_single_atom(void) {
17 1 : WhenNode *n = when_parse("from:*@github.com");
18 1 : ASSERT(n != NULL, "single from atom parses");
19 1 : ASSERT(n->type == WN_FROM, "node type is FROM");
20 1 : ASSERT(strcmp(n->pattern, "*@github.com") == 0, "pattern correct");
21 1 : when_node_free(n);
22 : }
23 :
24 1 : static void test_parse_all_fields(void) {
25 : WhenNode *n;
26 :
27 1 : n = when_parse("to:*boss*");
28 1 : ASSERT(n && n->type == WN_TO, "to: field");
29 1 : when_node_free(n);
30 :
31 1 : n = when_parse("subject:*urgent*");
32 1 : ASSERT(n && n->type == WN_SUBJECT, "subject: field");
33 1 : when_node_free(n);
34 :
35 1 : n = when_parse("label:UNREAD");
36 1 : ASSERT(n && n->type == WN_LABEL, "label: field");
37 1 : when_node_free(n);
38 :
39 1 : n = when_parse("body:*password*");
40 1 : ASSERT(n && n->type == WN_BODY, "body: field");
41 1 : when_node_free(n);
42 :
43 1 : n = when_parse("age-gt:30");
44 1 : ASSERT(n && n->type == WN_AGE_GT && n->age_val == 30, "age-gt: field");
45 1 : when_node_free(n);
46 :
47 1 : n = when_parse("age-lt:7");
48 1 : ASSERT(n && n->type == WN_AGE_LT && n->age_val == 7, "age-lt: field");
49 1 : when_node_free(n);
50 : }
51 :
52 1 : static void test_parse_not(void) {
53 1 : WhenNode *n = when_parse("!from:*spam*");
54 1 : ASSERT(n != NULL, "NOT parses");
55 1 : ASSERT(n->type == WN_NOT, "root is NOT");
56 1 : ASSERT(n->left != NULL && n->left->type == WN_FROM, "child is FROM");
57 1 : when_node_free(n);
58 : }
59 :
60 1 : static void test_parse_and(void) {
61 1 : WhenNode *n = when_parse("from:*@a.hu* and subject:*news*");
62 1 : ASSERT(n != NULL, "AND parses");
63 1 : ASSERT(n->type == WN_AND, "root is AND");
64 1 : ASSERT(n->left->type == WN_FROM, "left is FROM");
65 1 : ASSERT(n->right->type == WN_SUBJECT, "right is SUBJECT");
66 1 : when_node_free(n);
67 : }
68 :
69 1 : static void test_parse_or(void) {
70 1 : WhenNode *n = when_parse("from:*@a.hu* or from:*@b.hu*");
71 1 : ASSERT(n != NULL, "OR parses");
72 1 : ASSERT(n->type == WN_OR, "root is OR");
73 1 : when_node_free(n);
74 : }
75 :
76 1 : static void test_parse_precedence(void) {
77 : /* "A or B and C" → "A or (B and C)" */
78 1 : WhenNode *n = when_parse("from:*@a.hu* or from:*@b.hu* and !label:UNREAD");
79 1 : ASSERT(n != NULL, "complex expr parses");
80 1 : ASSERT(n->type == WN_OR, "root is OR (lower prec)");
81 1 : ASSERT(n->right->type == WN_AND, "right subtree is AND");
82 1 : ASSERT(n->right->right->type == WN_NOT, "NOT binds tightest");
83 1 : when_node_free(n);
84 : }
85 :
86 1 : static void test_parse_parens(void) {
87 1 : WhenNode *n = when_parse("(from:*@a.hu* or from:*@b.hu*) and !label:UNREAD");
88 1 : ASSERT(n != NULL, "parens expr parses");
89 1 : ASSERT(n->type == WN_AND, "parens override: root is AND");
90 1 : ASSERT(n->left->type == WN_OR, "left is OR group");
91 1 : when_node_free(n);
92 : }
93 :
94 1 : static void test_parse_double_not(void) {
95 1 : WhenNode *n = when_parse("!!from:*@a.hu*");
96 1 : ASSERT(n != NULL, "double NOT parses");
97 1 : ASSERT(n->type == WN_NOT, "outer NOT");
98 1 : ASSERT(n->left->type == WN_NOT, "inner NOT");
99 1 : when_node_free(n);
100 : }
101 :
102 1 : static void test_parse_error_unknown_field(void) {
103 1 : WhenNode *n = when_parse("unknownfield:*x*");
104 1 : ASSERT(n == NULL, "unknown field → parse error → NULL");
105 : }
106 :
107 1 : static void test_parse_error_missing_rparen(void) {
108 1 : WhenNode *n = when_parse("(from:*@a.hu*");
109 1 : ASSERT(n == NULL, "missing ) → parse error → NULL");
110 : }
111 :
112 : /* ── when_eval tests ──────────────────────────────────────────────── */
113 :
114 1 : static void test_eval_null_ast_always_matches(void) {
115 1 : int r = when_eval(NULL, "a@b.com", "Hi", NULL, NULL, NULL, 0);
116 1 : ASSERT(r == 1, "NULL AST matches everything");
117 : }
118 :
119 1 : static void test_eval_from(void) {
120 1 : WhenNode *n = when_parse("from:*@github.com");
121 1 : ASSERT(n != NULL, "parse ok");
122 1 : ASSERT(when_eval(n, "notifications@github.com", "PR", NULL, NULL, NULL, 0) == 1, "from match");
123 1 : ASSERT(when_eval(n, "user@gmail.com", "PR", NULL, NULL, NULL, 0) == 0, "from no match");
124 1 : when_node_free(n);
125 : }
126 :
127 1 : static void test_eval_subject(void) {
128 1 : WhenNode *n = when_parse("subject:*URGENT*");
129 1 : ASSERT(n != NULL, "parse ok");
130 1 : ASSERT(when_eval(n, NULL, "This is URGENT", NULL, NULL, NULL, 0) == 1, "subject match");
131 1 : ASSERT(when_eval(n, NULL, "Normal mail", NULL, NULL, NULL, 0) == 0, "subject no match");
132 1 : when_node_free(n);
133 : }
134 :
135 1 : static void test_eval_label(void) {
136 1 : WhenNode *n = when_parse("label:UNREAD");
137 1 : ASSERT(n != NULL, "parse ok");
138 1 : ASSERT(when_eval(n, NULL, NULL, NULL, "INBOX,UNREAD", NULL, 0) == 1, "label present");
139 1 : ASSERT(when_eval(n, NULL, NULL, NULL, "INBOX,GitHub", NULL, 0) == 0, "label absent");
140 1 : ASSERT(when_eval(n, NULL, NULL, NULL, NULL, NULL, 0) == 0, "NULL labels csv");
141 1 : when_node_free(n);
142 : }
143 :
144 1 : static void test_eval_not(void) {
145 1 : WhenNode *n = when_parse("!from:*@spam.com*");
146 1 : ASSERT(n != NULL, "parse ok");
147 1 : ASSERT(when_eval(n, "user@legit.org", NULL, NULL, NULL, NULL, 0) == 1, "NOT: non-spam matches");
148 1 : ASSERT(when_eval(n, "ad@spam.com", NULL, NULL, NULL, NULL, 0) == 0, "NOT: spam rejected");
149 1 : when_node_free(n);
150 : }
151 :
152 1 : static void test_eval_and(void) {
153 1 : WhenNode *n = when_parse("from:*@a.hu* and subject:*news*");
154 1 : ASSERT(n != NULL, "parse ok");
155 1 : ASSERT(when_eval(n, "x@a.hu", "*news* report", NULL, NULL, NULL, 0) == 1, "AND: both match");
156 1 : ASSERT(when_eval(n, "x@a.hu", "hello", NULL, NULL, NULL, 0) == 0, "AND: subj fails");
157 1 : ASSERT(when_eval(n, "x@b.hu", "*news*", NULL, NULL, NULL, 0) == 0, "AND: from fails");
158 1 : when_node_free(n);
159 : }
160 :
161 1 : static void test_eval_or(void) {
162 1 : WhenNode *n = when_parse("from:*@a.hu* or from:*@b.hu*");
163 1 : ASSERT(n != NULL, "parse ok");
164 1 : ASSERT(when_eval(n, "x@a.hu", NULL, NULL, NULL, NULL, 0) == 1, "OR: first branch");
165 1 : ASSERT(when_eval(n, "x@b.hu", NULL, NULL, NULL, NULL, 0) == 1, "OR: second branch");
166 1 : ASSERT(when_eval(n, "x@c.hu", NULL, NULL, NULL, NULL, 0) == 0, "OR: neither");
167 1 : when_node_free(n);
168 : }
169 :
170 1 : static void test_eval_case_insensitive(void) {
171 1 : WhenNode *n = when_parse("from:*@GITHUB.com");
172 1 : ASSERT(n != NULL, "parse ok");
173 1 : ASSERT(when_eval(n, "notifications@github.com", NULL, NULL, NULL, NULL, 0) == 1,
174 : "case-insensitive match");
175 1 : when_node_free(n);
176 : }
177 :
178 1 : static void test_eval_age_gt(void) {
179 1 : WhenNode *n = when_parse("age-gt:10");
180 1 : ASSERT(n != NULL, "parse ok");
181 1 : time_t old = time(NULL) - 15 * 86400;
182 1 : time_t fresh = time(NULL) - 5 * 86400;
183 1 : ASSERT(when_eval(n, NULL, NULL, NULL, NULL, NULL, old) == 1, "age > 10 days matches");
184 1 : ASSERT(when_eval(n, NULL, NULL, NULL, NULL, NULL, fresh) == 0, "age < 10 days no match");
185 1 : ASSERT(when_eval(n, NULL, NULL, NULL, NULL, NULL, 0) == 0, "unknown date no match");
186 1 : when_node_free(n);
187 : }
188 :
189 1 : static void test_eval_age_lt(void) {
190 1 : WhenNode *n = when_parse("age-lt:7");
191 1 : ASSERT(n != NULL, "parse ok");
192 1 : time_t fresh = time(NULL) - 3 * 86400;
193 1 : time_t old = time(NULL) - 9 * 86400;
194 1 : ASSERT(when_eval(n, NULL, NULL, NULL, NULL, NULL, fresh) == 1, "age < 7 days matches");
195 1 : ASSERT(when_eval(n, NULL, NULL, NULL, NULL, NULL, old) == 0, "age > 7 days no match");
196 1 : when_node_free(n);
197 : }
198 :
199 1 : static void test_eval_body_null(void) {
200 1 : WhenNode *n = when_parse("body:*password*");
201 1 : ASSERT(n != NULL, "parse ok");
202 1 : ASSERT(when_eval(n, NULL, NULL, NULL, NULL, "please enter your password", 0) == 1, "body match");
203 1 : ASSERT(when_eval(n, NULL, NULL, NULL, NULL, NULL, 0) == 0, "NULL body no match");
204 1 : when_node_free(n);
205 : }
206 :
207 1 : static void test_eval_complex(void) {
208 1 : WhenNode *n = when_parse("(from:*@a.hu* or from:*@b.hu*) and !label:UNREAD");
209 1 : ASSERT(n != NULL, "complex parse ok");
210 1 : ASSERT(when_eval(n, "x@a.hu", NULL, NULL, "INBOX", NULL, 0) == 1, "from-a, no UNREAD");
211 1 : ASSERT(when_eval(n, "x@b.hu", NULL, NULL, "INBOX", NULL, 0) == 1, "from-b, no UNREAD");
212 1 : ASSERT(when_eval(n, "x@a.hu", NULL, NULL, "INBOX,UNREAD",NULL, 0) == 0, "from-a, has UNREAD");
213 1 : ASSERT(when_eval(n, "x@c.hu", NULL, NULL, "INBOX", NULL, 0) == 0, "other from");
214 1 : when_node_free(n);
215 : }
216 :
217 : /* ── when_from_flat tests ────────────────────────────────────────── */
218 :
219 1 : static void test_from_flat_null_all(void) {
220 1 : char *w = when_from_flat(NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0);
221 1 : ASSERT(w == NULL, "all-null flat → NULL");
222 : }
223 :
224 1 : static void test_from_flat_single(void) {
225 2 : RAII_STRING char *w = when_from_flat("*@github.com", NULL, NULL, NULL,
226 : NULL, NULL, NULL, NULL, 0, 0);
227 1 : ASSERT(w != NULL, "single from_flat not NULL");
228 1 : ASSERT(strcmp(w, "from:*@github.com") == 0, "single from_flat value");
229 : }
230 :
231 1 : static void test_from_flat_and_chain(void) {
232 2 : RAII_STRING char *w = when_from_flat("*@a.hu*", "*news*", NULL, NULL,
233 : NULL, NULL, NULL, NULL, 0, 0);
234 1 : ASSERT(w != NULL, "and-chain not NULL");
235 1 : ASSERT(strcmp(w, "from:*@a.hu* and subject:*news*") == 0, "and-chain value");
236 : }
237 :
238 1 : static void test_from_flat_negated(void) {
239 2 : RAII_STRING char *w = when_from_flat(NULL, NULL, NULL, NULL,
240 : "*@spam.com*", NULL, NULL, NULL, 0, 0);
241 1 : ASSERT(w != NULL, "negated from_flat not NULL");
242 1 : ASSERT(strcmp(w, "!from:*@spam.com*") == 0, "negated value");
243 : }
244 :
245 1 : static void test_from_flat_age(void) {
246 2 : RAII_STRING char *w = when_from_flat(NULL, NULL, NULL, NULL,
247 : NULL, NULL, NULL, NULL, 30, 0);
248 1 : ASSERT(w != NULL, "age-gt flat not NULL");
249 1 : ASSERT(strcmp(w, "age-gt:30") == 0, "age-gt value");
250 : }
251 :
252 : /* ── when_from_conds tests ──────────────────────────────────────── */
253 :
254 1 : static void test_from_conds_empty(void) {
255 1 : char *w = when_from_conds(NULL, 0, 1);
256 1 : ASSERT(w == NULL, "empty conds → NULL");
257 : }
258 :
259 1 : static void test_from_conds_or(void) {
260 1 : WhenCond cs[] = {
261 : { "from", "*@a.hu*", 0 },
262 : { "from", "*@b.hu*", 0 },
263 : { "from", "*@c.hu*", 0 },
264 : };
265 2 : RAII_STRING char *w = when_from_conds(cs, 3, 1);
266 1 : ASSERT(w != NULL, "OR conds not NULL");
267 1 : ASSERT(strcmp(w, "from:*@a.hu* or from:*@b.hu* or from:*@c.hu*") == 0, "OR chain value");
268 : }
269 :
270 1 : static void test_from_conds_and(void) {
271 1 : WhenCond cs[] = {
272 : { "from", "*@a.hu*", 0 },
273 : { "subject", "*news*", 0 },
274 : };
275 2 : RAII_STRING char *w = when_from_conds(cs, 2, 0);
276 1 : ASSERT(w != NULL, "AND conds not NULL");
277 1 : ASSERT(strcmp(w, "from:*@a.hu* and subject:*news*") == 0, "AND chain value");
278 : }
279 :
280 1 : static void test_from_conds_negated(void) {
281 1 : WhenCond cs[] = {
282 : { "from", "*@spam.com*", 1 },
283 : };
284 2 : RAII_STRING char *w = when_from_conds(cs, 1, 0);
285 1 : ASSERT(w != NULL, "negated cond not NULL");
286 1 : ASSERT(strcmp(w, "!from:*@spam.com*") == 0, "negated cond value");
287 : }
288 :
289 : /* Round-trip: build from_conds string, parse it, eval it */
290 1 : static void test_roundtrip_or_eval(void) {
291 1 : WhenCond cs[] = {
292 : { "from", "*@a.hu*", 0 },
293 : { "from", "*@b.hu*", 0 },
294 : };
295 2 : RAII_STRING char *w = when_from_conds(cs, 2, 1);
296 1 : ASSERT(w != NULL, "roundtrip: conds → string ok");
297 1 : WhenNode *tree = when_parse(w);
298 1 : ASSERT(tree != NULL, "roundtrip: parse ok");
299 1 : ASSERT(when_eval(tree, "x@a.hu", NULL, NULL, NULL, NULL, 0) == 1, "roundtrip: a matches");
300 1 : ASSERT(when_eval(tree, "x@b.hu", NULL, NULL, NULL, NULL, 0) == 1, "roundtrip: b matches");
301 1 : ASSERT(when_eval(tree, "x@c.hu", NULL, NULL, NULL, NULL, 0) == 0, "roundtrip: c no match");
302 1 : when_node_free(tree);
303 : }
304 :
305 : /* ── Registration ───────────────────────────────────────────────── */
306 :
307 1 : void test_when_expr(void) {
308 1 : RUN_TEST(test_parse_null_empty);
309 1 : RUN_TEST(test_parse_single_atom);
310 1 : RUN_TEST(test_parse_all_fields);
311 1 : RUN_TEST(test_parse_not);
312 1 : RUN_TEST(test_parse_and);
313 1 : RUN_TEST(test_parse_or);
314 1 : RUN_TEST(test_parse_precedence);
315 1 : RUN_TEST(test_parse_parens);
316 1 : RUN_TEST(test_parse_double_not);
317 1 : RUN_TEST(test_parse_error_unknown_field);
318 1 : RUN_TEST(test_parse_error_missing_rparen);
319 :
320 1 : RUN_TEST(test_eval_null_ast_always_matches);
321 1 : RUN_TEST(test_eval_from);
322 1 : RUN_TEST(test_eval_subject);
323 1 : RUN_TEST(test_eval_label);
324 1 : RUN_TEST(test_eval_not);
325 1 : RUN_TEST(test_eval_and);
326 1 : RUN_TEST(test_eval_or);
327 1 : RUN_TEST(test_eval_case_insensitive);
328 1 : RUN_TEST(test_eval_age_gt);
329 1 : RUN_TEST(test_eval_age_lt);
330 1 : RUN_TEST(test_eval_body_null);
331 1 : RUN_TEST(test_eval_complex);
332 :
333 1 : RUN_TEST(test_from_flat_null_all);
334 1 : RUN_TEST(test_from_flat_single);
335 1 : RUN_TEST(test_from_flat_and_chain);
336 1 : RUN_TEST(test_from_flat_negated);
337 1 : RUN_TEST(test_from_flat_age);
338 :
339 1 : RUN_TEST(test_from_conds_empty);
340 1 : RUN_TEST(test_from_conds_or);
341 1 : RUN_TEST(test_from_conds_and);
342 1 : RUN_TEST(test_from_conds_negated);
343 1 : RUN_TEST(test_roundtrip_or_eval);
344 1 : }
|