You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ansible/test/units/_internal/templating/test_common_.py

192 lines
7.0 KiB
Python

from __future__ import annotations
import copy
import pickle
import pytest
import jinja2
import jinja2.utils
from ansible._internal._templating._jinja_common import MarkerError, Marker
# NB: this module is named with a trailing underscore to work around a PyCharm/pydevd bug:
# https://youtrack.jetbrains.com/issue/PY-70408/Debugger-skips-all-breakpoints-in-a-single-test-file
def get_dunder_methods_to_intercept() -> list[str]:
"""
Return a list of dunder methods on Jinja's StrictUndefined that should be overridden by Ansible's Marker.
When new methods are added in future Python/Jinja versions, they need to be added to Marker or the ignore_names list below.
"""
dunder_names = set(name for name in dir(jinja2.StrictUndefined) if name.startswith('__') and name.endswith('__'))
strict_undefined_intercepted_method_names = set(
name for name in dir(jinja2.StrictUndefined)
if getattr(jinja2.StrictUndefined, name) is jinja2.StrictUndefined._fail_with_undefined_error and name != '_fail_with_undefined_error'
)
# Some attributes/methods are necessary for core Python interactions and must not be intercepted, thus they are excluded from this test.
ignore_names = {
'__class__',
'__dir__',
'__doc__',
'__firstlineno__',
'__getattr__', # tested separately since it is intercepted with a custom method
'__getattribute__',
'__getitem__', # tested separately since it is intercepted with a custom method
'__getstate__',
'__init__',
'__init_subclass__',
'__module__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__setattr__', # tested separately since it is intercepted with a custom method
'__slots__', # tested separately since it's not a method
'__sizeof__',
'__static_attributes__',
'__subclasshook__',
}
# Some methods not intercepted by Jinja's StrictUndefined should be intercepted by Marker.
additional_method_names = {
'__aiter__',
'__delattr__',
'__format__',
'__repr__',
'__setitem__',
}
assert not strict_undefined_intercepted_method_names - dunder_names # ensure Jinja intercepted methods have not been overlooked
assert not ignore_names & additional_method_names # ensure no overap between ignore_names and additional_method_names
return sorted(dunder_names - ignore_names | additional_method_names)
def test_jinja_undefined_shape():
"""
Assert that the internal shape of Jinja's StrictUndefined matches Marker's expectations.
If this test fails, it likely means Jinja has changed something about the internals of Undefined in a way we'll need to address.
"""
assert jinja2.StrictUndefined in Marker.__bases__ # ensure we're directly inheriting the base we're validating
required_attrs = (
'_undefined_message',
'_undefined_name',
'_fail_with_undefined_error',
)
assert all(hasattr(jinja2.StrictUndefined, a) for a in required_attrs)
assert isinstance(jinja2.StrictUndefined.__slots__, tuple) # we don't actually care what's slotted, just that the attr exists (or has been patched back on)
@pytest.mark.parametrize("name", get_dunder_methods_to_intercept())
def test_marker_methods(marker: Marker, name: str) -> None:
"""Verify all expected dunder methods on Marker raise a MarkerError."""
method = getattr(marker, name)
with pytest.raises(MarkerError) as err:
method()
assert err.value.source is marker
def test_getattr_attribute_error(marker: Marker) -> None:
"""Verify unknown dunder attributes on Marker raise an AttributeError."""
with pytest.raises(AttributeError) as err:
getattr(marker, '__does_not_exist__')
assert err.value.obj is marker
assert err.value.name == '__does_not_exist__'
def test_getattr_propagation(marker: Marker) -> None:
"""Verify unknown non-dunder attributes on Marker return self."""
assert marker.does_not_exist is marker
def test_getitem_propagation(marker: Marker) -> None:
"""Verify items return self."""
assert marker['does_not_exist'] is marker
def test_setattr(marker: Marker) -> None:
"""Verify setattr raises MarkerError."""
with pytest.raises(MarkerError) as err:
marker.something = True
assert err.value.source is marker
def test_setitem(marker: Marker) -> None:
"""Verify setitem raises MarkerError."""
with pytest.raises(MarkerError) as err:
marker['something'] = True
assert err.value.source is marker
def test_slots(marker: Marker) -> None:
"""Verify all Marker instances are slotted."""
assert isinstance(marker.__slots__, tuple)
assert not hasattr(marker, '__dict__')
def marker_attrs(marker: Marker) -> list[str]:
"""Returns a list of internal attributes for a Marker type (using known prefixes)."""
return [name for name in dir(marker) if name.startswith('_undefined_') or name.startswith('_marker_')]
def test_copy(marker: Marker) -> None:
"""Verify copying an Marker works."""
copied = copy.copy(marker)
assert copied is not marker
for attribute_name in marker_attrs(marker):
assert getattr(copied, attribute_name) is getattr(marker, attribute_name), attribute_name
def test_deepcopy(marker: Marker) -> None:
"""Verify deep copying an Marker works."""
copied = copy.deepcopy(marker)
assert copied is not marker
for attribute_name in marker_attrs(marker):
copied_value = getattr(copied, attribute_name)
marker_value = getattr(marker, attribute_name)
if attribute_name == '_undefined_exception':
# The `_undefined_exception` attribute is a type, so the identity remains unchanged.
assert copied_value is marker_value, attribute_name
elif attribute_name == '_undefined_obj' and marker_value is jinja2.utils.missing:
# The `_undefined_obj` attribute defaults to the `jinja2.utils.missing`, a singleton of `jinja2.utils.MissingType`.
assert copied_value is marker_value, attribute_name
elif type(marker_value) in (str, type(None)):
# Values of type `str` or `None` should be equal.
assert copied_value == marker_value, attribute_name
else:
# All other types should be actual copies.
assert copied_value is not marker_value, attribute_name
def test_pickle(marker: Marker) -> None:
"""Verify pickling a Marker works."""
pickled = pickle.loads(pickle.dumps(marker))
assert pickled is not marker
for attribute_name in marker_attrs(marker):
pickled_value = getattr(pickled, attribute_name)
marker_value = getattr(marker, attribute_name)
if attribute_name == '_marker_captured_exception':
# The `_marker_captured_exception` attribute is an `Exception` instance, which doesn't support equality comparisons, compare as a `str` instead.
pickled_value = str(pickled_value)
marker_value = str(marker_value)
assert pickled_value == marker_value, attribute_name