# -*- 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 the ``cpp_nesting`` project.
"""
from __future__ import unicode_literals
import os
import re
import textwrap
from testing import get_exhale_root
from testing.base import ExhaleTestCase
from testing.decorators import confoverrides
from testing.hierarchies import \
class_hierarchy, compare_class_hierarchy, compare_file_hierarchy, file_hierarchy
[docs]
class TestedExclusionTypes(object):
"""
An "enum" for listing exclusions we care about.
Yes, I still need to support python 2.
"""
NoExclusions = 0
"""No exclusions were requested."""
AllImpl = 1
"""All ``r".*Impl$"`` should be excluded."""
DetailImpl = 2
"""Only ``r".*detail::.*Impl$"`` should be excluded."""
[docs]
class CPPPimpl(ExhaleTestCase):
"""
Primary test class for project ``cpp_pimpl``.
"""
test_project = "cpp_pimpl"
""".. testproject:: cpp_pimpl"""
[docs]
def impl_link_names(self):
"""Link names for ``pimpl::{Earth,Jupiter}Impl``."""
links = self.link_name_format_dict()
return {
"- :ref:`{pimpl_EarthImpl}`".format(**links),
"- :ref:`{pimpl_JupiterImpl}`".format(**links)
}
[docs]
def detail_impl_link_names(self):
"""Link names for ``pimpl::detail::{Earth,Jupiter}Impl``."""
links = self.link_name_format_dict()
return {
"- :ref:`{pimpl_detail_EarthImpl}`".format(**links),
"- :ref:`{pimpl_detail_JupiterImpl}`".format(**links),
}
[docs]
def earth_hpp_link_names(self):
"""Link names for ``earth.hpp``."""
links = self.link_name_format_dict()
return {
"- :ref:`{pimpl_Earth}`".format(**links),
"- :ref:`{pimpl_Earth_v2}`".format(**links),
"- :ref:`{pimpl_EarthImpl}`".format(**links),
"- :ref:`{pimpl_detail_EarthImpl}`".format(**links)
}
[docs]
def jupiter_hpp_link_names(self):
"""Link names for ``jupiter.hpp``."""
links = self.link_name_format_dict()
return {
"- :ref:`{pimpl_Jupiter}`".format(**links),
"- :ref:`{pimpl_Jupiter_v2}`".format(**links),
"- :ref:`{pimpl_JupiterImpl}`".format(**links),
"- :ref:`{pimpl_detail_JupiterImpl}`".format(**links)
}
[docs]
def nspace_pimpl_link_names(self):
"""Link names for ``namespace pimpl``."""
links = self.link_name_format_dict()
return {
"- :ref:`{pimpl_detail}`".format(**links),
"- :ref:`{pimpl_Earth}`".format(**links),
"- :ref:`{pimpl_Earth_v2}`".format(**links),
"- :ref:`{pimpl_EarthImpl}`".format(**links),
"- :ref:`{pimpl_Jupiter}`".format(**links),
"- :ref:`{pimpl_Jupiter_v2}`".format(**links),
"- :ref:`{pimpl_JupiterImpl}`".format(**links),
"- :ref:`{pimpl_Planet}`".format(**links)
}
[docs]
def nspace_pimpl_detail_link_names(self):
"""Link names for ``namespace pimpl::detail``."""
links = self.link_name_format_dict()
return {
"- :ref:`{pimpl_detail_EarthImpl}`".format(**links),
"- :ref:`{pimpl_detail_JupiterImpl}`".format(**links)
}
[docs]
def expected_class_hierarchy(self, exclusions):
"""
Return expected rst class hierarchy listing based on specified ``exclusions``.
Helper method for :func:`validate_class_hierarchy`.
**Parameters**
``exclusions`` (:class:`TestedExclusionTypes`)
The exclusion that is currently being tested.
**Return** (:class:`python:str`)
The expected rst class listing. If ``exclusions`` is invalid, then the
string ``"INTERNAL TESTING ERROR"`` is returned.
"""
full_class_hierarchy_lines = textwrap.dedent('''\
- :ref:`{pimpl}`
- :ref:`{pimpl_detail}`
- :ref:`{pimpl_detail_EarthImpl}`
- :ref:`{pimpl_detail_JupiterImpl}`
- :ref:`{pimpl_Earth}`
- :ref:`{pimpl_Earth_v2}`
- :ref:`{pimpl_EarthImpl}`
- :ref:`{pimpl_Jupiter}`
- :ref:`{pimpl_Jupiter_v2}`
- :ref:`{pimpl_JupiterImpl}`
- :ref:`{pimpl_Planet}`
''').format(**self.link_name_format_dict()).splitlines()
if exclusions == TestedExclusionTypes.NoExclusions:
return "\n".join(full_class_hierarchy_lines)
elif exclusions == TestedExclusionTypes.AllImpl:
return "\n".join([
line for line in full_class_hierarchy_lines
if "detail" not in line and "impl" not in line.lower()
])
elif exclusions == TestedExclusionTypes.DetailImpl:
return "\n".join([
line for line in full_class_hierarchy_lines
if "detail" not in line
])
return "INTERNAL TESTING ERROR"
[docs]
def validate_class_hierarchy(self, exclusions):
"""
Validate generated class hierarchy rst list is correct based on ``exclusions``.
**Parameters**
``exclusions`` (:class:`TestedExclusionTypes`)
The exclusion that is currently being tested.
"""
class_hierarchy_path = os.path.join(
self.getAbsContainmentFolder(), "class_view_hierarchy.rst.include"
)
expected_class_hierarchy = self.expected_class_hierarchy(exclusions)
with open(class_hierarchy_path) as class_hierarchy:
class_hierarchy_contents = class_hierarchy.read()
self.assertTrue(
expected_class_hierarchy in class_hierarchy_contents,
textwrap.dedent('''\
Expected class hierarchy not in generated class hierarchy.
Expected class hierarchy:
{vsep}
{expected}
{vsep}
Generated class hierarchy:
{vsep}
{generated}
''').format(
vsep=('-' * 44),
expected=expected_class_hierarchy,
generated=class_hierarchy_contents
)
)
[docs]
def validate_namespace_listings(self, exclusions):
"""
Validate generated namespace rst listings are correct based on ``exclusions``.
This project contains two namespaces that are tested: ``namespace pimpl`` and
``namespace pimpl::detail``.
**Parameters**
``exclusions`` (:class:`TestedExclusionTypes`)
The exclusion that is currently being tested.
"""
# First gather the two namespace nodes for this project so we can read in the
# generated file's contents.
exhale_root = get_exhale_root(self)
# TODO: this may break if you un-do the reparenting stuff
# There's only one top-level namespace
pimpl = exhale_root.namespaces[0]
pimpl_detail = None
for child in pimpl.children:
if child.kind == "namespace" and child.name == "pimpl::detail":
pimpl_detail = child
break
# Make sure required / forbidden items check out for namespace pimpl
pimpl_contents = self.contents_for_node(pimpl)
if exclusions in {TestedExclusionTypes.NoExclusions, TestedExclusionTypes.DetailImpl}:
pimpl_required = self.nspace_pimpl_link_names()
pimpl_forbidden = None
elif exclusions == TestedExclusionTypes.AllImpl:
pimpl_required = self.nspace_pimpl_link_names() - self.impl_link_names()
pimpl_forbidden = self.impl_link_names()
self.cross_validate(
pimpl_contents, required=pimpl_required, forbidden=pimpl_forbidden
)
# Make sure required / forbidden items check out for namespace pimpl::detail
pimpl_detail_contents = self.contents_for_node(pimpl_detail)
if exclusions == TestedExclusionTypes.NoExclusions:
pimpl_detail_required = self.nspace_pimpl_detail_link_names()
pimpl_detail_forbidden = None
elif exclusions in {TestedExclusionTypes.AllImpl, TestedExclusionTypes.DetailImpl}:
pimpl_detail_required = None
pimpl_detail_forbidden = self.nspace_pimpl_detail_link_names()
self.cross_validate(
pimpl_detail_contents, required=pimpl_detail_required, forbidden=pimpl_detail_forbidden
)
[docs]
def validate_file_listings(self):
"""
Validate ``{earth,jupiter}.hpp`` link to all items (regardless of ``"listingExclude"``).
"""
# Gather the exhale nodes for the two files we care about.
exhale_root = get_exhale_root(self)
earth_hpp = None
jupiter_hpp = None
for f in exhale_root.files:
if "earth.hpp" in f.name:
earth_hpp = f
elif "jupiter.hpp" in f.name:
jupiter_hpp = f
# Make sure the "excluded" items are always included on file pages.
self.cross_validate(
self.contents_for_node(earth_hpp), required=self.earth_hpp_link_names(), forbidden=None
)
self.cross_validate(
self.contents_for_node(jupiter_hpp), required=self.jupiter_hpp_link_names(), forbidden=None
)
[docs]
def test_hierarchies(self):
"""Verify the class and file hierarchies."""
compare_class_hierarchy(self, class_hierarchy(self.class_hierarchy_dict()))
compare_file_hierarchy(self, file_hierarchy(self.file_hierarchy_dict()))
# TODO: once config objects are in this override is not necessary, but currently
# the tests in tests/configs.py populate a listingExclude....
[docs]
@confoverrides(exhale_args={"listingExclude": []})
def test_no_listing_exclusions(self):
"""Verify empty listing exclude results in no change in listed API."""
self.validate_class_hierarchy(TestedExclusionTypes.NoExclusions)
self.validate_namespace_listings(TestedExclusionTypes.NoExclusions)
self.validate_file_listings()
self.checkAllFilesIncluded()
[docs]
@confoverrides(exhale_args={"listingExclude": [r".*Impl$"]})
def test_impl_exclude(self):
"""Verify ``r".*Impl$"`` excludes ``*Impl`` class names."""
self.validate_class_hierarchy(TestedExclusionTypes.AllImpl)
self.validate_namespace_listings(TestedExclusionTypes.AllImpl)
self.validate_file_listings()
self.checkAllFilesIncluded()
[docs]
@confoverrides(exhale_args={"listingExclude": [(r".*impl$", re.IGNORECASE)]})
def test_impl_exclude_ignorecase(self):
"""
Verify ``r".*impl$`` with :data:`python:re.IGNORECASE` excludes ``*Impl`` items.
"""
self.validate_class_hierarchy(TestedExclusionTypes.AllImpl)
self.validate_namespace_listings(TestedExclusionTypes.AllImpl)
self.validate_file_listings()
self.checkAllFilesIncluded()
[docs]
@confoverrides(exhale_args={"listingExclude": [r".*detail::.*Impl$"]})
def test_detail_impl_exclude(self):
"""
Verify ``r".*detail::.*Impl$`` excludes ``*detail::*Impl`` items.
"""
self.validate_class_hierarchy(TestedExclusionTypes.DetailImpl)
self.validate_namespace_listings(TestedExclusionTypes.DetailImpl)
self.validate_file_listings()
self.checkAllFilesIncluded()
[docs]
@confoverrides(exhale_args={"listingExclude": [(r".*detail::.*impl$", re.IGNORECASE)]})
def test_detail_impl_exclude_ignorecase(self):
"""
Verify ``r".*detail::.*impl$`` with |I| excludes ``*detail::*Impl`` items.
.. |I| replace:: :data:`python:re.IGNORECASE`
"""
self.validate_class_hierarchy(TestedExclusionTypes.DetailImpl)
self.validate_namespace_listings(TestedExclusionTypes.DetailImpl)
self.validate_file_listings()
self.checkAllFilesIncluded()