File size: 6,302 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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
#include "unity/unity.h"
#include <libxml/HTMLparser.h>
#include <libxml/parserInternals.h>
#include <libxml/xmlstring.h>
#include <libxml/encoding.h>
#include <string.h>
#include <stdlib.h>

/* 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: ... <xdiv><xspan><xem> (xem is on top) */
    htmlParserCtxtPtr ctxt = build_ctx_with_stack("<xdiv><xspan><xem>", &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: ... <xdiv><xspan><xem> */
    htmlParserCtxtPtr ctxt = build_ctx_with_stack("<xdiv><xspan><xem>", &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: ... <xdiv><xspan> (xspan is on top) */
    htmlParserCtxtPtr ctxt = build_ctx_with_stack("<xdiv><xspan>", &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: ... <xdiv><xspan><xem> */
    htmlParserCtxtPtr ctxt = build_ctx_with_stack("<xdiv><xspan><xem>", &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();
}