# -*- coding: utf8 -*-
########################################################################################
# This file is part of exhale. Copyright (c) 2017-2024, Stephen McDowell. #
# Full BSD 3-Clause license available here: #
# #
# https://github.com/svenevs/exhale/blob/master/LICENSE #
########################################################################################
"""
Tests for validating error handling with configs set in ``conf.py``.
"""
from __future__ import unicode_literals
import re
import textwrap
from pathlib import Path
import pytest
from sphinx.errors import ConfigError
from testing import get_exhale_root
from testing.base import ExhaleTestCase
from testing.decorators import confoverrides
[docs]
def assert_message_not_present(test, message, text, flags=0):
"""
Assert that ``message`` **is** found in ``text``.
This method is useful for creating consistent error messages when a test fails,
including printing out what the contents of ``message`` and ``text`` were.
**Parameters**
``test`` (:class:`ExhaleTestCase <testing.base.ExhaleTestCase>`)
The test case to call ``assertTrue`` with, should just be ``self`` for most test
cases in the framework.
``message`` (:class:`python:str`)
The message to search for in ``text``. This will be the ``pattern`` parameter
for a call to :func:`python:re.search`.
``text`` (:class:`python:str`)
The text to be searched. This will be the ``string`` parameter for a call to
:func:`python:re.search`.
``flags`` (:class:`python:int`)
Optional flags to supply as ``flags`` to :func:`python:re.search`. Default of
``0`` means no special flags sent.
"""
test.assertTrue(
re.search(message, text, flags) is None,
textwrap.dedent('''\
Sphinx message stream contained '{message}' but should not have.
Stream contents:
{vsep}
{text}
{vsep}
''').format(
message=message, vsep=("*" * 44), text=text
)
)
[docs]
def assert_message_present(test, message, text, flags=0):
"""
Assert that ``message`` is **not** found in ``text``.
This method is useful for creating consistent error messages when a test fails,
including printing out what the contents of ``message`` and ``text`` were.
**Parameters**
``test`` (:class:`ExhaleTestCase <testing.base.ExhaleTestCase>`)
The test case to call ``assertTrue`` with, should just be ``self`` for most test
cases in the framework.
``message`` (:class:`python:str`)
The message to search for in ``text``. This will be the ``pattern`` parameter
for a call to :func:`python:re.search`.
``text`` (:class:`python:str`)
The text to be searched. This will be the ``string`` parameter for a call to
:func:`python:re.search`.
``flags`` (:class:`python:int`)
Optional flags to supply as ``flags`` to :func:`python:re.search`. Default of
``0`` means no special flags sent.
"""
test.assertTrue(
re.search(message, text, flags) is not None,
textwrap.dedent('''\
Sphinx message stream did not contain '{message}' but should have.
Stream contents:
{vsep}
{text}
{vsep}
''').format(
message=message, vsep=("*" * 44), text=text
)
)
[docs]
class ConfigurationStatusTests(ExhaleTestCase):
"""
Tests to ensure expected status messages are displayed.
"""
test_project = "cpp_nesting"
"""
.. testproject:: cpp_nesting
.. note::
The ``cpp_nesting`` project is just being recycled, the tests for that project
take place in
:class:`CPPNesting <testing.tests.cpp_nesting.CPPNesting>`.
"""
treeview_add_start_message = r"Exhale: adding tree view css / javascript\."
"""
Start message displayed only when :data:`createTreeView <exhale.configs.createTreeView>` is ``True``.
"""
treeview_add_close_message = r"Exhale: added tree view css / javascript\."
"""
Closing message displayed only when :data:`createTreeView <exhale.configs.createTreeView>` is ``True``.
"""
[docs]
def test_no_treeview(self):
"""
Verify no notification for adding CSS / JavaScript issued when no Tree View requested.
"""
sphinx_status = self.app._status.getvalue()
assert_message_not_present(self, self.treeview_add_start_message, sphinx_status)
assert_message_not_present(self, self.treeview_add_close_message, sphinx_status)
[docs]
@confoverrides(exhale_args={"createTreeView": True})
def test_treeview(self):
"""
Verify notification for adding CSS / JavaScript issued when Tree View requested.
"""
sphinx_status = self.app._status.getvalue()
assert_message_present(self, self.treeview_add_start_message, sphinx_status)
assert_message_present(self, self.treeview_add_close_message, sphinx_status)
[docs]
class ConfigurationWarningTests(ExhaleTestCase):
"""
Tests to ensure non-fatal configuration discrepancies receive warnings.
"""
test_project = "cpp_nesting"
"""
.. testproject:: cpp_nesting
.. note::
The ``cpp_nesting`` project is just being recycled, the tests for that project
take place in
:class:`CPPNesting <testing.tests.cpp_nesting.CPPNesting>`.
"""
[docs]
@confoverrides(exhale_args={"createTreeView": False, "treeViewIsBootstrap": True})
def test_treeview_mismatch(self):
"""
Verify warning issued with ``createTreeView=False`` but ``treeViewIsBootstrap=True``.
"""
sphinx_warnings = self.app._warning.getvalue()
expected = "Exhale: `treeViewIsBootstrap=True` ignored since `createTreeView=False`"
self.assertTrue(
expected in sphinx_warnings,
"Sphinx Warnings did not contain '{0}'.".format(expected)
)
[docs]
class ListingExcludeTests(ExhaleTestCase):
"""Test for expected failures when invalid configurations are given in ``conf.py``."""
test_project = "cpp_nesting"
"""
.. testproject:: cpp_nesting
.. note::
The ``cpp_nesting`` project is just being recycled, the tests for that project
take place in
:class:`CPPNesting <testing.tests.cpp_nesting.CPPNesting>`.
"""
[docs]
class BadStr(object):
"""
Helper for :func:`ListingExcludeTests.test_invalid_report_index`.
"""
def __str__(self):
"""Raise :class:`python:AttributeError`."""
raise AttributeError("No string for you!")
[docs]
@pytest.mark.setup_raises(
exception=ConfigError,
match=r"listingExclude item at index 1 cannot be unpacked as `pattern, flags = item`:"
)
@confoverrides(exhale_args={
"listingExclude": [r"valid", BadStr()]
})
def test_invalid_report_index(self):
"""Verify list index is indicated when item cannot be converted to string."""
pass
[docs]
@pytest.mark.setup_raises(
exception=ConfigError,
match=r"Unable to compile specified listingExclude"
)
@confoverrides(exhale_args={
"listingExclude": [(r"some_string", "an invalid flag")]
})
def test_invalid_regex_flags(self):
"""Verify invalid regex ``flags`` are rejected."""
pass
[docs]
@pytest.mark.setup_raises(
exception=ConfigError,
match=r"Unable to compile specified listingExclude"
)
@confoverrides(exhale_args={
"listingExclude": [
r"valid", r"*I don't compile$"
]
})
def test_bad_regex(self):
"""Verify string pattern that does not compile is gracefully rejected."""
pass
[docs]
@pytest.mark.setup_raises(
exception=ConfigError,
match=r"listingExclude item .* cannot be unpacked as `pattern, flags = item`:"
)
@confoverrides(exhale_args={
"listingExclude": [
r"valid",
(r"valid", 0),
(r"invalid", 0, 1) # length 3 and longer cannot unpack
]
})
def test_too_many(self):
"""
Verify that length three item is rejected.
Only ``pattern:str`` or ``(pattern:str, flags:int)`` are allowed.
"""
pass
[docs]
@pytest.mark.setup_raises(
exception=ConfigError,
match=r"listingExclude item .* cannot be unpacked as `pattern, flags = item`:"
)
@confoverrides(exhale_args={
"listingExclude": [1]
})
def test_invalid_pattern(self):
"""Verify that non-string argument for pattern is rejected."""
pass
[docs]
class UnabridgedOrphanKindsTests(ExhaleTestCase):
"""Test various values of :data:`~exhale.configs.unabridgedOrphanKinds`."""
test_project = "cpp_long_names"
"""
.. testproject:: cpp_long_names
The ``cpp_long_names`` project essentially has 1 compound of each kind, which makes
it an ideal project to reuse here.
"""
[docs]
@pytest.mark.setup_raises(
exception=ConfigError,
match=r"^The type of the value for key `unabridgedOrphanKinds`"
)
@confoverrides(exhale_args={
"unabridgedOrphanKinds": False
})
def test_not_iterable_fails(self):
"""Verify that non-list/set values raise a configuration error."""
pass
[docs]
@pytest.mark.setup_raises(
exception=ConfigError,
match=r"^`unabridgedOrphanKinds` must be a list of strings."
)
@confoverrides(exhale_args={
"unabridgedOrphanKinds": ["file", 22, "dir"]
})
def test_non_string_fails(self):
"""Verify that non-string entries raise a configuration error."""
pass
[docs]
@pytest.mark.setup_raises(
exception=ConfigError,
match=r"^Unknown kind `jabooty` given in `unabridgedOrphanKinds`."
)
@confoverrides(exhale_args={
"unabridgedOrphanKinds": ["file", "jabooty", "dir"]
})
def test_invalid_kind(self):
"""Verify that invalid ``kind`` raises a configuration error."""
pass
[docs]
def total(self, root):
"""Count all nodes that are not ``enumvalue`` and ``group``."""
# TODO: probably should make this available in ExhaleRoot...
return sum(n.kind not in {"group", "enumvalue"} for n in root.all_nodes)
[docs]
@confoverrides(exhale_args={"unabridgedOrphanKinds": []})
def test_no_exclusion(self):
"""Verify empty list means no exclusions from full API."""
full_names, orphan_names = self.checkAllFilesIncluded()
root = get_exhale_root(self)
total = self.total(root)
assert len(full_names) == total
assert len(orphan_names) == 0
def _validate_diff_for_kinds(self, diff, *kinds):
"""Check if ``diff`` items got orphaned."""
# Initial check: verify expected lengths.
full_names, orphan_names = self.checkAllFilesIncluded()
root = get_exhale_root(self)
total = self.total(root)
assert len(full_names) == total - diff
assert len(orphan_names) == diff
# Verify the right things actually got excluded.
for node in root.all_nodes:
if node.kind in kinds:
assert node.file_name in orphan_names
else:
assert node.file_name not in orphan_names
[docs]
@confoverrides(exhale_args={"unabridgedOrphanKinds": {"namespace"}})
def test_orphan_namespace(self):
"""Verify excluding ``namespace`` behaves as expected."""
self._validate_diff_for_kinds(1, "namespace")
[docs]
@confoverrides(exhale_args={"unabridgedOrphanKinds": {"class"}})
def test_orphan_class(self):
"""Verify excluding ``class`` behaves as expected."""
self._validate_diff_for_kinds(2, "class", "struct")
[docs]
@confoverrides(exhale_args={"unabridgedOrphanKinds": {"struct"}})
def test_orphan_struct(self):
"""Verify excluding ``struct`` behaves as expected."""
self._validate_diff_for_kinds(2, "struct", "class")
[docs]
@confoverrides(exhale_args={"unabridgedOrphanKinds": {"class", "struct"}})
def test_orphan_class_struct(self):
"""Verify excluding ``class`` and ``struct`` behaves as expected."""
self._validate_diff_for_kinds(2, "class", "struct")
[docs]
@confoverrides(exhale_args={"unabridgedOrphanKinds": {"enum"}})
def test_orphan_enum(self):
"""Verify excluding ``enum`` behaves as expected."""
self._validate_diff_for_kinds(1, "enum")
[docs]
@confoverrides(exhale_args={"unabridgedOrphanKinds": {"union"}})
def test_orphan_union(self):
"""Verify excluding ``union`` behaves as expected."""
self._validate_diff_for_kinds(1, "union")
[docs]
@confoverrides(exhale_args={"unabridgedOrphanKinds": {"function"}})
def test_orphan_function(self):
"""Verify excluding ``function`` behaves as expected."""
self._validate_diff_for_kinds(1, "function")
[docs]
@confoverrides(exhale_args={"unabridgedOrphanKinds": {"variable"}})
def test_orphan_variable(self):
"""Verify excluding ``variable`` behaves as expected."""
# There are two variables in the project.
self._validate_diff_for_kinds(2, "variable")
[docs]
@confoverrides(exhale_args={"unabridgedOrphanKinds": {"define"}})
def test_orphan_define(self):
"""Verify excluding ``define`` behaves as expected."""
# There are two define's in the project.
self._validate_diff_for_kinds(2, "define")
[docs]
@confoverrides(exhale_args={"unabridgedOrphanKinds": {"typedef"}})
def test_orphan_typedef(self):
"""Verify excluding ``typedef`` behaves as expected."""
self._validate_diff_for_kinds(1, "typedef")
[docs]
@confoverrides(exhale_args={"unabridgedOrphanKinds": {"file"}})
def test_orphan_file(self):
"""Verify excluding ``file`` behaves as expected."""
self._validate_diff_for_kinds(1, "file")
[docs]
@confoverrides(exhale_args={
"exhaleDoxygenStdin": textwrap.dedent("""\
INPUT = ../include
EXCLUDE_PATTERNS = */page_town_rock*.hpp
""")})
class ManualIndexingTests(ExhaleTestCase):
"""
Tests for :ref:`manual_indexing`.
Includes checks for:
- :data:`~exhale.configs.rootFileName`
- :data:`~exhale.configs.classHierarchyFilename`
- :data:`~exhale.configs.fileHierarchyFilename`
- :data:`~exhale.configs.pageHierarchyFilename`
- :data:`~exhale.configs.unabridgedApiFilename`
- :data:`~exhale.configs.unabridgedOrphanFilename`
"""
test_project = "cpp_nesting"
""".. testproject:: cpp_nesting"""
[docs]
def root_file_path(self) -> Path:
"""Return the path to the ``rootFileName``."""
root_file_name = self.app.config.exhale_args["rootFileName"]
return Path(self.getAbsContainmentFolder()) / root_file_name
[docs]
def verify_expected_files(
self,
*,
page_default: str = "page_view_hierarchy.rst.include",
class_default: str = "class_view_hierarchy.rst.include",
file_default: str = "file_view_hierarchy.rst.include",
u_api_default: str = "unabridged_api.rst.include",
u_orphan_default: str = "unabridged_orphan.rst"
):
"""Verify the expected files were generated and included."""
# The files that may or may not be generated by exhale.
containment_folder = Path(self.getAbsContainmentFolder())
root_path = Path(self.app.exhale_root.full_root_file_path)
page_path = Path(self.app.exhale_root.page_hierarchy_file)
class_path = Path(self.app.exhale_root.class_hierarchy_file)
file_path = Path(self.app.exhale_root.file_hierarchy_file)
u_api_path = Path(self.app.exhale_root.unabridged_api_file)
u_orphan_path = Path(self.app.exhale_root.unabridged_orphan_file)
# Calling tests change the exhale_args and corresponding default parameter,
# forcing manual effort deliberately for test correctness.
def verify_name(name: str, actual: Path):
# NOTE: doing direct path comparisons doesn't work on Windows, exhale (for
# better or worse) currently puts the magic windows prefix
# AssertionError: WindowsPath('//?/D:/a/exhale/...') !=
# WindowsPath('D:/a/exhale/...')
# So instead just compare names and parent directory name and quit since I
# don't want to deal with this.
self.assertEqual(
name,
actual.name,
f"Expected {name} and {actual.name} to be the same ({str(actual)})."
)
self.assertEqual(
containment_folder.name,
actual.parent.name,
f"Expected parent '{actual.parent.name}' from '{str(actual.parent)}' "
f"to be '{containment_folder.name}' from '{str(containment_folder)}'."
)
verify_name(page_default, page_path)
verify_name(class_default, class_path)
verify_name(file_default, file_path)
verify_name(u_api_default, u_api_path)
verify_name(u_orphan_default, u_orphan_path)
# Make sure that everything is still getting included in the root document.
if root_path.is_file():
with open(root_path) as f:
root_contents = f.read()
def verify_include_directive(f: Path):
if f.is_file():
condition = f".. include:: {f.name}" in root_contents
prefix = f"Expected '{f.name}' to be `.. included::`ed in"
else:
condition = f".. include:: {f.name}" not in root_contents
prefix = f"'{f.name}' should *NOT* have been `.. include::`ed in"
self.assertTrue(
condition,
f"{prefix} '{root_path.name}':\n{root_contents}"
)
verify_include_directive(page_path)
verify_include_directive(class_path)
verify_include_directive(file_path)
verify_include_directive(u_api_path)
[docs]
@pytest.mark.setup_raises(
exception=ConfigError,
match=r"The given `rootFileName` \(no_good\) did not end with '.rst'.*"
)
@confoverrides(exhale_args={"rootFileName": "no_good"})
def test_invalid_root_filename(self):
"""
Test that non-'.rst' suffix file is rejected, the root document is rst only.
"""
pass
[docs]
@confoverrides(exhale_args={"rootFileName": "EXCLUDE"})
def test_no_root_generated(self):
"""Test that ``rootFileName=EXCLUDE`` does not generate a file."""
self.assertFalse(
Path(self.app.exhale_root.full_root_file_path).exists(),
"The rootFileName should not have been generated."
)
self.verify_expected_files()
[docs]
@confoverrides(exhale_args={"classHierarchyFilename": "classes.rst"})
def test_class_hierarchy_filename(self):
"""Test changing :data:`~exhale.configs.classHierarchyFilename` works."""
self.verify_expected_files(class_default="classes.rst")
[docs]
@confoverrides(exhale_args={"fileHierarchyFilename": "filez.rst"})
def test_file_hierarchy_filename(self):
"""Test changing :data:`~exhale.configs.fileHierarchyFilename` works."""
self.verify_expected_files(file_default="filez.rst")
[docs]
@confoverrides(exhale_args={
# Setup for getting pages.
"rootFileTitle": "",
"exhaleDoxygenStdin": textwrap.dedent("""\
INPUT = ../include
EXCLUDE_PATTERNS = */page_town_rock_alt.hpp
"""),
"pageHierarchyFilename": "pagez.rst"
})
def test_page_hierarchy_filename(self):
"""Test changing :data:`~exhale.configs.pageHierarchyFilename` works."""
page_path = Path(self.app.exhale_root.page_hierarchy_file)
self.assertTrue(page_path.is_file(), f"Expected '{page_path.name}' to exist.")
self.assertEqual(
len(self.app.exhale_root.pages),
1,
"Internal error: incorrect number of pages found."
)
self.verify_expected_files(page_default="pagez.rst")
[docs]
@confoverrides(exhale_args={"unabridgedApiFilename": "ub_api.rst"})
def test_unabridged_api_filename(self):
"""Test changing :data:`~exhale.configs.unabridgedApiFilename` works."""
u_api_path = Path(self.app.exhale_root.unabridged_api_file)
self.assertTrue(u_api_path.is_file(), f"Expected '{u_api_path.name}' to exist.")
self.verify_expected_files(u_api_default="ub_api.rst")
[docs]
@confoverrides(exhale_args={"unabridgedOrphanFilename": "ub_orphan.rst"})
def test_unabridged_orphan_filename(self):
"""Test changing :data:`~exhale.configs.unabridgedOrphanFilename` works."""
u_orphan_path = Path(self.app.exhale_root.unabridged_api_file)
self.assertTrue(
u_orphan_path.is_file(),
f"Expected '{u_orphan_path.name}' to exist."
)
self.verify_expected_files(u_orphan_default="ub_orphan.rst")