aboutsummaryrefslogtreecommitdiff
path: root/src/tests
diff options
context:
space:
mode:
Diffstat (limited to 'src/tests')
-rw-r--r--src/tests/lexer.c246
-rw-r--r--src/tests/parser.c542
-rw-r--r--src/tests/roscha.c189
-rw-r--r--src/tests/slice.c41
4 files changed, 1018 insertions, 0 deletions
diff --git a/src/tests/lexer.c b/src/tests/lexer.c
new file mode 100644
index 0000000..a9cb142
--- /dev/null
+++ b/src/tests/lexer.c
@@ -0,0 +1,246 @@
+#include "tests/tests.h"
+#include "lexer.h"
+
+#include <string.h>
+
+#include "slice.h"
+#include "token.h"
+
+static void
+test_next_token(void)
+{
+ char *input = "{% extends \"template\" %}\n"
+ "{% block rooster %}\n"
+ "{% if true %}\n"
+ "some content\n"
+ "{% elif not false %}\n"
+ "other content\n"
+ "{% else %}\n"
+ "yet something else\n"
+ "{% endif %}\n"
+ "{% for v in list %}\n"
+ "{{ v+(1-2)*4/5 }}\n"
+ "{% break %}\n"
+ "{% endfor %}\n"
+ "{% endblock %}\n"
+ "{{ list[1] }}\n"
+ "{{ map.value }}\n"
+ "{{ 5 < 10 }}\n"
+ "{{ 10 > 5 }}\n"
+ "{{ 5 <= 10 }}\n"
+ "{{ 10 >= 5 }}\n"
+ "{{ 10 != 5 }}\n"
+ "{{ 5 == 5 }}\n"
+ "{{ 5 and 5 }}\n"
+ "{{ 5 or 5 }}\n";
+
+ token_init_keywords();
+ struct lexer *lexer = lexer_new(input);
+ struct token expected[] = {
+ { TOKEN_LBRACE, slice_whole("{"), 1, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 1, 2 },
+ { TOKEN_EXTENDS, slice_whole("extends"), 1, 4 },
+ { TOKEN_STRING, slice_whole("\"template\""), 1, 12 },
+ { TOKEN_PERCENT, slice_whole("%"), 1, 23 },
+ { TOKEN_RBRACE, slice_whole("}"), 1, 24 },
+ { TOKEN_CONTENT, slice_whole("\n"), 1, 25 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 2, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 2, 2 },
+ { TOKEN_BLOCK, slice_whole("block"), 2, 4 },
+ { TOKEN_IDENT, slice_whole("rooster"), 2, 10 },
+ { TOKEN_PERCENT, slice_whole("%"), 2, 18 },
+ { TOKEN_RBRACE, slice_whole("}"), 2, 19 },
+ { TOKEN_CONTENT, slice_whole("\n"), 2, 20 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 3, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 3, 2 },
+ { TOKEN_IF, slice_whole("if"), 3, 4 },
+ { TOKEN_TRUE, slice_whole("true"), 3, 7 },
+ { TOKEN_PERCENT, slice_whole("%"), 3, 12 },
+ { TOKEN_RBRACE, slice_whole("}"), 3, 13 },
+ { TOKEN_CONTENT, slice_whole("\nsome content\n"), 3, 14 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 5, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 5, 2 },
+ { TOKEN_ELIF, slice_whole("elif"), 5, 4 },
+ { TOKEN_NOT, slice_whole("not"), 5, 9 },
+ { TOKEN_FALSE, slice_whole("false"), 5, 12 },
+ { TOKEN_PERCENT, slice_whole("%"), 5, 18 },
+ { TOKEN_RBRACE, slice_whole("}"), 5, 19 },
+ { TOKEN_CONTENT, slice_whole("\nother content\n"), 5, 20 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 7, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 7, 2 },
+ { TOKEN_ELSE, slice_whole("else"), 7, 4 },
+ { TOKEN_PERCENT, slice_whole("%"), 7, 9 },
+ { TOKEN_RBRACE, slice_whole("}"), 7, 10 },
+ { TOKEN_CONTENT, slice_whole("\nyet something else\n"), 7, 11 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 9, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 9, 2 },
+ { TOKEN_ENDIF, slice_whole("endif"), 9, 4 },
+ { TOKEN_PERCENT, slice_whole("%"), 9, 10 },
+ { TOKEN_RBRACE, slice_whole("}"), 9, 11 },
+ { TOKEN_CONTENT, slice_whole("\n"), 9, 12 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 10, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 10, 2 },
+ { TOKEN_FOR, slice_whole("for"), 10, 4 },
+ { TOKEN_IDENT, slice_whole("v"), 10, 8 },
+ { TOKEN_IN, slice_whole("in"), 10, 10 },
+ { TOKEN_IDENT, slice_whole("list"), 10, 13 },
+ { TOKEN_PERCENT, slice_whole("%"), 10, 18 },
+ { TOKEN_RBRACE, slice_whole("}"), 10, 19 },
+ { TOKEN_CONTENT, slice_whole("\n"), 10, 20 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 11, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 11, 2 },
+ { TOKEN_IDENT, slice_whole("v"), 11, 4 },
+ { TOKEN_PLUS, slice_whole("+"), 11, 5 },
+ { TOKEN_LPAREN, slice_whole("("), 11, 6 },
+ { TOKEN_INT, slice_whole("1"), 11, 7 },
+ { TOKEN_MINUS, slice_whole("-"), 11, 8 },
+ { TOKEN_INT, slice_whole("2"), 11, 9 },
+ { TOKEN_RPAREN, slice_whole(")"), 11, 10 },
+ { TOKEN_ASTERISK, slice_whole("*"), 11, 11 },
+ { TOKEN_INT, slice_whole("4"), 11, 12 },
+ { TOKEN_SLASH, slice_whole("/"), 11, 13 },
+ { TOKEN_INT, slice_whole("5"), 11, 14 },
+ { TOKEN_RBRACE, slice_whole("}"), 11, 16 },
+ { TOKEN_RBRACE, slice_whole("}"), 11, 17 },
+ { TOKEN_CONTENT, slice_whole("\n"), 11, 18 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 12, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 12, 2 },
+ { TOKEN_BREAK, slice_whole("break"), 12, 4 },
+ { TOKEN_PERCENT, slice_whole("%"), 12, 10 },
+ { TOKEN_RBRACE, slice_whole("}"), 12, 11 },
+ { TOKEN_CONTENT, slice_whole("\n"), 12, 12 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 13, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 13, 2 },
+ { TOKEN_ENDFOR, slice_whole("endfor"), 13, 4 },
+ { TOKEN_PERCENT, slice_whole("%"), 13, 11 },
+ { TOKEN_RBRACE, slice_whole("}"), 13, 12 },
+ { TOKEN_CONTENT, slice_whole("\n"), 13, 13 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 14, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 14, 2 },
+ { TOKEN_ENDBLOCK, slice_whole("endblock"), 14, 4 },
+ { TOKEN_PERCENT, slice_whole("%"), 14, 13 },
+ { TOKEN_RBRACE, slice_whole("}"), 14, 14 },
+ { TOKEN_CONTENT, slice_whole("\n"), 14, 15 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 15, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 15, 2 },
+ { TOKEN_IDENT, slice_whole("list"), 15, 4 },
+ { TOKEN_LBRACKET, slice_whole("["), 15, 8 },
+ { TOKEN_INT, slice_whole("1"), 15, 9 },
+ { TOKEN_RBRACKET, slice_whole("]"), 15, 10 },
+ { TOKEN_RBRACE, slice_whole("}"), 15, 12 },
+ { TOKEN_RBRACE, slice_whole("}"), 15, 13 },
+ { TOKEN_CONTENT, slice_whole("\n"), 15, 14 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 16, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 16, 2 },
+ { TOKEN_IDENT, slice_whole("map"), 16, 4 },
+ { TOKEN_DOT, slice_whole("."), 16, 7 },
+ { TOKEN_IDENT, slice_whole("value"), 16, 8 },
+ { TOKEN_RBRACE, slice_whole("}"), 16, 10 },
+ { TOKEN_RBRACE, slice_whole("}"), 16, 11 },
+ { TOKEN_CONTENT, slice_whole("\n"), 16, 12 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 17, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 17, 2 },
+ { TOKEN_INT, slice_whole("5"), 17, 4 },
+ { TOKEN_LT, slice_whole("<"), 17, 6 },
+ { TOKEN_INT, slice_whole("10"), 17, 8 },
+ { TOKEN_RBRACE, slice_whole("}"), 17, 11 },
+ { TOKEN_RBRACE, slice_whole("}"), 17, 12 },
+ { TOKEN_CONTENT, slice_whole("\n"), 17, 13 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 18, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 18, 2 },
+ { TOKEN_INT, slice_whole("10"), 18, 4 },
+ { TOKEN_GT, slice_whole(">"), 18, 7 },
+ { TOKEN_INT, slice_whole("5"), 18, 9 },
+ { TOKEN_RBRACE, slice_whole("}"), 18, 11 },
+ { TOKEN_RBRACE, slice_whole("}"), 18, 12 },
+ { TOKEN_CONTENT, slice_whole("\n"), 18, 13 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 19, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 19, 2 },
+ { TOKEN_INT, slice_whole("5"), 19, 4 },
+ { TOKEN_LTE, slice_whole("<="), 19, 6 },
+ { TOKEN_INT, slice_whole("10"), 19, 9 },
+ { TOKEN_RBRACE, slice_whole("}"), 19, 12 },
+ { TOKEN_RBRACE, slice_whole("}"), 19, 13 },
+ { TOKEN_CONTENT, slice_whole("\n"), 19, 14 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 20, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 20, 2 },
+ { TOKEN_INT, slice_whole("10"), 20, 4 },
+ { TOKEN_GTE, slice_whole(">="), 20, 7 },
+ { TOKEN_INT, slice_whole("5"), 20, 10 },
+ { TOKEN_RBRACE, slice_whole("}"), 20, 12 },
+ { TOKEN_RBRACE, slice_whole("}"), 20, 13 },
+ { TOKEN_CONTENT, slice_whole("\n"), 20, 14 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 21, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 21, 2 },
+ { TOKEN_INT, slice_whole("10"), 21, 4 },
+ { TOKEN_NOTEQ, slice_whole("!="), 21, 7 },
+ { TOKEN_INT, slice_whole("5"), 21, 10 },
+ { TOKEN_RBRACE, slice_whole("}"), 21, 12 },
+ { TOKEN_RBRACE, slice_whole("}"), 21, 13 },
+ { TOKEN_CONTENT, slice_whole("\n"), 21, 14 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 22, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 22, 2 },
+ { TOKEN_INT, slice_whole("5"), 22, 4 },
+ { TOKEN_EQ, slice_whole("=="), 22, 6 },
+ { TOKEN_INT, slice_whole("5"), 22, 9 },
+ { TOKEN_RBRACE, slice_whole("}"), 22, 11 },
+ { TOKEN_RBRACE, slice_whole("}"), 22, 12 },
+ { TOKEN_CONTENT, slice_whole("\n"), 22, 13 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 23, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 23, 2 },
+ { TOKEN_INT, slice_whole("5"), 23, 4 },
+ { TOKEN_AND, slice_whole("and"), 23, 6 },
+ { TOKEN_INT, slice_whole("5"), 23, 10 },
+ { TOKEN_RBRACE, slice_whole("}"), 23, 12 },
+ { TOKEN_RBRACE, slice_whole("}"), 23, 13 },
+ { TOKEN_CONTENT, slice_whole("\n"), 23, 14 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 24, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 24, 2 },
+ { TOKEN_INT, slice_whole("5"), 24, 4 },
+ { TOKEN_OR, slice_whole("or"), 24, 6 },
+ { TOKEN_INT, slice_whole("5"), 24, 9 },
+ { TOKEN_RBRACE, slice_whole("}"), 24, 11 },
+ { TOKEN_RBRACE, slice_whole("}"), 24, 12 },
+ { TOKEN_CONTENT, slice_whole("\n"), 24, 13 },
+
+ { TOKEN_EOF, },
+ };
+ size_t i = 0;
+
+ do {
+ struct token token = lexer_next_token(lexer);
+ asserteq(token.type, expected[i].type);
+ asserteq(slice_cmp(&token.literal, &expected[i].literal), 0);
+ i++;
+ } while (expected[i].type != TOKEN_EOF);
+
+ lexer_destroy(lexer);
+ token_free_keywords();
+}
+
+int
+main(void)
+{
+ INIT_TESTS();
+ RUN_TEST(test_next_token);
+}
diff --git a/src/tests/parser.c b/src/tests/parser.c
new file mode 100644
index 0000000..eb5c2a1
--- /dev/null
+++ b/src/tests/parser.c
@@ -0,0 +1,542 @@
+#define _POSIX_C_SOURCE 200809L
+#include "parser.h"
+#include "tests/tests.h"
+
+#include "ast.h"
+#include "slice.h"
+
+#include <string.h>
+
+enum value_type {
+ VALUE_IDENT,
+ VALUE_INT,
+ VALUE_BOOL,
+ VALUE_STRING,
+};
+
+struct value {
+ union {
+ struct slice ident;
+ struct slice string;
+ int64_t integer;
+ bool boolean;
+ };
+ enum value_type type;
+};
+
+static void
+check_parser_errors(struct parser *parser, const char *file, int line,
+ const char *func)
+{
+ if (parser->errors->len > 0) {
+ printf("\n");
+ size_t i;
+ sds val;
+ vector_foreach (parser->errors, i, val) {
+ printf("parser error %lu: %s\n", i, val);
+ }
+ printf(TBLD TRED "FAIL!\n" TRST);
+ printf("%s:%d: %s: ", file, line, func);
+ printf("parser encountered errors\n");
+ abort();
+ }
+}
+
+#define check_parser_errors(p) \
+ check_parser_errors(p, __FILE__, __LINE__, __func__)
+
+static void
+test_integer_literal(struct expression *expr, int64_t val)
+{
+ char buf[128];
+ struct slice sval;
+ asserteq(expr->type, EXPRESSION_INT);
+ asserteq(expr->integer.value, val);
+ sprintf(buf, "%ld", val);
+ sval = slice_whole(buf);
+ asserteq(slice_cmp(&expr->token.literal, &sval), 0);
+}
+
+static void
+test_identifier(struct expression *expr, struct slice *ident)
+{
+ asserteq(expr->type, EXPRESSION_IDENT);
+ asserteq(slice_cmp(&expr->token.literal, ident), 0);
+}
+
+static void
+test_boolean_literal(struct expression *expr, bool val)
+{
+ char *str = val ? "true" : "false";
+ struct slice sval = slice_whole(str);
+ asserteq(expr->type, EXPRESSION_BOOL);
+ asserteq(expr->boolean.value, val);
+ asserteq(slice_cmp(&expr->token.literal, &sval), 0);
+}
+
+static void
+test_string_literal(struct expression *expr, const struct slice *val)
+{
+ asserteq(expr->type, EXPRESSION_STRING);
+ asserteq(slice_cmp(&expr->string.value, val), 0);
+}
+
+
+static inline void
+test_expected(struct expression *expr, struct value v)
+{
+ switch(v.type) {
+ case VALUE_IDENT:
+ test_identifier(expr, &v.ident);
+ break;
+ case VALUE_INT:
+ test_integer_literal(expr, v.integer);
+ break;
+ case VALUE_BOOL:
+ test_boolean_literal(expr, v.boolean);
+ break;
+ case VALUE_STRING:
+ test_string_literal(expr, &v.string);
+ break;
+ }
+}
+
+#define VIDENT(v) \
+ (struct value){ .type = VALUE_IDENT, .ident = slice_whole(v) }
+
+#define VINT(v) \
+ (struct value){ .type = VALUE_INT, .integer = v }
+
+#define VBOOL(v) \
+ (struct value){ .type = VALUE_BOOL, .boolean = v }
+
+#define VSTR(v) \
+ (struct value){ .type = VALUE_STRING, .string = slice_whole(v) }
+
+static inline void
+test_infix(struct infix *expr, struct value lval, struct slice *op,
+ struct value rval)
+{
+ test_expected(expr->left, lval);
+ asserteq(slice_cmp(&expr->operator, op), 0);
+ test_expected(expr->right, rval);
+}
+
+static inline void
+test_literal_variables(void)
+{
+ struct {
+ char *input;
+ struct value val;
+ } tests[] = {
+ { "{{ foo }}", VIDENT("foo"), },
+ { "{{ 20 }}", VINT(20), },
+ { "{{ true }}", VBOOL(true), },
+ { "{{ false }}", VBOOL(false), },
+ { 0 },
+ };
+ for (size_t i = 0; tests[i].input != NULL; i++) {
+ struct parser *parser = parser_new(strdup("test"), tests[i].input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_VARIABLE);
+ test_expected(blk->variable.expression, tests[i].val);
+ parser_destroy(parser);
+ template_destroy(tmpl);
+ }
+}
+
+static inline void
+test_prefix_variables(void)
+{
+ struct {
+ char *input;
+ struct slice operator;
+ struct value val;
+ } tests[] = {
+ { "{{ !foo }}", slice_whole("!"), VIDENT("foo"), },
+ { "{{ -bar }}", slice_whole("-"), VIDENT("bar"), },
+ { "{{ -20 }}", slice_whole("-"), VINT(20), },
+ { "{{ !true }}", slice_whole("!"), VBOOL(true), },
+ { "{{ !false }}", slice_whole("!"), VBOOL(false), },
+ { "{{ not false }}", slice_whole("not"), VBOOL(false), },
+ { 0 },
+ };
+ for (size_t i = 0; tests[i].input != NULL; i++) {
+ struct parser *parser = parser_new(strdup("test"), tests[i].input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_VARIABLE);
+ asserteq(blk->variable.expression->type, EXPRESSION_PREFIX);
+ struct expression *pref = blk->variable.expression;
+ asserteq(slice_cmp(&pref->prefix.operator, &tests[i].operator), 0);
+ test_expected(pref->prefix.right, tests[i].val);
+ parser_destroy(parser);
+ template_destroy(tmpl);
+ }
+}
+
+static inline void
+test_infix_variables(void)
+{
+ struct {
+ char *input;
+ struct value left;
+ struct slice operator;
+ struct value right;
+ } tests[] = {
+ { "{{ foo + bar }}", VIDENT("foo"), slice_whole("+"), VIDENT("bar"), },
+ { "{{ 6 - 9 }}", VINT(6),slice_whole("-"), VINT(9), },
+ { "{{ 4 * 20 }}", VINT(4), slice_whole("*"), VINT(20), },
+ { "{{ foo / 20 }}", VIDENT("foo"), slice_whole("/"), VINT(20), },
+ { "{{ \"str\" == \"str\" }}", VSTR("str"), slice_whole("=="), VSTR("str"), },
+ { "{{ true != false }}", VBOOL(true), slice_whole("!="), VBOOL(false), },
+ { "{{ 4 < 20 }}", VINT(4), slice_whole("<"), VINT(20), },
+ { "{{ 4 <= 20 }}", VINT(4), slice_whole("<="), VINT(20), },
+ { "{{ 100 > 20 }}", VINT(100), slice_whole(">"), VINT(20), },
+ { "{{ 100 >= 20 }}", VINT(100), slice_whole(">="), VINT(20), },
+ { "{{ true and true }}", VBOOL(true), slice_whole("and"), VBOOL(true), },
+ { "{{ true or false }}", VBOOL(true), slice_whole("or"), VBOOL(false), },
+ { 0 },
+ };
+ for (size_t i = 0; tests[i].input != NULL; i++) {
+ struct parser *parser = parser_new(strdup("test"), tests[i].input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_VARIABLE);
+ asserteq(blk->variable.expression->type, EXPRESSION_INFIX);
+ test_infix(&blk->variable.expression->infix,
+ tests[i].left, &tests[i].operator, tests[i].right);
+ parser_destroy(parser);
+ template_destroy(tmpl);
+ }
+}
+
+static inline void
+test_map_variables(void)
+{
+ char *input = "{{ map.key }}";
+ struct value left = VIDENT("map");
+ struct value key = VIDENT("key");
+ struct parser *parser = parser_new(strdup("test"), input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_VARIABLE);
+ asserteq(blk->variable.expression->type, EXPRESSION_MAPKEY);
+ struct expression *map = blk->variable.expression;
+ test_expected(map->indexkey.left, left);
+ test_expected(map->indexkey.key, key);
+ parser_destroy(parser);
+ template_destroy(tmpl);
+}
+
+static inline void
+test_index_variables(void)
+{
+ char *input = "{{ arr[1 + 2] }}";
+ struct value left = VIDENT("arr");
+ struct value ileft = VINT(1);
+ struct slice iop = slice_whole("+");
+ struct value iright = VINT(2);
+ struct parser *parser = parser_new(strdup("test"), input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_VARIABLE);
+ asserteq(blk->variable.expression->type, EXPRESSION_INDEX);
+ struct expression *map = blk->variable.expression;
+ test_expected(map->indexkey.left, left);
+ test_infix(&map->indexkey.key->infix, ileft, &iop, iright);
+
+ parser_destroy(parser);
+ template_destroy(tmpl);
+}
+
+static inline void
+test_operator_precedence(void)
+{
+ struct {
+ char *input;
+ char *expected;
+ } tests[] = {
+ {
+ "{{ -a * b }}",
+ "{{ ((-a) * b) }}",
+ },
+ {
+ "{{ 1 + 2 * 3 }}",
+ "{{ (1 + (2 * 3)) }}",
+ },
+ {
+ "{{ !-a }}",
+ "{{ (!(-a)) }}",
+ },
+ {
+ "{{ a + b + c }}",
+ "{{ ((a + b) + c) }}",
+ },
+ {
+ "{{ a + b - c }}",
+ "{{ ((a + b) - c) }}",
+ },
+ {
+ "{{ a * b * c }}",
+ "{{ ((a * b) * c) }}",
+ },
+ {
+ "{{ a * b / c }}",
+ "{{ ((a * b) / c) }}",
+ },
+ {
+ "{{ a + b / c }}",
+ "{{ (a + (b / c)) }}",
+ },
+ {
+ "{{ a + b * c + d / e - f }}",
+ "{{ (((a + (b * c)) + (d / e)) - f) }}",
+ },
+ {
+ "{{ 5 > 4 == 3 < 4 }}",
+ "{{ ((5 > 4) == (3 < 4)) }}",
+ },
+ {
+ "{{ 5 < 4 != 3 > 4 }}",
+ "{{ ((5 < 4) != (3 > 4)) }}",
+ },
+ {
+ "{{ 3 + 4 * 5 == 3 * 1 + 4 * 5 }}",
+ "{{ ((3 + (4 * 5)) == ((3 * 1) + (4 * 5))) }}",
+ },
+ {
+ "{{ (5 + 5) * 2 }}",
+ "{{ ((5 + 5) * 2) }}",
+ },
+ {
+ "{{ 2 / (5 + 5) }}",
+ "{{ (2 / (5 + 5)) }}",
+ },
+ {
+ "{{ (5 + 5) * 2 * (5 + 5) }}",
+ "{{ (((5 + 5) * 2) * (5 + 5)) }}",
+ },
+ {
+ "{{ -(5 + 5) }}",
+ "{{ (-(5 + 5)) }}",
+ },
+ {
+ "{{ foo[0] + bar[1 + 2] }}",
+ "{{ (foo[0] + bar[(1 + 2)]) }}",
+ },
+ {
+ "{{ foo.bar + foo.baz }}",
+ "{{ (foo.bar + foo.baz) }}",
+ },
+ {
+ "{{ foo.bar + bar[0].baz * foo.bar.baz }}",
+ "{{ (foo.bar + (bar[0].baz * foo.bar.baz)) }}",
+ },
+ { 0 },
+ };
+ for (size_t i = 0; tests[i].input != NULL; i++) {
+ struct parser *parser = parser_new(strdup("test"), tests[i].input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_VARIABLE);
+ sds output = block_string(blk, sdsempty());
+ asserteq(strcmp(output, tests[i].expected), 0);
+ sdsfree(output);
+ parser_destroy(parser);
+ template_destroy(tmpl);
+ }
+}
+
+static inline void
+test_loop_tag(void)
+{
+ char *input = "{% for v in seq %}"
+ "{% break %}"
+ "{% endfor %}";
+ struct slice item = slice_whole("v");
+ struct slice seq = slice_whole("seq");
+ struct parser *parser = parser_new(strdup("test"), input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_TAG);
+ asserteq(blk->tag.type, TAG_FOR);
+ asserteq(slice_cmp(&blk->tag.loop.item.token.literal, &item), 0);
+ struct expression *seqexpr = blk->tag.loop.seq;
+ asserteq(seqexpr->type, EXPRESSION_IDENT);
+ asserteq(slice_cmp(&seqexpr->ident.token.literal, &seq), 0);
+ asserteq(blk->tag.loop.subblocks->len, 2);
+ struct block *sub1 = blk->tag.loop.subblocks->values[0];
+ struct block *sub2 = blk->tag.loop.subblocks->values[1];
+ asserteq(sub1->type, BLOCK_TAG);
+ asserteq(sub2->type, BLOCK_TAG);
+ asserteq(sub1->tag.type, TAG_BREAK);
+ asserteq(sub2->tag.type, TAG_CLOSE);
+ asserteq(sub2->tag.token.type, TOKEN_ENDFOR);
+
+ parser_destroy(parser);
+ template_destroy(tmpl);
+}
+
+static inline void
+test_cond_tag(void)
+{
+ char *input = "{% if false %}"
+ "{{ foo }}"
+ "{% elif 1 > 2 %}"
+ "{{ bar }}"
+ "{% else %}"
+ "baz"
+ "{% endif %}";
+ struct value ifexp = VIDENT("foo");
+ struct value elifl = VINT(1);
+ struct slice elifop = slice_whole(">");
+ struct value elifr = VINT(2);
+ struct value elifexp = VIDENT("bar");
+ struct slice elsecont = slice_whole("baz");
+ struct parser *parser = parser_new(strdup("test"), input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_TAG);
+ asserteq(blk->tag.type, TAG_IF);
+
+ struct branch *b1 = blk->tag.cond.root;
+ assertneq(b1->condition, NULL);
+ asserteq(b1->condition->type, EXPRESSION_BOOL);
+ asserteq(b1->condition->token.type, TOKEN_FALSE);
+ asserteq(b1->subblocks->len, 1);
+ struct block *b1sub1 = b1->subblocks->values[0];
+ asserteq(b1sub1->type, BLOCK_VARIABLE);
+ test_expected(b1sub1->variable.expression, ifexp);
+
+ struct branch *b2 = b1->next;
+ assertneq(b2->condition, NULL);
+ test_infix(&b2->condition->infix, elifl, &elifop, elifr);
+ asserteq(b2->subblocks->len, 1);
+ struct block *b2sub1 = b2->subblocks->values[0];
+ asserteq(b2sub1->type, BLOCK_VARIABLE);
+ test_expected(b2sub1->variable.expression, elifexp);
+
+ struct branch *b3 = b2->next;
+ asserteq(b3->condition, NULL);
+ asserteq(b3->subblocks->len, 2);
+ struct block *b3sub1 = b3->subblocks->values[0];
+ asserteq(b3sub1->type, BLOCK_CONTENT);
+ asserteq(slice_cmp(&b3sub1->content.token.literal, &elsecont), 0);
+ struct block *b3sub2 = b3->subblocks->values[1];
+ asserteq(b3sub2->tag.type, TAG_CLOSE);
+ asserteq(b3sub2->tag.token.type, TOKEN_ENDIF);
+
+ parser_destroy(parser);
+ template_destroy(tmpl);
+}
+
+static inline void
+test_parent_tag(void)
+{
+ char *input = "{% extends \"base.html\" %}";
+ struct slice name = slice_whole("base.html");
+ struct parser *parser = parser_new(strdup("test"), input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_TAG);
+ asserteq(blk->tag.type, TAG_EXTENDS);
+ asserteq(slice_cmp(&blk->tag.parent.name->value, &name), 0);
+
+ parser_destroy(parser);
+ template_destroy(tmpl);
+}
+
+static inline void
+test_tblock_tag(void)
+{
+ char *input = "{% block cock %}"
+ "{% endblock %}";
+ struct slice name = slice_whole("cock");
+ struct parser *parser = parser_new(strdup("test"), input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_TAG);
+ asserteq(blk->tag.type, TAG_BLOCK);
+ asserteq(slice_cmp(&blk->tag.tblock.name.token.literal, &name), 0);
+ asserteq(blk->tag.tblock.subblocks->len, 1);
+ struct block *sub1 = blk->tag.tblock.subblocks->values[0];
+ asserteq(sub1->type, BLOCK_TAG);
+ asserteq(sub1->tag.type, TAG_CLOSE);
+ asserteq(sub1->tag.token.type, TOKEN_ENDBLOCK);
+
+ blk = hmap_gets(tmpl->tblocks, &name);
+ assertneq(blk, NULL);
+ asserteq(blk->type, BLOCK_TAG);
+ asserteq(blk->tag.type, TAG_BLOCK);
+ asserteq(slice_cmp(&blk->tag.tblock.name.token.literal, &name), 0);
+
+ parser_destroy(parser);
+ template_destroy(tmpl);
+}
+
+static void
+init(void)
+{
+ parser_init();
+}
+
+static void
+cleanup(void)
+{
+ parser_deinit();
+}
+
+int
+main(void)
+{
+ init();
+ INIT_TESTS();
+ RUN_TEST(test_literal_variables);
+ RUN_TEST(test_prefix_variables);
+ RUN_TEST(test_infix_variables);
+ RUN_TEST(test_map_variables);
+ RUN_TEST(test_index_variables);
+ RUN_TEST(test_operator_precedence);
+ RUN_TEST(test_loop_tag);
+ RUN_TEST(test_cond_tag);
+ RUN_TEST(test_parent_tag);
+ RUN_TEST(test_tblock_tag);
+ cleanup();
+}
diff --git a/src/tests/roscha.c b/src/tests/roscha.c
new file mode 100644
index 0000000..92b8bc4
--- /dev/null
+++ b/src/tests/roscha.c
@@ -0,0 +1,189 @@
+#define _POSIX_C_SOURCE 200809L
+#include "tests/tests.h"
+#include "roscha.h"
+
+#include <string.h>
+
+static void
+check_env_errors(struct roscha_env *env, const char *file, int line,
+ const char *func)
+{
+ struct vector *errors = roscha_env_check_errors(env);
+ if (!errors) return;
+ printf("\n");
+ size_t i;
+ sds val;
+ vector_foreach (errors, i, val) {
+ printf("parser error %lu: %s\n", i, val);
+ }
+ printf(TBLD TRED "FAIL!\n" TRST);
+ printf("%s:%d: %s: ", file, line, func);
+ printf("parser encountered errors\n");
+ abort();
+}
+
+#define check_env_errors(p) \
+ check_env_errors(p, __FILE__, __LINE__, __func__)
+
+static void
+test_eval_variable(void)
+{
+ char *input = "{{ foo.bar }}"
+ "{{ foo.bar + foo.baz }}"
+ "{{ foo.bar - foo.baz }}"
+ "{{ foo.bar * foo.baz }}"
+ "{{ foo.bar / foo.baz }}"
+ "{{ l }}"
+ "{{ l[0] }}, {{ l[1] }}"
+ "";
+ char *expected = "8"
+ "12"
+ "4"
+ "32"
+ "2"
+ "[ hello, world, ]"
+ "hello, world"
+ "";
+ struct roscha_object *foo = roscha_object_new(hmap_new());
+ roscha_hmap_set_new(foo, "bar", 8);
+ roscha_hmap_set_new(foo, "baz", 4);
+ struct roscha_object *l = roscha_object_new(vector_new());
+ roscha_vector_push_new(l, (slice_whole("hello")));
+ roscha_vector_push_new(l, (slice_whole("world")));
+ struct roscha_env *env = roscha_env_new();
+ roscha_env_add_template(env, strdup("test"), input);
+ check_env_errors(env);
+ roscha_hmap_set(env->vars, "foo", foo);
+ roscha_hmap_set(env->vars, "l", l);
+
+ sds got = roscha_env_render(env, "test");
+ check_env_errors(env);
+ asserteq(strcmp(got, expected), 0);
+ roscha_env_destroy(env);
+ sdsfree(got);
+ roscha_object_unref(foo);
+ roscha_object_unref(l);
+}
+
+static void
+test_eval_cond(void)
+{
+ char *input = "{% if foo > bar %}"
+ "Yes"
+ "{% elif baz %}"
+ "Maybe"
+ "{% else %}"
+ "No"
+ "{% endif %}";
+ char *expected1 = "No";
+ char *expected2 = "Maybe";
+ char *expected3 = "Yes";
+ struct roscha_object *foo = roscha_object_new(10);
+ struct roscha_object *bar = roscha_object_new(20);
+ struct roscha_object *baz = roscha_object_new(69);
+
+ struct roscha_env *env = roscha_env_new();
+ roscha_env_add_template(env, strdup("test"), input);
+ check_env_errors(env);
+ roscha_hmap_set(env->vars, "foo", foo);
+ roscha_hmap_set(env->vars, "bar", bar);
+
+ sds got = roscha_env_render(env, "test");
+ check_env_errors(env);
+ asserteq(strcmp(got, expected1), 0);
+ sdsfree(got);
+
+ roscha_hmap_set(env->vars, "baz", baz);
+ got = roscha_env_render(env, "test");
+ check_env_errors(env);
+ asserteq(strcmp(got, expected2), 0);
+ sdsfree(got);
+
+ foo->integer = 420;
+ got = roscha_env_render(env, "test");
+ check_env_errors(env);
+ asserteq(strcmp(got, expected3), 0);
+ sdsfree(got);
+
+ roscha_env_destroy(env);
+ roscha_object_unref(foo);
+ roscha_object_unref(bar);
+ roscha_object_unref(baz);
+}
+
+static void
+test_eval_loop(void)
+{
+ char *input = "{% for v in foo %}"
+ "{{ loop.index }}"
+ "{{ v }}"
+ "{% endfor %}";
+ char *expected = "0hello1world";
+
+ struct roscha_object *foo = roscha_object_new(vector_new());
+ roscha_vector_push_new(foo, (slice_whole("hello")));
+ roscha_vector_push_new(foo, (slice_whole("world")));
+
+ struct roscha_env *env = roscha_env_new();
+ roscha_env_add_template(env, strdup("test"), input);
+ check_env_errors(env);
+ roscha_hmap_set(env->vars, "foo", foo);
+ sds got = roscha_env_render(env, "test");
+ check_env_errors(env);
+ asserteq(strcmp(got, expected), 0);
+
+ sdsfree(got);
+ roscha_env_destroy(env);
+ roscha_object_unref(foo);
+}
+
+static void
+test_eval_child(void)
+{
+ char *parent = "hello{% block title %}{% endblock %}"
+ "{% block content %}Content{% endblock %}"
+ "{% block foot %}Foot{% endblock %}";
+ char *child = "{% extends \"parent\" %}"
+ "{% block title %}, world{% endblock %}"
+ "{% block content %}"
+ "In a beautiful place out in the country."
+ "{% endblock %}";
+ char *expected = "hello, world"
+ "In a beautiful place out in the country."
+ "Foot";
+ struct roscha_env *env = roscha_env_new();
+ roscha_env_add_template(env, strdup("parent"), parent);
+ check_env_errors(env);
+ roscha_env_add_template(env, strdup("child"), child);
+ check_env_errors(env);
+ sds got = roscha_env_render(env, "child");
+ check_env_errors(env);
+ asserteq(strcmp(got, expected), 0);
+
+ sdsfree(got);
+ roscha_env_destroy(env);
+}
+
+static void
+init(void)
+{
+ roscha_init();
+}
+
+static void
+cleanup(void)
+{
+ roscha_deinit();
+}
+
+int
+main(void)
+{
+ init();
+ INIT_TESTS();
+ RUN_TEST(test_eval_variable);
+ RUN_TEST(test_eval_cond);
+ RUN_TEST(test_eval_loop);
+ RUN_TEST(test_eval_child);
+ cleanup();
+}
diff --git a/src/tests/slice.c b/src/tests/slice.c
new file mode 100644
index 0000000..1f3875a
--- /dev/null
+++ b/src/tests/slice.c
@@ -0,0 +1,41 @@
+#include "tests/tests.h"
+#include "slice.h"
+
+#include <string.h>
+
+static void
+test_slice_cmp(void)
+{
+ struct slice s1a = {
+ .str = "hello world",
+ .start = 6,
+ .end = 11,
+ };
+ struct slice s1b = {
+ .str = "world",
+ .start = 0,
+ .end = 5,
+ };
+ asserteq(slice_cmp(&s1a, &s1b), 0);
+}
+
+static void
+test_slice_string(void)
+{
+ struct slice slice = {
+ .str = "hello world",
+ .start = 6,
+ .end = 11,
+ };
+ sds str = sdsempty();
+ slice_string(&slice, str);
+ asserteq(strcmp("world", str), 0);
+}
+
+int
+main(void)
+{
+ INIT_TESTS();
+ RUN_TEST(test_slice_cmp);
+ RUN_TEST(test_slice_string);
+}