Files
stupidtools/tests/unit/test_value.c
T

633 lines
19 KiB
C

/* 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 <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
/* ---- 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);
}