Compare commits

...

3 Commits

Author SHA1 Message Date
Dylan Thacker-Smith
d5c31861f9 Fix theme runner for benchmarks 2020-08-18 10:04:42 -04:00
Dylan Thacker-Smith
895e63e40a Remove now unnecessary squash_instance_assigns_with_environments
Since it was just there to make sure instance assigns were overriden
when rending more than once with the same template instance.  It is
no longer necessary because the template no longer stores instance_assigns
which need to be overriden.
2020-08-18 10:04:42 -04:00
Dylan Thacker-Smith
219168e89f Remove mutable render state from Liquid::Template
Use Liquid::Context to access this mutable state after rendering or to have
state that is persisted between mutations
2020-08-18 10:04:41 -04:00
8 changed files with 181 additions and 245 deletions

View File

@@ -80,22 +80,24 @@ It is also recommended that you use it in the template editors of existing apps
By default, the renderer doesn't raise or in any other way notify you if some variables or filters are missing, i.e. not passed to the `render` method. By default, the renderer doesn't raise or in any other way notify you if some variables or filters are missing, i.e. not passed to the `render` method.
You can improve this situation by passing `strict_variables: true` and/or `strict_filters: true` options to the `render` method. You can improve this situation by passing `strict_variables: true` and/or `strict_filters: true` options to the `render` method.
When one of these options is set to true, all errors about undefined variables and undefined filters will be stored in `errors` array of a `Liquid::Template` instance. When one of these options is set to true, all errors about undefined variables and undefined filters will be stored in an `errors` array on the `Liquid::Context` instance used for rendering.
Here are some examples: Here are some examples:
```ruby ```ruby
template = Liquid::Template.parse("{{x}} {{y}} {{z.a}} {{z.b}}") template = Liquid::Template.parse("{{x}} {{y}} {{z.a}} {{z.b}}")
template.render({ 'x' => 1, 'z' => { 'a' => 2 } }, { strict_variables: true }) context = Liquid::Context.new({ 'x' => 1, 'z' => { 'a' => 2 } })
template.render(context, { strict_variables: true })
#=> '1 2 ' # when a variable is undefined, it's rendered as nil #=> '1 2 ' # when a variable is undefined, it's rendered as nil
template.errors context.errors
#=> [#<Liquid::UndefinedVariable: Liquid error: undefined variable y>, #<Liquid::UndefinedVariable: Liquid error: undefined variable b>] #=> [#<Liquid::UndefinedVariable: Liquid error: undefined variable y>, #<Liquid::UndefinedVariable: Liquid error: undefined variable b>]
``` ```
```ruby ```ruby
template = Liquid::Template.parse("{{x | filter1 | upcase}}") template = Liquid::Template.parse("{{x | filter1 | upcase}}")
template.render({ 'x' => 'foo' }, { strict_filters: true }) context = Liquid::Context.new({ 'x' => 'foo' })
template.render(context, { strict_filters: true })
#=> '' # when at least one filter in the filter chain is undefined, a whole expression is rendered as nil #=> '' # when at least one filter in the filter chain is undefined, a whole expression is rendered as nil
template.errors context.errors
#=> [#<Liquid::UndefinedFilter: Liquid error: undefined filter filter1>] #=> [#<Liquid::UndefinedFilter: Liquid error: undefined filter filter1>]
``` ```
@@ -111,4 +113,4 @@ template.render!({ 'x' => 1}, { strict_variables: true })
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. 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.
Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns. Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns.

View File

@@ -34,7 +34,6 @@ module Liquid
@strict_variables = false @strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits) @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
@base_scope_depth = 0 @base_scope_depth = 0
squash_instance_assigns_with_environments
self.exception_renderer = Template.default_exception_renderer self.exception_renderer = Template.default_exception_renderer
if rethrow_errors if rethrow_errors
@@ -246,16 +245,5 @@ module Liquid
rescue Liquid::InternalError => exc rescue Liquid::InternalError => exc
exc exc
end end
def squash_instance_assigns_with_environments
@scopes.last.each_key do |k|
@environments.each do |env|
if env.key?(k)
scopes.last[k] = lookup_and_evaluate(env, k)
break
end
end
end
end # squash_instance_assigns_with_environments
end # Context end # Context
end # Liquid end # Liquid

