diff --git a/requirements/test/pyproject.toml b/requirements/test/pyproject.toml index 9f0a41b..74a8d3a 100644 --- a/requirements/test/pyproject.toml +++ b/requirements/test/pyproject.toml @@ -6,7 +6,6 @@ name = "dependencies" requires-python = ">=3.10" dependencies = [ "coverage[toml]", - "pyfakefs", "pytest", "pytest-randomly", ] diff --git a/requirements/test/requirements.txt b/requirements/test/requirements.txt index dded8e8..609b1b8 100644 --- a/requirements/test/requirements.txt +++ b/requirements/test/requirements.txt @@ -4,7 +4,6 @@ exceptiongroup==1.3.1 ; python_version == "3.10" iniconfig==2.3.0 ; python_version >= "3.10" packaging==26.2 ; python_version >= "3.10" pluggy==1.6.0 ; python_version >= "3.10" -pyfakefs==6.2.0 ; python_version >= "3.10" pygments==2.20.0 ; python_version >= "3.10" pytest-randomly==4.1.0 ; python_version >= "3.10" pytest==9.1.1 ; python_version >= "3.10" diff --git a/src/feedparser/sgmllib/__init__.py b/src/feedparser/sgmllib/__init__.py index e4988ac..1febf15 100644 --- a/src/feedparser/sgmllib/__init__.py +++ b/src/feedparser/sgmllib/__init__.py @@ -100,11 +100,11 @@ def feed(self, data: str) -> None: """ self.rawdata = self.rawdata + data - self.goahead(0) + self.goahead(False) def close(self) -> None: """Handle the remaining data.""" - self.goahead(1) + self.goahead(True) def error(self, message: str) -> t.NoReturn: raise SGMLParseError(message) @@ -112,7 +112,7 @@ def error(self, message: str) -> t.NoReturn: # Internal -- handle data as far as reasonable. May leave state # and data to be processed by a subsequent call. If 'end' is # true, force handling all data as if followed by EOF marker. - def goahead(self, end: int) -> None: + def goahead(self, end: bool) -> None: rawdata = self.rawdata i = 0 n = len(rawdata) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..97dea4b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,130 @@ +import re + +import pytest + +import feedparser.sgmllib as sgmllib + + +@pytest.fixture +def check_parse_error(): + def checker(source: str): + parser = EventCollector() + with pytest.raises(sgmllib.SGMLParseError): + parser.feed(source) + parser.close() + + yield checker + + +@pytest.fixture +def event_collector(): + return EventCollector() + + +@pytest.fixture +def cdata_event_collector(): + return CDATAEventCollector() + + +@pytest.fixture +def html_entity_collector(): + return HTMLEntityCollector() + + +class EventCollector(sgmllib.SGMLParser): + def check_events(self, source, expected_events): + self._consume_source(source) + self._normalize_events() + assert self.events == expected_events + + def _normalize_events(self): + # Normalize the list of events so that buffer artifacts don't + # separate runs of contiguous characters. + normalized_events = [] + previous_type = None + for event in self.events: + current_type = event[0] + if current_type == previous_type == "data": + normalized_events[-1] = ("data", normalized_events[-1][1] + event[1]) + else: + normalized_events.append(event) + previous_type = current_type + self.events = normalized_events + + def _consume_source(self, source): + for s in source: + self.feed(s) + self.close() + + def __init__(self) -> None: + self.events = [] + super().__init__() + + # structure markup + + def unknown_starttag(self, tag, attrs): + self.events.append(("starttag", tag, attrs)) + + def unknown_endtag(self, tag): + self.events.append(("endtag", tag)) + + # all other markup + + def handle_comment(self, data): + self.events.append(("comment", data)) + + def handle_charref(self, name): + self.events.append(("charref", name)) + + def handle_data(self, data): + self.events.append(("data", data)) + + def handle_decl(self, decl): + self.events.append(("decl", decl)) + + def handle_entityref(self, name): + self.events.append(("entityref", name)) + + def handle_pi(self, data): + self.events.append(("pi", data)) + + def unknown_decl(self, data): + self.events.append(("unknown decl", data)) + + +class CDATAEventCollector(EventCollector): + def start_cdata(self, attrs): + self.events.append(("starttag", "cdata", attrs)) + self.setliteral() + + +class HTMLEntityCollector(EventCollector): + + entity_or_charref = re.compile( + "(?:&([a-zA-Z][-.a-zA-Z0-9]*)|&#(x[0-9a-zA-Z]+|[0-9]+))(;?)" + ) + + def convert_charref(self, name): + self.events.append(("charref", "convert", name)) + if name[0] != "x": + return super().convert_charref(name) + + def convert_codepoint(self, codepoint): + self.events.append(("codepoint", "convert", codepoint)) + super().convert_codepoint(codepoint) + + def convert_entityref(self, name): + self.events.append(("entityref", "convert", name)) + return super().convert_entityref(name) + + # These to record that they were called, then pass the call along + # to the default implementation so that its actions can be + # recorded. + + def handle_charref(self, name): + self.events.append(("charref", name)) + sgmllib.SGMLParser.handle_charref(self, name) + + def handle_entityref(self, name): + self.events.append(("entityref", name)) + sgmllib.SGMLParser.handle_entityref(self, name) diff --git a/tests/test_sgmllib.py b/tests/test_sgmllib.py index 8b14b75..8997cfe 100644 --- a/tests/test_sgmllib.py +++ b/tests/test_sgmllib.py @@ -1,524 +1,431 @@ +import io import pathlib -import pprint -import re -import unittest + +import pytest import feedparser.sgmllib as sgmllib -class EventCollector(sgmllib.SGMLParser): +def test_doctype_decl_internal(event_collector): + inside = """\ +DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.01//EN' + SYSTEM 'http://www.w3.org/TR/html401/strict.dtd' [ + + + + + + +%paramEntity; + +]""" + event_collector.check_events( + [f""], + [ + ("decl", inside), + ], + ) - def __init__(self) -> None: - self.events = [] - self.append = self.events.append - sgmllib.SGMLParser.__init__(self) - def get_events(self): - # Normalize the list of events so that buffer artefacts don't - # separate runs of contiguous characters. - L = [] - prevtype = None - for event in self.events: - type = event[0] - if type == prevtype == "data": - L[-1] = ("data", L[-1][1] + event[1]) - else: - L.append(event) - prevtype = type - self.events = L - return L +def test_doctype_decl_external(event_collector): + inside = "DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.01//EN'" + event_collector.check_events( + "" % inside, + [ + ("decl", inside), + ], + ) - # structure markup - def unknown_starttag(self, tag, attrs): - self.append(("starttag", tag, attrs)) +def test_underscore_in_attrname(event_collector): + # SF bug #436621 + """Make sure attribute names with underscores are accepted""" + event_collector.check_events( + "", + [ + ("starttag", "a", [("has_under", "has_under"), ("_under", "_under")]), + ], + ) - def unknown_endtag(self, tag): - self.append(("endtag", tag)) - # all other markup +def test_underscore_in_tagname(event_collector): + # SF bug #436621 + """Make sure tag names with underscores are accepted""" + event_collector.check_events( + "", + [ + ("starttag", "has_under", []), + ("endtag", "has_under"), + ], + ) - def handle_comment(self, data): - self.append(("comment", data)) - def handle_charref(self, data): - self.append(("charref", data)) +def test_quotes_in_unquoted_attrs(event_collector): + # SF bug #436621 + """Be sure quotes in unquoted attributes are made part of the value""" + event_collector.check_events( + "", + [ + ("starttag", "a", [("href", "foo'bar\"baz")]), + ], + ) - def handle_data(self, data): - self.append(("data", data)) - def handle_decl(self, decl): - self.append(("decl", decl)) +def test_xhtml_empty_tag(event_collector): + """Handling of XHTML-style empty start tags""" + event_collector.check_events( + "
text", + [ + ("starttag", "br", []), + ("data", "text"), + ("starttag", "i", []), + ("endtag", "i"), + ], + ) - def handle_entityref(self, data): - self.append(("entityref", data)) - def handle_pi(self, data): - self.append(("pi", data)) +def test_processing_instruction_only(event_collector): + event_collector.check_events( + "", + [ + ("pi", "processing instruction"), + ], + ) - def unknown_decl(self, decl): - self.append(("unknown decl", decl)) +def test_bad_nesting(event_collector): + event_collector.check_events( + "
", + [ + ("starttag", "a", []), + ("starttag", "b", []), + ("endtag", "a"), + ("endtag", "b"), + ], + ) -class CDATAEventCollector(EventCollector): - def start_cdata(self, attrs): - self.append(("starttag", "cdata", attrs)) - self.setliteral() +def test_bare_ampersands(event_collector): + event_collector.check_events( + "this text & contains & ampersands &", + [ + ("data", "this text & contains & ampersands &"), + ], + ) -class HTMLEntityCollector(EventCollector): - entity_or_charref = re.compile( - "(?:&([a-zA-Z][-.a-zA-Z0-9]*)|&#(x[0-9a-zA-Z]+|[0-9]+))(;?)" +def test_bare_pointy_brackets(event_collector): + event_collector.check_events( + "this < text > contains < bare>pointy< brackets", + [ + ("data", "this < text > contains < bare>pointy< brackets"), + ], ) - def convert_charref(self, name): - self.append(("charref", "convert", name)) - if name[0] != "x": - return EventCollector.convert_charref(self, name) - def convert_codepoint(self, codepoint): - self.append(("codepoint", "convert", codepoint)) - EventCollector.convert_codepoint(self, codepoint) +@pytest.mark.parametrize( + "source", + ( + """""", + """""", + """""", + """""", + ), +) +def test_attr_syntax(event_collector, source): + output = [("starttag", "a", [("b", "v"), ("c", "v"), ("d", "v"), ("e", "e")])] + event_collector.check_events(source, output) + + +@pytest.mark.parametrize( + "attribute", + ( + "xxx\n\txxx", + "yyy\t\nyyy", + "\txyz\n", + "", + ), +) +@pytest.mark.parametrize("quote", ('"', "'")) +def test_attr_values_quoted(event_collector, attribute, quote): + event_collector.check_events( + f"", + [("starttag", "a", [("b", attribute)])], + ) - def convert_entityref(self, name): - self.append(("entityref", "convert", name)) - return EventCollector.convert_entityref(self, name) - # These to record that they were called, then pass the call along - # to the default implementation so that it's actions can be - # recorded. +def test_attr_values_unquoted_url(event_collector): + # URL construction stuff from RFC 1808: + safe = "$-_.+" + extra = "!*'()," + reserved = ";/?:@&=" + url = f"https://example.com:8080/path/to/file?{safe}{extra}{reserved}" + event_collector.check_events( + """""" % url, + [ + ("starttag", "e", [("a", url)]), + ], + ) - def handle_charref(self, data): - self.append(("charref", data)) - sgmllib.SGMLParser.handle_charref(self, data) - def handle_entityref(self, data): - self.append(("entityref", data)) - sgmllib.SGMLParser.handle_entityref(self, data) +def test_attr_values_unquoted(event_collector): + # Regression test for SF patch #669683. + event_collector.check_events( + "", + [ + ("starttag", "e", [("a", "rgb(1,2,3)")]), + ], + ) -class SGMLParserTestCase(unittest.TestCase): +def test_attr_values_entities(event_collector): + """Substitution of entities and charrefs in attribute values""" + # SF bug #1452246 + event_collector.check_events( + """""", + [ + ( + "starttag", + "a", + [ + ("b", "<"), + ("c", "<>"), + ("d", "<->"), + ("e", "< "), + ("f", "&xxx;"), + ("g", " !"), + ("h", "Ǵ"), + ("i", "x?a=b&c=d;"), + ("j", "*"), + ("k", "*"), + ], + ) + ], + ) - collector = EventCollector - def get_events(self, source): - parser = self.collector() - for s in source: - parser.feed(s) - parser.close() - return parser.get_events() +def test_convert_overrides(html_entity_collector): + # This checks that the character and entity reference + # conversion helpers are called at the documented times. No + # attempt is made to really change what the parser accepts. + # + html_entity_collector.check_events( + 'foo&foobar;*', + [ + ("entityref", "convert", "ldquo"), + ("charref", "convert", "x201d"), + ("starttag", "a", [("title", "“test”")]), + ("data", "foo"), + ("endtag", "a"), + ("entityref", "foobar"), + ("entityref", "convert", "foobar"), + ("charref", "42"), + ("charref", "convert", "42"), + ("codepoint", "convert", 42), + ], + ) - def check_events(self, source, expected_events): - events = self.get_events(source) - if events != expected_events: - self.fail( - "received events did not match expected events\n" - "Expected:\n" - + pprint.pformat(expected_events) - + "\nReceived:\n" - + pprint.pformat(events) - ) - def check_parse_error(self, source): - parser = EventCollector() - try: - parser.feed(source) - parser.close() - except sgmllib.SGMLParseError: - pass - else: - self.fail( - "expected SGMLParseError for %r\nReceived:\n%s" - % (source, pprint.pformat(parser.get_events())) - ) +def test_attr_funky_names(event_collector): + event_collector.check_events( + """""", + [ + ("starttag", "a", [("a.b", "v"), ("c:d", "v"), ("e-f", "v")]), + ], + ) - def test_doctype_decl_internal(self): - inside = """\ -DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.01//EN' - SYSTEM 'http://www.w3.org/TR/html401/strict.dtd' [ - - - - - - - %paramEntity; - -]""" - self.check_events( - ["" % inside], - [ - ("decl", inside), - ], - ) - - def test_doctype_decl_external(self): - inside = "DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.01//EN'" - self.check_events( - "" % inside, - [ - ("decl", inside), - ], - ) - - def test_underscore_in_attrname(self): - # SF bug #436621 - """Make sure attribute names with underscores are accepted""" - self.check_events( - "", - [ - ("starttag", "a", [("has_under", "has_under"), ("_under", "_under")]), - ], - ) - - def test_underscore_in_tagname(self): - # SF bug #436621 - """Make sure tag names with underscores are accepted""" - self.check_events( - "", - [ - ("starttag", "has_under", []), - ("endtag", "has_under"), - ], - ) - - def test_quotes_in_unquoted_attrs(self): - # SF bug #436621 - """Be sure quotes in unquoted attributes are made part of the value""" - self.check_events( - "", - [ - ("starttag", "a", [("href", "foo'bar\"baz")]), - ], - ) - - def test_xhtml_empty_tag(self): - """Handling of XHTML-style empty start tags""" - self.check_events( - "
text", - [ - ("starttag", "br", []), - ("data", "text"), - ("starttag", "i", []), - ("endtag", "i"), - ], - ) - - def test_processing_instruction_only(self): - self.check_events( - "", - [ - ("pi", "processing instruction"), - ], - ) - - def test_bad_nesting(self): - self.check_events( - "
", - [ - ("starttag", "a", []), - ("starttag", "b", []), - ("endtag", "a"), - ("endtag", "b"), - ], - ) - - def test_bare_ampersands(self): - self.check_events( - "this text & contains & ampersands &", - [ - ("data", "this text & contains & ampersands &"), - ], - ) - - def test_bare_pointy_brackets(self): - self.check_events( - "this < text > contains < bare>pointy< brackets", - [ - ("data", "this < text > contains < bare>pointy< brackets"), - ], - ) - - def test_attr_syntax(self): - output = [("starttag", "a", [("b", "v"), ("c", "v"), ("d", "v"), ("e", "e")])] - self.check_events("""""", output) - self.check_events("""""", output) - self.check_events("""""", output) - self.check_events("""""", output) - - def test_attr_values(self): - self.check_events( - """""", - [ - ( - "starttag", - "a", - [("b", "xxx\n\txxx"), ("c", "yyy\t\nyyy"), ("d", "\txyz\n")], - ) - ], - ) - self.check_events( - """""", - [ - ("starttag", "a", [("b", ""), ("c", "")]), - ], - ) - # URL construction stuff from RFC 1808: - safe = "$-_.+" - extra = "!*'()," - reserved = ";/?:@&=" - url = "http://example.com:8080/path/to/file?{}{}{}".format( - safe, extra, reserved - ) - self.check_events( - """""" % url, - [ - ("starttag", "e", [("a", url)]), - ], - ) - # Regression test for SF patch #669683. - self.check_events( - "", - [ - ("starttag", "e", [("a", "rgb(1,2,3)")]), - ], - ) - - def test_attr_values_entities(self): - """Substitution of entities and charrefs in attribute values""" - # SF bug #1452246 - self.check_events( - """""", - [ - ( - "starttag", - "a", - [ - ("b", "<"), - ("c", "<>"), - ("d", "<->"), - ("e", "< "), - ("f", "&xxx;"), - ("g", " !"), - ("h", "Ǵ"), - ("i", "x?a=b&c=d;"), - ("j", "*"), - ("k", "*"), - ], - ) - ], - ) - - def test_convert_overrides(self): - # This checks that the character and entity reference - # conversion helpers are called at the documented times. No - # attempt is made to really change what the parser accepts. - # - self.collector = HTMLEntityCollector - self.check_events( - 'foo&foobar;*', - [ - ("entityref", "convert", "ldquo"), - ("charref", "convert", "x201d"), - ("starttag", "a", [("title", "“test”")]), - ("data", "foo"), - ("endtag", "a"), - ("entityref", "foobar"), - ("entityref", "convert", "foobar"), - ("charref", "42"), - ("charref", "convert", "42"), - ("codepoint", "convert", 42), - ], - ) - - def test_attr_funky_names(self): - self.check_events( - """""", - [ - ("starttag", "a", [("a.b", "v"), ("c:d", "v"), ("e-f", "v")]), - ], - ) - - def test_attr_value_ip6_url(self): - # http://www.python.org/sf/853506 - self.check_events( + +def test_attr_value_ip6_url(event_collector): + # http://www.python.org/sf/853506 + event_collector.check_events( + ( + "" + "" + ), + [ + ("starttag", "a", [("href", "http://[1080::8:800:200C:417A]/")]), + ("starttag", "a", [("href", "http://[1080::8:800:200C:417A]/")]), + ], + ) + + +@pytest.mark.parametrize( + "source, expected", + ( + ("", [("starttag", "a", []), ("starttag", "a", [])]), + ("", [("endtag", "a"), ("starttag", "a", [])]), + ), +) +def test_weird_starttags(event_collector, source, expected): + event_collector.check_events(source, expected) + + +def test_declaration_junk_chars(check_parse_error): + check_parse_error("") + + +def test_get_starttag_text(event_collector): + s = """""" + event_collector.check_events( + s, + [ + ("starttag", "foobar", [("one", "1"), ("two", "2")]), + ], + ) + + +@pytest.mark.parametrize( + "data", + ( + "", + "¬-an-entity-ref;", + "", + ), +) +def test_cdata_content(cdata_event_collector, data): + s = f" {data} " + cdata_event_collector.check_events( + s, + [ + ("starttag", "cdata", []), + ("data", f" {data} "), + ("endtag", "cdata"), + ("starttag", "notcdata", []), + ("data", " "), + ("comment", " comment "), + ("data", " "), + ("endtag", "notcdata"), + ], + ) + + +def test_illegal_declarations(event_collector): + s = 'abcdef' + event_collector.check_events( + s, + [ + ("data", "abc"), + ("unknown decl", 'spacer type="block" height="25"'), + ("data", "def"), + ], + ) + + +def test_enumerated_attr_type(event_collector): + s = "]>" + event_collector.check_events( + s, + [ + ("decl", "DOCTYPE doc []"), + ], + ) + + +@pytest.fixture(scope="session") +def _sgml_input_html(): + # Read the file exactly once. + path = pathlib.Path(__file__).parent / "sgml_input.html" + return path.read_text(encoding="ISO-8859-1") + + +@pytest.fixture() +def sgml_input_html(_sgml_input_html): + return io.StringIO(_sgml_input_html) + + +@pytest.mark.parametrize("chunk_size", (1, 1024, 8212)) +def test_read_chunks(chunk_size, sgml_input_html): + # SF bug #1541697, this caused sgml parser to hang + # Just verify this code doesn't cause a hang. + # The problem goes away if the chunk size is 8212. + + fp = sgmllib.SGMLParser() + while 1: + data = sgml_input_html.read(chunk_size) + fp.feed(data) + if len(data) != chunk_size: + break + + +def test_only_decode_ascii(event_collector): + # SF bug #1651995, make sure non-ascii character references are not decoded + s = '' + event_collector.check_events( + s, + [ ( - "" - "" + "starttag", + "signs", + [ + ("exclamation", "!"), + ("copyright", "©"), + ("quoteleft", "‘"), + ], ), - [ - ("starttag", "a", [("href", "http://[1080::8:800:200C:417A]/")]), - ("starttag", "a", [("href", "http://[1080::8:800:200C:417A]/")]), - ], - ) - - def test_weird_starttags(self): - self.check_events( - "", - [ - ("starttag", "a", []), - ("starttag", "a", []), - ], - ) - self.check_events( - "", - [ - ("endtag", "a"), - ("starttag", "a", []), - ], - ) - - def test_declaration_junk_chars(self): - self.check_parse_error("") - - def test_get_starttag_text(self): - s = """""" - self.check_events( - s, - [ - ("starttag", "foobar", [("one", "1"), ("two", "2")]), - ], - ) - - def test_cdata_content(self): - s = ( - " ¬-an-entity-ref; " - " " - ) - self.collector = CDATAEventCollector - self.check_events( - s, - [ - ("starttag", "cdata", []), - ("data", " ¬-an-entity-ref; "), - ("endtag", "cdata"), - ("starttag", "notcdata", []), - ("data", " "), - ("comment", " comment "), - ("data", " "), - ("endtag", "notcdata"), - ], - ) - s = """ """ - self.check_events( - s, - [ - ("starttag", "cdata", []), - ("data", " "), - ("endtag", "cdata"), - ], - ) - - def test_illegal_declarations(self): - s = 'abcdef' - self.check_events( - s, - [ - ("data", "abc"), - ("unknown decl", 'spacer type="block" height="25"'), - ("data", "def"), - ], - ) - - def test_enumerated_attr_type(self): - s = "]>" - self.check_events( - s, - [ - ("decl", "DOCTYPE doc []"), - ], - ) - - def test_read_chunks(self): - # SF bug #1541697, this caused sgml parser to hang - # Just verify this code doesn't cause a hang. - CHUNK = 1024 # increasing this to 8212 makes the problem go away - - fp = sgmllib.SGMLParser() - path = pathlib.Path(__file__).parent / "sgml_input.html" - with path.open(encoding="ISO-8859-1") as f: - while 1: - data = f.read(CHUNK) - fp.feed(data) - if len(data) != CHUNK: - break - - def test_only_decode_ascii(self): - # SF bug #1651995, make sure non-ascii character references are not decoded - s = '' - self.check_events( - s, - [ - ( - "starttag", - "signs", - [ - ("exclamation", "!"), - ("copyright", "©"), - ("quoteleft", "‘"), - ], - ), - ], - ) - - # XXX These tests have been disabled by prefixing their names with - # an underscore. The first two exercise outstanding bugs in the - # sgmllib module, and the third exhibits questionable behavior - # that needs to be carefully considered before changing it. - - def _test_starttag_end_boundary(self): - self.check_events("", [("starttag", "a", [("b", "<")])]) - self.check_events("", [("starttag", "a", [("b", ">")])]) - - def _test_buffer_artefacts(self): - output = [("starttag", "a", [("b", "<")])] - self.check_events([""], output) - self.check_events([""], output) - self.check_events([""], output) - self.check_events([""], output) - self.check_events([""], output) - self.check_events([""], output) - - output = [("starttag", "a", [("b", ">")])] - self.check_events([""], output) - self.check_events([""], output) - self.check_events([""], output) - self.check_events(["'>"], output) - self.check_events([""], output) - self.check_events([""], output) - - output = [("comment", "abc")] - self.check_events(["", ""], output) - self.check_events(["<", "!--abc-->"], output) - self.check_events([""], output) - self.check_events([""], output) - self.check_events([""], output) - self.check_events([""], output) - self.check_events([""], output) - self.check_events([""], output) - self.check_events(["", ""], output) - - def _test_starttag_junk_chars(self): - self.check_parse_error("<") - self.check_parse_error("<>") - self.check_parse_error("") - self.check_parse_error("") - self.check_parse_error("") - self.check_parse_error("'") - self.check_parse_error("", [("starttag", "a", [("b", "<")])]) + event_collector.check_events("", [("starttag", "a", [("b", ">")])]) + + +def _test_buffer_artefacts(event_collector): + output = [("starttag", "a", [("b", "<")])] + event_collector.check_events([""], output) + event_collector.check_events([""], output) + event_collector.check_events([""], output) + event_collector.check_events([""], output) + event_collector.check_events([""], output) + event_collector.check_events([""], output) + + output = [("starttag", "a", [("b", ">")])] + event_collector.check_events([""], output) + event_collector.check_events([""], output) + event_collector.check_events([""], output) + event_collector.check_events(["'>"], output) + event_collector.check_events([""], output) + event_collector.check_events([""], output) + + output = [("comment", "abc")] + event_collector.check_events(["", ""], output) + event_collector.check_events(["<", "!--abc-->"], output) + event_collector.check_events([""], output) + event_collector.check_events([""], output) + event_collector.check_events([""], output) + event_collector.check_events([""], output) + event_collector.check_events([""], output) + event_collector.check_events([""], output) + event_collector.check_events(["", ""], output) + + +def _test_starttag_junk_chars(check_parse_error): + check_parse_error("<") + check_parse_error("<>") + check_parse_error("") + check_parse_error("") + check_parse_error("") + check_parse_error("'") + check_parse_error("