Code

frontend/grammar: Changed '<obj> <cmp> <expr>' to 'ANY <obj> <cmp> <expr>'.
[sysdb.git] / t / unit / frontend / parser_test.c
index bcd46af274c6bddd8a1d2bc6bbf8db27fcf9b2a0..fd9e32fa3ee19ee7e99edf7b1e0dc3fc49be1538 100644 (file)
@@ -32,6 +32,7 @@
 #include "libsysdb_test.h"
 
 #include <check.h>
+#include <limits.h>
 
 /*
  * tests
@@ -62,6 +63,12 @@ START_TEST(test_parse)
                { "LIST hosts; INVALID", 11,  1, CONNECTION_LIST   },
                { "LIST hosts FILTER "
                  "host = 'host'",       -1,  1, CONNECTION_LIST   },
+               { "LIST services",       -1,  1, CONNECTION_LIST   },
+               { "LIST services FILTER "
+                 "ANY service = 'svc'", -1,  1, CONNECTION_LIST   },
+               { "LIST metrics",        -1,  1, CONNECTION_LIST   },
+               { "LIST metrics FILTER "
+                 "ANY metric = 'm'",    -1,  1, CONNECTION_LIST   },
 
                { "LOOKUP hosts",        -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
@@ -70,32 +77,32 @@ START_TEST(test_parse)
                  "host = 'host'",       -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
                  "host =~ 'p' AND "
-                 "service =~ 'p'",      -1,  1, CONNECTION_LOOKUP },
+                 "ANY service =~ 'p'",  -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING NOT "
                  "host =~ 'p' AND "
-                 "service =~ 'p'",      -1,  1, CONNECTION_LOOKUP },
+                 "ANY service =~ 'p'",  -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
                  "host =~ 'p' AND "
-                 "service =~ 'p' OR "
-                 "service =~ 'r'",      -1,  1, CONNECTION_LOOKUP },
+                 "ANY service =~ 'p' OR "
+                 "ANY service =~ 'r'",  -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING NOT "
                  "host =~ 'p' AND "
-                 "service =~ 'p' OR "
-                 "service =~ 'r'",      -1,  1, CONNECTION_LOOKUP },
+                 "ANY service =~ 'p' OR "
+                 "ANY service =~ 'r'",  -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
                  "host =~ 'p' "
-                 "FILTER :age > 1D",    -1,  1, CONNECTION_LOOKUP },
+                 "FILTER .age > 1D",    -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
                  "host =~ 'p' "
-                 "FILTER :age > 1D AND "
-                 ":interval < 240s" ,   -1,  1, CONNECTION_LOOKUP },
+                 "FILTER .age > 1D AND "
+                 ".interval < 240s" ,   -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
                  "host =~ 'p' "
-                 "FILTER NOT :age>1D",  -1,  1, CONNECTION_LOOKUP },
+                 "FILTER NOT .age>1D",  -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
                  "host =~ 'p' "
-                 "FILTER :age>"
-                 ":interval",           -1,  1, CONNECTION_LOOKUP },
+                 "FILTER .age>"
+                 ".interval",           -1,  1, CONNECTION_LOOKUP },
 
                { "TIMESERIES 'host'.'metric' "
                  "START 2014-01-01 "
@@ -123,86 +130,93 @@ START_TEST(test_parse)
 
                /* numeric constants */
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo = "
+                 "attribute['foo'] = "
                  "1234",                -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo != "
+                 "attribute['foo'] != "
                  "+234",                -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo < "
+                 "attribute['foo'] < "
                  "-234",                -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo > "
+                 "attribute['foo'] > "
                  "12.4",                -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo <= "
+                 "attribute['foo'] <= "
                  "12. + .3",            -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo >= "
+                 "attribute['foo'] <= "
+                 "'f' || 'oo'",         -1,  1, CONNECTION_LOOKUP },
+               { "LOOKUP hosts MATCHING "
+                 "attribute['foo'] >= "
                  ".4",                  -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo = "
+                 "attribute['foo'] = "
                  "+12e3",               -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo = "
+                 "attribute['foo'] = "
                  "+12e-3",              -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo = "
+                 "attribute['foo'] = "
                  "-12e+3",              -1,  1, CONNECTION_LOOKUP },
 
                /* date, time, interval constants */
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo = "
+                 "attribute['foo'] = "
                  "1 Y 42D",             -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo = "
