Skip to content

Commit

Permalink
Add HTML tests to avoid regressions (#33)
Browse files Browse the repository at this point in the history
Essentially, copy the HTML test approach
from the Sphinx project.
  • Loading branch information
mikemckiernan authored Feb 7, 2023
1 parent 3a5d005 commit f60fd4d
Show file tree
Hide file tree
Showing 8 changed files with 664 additions and 370 deletions.
848 changes: 478 additions & 370 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ mypy = "^0.910"
types-docutils = "^0.17.0"
pep8-naming = "^0.13"
coverage = "^6.5"
lxml = "^4.9.2"
lxml-stubs = "^0.4.0"

[tool.poetry.group.dev.dependencies.isort]
version = "^5.10"
Expand Down
108 changes: 108 additions & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""Test HTML output the same way that Sphinx does in test_build_html.py."""
import re
from itertools import chain, cycle
from pathlib import Path
from typing import Dict

import pytest
from docutils import nodes
from lxml import etree as lxmltree
from sphinx.testing.path import path as sphinx_path
from sphinx.testing.util import SphinxTestApp

pytest_plugins = "sphinx.testing.fixtures"

etree_cache: Dict[str, str] = {}


@pytest.fixture(scope='session')
def rootdir():
return sphinx_path(__file__).parent.abspath() / 'roots'


class SphinxBuilder:
def __init__(self, app: SphinxTestApp, src_path: Path):
self.app = app
self._src_path = src_path

@property
def src_path(self) -> Path:
return self._src_path

@property
def out_path(self) -> Path:
return Path(self.app.outdir)

def build(self, assert_pass=True):
self.app.build()
if assert_pass:
assert self.warnings == "", self.status
return self

@property
def status(self):
return self.app._status.getvalue()

@property
def warnings(self):
return self.app._warning.getvalue()

def get_doctree(self, docname: str, post_transforms: bool = False) -> nodes.document:
assert self.app.env is not None
doctree = self.app.env.get_doctree(docname)
if post_transforms:
self.app.env.apply_post_transforms(doctree, docname)
return doctree


@pytest.fixture(scope='module')
def cached_etree_parse():
def parse(fname):
if fname in etree_cache:
return etree_cache[fname]
with (fname).open('r') as fp:
data = fp.read().replace('\n', '')
etree = lxmltree.HTML(data)
etree_cache.clear()
etree_cache[fname] = etree
return etree

yield parse
etree_cache.clear()


def flat_dict(d):
return chain.from_iterable([zip(cycle([fname]), values) for fname, values in d.items()])


def check_xpath(etree, fname, path, check, be_found=True):
nodes = list(etree.xpath(path))
if check is None:
assert nodes == [], f'found any nodes matching xpath {path!r} in file {fname}'
return
else:
assert nodes != [], f'did not find any node matching xpath {path!r} in file {fname}'
if callable(check):
check(nodes)
elif not check:
# only check for node presence
pass
else:

def get_text(node):
if node.text is not None:
# the node has only one text
return node.text
else:
# the node has tags and text; gather texts just under the node
return ''.join(n.tail or '' for n in node)

rex = re.compile(check)
if be_found:
if any(rex.search(get_text(node)) for node in nodes):
return
else:
if all(not rex.search(get_text(node)) for node in nodes):
return

raise AssertionError(f'{check!r} not found in any node matching path {path} in {fname}: {[node.text for node in nodes]!r}')
1 change: 1 addition & 0 deletions test/roots/test-default-html/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
extensions = ["sphinxarg.ext"]
7 changes: 7 additions & 0 deletions test/roots/test-default-html/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Sample
######

.. argparse::
:filename: test/sample-directive-opts.py
:prog: sample-directive-opts
:func: get_parser
8 changes: 8 additions & 0 deletions test/roots/test-default-html/subcommand-a.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Command A
=========

.. argparse::
:filename: test/sample-directive-opts.py
:prog: sample-directive-opts
:func: get_parser
:path: A
21 changes: 21 additions & 0 deletions test/sample-directive-opts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import argparse


def get_parser():
parser = argparse.ArgumentParser(prog='sample-directive-opts', description='Support SphinxArgParse HTML testing')
subparsers = parser.add_subparsers()
parser_a = subparsers.add_parser('A', help='A subparser')
parser_a.add_argument('baz', type=int, help='An integer')
parser_b = subparsers.add_parser('B', help='B subparser')
parser_b.add_argument('--barg', choices='XYZ', help='A list of choices')

parser.add_argument('--foo', help='foo help')
parser.add_argument('foo2', metavar='foo2 metavar', help='foo2 help')
grp1 = parser.add_argument_group('bar options')
grp1.add_argument('--bar', help='bar help')
grp1.add_argument('quux', help='quux help')
grp2 = parser.add_argument_group('bla options')
grp2.add_argument('--blah', help='blah help')
grp2.add_argument('sniggly', help='sniggly help')

return parser
39 changes: 39 additions & 0 deletions test/test_default_html.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Test the HTML builder and check output against XPath."""

import pytest

from .conftest import check_xpath, flat_dict


@pytest.mark.parametrize(
"fname,expect",
flat_dict(
{
'index.html': [
(".//h1", 'Sample'),
(".//h1", 'blah-blah', False),
(".//div[@class='highlight']//span", 'usage'),
(".//h2", 'Positional Arguments'),
(".//section[@id='positional-arguments']", ''),
(".//section[@id='positional-arguments']/dl/dt[1]/kbd", 'foo2 metavar'),
(".//section[@id='named-arguments']", ''),
(".//section[@id='named-arguments']/dl/dt[1]/kbd", '--foo'),
(".//section[@id='bar-options']", ''),
(".//section[@id='bar-options']/dl/dt[1]/kbd", '--bar'),
],
'subcommand-a.html': [
(".//h1", 'Sample', False),
(".//h1", 'Command A'),
(".//div[@class='highlight']//span", 'usage'),
(".//h2", 'Positional Arguments'),
(".//section[@id='positional-arguments']", ''),
(".//section[@id='positional-arguments']/dl/dt[1]/kbd", 'baz'),
],
}
),
)
@pytest.mark.sphinx('html', testroot='default-html')
def test_default_html(app, cached_etree_parse, fname, expect):
app.build()
print(app.outdir / fname)
check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)

0 comments on commit f60fd4d

Please sign in to comment.