Compare commits

..

1 Commits

Author SHA1 Message Date
Maarten van Grootel
d7e7285fcb Fix leaking error when comparison leads to TypeError 2017-01-16 12:26:19 -05:00
26 changed files with 108 additions and 261 deletions

View File

@@ -19,10 +19,6 @@ matrix:
allow_failures: allow_failures:
- rvm: jruby-head - rvm: jruby-head
install:
- gem install rainbow -v 2.2.1
- bundle install
script: "bundle exec rake" script: "bundle exec rake"
notifications: notifications:

View File

@@ -1,14 +1,11 @@
source 'https://rubygems.org' source 'https://rubygems.org'
gemspec gemspec
gem 'stackprof', platforms: :mri_21
gem 'stackprof', platforms: :mri
group :benchmark, :test do
gem 'benchmark-ips'
end
group :test do group :test do
gem 'spy', '0.4.1'
gem 'benchmark-ips'
gem 'rubocop', '0.34.2' gem 'rubocop', '0.34.2'
platform :mri do platform :mri do

View File

@@ -20,13 +20,10 @@
* Add concat filter to concatenate arrays (#429) [Diogo Beato] * Add concat filter to concatenate arrays (#429) [Diogo Beato]
* Ruby 1.9 support dropped (#491) [Justin Li] * Ruby 1.9 support dropped (#491) [Justin Li]
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith] * Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
* Remove `liquid_methods` (See https://github.com/Shopify/liquid/pull/568 for replacement) * Remove support for `liquid_methods`
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande] * Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
### Fixed ### Fixed
* Fix variable names being detected as an operator when starting with contains (#788) [Michael Angell]
* Fix include tag used with strict_variables (#828) [QuickPay]
* Fix map filter when value is a Proc (#672) [Guillaume Malette] * Fix map filter when value is a Proc (#672) [Guillaume Malette]
* Fix truncate filter when value is not a string (#672) [Guillaume Malette] * Fix truncate filter when value is not a string (#672) [Guillaume Malette]
* Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo] * Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo]

View File

@@ -1,7 +1,5 @@
module Liquid module Liquid
class Block < Tag class Block < Tag
MAX_DEPTH = 100
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@blank = true @blank = true
@@ -50,11 +48,6 @@ module Liquid
protected protected
def parse_body(body, tokens) def parse_body(body, tokens)
if parse_context.depth >= MAX_DEPTH
raise StackLevelError, "Nesting too deep".freeze
end
parse_context.depth += 1
begin
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params| body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
@blank &&= body.blank? @blank &&= body.blank?
@@ -67,9 +60,6 @@ module Liquid
# pass it to the current block for special handling or error reporting # pass it to the current block for special handling or error reporting
unknown_tag(end_tag_name, end_tag_params, tokens) unknown_tag(end_tag_name, end_tag_params, tokens)
end end
ensure
parse_context.depth -= 1
end
true true
end end

View File

@@ -93,11 +93,10 @@ module Liquid
rescue MemoryError => e rescue MemoryError => e
raise e raise e
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, token.line_number) context.handle_error(e, token.line_number, token.raw)
output << nil output << nil
rescue ::StandardError => e rescue ::StandardError => e
line_number = token.is_a?(String) ? nil : token.line_number output << context.handle_error(e, token.line_number, token.raw)
output << context.handle_error(e, line_number)
end end
end end
@@ -107,7 +106,7 @@ module Liquid
private private
def render_node(node, context) def render_node(node, context)
node_output = node.is_a?(String) ? node : node.render(context) node_output = (node.respond_to?(:render) ? node.render(context) : node)
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
context.resource_limits.render_length += node_output.length context.resource_limits.render_length += node_output.length

View File

@@ -41,23 +41,17 @@ module Liquid
end end
def evaluate(context = Context.new) def evaluate(context = Context.new)
condition = self result = interpret_condition(left, right, operator, context)
result = nil
loop do
result = interpret_condition(condition.left, condition.right, condition.operator, context)
case condition.child_relation case @child_relation
when :or when :or
break if result result || @child_condition.evaluate(context)
when :and when :and
break unless result result && @child_condition.evaluate(context)
else else
break
end
condition = condition.child_condition
end
result result
end end
end
def or(condition) def or(condition)
@child_relation = :or @child_relation = :or
@@ -81,10 +75,6 @@ module Liquid
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>" "#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
end end
protected
attr_reader :child_relation, :child_condition
private private
def equal_variables(left, right) def equal_variables(left, right)
@@ -120,10 +110,10 @@ module Liquid
if operation.respond_to?(:call) if operation.respond_to?(:call)
operation.call(self, left, right) operation.call(self, left, right)
elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash) elsif left.respond_to?(operation) && right.respond_to?(operation)
begin begin
left.send(operation, right) left.send(operation, right)
rescue ::ArgumentError => e rescue ::ArgumentError, TypeError => e
raise Liquid::ArgumentError.new(e.message) raise Liquid::ArgumentError.new(e.message)
end end
end end

View File

@@ -74,7 +74,7 @@ module Liquid
@interrupts.pop @interrupts.pop
end end
def handle_error(e, line_number = nil) def handle_error(e, line_number = nil, raw_token = nil)
e = internal_error unless e.is_a?(Liquid::Error) e = internal_error unless e.is_a?(Liquid::Error)
e.template_name ||= template_name e.template_name ||= template_name
e.line_number ||= line_number e.line_number ||= line_number
@@ -89,7 +89,7 @@ module Liquid
# Push new local scope on the stack. use <tt>Context#stack</tt> instead # Push new local scope on the stack. use <tt>Context#stack</tt> instead
def push(new_scope = {}) def push(new_scope = {})
@scopes.unshift(new_scope) @scopes.unshift(new_scope)
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100
end end
# Merge a hash of variables in the current local scope # Merge a hash of variables in the current local scope
@@ -160,7 +160,7 @@ module Liquid
end end
# Fetches an object starting at the local scope and then moving up the hierachy # Fetches an object starting at the local scope and then moving up the hierachy
def find_variable(key, raise_on_not_found: true) def find_variable(key)
# This was changed from find() to find_index() because this is a very hot # This was changed from find() to find_index() because this is a very hot
# path and find_index() is optimized in MRI to reduce object allocation # path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.key?(key) } index = @scopes.find_index { |s| s.key?(key) }
@@ -170,7 +170,7 @@ module Liquid
if scope.nil? if scope.nil?
@environments.each do |e| @environments.each do |e|
variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found) variable = lookup_and_evaluate(e, key)
unless variable.nil? unless variable.nil?
scope = e scope = e
break break
@@ -179,7 +179,7 @@ module Liquid
end end
scope ||= @environments.last || @scopes.last scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found) variable ||= lookup_and_evaluate(scope, key)
variable = variable.to_liquid variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=) variable.context = self if variable.respond_to?(:context=)
@@ -187,8 +187,8 @@ module Liquid
variable variable
end end
def lookup_and_evaluate(obj, key, raise_on_not_found: true) def lookup_and_evaluate(obj, key)
if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key) if @strict_variables && obj.respond_to?(:key?) && !obj.key?(key)
raise Liquid::UndefinedVariable, "undefined variable #{key}" raise Liquid::UndefinedVariable, "undefined variable #{key}"
end end