+                 "attribute['foo'] = "
                  "1s 42D",              -1,  1, CONNECTION_LOOKUP },
                /*
                 * TODO: Something like 1Y42D should work as well but it doesn't since
                 * the scanner will tokenize it into {digit}{identifier} :-/
                 *
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo = "
+                 "attribute['foo'] = "
                  "1Y42D",               -1,  1, CONNECTION_LOOKUP },
                 */
 
                /* NULL */
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo "
+                 "attribute['foo'] "
                  "IS NULL",             -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo "
+                 "attribute['foo'] "
                  "IS NOT NULL",         -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
-                 "NOT attribute.foo "
+                 "NOT attribute['foo'] "
                  "IS NULL",             -1,  1, CONNECTION_LOOKUP },
                { "LOOKUP hosts MATCHING "
                  "host IS NULL",        -1, -1, 0 },
                { "LOOKUP hosts MATCHING "
-                 "service IS NULL",     -1, -1, 0 },
+                 "ANY service IS NULL", -1, -1, 0 },
 
                /* invalid numeric constants */
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo = "
+                 "attribute['foo'] = "
                  "+-12e+3",             -1, -1, 0 },
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo = "
+                 "attribute['foo'] = "
                  "-12e-+3",             -1, -1, 0 },
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo = "
+                 "attribute['foo'] = "
                  "e+3",                 -1, -1, 0 },
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo = "
+                 "attribute['foo'] = "
                  "3e",                  -1, -1, 0 },
                /* following SQL standard, we don't support hex numbers */
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo = "
+                 "attribute['foo'] = "
                  "0x12",                -1, -1, 0 },
 
                /* invalid expressions */
                { "LOOKUP hosts MATCHING "
-                 "attribute.foo = "
+                 "attribute['foo'] = "
                  "1.23 + 'foo'",        -1, -1, 0 },
+               { "LOOKUP hosts MATCHING "
+                 "attr['foo'] = 1.23",  -1, -1, 0 },
+               { "LOOKUP hosts MATCHING "
+                 "attr['foo'] IS NULL", -1, -1, 0 },
 
                /* comments */
                { "/* some comment */",  -1,  0, 0 },
@@ -235,6 +249,12 @@ START_TEST(test_parse)
                { "LOOKUP foo MATCHING "
                  "host = 'host' FILTER "
                  "host = 'host'",       -1, -1, 0 },
+               { "LOOKUP hosts MATCHING "
+                 "attribute['foo'] <= "
+                 "f || 'oo'",           -1, -1, 0 },
+               { "LOOKUP hosts MATCHING "
+                 "attribute['foo'] <= "
+                 "'f' || oo",           -1, -1, 0 },
        };
 
        size_t i;
