Compare commits

..
Author SHA1 Message Date
Alok SwamyandGitHub 7d5a4d092a Merge pull request #2064 from Shopify/strict2-include-tablerow
Add for_loop? to Include tag for AST-based keyword detection
2026-04-24 16:06:06 -04:00
42 changed files with 74 additions and 1014 deletions
-1
View File
@@ -22,7 +22,6 @@ jobs:
}
- { ruby: 4.0, allowed-failure: false, rubyopt: "--yjit" }
- { ruby: 4.0, allowed-failure: false, rubyopt: "--zjit" }
- { ruby: truffleruby, allowed-failure: false }
# Head can have failures due to being in development
- { ruby: head, allowed-failure: true }
+1 -1
View File
@@ -1 +1 @@
4.0.2
3.4.1
+1 -2
View File
@@ -32,7 +32,6 @@ group :test do
end
group :spec do
# Includes the range resource-limit specs from https://github.com/Shopify/liquid-spec/pull/165.
gem 'liquid-spec', github: 'Shopify/liquid-spec', ref: '84bf25e0edbca5f7e530574788b0f875ab331f50'
gem 'liquid-spec', github: 'Shopify/liquid-spec', branch: 'main'
gem 'activesupport', require: false
end
-16
View File
@@ -1,21 +1,5 @@
# Liquid Change Log
## 5.14.0
* Avoid materializing integer ranges in `for` and `tablerow` loops, and account for each visited range item [Ian Ker-Seymer]
## 5.13.0
* Add TruffleRuby in CI [Benoit Daloze]
* Skip slow test raising many exceptions on non-CRuby [Benoit Daloze]
* Reject bare-bracket syntax in strict2 and introduce `self` keyword by [Alok Swamy]
* Add strict2_parse to assign and capture tags by [Alok Swamy]
* Add strict2_parse to increment and decrement tags by [Alok Swamy]
* Update liquid-spec adapters for `missing_features` [Ian Ker-Seymer]
* Prevent `SelfDrop` context mutation across render boundaries [Guilherme Carreiro]
* Fix `SelfDrop` equality [Guilherme Carreiro]
* Let environment `self` shadow `SelfDrop` [Ian Ker-Seymer]
## 5.11.0
* Revert the Inline Snippets tag (#2001), treat its inclusion in the latest Liquid release as a bug, and allow for feedback on RFC#1916 to better support Liquid developers [Guilherme Carreiro]
* Rename the `:rigid` error mode to `:strict2` and display a warning when users attempt to use the `:rigid` mode [Guilherme Carreiro]
-7
View File
@@ -149,13 +149,6 @@ template.render!({ 'x' => 1}, { strict_variables: true })
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y
```
### Resource limits
`render_score_limit` and `cumulative_render_score_limit` account for each item visited by
integer-range `for` and `tablerow` loops, including loops with empty bodies. This bounds range
iteration work when a score limit is configured; `render_length_limit` only bounds generated
output and does not by itself limit CPU work for output-free loops.
### Usage tracking
To help track usages of a feature or code path in production, we have released opt-in usage tracking. To enable this, we provide an empty `Liquid:: Usage.increment` method which you can customize to your needs. The feature is well suited to https://github.com/Shopify/statsd-instrument. However, the choice of implementation is up to you.
+2 -4
View File
@@ -151,8 +151,6 @@ end
desc('run liquid-spec suite across all adapters')
task :spec do
Dir['./spec/*.rb'].sort.each do |adapter|
puts "=== Running #{adapter} ==="
sh 'bundle', 'exec', 'liquid-spec', 'run', adapter, '--no-max-failures'
end
adapters = Dir['./spec/*.rb'].join(',')
sh "bundle exec liquid-spec matrix --adapters=#{adapters} --reference=ruby_liquid"
end
-1
View File
@@ -83,7 +83,6 @@ require 'liquid/resource_limits'
require 'liquid/expression'
require 'liquid/template'
require 'liquid/condition'
require 'liquid/range_slice'
require 'liquid/utils'
require 'liquid/tokenizer'
require 'liquid/parse_context'
+1 -3
View File
@@ -99,9 +99,7 @@ module Liquid
context.handle_error(exc, line_number)
else
error_message = context.handle_error(exc, line_number)
error_mode = context.registers.static[:template_error_mode]
suppress_error_text = blank_tag && error_mode != :strict2 && error_mode != :rigid
unless suppress_error_text # blank-tag suppression is kept for backwards compatibility outside strict2
unless blank_tag # conditional for backwards compatibility
output << error_message
end
end
+4 -9
View File
@@ -206,21 +206,16 @@ module Liquid
# path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.key?(key) }
fallback_to_self_drop = key == Expression::SELF && index.nil?
# `self` resolves to a SelfDrop (enabling `self['var']` lookups),
# but only when it hasn't been explicitly assigned as a local variable.
return SelfDrop.new(self) if key == Expression::SELF && !index
variable = if index
lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
else
try_variable_find_in_environments(
key,
raise_on_not_found: raise_on_not_found && !fallback_to_self_drop,
)
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
end
# `self` resolves to a SelfDrop (enabling `self['var']` lookups),
# but only after the normal environment lookup doesn't find a value.
return @self_drop ||= SelfDrop.new(self) if fallback_to_self_drop && variable.nil?
# update variable's context before invoking #to_liquid
variable.context = self if variable.respond_to?(:context=)
+1 -1
View File
@@ -55,7 +55,7 @@ module Liquid
str << variable_lookups
when :open_square
if @reject_bare_brackets
raise SyntaxError, "Bare bracket access is not allowed. Use #{Expression::SELF}['...'] instead"
raise SyntaxError, "Bare bracket access is not allowed in strict2 mode. Use #{Expression::SELF}['...'] instead"
end
str = consume.dup
str << expression
-44
View File
@@ -1,44 +0,0 @@
# frozen_string_literal: true
module Liquid
class RangeSlice
attr_reader :length
def initialize(range, from, to, resource_limits)
range_length = range.end - range.begin
range_length += 1 unless range.exclude_end?
range_length = 0 if range_length.negative?
start = [from, 0].max
finish = [to || range_length, range_length].min
@first = range.begin + start
@length = [finish - start, 0].max
@direction = 1
@resource_limits = resource_limits
end
def empty?
@length.zero?
end
def each
return enum_for(:each) unless block_given?
value = @first
@length.times do
@resource_limits.increment_render_score(1)
yield value
value += @direction
end
end
def reverse!
unless empty?
@first += @direction * (@length - 1)
@direction = -@direction
end
self
end
end
end
+4 -20
View File
@@ -16,39 +16,23 @@ module Liquid
# then the local value takes precedence over the `self` object.
# @liquid_access global
class SelfDrop < Drop
def initialize(self_context)
def initialize(context)
super()
@self_context = self_context
@context = context
end
def [](key)
@self_context.find_variable(key)
@context.find_variable(key)
rescue UndefinedVariable
nil
end
def key?(key)
@self_context.variable_defined?(key)
@context.variable_defined?(key)
end
def to_liquid
self
end
def ==(other)
other.is_a?(SelfDrop) && other.self_context.equal?(@self_context)
end
alias_method :eql?, :==
def hash
@self_context.object_id.hash
end
protected
attr_reader :self_context
undef context=
end
end
+7 -16
View File
@@ -8,19 +8,10 @@ module Liquid
MAX_I32 = (1 << 31) - 1
private_constant :MAX_I32
supports_64bit_indices = begin
[][1 << 33, 1 << 33]
true
rescue RangeError
false
end
INDEX_RANGE = if supports_64bit_indices
(-(1 << 63))..((1 << 63) - 1)
else
(-(1 << 31))..((1 << 31) - 1)
end
private_constant :INDEX_RANGE
MIN_I64 = -(1 << 63)
MAX_I64 = (1 << 63) - 1
I64_RANGE = MIN_I64..MAX_I64
private_constant :MIN_I64, :MAX_I64, :I64_RANGE
HTML_ESCAPE = {
'&' => '&amp;',
@@ -223,11 +214,11 @@ module Liquid
Utils.to_s(input).slice(offset, length) || ''
end
rescue RangeError
if INDEX_RANGE.cover?(length) && INDEX_RANGE.cover?(offset)
if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset)
raise # unexpected error
end
offset = offset.clamp(INDEX_RANGE)
length = length.clamp(INDEX_RANGE)
offset = offset.clamp(I64_RANGE)
length = length.clamp(I64_RANGE)
retry
end
end
-25
View File
@@ -18,8 +18,6 @@ module Liquid
# @liquid_syntax_keyword variable_name The name of the variable being created.
# @liquid_syntax_keyword value The value you want to assign to the variable.
class Assign < Tag
include ParserSwitching
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
# @api private
@@ -31,10 +29,6 @@ module Liquid
def initialize(tag_name, markup, parse_context)
super
parse_with_selected_parser(markup)
end
def lax_parse(markup)
if markup =~ Syntax
@to = Regexp.last_match(1)
@from = Variable.new(Regexp.last_match(2), parse_context)
@@ -43,25 +37,6 @@ module Liquid
end
end
def strict_parse(markup)
lax_parse(markup)
end
def strict2_parse(markup)
unless markup =~ Syntax
self.class.raise_syntax_error(parse_context)
end
lhs = Regexp.last_match(1).strip
rhs = Regexp.last_match(2)
p = @parse_context.new_parser(lhs)
@to = p.consume(:id)
p.consume(:end_of_string)
@from = Variable.new(rhs, parse_context)
end
def render_to_output_buffer(context, output)
val = @from.render(context)
context.scopes.last[@to] = val
-18
View File
@@ -20,18 +20,10 @@ module Liquid
# @liquid_syntax_keyword variable The name of the variable being created.
# @liquid_syntax_keyword value The value you want to assign to the variable.
class Capture < Block
include ParserSwitching
Syntax = /(#{VariableSignature}+)/o
attr_reader :to
def initialize(tag_name, markup, options)
super
parse_with_selected_parser(markup)
end
def lax_parse(markup)
if markup =~ Syntax
@to = Regexp.last_match(1)
else
@@ -39,16 +31,6 @@ module Liquid
end
end
def strict_parse(markup)
lax_parse(markup)
end
def strict2_parse(markup)
p = @parse_context.new_parser(markup.strip)
@to = p.consume(:id)
p.consume(:end_of_string)
end
def render_to_output_buffer(context, output)
context.resource_limits.with_capture do
capture_output = render(context)
-16
View File
@@ -23,29 +23,13 @@ module Liquid
# {% decrement variable_name %}
# @liquid_syntax_keyword variable_name The name of the variable being decremented.
class Decrement < Tag
include ParserSwitching
attr_reader :variable_name
def initialize(tag_name, markup, options)
super
parse_with_selected_parser(markup)
end
def lax_parse(markup)
@variable_name = markup.strip
end
def strict_parse(markup)
lax_parse(markup)
end
def strict2_parse(markup)
p = @parse_context.new_parser(markup.strip)
@variable_name = p.consume(:id)
p.consume(:end_of_string)
end
def render_to_output_buffer(context, output)
counter_environment = context.environments.first
value = counter_environment[@variable_name] || 0
+2 -3
View File
@@ -130,6 +130,7 @@ module Liquid
end
collection = context.evaluate(@collection_name)
collection = collection.to_a if collection.is_a?(Range)
limit_value = context.evaluate(@limit)
to = if limit_value.nil?
@@ -138,9 +139,7 @@ module Liquid
Utils.to_integer(limit_value) + from
end
segment = Utils.slice_collection_for_iteration(
collection, from, to, context.resource_limits, use_range_to_a: true
)
segment = Utils.slice_collection(collection, from, to)
segment.reverse! if @reversed
offsets[@name] = from + segment.length
-16
View File
@@ -23,29 +23,13 @@ module Liquid
# {% increment variable_name %}
# @liquid_syntax_keyword variable_name The name of the variable being incremented.
class Increment < Tag
include ParserSwitching
attr_reader :variable_name
def initialize(tag_name, markup, options)
super
parse_with_selected_parser(markup)
end
def lax_parse(markup)
@variable_name = markup.strip
end
def strict_parse(markup)
lax_parse(markup)
end
def strict2_parse(markup)
p = @parse_context.new_parser(markup.strip)
@variable_name = p.consume(:id)
p.consume(:end_of_string)
end
def render_to_output_buffer(context, output)
counter_environment = context.environments.first
value = counter_environment[@variable_name] || 0
+1 -5
View File
@@ -85,13 +85,12 @@ module Liquid
from = @attributes.key?('offset') ? to_integer(context.evaluate(@attributes['offset'])) : 0
to = @attributes.key?('limit') ? from + to_integer(context.evaluate(@attributes['limit'])) : nil
collection = Utils.slice_collection_for_iteration(collection, from, to, context.resource_limits, allow_endless: true)
collection = Utils.slice_collection(collection, from, to)
length = collection.length
cols = @attributes.key?('cols') ? to_integer(context.evaluate(@attributes['cols'])) : length
output << "<tr class=\"row1\">\n"
context.resource_limits.increment_write_score(output)
context.stack do
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
context['tablerowloop'] = tablerowloop
@@ -102,7 +101,6 @@ module Liquid
output << "<td class=\"col#{tablerowloop.col}\">"
super
output << '</td>'
context.resource_limits.increment_write_score(output)
# Handle any interrupts if they exist.
if context.interrupt?
@@ -112,7 +110,6 @@ module Liquid
if tablerowloop.col_last && !tablerowloop.last
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
context.resource_limits.increment_write_score(output)
end
tablerowloop.send(:increment!)
@@ -120,7 +117,6 @@ module Liquid
end
output << "</tr>\n"
context.resource_limits.increment_write_score(output)
output
end
+2 -13
View File
@@ -151,10 +151,8 @@ module Liquid
c
when Liquid::Drop
drop = args.shift
c = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
drop.context = c if drop.respond_to?(:context=)
c
drop = args.shift
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
when Hash
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
when nil
@@ -189,20 +187,12 @@ module Liquid
context.template_name ||= name
previous_error_mode = context.registers.static[:template_error_mode]
context.registers.static[:template_error_mode] = @error_mode
begin
# render the nodelist.
@root.render_to_output_buffer(context, output || +'')
rescue Liquid::MemoryError => e
context.handle_error(e)
ensure
if previous_error_mode
context.registers.static[:template_error_mode] = previous_error_mode
else
context.registers.static.delete(:template_error_mode)
end
@errors = context.errors
end
end
@@ -234,7 +224,6 @@ module Liquid
end
@warnings = parse_context.warnings
@error_mode = parse_context.error_mode
parse_context
end
-69
View File
@@ -13,26 +13,6 @@ module Liquid
end
end
# This is intentionally separate from slice_collection, whose Array-returning
# behavior is used outside of the iteration tags.
def self.slice_collection_for_iteration(
collection, from, to, resource_limits, allow_endless: false, use_range_to_a: false
)
if integer_range?(collection)
RangeSlice.new(collection, from, to, resource_limits)
elsif collection.is_a?(Range)
if use_range_to_a && range_method_overridden?(collection, :to_a)
# For historically honored custom Range#to_a. Charge the resulting
# selection before buffering it, just as for a custom #each.
slice_collection_for_iteration_using_each(collection.to_a, from, to, resource_limits)
else
slice_range_using_each(collection, from, to, resource_limits, allow_endless: allow_endless)
end
else
slice_collection(collection, from, to)
end
end
def self.slice_collection_using_each(collection, from, to)
segments = []
index = 0
@@ -58,55 +38,6 @@ module Liquid
segments
end
# Arithmetic slicing must not bypass a Range subclass's custom #each.
def self.integer_range?(collection)
collection.instance_of?(Range) && collection.begin.is_a?(Integer) && collection.end.is_a?(Integer)
end
private_class_method :integer_range?
# Preserve support for Ruby-supplied string ranges and custom Range#each.
# Their selected length cannot be inferred from integer bounds, but the tags
# need it before rendering for loop metadata, continuation offsets, and columns.
# Buffer the selection so we do not have to replay a potentially custom iterator.
def self.slice_range_using_each(collection, from, to, resource_limits, allow_endless:)
# TableRow historically accepted an endless subclass when its custom #each
# was finite, while For historically raised through Range#to_a.
if collection.end.nil? && !(allow_endless && (!to.nil? || range_method_overridden?(collection, :each)))
raise RangeError, "cannot convert endless range to an array"
end
if collection.begin.nil? && !range_method_overridden?(collection, :each)
raise TypeError, "can't iterate from NilClass"
end
slice_collection_for_iteration_using_each(collection, from, to, resource_limits)
end
private_class_method :slice_range_using_each
# Custom Range#each can make a nominally beginless range finite; standard
# beginless ranges were rejected before reaching this budgeted traversal.
def self.slice_collection_for_iteration_using_each(collection, from, to, resource_limits)
return [] if to && to <= from
segments = []
index = 0
collection.each do |item|
break if to && to <= index
# Charge preparation, including skipped offsets, before buffering; checking
# only while rendering the buffered values would leave this work unbudgeted.
resource_limits.increment_render_score(1)
segments << item if from <= index
index += 1
end
segments
end
private_class_method :slice_collection_for_iteration_using_each
def self.range_method_overridden?(collection, method_name)
collection.method(method_name).owner != Range.instance_method(method_name).owner
end
private_class_method :range_method_overridden?
def self.to_integer(num)
return num if num.is_a?(Integer)
num = num.to_s
+1 -1
View File
@@ -2,5 +2,5 @@
# frozen_string_literal: true
module Liquid
VERSION = "5.14.0"
VERSION = "5.12.0"
end
+3 -16
View File
@@ -6,24 +6,14 @@
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
require 'liquid'
require_relative 'support/liquid_spec_adapter_helper'
LiquidSpec.configure do |config|
config.missing_features = [
:activesupport,
:lax_parsing,
:shopify_filters,
:shopify_includes,
:shopify_blank,
:shopify_error_handling,
:shopify_error_format,
:shopify_string_access,
]
# Run core Liquid specs
config.features = [:core]
end
# Compile a template string into a Liquid::Template
LiquidSpec.compile do |ctx, source, options|
options[:error_mode] ||= :strict
ctx[:template] = Liquid::Template.parse(source, **options)
end
@@ -38,12 +28,9 @@ LiquidSpec.render do |ctx, assigns, options|
static_environments: assigns,
registers: registers,
rethrow_errors: options[:strict_errors],
resource_limits: LiquidSpecAdapterHelper.resource_limits(options),
)
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
LiquidSpecAdapterHelper.with_frozen_time do
ctx[:template].render(context)
end
ctx[:template].render(context)
end
+4 -16
View File
@@ -6,24 +6,15 @@
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
require 'liquid'
require_relative 'support/liquid_spec_adapter_helper'
LiquidSpec.configure do |config|
config.missing_features = [
:activesupport,
:shopify_filters,
:shopify_includes,
:shopify_blank,
:shopify_error_handling,
:shopify_error_format,
:shopify_string_access,
]
config.features = [:core, :lax_parsing]
end
# Compile a template string into a Liquid::Template
LiquidSpec.compile do |ctx, source, options|
# Default to lax mode while still honoring specs that explicitly set error_mode.
options = { error_mode: :lax }.merge(options)
# Force lax mode
options = options.merge(error_mode: :lax)
ctx[:template] = Liquid::Template.parse(source, **options)
end
@@ -35,12 +26,9 @@ LiquidSpec.render do |ctx, assigns, options|
static_environments: assigns,
registers: registers,
rethrow_errors: options[:strict_errors],
resource_limits: LiquidSpecAdapterHelper.resource_limits(options),
)
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
LiquidSpecAdapterHelper.with_frozen_time do
ctx[:template].render(context)
end
ctx[:template].render(context)
end
+3 -15
View File
@@ -7,23 +7,14 @@
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
require 'active_support/all'
require 'liquid'
require_relative 'support/liquid_spec_adapter_helper'
LiquidSpec.configure do |config|
config.missing_features = [
:lax_parsing,
:shopify_filters,
:shopify_includes,
:shopify_blank,
:shopify_error_handling,
:shopify_error_format,
:shopify_string_access,
]
# Run core Liquid specs plus ActiveSupport SafeBuffer tests
config.features = [:core, :activesupport]
end
# Compile a template string into a Liquid::Template
LiquidSpec.compile do |ctx, source, options|
options[:error_mode] ||= :strict
ctx[:template] = Liquid::Template.parse(source, **options)
end
@@ -38,12 +29,9 @@ LiquidSpec.render do |ctx, assigns, options|
static_environments: assigns,
registers: registers,
rethrow_errors: options[:strict_errors],
resource_limits: LiquidSpecAdapterHelper.resource_limits(options),
)
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
LiquidSpecAdapterHelper.with_frozen_time do
ctx[:template].render(context)
end
ctx[:template].render(context)
end
+2 -14
View File
@@ -13,18 +13,9 @@ end
require 'active_support/all'
require 'liquid'
require_relative 'support/liquid_spec_adapter_helper'
LiquidSpec.configure do |config|
config.missing_features = [
:lax_parsing,
:shopify_filters,
:shopify_includes,
:shopify_blank,
:shopify_error_handling,
:shopify_error_format,
:shopify_string_access,
]
config.features = [:core, :activesupport]
end
# Compile a template string into a Liquid::Template
@@ -42,12 +33,9 @@ LiquidSpec.render do |ctx, assigns, options|
static_environments: assigns,
registers: registers,
rethrow_errors: options[:strict_errors],
resource_limits: LiquidSpecAdapterHelper.resource_limits(options),
)
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
LiquidSpecAdapterHelper.with_frozen_time do
ctx[:template].render(context)
end
ctx[:template].render(context)
end
@@ -1,24 +0,0 @@
# frozen_string_literal: true
module LiquidSpecAdapterHelper
extend self
def resource_limits(render_options)
return unless render_options[:resource_limits]
Liquid::ResourceLimits.new({}).tap do |limits|
render_options[:resource_limits].each do |key, value|
limits.public_send(:"#{key}=", value)
end
end
end
def with_frozen_time(&block)
original_tz = ENV['TZ']
ENV['TZ'] = 'UTC'
Liquid::Spec::TimeFreezer.freeze(Liquid::Spec::AdapterRunner::TEST_TIME, &block)
ensure
ENV['TZ'] = original_tz
end
end
-40
View File
@@ -97,46 +97,6 @@ class AssignTest < Minitest::Test
assert_equal(12, assign_score_of('int' => 123, 'str' => 'abcd'))
end
def test_assign_with_valid_identifier_in_strict2
assert_template_result("hello", "{% assign my_var = 'hello' %}{{ my_var }}", error_mode: :strict2)
end
def test_assign_with_hyphen_in_strict2
assert_template_result("hello", "{% assign my-var = 'hello' %}{{ my-var }}", error_mode: :strict2)
end
def test_assign_rejects_parentheses_in_variable_name_in_strict2
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse("{% assign (a(b(c) = 1234 %}", error_mode: :strict2)
end
end
def test_assign_rejects_brackets_in_variable_name_in_strict2
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse("{% assign [x.y] = 'hello' %}", error_mode: :strict2)
end
end
def test_assign_rejects_dot_in_variable_name_in_strict2
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse("{% assign a.b = 'hello' %}", error_mode: :strict2)
end
end
def test_assign_rejects_numeric_variable_name_in_strict2
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse("{% assign 1abc = 'hello' %}", error_mode: :strict2)
end
end
def test_assign_allows_invalid_names_in_lax
assert_template_result("1234", "{% assign (a(b(c) = 1234 %}{{ self['(a(b(c)'] }}", error_mode: :lax)
end
def test_assign_with_filter_in_strict2
assert_template_result("HELLO", "{% assign my_var = 'hello' | upcase %}{{ my_var }}", error_mode: :strict2)
end
private
class ObjectWrapperDrop < Liquid::Drop
@@ -1,85 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class BlankBodyErrorHandlingTest < Minitest::Test
COMPARISON_ERROR = 'Liquid error (line 1): comparison of Integer with String failed'
INVALID_INTEGER_ERROR = 'Liquid error (line 1): invalid integer'
def render_inline(source, error_mode:, assigns: {})
Liquid::Template.parse(source, line_numbers: true, error_mode: error_mode).render(assigns, render_errors: true)
end
def assert_render_raises(source, error_mode:, assigns: {}, message: nil)
error = assert_raises(Liquid::ArgumentError) do
Liquid::Template.parse(source, line_numbers: true, error_mode: error_mode).render!(assigns)
end
assert_includes(error.message, message) if message
end
def test_blank_if_body_suppresses_inline_error_text_in_lax_and_strict
[:lax, :strict].each do |mode|
assert_equal('', render_inline('{% if 5 > "x" %}{% endif %}', error_mode: mode))
end
end
def test_blank_unless_body_suppresses_inline_error_text_in_lax_and_strict
[:lax, :strict].each do |mode|
assert_equal('', render_inline('{% unless 5 > "x" %} {% endunless %}', error_mode: mode))
end
end
def test_blank_for_body_suppresses_inline_error_text_in_lax_and_strict
[:lax, :strict].each do |mode|
assert_equal('', render_inline('{% for i in (1..3) offset: xs %}{% endfor %}', error_mode: mode, assigns: { 'xs' => 'bad' }))
end
end
def test_strict2_blank_if_body_shows_inline_error_text
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{% endif %}', error_mode: :strict2))
end
def test_strict2_whitespace_if_body_shows_inline_error_text
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %} {% endif %}', error_mode: :strict2))
end
def test_strict2_assign_if_body_shows_inline_error_text
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{% assign a = 1 %}{% endif %}', error_mode: :strict2))
end
def test_strict2_comment_if_body_shows_inline_error_text
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{% comment %}c{% endcomment %}{% endif %}', error_mode: :strict2))
end
def test_strict2_capture_if_body_shows_inline_error_text
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{% capture c %}text{% endcapture %}{% endif %}', error_mode: :strict2))
end
def test_strict2_blank_unless_body_shows_inline_error_text
assert_equal(COMPARISON_ERROR, render_inline('{% unless 5 > "x" %} {% endunless %}', error_mode: :strict2))
end
def test_strict2_blank_for_body_shows_inline_error_text
assert_equal(INVALID_INTEGER_ERROR, render_inline('{% for i in (1..3) offset: xs %}{% endfor %}', error_mode: :strict2, assigns: { 'xs' => 'bad' }))
end
def test_nonblank_bodies_show_inline_error_text_in_all_modes
[:lax, :strict, :strict2].each do |mode|
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{% echo 1 %}{% endif %}', error_mode: mode))
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{{ "" }}{% endif %}', error_mode: mode))
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{% else %}E{% endif %}', error_mode: mode))
end
end
def test_raised_errors_are_not_swallowed_by_blank_if_body
[:lax, :strict, :strict2].each do |mode|
assert_render_raises('{% if 5 > "x" %}{% endif %}', error_mode: mode, message: 'comparison of Integer with String failed')
end
end
def test_raised_errors_are_not_swallowed_by_blank_for_body
[:lax, :strict, :strict2].each do |mode|
assert_render_raises('{% for i in (1..3) offset: xs %}{% endfor %}', error_mode: mode, assigns: { 'xs' => 'bad' }, message: 'invalid integer')
end
end
end
+1 -36
View File
@@ -6,11 +6,7 @@ class CaptureTest < Minitest::Test
include Liquid
def test_captures_block_content_in_variable
assert_template_result("test string", "{% capture var %}test string{% endcapture %}{{var}}", {})
end
def test_captures_block_content_in_quoted_variable_in_lax
assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {}, error_mode: :lax)
assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {})
end
def test_capture_with_hyphen_in_variable_name
@@ -53,35 +49,4 @@ class CaptureTest < Minitest::Test
t.render!
assert_equal(9, t.resource_limits.assign_score)
end
def test_capture_with_valid_identifier_in_strict2
assert_template_result("hello", "{% capture my_var %}hello{% endcapture %}{{ my_var }}", error_mode: :strict2)
end
def test_capture_with_hyphen_in_strict2
assert_template_result("hello", "{% capture my-var %}hello{% endcapture %}{{ my-var }}", error_mode: :strict2)
end
def test_capture_rejects_parentheses_in_variable_name_in_strict2
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse("{% capture (x[y %}hello{% endcapture %}", error_mode: :strict2)
end
end
def test_capture_rejects_dot_in_variable_name_in_strict2
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse("{% capture a.b %}hello{% endcapture %}", error_mode: :strict2)
end
end
def test_capture_rejects_numeric_variable_name_in_strict2
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse("{% capture 1abc %}hello{% endcapture %}", error_mode: :strict2)
end
end
def test_capture_allows_invalid_names_in_lax
t = Liquid::Template.parse("{% capture (x[y %}hello{% endcapture %}", error_mode: :lax)
assert_equal("(x[y", t.root.nodelist.first.to)
end
end
+4 -6
View File
@@ -265,13 +265,11 @@ class ErrorHandlingTest < Minitest::Test
end
def test_bug_compatible_silencing_of_errors_in_blank_nodes
with_error_modes(:lax, :strict) do
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}not blank{% assign x = 3 %}{% endif %}{{ x }}").render
assert_equal("Liquid error: comparison of Integer with String failed0", output)
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}not blank{% assign x = 3 %}{% endif %}{{ x }}").render
assert_equal("Liquid error: comparison of Integer with String failed0", output)
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}{% assign x = 3 %}{% endif %}{{ x }}").render
assert_equal("0", output)
end
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}{% assign x = 3 %}{% endif %}{{ x }}").render
assert_equal("0", output)
end
def test_syntax_error_is_raised_with_template_name
+21 -31
View File
@@ -44,42 +44,32 @@ class SecurityTest < Minitest::Test
end
def test_does_not_permanently_add_filters_to_symbol_table
assert_no_new_symbols do
# MRI imprecisely marks objects found on the C stack, which can result
# in uninitialized memory being marked. This can even result in the test failing
# deterministically for a given compilation of ruby. Using a separate thread will
# keep these writes of the symbol pointer on a separate stack that will be garbage
# collected after Thread#join.
Thread.new do
test = %( {{ "some_string" | a_bad_filter }} )
Template.parse(test).render!
nil
end.join
current_symbols = Symbol.all_symbols
GC.start
end
# MRI imprecisely marks objects found on the C stack, which can result
# in uninitialized memory being marked. This can even result in the test failing
# deterministically for a given compilation of ruby. Using a separate thread will
# keep these writes of the symbol pointer on a separate stack that will be garbage
# collected after Thread#join.
Thread.new do
test = %( {{ "some_string" | a_bad_filter }} )
Template.parse(test).render!
nil
end.join
GC.start
assert_equal([], Symbol.all_symbols - current_symbols)
end
def test_does_not_add_drop_methods_to_symbol_table
assigns = { 'drop' => Drop.new }
method_names = Array.new(3) { |index| "untrusted_drop_method_#{object_id}_#{index}" }
method_names.each do |method_name|
assert_equal("", Template.parse("{{ drop.#{method_name} }}").render!(assigns))
end
# JITs can intern internal metadata; untrusted Drop method names must not be interned.
assert_equal([], Symbol.all_symbols.map(&:to_s) & method_names)
end
def assert_no_new_symbols
# Run once to trigger any first-time initialization which might create some symbols,
# for example autoload or lazy method parsing might create symbols on first execution.
yield
# Ensure no new symbols for further runs, i.e. the code does not leak symbols
current_symbols = Symbol.all_symbols
yield
assigns = { 'drop' => Drop.new }
assert_equal("", Template.parse("{{ drop.custom_method_1 }}", assigns).render!)
assert_equal("", Template.parse("{{ drop.custom_method_2 }}", assigns).render!)
assert_equal("", Template.parse("{{ drop.custom_method_3 }}", assigns).render!)
assert_equal([], Symbol.all_symbols - current_symbols)
end
-120
View File
@@ -1,120 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class SelfDropContextTest < Minitest::Test
include Liquid
def test_self_drop_passed_as_render_param_preserves_original_scope
source = <<~LIQUID
{%- assign var = 42 -%}
{%- assign s = self -%}
{%- render "snippet1", other_self: s -%}
LIQUID
partials = {
'snippet1' => <<~LIQUID,
{%- assign var = 43 -%}
{{- other_self.var }}|{{ self.var -}}
LIQUID
}
assert_template_result('42|43', source, partials: partials)
end
def test_self_drop_in_render_without_passing_resolves_inner_scope
source = <<~LIQUID
{%- assign var = 42 -%}
{%- render "snippet1" -%}
LIQUID
partials = {
'snippet1' => <<~LIQUID,
{%- assign var = 99 -%}
{{- self.var -}}
LIQUID
}
assert_template_result('99', source, partials: partials)
end
def test_self_drop_passed_to_nested_renders_preserves_each_level
source = <<~LIQUID
{%- assign a = 1 -%}
{%- assign s1 = self -%}
{%- render "snippet1", outer: s1 -%}
LIQUID
partials = {
'snippet1' => <<~LIQUID,
{%- assign a = 2 -%}
{%- assign s2 = self -%}
{%- render "snippet2", outer: outer, middle: s2 -%}
LIQUID
'snippet2' => <<~LIQUID,
{%- assign a = 3 -%}
{{- outer.a }}|{{ middle.a }}|{{ self.a -}}
LIQUID
}
assert_template_result('1|2|3', source, partials: partials)
end
def test_self_drop_reflects_variables_assigned_after_creation
source = <<~LIQUID
{%- assign s = self -%}
{%- assign x = 42 %}{{ s.x -}}
LIQUID
assert_template_result('42', source)
end
def test_self_drop_context_setter_is_undefined
context = Context.new
drop = SelfDrop.new(context)
refute(drop.respond_to?(:context=))
assert_template_result('42', '{{ self.x }}', { 'x' => 42 })
end
def test_self_drop_repeated_lookups_compare_equal_for_same_context
context = Context.new
drop = context.find_variable("self")
cached_drop = context.find_variable("self")
assert_same(drop, cached_drop)
assert_equal(drop.object_id, cached_drop.object_id)
assert_equal(drop, cached_drop)
end
def test_assigned_self_drop_compares_equal_to_itself
assert_template_result('T', '{% assign s = self %}{% if s == s %}T{% else %}F{% endif %}')
end
def test_distinct_self_assignments_compare_equal_for_same_context
assert_template_result('T', '{% assign a = self %}{% assign b = self %}{% if a == b %}T{% else %}F{% endif %}')
end
def test_bare_self_compares_equal_to_bare_self
assert_template_result('T', '{% if self == self %}T{% else %}F{% endif %}')
end
def test_self_drop_with_strict_variables_does_not_raise_for_defined_var
t = Template.parse('{{ self.x }}')
result = t.render({ 'x' => 42 }, strict_variables: true)
assert_equal('42', result)
end
def test_self_drop_with_strict_variables_returns_nil_for_undefined_var
t = Template.parse('{{ self.x }}')
result = t.render({}, strict_variables: true)
assert_equal('', result)
end
def test_self_drop_can_be_passed_as_bare_drop_to_render
t = Template.parse('{{ self.x }}')
drop = SelfDrop.new(Context.new({ 'x' => 42 }))
result = t.render(drop)
assert_equal('42', result)
end
end
-2
View File
@@ -1181,8 +1181,6 @@ class StandardFiltersTest < Minitest::Test
end
def test_all_filters_never_raise_non_liquid_exception
skip("too slow on non-CRuby due to many exceptions") unless RUBY_ENGINE == 'ruby'
test_drop = TestDrop.new(value: "test")
test_drop.context = Context.new
test_enum = TestEnumerable.new
+4 -2
View File
@@ -105,8 +105,10 @@ class CycleTagTest < Minitest::Test
error1 = assert_raises(Liquid::SyntaxError) { Template.parse(template1) }
error2 = assert_raises(Liquid::SyntaxError) { Template.parse(template2) }
assert_match(/Liquid syntax error:/, error1.message)
assert_match(/Liquid syntax error: \[:dot, "."\] is not a valid expression/, error2.message)
expected_error = /Liquid syntax error: \[:dot, "."\] is not a valid expression/
assert_match(expected_error, error1.message)
assert_match(expected_error, error2.message)
end
end
-81
View File
@@ -465,85 +465,4 @@ 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
@@ -27,50 +27,4 @@ class IncrementTagTest < Minitest::Test
'{%decrement starboard %}',
)
end
def test_increment_strict2_rejects_invalid_variable_name
assert_raises(Liquid::SyntaxError) do
Template.parse('{% increment foo bar %}', error_mode: :strict2)
end
end
def test_increment_strict2_rejects_variable_starting_with_number
assert_raises(Liquid::SyntaxError) do
Template.parse('{% increment 11aa %}', error_mode: :strict2)
end
end
def test_increment_strict2_accepts_valid_variable_name
template = Template.parse('{% increment my-var %}', error_mode: :strict2)
assert_equal('0', template.render)
end
def test_decrement_strict2_rejects_invalid_variable_name
assert_raises(Liquid::SyntaxError) do
Template.parse('{% decrement foo bar %}', error_mode: :strict2)
end
end
def test_decrement_strict2_rejects_variable_starting_with_number
assert_raises(Liquid::SyntaxError) do
Template.parse('{% decrement 11aa %}', error_mode: :strict2)
end
end
def test_decrement_strict2_accepts_valid_variable_name
template = Template.parse('{% decrement my-var %}', error_mode: :strict2)
assert_equal('-1', template.render)
end
def test_increment_strict2_rejects_empty_variable_name
assert_raises(Liquid::SyntaxError) do
Template.parse('{% increment %}', error_mode: :strict2)
end
end
def test_decrement_strict2_rejects_empty_variable_name
assert_raises(Liquid::SyntaxError) do
Template.parse('{% decrement %}', error_mode: :strict2)
end
end
end
+4 -4
View File
@@ -174,16 +174,16 @@ class RenderTagTest < Minitest::Test
def test_increment_is_isolated_between_renders
assert_template_result(
'010',
'{% increment port %}{% increment port %}{% render "incr" %}',
partials: { 'incr' => '{% increment port %}' },
'{% increment %}{% increment %}{% render "incr" %}',
partials: { 'incr' => '{% increment %}' },
)
end
def test_decrement_is_isolated_between_renders
assert_template_result(
'-1-2-1',
'{% decrement port %}{% decrement port %}{% render "decr" %}',
partials: { 'decr' => '{% decrement port %}' },
'{% decrement %}{% decrement %}{% render "decr" %}',
partials: { 'decr' => '{% decrement %}' },
)
end
-90
View File
@@ -465,94 +465,4 @@ 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
+1 -1
View File
@@ -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 = 201
t.resource_limits.render_score_limit = 200
assert_equal(" foo " * 100, t.render!)
refute_nil(t.resource_limits.render_score)
end
-9
View File
@@ -32,15 +32,6 @@ module Minitest
module Assertions
include Liquid
# Exact Range fixture for fast-path tests; singleton tripwires must remain
# untouched because the arithmetic path does not materialize or traverse it.
def bounded_integer_range_with_tripwires
range = (1..1000).dup
range.define_singleton_method(:to_a) { raise 'range was materialized' }
range.define_singleton_method(:each) { raise 'range was traversed' }
range
end
def assert_template_result(
expected, template, assigns = {},
message: nil, partials: nil, error_mode: Liquid::Environment.default.error_mode, render_errors: false,
-85
View File
@@ -1,85 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class RangeSliceUnitTest < Minitest::Test
def test_selects_and_reverses_an_integer_range_without_enumerating_it
limits = Liquid::ResourceLimits.new({})
slice = Liquid::RangeSlice.new(bounded_integer_range_with_tripwires, 997, 999, limits)
assert_equal(2, slice.length)
refute(slice.empty?)
slice.reverse!
assert_equal([999, 998], slice.each.to_a)
assert_equal(2, limits.render_score)
end
def test_charges_only_values_yielded_before_a_break
limits = Liquid::ResourceLimits.new({})
slice = Liquid::RangeSlice.new(bounded_integer_range_with_tripwires, 0, nil, limits)
slice.each { break }
assert_equal(1, limits.render_score)
end
def test_non_integer_ranges_are_sliced_without_to_a_and_charge_visited_values
limits = Liquid::ResourceLimits.new({})
range = Class.new(Range) do
def to_a
raise 'range was materialized'
end
end.new('a', 'c')
assert_equal(['b', 'c'], Liquid::Utils.slice_collection_for_iteration(range, 1, nil, limits))
assert_equal(3, limits.render_score)
limited = Liquid::ResourceLimits.new(render_score_limit: 2)
assert_raises(Liquid::MemoryError) do
Liquid::Utils.slice_collection_for_iteration('a'..'z', 10, 11, limited)
end
end
def test_non_integer_range_empty_windows_do_not_visit_a_sentinel_value
limits = Liquid::ResourceLimits.new({})
assert_equal([], Liquid::Utils.slice_collection_for_iteration('a'..'z', 2, 2, limits))
assert_equal(0, limits.render_score)
assert_equal(['a', 'b'], Liquid::Utils.slice_collection_for_iteration('a'..'z', 0, 2, limits))
assert_equal(2, limits.render_score)
end
def test_standard_beginless_range_raises_for_empty_windows
[[0, 0], [1, 0]].each do |from, to|
assert_raises(TypeError) do
Liquid::Utils.slice_collection_for_iteration(Range.new(nil, 3), from, to, Liquid::ResourceLimits.new({}))
end
end
end
def test_custom_range_to_a_is_sliced_with_a_budget
range = Class.new(Range) do
def to_a
[1, 2, 3]
end
end.new(nil, 3)
limits = Liquid::ResourceLimits.new(render_score_limit: 2)
assert_raises(Liquid::MemoryError) do
Liquid::Utils.slice_collection_for_iteration(range, 1, 3, limits, use_range_to_a: true)
end
assert_equal(3, limits.render_score)
end
def test_preserves_slice_bounds_for_negative_offsets_and_limits
limits = Liquid::ResourceLimits.new({})
assert_equal([1, 2], Liquid::RangeSlice.new(1..5, -2, 2, limits).each.to_a)
empty = Liquid::RangeSlice.new(1..5, 2, 1, limits)
assert(empty.empty?)
assert_equal([], empty.each.to_a)
assert_equal(2, limits.render_score) # the empty window performs no work
assert(Liquid::RangeSlice.new(5..1, 0, nil, limits).empty?)
assert_equal([1, 2, 3, 4], Liquid::RangeSlice.new(1...5, 0, nil, limits).each.to_a)
end
end