#include "unity/unity.h" #include #include #include #include #include /* Wrapper provided in the module under test for the static function */ void test_htmlAutoCloseOnEnd(htmlParserCtxtPtr ctxt); /* Recorder for SAX endElement calls */ typedef struct { int call_count; char names[64][64]; /* store up to 64 element names, 63 chars each */ } EndRec; static xmlSAXHandler g_sax; /* must be static/global so the pointer remains valid */ static void on_end_element(void *userData, const xmlChar *name) { EndRec *rec = (EndRec *)userData; if (rec == NULL || name == NULL) return; if (rec->call_count < 64) { strncpy(rec->names[rec->call_count], (const char *)name, sizeof(rec->names[0]) - 1); rec->names[rec->call_count][sizeof(rec->names[0]) - 1] = '\0'; rec->call_count++; } } static int ci_equal(const char *a, const char *b) { if (a == NULL || b == NULL) return 0; while (*a && *b) { unsigned char ca = (unsigned char)*a; unsigned char cb = (unsigned char)*b; if (tolower(ca) != tolower(cb)) return 0; a++; b++; } return *a == *b; } /* Helper to create a push parser context with our SAX recorder */ static htmlParserCtxtPtr create_push_ctxt(EndRec *rec) { return htmlCreatePushParserCtxt(&g_sax, rec, NULL, 0, NULL, XML_CHAR_ENCODING_NONE); } /* Helper to feed a chunk to the push parser */ static void feed_chunk(htmlParserCtxtPtr ctxt, const char *data) { htmlParseChunk(ctxt, data, (int)strlen(data), 0); } void setUp(void) { /* initialize global SAX handler and libxml2 */ memset(&g_sax, 0, sizeof(g_sax)); g_sax.endElement = on_end_element; xmlInitParser(); } void tearDown(void) { xmlCleanupParser(); } /* Test: when there are no open elements (nameNr == 0), function does nothing */ void test_htmlAutoCloseOnEnd_no_open_elements_noop(void) { EndRec rec = {0}; htmlParserCtxtPtr ctxt = create_push_ctxt(&rec); TEST_ASSERT_NOT_NULL(ctxt); /* Ensure no input was fed; nameNr should be 0 */ TEST_ASSERT_EQUAL_INT(0, ctxt->nameNr); test_htmlAutoCloseOnEnd(ctxt); TEST_ASSERT_EQUAL_INT(0, ctxt->nameNr); TEST_ASSERT_EQUAL_INT(0, rec.call_count); htmlFreeParserCtxt(ctxt); } /* Test: closes all open elements and invokes endElement for each in reverse order */ void test_htmlAutoCloseOnEnd_closes_stack_and_calls_endElement_reverse_order(void) { EndRec rec = {0}; htmlParserCtxtPtr ctxt = create_push_ctxt(&rec); TEST_ASSERT_NOT_NULL(ctxt); /* Feed partial HTML leaving elements unclosed */ feed_chunk(ctxt, "
"); /* The parser likely opened implicit elements like html/body; verify we have some open names */ TEST_ASSERT_TRUE_MESSAGE(ctxt->nameNr > 0, "Expected at least one open element on the stack"); /* Capture the top-of-stack element name before we call the function */ char top_before[64] = {0}; if (ctxt->name != NULL) { strncpy(top_before, (const char *)ctxt->name, sizeof(top_before) - 1); } /* Reset recorder to only capture calls from htmlAutoCloseOnEnd */ rec.call_count = 0; int initial_nameNr = ctxt->nameNr; test_htmlAutoCloseOnEnd(ctxt); /* All names should be popped */ TEST_ASSERT_EQUAL_INT(0, ctxt->nameNr); /* endElement should have been called once per previously open element */ TEST_ASSERT_EQUAL_INT(initial_nameNr, rec.call_count); /* First callback should correspond to the previous top element (reverse order) */ if (top_before[0] != '\0') { TEST_ASSERT_TRUE_MESSAGE(ci_equal(rec.names[0], top_before), "First endElement should be for the previous top-of-stack element"); } htmlFreeParserCtxt(ctxt); } /* Test: returns early when HTML_PARSE_HTML5 option is set */ void test_htmlAutoCloseOnEnd_returns_early_with_HTML5_option(void) { EndRec rec = {0}; htmlParserCtxtPtr ctxt = create_push_ctxt(&rec); TEST_ASSERT_NOT_NULL(ctxt); feed_chunk(ctxt, "
"); TEST_ASSERT_TRUE_MESSAGE(ctxt->nameNr > 0, "Expected open elements before testing HTML5 early return"); int before_nameNr = ctxt->nameNr; ctxt->options |= HTML_PARSE_HTML5; test_htmlAutoCloseOnEnd(ctxt); /* Should be unchanged and no callbacks should be invoked */ TEST_ASSERT_EQUAL_INT(before_nameNr, ctxt->nameNr); TEST_ASSERT_EQUAL_INT(0, rec.call_count); htmlFreeParserCtxt(ctxt); } /* Test: with NULL SAX handler, the function still empties the stack but doesn't call endElement */ void test_htmlAutoCloseOnEnd_handles_null_sax_handler(void) { EndRec rec = {0}; htmlParserCtxtPtr ctxt = create_push_ctxt(&rec); TEST_ASSERT_NOT_NULL(ctxt); feed_chunk(ctxt, "
"); TEST_ASSERT_TRUE_MESSAGE(ctxt->nameNr > 0, "Expected open elements before null SAX test"); int initial_nameNr = ctxt->nameNr; /* Remove SAX handler to simulate NULL sax/endElement */ ctxt->sax = NULL; test_htmlAutoCloseOnEnd(ctxt); TEST_ASSERT_EQUAL_INT(0, ctxt->nameNr); TEST_ASSERT_EQUAL_INT(0, rec.call_count); /* no callbacks since sax is NULL */ (void)initial_nameNr; /* suppress unused warning if not used in some configs */ htmlFreeParserCtxt(ctxt); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_htmlAutoCloseOnEnd_no_open_elements_noop); RUN_TEST(test_htmlAutoCloseOnEnd_closes_stack_and_calls_endElement_reverse_order); RUN_TEST(test_htmlAutoCloseOnEnd_returns_early_with_HTML5_option); RUN_TEST(test_htmlAutoCloseOnEnd_handles_null_sax_handler); return UNITY_END(); }