@@ -282,95 +302,142 @@ START_TEST(test_parse_matcher)
                int expected;
        } golden_data[] = {
                /* empty expressions */
-               { NULL,                        -1, -1 },
-               { "",                          -1, -1 },
-
-               /* valid expressions */
-               { "host = 'localhost'",        -1,  MATCHER_NAME },
-               { "host != 'localhost'",       -1,  MATCHER_NOT },
-               { "host =~ 'host'",            -1,  MATCHER_NAME },
-               { "host !~ 'host'",            -1,  MATCHER_NOT },
-               { "host = 'localhost' -- foo", -1,  MATCHER_NAME },
-               { "host = 'host' <garbage>",   13,  MATCHER_NAME },
+               { NULL,                           -1, -1 },
+               { "",                             -1, -1 },
+
+               /* match hosts by name */
+               { "host < 'localhost'",           -1,  MATCHER_LT },
+               { "host <= 'localhost'",          -1,  MATCHER_LE },
+               { "host = 'localhost'",           -1,  MATCHER_EQ },
+               { "host != 'localhost'",          -1,  MATCHER_NE },
+               { "host >= 'localhost'",          -1,  MATCHER_GE },
+               { "host > 'localhost'",           -1,  MATCHER_GT },
+               { "host =~ 'host'",               -1,  MATCHER_REGEX },
+               { "host !~ 'host'",               -1,  MATCHER_NREGEX },
+               { "host = 'localhost' -- foo",    -1,  MATCHER_EQ },
+               { "host = 'host' <garbage>",      13,  MATCHER_EQ },
+               { "host &^ 'localhost'",          -1,  -1 },
                /* match hosts by service */
-               { "service = 'name'",          -1,  MATCHER_NAME },
-               { "service != 'name'",         -1,  MATCHER_NOT },
-               { "service =~ 'pattern'",      -1,  MATCHER_NAME },
-               { "service !~ 'pattern'",      -1,  MATCHER_NOT },
+               { "ANY service < 'name'",         -1,  MATCHER_ANY },
+               { "ANY service <= 'name'",        -1,  MATCHER_ANY },
+               { "ANY service = 'name'",         -1,  MATCHER_ANY },
+               { "ANY service != 'name'",        -1,  MATCHER_ANY },
+               { "ANY service >= 'name'",        -1,  MATCHER_ANY },
+               { "ANY service > 'name'",         -1,  MATCHER_ANY },
+               { "ANY service =~ 'pattern'",     -1,  MATCHER_ANY },
+               { "ANY service !~ 'pattern'",     -1,  MATCHER_ANY },
+               { "ANY service &^ 'name'",        -1,  -1 },
+               /* match hosts by metric */
+               { "ANY metric < 'name'",          -1,  MATCHER_ANY },
+               { "ANY metric <= 'name'",         -1,  MATCHER_ANY },
+               { "ANY metric = 'name'",          -1,  MATCHER_ANY },
+               { "ANY metric != 'name'",         -1,  MATCHER_ANY },
+               { "ANY metric >= 'name'",         -1,  MATCHER_ANY },
+               { "ANY metric > 'name'",          -1,  MATCHER_ANY },
+               { "ANY metric =~ 'pattern'",      -1,  MATCHER_ANY },
+               { "ANY metric !~ 'pattern'",      -1,  MATCHER_ANY },
                /* match hosts by attribute */
-               { "attribute = 'name'",        -1,  MATCHER_NAME },
-               { "attribute != 'name'",       -1,  MATCHER_NOT },
-               { "attribute =~ 'pattern'",    -1,  MATCHER_NAME },
-               { "attribute !~ 'pattern'",    -1,  MATCHER_NOT },
+               { "ANY attribute < 'name'",       -1,  MATCHER_ANY },
+               { "ANY attribute <= 'name'",      -1,  MATCHER_ANY },
+               { "ANY attribute = 'name'",       -1,  MATCHER_ANY },
+               { "ANY attribute != 'name'",      -1,  MATCHER_ANY },
+               { "ANY attribute >= 'name'",      -1,  MATCHER_ANY },
+               { "ANY attribute > 'name'",       -1,  MATCHER_ANY },
+               { "ANY attribute =~ 'pattern'",   -1,  MATCHER_ANY },
+               { "ANY attribute !~ 'pattern'",   -1,  MATCHER_ANY },
+               { "ANY attribute &^ 'pattern'",   -1,  -1 },
                /* composite expressions */
                { "host =~ 'pattern' AND "
-                 "service =~ 'pattern'",      -1,  MATCHER_AND },
+                 "ANY service =~ 'pattern'",     -1,  MATCHER_AND },
                { "host =~ 'pattern' OR "
-                 "service =~ 'pattern'",      -1,  MATCHER_OR },
-               { "NOT host = 'host'",         -1,  MATCHER_NOT },
+                 "ANY service =~ 'pattern'",     -1,  MATCHER_OR },
+               { "NOT host = 'host'",            -1,  MATCHER_NOT },
                /* numeric expressions */