View File

@@ -18,10 +18,10 @@ module Liquid
DOUBLE_STRING_LITERAL = /"[^\"]*"/ DOUBLE_STRING_LITERAL = /"[^\"]*"/
NUMBER_LITERAL = /-?\d+(\.\d+)?/ NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./ DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/
def initialize(input) def initialize(input)
@ss = StringScanner.new(input) @ss = StringScanner.new(input.rstrip)
end end
def tokenize def tokenize
@@ -29,7 +29,6 @@ module Liquid
until @ss.eos? until @ss.eos?
@ss.skip(/\s*/) @ss.skip(/\s*/)
break if @ss.eos?
tok = case tok = case
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t] when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t] when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]

View File

@@ -1,13 +1,12 @@
module Liquid module Liquid
class ParseContext class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth attr_accessor :locale, :line_number, :trim_whitespace
attr_reader :partial, :warnings, :error_mode attr_reader :partial, :warnings, :error_mode
def initialize(options = {}) def initialize(options = {})
@template_options = options ? options.dup : {} @template_options = options ? options.dup : {}
@locale = @template_options[:locale] ||= I18n.new @locale = @template_options[:locale] ||= I18n.new
@warnings = [] @warnings = []
self.depth = 0
self.partial = false self.partial = false
end end

View File

