mirror of https://github.com/ansible/ansible.git
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.
373 lines
15 KiB
Python
373 lines
15 KiB
Python
from __future__ import annotations
|
|
|
|
import io
|
|
import pathlib
|
|
|
|
import typing as t
|
|
|
|
import pytest
|
|
|
|
from contextlib import nullcontext
|
|
|
|
from ansible import errors as _errors
|
|
from ansible import template as _template
|
|
from ansible._internal._templating import _engine, _jinja_bits
|
|
from ansible._internal._templating._jinja_common import UndefinedMarker
|
|
from ansible.errors import AnsibleUndefinedVariable
|
|
from ansible.module_utils._internal._datatag import NotTaggableError
|
|
from ansible.template import Templar, trust_as_template, is_trusted_as_template
|
|
from ansible._internal._datatag._tags import TrustedAsTemplate
|
|
from ansible._internal._templating import _lazy_containers
|
|
|
|
from ..test_utils.controller.display import emits_warnings
|
|
|
|
TRUST = TrustedAsTemplate()
|
|
|
|
|
|
def test_templar_do_template_trusted_template_str() -> None:
|
|
"""Verify `Templar.do_template` processes a trusted template and emits a deprecation warning."""
|
|
data = TRUST.tag('{{ 1 }}')
|
|
|
|
with emits_warnings(deprecation_pattern='do_template.* is deprecated'):
|
|
result = Templar().do_template(data)
|
|
|
|
assert result == 1
|
|
|
|
|
|
def test_templar_do_template_non_str() -> None:
|
|
"""Verify `Templar.do_template` returns non-string inputs as-is and emits a deprecation warning."""
|
|
trusted_template = TRUST.tag('{{ 1 }}')
|
|
data = dict(value=trusted_template)
|
|
|
|
with emits_warnings(deprecation_pattern='do_template.* is deprecated'):
|
|
result = Templar().do_template(data)
|
|
|
|
assert result is data
|
|
assert result == dict(value=trusted_template)
|
|
assert result['value'] is trusted_template
|
|
|
|
|
|
@pytest.mark.parametrize("value, result", (
|
|
(TRUST.tag('{{ 1 )}'), True),
|
|
('{{ 1 }}', False),
|
|
(TRUST.tag('{{ invalid'), True),
|
|
(dict(value=TRUST.tag('{{ invalid')), True),
|
|
))
|
|
def test_is_template(value: t.Any, result: bool) -> None:
|
|
"""Verify `Templar.is_template` works as expected."""
|
|
assert Templar().is_template(value) is result
|
|
|
|
|
|
@pytest.mark.parametrize("value, overrides, result", (
|
|
('foo {{ 123 }} bar', {}, True),
|
|
('}}', {}, False),
|
|
('<< blah >>', dict(variable_start_string='<<', variable_end_string='>>'), True),
|
|
))
|
|
def test_is_possibly_template(value: t.Any, overrides: dict[str, t.Any], result: bool) -> None:
|
|
templar = Templar()
|
|
assert templar.is_possibly_template(value, overrides) is result
|
|
|
|
|
|
def test_is_possibly_template_override_merge() -> None:
|
|
"""Verify override merge in `Templar.is_possibly_template` works as expected."""
|
|
templar = Templar()
|
|
|
|
with templar.set_temporary_context(variable_start_string='<<'):
|
|
assert templar.is_possibly_template('{{ nope }}') is False # temporary global override
|
|
assert templar.is_possibly_template('<< yep >>') is True # temporary global override
|
|
assert templar.is_possibly_template('<< nope >>', overrides=dict(variable_start_string='!!')) is False # local override masks global
|
|
assert templar.is_possibly_template('<< !!yep >>', overrides=dict(variable_start_string='!!')) is True # local override masks global
|
|
|
|
|
|
def test_templar_template_non_template_str() -> None:
|
|
"""Verify `Templar.template` returns non-template strings as-is."""
|
|
data = TRUST.tag('hello')
|
|
result = Templar().template(data)
|
|
|
|
assert result is data
|
|
|
|
|
|
def test_templar_template_untrusted_template() -> None:
|
|
"""
|
|
Verify `Templar.template` on an untrusted template triggers an exception.
|
|
The exception is due to unit tests setting the default trust behavior to error on untrusted templates, the default is to warn instead.
|
|
"""
|
|
templar = Templar()
|
|
data = '{{ 1 }}'
|
|
|
|
with pytest.raises(_errors.TemplateTrustCheckFailedError):
|
|
templar.template(data)
|
|
|
|
|
|
def test_templar_template_fail_on_undefined_truthy_falsey() -> None:
|
|
"""Verify `fail_on_undefined` compat behaviors behave as expected."""
|
|
template = TRUST.tag('{{ bogusvar }}')
|
|
|
|
with emits_warnings(deprecation_pattern='Falling back to `True` for `fail_on_undefined'), pytest.raises(_errors.AnsibleUndefinedVariable):
|
|
# fail_on_undefined None == True + dep warning
|
|
Templar().template(template, fail_on_undefined=None) # type: ignore
|
|
|
|
assert Templar().template(template, fail_on_undefined=False) is template
|
|
|
|
with pytest.raises(_errors.AnsibleUndefinedVariable):
|
|
Templar().template(template, fail_on_undefined=1) # type: ignore
|
|
|
|
assert Templar().template(template, fail_on_undefined=0) is template # type: ignore
|
|
|
|
|
|
@pytest.mark.parametrize("template, fail_on_undefined, result", (
|
|
("somevar", True, "somevar value"), # success, starts with identifier that's a valid var
|
|
("123 | int", True, 123), # success, has filter
|
|
("bogusvar.somevar", True, "bogusvar.somevar"), # fail silently, starts with identifier that is not a var, so no-op
|
|
("somevar.bogusvar", True, _errors.AnsibleUndefinedVariable), # fail with exception, starts with valid var, but the overall expression results in undefined
|
|
("somevar.bogusvar", False, "somevar.bogusvar"), # fail silently, starts with valid var, but the overall expression results in undefined
|
|
("1notavar", True, "1notavar"), # fail silently, does not start with a valid identifier
|
|
("somevar | notafilter", True, _errors.AnsibleTemplateError), # fail with exception, has a filter-looking expression that is invalid
|
|
))
|
|
def test_templar_template_convert_bare(template: str, fail_on_undefined: bool, result: t.Any) -> None:
|
|
"""Verify the `convert_bare` selection heuristics behave properly."""
|
|
with emits_warnings(deprecation_pattern='convert_bare.* is deprecated'):
|
|
with pytest.raises(result) if isinstance(result, type) and issubclass(result, Exception) else nullcontext():
|
|
assert Templar(
|
|
variables=dict(somevar='somevar value'),
|
|
).template(TRUST.tag(template), convert_bare=True, fail_on_undefined=fail_on_undefined) == result
|
|
|
|
|
|
def test_templar_template_convert_bare_truthy_falsey() -> None:
|
|
templar = Templar(variables=dict(somevar=1))
|
|
template = TRUST.tag('somevar')
|
|
|
|
assert templar.template(template, convert_bare=1) == 1 # type: ignore
|
|
assert templar.template(template, convert_bare=0) == 'somevar' # type: ignore
|
|
|
|
|
|
def test_templar_template_convert_data() -> None:
|
|
with emits_warnings(deprecation_pattern='convert_data.* is deprecated'):
|
|
assert Templar().template(TRUST.tag("{{123}}"), convert_data=True) == 123
|
|
|
|
|
|
def test_templar_template_disable_lookups() -> None:
|
|
with emits_warnings(deprecation_pattern='disable_lookups.* is deprecated'):
|
|
assert Templar().template(TRUST.tag("{{lookup('list', [1,2])}}"), disable_lookups=True) == [1, 2]
|
|
|
|
|
|
def test_resolve_variable_expression() -> None:
|
|
assert Templar().resolve_variable_expression('a_local', local_variables=dict(a_local=1)) == 1
|
|
|
|
|
|
def test_evaluate_expression() -> None:
|
|
assert Templar().evaluate_expression(TRUST.tag('a_local'), local_variables=dict(a_local=1)) == 1
|
|
|
|
|
|
def test_evaluate_conditional() -> None:
|
|
assert Templar().evaluate_conditional(True) is True
|
|
|
|
|
|
def test_from_template_engine() -> None:
|
|
engine = _engine.TemplateEngine()
|
|
templar = Templar._from_template_engine(engine)
|
|
|
|
assert templar._engine is not engine
|
|
assert isinstance(templar._engine, _engine.TemplateEngine)
|
|
assert templar._overrides is _engine.TemplateOverrides.DEFAULT
|
|
|
|
|
|
def test_basedir() -> None:
|
|
templar = Templar()
|
|
|
|
assert templar.basedir == templar._engine.basedir
|
|
|
|
|
|
def test_environment() -> None:
|
|
templar = Templar()
|
|
|
|
with emits_warnings(deprecation_pattern='environment.* is deprecated'):
|
|
assert templar.environment is templar._engine.environment
|
|
|
|
|
|
def test_available_variables() -> None:
|
|
variables: _template._VariableContainer = dict()
|
|
templar = Templar(variables=variables)
|
|
|
|
assert variables is templar.available_variables
|
|
assert templar.available_variables is templar._engine.available_variables
|
|
|
|
with emits_warnings(deprecation_pattern='_available_variables.* internal attribute is deprecated'):
|
|
assert variables is templar._available_variables
|
|
|
|
variables = dict(a=1)
|
|
templar.available_variables = variables
|
|
|
|
assert templar.available_variables is variables
|
|
assert templar._available_variables is variables
|
|
assert templar._engine.available_variables is variables
|
|
|
|
|
|
def test_loader() -> None:
|
|
templar = Templar()
|
|
|
|
with emits_warnings(deprecation_pattern='_loader.* is deprecated'):
|
|
assert templar._loader is templar._engine._loader
|
|
|
|
|
|
def test_copy_with_new_env_environment_class() -> None:
|
|
with emits_warnings(deprecation_pattern='environment_class.* is ignored'):
|
|
Templar().copy_with_new_env(environment_class=_jinja_bits.AnsibleEnvironment)
|
|
|
|
|
|
def test_copy_with_new_env_overrides() -> None:
|
|
with emits_warnings(deprecation_pattern='overrides.*copy_with_new_env.* is deprecated'):
|
|
assert Templar().copy_with_new_env(variable_start_string='!!').template(TRUST.tag('!! 1 }}')) == 1
|
|
|
|
|
|
def test_copy_with_new_env_invalid_overrides() -> None:
|
|
with emits_warnings(deprecation_pattern='overrides.* is deprecated'):
|
|
with pytest.raises(TypeError, match='variable_start_string must be'):
|
|
Templar().copy_with_new_env(variable_start_string=1)
|
|
|
|
|
|
def test_copy_with_new_env_available_variables() -> None:
|
|
templar = Templar()
|
|
new_variables: _template._VariableContainer = {}
|
|
|
|
assert templar.available_variables == {} # trigger lazy creation of available_variables
|
|
assert templar.copy_with_new_env().available_variables is templar.available_variables
|
|
assert templar.copy_with_new_env(available_variables={}).available_variables is not templar.available_variables
|
|
assert templar.copy_with_new_env(available_variables=new_variables).available_variables is new_variables
|
|
|
|
|
|
def test_copy_with_new_searchpath() -> None:
|
|
assert Templar().copy_with_new_env(searchpath='hello')._engine.environment.loader.searchpath == 'hello'
|
|
|
|
|
|
def test_set_temporary_context_overrides() -> None:
|
|
templar = Templar()
|
|
|
|
with emits_warnings(deprecation_pattern='set_temporary_context.* is deprecated'):
|
|
with templar.set_temporary_context(variable_start_string='!!'):
|
|
assert templar.template(TRUST.tag('!! 1 }}')) == 1
|
|
|
|
|
|
def test_set_temporary_context_searchpath() -> None:
|
|
templar = Templar()
|
|
|
|
with templar.set_temporary_context(searchpath='hello'):
|
|
assert templar._engine.environment.loader.searchpath == 'hello'
|
|
|
|
|
|
def test_set_temporary_context_available_variables() -> None:
|
|
templar = Templar()
|
|
available_variables = templar.available_variables
|
|
new_variables: _template._VariableContainer = {}
|
|
|
|
assert templar.available_variables == {}
|
|
|
|
with templar.set_temporary_context():
|
|
assert templar.available_variables is available_variables
|
|
|
|
with templar.set_temporary_context(available_variables={}):
|
|
assert templar.available_variables is not available_variables
|
|
|
|
with templar.set_temporary_context(available_variables=new_variables):
|
|
assert templar.available_variables is new_variables
|
|
|
|
|
|
class CustomStr(str): ...
|
|
|
|
|
|
@pytest.mark.parametrize("value", (
|
|
"yep",
|
|
lambda tmppath: pathlib.Path(tmppath / "afile").open("w"),
|
|
io.StringIO("blee"),
|
|
io.BytesIO(b"blee"),
|
|
))
|
|
def test_trust_as_template(value: str | io.IOBase, tmp_path: pytest.TempPathFactory) -> None:
|
|
"""Validate expected success behavior for `trust_value`."""
|
|
if callable(value):
|
|
value = value(tmp_path)
|
|
|
|
result = trust_as_template(value)
|
|
|
|
assert result is not value
|
|
assert TrustedAsTemplate.is_tagged_on(result)
|
|
assert isinstance(result, type(value))
|
|
assert is_trusted_as_template(result)
|
|
|
|
|
|
@pytest.mark.parametrize("value", (
|
|
None,
|
|
123,
|
|
dict(x=TrustedAsTemplate().tag("nope")),
|
|
[123],
|
|
))
|
|
def test_not_is_trusted_as_template(value: object) -> None:
|
|
"""Validate that types incorrectly tagged with trust are not reported as trusted."""
|
|
result = TrustedAsTemplate().tag(value) # force application of trust
|
|
|
|
assert not is_trusted_as_template(value)
|
|
assert not is_trusted_as_template(result)
|
|
|
|
|
|
@pytest.mark.parametrize("value, error_type, error_pattern", (
|
|
(123, TypeError, "cannot be applied to"),
|
|
(CustomStr("hey"), NotTaggableError, "is not taggable"),
|
|
))
|
|
def test_trust_as_template_errors(value: object, error_type: t.Type[Exception], error_pattern: str | None) -> None:
|
|
"""Validate expected error behavior for `trust_value`."""
|
|
with pytest.raises(error_type, match=error_pattern):
|
|
trust_as_template(value) # type: ignore
|
|
|
|
|
|
def test_templar_finalize_lazy() -> None:
|
|
"""Ensure that containers returned via the `Templar.template` public API shim are always finalized, even under a TemplateContext."""
|
|
variables = dict(
|
|
indirect=TRUST.tag('{{ to_lazy_dict }}'),
|
|
to_lazy_dict=dict(t1=TRUST.tag('{{ "t1value" }}'), t2=TRUST.tag('{{ "t2value" }}'), scalar=42),
|
|
)
|
|
|
|
templar = _template.Templar(variables=variables)
|
|
|
|
with _engine.TemplateContext(template_value="bogus", templar=templar._engine, options=_engine.TemplateOptions.DEFAULT):
|
|
template = TRUST.tag('{{ indirect }}')
|
|
|
|
# self-test to ensure that this test would fail against the engine by default
|
|
assert type(templar._engine.template(template)) is _lazy_containers._AnsibleLazyTemplateDict # pylint: disable=unidiomatic-typecheck
|
|
|
|
result = templar.template(template)
|
|
|
|
assert type(result) is dict # pylint: disable=unidiomatic-typecheck
|
|
assert result['t1'] == 't1value'
|
|
assert result['t2'] == 't2value'
|
|
assert result['scalar'] == 42
|
|
|
|
|
|
def test_templar_finalize_undefined() -> None:
|
|
"""Ensure that undefined values returned via the `Templar.template` public API are always finalized, even under a TemplateContext."""
|
|
templar = _template.Templar()
|
|
|
|
with _engine.TemplateContext(template_value="bogus", templar=templar._engine, options=_engine.TemplateOptions.DEFAULT):
|
|
undef_template = TRUST.tag('{{ with_undefined }}')
|
|
|
|
# self-test to ensure that this test would fail against the engine by default
|
|
assert isinstance(templar._engine.template(undef_template), UndefinedMarker)
|
|
|
|
with pytest.raises(AnsibleUndefinedVariable):
|
|
templar.template(undef_template)
|
|
|
|
|
|
def test_set_temporary_context_with_none() -> None:
|
|
"""Verify that `set_temporary_context` ignores `None` overrides."""
|
|
templar = _template.Templar()
|
|
|
|
with templar.set_temporary_context(variable_start_string=None):
|
|
assert templar.template(trust_as_template('{{ True }}')) is True
|
|
|
|
|
|
def test_copy_with_new_env_with_none() -> None:
|
|
"""Verify that `copy_with_new_env` ignores `None` overrides."""
|
|
templar = _template.Templar()
|
|
|
|
copied = templar.copy_with_new_env(variable_start_string=None)
|
|
|
|
assert copied.template(trust_as_template('{{ True }}')) is True
|