-               { "attribute.foo < 123",       -1,  MATCHER_LT },
-               { "attribute.foo <= 123",      -1,  MATCHER_LE },
-               { "attribute.foo = 123",       -1,  MATCHER_EQ },
-               { "attribute.foo >= 123",      -1,  MATCHER_GE },
-               { "attribute.foo > 123",       -1,  MATCHER_GT },
+               { "attribute['foo'] < 123",       -1,  MATCHER_LT },
+               { "attribute['foo'] <= 123",      -1,  MATCHER_LE },
+               { "attribute['foo'] = 123",       -1,  MATCHER_EQ },
+               { "attribute['foo'] >= 123",      -1,  MATCHER_GE },
+               { "attribute['foo'] > 123",       -1,  MATCHER_GT },
                /* datetime expressions */
-               { "attribute.foo = "
-                 "2014-08-16",                -1,  MATCHER_EQ },
-               { "attribute.foo = "
-                 "17:23",                     -1,  MATCHER_EQ },
-               { "attribute.foo = "
-                 "17:23:53",                  -1,  MATCHER_EQ },
-               { "attribute.foo = "
-                 "17:23:53.123",              -1,  MATCHER_EQ },
-               { "attribute.foo = "
-                 "17:23:53.123456789",        -1,  MATCHER_EQ },
-               { "attribute.foo = "
-                 "2014-08-16 17:23",          -1,  MATCHER_EQ },
-               { "attribute.foo = "
-                 "2014-08-16 17:23:53",       -1,  MATCHER_EQ },
+               { "attribute['foo'] = "
+                 "2014-08-16",                   -1,  MATCHER_EQ },
+               { "attribute['foo'] = "
+                 "17:23",                        -1,  MATCHER_EQ },
+               { "attribute['foo'] = "
+                 "17:23:53",                     -1,  MATCHER_EQ },
+               { "attribute['foo'] = "
+                 "17:23:53.123",                 -1,  MATCHER_EQ },
+               { "attribute['foo'] = "
+                 "17:23:53.123456789",           -1,  MATCHER_EQ },
+               { "attribute['foo'] = "
+                 "2014-08-16 17:23",             -1,  MATCHER_EQ },
+               { "attribute['foo'] = "
+                 "2014-08-16 17:23:53",          -1,  MATCHER_EQ },
                /* NULL; while this is an implementation detail,
                 * IS NULL currently maps to an equality matcher */
-               { "attribute.foo IS NULL",     -1,  MATCHER_ISNULL },
-               { "attribute.foo IS NOT NULL", -1,  MATCHER_NOT },
+               { "attribute['foo'] IS NULL",     -1,  MATCHER_ISNULL },
+               { "attribute['foo'] IS NOT NULL", -1,  MATCHER_ISNNULL },
 
                /* object field matchers */
