/* LINK: ../../src/kdl/value.c ../../src/kdl/parser.c ../../src/kdl/lexer.c ../../src/error.c ../../src/span.c */ /* tests/unit/test_value.c * * Unit tests for the typed KDL value model (todo 8). * * 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). value.c depends on error.c (typed errors) and * span.c (caret rendering); parser.c + lexer.c are linked so the value-level * type-annotation integration can be tested END-TO-END through the parser. * * Most value-model cases are exercised by constructing a token ref DIRECTLY * (mkref), which bypasses the lexer's already-strict escape/radix validation * and lets us probe the value model's own boundary handling (unknown escapes, * `0xZZ`, overflow, i128 width) that the lexer would otherwise reject first. * Assertions check CONCRETE parsed values (kind + payload + annotation), never * just "does not crash". */ #include "munit.h" #include "error.h" #include "kdl/ast.h" #include "kdl/lexer.h" #include "kdl/value.h" #include #include #include #include /* ---- token-ref construction ------------------------------------------- */ /* Build a borrowed token ref over a NUL-terminated C string (the string * literal outlives the value call, so the borrow is safe). */ static struct st_kdl_token_ref mkref(enum st_token_kind kind, const char *text) { struct st_kdl_token_ref r; r.kind = kind; r.span.file = "test.kdl"; r.span.line = 1; r.span.col = 1; r.text = text; r.len = strlen(text); return r; } /* Parse a value token directly (no annotation); assert success and return * the value. munit's longjmp aborts the test on assertion failure. */ static struct st_kdl_value ok_value(enum st_token_kind kind, const char *text) { struct st_kdl_token_ref r = mkref(kind, text); struct st_kdl_value v; struct st_error *err = st_kdl_value_from_token(&r, &v); munit_assert_null(err); return v; } /* Parse a value token with an explicit type-annotation ref; assert success. */ static struct st_kdl_value ok_annotated(enum st_token_kind kind, const char *text, const char *ann) { struct st_kdl_token_ref r = mkref(kind, text); struct st_kdl_token_ref a = mkref(ST_TOK_TYPE, ann); struct st_kdl_value v; struct st_error *err = st_kdl_value_from_token_annotated(&r, &a, &v); munit_assert_null(err); return v; } /* Assert that parsing `text` (kind) fails with category KDL_PARSE and a * message containing `needle`. */ static void expect_error(enum st_token_kind kind, const char *text, const char *needle) { struct st_kdl_token_ref r = mkref(kind, text); struct st_kdl_value v; struct st_error *err = st_kdl_value_from_token(&r, &v); munit_assert_not_null(err); munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_PARSE); munit_assert_not_null(err->span); munit_assert_true(strstr(st_error_message(err), needle) != NULL); st_error_free(err); } /* ---- number tests ------------------------------------------------------ */ static MunitResult test_radix_ints(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_kdl_value v; v = ok_value(ST_TOK_NUMBER, "0xff"); munit_assert_int(v.kind, ==, ST_KDL_VAL_INT); munit_assert_llong(v.as.i, ==, 255); v = ok_value(ST_TOK_NUMBER, "0o17"); munit_assert_int(v.kind, ==, ST_KDL_VAL_INT); munit_assert_llong(v.as.i, ==, 15); v = ok_value(ST_TOK_NUMBER, "0b101"); munit_assert_int(v.kind, ==, ST_KDL_VAL_INT); munit_assert_llong(v.as.i, ==, 5); st_kdl_value_free(&v); return MUNIT_OK; } static MunitResult test_decimal_and_sign(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_kdl_value v; v = ok_value(ST_TOK_NUMBER, "42"); munit_assert_int(v.kind, ==, ST_KDL_VAL_INT); munit_assert_llong(v.as.i, ==, 42); v = ok_value(ST_TOK_NUMBER, "-42"); munit_assert_int(v.kind, ==, ST_KDL_VAL_INT); munit_assert_llong(v.as.i, ==, -42); v = ok_value(ST_TOK_NUMBER, "+7"); munit_assert_int(v.kind, ==, ST_KDL_VAL_INT); munit_assert_llong(v.as.i, ==, 7); v = ok_value(ST_TOK_NUMBER, "1_000"); munit_assert_int(v.kind, ==, ST_KDL_VAL_INT); munit_assert_llong(v.as.i, ==, 1000); st_kdl_value_free(&v); return MUNIT_OK; } static MunitResult test_floats(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_kdl_value v; v = ok_value(ST_TOK_NUMBER, "1.5"); munit_assert_int(v.kind, ==, ST_KDL_VAL_FLOAT); munit_assert_double_equal(v.as.f, 1.5, 12); /* trailing dot, no fraction digits (direct ref: the lexer splits this) */ v = ok_value(ST_TOK_NUMBER, "3."); munit_assert_int(v.kind, ==, ST_KDL_VAL_FLOAT); munit_assert_double_equal(v.as.f, 3.0, 12); st_kdl_value_free(&v); return MUNIT_OK; } static MunitResult test_exponents(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_kdl_value v; v = ok_value(ST_TOK_NUMBER, "1e10"); munit_assert_int(v.kind, ==, ST_KDL_VAL_FLOAT); munit_assert_double_equal(v.as.f, 1e10, 12); v = ok_value(ST_TOK_NUMBER, "2.5e-3"); munit_assert_int(v.kind, ==, ST_KDL_VAL_FLOAT); munit_assert_double_equal(v.as.f, 2.5e-3, 12); st_kdl_value_free(&v); return MUNIT_OK; } static MunitResult test_keyword_numbers(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_kdl_value v; v = ok_value(ST_TOK_INF, "#inf"); munit_assert_int(v.kind, ==, ST_KDL_VAL_FLOAT); munit_assert_true(isinf(v.as.f)); munit_assert_true(v.as.f > 0); v = ok_value(ST_TOK_NEG_INF, "#-inf"); munit_assert_int(v.kind, ==, ST_KDL_VAL_FLOAT); munit_assert_true(isinf(v.as.f)); munit_assert_true(v.as.f < 0); v = ok_value(ST_TOK_NAN, "#nan"); munit_assert_int(v.kind, ==, ST_KDL_VAL_FLOAT); munit_assert_true(isnan(v.as.f)); st_kdl_value_free(&v); return MUNIT_OK; } static MunitResult test_bool_null(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_kdl_value v; v = ok_value(ST_TOK_TRUE, "#true"); munit_assert_int(v.kind, ==, ST_KDL_VAL_BOOL); munit_assert_true(v.as.b); v = ok_value(ST_TOK_FALSE, "#false"); munit_assert_int(v.kind, ==, ST_KDL_VAL_BOOL); munit_assert_false(v.as.b); v = ok_value(ST_TOK_NULL, "#null"); munit_assert_int(v.kind, ==, ST_KDL_VAL_NULL); st_kdl_value_free(&v); return MUNIT_OK; } /* ---- string tests ------------------------------------------------------ */ static MunitResult test_ident_and_quoted(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_kdl_value v; v = ok_value(ST_TOK_IDENT, "foo"); munit_assert_int(v.kind, ==, ST_KDL_VAL_STRING); munit_assert_string_equal(v.as.str, "foo"); st_kdl_value_free(&v); v = ok_value(ST_TOK_STRING, "\"hello\""); munit_assert_int(v.kind, ==, ST_KDL_VAL_STRING); munit_assert_string_equal(v.as.str, "hello"); st_kdl_value_free(&v); /* empty quoted string */ v = ok_value(ST_TOK_STRING, "\"\""); munit_assert_int(v.kind, ==, ST_KDL_VAL_STRING); munit_assert_string_equal(v.as.str, ""); st_kdl_value_free(&v); return MUNIT_OK; } static MunitResult test_escapes(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_kdl_value v; /* "a\nb" -> a, newline, b */ v = ok_value(ST_TOK_STRING, "\"a\\nb\""); munit_assert_int(v.kind, ==, ST_KDL_VAL_STRING); munit_assert_string_equal(v.as.str, "a\nb"); st_kdl_value_free(&v); /* backslash + quote + tab + carriage return */ v = ok_value(ST_TOK_STRING, "\"\\\\\\\"\\t\\r\""); munit_assert_int(v.kind, ==, ST_KDL_VAL_STRING); munit_assert_string_equal(v.as.str, "\\\"\t\r"); st_kdl_value_free(&v); /* space escape \s */ v = ok_value(ST_TOK_STRING, "\"a\\sb\""); munit_assert_string_equal(v.as.str, "a b"); st_kdl_value_free(&v); /* backspace + form feed + vertical tab + bell + apostrophe (lenient) */ v = ok_value(ST_TOK_STRING, "\"\\b\\f\\v\\a\\'\""); munit_assert_string_equal(v.as.str, "\b\f\v\a'"); st_kdl_value_free(&v); /* hex byte \x41 -> 'A' */ v = ok_value(ST_TOK_STRING, "\"\\x41\""); munit_assert_string_equal(v.as.str, "A"); st_kdl_value_free(&v); /* unicode \u{1F600} -> U+1F600 (UTF-8 F0 9F 98 80) */ v = ok_value(ST_TOK_STRING, "\"\\u{1F600}\""); munit_assert_string_equal(v.as.str, "\xF0\x9F\x98\x80"); st_kdl_value_free(&v); /* \u{10FFFF} is the maximum Unicode scalar value (UTF-8 F4 8F BF BF) */ v = ok_value(ST_TOK_STRING, "\"\\u{10FFFF}\""); munit_assert_string_equal(v.as.str, "\xF4\x8F\xBF\xBF"); st_kdl_value_free(&v); /* whitespace escape: backslash + spaces + newline -> nothing */ v = ok_value(ST_TOK_STRING, "\"Hello \\ World\""); munit_assert_string_equal(v.as.str, "Hello World"); st_kdl_value_free(&v); return MUNIT_OK; } static MunitResult test_raw_string(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_kdl_value v; /* #"raw\n"# -> literal backslash-n (no escape processing) */ v = ok_value(ST_TOK_RAW_STRING, "#\"raw\\n\"#"); munit_assert_int(v.kind, ==, ST_KDL_VAL_STRING); munit_assert_string_equal(v.as.str, "raw\\n"); st_kdl_value_free(&v); return MUNIT_OK; } static MunitResult test_multiline_dedent(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_kdl_value v; const char *ml = "\"\"\"\n foo\n bar\n \"\"\""; v = ok_value(ST_TOK_MULTILINE_STRING, ml); munit_assert_int(v.kind, ==, ST_KDL_VAL_STRING); munit_assert_string_equal(v.as.str, "foo\nbar"); st_kdl_value_free(&v); /* no trailing newline issues: an empty multi-line string */ v = ok_value(ST_TOK_MULTILINE_STRING, "\"\"\"\n \"\"\""); munit_assert_string_equal(v.as.str, ""); st_kdl_value_free(&v); return MUNIT_OK; } static MunitResult test_multiline_escaped_newline(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_kdl_value v; /* interior \n escape stays a newline (not dedented away) */ const char *ml = "\"\"\"\n a\\nb\n \"\"\""; v = ok_value(ST_TOK_MULTILINE_STRING, ml); munit_assert_int(v.kind, ==, ST_KDL_VAL_STRING); munit_assert_string_equal(v.as.str, "a\nb"); st_kdl_value_free(&v); return MUNIT_OK; } /* ---- error tests ------------------------------------------------------- */ static MunitResult test_unknown_escape(const MunitParameter params[], void *data) { (void)params; (void)data; /* '\q' is not a KDL escape (direct ref; the lexer would reject it) */ expect_error(ST_TOK_STRING, "\"a\\qb\"", "unknown escape"); return MUNIT_OK; } static MunitResult test_unterminated_escape(const MunitParameter params[], void *data) { (void)params; (void)data; /* lone trailing backslash inside the quotes ("abc\" -> body "abc\") */ expect_error(ST_TOK_STRING, "\"abc\\\"", "unterminated"); return MUNIT_OK; } static MunitResult test_bad_radix(const MunitParameter params[], void *data) { (void)params; (void)data; expect_error(ST_TOK_NUMBER, "0xZZ", "invalid digit"); expect_error(ST_TOK_NUMBER, "0o8", "invalid digit"); expect_error(ST_TOK_NUMBER, "0b2", "invalid digit"); return MUNIT_OK; } static MunitResult test_integer_overflow(const MunitParameter params[], void *data) { (void)params; (void)data; /* 24 digits, far beyond u64 (and i64) */ expect_error(ST_TOK_NUMBER, "999999999999999999999999", "overflow"); /* 2^63 as an unannotated (signed) literal is one past i64 max */ expect_error(ST_TOK_NUMBER, "9223372036854775808", "overflow"); return MUNIT_OK; } /* ---- annotation tests -------------------------------------------------- */ static MunitResult test_unicode_out_of_range(const MunitParameter params[], void *data) { (void)params; (void)data; /* above the scalar-value range */ expect_error(ST_TOK_STRING, "\"\\u{110000}\"", "Unicode scalar"); /* a UTF-16 surrogate is not a scalar value */ expect_error(ST_TOK_STRING, "\"\\u{D800}\"", "Unicode scalar"); return MUNIT_OK; } static MunitResult test_annotation_u8(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_kdl_value v = ok_annotated(ST_TOK_NUMBER, "42", "u8"); munit_assert_int(v.kind, ==, ST_KDL_VAL_UINT); munit_assert_ullong(v.as.u, ==, 42); munit_assert_not_null(v.annotation); munit_assert_string_equal(v.annotation, "u8"); st_kdl_value_free(&v); return MUNIT_OK; } static MunitResult test_annotation_unsigned_rejects_negative(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_kdl_token_ref r = mkref(ST_TOK_NUMBER, "-1"); struct st_kdl_token_ref a = mkref(ST_TOK_TYPE, "u8"); struct st_kdl_value v; struct st_error *err = st_kdl_value_from_token_annotated(&r, &a, &v); munit_assert_not_null(err); munit_assert_true(strstr(st_error_message(err), "unsigned") != NULL); st_error_free(err); return MUNIT_OK; } static MunitResult test_annotation_unsupported_width(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_kdl_token_ref r = mkref(ST_TOK_NUMBER, "42"); struct st_kdl_token_ref a128 = mkref(ST_TOK_TYPE, "i128"); struct st_kdl_token_ref u128 = mkref(ST_TOK_TYPE, "u128"); struct st_kdl_value v; struct st_error *err; err = st_kdl_value_from_token_annotated(&r, &a128, &v); munit_assert_not_null(err); munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_PARSE); munit_assert_true(strstr(st_error_message(err), "unsupported") != NULL); st_error_free(err); err = st_kdl_value_from_token_annotated(&r, &u128, &v); munit_assert_not_null(err); munit_assert_true(strstr(st_error_message(err), "unsupported") != NULL); st_error_free(err); return MUNIT_OK; } /* ---- end-to-end annotation integration (through the parser) ------------ */ static MunitResult test_parser_annotation_arg(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_error *err = NULL; struct st_kdl_document *doc = st_kdl_parse("node (u8)42", "t.kdl", &err); struct st_kdl_node *n; struct st_kdl_value v; munit_assert_not_null(doc); munit_assert_null(err); n = doc->nodes; munit_assert_not_null(n); munit_assert_not_null(n->args); munit_assert_int(n->args->value.kind, ==, ST_TOK_NUMBER); munit_assert_not_null(n->args->annotation); munit_assert_int(n->args->annotation->kind, ==, ST_TOK_TYPE); munit_assert_memory_equal(2, n->args->annotation->text, "u8"); err = st_kdl_value_from_token_annotated(&n->args->value, n->args->annotation, &v); munit_assert_null(err); munit_assert_int(v.kind, ==, ST_KDL_VAL_UINT); munit_assert_ullong(v.as.u, ==, 42); munit_assert_string_equal(v.annotation, "u8"); st_kdl_value_free(&v); st_kdl_document_free(doc); return MUNIT_OK; } static MunitResult test_parser_annotation_prop(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_error *err = NULL; struct st_kdl_document *doc = st_kdl_parse("node key=(u8)42", "t.kdl", &err); struct st_kdl_node *n; struct st_kdl_value v; munit_assert_not_null(doc); munit_assert_null(err); n = doc->nodes; munit_assert_not_null(n); munit_assert_not_null(n->props); munit_assert_int(n->props->value.kind, ==, ST_TOK_NUMBER); munit_assert_not_null(n->props->annotation); munit_assert_int(n->props->annotation->kind, ==, ST_TOK_TYPE); munit_assert_memory_equal(2, n->props->annotation->text, "u8"); err = st_kdl_value_from_token_annotated(&n->props->value, n->props->annotation, &v); munit_assert_null(err); munit_assert_int(v.kind, ==, ST_KDL_VAL_UINT); munit_assert_ullong(v.as.u, ==, 42); st_kdl_value_free(&v); st_kdl_document_free(doc); return MUNIT_OK; } static MunitResult test_free_null(const MunitParameter params[], void *data) { (void)params; (void)data; st_kdl_value_free(NULL); return MUNIT_OK; } static MunitResult test_kind_name(const MunitParameter params[], void *data) { (void)params; (void)data; munit_assert_string_equal(st_kdl_value_kind_name(ST_KDL_VAL_STRING), "string"); munit_assert_string_equal(st_kdl_value_kind_name(ST_KDL_VAL_NULL), "null"); munit_assert_string_equal(st_kdl_value_kind_name((enum st_kdl_value_kind)99), "?"); return MUNIT_OK; } static MunitTest tests[] = { { "/value/radix-ints", test_radix_ints, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/decimal-and-sign", test_decimal_and_sign, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/floats", test_floats, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/exponents", test_exponents, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/keyword-numbers", test_keyword_numbers, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/bool-null", test_bool_null, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/ident-and-quoted", test_ident_and_quoted, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/escapes", test_escapes, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/raw-string", test_raw_string, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/multiline-dedent", test_multiline_dedent, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/multiline-escaped-newline", test_multiline_escaped_newline, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/unknown-escape", test_unknown_escape, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/unterminated-escape", test_unterminated_escape, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/unicode-out-of-range", test_unicode_out_of_range, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/bad-radix", test_bad_radix, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/integer-overflow", test_integer_overflow, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/annotation-u8", test_annotation_u8, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/annotation-unsigned-negative", test_annotation_unsigned_rejects_negative, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/annotation-unsupported-width", test_annotation_unsupported_width, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/parser-annotation-arg", test_parser_annotation_arg, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/parser-annotation-prop", test_parser_annotation_prop, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/free-null", test_free_null, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/value/kind-name", test_kind_name, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, }; static const MunitSuite suite = { "/value", 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); }