#include "unity/unity.h" #include #include #include #include #include #include /* The function under test is exposed via a test wrapper */ void test_htmlAutoCloseOnClose(htmlParserCtxtPtr ctxt, const xmlChar * newtag); /* Helper to collect endElement events */ typedef struct { xmlChar **names; int count; int cap; } EndEvents; static void EndEvents_init(EndEvents *ev) { ev->names = NULL; ev->count = 0; ev->cap = 0; } static void EndEvents_push(EndEvents *ev, const xmlChar *name) { if (ev->count == ev->cap) { int newCap = (ev->cap == 0) ? 8 : ev->cap * 2; xmlChar **newArr = (xmlChar **)realloc(ev->names, (size_t)newCap * sizeof(xmlChar *)); TEST_ASSERT_NOT_NULL_MESSAGE(newArr, "Failed to allocate event array"); ev->names = newArr; ev->cap = newCap; } ev->names[ev->count++] = xmlStrdup(name); } static void EndEvents_free(EndEvents *ev) { if (ev->names) { for (int i = 0; i < ev->count; i++) { if (ev->names[i]) xmlFree(ev->names[i]); } free(ev->names); } ev->names = NULL; ev->count = 0; ev->cap = 0; } /* SAX handler that records endElement calls */ static void recordingEndElement(void *userData, const xmlChar *name) { EndEvents *ev = (EndEvents *)userData; if (ev && name) { EndEvents_push(ev, name); } } /* We use a static SAX handler to ensure its lifetime outlives the parser context */ static xmlSAXHandler gSAX; /* Build a parser context, attach SAX handler to capture endElement events, and feed HTML chunk */ static htmlParserCtxtPtr build_ctx_with_stack(const char *htmlChunk, EndEvents *events) { memset(&gSAX, 0, sizeof(gSAX)); gSAX.endElement = recordingEndElement; /* SAX1 endElement will be called by the target function */ htmlParserCtxtPtr ctxt = htmlCreatePushParserCtxt(&gSAX, events, NULL, 0, NULL, XML_CHAR_ENCODING_NONE); TEST_ASSERT_NOT_NULL_MESSAGE(ctxt, "Failed to create HTML push parser context"); /* Feed only opening tags to build a stack of open elements */ int len = (int)strlen(htmlChunk); int rc = htmlParseChunk(ctxt, htmlChunk, len, 0); TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "htmlParseChunk returned error building initial stack"); return ctxt; } void setUp(void) { /* Setup code here, or leave empty */ } void tearDown(void) { /* Cleanup code here, or leave empty */ } /* Test: closes intermediate elements until the matching tag is on top; endElement called for each */ void test_htmlAutoCloseOnClose_closes_intermediate_elements_until_match(void) { EndEvents events; EndEvents_init(&events); /* Build a stack: ... (xem is on top) */ htmlParserCtxtPtr ctxt = build_ctx_with_stack("", &events); TEST_ASSERT_NOT_NULL(ctxt->name); int beforeNr = ctxt->nameNr; /* Trigger autoclose with closing tag for xdiv */ test_htmlAutoCloseOnClose(ctxt, BAD_CAST "xdiv"); /* Should have closed xem then xspan (LIFO), leaving xdiv on top */ TEST_ASSERT_EQUAL_INT(2, events.count); TEST_ASSERT_TRUE(xmlStrEqual(events.names[0], BAD_CAST "xem")); TEST_ASSERT_TRUE(xmlStrEqual(events.names[1], BAD_CAST "xspan")); TEST_ASSERT_NOT_NULL(ctxt->name); TEST_ASSERT_TRUE(xmlStrEqual(ctxt->name, BAD_CAST "xdiv")); TEST_ASSERT_EQUAL_INT(beforeNr - 2, ctxt->nameNr); htmlFreeParserCtxt(ctxt); EndEvents_free(&events); } /* Test: no action when target tag is not found in the open element stack */ void test_htmlAutoCloseOnClose_no_action_if_target_not_found(void) { EndEvents events; EndEvents_init(&events); /* Build a stack: ... */ htmlParserCtxtPtr ctxt = build_ctx_with_stack("", &events); int beforeNr = ctxt->nameNr; const xmlChar *beforeTop = ctxt->name; /* Target "xfoo" doesn't exist in the stack: function should do nothing */ test_htmlAutoCloseOnClose(ctxt, BAD_CAST "xfoo"); TEST_ASSERT_EQUAL_INT(0, events.count); TEST_ASSERT_EQUAL_INT(beforeNr, ctxt->nameNr); TEST_ASSERT_TRUE(xmlStrEqual(ctxt->name, beforeTop)); htmlFreeParserCtxt(ctxt); EndEvents_free(&events); } /* Test: no action when the target tag already matches the top of the stack */ void test_htmlAutoCloseOnClose_no_action_if_match_at_top(void) { EndEvents events; EndEvents_init(&events); /* Build a stack: ... (xspan is on top) */ htmlParserCtxtPtr ctxt = build_ctx_with_stack("", &events); int beforeNr = ctxt->nameNr; const xmlChar *beforeTop = ctxt->name; TEST_ASSERT_TRUE(xmlStrEqual(beforeTop, BAD_CAST "xspan")); /* Target is already the top element; no intermediate pops expected */ test_htmlAutoCloseOnClose(ctxt, BAD_CAST "xspan"); TEST_ASSERT_EQUAL_INT(0, events.count); TEST_ASSERT_EQUAL_INT(beforeNr, ctxt->nameNr); TEST_ASSERT_TRUE(xmlStrEqual(ctxt->name, BAD_CAST "xspan")); htmlFreeParserCtxt(ctxt); EndEvents_free(&events); } /* Test: in HTML5 mode (HTML_PARSE_HTML5), the function must return immediately and do nothing */ void test_htmlAutoCloseOnClose_early_return_in_html5_mode(void) { EndEvents events; EndEvents_init(&events); /* Build a stack: ... */ htmlParserCtxtPtr ctxt = build_ctx_with_stack("", &events); int beforeNr = ctxt->nameNr; const xmlChar *beforeTop = ctxt->name; /* Enable HTML5 mode */ ctxt->options |= HTML_PARSE_HTML5; test_htmlAutoCloseOnClose(ctxt, BAD_CAST "xdiv"); /* No changes expected */ TEST_ASSERT_EQUAL_INT(0, events.count); TEST_ASSERT_EQUAL_INT(beforeNr, ctxt->nameNr); TEST_ASSERT_TRUE(xmlStrEqual(ctxt->name, beforeTop)); htmlFreeParserCtxt(ctxt); EndEvents_free(&events); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_htmlAutoCloseOnClose_closes_intermediate_elements_until_match); RUN_TEST(test_htmlAutoCloseOnClose_no_action_if_target_not_found); RUN_TEST(test_htmlAutoCloseOnClose_no_action_if_match_at_top); RUN_TEST(test_htmlAutoCloseOnClose_early_return_in_html5_mode); return UNITY_END(); }