-               { ":last_update < 10s",        -1,  MATCHER_LT },
-               { ":AGE <= 1m",                -1,  MATCHER_LE },
-               { ":interval = 10h",           -1,  MATCHER_EQ },
-               { ":Last_Update >= 24D",       -1,  MATCHER_GE },
-               { ":age > 1M",                 -1,  MATCHER_GT },
-               { ":age != 20Y",               -1,  MATCHER_NOT },
-               { ":backend != 'be'",          -1,  MATCHER_NOT },
-               { ":age <= 2 * :interval",     -1,  MATCHER_LE },
+               { ".name < 'a'",                  -1,  MATCHER_LT },
+               { ".name <= 'a'",                 -1,  MATCHER_LE },
+               { ".name = 'a'",                  -1,  MATCHER_EQ },
+               { ".name != 'a'",                 -1,  MATCHER_NE },
+               { ".name >= 'a'",                 -1,  MATCHER_GE },
+               { ".name > 'a'",                  -1,  MATCHER_GT },
+               { ".last_update < 2014-10-01",    -1,  MATCHER_LT },
+               { ".last_update <= 2014-10-01",   -1,  MATCHER_LE },
+               { ".last_update = 2014-10-01",    -1,  MATCHER_EQ },
+               { ".last_update != 2014-10-01",   -1,  MATCHER_NE },
+               { ".last_update >= 2014-10-01",   -1,  MATCHER_GE },
+               { ".last_update > 2014-10-01",    -1,  MATCHER_GT },
+               { ".Last_Update >= 24D",          -1,  MATCHER_GE },
+               { ".age < 20s",                   -1,  MATCHER_LT },
+               { ".age <= 20s",                  -1,  MATCHER_LE },
+               { ".age = 20s",                   -1,  MATCHER_EQ },
+               { ".age != 20s",                  -1,  MATCHER_NE },
+               { ".age >= 20s",                  -1,  MATCHER_GE },
+               { ".age > 20s",                   -1,  MATCHER_GT },
+               { ".AGE <= 1m",                   -1,  MATCHER_LE },
+               { ".age > 1M",                    -1,  MATCHER_GT },
+               { ".age != 20Y",                  -1,  MATCHER_NE },
+               { ".age <= 2 * .interval",        -1,  MATCHER_LE },
+               { ".interval < 20s",              -1,  MATCHER_LT },
+               { ".interval <= 20s",             -1,  MATCHER_LE },
+               { ".interval = 20s",              -1,  MATCHER_EQ },
+               { ".interval != 20s",             -1,  MATCHER_NE },
+               { ".interval >= 20s",             -1,  MATCHER_GE },
+               { ".interval > 20s",              -1,  MATCHER_GT },
+               { "'be' IN .backend",             -1,  MATCHER_IN },
 
                /* check operator precedence */
                { "host = 'name' OR "
-                 "service = 'name' AND "
-                 "attribute = 'name' OR "
-                 "attribute.foo = 'bar'",     -1,  MATCHER_OR },
+                 "ANY service = 'name' AND "
+                 "ANY attribute = 'name' OR "
+                 "attribute['foo'] = 'bar'",     -1,  MATCHER_OR },
                { "host = 'name' AND "
-                 "service = 'name' AND "
-                 "attribute = 'name' OR "
-                 "attribute.foo = 'bar'",     -1,  MATCHER_OR },
+                 "ANY service = 'name' AND "
+                 "ANY attribute = 'name' OR "
+                 "attribute['foo'] = 'bar'",     -1,  MATCHER_OR },
                { "host = 'name' AND "
-                 "service = 'name' OR "
-                 "attribute = 'name' AND "
-                 "attribute.foo = 'bar'",     -1,  MATCHER_OR },
+                 "ANY service = 'name' OR "
+                 "ANY attribute = 'name' AND "
+                 "attribute['foo'] = 'bar'",     -1,  MATCHER_OR },
                { "(host = 'name' OR "
-                 "service = 'name') AND "
-                 "(attribute = 'name' OR "
-                 "attribute.foo = 'bar')",    -1,  MATCHER_AND },
+                 "ANY service = 'name') AND "
+                 "(ANY attribute = 'name' OR "
+                 "attribute['foo'] = 'bar')",    -1,  MATCHER_AND },
                { "NOT host = 'name' OR "
-                 "service = 'name'",          -1,  MATCHER_OR },
+                 "ANY service = 'name'",         -1,  MATCHER_OR },
                { "NOT host = 'name' OR "
-                 "NOT service = 'name'",      -1,  MATCHER_OR },
+                 "NOT ANY service = 'name'",     -1,  MATCHER_OR },
                { "NOT (host = 'name' OR "
-                 "NOT service = 'name')",     -1,  MATCHER_NOT },
+                 "NOT ANY service = 'name')",    -1,  MATCHER_NOT },
 
                /* syntax errors */
-               { "LIST",                      -1, -1 },
-               { "foo &^ bar",                -1, -1 },
+               { "LIST",                         -1, -1 },
+               { "foo &^ bar",                   -1, -1 },
+               { ".invalid",                     -1, -1 },
        };
 
        size_t i;
@@ -398,6 +465,95 @@ START_TEST(test_parse_matcher)
 }
 END_TEST
 