View File

@@ -16,7 +16,7 @@ module Liquid
# #
class Template class Template
attr_accessor :root attr_accessor :root
attr_reader :resource_limits, :warnings attr_reader :warnings
class TagRegistry class TagRegistry
include Enumerable include Enumerable
@@ -106,11 +106,6 @@ module Liquid
end end
end end
def initialize
@rethrow_errors = false
@resource_limits = ResourceLimits.new(Template.default_resource_limits)
end
# Parse source code. # Parse source code.
# Returns self for easy chaining # Returns self for easy chaining
def parse(source, options = {}) def parse(source, options = {})
@@ -123,22 +118,6 @@ module Liquid
self self
end end
def registers
@registers ||= {}
end
def assigns
@assigns ||= {}
end
def instance_assigns
@instance_assigns ||= {}
end
def errors
@errors ||= []
end
# Render takes a hash with local variables. # Render takes a hash with local variables.
# #
# if you use the same filters over and over again consider registering them globally # if you use the same filters over and over again consider registering them globally
@@ -153,37 +132,18 @@ module Liquid
# * <tt>registers</tt> : hash with register variables. Those can be accessed from # * <tt>registers</tt> : hash with register variables. Those can be accessed from
# filters and tags and might be useful to integrate liquid more with its host application # filters and tags and might be useful to integrate liquid more with its host application
# #
def render(*args) def render(assigns_or_context = nil, options = nil)
return '' if @root.nil? return '' if @root.nil?
context = case args.first context = coerce_context(assigns_or_context)
when Liquid::Context
c = args.shift
if @rethrow_errors
c.exception_renderer = ->(_e) { raise }
end
c
when Liquid::Drop
drop = args.shift
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
when Hash
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
when nil
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
else
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
end
output = nil output = nil
context_register = context.registers.is_a?(StaticRegisters) ? context.registers.static : context.registers context_register = context.registers.is_a?(StaticRegisters) ? context.registers.static : context.registers
case args.last case options
when Hash when Hash
options = args.pop output = options[:output] if options[:output]
output = options[:output] if options[:output]
options[:registers]&.each do |key, register| options[:registers]&.each do |key, register|
context_register[key] = register context_register[key] = register
@@ -191,7 +151,7 @@ module Liquid
apply_options_to_context(context, options) apply_options_to_context(context, options)
when Module, Array when Module, Array
context.add_filters(args.pop) context.add_filters(options)
end end
Template.registers.each do |key, register| Template.registers.each do |key, register|
@@ -209,14 +169,15 @@ module Liquid
end end
rescue Liquid::MemoryError => e rescue Liquid::MemoryError => e
context.handle_error(e) context.handle_error(e)
ensure
@errors = context.errors
end end
end end
def render!(*args) def render!(assigns_or_context = nil, options = nil)
@rethrow_errors = true context = coerce_context(assigns_or_context)
render(*args) # rethrow errors
context.exception_renderer = ->(_e) { raise }
render(context, options)
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
@@ -225,6 +186,22 @@ module Liquid
private private
def coerce_context(assigns_or_context)
case assigns_or_context
when Liquid::Context
assigns_or_context
when Liquid::Drop
drop = assigns_or_context
drop.context = Context.build(environments: [drop])
when Hash
Context.build(environments: [assigns_or_context])
when nil
Context.build
else
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
end
end
def tokenize(source) def tokenize(source)
Tokenizer.new(source, @line_numbers) Tokenizer.new(source, @line_numbers)
end end

View File

