forked from HAL9000/cleveragents-core
648 lines
19 KiB
Python
648 lines
19 KiB
Python
"""
|
|
Unit tests for inline_jinja_handler module.
|
|
"""
|
|
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from cleveragents.templates.inline_jinja_handler import InlineJinjaHandler
|
|
|
|
|
|
class TestInlineJinjaHandler:
|
|
"""Test cases for InlineJinjaHandler class."""
|
|
|
|
def test_init(self):
|
|
"""Test InlineJinjaHandler initialization."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
assert handler.env is not None
|
|
assert handler.env.block_start_string == "{%"
|
|
assert handler.env.block_end_string == "%}"
|
|
assert handler.env.variable_start_string == "{{"
|
|
assert handler.env.variable_end_string == "}}"
|
|
|
|
def test_process_yaml_file_without_templates(self):
|
|
"""Test processing YAML file without templates."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: test
|
|
value: 123
|
|
items:
|
|
- one
|
|
- two
|
|
"""
|
|
with tempfile.NamedTemporaryFile(mode="w", encoding='utf-8', prefix='cleveragent_', suffix=".yaml", delete=False) as f:
|
|
f.write(yaml_content)
|
|
temp_path = Path(f.name)
|
|
|
|
try:
|
|
result = handler.process_yaml_file(temp_path)
|
|
|
|
assert result["name"] == "test"
|
|
assert result["value"] == 123
|
|
assert len(result["items"]) == 2
|
|
finally:
|
|
temp_path.unlink()
|
|
|
|
def test_process_yaml_string_without_templates(self):
|
|
"""Test processing YAML string without templates."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: test
|
|
value: 123
|
|
"""
|
|
result = handler.process_yaml_string(yaml_content)
|
|
|
|
assert result["name"] == "test"
|
|
assert result["value"] == 123
|
|
|
|
def test_process_yaml_string_with_inline_template_defer(self):
|
|
"""Test processing YAML with inline template (deferred)."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: test
|
|
greeting: {{ message }}
|
|
"""
|
|
result = handler.process_yaml_string(yaml_content, defer_rendering=True)
|
|
|
|
assert "__templates__" in result
|
|
# Check that template placeholder was created
|
|
greeting_value = result["greeting"]
|
|
assert greeting_value.startswith("__template_") and greeting_value.endswith("__")
|
|
|
|
def test_process_yaml_string_with_inline_template_render(self):
|
|
"""Test processing YAML with inline template (immediate rendering)."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: test
|
|
greeting: Hello {{ name }}!
|
|
"""
|
|
context = {"name": "World"}
|
|
result = handler.process_yaml_string(yaml_content, defer_rendering=False, context=context)
|
|
|
|
assert result["name"] == "test"
|
|
assert result["greeting"] == "Hello World!"
|
|
|
|
def test_process_yaml_string_with_for_loop_defer(self):
|
|
"""Test processing YAML with for loop (deferred)."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: test
|
|
items:
|
|
{% for i in range(3) %}
|
|
item_{{ i }}: value_{{ i }}
|
|
{% endfor %}
|
|
"""
|
|
result = handler.process_yaml_string(yaml_content, defer_rendering=True)
|
|
|
|
assert "__templates__" in result
|
|
assert "items" in result
|
|
|
|
def test_process_yaml_string_with_for_loop_render(self):
|
|
"""Test processing YAML with for loop (immediate rendering)."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: test
|
|
items:
|
|
{% for i in range(3) %}
|
|
- item{{ i }}
|
|
{% endfor %}
|
|
"""
|
|
result = handler.process_yaml_string(yaml_content, defer_rendering=False, context={})
|
|
|
|
assert result["name"] == "test"
|
|
assert len(result["items"]) == 3
|
|
|
|
def test_process_yaml_string_with_if_block_render(self):
|
|
"""Test processing YAML with if block (immediate rendering)."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: test
|
|
{% if enabled %}
|
|
feature: active
|
|
{% endif %}
|
|
"""
|
|
context = {"enabled": True}
|
|
result = handler.process_yaml_string(yaml_content, defer_rendering=False, context=context)
|
|
|
|
assert result["name"] == "test"
|
|
assert result["feature"] == "active"
|
|
|
|
def test_process_yaml_string_with_if_block_false_render(self):
|
|
"""Test processing YAML with if block when condition is false."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: test
|
|
{% if enabled %}
|
|
feature: active
|
|
{% endif %}
|
|
"""
|
|
context = {"enabled": False}
|
|
result = handler.process_yaml_string(yaml_content, defer_rendering=False, context=context)
|
|
|
|
assert result["name"] == "test"
|
|
assert "feature" not in result
|
|
|
|
def test_extract_templates_with_inline_variable(self):
|
|
"""Test template extraction with inline variable."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: test
|
|
message: {{ greeting }}
|
|
value: 123
|
|
"""
|
|
result = handler._extract_templates(yaml_content)
|
|
|
|
assert "__templates__" in result
|
|
assert result["name"] == "test"
|
|
assert result["value"] == 123
|
|
|
|
# Check template was extracted
|
|
templates = result["__templates__"]
|
|
assert any(t["type"] == "inline" for t in templates.values())
|
|
|
|
def test_extract_templates_with_block_template(self):
|
|
"""Test template extraction with block template."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: test
|
|
items:
|
|
{% for i in range(3) %}
|
|
item{{ i }}: value{{ i }}
|
|
{% endfor %}
|
|
"""
|
|
result = handler._extract_templates(yaml_content)
|
|
|
|
assert "__templates__" in result
|
|
templates = result["__templates__"]
|
|
assert any(t["type"] == "block" for t in templates.values())
|
|
|
|
def test_extract_templates_with_nested_structure(self):
|
|
"""Test template extraction with nested structure."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
root:
|
|
level1:
|
|
name: {{ name }}
|
|
value: {{ value }}
|
|
"""
|
|
result = handler._extract_templates(yaml_content)
|
|
|
|
assert result is not None
|
|
if "__templates__" in result:
|
|
assert len(result["__templates__"]) > 0
|
|
|
|
def test_render_templates_with_context(self):
|
|
"""Test rendering templates with context."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: {{ app_name }}
|
|
version: {{ version }}
|
|
"""
|
|
context = {"app_name": "MyApp", "version": "1.0"}
|
|
result = handler._render_templates(yaml_content, context)
|
|
|
|
assert result["name"] == "MyApp"
|
|
assert result["version"] == 1.0 # YAML parses "1.0" as float
|
|
|
|
def test_render_templates_with_builtin_functions(self):
|
|
"""Test rendering templates with built-in functions."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
count: {{ len(items) }}
|
|
first: {{ items[0] }}
|
|
"""
|
|
context = {"items": ["a", "b", "c"]}
|
|
result = handler._render_templates(yaml_content, context)
|
|
|
|
assert result["count"] == 3
|
|
assert result["first"] == "a"
|
|
|
|
def test_apply_templates_without_templates(self):
|
|
"""Test applying templates to config without templates."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
config = {"name": "test", "value": 123}
|
|
context = {"var": "value"}
|
|
|
|
result = handler.apply_templates(config, context)
|
|
|
|
assert result == config
|
|
|
|
def test_apply_templates_with_inline_template(self):
|
|
"""Test applying templates with inline template."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
# First extract templates
|
|
yaml_content = """
|
|
name: test
|
|
message: {{ greeting }}
|
|
"""
|
|
config = handler._extract_templates(yaml_content)
|
|
|
|
# Then apply templates
|
|
context = {"greeting": "Hello!"}
|
|
result = handler.apply_templates(config, context)
|
|
|
|
assert result["name"] == "test"
|
|
assert result["message"] == "Hello!"
|
|
|
|
def test_apply_templates_with_block_template(self):
|
|
"""Test applying templates with block template."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: test
|
|
items:
|
|
{% for i in range(2) %}
|
|
item{{ i }}: value{{ i }}
|
|
{% endfor %}
|
|
"""
|
|
config = handler._extract_templates(yaml_content)
|
|
|
|
context = {}
|
|
result = handler.apply_templates(config, context)
|
|
|
|
assert result["name"] == "test"
|
|
# Block templates should be rendered
|
|
assert "items" in result or "item0" in result
|
|
|
|
def test_apply_templates_recursive_dict(self):
|
|
"""Test recursive template application in nested dict."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
templates = {
|
|
"__template_abc123__": {
|
|
"type": "inline",
|
|
"key": "value",
|
|
"content": "{{ var }}",
|
|
"indent": 0,
|
|
}
|
|
}
|
|
|
|
data = {
|
|
"outer": {
|
|
"inner": {"value": "__template_abc123__"},
|
|
}
|
|
}
|
|
|
|
context = {"var": "rendered_value"}
|
|
result = handler._apply_templates_recursive(data, templates, context)
|
|
|
|
assert result["outer"]["inner"]["value"] == "rendered_value"
|
|
|
|
def test_apply_templates_recursive_list(self):
|
|
"""Test recursive template application in list."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
templates = {
|
|
"__template_abc123__": {
|
|
"type": "inline",
|
|
"key": "item",
|
|
"content": "{{ var }}",
|
|
"indent": 0,
|
|
}
|
|
}
|
|
|
|
data = ["__template_abc123__", "regular_value"]
|
|
|
|
context = {"var": "rendered"}
|
|
result = handler._apply_templates_recursive(data, templates, context)
|
|
|
|
# Should process the list
|
|
assert isinstance(result, list)
|
|
|
|
def test_render_template_string(self):
|
|
"""Test rendering a single template string."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
template_str = "Hello {{ name }}!"
|
|
context = {"name": "World"}
|
|
|
|
result = handler._render_template_string(template_str, context)
|
|
|
|
assert result == "Hello World!"
|
|
|
|
def test_render_template_string_with_utilities(self):
|
|
"""Test rendering template string with utility functions."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
template_str = "Count: {{ len(items) }}"
|
|
context = {"items": [1, 2, 3]}
|
|
|
|
result = handler._render_template_string(template_str, context)
|
|
|
|
assert result == "Count: 3"
|
|
|
|
def test_parse_rendered_value_string(self):
|
|
"""Test parsing rendered value as string."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
result = handler._parse_rendered_value("simple string")
|
|
|
|
assert result == "simple string"
|
|
|
|
def test_parse_rendered_value_number(self):
|
|
"""Test parsing rendered value as number."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
result = handler._parse_rendered_value("123")
|
|
|
|
assert result == 123
|
|
|
|
def test_parse_rendered_value_list(self):
|
|
"""Test parsing rendered value as list."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
result = handler._parse_rendered_value("[1, 2, 3]")
|
|
|
|
assert result == [1, 2, 3]
|
|
|
|
def test_parse_rendered_value_dict(self):
|
|
"""Test parsing rendered value as dict."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
result = handler._parse_rendered_value("{key: value}")
|
|
|
|
assert isinstance(result, dict)
|
|
assert result["key"] == "value"
|
|
|
|
def test_parse_rendered_value_boolean(self):
|
|
"""Test parsing rendered value as boolean."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
result_true = handler._parse_rendered_value("true")
|
|
result_false = handler._parse_rendered_value("false")
|
|
|
|
assert result_true is True
|
|
assert result_false is False
|
|
|
|
def test_extract_templates_with_multiple_inline_templates(self):
|
|
"""Test extracting multiple inline templates."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: {{ name }}
|
|
age: {{ age }}
|
|
city: {{ city }}
|
|
"""
|
|
result = handler._extract_templates(yaml_content)
|
|
|
|
assert "__templates__" in result
|
|
assert len(result["__templates__"]) == 3
|
|
|
|
def test_extract_templates_with_macro(self):
|
|
"""Test extracting templates with macro block."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: test
|
|
template:
|
|
{% macro greeting(name) %}
|
|
Hello {{ name }}
|
|
{% endmacro %}
|
|
"""
|
|
result = handler._extract_templates(yaml_content)
|
|
|
|
assert result is not None
|
|
|
|
def test_extract_templates_with_endblock(self):
|
|
"""Test extracting templates with block/endblock."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: test
|
|
content:
|
|
{% block content %}
|
|
This is content
|
|
{% endblock %}
|
|
"""
|
|
result = handler._extract_templates(yaml_content)
|
|
|
|
assert result is not None
|
|
|
|
def test_extract_templates_invalid_yaml(self):
|
|
"""Test handling of invalid YAML after extraction."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: test
|
|
invalid: [unclosed bracket
|
|
"""
|
|
with pytest.raises(yaml.YAMLError):
|
|
handler._extract_templates(yaml_content)
|
|
|
|
def test_process_yaml_file_defer_and_apply(self):
|
|
"""Test full workflow: defer and then apply templates."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
app_name: {{ name }}
|
|
version: {{ version }}
|
|
"""
|
|
# First pass: defer rendering
|
|
config = handler.process_yaml_string(yaml_content, defer_rendering=True)
|
|
|
|
# Second pass: apply templates
|
|
context = {"name": "MyApp", "version": "2.0"}
|
|
result = handler.apply_templates(config, context)
|
|
|
|
assert result["app_name"] == "MyApp"
|
|
assert result["version"] == 2.0 # YAML parses "2.0" as float
|
|
|
|
def test_extract_templates_preserves_indent_info(self):
|
|
"""Test that template extraction preserves indent information."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
root:
|
|
nested:
|
|
value: {{ var }}
|
|
"""
|
|
result = handler._extract_templates(yaml_content)
|
|
|
|
if "__templates__" in result:
|
|
for template_info in result["__templates__"].values():
|
|
assert "indent" in template_info
|
|
|
|
def test_apply_templates_with_failed_yaml_parse(self):
|
|
"""Test template application when rendered content fails to parse as YAML."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
templates = {
|
|
"__template_abc123__": {
|
|
"type": "block",
|
|
"key": "items",
|
|
"content": "{% for i in range(1) %}\ninvalid: [bracket\n{% endfor %}",
|
|
"indent": 0,
|
|
}
|
|
}
|
|
|
|
data = {"items": "__template_abc123__"}
|
|
context = {}
|
|
|
|
# Should handle the exception gracefully
|
|
result = handler._apply_templates_recursive(data, templates, context)
|
|
|
|
# Result should still be a dict
|
|
assert isinstance(result, dict)
|
|
|
|
def test_render_templates_with_complex_expressions(self):
|
|
"""Test rendering templates with complex Jinja expressions."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
result: {{ (value * 2) + 10 }}
|
|
"""
|
|
context = {"value": 5}
|
|
result = handler._render_templates(yaml_content, context)
|
|
|
|
assert result["result"] == 20
|
|
|
|
def test_extract_templates_with_nested_for_loops(self):
|
|
"""Test extracting nested for loops."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: test
|
|
matrix:
|
|
{% for i in range(2) %}
|
|
row{{ i }}:
|
|
{% for j in range(2) %}
|
|
col{{ j }}: {{ i }}_{{ j }}
|
|
{% endfor %}
|
|
{% endfor %}
|
|
"""
|
|
result = handler._extract_templates(yaml_content)
|
|
|
|
assert result is not None
|
|
assert "name" in result
|
|
|
|
def test_process_yaml_string_no_context_for_immediate_render(self):
|
|
"""Test immediate rendering without providing context."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: test
|
|
value: {{ unknown_var | default('default_value') }}
|
|
"""
|
|
result = handler.process_yaml_string(yaml_content, defer_rendering=False, context=None)
|
|
|
|
# Should use empty context
|
|
assert result["name"] == "test"
|
|
|
|
def test_apply_templates_removes_templates_key(self):
|
|
"""Test that apply_templates removes __templates__ key from result."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
config = {
|
|
"name": "test",
|
|
"__templates__": {"template_id": {"type": "inline", "content": "value"}},
|
|
}
|
|
context = {}
|
|
|
|
result = handler.apply_templates(config, context)
|
|
|
|
assert "__templates__" not in result
|
|
assert "name" in result
|
|
|
|
def test_process_yaml_string_non_dict_result(self):
|
|
"""Test processing YAML that parses to non-dict (e.g., list) with templates."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
# YAML content that produces a list with templates
|
|
yaml_content = """
|
|
- name: item1
|
|
value: {{ value1 }}
|
|
- name: item2
|
|
value: {{ value2 }}
|
|
"""
|
|
result = handler.process_yaml_string(yaml_content, defer_rendering=True)
|
|
|
|
# When parsed result is not a dict, it should be wrapped
|
|
assert isinstance(result, dict)
|
|
assert "__templates__" in result or "_content" in result
|
|
|
|
def test_parse_rendered_value_with_yaml_error(self):
|
|
"""Test _parse_rendered_value when YAML parsing fails."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
# Create a string that looks like YAML but will cause an error
|
|
# Using mock to force an exception
|
|
with patch('yaml.safe_load', side_effect=Exception("YAML error")):
|
|
result = handler._parse_rendered_value("some value")
|
|
# Should return the original string when YAML parsing fails
|
|
assert result == "some value"
|
|
|
|
def test_parse_rendered_value_plain_string(self):
|
|
"""Test _parse_rendered_value with a plain string that can't be parsed as YAML."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
# This string will fail YAML parsing and should be returned as-is
|
|
result = handler._parse_rendered_value("just a plain string")
|
|
|
|
assert isinstance(result, str)
|
|
|
|
def test_process_yaml_string_with_multiline_template(self):
|
|
"""Test processing YAML with multiline Jinja2 template."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
config:
|
|
{% if condition %}
|
|
enabled: true
|
|
{% else %}
|
|
enabled: false
|
|
{% endif %}
|
|
"""
|
|
context = {"condition": True}
|
|
result = handler.process_yaml_string(yaml_content, defer_rendering=False, context=context)
|
|
|
|
assert "config" in result
|
|
assert result["config"]["enabled"] is True
|
|
|
|
def test_process_yaml_string_empty_content(self):
|
|
"""Test processing empty or nearly empty YAML."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
# Just a comment
|
|
"""
|
|
# Comment-only YAML results in None, which raises ValueError
|
|
# because inline_jinja_handler expects dict
|
|
with pytest.raises(ValueError, match="Expected YAML to parse as dict, got NoneType"):
|
|
handler.process_yaml_string(yaml_content, defer_rendering=True)
|
|
|
|
def test_extract_templates_with_yaml_comment(self):
|
|
"""Test extracting templates with YAML comments."""
|
|
handler = InlineJinjaHandler()
|
|
|
|
yaml_content = """
|
|
name: test
|
|
# This is a YAML comment with {{ variable }}
|
|
value: {{ actual_template }}
|
|
description: test string
|
|
"""
|
|
result = handler._extract_templates(yaml_content)
|
|
|
|
assert result is not None
|
|
assert "name" in result
|
|
|