Files

625 lines
20 KiB
C

/* LINK: ../../src/kdl/lexer.c ../../src/error.c ../../src/span.c */
/* tests/unit/test_lexer.c
*
* Unit tests for the KDL 2.0.0 tokenizer (todo 6).
*
* The magic LINK comment on line 1 is REQUIRED by tests/run.sh: it lists
* the extra .c sources to compile into this test binary (paths relative
* to tests/unit/, space-separated). lexer.c depends on error.c (typed
* errors) and span.c (caret rendering), so all three are linked.
*
* These tests assert on REAL token streams — kind, span (line/col) and
* the text slice — never just "does not crash".
*/
#include "munit.h"
#include "error.h"
#include "kdl/lexer.h"
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#define MAX_TOKS 256
#define MAX_TEXT 512
/* A single captured token. `text` is a NUL-terminated copy of the lexer's
* text slice (which is otherwise not NUL-terminated). */
struct captured_tok {
enum st_token_kind kind;
size_t line;
size_t col;
char text[MAX_TEXT];
};
/* A full captured stream. When the lexer hits an error, `had_error` is
* set and the error's category + span + message are copied out (values,
* not borrowed pointers) before the lexer is freed — this sidesteps the
* borrowed-span lifetime entirely inside the test. */
struct capture {
struct captured_tok toks[MAX_TOKS];
size_t n;
bool had_error;
int err_category;
size_t err_line;
size_t err_col;
char err_msg[MAX_TEXT];
};
/* Tokenize `src` to completion. On a lex error the tokens produced before
* the error are still captured (and the error info is recorded). */
static struct capture
capture(const char *src)
{
struct capture cap;
struct st_lexer *lx;
struct st_error *err = NULL;
memset(&cap, 0, sizeof cap);
lx = st_lexer_new(src, "test.kdl");
munit_assert_not_null(lx);
for (;;) {
struct st_token t;
enum st_token_kind k = st_lexer_next(lx, &t, &err);
if (k == ST_TOK_ERROR) {
cap.had_error = true;
if (err != NULL) {
cap.err_category = (int)st_error_category_of(err);
if (err->span != NULL) {
cap.err_line = err->span->line;
cap.err_col = err->span->col;
}
snprintf(cap.err_msg, sizeof cap.err_msg, "%s",
st_error_message(err));
st_error_free(err);
}
break;
}
munit_assert_size(cap.n, <, MAX_TOKS);
cap.toks[cap.n].kind = k;
cap.toks[cap.n].line = t.span.line;
cap.toks[cap.n].col = t.span.col;
{
size_t len = t.len < MAX_TEXT - 1 ? t.len : MAX_TEXT - 1;
memcpy(cap.toks[cap.n].text, t.text, len);
cap.toks[cap.n].text[len] = '\0';
}
cap.n++;
if (k == ST_TOK_EOF) {
break;
}
}
st_lexer_free(lx);
return cap;
}
/* Assert token i is (kind, line, col, text). */
static void
assert_tok(const struct capture *cap, size_t i, enum st_token_kind kind,
size_t line, size_t col, const char *text)
{
munit_assert_size(i, <, cap->n);
munit_assert_int(cap->toks[i].kind, ==, kind);
munit_assert_size(cap->toks[i].line, ==, line);
munit_assert_size(cap->toks[i].col, ==, col);
munit_assert_string_equal(cap->toks[i].text, text);
}
/* Assert a lex error occurred with the given (line, col) span. */
static void
assert_error(const struct capture *cap, size_t line, size_t col)
{
munit_assert_true(cap->had_error);
munit_assert_int(cap->err_category, ==, ST_ERR_KDL_PARSE);
munit_assert_size(cap->err_line, ==, line);
munit_assert_size(cap->err_col, ==, col);
}
/* --- acceptance: the plan's canonical node ----------------------------- */
static MunitResult
test_acceptance_node(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("node 1 2 \"x\" key=\"v\" { child }");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_IDENT, 1, 1, "node");
assert_tok(&cap, 1, ST_TOK_NUMBER, 1, 6, "1");
assert_tok(&cap, 2, ST_TOK_NUMBER, 1, 8, "2");
assert_tok(&cap, 3, ST_TOK_STRING, 1, 10, "\"x\"");
assert_tok(&cap, 4, ST_TOK_IDENT, 1, 14, "key");
assert_tok(&cap, 5, ST_TOK_EQUALS, 1, 17, "=");
assert_tok(&cap, 6, ST_TOK_STRING, 1, 18, "\"v\"");
assert_tok(&cap, 7, ST_TOK_LBRACE, 1, 22, "{");
assert_tok(&cap, 8, ST_TOK_IDENT, 1, 24, "child");
assert_tok(&cap, 9, ST_TOK_RBRACE, 1, 30, "}");
assert_tok(&cap, 10, ST_TOK_EOF, 1, 31, "");
munit_assert_size(cap.n, ==, 11);
return MUNIT_OK;
}
/* --- numbers: every radix + sign/float/exponent/underscore ------------ */
static MunitResult
test_numbers(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("0x10 0o17 0b101 123 3.14 1e10 1_000");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_NUMBER, 1, 1, "0x10");
assert_tok(&cap, 1, ST_TOK_NUMBER, 1, 6, "0o17");
assert_tok(&cap, 2, ST_TOK_NUMBER, 1, 11, "0b101");
assert_tok(&cap, 3, ST_TOK_NUMBER, 1, 17, "123");
assert_tok(&cap, 4, ST_TOK_NUMBER, 1, 21, "3.14");
assert_tok(&cap, 5, ST_TOK_NUMBER, 1, 26, "1e10");
assert_tok(&cap, 6, ST_TOK_NUMBER, 1, 31, "1_000");
assert_tok(&cap, 7, ST_TOK_EOF, 1, 36, "");
return MUNIT_OK;
}
static MunitResult
test_signed_and_radix_numbers(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("-5 +7 -0x10 0XFF 0xff 1e-3");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_NUMBER, 1, 1, "-5");
assert_tok(&cap, 1, ST_TOK_NUMBER, 1, 4, "+7");
assert_tok(&cap, 2, ST_TOK_NUMBER, 1, 7, "-0x10");
assert_tok(&cap, 3, ST_TOK_NUMBER, 1, 13, "0XFF");
assert_tok(&cap, 4, ST_TOK_NUMBER, 1, 18, "0xff");
assert_tok(&cap, 5, ST_TOK_NUMBER, 1, 23, "1e-3");
assert_tok(&cap, 6, ST_TOK_EOF, 1, 27, "");
return MUNIT_OK;
}
/* --- keyword numbers, booleans, null ---------------------------------- */
static MunitResult
test_keywords(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("#inf #-inf #nan #true #false #null");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_INF, 1, 1, "#inf");
assert_tok(&cap, 1, ST_TOK_NEG_INF, 1, 6, "#-inf");
assert_tok(&cap, 2, ST_TOK_NAN, 1, 12, "#nan");
assert_tok(&cap, 3, ST_TOK_TRUE, 1, 17, "#true");
assert_tok(&cap, 4, ST_TOK_FALSE, 1, 23, "#false");
assert_tok(&cap, 5, ST_TOK_NULL, 1, 30, "#null");
assert_tok(&cap, 6, ST_TOK_EOF, 1, 35, "");
return MUNIT_OK;
}
/* --- type annotations -------------------------------------------------- */
static MunitResult
test_type_annotation(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("(u8)42 (published)date");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_TYPE, 1, 1, "u8");
assert_tok(&cap, 1, ST_TOK_NUMBER, 1, 5, "42");
assert_tok(&cap, 2, ST_TOK_TYPE, 1, 8, "published");
assert_tok(&cap, 3, ST_TOK_IDENT, 1, 19, "date");
assert_tok(&cap, 4, ST_TOK_EOF, 1, 23, "");
return MUNIT_OK;
}
/* --- string forms: quoted, raw (single + double hash) ------------------ */
static MunitResult
test_strings_quoted_raw(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("\"hello\" #\"raw\"# ##\"raw2\"##");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_STRING, 1, 1, "\"hello\"");
assert_tok(&cap, 1, ST_TOK_RAW_STRING, 1, 9, "#\"raw\"#");
assert_tok(&cap, 2, ST_TOK_RAW_STRING, 1, 17, "##\"raw2\"##");
assert_tok(&cap, 3, ST_TOK_EOF, 1, 27, "");
return MUNIT_OK;
}
/* --- multi-line string (delimiters only; dedent rules are todo 8) ------ */
static MunitResult
test_multiline_string(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("\"\"\"multi\"\"\"");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_MULTILINE_STRING, 1, 1, "\"\"\"multi\"\"\"");
assert_tok(&cap, 1, ST_TOK_EOF, 1, 12, "");
return MUNIT_OK;
}
/* --- comments ---------------------------------------------------------- */
static MunitResult
test_line_comment(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("a // comment\nb");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_IDENT, 1, 1, "a");
assert_tok(&cap, 1, ST_TOK_LINE_COMMENT, 1, 3, "// comment");
assert_tok(&cap, 2, ST_TOK_IDENT, 2, 1, "b");
assert_tok(&cap, 3, ST_TOK_EOF, 2, 2, "");
return MUNIT_OK;
}
static MunitResult
test_block_comment_nested(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("a /* x /* n */ */ b");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_IDENT, 1, 1, "a");
assert_tok(&cap, 1, ST_TOK_BLOCK_COMMENT, 1, 3, "/* x /* n */ */");
assert_tok(&cap, 2, ST_TOK_IDENT, 1, 19, "b");
assert_tok(&cap, 3, ST_TOK_EOF, 1, 20, "");
return MUNIT_OK;
}
static MunitResult
test_slashdash(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("/- node");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_SLASHDASH, 1, 1, "/-");
assert_tok(&cap, 1, ST_TOK_IDENT, 1, 4, "node");
assert_tok(&cap, 2, ST_TOK_EOF, 1, 8, "");
return MUNIT_OK;
}
/* --- structural tokens -------------------------------------------------- */
static MunitResult
test_semicolons_braces(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("a;b {c}");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_IDENT, 1, 1, "a");
assert_tok(&cap, 1, ST_TOK_SEMICOLON, 1, 2, ";");
assert_tok(&cap, 2, ST_TOK_IDENT, 1, 3, "b");
assert_tok(&cap, 3, ST_TOK_LBRACE, 1, 5, "{");
assert_tok(&cap, 4, ST_TOK_IDENT, 1, 6, "c");
assert_tok(&cap, 5, ST_TOK_RBRACE, 1, 7, "}");
assert_tok(&cap, 6, ST_TOK_EOF, 1, 8, "");
return MUNIT_OK;
}
/* --- identifiers: sign / dotted / hyphen forms ------------------------- */
static MunitResult
test_ident_specials(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("--this .md foo-bar a+b _under");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_IDENT, 1, 1, "--this");
assert_tok(&cap, 1, ST_TOK_IDENT, 1, 8, ".md");
assert_tok(&cap, 2, ST_TOK_IDENT, 1, 12, "foo-bar");
assert_tok(&cap, 3, ST_TOK_IDENT, 1, 20, "a+b");
assert_tok(&cap, 4, ST_TOK_IDENT, 1, 24, "_under");
assert_tok(&cap, 5, ST_TOK_EOF, 1, 30, "");
return MUNIT_OK;
}
/* --- bare keyword identifiers are syntax errors ------------------------ */
static MunitResult
test_bare_keywords_error(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
static const char *const bare[] = {
"inf", "-inf", "nan", "true", "false", "null",
};
size_t i;
for (i = 0; i < sizeof(bare) / sizeof(bare[0]); i++) {
struct capture cap = capture(bare[i]);
assert_error(&cap, 1, 1);
}
/* also rejected in position, with the span on the offending ident */
{
struct capture cap = capture("x inf");
assert_tok(&cap, 0, ST_TOK_IDENT, 1, 1, "x");
assert_error(&cap, 1, 3);
}
return MUNIT_OK;
}
/* --- unterminated strings (all three forms) ---------------------------- */
static MunitResult
test_unterminated_quoted(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("node \"unterminated");
assert_tok(&cap, 0, ST_TOK_IDENT, 1, 1, "node");
assert_error(&cap, 1, 6); /* span at the opening quote */
return MUNIT_OK;
}
static MunitResult
test_unterminated_raw(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("#\"unterminated");
assert_error(&cap, 1, 1); /* span at the opening '#' */
return MUNIT_OK;
}
static MunitResult
test_unterminated_multiline(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("\"\"\"multi");
assert_error(&cap, 1, 1); /* span at the opening '\"\"\"' */
return MUNIT_OK;
}
/* --- malformed numbers -------------------------------------------------- */
static MunitResult
test_bad_hex(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("0xZZ");
assert_error(&cap, 1, 1);
return MUNIT_OK;
}
static MunitResult
test_hex_no_digits(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("0x");
assert_error(&cap, 1, 1);
return MUNIT_OK;
}
static MunitResult
test_bad_octal_binary(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("0o8");
assert_error(&cap, 1, 1);
cap = capture("0b2");
assert_error(&cap, 1, 1);
return MUNIT_OK;
}
/* --- leading-dot numbers are errors ------------------------------------ */
static MunitResult
test_dot_number_error(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture(".5");
assert_error(&cap, 1, 1);
return MUNIT_OK;
}
/* --- invalid escape ----------------------------------------------------- */
static MunitResult
test_invalid_escape(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("\"\\q\"");
assert_error(&cap, 1, 1);
return MUNIT_OK;
}
/* --- stray '#' ---------------------------------------------------------- */
static MunitResult
test_hash_alone(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("#");
assert_error(&cap, 1, 1);
cap = capture("##foo");
assert_error(&cap, 1, 1);
return MUNIT_OK;
}
/* --- bad type annotation ------------------------------------------------ */
static MunitResult
test_bad_type_annotation(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("(u8");
assert_error(&cap, 1, 1);
return MUNIT_OK;
}
/* --- a stray '}' is a token, not a lexer error (nesting is todo 7) ----- */
static MunitResult
test_stray_brace_is_token(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("}");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_RBRACE, 1, 1, "}");
assert_tok(&cap, 1, ST_TOK_EOF, 1, 2, "");
return MUNIT_OK;
}
/* --- hostile span: column well past 60 stays exact --------------------- */
static MunitResult
test_long_line_span(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
char src[256];
memset(src, ' ', 70);
memcpy(src + 70, "foo", 4);
{
struct capture cap = capture(src);
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_IDENT, 1, 71, "foo");
assert_tok(&cap, 1, ST_TOK_EOF, 1, 74, "");
}
return MUNIT_OK;
}
/* --- line/col tracking across newlines --------------------------------- */
static MunitResult
test_line_col_tracking(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("foo\n bar");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_IDENT, 1, 1, "foo");
assert_tok(&cap, 1, ST_TOK_IDENT, 2, 3, "bar");
assert_tok(&cap, 2, ST_TOK_EOF, 2, 6, "");
/* a multi-line string advances line/col for following tokens */
cap = capture("\"\"\"\nx\n\"\"\" tail");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_MULTILINE_STRING, 1, 1, "\"\"\"\nx\n\"\"\"");
assert_tok(&cap, 1, ST_TOK_IDENT, 3, 5, "tail");
assert_tok(&cap, 2, ST_TOK_EOF, 3, 9, "");
return MUNIT_OK;
}
/* --- token kind names exist and are distinct --------------------------- */
static MunitResult
test_kind_names(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
const char *prev = NULL;
int k;
for (k = ST_TOK_EOF; k <= ST_TOK_SLASHDASH; k++) {
const char *n = st_token_kind_name((enum st_token_kind)k);
munit_assert_not_null(n);
munit_assert_int((int)strlen(n), >, 0);
if (prev != NULL) {
munit_assert_string_not_equal(n, prev);
}
prev = n;
}
return MUNIT_OK;
}
static MunitTest tests[] = {
{ "/lexer/acceptance-node", test_acceptance_node, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/numbers", test_numbers, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/signed-radix-numbers", test_signed_and_radix_numbers, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/keywords", test_keywords, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/type-annotation", test_type_annotation, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/strings-quoted-raw", test_strings_quoted_raw, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/multiline-string", test_multiline_string, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/line-comment", test_line_comment, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/block-comment-nested", test_block_comment_nested, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/slashdash", test_slashdash, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/semicolons-braces", test_semicolons_braces, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/ident-specials", test_ident_specials, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/bare-keywords-error", test_bare_keywords_error, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/unterminated-quoted", test_unterminated_quoted, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/unterminated-raw", test_unterminated_raw, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/unterminated-multiline", test_unterminated_multiline, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/bad-hex", test_bad_hex, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/hex-no-digits", test_hex_no_digits, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/bad-octal-binary", test_bad_octal_binary, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/dot-number-error", test_dot_number_error, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/invalid-escape", test_invalid_escape, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/hash-alone", test_hash_alone, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/bad-type-annotation", test_bad_type_annotation, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/stray-brace-is-token", test_stray_brace_is_token, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/long-line-span", test_long_line_span, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/line-col-tracking", test_line_col_tracking, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/kind-names", test_kind_names, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
};
static const MunitSuite suite = {
"/lexer", tests, NULL, 1, MUNIT_SUITE_OPTION_NONE,
};
int
main(int argc, char *argv[MUNIT_ARRAY_PARAM(argc + 1)])
{
return munit_suite_main(&suite, NULL, argc, argv);
}