#include "unity/unity.h" #include #include #include #include #include /* Wrapper provided in the module for testing the static function */ void test_htmlAutoClose(htmlParserCtxtPtr ctxt, const xmlChar * newtag); typedef struct { int count; const xmlChar *last; } TestSaxData; static void onEndElement(void *userData, const xmlChar *name) { TestSaxData *d = (TestSaxData *)userData; if (d) { d->count++; d->last = name; } } /* Helper: create parser context with a prepared name stack (bottom..top). tags[0] is bottom of stack, tags[count-1] is current top (ctxt->name). If withSax is non-zero, install a SAX handler that counts endElement calls. */ static htmlParserCtxtPtr make_ctxt_with_stack(const char **tags, int count, int withSax, TestSaxData *saxDataOut) { htmlParserCtxtPtr ctxt = htmlNewParserCtxt(); TEST_ASSERT_NOT_NULL(ctxt); /* Disable recording to avoid dereferencing uninitialized fields in htmlParserFinishElementParsing */ ctxt->record_info = 0; /* Prepare name stack */ if (count > 0) { ctxt->nameMax = count; ctxt->nameNr = count; ctxt->nameTab = (const xmlChar **)xmlMalloc(sizeof(xmlChar *) * (size_t)count); TEST_ASSERT_NOT_NULL(ctxt->nameTab); for (int i = 0; i < count; i++) { /* Duplicate to ensure safe lifetime if internals attempt to free */ ctxt->nameTab[i] = xmlStrdup((const xmlChar *)tags[i]); TEST_ASSERT_NOT_NULL(ctxt->nameTab[i]); } ctxt->name = ctxt->nameTab[count - 1]; } else { ctxt->nameMax = 0; ctxt->nameNr = 0; ctxt->nameTab = NULL; ctxt->name = NULL; } /* Options default to 0 unless overwritten by tests */ ctxt->options = 0; if (withSax) { static xmlSAXHandler sax; /* static so its storage lives long enough */ memset(&sax, 0, sizeof(sax)); sax.endElement = onEndElement; if (saxDataOut) { saxDataOut->count = 0; saxDataOut->last = NULL; } ctxt->sax = &sax; ctxt->userData = saxDataOut; } else { ctxt->sax = NULL; ctxt->userData = NULL; } return ctxt; } void setUp(void) { /* Optional global setup */ } void tearDown(void) { /* Optional global cleanup */ } /* Test: returns immediately when HTML5 tokenizer option is set */ void test_htmlAutoClose_returns_immediately_with_HTML5_option(void) { const char *stack[] = { "ul", "li" }; TestSaxData data; htmlParserCtxtPtr ctxt = make_ctxt_with_stack(stack, 2, 1, &data); /* Set HTML5 option to force early return */ ctxt->options |= HTML_PARSE_HTML5; const xmlChar *prevName = ctxt->name; int prevNr = ctxt->nameNr; test_htmlAutoClose(ctxt, (const xmlChar *)"li"); TEST_ASSERT_EQUAL_PTR(prevName, ctxt->name); TEST_ASSERT_EQUAL_INT(prevNr, ctxt->nameNr); TEST_ASSERT_EQUAL_INT(0, data.count); htmlFreeParserCtxt(ctxt); } /* Test: no action when newtag is NULL */ void test_htmlAutoClose_ignores_null_newtag(void) { const char *stack[] = { "ul", "li" }; TestSaxData data; htmlParserCtxtPtr ctxt = make_ctxt_with_stack(stack, 2, 1, &data); const xmlChar *prevName = ctxt->name; int prevNr = ctxt->nameNr; test_htmlAutoClose(ctxt, NULL); TEST_ASSERT_EQUAL_PTR(prevName, ctxt->name); TEST_ASSERT_EQUAL_INT(prevNr, ctxt->nameNr); TEST_ASSERT_EQUAL_INT(0, data.count); htmlFreeParserCtxt(ctxt); } /* Test: single auto-close for matching pair (li/li), calls endElement and pops one name */ void test_htmlAutoClose_closes_single_matching_tag_and_calls_endElement(void) { const char *stack[] = { "ul", "li" }; TestSaxData data; htmlParserCtxtPtr ctxt = make_ctxt_with_stack(stack, 2, 1, &data); test_htmlAutoClose(ctxt, (const xmlChar *)"li"); TEST_ASSERT_EQUAL_INT(1, data.count); TEST_ASSERT_NOT_NULL_MESSAGE(data.last, "endElement should be called with closed tag name"); TEST_ASSERT_EQUAL_INT(1, ctxt->nameNr); /* one pop */ TEST_ASSERT_EQUAL_STRING("ul", (const char *)ctxt->name); TEST_ASSERT_EQUAL_INT(0, xmlStrcmp((const xmlChar *)"li", data.last)); htmlFreeParserCtxt(ctxt); } /* Test: multiple auto-closes in a row (li closes li repeatedly), pops until non-li at bottom */ void test_htmlAutoClose_closes_multiple_in_a_row(void) { const char *stack[] = { "ul", "li", "li", "li" }; TestSaxData data; htmlParserCtxtPtr ctxt = make_ctxt_with_stack(stack, 4, 1, &data); test_htmlAutoClose(ctxt, (const xmlChar *)"li"); /* Expect three pops (all 'li') leaving 'ul' */ TEST_ASSERT_EQUAL_INT(3, data.count); TEST_ASSERT_EQUAL_INT(1, ctxt->nameNr); TEST_ASSERT_EQUAL_STRING("ul", (const char *)ctxt->name); htmlFreeParserCtxt(ctxt); } /* Test: no action when top-of-stack doesn't match autoclose rule (li doesn't close ul) */ void test_htmlAutoClose_no_action_when_no_match(void) { const char *stack[] = { "ul" }; TestSaxData data; htmlParserCtxtPtr ctxt = make_ctxt_with_stack(stack, 1, 1, &data); const xmlChar *prevName = ctxt->name; int prevNr = ctxt->nameNr; test_htmlAutoClose(ctxt, (const xmlChar *)"li"); TEST_ASSERT_EQUAL_PTR(prevName, ctxt->name); TEST_ASSERT_EQUAL_INT(prevNr, ctxt->nameNr); TEST_ASSERT_EQUAL_INT(0, data.count); htmlFreeParserCtxt(ctxt); } /* Test: works safely with NULL SAX handler (no callback), still pops if match */ void test_htmlAutoClose_handles_null_sax(void) { const char *stack[] = { "ul", "li" }; htmlParserCtxtPtr ctxt = make_ctxt_with_stack(stack, 2, 0, NULL); test_htmlAutoClose(ctxt, (const xmlChar *)"li"); TEST_ASSERT_EQUAL_INT(1, ctxt->nameNr); TEST_ASSERT_EQUAL_STRING("ul", (const char *)ctxt->name); htmlFreeParserCtxt(ctxt); } /* Test: no action when name is NULL (empty stack) */ void test_htmlAutoClose_noop_when_name_is_null(void) { TestSaxData data; htmlParserCtxtPtr ctxt = make_ctxt_with_stack(NULL, 0, 1, &data); test_htmlAutoClose(ctxt, (const xmlChar *)"li"); TEST_ASSERT_EQUAL_INT(0, data.count); TEST_ASSERT_EQUAL_INT(0, ctxt->nameNr); TEST_ASSERT_NULL(ctxt->name); htmlFreeParserCtxt(ctxt); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_htmlAutoClose_returns_immediately_with_HTML5_option); RUN_TEST(test_htmlAutoClose_ignores_null_newtag); RUN_TEST(test_htmlAutoClose_closes_single_matching_tag_and_calls_endElement); RUN_TEST(test_htmlAutoClose_closes_multiple_in_a_row); RUN_TEST(test_htmlAutoClose_no_action_when_no_match); RUN_TEST(test_htmlAutoClose_handles_null_sax); RUN_TEST(test_htmlAutoClose_noop_when_name_is_null); return UNITY_END(); }