/* LINK: ../../src/error.c ../../src/span.c */ /* tests/unit/test_error.c * * Unit tests for the error + source-span diagnostic infrastructure. * * 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). munit.c and the include dirs are * added automatically by the harness. */ #include "munit.h" #include "error.h" #include "span.h" #include #include #include /* Render into a tmpfile and slurp it back into a NUL-terminated string. * Returns NULL on any I/O failure. Caller frees with free(). */ static char * capture(FILE *f) { long n; char *buf; if (fseek(f, 0, SEEK_END) != 0) { return NULL; } n = ftell(f); if (n < 0 || fseek(f, 0, SEEK_SET) != 0) { return NULL; } buf = munit_malloc((size_t)n + 1); if (fread(buf, 1, (size_t)n, f) != (size_t)n) { free(buf); return NULL; } buf[n] = '\0'; return buf; } /* (a) the canonical span renders byte-for-byte per the plan's contract. */ static MunitResult test_span_exact_rendering(const MunitParameter params[], void *data) { (void)params; (void)data; const struct st_span span = { "file.kdl", 3, 5 }; FILE *f = tmpfile(); char *got; munit_assert_not_null(f); st_span_print(f, &span, ""); got = capture(f); fclose(f); munit_assert_not_null(got); munit_assert_string_equal(got, "file.kdl:3:5: \n ...^\n"); free(got); return MUNIT_OK; } /* (b) a span at line 0 / col 0 renders without underflow: no crash, * sane byte-exact output. */ static MunitResult test_span_zero_coords_no_underflow(const MunitParameter params[], void *data) { (void)params; (void)data; const struct st_span span = { "zero.kdl", 0, 0 }; FILE *f = tmpfile(); char *got; munit_assert_not_null(f); st_span_print(f, &span, ""); got = capture(f); fclose(f); munit_assert_not_null(got); munit_assert_string_equal(got, "zero.kdl:0:0: \n ...^\n"); free(got); return MUNIT_OK; } /* (c) error categories round-trip through their constructors. */ static MunitResult test_error_category_roundtrip(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_error *e = st_error_kdl_parse(""); munit_assert_not_null(e); munit_assert_int(st_error_category_of(e), ==, ST_ERR_KDL_PARSE); munit_assert_string_equal(st_error_message(e), ""); st_error_free(e); return MUNIT_OK; } /* every category constructor tags its own category. */ static MunitResult test_error_constructors(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_error *e; e = st_error_kdl_schema("m"); munit_assert_int(st_error_category_of(e), ==, ST_ERR_KDL_SCHEMA); st_error_free(e); e = st_error_io("m"); munit_assert_int(st_error_category_of(e), ==, ST_ERR_IO); st_error_free(e); e = st_error_usage("m"); munit_assert_int(st_error_category_of(e), ==, ST_ERR_USAGE); st_error_free(e); e = st_error_internal("m"); munit_assert_int(st_error_category_of(e), ==, ST_ERR_INTERNAL); st_error_free(e); st_error_free(NULL); /* must be a safe no-op */ return MUNIT_OK; } /* category names exist, are non-empty, and are pairwise distinct. */ static MunitResult test_error_category_names(const MunitParameter params[], void *data) { (void)params; (void)data; int c; const char *prev = NULL; for (c = ST_ERR_KDL_PARSE; c <= ST_ERR_INTERNAL; c++) { const char *n = st_error_category_name((enum st_error_category)c); munit_assert_not_null(n); munit_assert_int(strlen(n), >, 0); if (prev != NULL) { munit_assert_string_not_equal(n, prev); } prev = n; } return MUNIT_OK; } /* st_error_print with a span renders span shape + category-prefixed msg. */ static MunitResult test_error_print_with_span(const MunitParameter params[], void *data) { (void)params; (void)data; const struct st_span span = { "file.kdl", 3, 5 }; struct st_error *e = st_error_at(st_error_kdl_parse(""), &span); FILE *f = tmpfile(); char *got; munit_assert_not_null(e); munit_assert_not_null(f); st_error_print(f, e); got = capture(f); fclose(f); munit_assert_not_null(got); munit_assert_string_equal( got, "file.kdl:3:5: [kdl-parse] \n ...^\n"); free(got); st_error_free(e); return MUNIT_OK; } /* st_error_print without a span renders category-prefixed message only. */ static MunitResult test_error_print_without_span(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_error *e = st_error_io("disk on fire"); FILE *f = tmpfile(); char *got; munit_assert_not_null(e); munit_assert_not_null(f); st_error_print(f, e); got = capture(f); fclose(f); munit_assert_not_null(got); munit_assert_string_equal(got, "[io] disk on fire\n"); free(got); st_error_free(e); return MUNIT_OK; } static MunitTest tests[] = { { "/span/exact-rendering", test_span_exact_rendering, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/span/zero-coords-no-underflow", test_span_zero_coords_no_underflow, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/error/category-roundtrip", test_error_category_roundtrip, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/error/constructors", test_error_constructors, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/error/category-names", test_error_category_names, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/error/print-with-span", test_error_print_with_span, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/error/print-without-span", test_error_print_without_span, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, }; static const MunitSuite suite = { "/error", 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); }