mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-18 02:10:41 -07:00
Bound resource use when iterating ranges (#2117)
This commit is contained in:
@@ -465,4 +465,85 @@ HERE
|
||||
|
||||
assert(context.registers[:for_stack].empty?)
|
||||
end
|
||||
|
||||
def test_integer_range_is_not_materialized_and_charges_empty_iterations
|
||||
range = bounded_integer_range_with_tripwires
|
||||
template = Template.parse('{% for i in numbers %}{% endfor %}')
|
||||
template.resource_limits.render_score_limit = 3
|
||||
|
||||
assert_raises(Liquid::MemoryError) { template.render!('numbers' => range) }
|
||||
|
||||
assert(template.resource_limits.reached?)
|
||||
assert_equal(4, template.resource_limits.render_score)
|
||||
end
|
||||
|
||||
def test_integer_range_uses_arithmetic_offsets_reversal_and_metadata
|
||||
range = bounded_integer_range_with_tripwires
|
||||
template = Template.parse(
|
||||
'{% for i in numbers reversed offset:997 limit:2 %}{{ forloop.length }}:{{ i }}{% endfor %}',
|
||||
)
|
||||
template.resource_limits.render_score_limit = 10
|
||||
|
||||
assert_equal('2:9992:998', template.render!('numbers' => range))
|
||||
end
|
||||
|
||||
def test_range_scores_cannot_be_bypassed_by_repeated_renders
|
||||
template = Template.parse('{% for i in (1..2) %}{% endfor %}')
|
||||
template.resource_limits.cumulative_render_score_limit = 3
|
||||
assert_equal('', template.render!)
|
||||
assert_raises(Liquid::MemoryError) { template.render! }
|
||||
end
|
||||
|
||||
def test_range_break_charges_only_visited_items_and_preserves_full_metadata
|
||||
range = bounded_integer_range_with_tripwires
|
||||
template = Template.parse(
|
||||
'{% for i in numbers reversed %}{{ forloop.length }}:{{ i }}{% break %}{% endfor %}',
|
||||
)
|
||||
template.resource_limits.render_score_limit = 10
|
||||
|
||||
assert_equal('1000:1000', template.render!('numbers' => range))
|
||||
assert_operator(template.resource_limits.render_score, :<=, 10)
|
||||
end
|
||||
|
||||
def test_range_subclass_uses_its_custom_each
|
||||
range = Class.new(Range) do
|
||||
def each
|
||||
yield 10
|
||||
yield 20
|
||||
end
|
||||
end.new(nil, 3)
|
||||
|
||||
assert_template_result('1020', '{% for i in numbers %}{{ i }}{% endfor %}', { 'numbers' => range })
|
||||
end
|
||||
|
||||
def test_range_subclass_custom_to_a_is_honored_for_finite_and_open_bounds
|
||||
range_class = Class.new(Range) do
|
||||
def to_a
|
||||
[42]
|
||||
end
|
||||
end
|
||||
|
||||
[range_class.new(1, 3), range_class.new(nil, 3), range_class.new(1, nil)].each do |range|
|
||||
assert_template_result('42', '{% for i in numbers %}{{ i }}{% endfor %}', { 'numbers' => range })
|
||||
end
|
||||
end
|
||||
|
||||
def test_endless_range_remains_unsupported_with_a_limit
|
||||
template = Template.parse('{% for i in numbers limit:2 %}{{ i }}{% endfor %}')
|
||||
|
||||
assert_raises(RangeError) { template.render!('numbers' => (1..)) }
|
||||
end
|
||||
|
||||
def test_endless_range_subclass_with_custom_each_remains_unsupported
|
||||
range = Class.new(Range) do
|
||||
def each
|
||||
yield 10
|
||||
yield 20
|
||||
end
|
||||
end.new(1, nil)
|
||||
|
||||
assert_raises(RangeError) do
|
||||
Template.parse('{% for i in numbers %}{{ i }}{% endfor %}').render!('numbers' => range)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -465,4 +465,94 @@ class TableRowTest < Minitest::Test
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
end
|
||||
|
||||
def test_integer_range_is_not_materialized_and_charges_empty_iterations
|
||||
range = bounded_integer_range_with_tripwires
|
||||
template = Template.parse('{% tablerow i in numbers %}{% endtablerow %}')
|
||||
template.resource_limits.render_score_limit = 3
|
||||
|
||||
assert_raises(Liquid::MemoryError) { template.render!('numbers' => range) }
|
||||
|
||||
assert(template.resource_limits.reached?)
|
||||
assert_equal(4, template.resource_limits.render_score)
|
||||
end
|
||||
|
||||
def test_integer_range_uses_arithmetic_offsets_limit_and_full_metadata
|
||||
range = bounded_integer_range_with_tripwires
|
||||
template = Template.parse(
|
||||
'{% tablerow i in numbers offset:997 limit:2 %}{{ tablerowloop.length }}:{{ tablerowloop.index }}:{{ i }}{% endtablerow %}',
|
||||
)
|
||||
|
||||
assert_equal(
|
||||
"<tr class=\"row1\">\n<td class=\"col1\">2:1:998</td><td class=\"col2\">2:2:999</td></tr>\n",
|
||||
template.render!('numbers' => range),
|
||||
)
|
||||
end
|
||||
|
||||
def test_tablerow_checks_generated_output_during_empty_body_iteration
|
||||
template = Template.parse('{% tablerow i in (1..100) %}{% endtablerow %}')
|
||||
template.resource_limits.render_length_limit = 40
|
||||
checked_lengths = []
|
||||
limits = template.resource_limits
|
||||
original_increment_write_score = limits.method(:increment_write_score)
|
||||
limits.define_singleton_method(:increment_write_score) do |output|
|
||||
checked_lengths << output.bytesize
|
||||
original_increment_write_score.call(output)
|
||||
end
|
||||
|
||||
assert_equal('Liquid error: Memory limits exceeded', template.render)
|
||||
assert(template.resource_limits.reached?)
|
||||
assert_operator(checked_lengths.last, :<, 1000)
|
||||
end
|
||||
|
||||
def test_tablerow_range_scores_persist_across_renders
|
||||
template = Template.parse('{% tablerow i in (1..2) %}{% endtablerow %}')
|
||||
template.resource_limits.cumulative_render_score_limit = 3
|
||||
template.render!
|
||||
assert_raises(Liquid::MemoryError) { template.render! }
|
||||
end
|
||||
|
||||
def test_range_subclass_uses_its_custom_each_with_beginless_bounds
|
||||
range = Class.new(Range) do
|
||||
def each
|
||||
yield 10
|
||||
yield 20
|
||||
end
|
||||
end.new(nil, 3)
|
||||
|
||||
assert_template_result(
|
||||
"<tr class=\"row1\">\n<td class=\"col1\">10</td><td class=\"col2\">20</td></tr>\n",
|
||||
'{% tablerow i in numbers %}{{ i }}{% endtablerow %}',
|
||||
{ 'numbers' => range },
|
||||
)
|
||||
end
|
||||
|
||||
def test_endless_range_subclass_uses_its_custom_each_without_a_limit
|
||||
range = Class.new(Range) do
|
||||
def each
|
||||
yield 10
|
||||
yield 20
|
||||
end
|
||||
end.new(1, nil)
|
||||
|
||||
assert_template_result(
|
||||
"<tr class=\"row1\">\n<td class=\"col1\">10</td><td class=\"col2\">20</td></tr>\n",
|
||||
'{% tablerow i in numbers %}{{ i }}{% endtablerow %}',
|
||||
{ 'numbers' => range },
|
||||
)
|
||||
end
|
||||
|
||||
def test_range_subclass_custom_to_a_is_not_used
|
||||
range = Class.new(Range) do
|
||||
def to_a
|
||||
[42]
|
||||
end
|
||||
end.new(1, 3)
|
||||
|
||||
assert_template_result(
|
||||
"<tr class=\"row1\">\n<td class=\"col1\">1</td><td class=\"col2\">2</td><td class=\"col3\">3</td></tr>\n",
|
||||
'{% tablerow i in numbers %}{{ i }}{% endtablerow %}',
|
||||
{ 'numbers' => range },
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -132,7 +132,7 @@ class TemplateTest < Minitest::Test
|
||||
assert_equal("Liquid error: Memory limits exceeded", t.render)
|
||||
assert(t.resource_limits.reached?)
|
||||
|
||||
t.resource_limits.render_score_limit = 200
|
||||
t.resource_limits.render_score_limit = 201
|
||||
assert_equal(" foo " * 100, t.render!)
|
||||
refute_nil(t.resource_limits.render_score)
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user