@@ -73,26 +73,29 @@ class ThemeRunner
private private
def compile_and_render(template, layout, assigns, page_template, template_file) def compile_and_render(template, layout, assigns, page_template)
compiled_test = compile_test(template, layout, assigns, page_template, template_file) assigns = assigns.merge(
'page_title' => 'Page title',
'template' => page_template,
)
compiled_test = compile_test(template, layout, assigns)
assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns) assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns)
compiled_test[:layout].render!(assigns) if layout compiled_test[:layout].render!(assigns) if layout
end end
def compile_all_tests def compile_all_tests
@compiled_tests = [] @compiled_tests = []
each_test do |liquid, layout, assigns, page_template, template_name| each_test do |liquid, layout, assigns, _page_template, _template_name|
@compiled_tests << compile_test(liquid, layout, assigns, page_template, template_name) @compiled_tests << compile_test(liquid, layout, assigns)
end end
@compiled_tests @compiled_tests
end end
def compile_test(template, layout, assigns, page_template, template_file) def compile_test(template, layout, assigns)
tmpl = init_template(page_template, template_file) parsed_template = Liquid::Template.parse(template).dup
parsed_template = tmpl.parse(template).dup
if layout if layout
parsed_layout = tmpl.parse(layout) parsed_layout = Liquid::Template.parse(layout)
{ tmpl: parsed_template, assigns: assigns, layout: parsed_layout } { tmpl: parsed_template, assigns: assigns, layout: parsed_layout }
else else
{ tmpl: parsed_template, assigns: assigns } { tmpl: parsed_template, assigns: assigns }
@@ -107,16 +110,7 @@ class ThemeRunner
@tests.each do |test_hash| @tests.each do |test_hash|
# Compute page_template outside of profiler run, uninteresting to profiler # Compute page_template outside of profiler run, uninteresting to profiler
page_template = File.basename(test_hash[:template_name], File.extname(test_hash[:template_name])) page_template = File.basename(test_hash[:template_name], File.extname(test_hash[:template_name]))
yield(test_hash[:liquid], test_hash[:layout], assigns, page_template, test_hash[:template_name]) yield(test_hash[:liquid], test_hash[:layout], assigns, page_template)
end end
end end
# set up a new Liquid::Template object for use in `compile_and_render` and `compile_test`
def init_template(page_template, template_file)
tmpl = Liquid::Template.new
tmpl.assigns['page_title'] = 'Page title'
tmpl.assigns['template'] = page_template
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
tmpl
end
end end

View File

