diff --git a/examples/liquid_ruby.rb b/examples/liquid_ruby.rb index 542a1c1..f3c832c 100644 --- a/examples/liquid_ruby.rb +++ b/examples/liquid_ruby.rb @@ -29,10 +29,19 @@ end LiquidSpec.render do |ctx, assigns, render_options| + resource_limits = if render_options[:resource_limits] + limits = Liquid::ResourceLimits.new({}) + render_options[:resource_limits].each do |key, value| + limits.send(:"#{key}=", value) + end + limits + end + context = Liquid::Context.build( static_environments: assigns, registers: Liquid::Registers.new(render_options[:registers] || {}), rethrow_errors: render_options[:strict_errors], + resource_limits: resource_limits, ) context.exception_renderer = render_options[:exception_renderer] if render_options[:exception_renderer] diff --git a/lib/liquid/spec/cli/runner.rb b/lib/liquid/spec/cli/runner.rb index e79e39a..f968494 100644 --- a/lib/liquid/spec/cli/runner.rb +++ b/lib/liquid/spec/cli/runner.rb @@ -1489,6 +1489,7 @@ def run_single_spec(spec, _config) render_options = { registers: build_registers(spec, filesystem), strict_errors: !render_errors, + resource_limits: spec.raw_resource_limits.empty? ? nil : spec.raw_resource_limits, }.compact begin diff --git a/lib/liquid/spec/lazy_spec.rb b/lib/liquid/spec/lazy_spec.rb index 90ac579..7d00025 100644 --- a/lib/liquid/spec/lazy_spec.rb +++ b/lib/liquid/spec/lazy_spec.rb @@ -30,7 +30,7 @@ class LazySpec attr_reader :name, :template, :template_name, :expected, :expected_pattern, :errors, :hint, :doc, :complexity attr_reader :error_mode, :render_errors, :required_features attr_reader :source_file, :line_number - attr_reader :raw_environment, :raw_filesystem, :raw_template_factory + attr_reader :raw_environment, :raw_filesystem, :raw_template_factory, :raw_resource_limits def initialize( name:, @@ -50,6 +50,7 @@ def initialize( raw_environment: {}, raw_filesystem: {}, raw_template_factory: nil, + raw_resource_limits: {}, source_hint: nil, source_required_options: {} ) @@ -70,6 +71,7 @@ def initialize( @raw_environment = raw_environment || {} @raw_filesystem = raw_filesystem || {} @raw_template_factory = raw_template_factory + @raw_resource_limits = raw_resource_limits || {} @source_hint = source_hint @source_required_options = source_required_options || {} @@ -300,7 +302,7 @@ def deep_instantiate(obj, seen = {}.compare_by_identity) key = obj.keys.first value = obj.values.first if key.is_a?(String) && key.start_with?("instantiate:") - class_name = key.sub("instantiate:", "") + class_name = key.sub("instantiate:", "").chomp(":") # Deep instantiate the parameters first params = deep_instantiate(value, seen) # Create a fresh instance via the registry diff --git a/lib/liquid/spec/spec_loader.rb b/lib/liquid/spec/spec_loader.rb index fb9819f..5eb1d10 100644 --- a/lib/liquid/spec/spec_loader.rb +++ b/lib/liquid/spec/spec_loader.rb @@ -56,7 +56,7 @@ module SpecLoader VALID_METADATA_KEYS = %w[hint doc required_options render_errors minimum_complexity complexity required_features data_files].freeze VALID_SPEC_KEYS = %w[ name template expected expected_pattern environment filesystem complexity hint doc - error_mode render_errors required_features errors template_name + error_mode render_errors required_features errors template_name resource_limits ].freeze class << self @@ -259,6 +259,7 @@ def load_yaml_file(path, suite: nil) raw_environment: raw_env, raw_filesystem: spec_data["filesystem"] || {}, raw_template_factory: spec_data["template_factory"], + raw_resource_limits: spec_data["resource_limits"], source_hint: source_hint, source_required_options: source_required_options, ) diff --git a/specs/basics/resource-limits.yml b/specs/basics/resource-limits.yml new file mode 100644 index 0000000..57700c3 --- /dev/null +++ b/specs/basics/resource-limits.yml @@ -0,0 +1,49 @@ +_metadata: + hint: | + Tests for resource limit enforcement during rendering. + Cumulative scores survive reset() across partial renders, + preventing unbounded work via many small partials. + complexity: 300 + required_features: + - core + +specs: +- name: cumulative_render_score_across_partials + template: "{% render 'a' %}{% render 'b' %}{% render 'c' %}" + filesystem: + a: "{% for i in (1..10) %}x{% endfor %}" + b: "{% for i in (1..10) %}x{% endfor %}" + c: "{% for i in (1..10) %}x{% endfor %}" + resource_limits: + render_score_limit: 100 + cumulative_render_score_limit: 25 + errors: + render_error: + - Liquid error + hint: | + Each partial renders a for-loop scoring ~10 points. Per-template limit (100) + is fine, but cumulative limit (25) is exceeded after the second partial. + +- name: cumulative_limits_not_triggered_when_unset + template: "{% render 'a' %}{% render 'b' %}" + filesystem: + a: "{% for i in (1..5) %}x{% endfor %}" + b: "{% for i in (1..5) %}y{% endfor %}" + resource_limits: + render_score_limit: 100 + expected: "xxxxxyyyyy" + hint: | + Without cumulative limits set, partials render normally even though + cumulative score grows. Only per-template limits are checked. + +- name: per_template_limit_still_works_with_cumulative + template: "{% for i in (1..100) %}x{% endfor %}" + resource_limits: + render_score_limit: 10 + cumulative_render_score_limit: 1000 + errors: + render_error: + - Liquid error + hint: | + Per-template render_score_limit (10) fires even when + cumulative_render_score_limit (1000) is generous.