#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();
}