@@ -40,26 +40,29 @@ class ErrorHandlingTest < Minitest::Test
def test_standard_error def test_standard_error
template = Liquid::Template.parse(' {{ errors.standard_error }} ') template = Liquid::Template.parse(' {{ errors.standard_error }} ')
assert_equal(' Liquid error: standard error ', template.render('errors' => ErrorDrop.new)) context = Liquid::Context.new('errors' => ErrorDrop.new)
assert_equal(' Liquid error: standard error ', template.render(context))
assert_equal(1, template.errors.size) assert_equal(1, context.errors.size)
assert_equal(StandardError, template.errors.first.class) assert_equal(StandardError, context.errors.first.class)
end end
def test_syntax def test_syntax
template = Liquid::Template.parse(' {{ errors.syntax_error }} ') template = Liquid::Template.parse(' {{ errors.syntax_error }} ')
assert_equal(' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new)) context = Liquid::Context.new('errors' => ErrorDrop.new)
assert_equal(' Liquid syntax error: syntax error ', template.render(context))
assert_equal(1, template.errors.size) assert_equal(1, context.errors.size)
assert_equal(SyntaxError, template.errors.first.class) assert_equal(SyntaxError, context.errors.first.class)
end end
def test_argument def test_argument
template = Liquid::Template.parse(' {{ errors.argument_error }} ') template = Liquid::Template.parse(' {{ errors.argument_error }} ')
assert_equal(' Liquid error: argument error ', template.render('errors' => ErrorDrop.new)) context = Liquid::Context.new('errors' => ErrorDrop.new)
assert_equal(' Liquid error: argument error ', template.render(context))
assert_equal(1, template.errors.size) assert_equal(1, context.errors.size)
assert_equal(ArgumentError, template.errors.first.class) assert_equal(ArgumentError, context.errors.first.class)
end end
def test_missing_endtag_parse_time_error def test_missing_endtag_parse_time_error
@@ -78,9 +81,10 @@ class ErrorHandlingTest < Minitest::Test
def test_lax_unrecognized_operator def test_lax_unrecognized_operator
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax) template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax)
assert_equal(' Liquid error: Unknown operator =! ', template.render) context = Liquid::Context.new('errors' => ErrorDrop.new)
assert_equal(1, template.errors.size) assert_equal(' Liquid error: Unknown operator =! ', template.render(context))
assert_equal(Liquid::ArgumentError, template.errors.first.class) assert_equal(1, context.errors.size)
assert_equal(Liquid::ArgumentError, context.errors.first.class)
end end
def test_with_line_numbers_adds_numbers_to_parser_errors def test_with_line_numbers_adds_numbers_to_parser_errors
@@ -202,10 +206,11 @@ class ErrorHandlingTest < Minitest::Test
def test_default_exception_renderer_with_internal_error def test_default_exception_renderer_with_internal_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true) template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
output = template.render('errors' => ErrorDrop.new) context = Liquid::Context.new('errors' => ErrorDrop.new)
output = template.render(context)
assert_equal('This is a runtime error: Liquid error (line 1): internal', output) assert_equal('This is a runtime error: Liquid error (line 1): internal', output)
assert_equal([Liquid::InternalError], template.errors.map(&:class)) assert_equal([Liquid::InternalError], context.errors.map(&:class))
end end
def test_setting_default_exception_renderer def test_setting_default_exception_renderer
@@ -217,10 +222,11 @@ class ErrorHandlingTest < Minitest::Test
} }
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}') template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
output = template.render('errors' => ErrorDrop.new) context = Liquid::Context.new('errors' => ErrorDrop.new)
output = template.render(context)
assert_equal('This is a runtime error: ', output) assert_equal('This is a runtime error: ', output)
assert_equal([Liquid::ArgumentError], template.errors.map(&:class)) assert_equal([Liquid::ArgumentError], context.errors.map(&:class))
ensure ensure
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
end end
@@ -233,11 +239,12 @@ class ErrorHandlingTest < Minitest::Test
e.cause e.cause
} }
output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler) context = Liquid::Context.new('errors' => ErrorDrop.new)
output = template.render(context, exception_renderer: handler)
assert_equal('This is a runtime error: runtime error', output) assert_equal('This is a runtime error: runtime error', output)
assert_equal([Liquid::InternalError], exceptions.map(&:class)) assert_equal([Liquid::InternalError], exceptions.map(&:class))
assert_equal(exceptions, template.errors) assert_equal(exceptions, context.errors)
assert_equal('#<RuntimeError: runtime error>', exceptions.first.cause.inspect) assert_equal('#<RuntimeError: runtime error>', exceptions.first.cause.inspect)
end end
@@ -250,15 +257,16 @@ class ErrorHandlingTest < Minitest::Test
def test_included_template_name_with_line_numbers def test_included_template_name_with_line_numbers
old_file_system = Liquid::Template.file_system old_file_system = Liquid::Template.file_system
context = Liquid::Context.new('errors' => ErrorDrop.new)
begin begin
Liquid::Template.file_system = TestFileSystem.new Liquid::Template.file_system = TestFileSystem.new
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true) template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true)
page = template.render('errors' => ErrorDrop.new) page = template.render(context)
ensure ensure
Liquid::Template.file_system = old_file_system Liquid::Template.file_system = old_file_system
end end
assert_equal("Argument error:\nLiquid error (product line 1): argument error", page) assert_equal("Argument error:\nLiquid error (product line 1): argument error", page)
assert_equal("product", template.errors.first.template_name) assert_equal("product", context.errors.first.template_name)
end end
end end

View File

@@ -217,8 +217,9 @@ class IncludeTagTest < Minitest::Test
Liquid::Template.file_system = TestFileSystem.new Liquid::Template.file_system = TestFileSystem.new
a = Liquid::Template.parse(' {% include "nested_template" %}') a = Liquid::Template.parse(' {% include "nested_template" %}')
a.render! context = Liquid::Context.new
assert_empty(a.errors) a.render!(context)
assert_empty(context.errors)
end end
def test_passing_options_to_included_templates def test_passing_options_to_included_templates
@@ -257,9 +258,10 @@ class IncludeTagTest < Minitest::Test
def test_including_with_strict_variables def test_including_with_strict_variables
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn) template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn)
template.render(nil, strict_variables: true) context = Liquid::Context.new
template.render(context, strict_variables: true)
assert_equal([], template.errors) assert_equal([], context.errors)
end end
def test_break_through_include def test_break_through_include

View File