+START_TEST(test_parse_expr)
+{
+       struct {
+               const char *expr;
+               int len;
+               int expected;
+       } golden_data[] = {
+               /* empty expressions */
+               { NULL,                   -1, INT_MAX },
+               { "",                     -1, INT_MAX },
+
+               /* constant expressions */
+               { "'localhost'",          -1, 0 },
+               { "123",                  -1, 0 },
+               { "2014-08-16",           -1, 0 },
+               { "17:23",                -1, 0 },
+               { "17:23:53",             -1, 0 },
+               { "17:23:53.123",         -1, 0 },
+               { "17:23:53.123456789",   -1, 0 },
+               { "2014-08-16 17:23",     -1, 0 },
+               { "2014-08-16 17:23:53",  -1, 0 },
+               { "10s",                  -1, 0 },
+               { "60m",                  -1, 0 },
+               { "10Y 24D 1h",           -1, 0 },
+
+               { "123 + 456",            -1, 0 },
+               { "'foo' || 'bar'",       -1, 0 },
+               { "456 - 123",            -1, 0 },
+               { "1.2 * 3.4",            -1, 0 },
+               { "1.2 / 3.4",            -1, 0 },
+               { "5 % 2",                -1, 0 },
+
+               /* queryable fields */
+               { ".last_update",         -1, FIELD_VALUE },
+               { ".AGE",                 -1, FIELD_VALUE },
+               { ".interval",            -1, FIELD_VALUE },
+               { ".Last_Update",         -1, FIELD_VALUE },
+               { ".backend",             -1, FIELD_VALUE },
+
+               /* attributes */
+               { "attribute['foo']",     -1, ATTR_VALUE },
+
+               /* arithmetic expressions */
+               { ".age + .age",          -1, SDB_DATA_ADD },
+               { ".age - .age",          -1, SDB_DATA_SUB },
+               { ".age * .age",          -1, SDB_DATA_MUL },
+               { ".age / .age",          -1, SDB_DATA_DIV },
+               { ".age % .age",          -1, SDB_DATA_MOD },
+               { ".age || .age",         -1, SDB_DATA_CONCAT },
+
+               /* operator precedence */
+               { ".age + .age * .age",   -1, SDB_DATA_ADD },
+               { ".age * .age + .age",   -1, SDB_DATA_ADD },
+               { ".age + .age - .age",   -1, SDB_DATA_SUB },
+               { ".age - .age + .age",   -1, SDB_DATA_ADD },
+               { "(.age + .age) * .age", -1, SDB_DATA_MUL },
+               { ".age + (.age * .age)", -1, SDB_DATA_ADD },
+
+               /* syntax errors */
+               { "LIST",                 -1, INT_MAX },
+               { "foo &^ bar",           -1, INT_MAX },
+               { ".invalid",             -1, INT_MAX },
+       };
+
+       size_t i;
+
+       for (i = 0; i < SDB_STATIC_ARRAY_LEN(golden_data); ++i) {
+               sdb_store_expr_t *e;
+               e = sdb_fe_parse_expr(golden_data[i].expr, golden_data[i].len);
+
+               if (golden_data[i].expected == INT_MAX) {
+                       fail_unless(e == NULL,
+                                       "sdb_fe_parse_expr(%s) = %p; expected: NULL",
+                                       golden_data[i].expr, e);
+                       continue;
+               }
+
+               fail_unless(e != NULL, "sdb_fe_parse_expr(%s) = NULL; "
+                               "expected: <expr>", golden_data[i].expr);
+               fail_unless(e->type == golden_data[i].expected,
+                               "sdb_fe_parse_expr(%s) returned expression of type %d; "
+                               "expected: %d", golden_data[i].expr, e->type,
+                               golden_data[i].expected);
+
+               sdb_object_deref(SDB_OBJ(e));
+       }
+}
+END_TEST
+
 Suite *
 fe_parser_suite(void)
 {
@@ -407,6 +563,7 @@ fe_parser_suite(void)
        tc = tcase_create("core");
        tcase_add_test(tc, test_parse);
        tcase_add_test(tc, test_parse_matcher);
+       tcase_add_test(tc, test_parse_expr);
        suite_add_tcase(s, tc);
 
        return s;