@@ -33,7 +33,7 @@ module Liquid
end end
def escape(input) def escape(input)
CGI.escapeHTML(input.to_s).untaint unless input.nil? CGI.escapeHTML(input).untaint unless input.nil?
end end
alias_method :h, :escape alias_method :h, :escape
@@ -42,11 +42,11 @@ module Liquid
end end
def url_encode(input) def url_encode(input)
CGI.escape(input.to_s) unless input.nil? CGI.escape(input) unless input.nil?
end end
def url_decode(input) def url_decode(input)
CGI.unescape(input.to_s) unless input.nil? CGI.unescape(input) unless input.nil?
end end
def slice(input, offset, length = nil) def slice(input, offset, length = nil)
@@ -384,7 +384,7 @@ module Liquid
end end
def join(glue) def join(glue)
to_a.join(glue.to_s) to_a.join(glue)
end end
def concat(args) def concat(args)

View File

@@ -27,7 +27,7 @@ module Liquid
def self.add_filter(filter) def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module) raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
unless self.include?(filter) unless self.class.include?(filter)
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) } invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any? if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}" raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"

View File

@@ -23,7 +23,7 @@ module Liquid
# {{ item.name }} # {{ item.name }}
# {% end %} # {% end %}
# #
# To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`) # To reverse the for loop simply use {% for item in collection reversed %}
# #
# == Available variables: # == Available variables:
# #
@@ -46,9 +46,6 @@ module Liquid
class For < Block class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
attr_reader :collection_name
attr_reader :variable_name
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@from = @limit = nil @from = @limit = nil
@@ -129,7 +126,7 @@ module Liquid
end end
collection = context.evaluate(@collection_name) collection = context.evaluate(@collection_name)
collection = collection.step(1).to_a if collection.is_a?(Range) collection = collection.to_a if collection.is_a?(Range)
limit = context.evaluate(@limit) limit = context.evaluate(@limit)
to = limit ? limit.to_i + from : nil to = limit ? limit.to_i + from : nil

View File

@@ -83,20 +83,17 @@ module Liquid
def strict_parse(markup) def strict_parse(markup)
p = Parser.new(markup) p = Parser.new(markup)
condition = parse_binary_comparisons(p) condition = parse_binary_comparison(p)
p.consume(:end_of_string) p.consume(:end_of_string)
condition condition
end end
def parse_binary_comparisons(p) def parse_binary_comparison(p)
condition = parse_comparison(p) condition = parse_comparison(p)
first_condition = condition if op = (p.id?('and'.freeze) || p.id?('or'.freeze))
while op = (p.id?('and'.freeze) || p.id?('or'.freeze)) condition.send(op, parse_binary_comparison(p))
child_condition = parse_comparison(p)
condition.send(op, child_condition)
condition = child_condition
end end
first_condition condition
end end
def parse_comparison(p) def parse_comparison(p)

View File

@@ -50,7 +50,7 @@ module Liquid
variable = if @variable_name_expr variable = if @variable_name_expr
context.evaluate(@variable_name_expr) context.evaluate(@variable_name_expr)
else else
context.find_variable(template_name, raise_on_not_found: false) context.find_variable(template_name)
end end
old_template_name = context.template_name old_template_name = context.template_name

View File