@@ -38,12 +38,6 @@ end
class TemplateTest < Minitest::Test class TemplateTest < Minitest::Test
include Liquid include Liquid
def test_instance_assigns_persist_on_same_template_object_between_parses
t = Template.new
assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!)
assert_equal('from instance assigns', t.parse("{{ foo }}").render!)
end
def test_warnings_is_not_exponential_time def test_warnings_is_not_exponential_time
str = "false" str = "false"
100.times do 100.times do
@@ -54,43 +48,15 @@ class TemplateTest < Minitest::Test
assert_equal([], Timeout.timeout(1) { t.warnings }) assert_equal([], Timeout.timeout(1) { t.warnings })
end end
def test_instance_assigns_persist_on_same_template_parsing_between_renders
t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}")
assert_equal('foo', t.render!)
assert_equal('foofoo', t.render!)
end
def test_custom_assigns_do_not_persist_on_same_template def test_custom_assigns_do_not_persist_on_same_template
t = Template.new t = Template.new
assert_equal('from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns')) assert_equal('from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns'))
assert_equal('', t.parse("{{ foo }}").render!) assert_equal('', t.parse("{{ foo }}").render!)
end
def test_custom_assigns_squash_instance_assigns
t = Template.new
assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!) assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!)
assert_equal('from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns')) assert_equal('from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns'))
end end
def test_persistent_assigns_squash_instance_assigns
t = Template.new
assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!)
t.assigns['foo'] = 'from persistent assigns'
assert_equal('from persistent assigns', t.parse("{{ foo }}").render!)
end
def test_lambda_is_called_once_from_persistent_assigns_over_multiple_parses_and_renders
t = Template.new
t.assigns['number'] = -> {
@global ||= 0
@global += 1
}
assert_equal('1', t.parse("{{number}}").render!)
assert_equal('1', t.parse("{{number}}").render!)
assert_equal('1', t.render!)
@global = nil
end
def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders
t = Template.new t = Template.new
assigns = { 'number' => -> { assigns = { 'number' => -> {
@@ -105,112 +71,124 @@ class TemplateTest < Minitest::Test
def test_resource_limits_works_with_custom_length_method def test_resource_limits_works_with_custom_length_method
t = Template.parse("{% assign foo = bar %}") t = Template.parse("{% assign foo = bar %}")
t.resource_limits.render_length_limit = 42 context = Liquid::Context.new("bar" => SomethingWithLength.new)
assert_equal("", t.render!("bar" => SomethingWithLength.new)) context.resource_limits.render_length_limit = 42
assert_equal("", t.render!(context))
end end
def test_resource_limits_render_length def test_resource_limits_render_length
t = Template.parse("0123456789") t = Template.parse("0123456789")
t.resource_limits.render_length_limit = 5 context = Liquid::Context.new
assert_equal("Liquid error: Memory limits exceeded", t.render) context.resource_limits.render_length_limit = 5
assert(t.resource_limits.reached?) assert_equal("Liquid error: Memory limits exceeded", t.render(context))
assert(context.resource_limits.reached?)
t.resource_limits.render_length_limit = 10 context.resource_limits.render_length_limit = 10
assert_equal("0123456789", t.render!) assert_equal("0123456789", t.render!)
refute_nil(t.resource_limits.render_length) refute_nil(context.resource_limits.render_length)
end end
def test_resource_limits_render_score def test_resource_limits_render_score
t = Template.parse("{% for a in (1..10) %} {% for a in (1..10) %} foo {% endfor %} {% endfor %}") t = Template.parse("{% for a in (1..10) %} {% for a in (1..10) %} foo {% endfor %} {% endfor %}")
t.resource_limits.render_score_limit = 50 context = Liquid::Context.new
assert_equal("Liquid error: Memory limits exceeded", t.render) context.resource_limits.render_score_limit = 50
assert(t.resource_limits.reached?) assert_equal("Liquid error: Memory limits exceeded", t.render(context))
assert(context.resource_limits.reached?)
t = Template.parse("{% for a in (1..100) %} foo {% endfor %}") t = Template.parse("{% for a in (1..100) %} foo {% endfor %}")
t.resource_limits.render_score_limit = 50 context.resource_limits.render_score_limit = 50
assert_equal("Liquid error: Memory limits exceeded", t.render) assert_equal("Liquid error: Memory limits exceeded", t.render(context))
assert(t.resource_limits.reached?) assert(context.resource_limits.reached?)
t.resource_limits.render_score_limit = 200 context.resource_limits.render_score_limit = 200
assert_equal((" foo " * 100), t.render!) assert_equal((" foo " * 100), t.render!(context))
refute_nil(t.resource_limits.render_score) refute_nil(context.resource_limits.render_score)
end end
def test_resource_limits_assign_score def test_resource_limits_assign_score
t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}") t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}")
t.resource_limits.assign_score_limit = 1 context = Liquid::Context.new
assert_equal("Liquid error: Memory limits exceeded", t.render) context.resource_limits.assign_score_limit = 1
assert(t.resource_limits.reached?) assert_equal("Liquid error: Memory limits exceeded", t.render(context))
assert(context.resource_limits.reached?)
t.resource_limits.assign_score_limit = 2 context.resource_limits.assign_score_limit = 2
assert_equal("", t.render!) assert_equal("", t.render!(context))
refute_nil(t.resource_limits.assign_score) refute_nil(context.resource_limits.assign_score)
end end
def test_resource_limits_assign_score_counts_bytes_not_characters def test_resource_limits_assign_score_counts_bytes_not_characters
t = Template.parse("{% assign foo = 'すごい' %}") t = Template.parse("{% assign foo = 'すごい' %}")
t.render context = Liquid::Context.new
assert_equal(9, t.resource_limits.assign_score) t.render(context)
assert_equal(9, context.resource_limits.assign_score)
t = Template.parse("{% capture foo %}すごい{% endcapture %}") t = Template.parse("{% capture foo %}すごい{% endcapture %}")
t.render t.render(context)
assert_equal(9, t.resource_limits.assign_score) assert_equal(9, context.resource_limits.assign_score)
end end
def test_resource_limits_assign_score_nested def test_resource_limits_assign_score_nested
t = Template.parse("{% assign foo = 'aaaa' | reverse %}") t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
t.resource_limits.assign_score_limit = 3 context = Liquid::Context.new
assert_equal("Liquid error: Memory limits exceeded", t.render) context.resource_limits.assign_score_limit = 3
assert(t.resource_limits.reached?) assert_equal("Liquid error: Memory limits exceeded", t.render(context))
assert(context.resource_limits.reached?)
t.resource_limits.assign_score_limit = 5 context.resource_limits.assign_score_limit = 5
assert_equal("", t.render!) assert_equal("", t.render!(context))
end end
def test_resource_limits_aborts_rendering_after_first_error def test_resource_limits_aborts_rendering_after_first_error
t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}") t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
t.resource_limits.render_score_limit = 50 context = Liquid::Context.new
assert_equal("Liquid error: Memory limits exceeded", t.render) context.resource_limits.render_score_limit = 50
assert(t.resource_limits.reached?) assert_equal("Liquid error: Memory limits exceeded", t.render(context))
assert(context.resource_limits.reached?)
end end
def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set
t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}") t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}")
t.render! context = Liquid::Context.new
assert(t.resource_limits.assign_score > 0) t.render!(context)
assert(t.resource_limits.render_score > 0) assert(context.resource_limits.assign_score > 0)
assert(t.resource_limits.render_length > 0) assert(context.resource_limits.render_score > 0)
assert(context.resource_limits.render_length > 0)
end end
def test_render_length_persists_between_blocks def test_render_length_persists_between_blocks
t = Template.parse("{% if true %}aaaa{% endif %}") t = Template.parse("{% if true %}aaaa{% endif %}")
t.resource_limits.render_length_limit = 7 context = Liquid::Context.new
assert_equal("Liquid error: Memory limits exceeded", t.render) context.resource_limits.render_length_limit = 7
t.resource_limits.render_length_limit = 8 assert_equal("Liquid error: Memory limits exceeded", t.render(context))
assert_equal("aaaa", t.render) context.resource_limits.render_length_limit = 8
assert_equal("aaaa", t.render(context))
t = Template.parse("{% if true %}aaaa{% endif %}{% if true %}bbb{% endif %}") t = Template.parse("{% if true %}aaaa{% endif %}{% if true %}bbb{% endif %}")
t.resource_limits.render_length_limit = 13 context = Liquid::Context.new
assert_equal("Liquid error: Memory limits exceeded", t.render) context.resource_limits.render_length_limit = 13
t.resource_limits.render_length_limit = 14 assert_equal("Liquid error: Memory limits exceeded", t.render(context))
assert_equal("aaaabbb", t.render) context.resource_limits.render_length_limit = 14
assert_equal("aaaabbb", t.render(context))
t = Template.parse("{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}") t = Template.parse("{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}")
t.resource_limits.render_length_limit = 5 context = Liquid::Context.new
assert_equal("Liquid error: Memory limits exceeded", t.render) context.resource_limits.render_length_limit = 5
t.resource_limits.render_length_limit = 11 assert_equal("Liquid error: Memory limits exceeded", t.render(context))
assert_equal("Liquid error: Memory limits exceeded", t.render) context.resource_limits.render_length_limit = 11
t.resource_limits.render_length_limit = 12 assert_equal("Liquid error: Memory limits exceeded", t.render(context))
assert_equal("ababab", t.render) context.resource_limits.render_length_limit = 12
assert_equal("ababab", t.render(context))
end end
def test_render_length_uses_number_of_bytes_not_characters def test_render_length_uses_number_of_bytes_not_characters
t = Template.parse("{% if true %}すごい{% endif %}") t = Template.parse("{% if true %}すごい{% endif %}")
t.resource_limits.render_length_limit = 10 context = Liquid::Context.new
assert_equal("Liquid error: Memory limits exceeded", t.render) context.resource_limits.render_length_limit = 10
t.resource_limits.render_length_limit = 18 assert_equal("Liquid error: Memory limits exceeded", t.render(context))
assert_equal("すごい", t.render) context.resource_limits.render_length_limit = 18
assert_equal("すごい", t.render(context))
end end
def test_default_resource_limits_unaffected_by_render_with_context def test_default_resource_limits_unaffected_by_render_with_context
@@ -224,11 +202,12 @@ class TemplateTest < Minitest::Test
def test_can_use_drop_as_context def test_can_use_drop_as_context
t = Template.new t = Template.new
t.registers['lulz'] = 'haha'
drop = TemplateContextDrop.new drop = TemplateContextDrop.new
assert_equal('fizzbuzz', t.parse('{{foo}}').render!(drop)) context = Liquid::Context.build(environments: drop, registers: { 'lulz' => 'haha' })
assert_equal('bar', t.parse('{{bar}}').render!(drop)) drop.context = context
assert_equal('haha', t.parse("{{baz}}").render!(drop)) assert_equal('fizzbuzz', t.parse('{{foo}}').render!(context))
assert_equal('bar', t.parse('{{bar}}').render!(context))
assert_equal('haha', t.parse("{{baz}}").render!(context))
end end
def test_render_bang_force_rethrow_errors_on_passed_context def test_render_bang_force_rethrow_errors_on_passed_context
@@ -280,25 +259,27 @@ class TemplateTest < Minitest::Test
end end
def test_undefined_variables def test_undefined_variables
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}") t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true) context = Liquid::Context.new('x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } })
result = t.render(context, strict_variables: true)
assert_equal('33 32 ', result) assert_equal('33 32 ', result)
assert_equal(3, t.errors.count) assert_equal(3, context.errors.count)
assert_instance_of(Liquid::UndefinedVariable, t.errors[0]) assert_instance_of(Liquid::UndefinedVariable, context.errors[0])
assert_equal('Liquid error: undefined variable y', t.errors[0].message) assert_equal('Liquid error: undefined variable y', context.errors[0].message)
assert_instance_of(Liquid::UndefinedVariable, t.errors[1]) assert_instance_of(Liquid::UndefinedVariable, context.errors[1])
assert_equal('Liquid error: undefined variable b', t.errors[1].message) assert_equal('Liquid error: undefined variable b', context.errors[1].message)
assert_instance_of(Liquid::UndefinedVariable, t.errors[2]) assert_instance_of(Liquid::UndefinedVariable, context.errors[2])
assert_equal('Liquid error: undefined variable d', t.errors[2].message) assert_equal('Liquid error: undefined variable d', context.errors[2].message)
end end
def test_nil_value_does_not_raise def test_nil_value_does_not_raise
Liquid::Template.error_mode = :strict Liquid::Template.error_mode = :strict
t = Template.parse("some{{x}}thing") t = Template.parse("some{{x}}thing")
result = t.render!({ 'x' => nil }, strict_variables: true) context = Liquid::Context.new('x' => nil)
result = t.render!(context, strict_variables: true)
assert_equal(0, t.errors.count) assert_equal(0, context.errors.count)
assert_equal('something', result) assert_equal('something', result)
end end
@@ -313,11 +294,13 @@ class TemplateTest < Minitest::Test
def test_undefined_drop_methods def test_undefined_drop_methods
d = DropWithUndefinedMethod.new d = DropWithUndefinedMethod.new
t = Template.new.parse('{{ foo }} {{ woot }}') t = Template.new.parse('{{ foo }} {{ woot }}')
result = t.render(d, strict_variables: true) context = Liquid::Context.new(d)
d.context = context
result = t.render(context, strict_variables: true)
assert_equal('foo ', result) assert_equal('foo ', result)
assert_equal(1, t.errors.count) assert_equal(1, context.errors.count)
assert_instance_of(Liquid::UndefinedDropMethod, t.errors[0]) assert_instance_of(Liquid::UndefinedDropMethod, context.errors[0])
end end
def test_undefined_drop_methods_raise def test_undefined_drop_methods_raise
@@ -336,12 +319,13 @@ class TemplateTest < Minitest::Test
"-#{v}-" "-#{v}-"
end end
end end
result = t.render({ 'a' => 123, 'x' => 'foo' }, filters: [filters], strict_filters: true) context = Liquid::Context.new('a' => 123, 'x' => 'foo')
result = t.render(context, filters: [filters], strict_filters: true)
assert_equal('123 ', result) assert_equal('123 ', result)
assert_equal(1, t.errors.count) assert_equal(1, context.errors.count)
assert_instance_of(Liquid::UndefinedFilter, t.errors[0]) assert_instance_of(Liquid::UndefinedFilter, context.errors[0])
assert_equal('Liquid error: undefined filter somefilter1', t.errors[0].message) assert_equal('Liquid error: undefined filter somefilter1', context.errors[0].message)
end end
def test_undefined_filters_raise def test_undefined_filters_raise

