From: Stefano Rivera <stefanor@debian.org>
Date: Wed, 30 Jan 2019 15:03:37 +0100
Subject: Skip tests that require html5lib or LXML

Forwarded: https://github.com/facelessuser/soupsieve/pull/100
---
 tests/test_bs4_cases.py |  4 ++++
 tests/test_level2.py    |  1 +
 tests/test_level3.py    |  2 +-
 tests/test_level4.py    |  5 +++--
 tests/test_soupsieve.py |  6 ++++++
 tests/util.py           | 47 ++++++++++++++++++++++++++++++++++++++++++++++-
 6 files changed, 61 insertions(+), 4 deletions(-)

diff --git a/tests/test_bs4_cases.py b/tests/test_bs4_cases.py
index 21e2ad6..7ba86ce 100644
--- a/tests/test_bs4_cases.py
+++ b/tests/test_bs4_cases.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
 from bs4 import BeautifulSoup
 import unittest
 import soupsieve as sv
+from . import util
 
 
 class SelectorNthOfTypeBugTest(unittest.TestCase):
@@ -105,6 +106,7 @@ NAMESPACES = dict(x="http://www.w3.org/2003/05/soap-envelope",
                   z="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd")
 
 
+@util.requires_lxml
 def test_simple_xml():
     """Test basic XML."""
     xml = BeautifulSoup(SIMPLE_XML, "xml")
@@ -118,6 +120,7 @@ def test_simple_xml():
     assert not xml.select_one("header")
 
 
+@util.requires_lxml
 def test_namespace_xml():
     """Test namespace XML."""
     xml = BeautifulSoup(NAMESPACE_XML, "xml")
@@ -131,6 +134,7 @@ def test_namespace_xml():
     assert not xml.select_one("action")
 
 
+@util.requires_lxml
 def test_namespace_xml_with_namespace():
     """Test namespace selectors with XML."""
     xml = BeautifulSoup(NAMESPACE_XML, "xml")
diff --git a/tests/test_level2.py b/tests/test_level2.py
index 3f52dc9..7c53a74 100644
--- a/tests/test_level2.py
+++ b/tests/test_level2.py
@@ -153,6 +153,7 @@ class TestLevel2(util.TestCase):
             flags=util.HTML
         )
 
+    @util.requires_html5lib
     @util.skip_no_quirks
     def test_leading_combinator_quirks(self):
         """Test scope with quirks."""
diff --git a/tests/test_level3.py b/tests/test_level3.py
index cabc224..05a854d 100644
--- a/tests/test_level3.py
+++ b/tests/test_level3.py
@@ -920,7 +920,7 @@ class TestLevel3(util.TestCase):
             flags=util.HTML
         )
 
-        for parser in ('html.parser', 'lxml', 'html5lib'):
+        for parser in util.available_parsers('html.parser', 'lxml', 'html5lib'):
             # Paragraph is the root. There is no document.
             markup = """<p id="1">text</p>"""
             soup = bs4.BeautifulSoup(markup, parser)
diff --git a/tests/test_level4.py b/tests/test_level4.py
index efdf28e..79e068b 100644
--- a/tests/test_level4.py
+++ b/tests/test_level4.py
@@ -1075,7 +1075,8 @@ class TestLevel4(util.TestCase):
             flags=util.HTML
         )
 
-        for parser in ('html.parser', 'lxml', 'html5lib', 'xml'):
+        for parser in util.available_parsers(
+                'html.parser', 'lxml', 'html5lib', 'xml'):
             soup = bs4.BeautifulSoup(textwrap.dedent(markup.replace('\r\n', '\n')), parser)
             el = soup.html
 
@@ -1462,7 +1463,7 @@ class TestLevel4(util.TestCase):
         )
 
         # Input is root
-        for parser in ('html.parser', 'lxml', 'html5lib'):
+        for parser in util.available_parsers('html.parser', 'lxml', 'html5lib'):
             markup = """<input id="1" type="text" dir="auto">"""
             soup = bs4.BeautifulSoup(markup, parser)
             fragment = soup.input.extract()