@@ -5,7 +5,7 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
Benchmark.ips do |x| Benchmark.ips do |x|
x.time = 10 x.time = 60
x.warmup = 5 x.warmup = 5
puts puts
@@ -13,6 +13,5 @@ Benchmark.ips do |x|
puts puts
x.report("parse:") { profiler.compile } x.report("parse:") { profiler.compile }
x.report("render:") { profiler.render } x.report("parse & run:") { profiler.run }
x.report("parse & render:") { profiler.run }
end end

View File

@@ -21,100 +21,53 @@ class ThemeRunner
end end
end end
# Initialize a new liquid ThemeRunner instance # Load all templates into memory, do this now so that
# Will load all templates into memory, do this now so that we don't profile IO. # we don't profile IO.
def initialize def initialize
@tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test| @tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test|
next if File.basename(test) == 'theme.liquid' next if File.basename(test) == 'theme.liquid'
theme_path = File.dirname(test) + '/theme.liquid' theme_path = File.dirname(test) + '/theme.liquid'
{
liquid: File.read(test), [File.read(test), (File.file?(theme_path) ? File.read(theme_path) : nil), test]
layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
template_name: test
}
end.compact end.compact
compile_all_tests
end end
# `compile` will test just the compilation portion of liquid without any templates
def compile def compile
@tests.each do |test_hash| # Dup assigns because will make some changes to them
Liquid::Template.new.parse(test_hash[:liquid])
Liquid::Template.new.parse(test_hash[:layout]) @tests.each do |liquid, layout, template_name|
tmpl = Liquid::Template.new
tmpl.parse(liquid)
tmpl = Liquid::Template.new
tmpl.parse(layout)
end end
end end
# `run` is called to benchmark rendering and compiling at the same time
def run def run
each_test do |liquid, layout, assigns, page_template, template_name| # Dup assigns because will make some changes to them
assigns = Database.tables.dup
@tests.each do |liquid, layout, template_name|
# Compute page_tempalte outside of profiler run, uninteresting to profiler
page_template = File.basename(template_name, File.extname(template_name))
compile_and_render(liquid, layout, assigns, page_template, template_name) compile_and_render(liquid, layout, assigns, page_template, template_name)
end end
end end
# `render` is called to benchmark just the render portion of liquid
def render
@compiled_tests.each do |test|
tmpl = test[:tmpl]
assigns = test[:assigns]
layout = test[:layout]
if layout
assigns['content_for_layout'] = tmpl.render!(assigns)
layout.render!(assigns)
else
tmpl.render!(assigns)
end
end
end
private
def compile_and_render(template, layout, assigns, page_template, template_file) def compile_and_render(template, layout, assigns, page_template, template_file)
compiled_test = compile_test(template, layout, assigns, page_template, template_file)
assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns)
compiled_test[:layout].render!(assigns) if layout
end
def compile_all_tests
@compiled_tests = []
each_test do |liquid, layout, assigns, page_template, template_name|
@compiled_tests << compile_test(liquid, layout, assigns, page_template, template_name)
end
@compiled_tests
end
def compile_test(template, layout, assigns, page_template, template_file)
tmpl = init_template(page_template, template_file)
parsed_template = tmpl.parse(template).dup
if layout
parsed_layout = tmpl.parse(layout)
{ tmpl: parsed_template, assigns: assigns, layout: parsed_layout }
else
{ tmpl: parsed_template, assigns: assigns }
end
end
# utility method with similar functionality needed in `compile_all_tests` and `run`
def each_test
# Dup assigns because will make some changes to them
assigns = Database.tables.dup
@tests.each do |test_hash|
# Compute page_template outside of profiler run, uninteresting to profiler
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])
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 = Liquid::Template.new
tmpl.assigns['page_title'] = 'Page title' tmpl.assigns['page_title'] = 'Page title'
tmpl.assigns['template'] = page_template tmpl.assigns['template'] = page_template
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file)) tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
tmpl
content_for_layout = tmpl.parse(template).render!(assigns)
if layout
assigns['content_for_layout'] = content_for_layout
tmpl.parse(layout).render!(assigns)
else
content_for_layout
end
end end
end end