View File

@@ -51,29 +51,10 @@ class VariableTest < Minitest::Test
assert_equal('cat', Template.parse("{{ nil | append: 'cat' }}").render!) assert_equal('cat', Template.parse("{{ nil | append: 'cat' }}").render!)
end end
def test_preset_assigns
template = Template.parse(%({{ test }}))
template.assigns['test'] = 'worked'
assert_equal('worked', template.render!)
end
def test_reuse_parsed_template def test_reuse_parsed_template
template = Template.parse(%({{ greeting }} {{ name }})) template = Template.parse(%({{ greeting }} {{ name }}))
template.assigns['greeting'] = 'Goodbye'
assert_equal('Hello Tobi', template.render!('greeting' => 'Hello', 'name' => 'Tobi')) assert_equal('Hello Tobi', template.render!('greeting' => 'Hello', 'name' => 'Tobi'))
assert_equal('Hello ', template.render!('greeting' => 'Hello', 'unknown' => 'Tobi')) assert_equal('Goodbye Brian', template.render!('greeting' => 'Goodbye', 'name' => 'Brian'))
assert_equal('Hello Brian', template.render!('greeting' => 'Hello', 'name' => 'Brian'))
assert_equal('Goodbye Brian', template.render!('name' => 'Brian'))
assert_equal({ 'greeting' => 'Goodbye' }, template.assigns)
end
def test_assigns_not_polluted_from_template
template = Template.parse(%({{ test }}{% assign test = 'bar' %}{{ test }}))
template.assigns['test'] = 'baz'
assert_equal('bazbar', template.render!)
assert_equal('bazbar', template.render!)
assert_equal('foobar', template.render!('test' => 'foo'))
assert_equal('bazbar', template.render!)
end end
def test_hash_with_default_proc def test_hash_with_default_proc