From 5d66c96a190a396a1535c89bed4e33c2a005fe8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaroslav=20de=20la=20Pe=C3=B1a=20Smirnov?= Date: Thu, 24 Mar 2022 01:04:02 +0300 Subject: Initial commit Basically it works, just needs some polishing and maybe a couple of features that I could actually use. Also probably better docs. Not sure if it will be of use to anybody besides me. --- src/tests/lexer.c | 246 ++++++++++++++++++++++++ src/tests/parser.c | 542 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/tests/roscha.c | 189 +++++++++++++++++++ src/tests/slice.c | 41 ++++ 4 files changed, 1018 insertions(+) create mode 100644 src/tests/lexer.c create mode 100644 src/tests/parser.c create mode 100644 src/tests/roscha.c create mode 100644 src/tests/slice.c (limited to 'src/tests') 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 + +#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 + +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 + +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 + +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); +} -- cgit v1.2.3