View File

@@ -115,8 +115,4 @@ class ParsingQuirksTest < Minitest::Test
assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}") assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}")
end end
end end
def test_contains_in_id
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true)
end
end # ParsingQuirksTest end # ParsingQuirksTest

View File

@@ -63,18 +63,4 @@ class SecurityTest < Minitest::Test
assert_equal [], (Symbol.all_symbols - current_symbols) assert_equal [], (Symbol.all_symbols - current_symbols)
end end
def test_max_depth_nested_blocks_does_not_raise_exception
depth = Liquid::Block::MAX_DEPTH
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
assert_equal "rendered", Template.parse(code).render!
end
def test_more_than_max_depth_nested_blocks_raises_exception
depth = Liquid::Block::MAX_DEPTH + 1
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
assert_raises(Liquid::StackLevelError) do
Template.parse(code).render!
end
end
end # SecurityTest end # SecurityTest

View File

@@ -128,16 +128,8 @@ class StandardFiltersTest < Minitest::Test
def test_escape def test_escape
assert_equal '&lt;strong&gt;', @filters.escape('<strong>') assert_equal '&lt;strong&gt;', @filters.escape('<strong>')
assert_equal '1', @filters.escape(1) assert_equal nil, @filters.escape(nil)
assert_equal '2001-02-03', @filters.escape(Date.new(2001, 2, 3))
assert_nil @filters.escape(nil)
end
def test_h
assert_equal '&lt;strong&gt;', @filters.h('<strong>') assert_equal '&lt;strong&gt;', @filters.h('<strong>')
assert_equal '1', @filters.h(1)
assert_equal '2001-02-03', @filters.h(Date.new(2001, 2, 3))
assert_nil @filters.h(nil)
end end
def test_escape_once def test_escape_once
@@ -146,18 +138,14 @@ class StandardFiltersTest < Minitest::Test
def test_url_encode def test_url_encode
assert_equal 'foo%2B1%40example.com', @filters.url_encode('foo+1@example.com') assert_equal 'foo%2B1%40example.com', @filters.url_encode('foo+1@example.com')
assert_equal '1', @filters.url_encode(1) assert_equal nil, @filters.url_encode(nil)
assert_equal '2001-02-03', @filters.url_encode(Date.new(2001, 2, 3))
assert_nil @filters.url_encode(nil)
end end
def test_url_decode def test_url_decode
assert_equal 'foo bar', @filters.url_decode('foo+bar') assert_equal 'foo bar', @filters.url_decode('foo+bar')
assert_equal 'foo bar', @filters.url_decode('foo%20bar') assert_equal 'foo bar', @filters.url_decode('foo%20bar')
assert_equal 'foo+1@example.com', @filters.url_decode('foo%2B1%40example.com') assert_equal 'foo+1@example.com', @filters.url_decode('foo%2B1%40example.com')
assert_equal '1', @filters.url_decode(1) assert_equal nil, @filters.url_decode(nil)
assert_equal '2001-02-03', @filters.url_decode(Date.new(2001, 2, 3))
assert_nil @filters.url_decode(nil)
end end
def test_truncatewords def test_truncatewords
@@ -182,7 +170,6 @@ class StandardFiltersTest < Minitest::Test
def test_join def test_join
assert_equal '1 2 3 4', @filters.join([1, 2, 3, 4]) assert_equal '1 2 3 4', @filters.join([1, 2, 3, 4])
assert_equal '1 - 2 - 3 - 4', @filters.join([1, 2, 3, 4], ' - ') assert_equal '1 - 2 - 3 - 4', @filters.join([1, 2, 3, 4], ' - ')
assert_equal '1121314', @filters.join([1, 2, 3, 4], 1)
end end
def test_sort def test_sort
@@ -342,7 +329,7 @@ class StandardFiltersTest < Minitest::Test
assert_equal "#{Date.today.year}", @filters.date('today', '%Y') assert_equal "#{Date.today.year}", @filters.date('today', '%Y')
assert_equal "#{Date.today.year}", @filters.date('Today', '%Y') assert_equal "#{Date.today.year}", @filters.date('Today', '%Y')
assert_nil @filters.date(nil, "%B") assert_equal nil, @filters.date(nil, "%B")
assert_equal '', @filters.date('', "%B") assert_equal '', @filters.date('', "%B")
@@ -355,8 +342,8 @@ class StandardFiltersTest < Minitest::Test
def test_first_last def test_first_last
assert_equal 1, @filters.first([1, 2, 3]) assert_equal 1, @filters.first([1, 2, 3])
assert_equal 3, @filters.last([1, 2, 3]) assert_equal 3, @filters.last([1, 2, 3])
assert_nil @filters.first([]) assert_equal nil, @filters.first([])
assert_nil @filters.last([]) assert_equal nil, @filters.last([])
end end
def test_replace def test_replace

