From b917e8fdffd145cde58a8959fc068f46abefcc7b Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman Date: Sun, 6 Jul 2025 18:15:16 +0000 Subject: [PATCH] test: add tests for TemplateRenderer error handling and edge cases --- .../steps/template_renderer_errors_steps.py | 162 ++++++++++++++++++ .../features/template_renderer_errors.feature | 108 ++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 tests/features/steps/template_renderer_errors_steps.py create mode 100644 tests/features/template_renderer_errors.feature diff --git a/tests/features/steps/template_renderer_errors_steps.py b/tests/features/steps/template_renderer_errors_steps.py new file mode 100644 index 000000000..82dff505f --- /dev/null +++ b/tests/features/steps/template_renderer_errors_steps.py @@ -0,0 +1,162 @@ +import json +import sys +from unittest.mock import patch + +from behave import given +from behave import then +from behave import when + +from cleveragents.core.exceptions import TemplateError +from cleveragents.templates.renderer import TemplateEngine +from cleveragents.templates.renderer import TemplateRenderer + + +@given('I try to initialize a TemplateRenderer with the "{engine}" engine') +def step_impl(context, engine): + context.engine_type_str = engine + if engine == "jinja2": + context.engine_type_enum = TemplateEngine.JINJA2 + elif engine == "mustache": + context.engine_type_enum = TemplateEngine.MUSTACHE + + +@when('the required package "{package}" is not installed') +def step_impl(context, package): + with patch.dict(sys.modules, {package: None}): + try: + TemplateRenderer(context.engine_type_enum) + except TemplateError as e: + context.error = e + + +@then('a TemplateError should be raised with a message containing "{message}"') +def step_impl(context, message): + assert hasattr(context, "error"), "TemplateError was not raised" + assert isinstance( + context.error, TemplateError + ), f"Expected TemplateError, but got {type(context.error)}" + assert message in str(context.error), f"'{message}' not in '{str(context.error)}'" + + +@given("I have an unsupported template engine type") +def step_impl(context): + context.unsupported_engine = "unsupported_engine_type" + + +@when("I try to initialize a TemplateRenderer with this engine type") +def step_impl(context): + try: + # We need to bypass the Enum type check for this test + renderer = TemplateRenderer() + renderer.engine_type = context.unsupported_engine + renderer._initialize_engine() + except TemplateError as e: + context.error = e + + +@given('a TemplateRenderer with the "{engine}" engine') +def step_impl(context, engine): + # This step assumes packages are installed for successful initialization + context.renderer = TemplateRenderer(TemplateEngine(engine)) + + +@when("I attempt to register a template with an empty name") +def step_impl(context): + try: + context.renderer.register_template("", "some content") + except TemplateError as e: + context.error = e + + +@when('I try to register a template with invalid Jinja2 syntax "{syntax}"') +def step_impl(context, syntax): + try: + context.renderer.register_template("invalid", syntax) + except TemplateError as e: + context.error = e + + +@given('a template named "{name}" with content "{content}"') +def step_impl(context, name, content): + context.renderer.register_template(name, content) + + +@when('I try to render the "{template_name}" template with an empty context') +def step_impl(context, template_name): + try: + context.renderer.render(template_name, {}) + except TemplateError as e: + context.error = e + + +@when('I try to render the string "{template_str}" with an empty context') +def step_impl(context, template_str): + try: + context.renderer.render_string(template_str, {}) + except TemplateError as e: + context.error = e + + +@when("I render the \"{template_name}\" template with context '{json_context}'") +def step_impl(context, template_name, json_context): + ctx = json.loads(json_context) + context.result = context.renderer.render(template_name, ctx) + + +@then('the result should be "{expected_result}"') +def step_impl(context, expected_result): + assert ( + context.result == expected_result + ), f"Expected '{expected_result}', got '{context.result}'" + + +@when('I try to render a template named "{template_name}" with any context') +@when('I try to render the "{template_name}" template with any context') +def step_impl(context, template_name): + try: + context.renderer.render(template_name, {"foo": "bar"}) + except TemplateError as e: + context.error = e + + +@when('I try to render the string "{template_str}" with any context') +def step_impl(context, template_str): + try: + context.renderer.render_string(template_str, {"foo": "bar"}) + except TemplateError as e: + context.error = e + + +@when('I try to get a template named "{template_name}"') +def step_impl(context, template_name): + try: + context.renderer.get_template(template_name) + except TemplateError as e: + context.error = e + + +@given("I have manually modified the engine to be invalid") +def step_impl(context): + # Replace engine with an object that doesn't have the required methods + context.renderer.engine = object() + + +@when('I try to register a template named "{name}" with content "{content}"') +def step_impl(context, name, content): + try: + context.renderer.register_template(name, content) + except TemplateError as e: + context.error = e + + +@given( + 'I have manually registered a template named "{template_name}" that is not a valid Jinja2 template object' +) +def step_impl(context, template_name): + # Manually insert a non-template object into the templates dict + context.renderer.templates[template_name] = "just a string" + + +@given("I have manually set the engine type to an unsupported value") +def step_impl(context): + context.renderer.engine_type = "unsupported" diff --git a/tests/features/template_renderer_errors.feature b/tests/features/template_renderer_errors.feature new file mode 100644 index 000000000..2be5196af --- /dev/null +++ b/tests/features/template_renderer_errors.feature @@ -0,0 +1,108 @@ +Feature: Template Renderer Error Handling and Edge Cases + + Scenario Outline: Template renderer initialization failures for missing packages + Given I try to initialize a TemplateRenderer with the "" engine + When the required package "" is not installed + Then a TemplateError should be raised with a message containing "" + + Examples: + | engine | package | message | + | jinja2 | jinja2 | Jinja2 is not installed. Install it with 'pip install jinja2'. | + | mustache | pystache | Pystache is not installed. Install it with 'pip install pystache'. | + + Scenario: Template renderer initialization with unsupported engine + Given I have an unsupported template engine type + When I try to initialize a TemplateRenderer with this engine type + Then a TemplateError should be raised with a message containing "Unsupported template engine" + + Scenario: Registering a template with an empty name + Given a TemplateRenderer with the "simple" engine + When I attempt to register a template with an empty name + Then a TemplateError should be raised with a message containing "Template name cannot be empty" + + Scenario: Registering an invalid Jinja2 template + Given a TemplateRenderer with the "jinja2" engine + When I try to register a template with invalid Jinja2 syntax "Hello, {{ name" + Then a TemplateError should be raised with a message containing "Failed to register Jinja2 template 'invalid'" + + Scenario: Rendering a simple template with missing format variables + Given a TemplateRenderer with the "simple" engine + And a template named "test" with content "Hello, {name}" + When I try to render the "test" template with an empty context + Then a TemplateError should be raised with a message containing "Missing template variable: name" + + Scenario: Rendering a simple string with missing format variables + Given a TemplateRenderer with the "simple" engine + When I try to render the string "Hello, {name}" with an empty context + Then a TemplateError should be raised with a message containing "Missing template variable: name" + + Scenario: Rendering a simple template with a placeholder evaluating to None + Given a TemplateRenderer with the "simple" engine + And a template named "test" with content "Value: {{novalue}}" + When I render the "test" template with context '{"novalue": null}' + Then the result should be "Value: " + + Scenario: Rendering a simple template with an invalid expression + Given a TemplateRenderer with the "simple" engine + And a template named "test" with content "Value: {{ 1 +/ 2 }}" + When I try to render the "test" template with any context + Then a TemplateError should be raised with a message containing "Failed to render template 'test'" + + Scenario: Rendering a simple string with an invalid expression + Given a TemplateRenderer with the "simple" engine + When I try to render the string "Value: {{ 1 +/ 2 }}" with any context + Then a TemplateError should be raised with a message containing "Failed to render template string" + + Scenario: Getting a non-existent template + Given a TemplateRenderer with the "simple" engine + When I try to get a template named "nonexistent" + Then a TemplateError should be raised with a message containing "Template 'nonexistent' not found" + + Scenario: Rendering a non-existent template + Given a TemplateRenderer with the "simple" engine + When I try to render a template named "nonexistent" with any context + Then a TemplateError should be raised with a message containing "Template 'nonexistent' not found" + + Scenario: Registration failure with Jinja2 due to bad engine object + Given a TemplateRenderer with the "jinja2" engine + And I have manually modified the engine to be invalid + When I try to register a template named "test" with content "Hello" + Then a TemplateError should be raised with a message containing "Jinja2 environment has no from_string method" + + Scenario: Rendering failure with Jinja2 due to bad template object + Given a TemplateRenderer with the "jinja2" engine + And I have manually registered a template named "bad_template" that is not a valid Jinja2 template object + When I try to render the "bad_template" template with any context + Then a TemplateError should be raised with a message containing "Jinja2 template has no render method" + + Scenario: String rendering failure with Jinja2 due to bad engine object + Given a TemplateRenderer with the "jinja2" engine + And I have manually modified the engine to be invalid + When I try to render the string "Hello, {{name}}" with any context + Then a TemplateError should be raised with a message containing "Jinja2 environment has no from_string method" + + Scenario: Rendering failure with Mustache due to bad engine object + Given a TemplateRenderer with the "mustache" engine + And I have manually modified the engine to be invalid + And a template named "test" with content "Hello, {{name}}" + When I try to render the "test" template with any context + Then a TemplateError should be raised with a message containing "Mustache renderer has no render method" + + Scenario: String rendering failure with Mustache due to bad engine object + Given a TemplateRenderer with the "mustache" engine + And I have manually modified the engine to be invalid + When I try to render the string "Hello, {{name}}" with any context + Then a TemplateError should be raised with a message containing "Mustache renderer has no render method" + + Scenario: Rendering with an unsupported engine type + Given a TemplateRenderer with the "simple" engine + And I have manually set the engine type to an unsupported value + And a template named "test" with content "Hello" + When I try to render the "test" template with any context + Then a TemplateError should be raised with a message containing "Unsupported template engine" + + Scenario: String rendering with an unsupported engine type + Given a TemplateRenderer with the "simple" engine + And I have manually set the engine type to an unsupported value + When I try to render the string "Hello" with any context + Then a TemplateError should be raised with a message containing "Unsupported template engine"