File size: 5,695 Bytes
6baed57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
#include "unity/unity.h"
#include <libxml/HTMLparser.h>
#include <libxml/parser.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>

/* 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, "<div><span>");

    /* 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, "<div><span>");

    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, "<div>");

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