View File

@@ -48,10 +48,6 @@ HERE
def test_for_with_variable_range def test_for_with_variable_range
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', "foobar" => 3) assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', "foobar" => 3)
assert_template_result(' 1.0 2.0 3.0 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1..3.0))
assert_template_result(' 1.0 2.0 3.0 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1.0..3))
assert_template_result(' 1.0 2.0 3.0 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1.0..3.0))
assert_template_result(' 1.5 2.5 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1.5..3))
end end
def test_for_with_hash_value_range def test_for_with_hash_value_range

View File

@@ -137,7 +137,7 @@ class IncludeTagTest < Minitest::Test
Liquid::Template.file_system = infinite_file_system.new Liquid::Template.file_system = infinite_file_system.new
assert_raises(Liquid::StackLevelError) do assert_raises(Liquid::StackLevelError, SystemStackError) do
Template.parse("{% include 'loop' %}").render! Template.parse("{% include 'loop' %}").render!
end end
end end
@@ -235,11 +235,4 @@ class IncludeTagTest < Minitest::Test
assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' } assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' }
end end
def test_including_with_strict_variables
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn)
template.render(nil, strict_variables: true)
assert_equal [], template.errors
end
end # IncludeTagTest end # IncludeTagTest

View File

@@ -2,6 +2,7 @@
ENV["MT_NO_EXPECTATIONS"] = "1" ENV["MT_NO_EXPECTATIONS"] = "1"
require 'minitest/autorun' require 'minitest/autorun'
require 'spy/integration'
$LOAD_PATH.unshift(File.join(File.expand_path(__dir__), '..', 'lib')) $LOAD_PATH.unshift(File.join(File.expand_path(__dir__), '..', 'lib'))
require 'liquid.rb' require 'liquid.rb'

View File

@@ -57,6 +57,11 @@ class ConditionUnitTest < Minitest::Test
assert_evaluates_argument_error 1, '~~', 0 assert_evaluates_argument_error 1, '~~', 0
end end
def test_comparing_hash_and_integer
assert_evaluates_argument_error({a: 1}, '>', 1)
assert_evaluates_argument_error(1, '>', {a: 1})
end
def test_comparation_of_int_and_str def test_comparation_of_int_and_str
assert_evaluates_argument_error '1', '>', 0 assert_evaluates_argument_error '1', '>', 0
assert_evaluates_argument_error '1', '<', 0 assert_evaluates_argument_error '1', '<', 0
@@ -64,14 +69,6 @@ class ConditionUnitTest < Minitest::Test
assert_evaluates_argument_error '1', '<=', 0 assert_evaluates_argument_error '1', '<=', 0
end end
def test_hash_compare_backwards_compatibility
assert_nil Condition.new({}, '>', 2).evaluate
assert_nil Condition.new(2, '>', {}).evaluate
assert_equal false, Condition.new({}, '==', 2).evaluate
assert_equal true, Condition.new({ 'a' => 1 }, '==', { 'a' => 1 }).evaluate
assert_equal true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate
end
def test_contains_works_on_arrays def test_contains_works_on_arrays
@context = Liquid::Context.new @context = Liquid::Context.new
@context['array'] = [1, 2, 3, 4, 5] @context['array'] = [1, 2, 3, 4, 5]

View File

@@ -70,6 +70,10 @@ class ContextUnitTest < Minitest::Test
@context = Liquid::Context.new @context = Liquid::Context.new
end end
def teardown
Spy.teardown
end
def test_variables def test_variables
@context['string'] = 'string' @context['string'] = 'string'
assert_equal 'string', @context['string'] assert_equal 'string', @context['string']
@@ -94,12 +98,12 @@ class ContextUnitTest < Minitest::Test
assert_equal false, @context['bool'] assert_equal false, @context['bool']
@context['nil'] = nil @context['nil'] = nil
assert_nil @context['nil'] assert_equal nil, @context['nil']
assert_nil @context['nil'] assert_equal nil, @context['nil']
end end
def test_variables_not_existing def test_variables_not_existing
assert_nil @context['does_not_exist'] assert_equal nil, @context['does_not_exist']
end end
def test_scoping def test_scoping
@@ -181,7 +185,7 @@ class ContextUnitTest < Minitest::Test
@context['test'] = 'test' @context['test'] = 'test'
assert_equal 'test', @context['test'] assert_equal 'test', @context['test']
@context.pop @context.pop
assert_nil @context['test'] assert_equal nil, @context['test']
end end
def test_hierachical_data def test_hierachical_data
@@ -296,7 +300,7 @@ class ContextUnitTest < Minitest::Test
@context['hash'] = { 'first' => 'Hello' } @context['hash'] = { 'first' => 'Hello' }
assert_equal 1, @context['array.first'] assert_equal 1, @context['array.first']
assert_nil @context['array["first"]'] assert_equal nil, @context['array["first"]']
assert_equal 'Hello', @context['hash["first"]'] assert_equal 'Hello', @context['hash["first"]']
end end
@@ -446,10 +450,14 @@ class ContextUnitTest < Minitest::Test
assert_equal @context, @context['category'].context assert_equal @context, @context['category'].context
end end
def test_interrupt_avoids_object_allocations def test_use_empty_instead_of_any_in_interrupt_handling_to_avoid_lots_of_unnecessary_object_allocations
assert_no_object_allocations do mock_any = Spy.on_instance_method(Array, :any?)
mock_empty = Spy.on_instance_method(Array, :empty?)
@context.interrupt? @context.interrupt?
end
refute mock_any.has_been_called?
assert mock_empty.has_been_called?
end end
def test_context_initialization_with_a_proc_in_environment def test_context_initialization_with_a_proc_in_environment
@@ -472,18 +480,4 @@ class ContextUnitTest < Minitest::Test
context = Context.new context = Context.new
assert_equal 'hi', context.apply_global_filter('hi') assert_equal 'hi', context.apply_global_filter('hi')
end end
private
def assert_no_object_allocations
unless RUBY_ENGINE == 'ruby'
skip "stackprof needed to count object allocations"
end
require 'stackprof'
profile = StackProf.run(mode: :object) do
yield
end
assert_equal 0, profile[:samples]
end
end # ContextTest end # ContextTest

View File

@@ -145,20 +145,4 @@ class StrainerUnitTest < Minitest::Test
Strainer.global_filter(LateAddedFilter) Strainer.global_filter(LateAddedFilter)
assert_equal 'filtered', Strainer.create(nil).invoke('late_added_filter', 'input') assert_equal 'filtered', Strainer.create(nil).invoke('late_added_filter', 'input')
end end
def test_add_filter_does_not_include_already_included_module
mod = Module.new do
class << self
attr_accessor :include_count
def included(mod)
self.include_count += 1
end
end
self.include_count = 0
end
strainer = Context.new.strainer
strainer.class.add_filter(mod)
strainer.class.add_filter(mod)
assert_equal 1, mod.include_count
end
end # StrainerTest end # StrainerTest