diff --git a/tests/test_soupsieve.py b/tests/test_soupsieve.py
index 2300b69..616a7fa 100644
--- a/tests/test_soupsieve.py
+++ b/tests/test_soupsieve.py
@@ -10,11 +10,13 @@ import random
 import pytest
 import pickle
 import warnings
+from .util import requires_html5lib
 
 
 class TestSoupSieve(unittest.TestCase):
     """Test Soup Sieve."""
 
+    @requires_html5lib
     def test_comments(self):
         """Test comments."""
 
@@ -50,6 +52,7 @@ class TestSoupSieve(unittest.TestCase):
         comments = [sv_util.ustr(c).strip() for c in pattern.icomments(soup, limit=2)]
         self.assertEqual(sorted(comments), sorted(['before header', 'comment']))
 
+    @requires_html5lib
     def test_select(self):
         """Test select."""
 
@@ -103,6 +106,7 @@ class TestSoupSieve(unittest.TestCase):
 
         self.assertEqual(sorted(['5']), sorted(ids))
 
+    @requires_html5lib
     def test_match(self):
         """Test matching."""
 
@@ -128,6 +132,7 @@ class TestSoupSieve(unittest.TestCase):
         self.assertTrue(sv.match('span#\\35', nodes[0]))
         self.assertFalse(sv.match('span#\\35', nodes[1]))
 
+    @requires_html5lib
     def test_filter(self):
         """Test filter."""
 
@@ -157,6 +162,7 @@ class TestSoupSieve(unittest.TestCase):
         self.assertEqual(len(nodes), 1)
         self.assertEqual(nodes[0].attrs['id'], '6')
 
+    @requires_html5lib
     def test_closest(self):
         """Test closest."""
 
diff --git a/tests/util.py b/tests/util.py
index 003d5cd..00afcf1 100644
--- a/tests/util.py
+++ b/tests/util.py
@@ -5,6 +5,22 @@ import bs4
 import textwrap
 import soupsieve as sv
 import sys
+import pytest
+
+try:
+    from bs4.builder import HTML5TreeBuilder
+    HTML5LIB_PRESENT = True
+except ImportError:
+    HTML5LIB_PRESENT = False
+
+try:
+    from bs4.builder import (
+        LXMLTreeBuilderForXML,
+        LXMLTreeBuilder,
+        )
+    LXML_PRESENT = True
+except ImportError:
+    LXML_PRESENT = False
 
 PY3 = sys.version_info >= (3, 0)
 
@@ -79,7 +95,7 @@ class TestCase(unittest.TestCase):
         elif mode in (XHTML, XML):
             bs_mode = ('xml',)
 
-        for parser in bs_mode:
+        for parser in available_parsers(*bs_mode):
             print('PARSER: ', parser)
             soup = bs4.BeautifulSoup(textwrap.dedent(markup.replace('\r\n', '\n')), parser)
             # print(soup)
@@ -92,3 +108,32 @@ class TestCase(unittest.TestCase):
                 print('TAG: ', el.name)
                 ids.append(el.attrs['id'])
             self.assertEqual(sorted(ids), sorted(expected_ids))
+
+
+def available_parsers(*parsers):
+    '''Filter a list of bs4 parsers, down to the available ones.
+
+    If there are non, report the test as skipped to pytest.
+    '''
+    ran_test = False
+    for parser in parsers:
+        if parser in ('xml', 'lxml') and not LXML_PRESENT or (
+                parser == 'html5lib' and not HTML5LIB_PRESENT):
+            print('SKIPPED {}, not installed'.format(parser))
+        else:
+            ran_test = True
+            yield parser
+    if not ran_test:
+        raise pytest.skip('no available parsers')
+
+
+def requires_lxml(test):
+    '''Decorator that marks a test as requiring LXML'''
+    return pytest.mark.skipif(
+        not LXML_PRESENT, reason='test requires lxml')(test)
+
+
+def requires_html5lib(test):
+    '''Decorator that marks a test as requiring html5lib'''
+    return pytest.mark.skipif(
+        not LXML_PRESENT, reason='test requires lxml')(test)
